How We Revamped the Pharst Care Mobile App

How We Revamped the Pharst Care Mobile App

Lessons Learned and Our New Approach with Flutter

(Updated Apr 07, 2024)

At Pharst Care, we've always had a unique way of doing things – "the way that works for us," as Theo likes to say. This philosophy extends to our mobile app strategy. In early 2020, we launched a progressive web app (PWA) for Android users. PWAs were ideal for our target audience – faster loading times and lower data usage meant smoother access to our groundbreaking services, even for those with limited resources.

However, PWAs came with limitations. While convenient, they lacked some functionalities that could enhance the user experience. Installing a PWA on the home screen wasn't always straightforward, and some user browsers simply didn't support PWA installations. This led to user frustration and ultimately, many resorted to just using the website without installing the app. Consequently, features like push notifications for orders and other crucial information were unavailable.

The Android Solution: Custom Tabs Open the Door

To address these limitations on Android, we explored custom tabs. This innovative solution allowed us to create a lightweight app shell for our PWA. This shell felt like a native app but retained the core functionality of the PWA, we were able to keep the app size under 1MB – a significant advantage for users with limited data plans or low-end devices, perfectly aligning with our target audience. Importantly, custom tabs granted access to essential native features like location services, camera functionality, and notification support – features crucial for a seamless user experience. With custom tabs in place, we could finally ship our PWA to the Google Play Store, making it easily discoverable for Android users.

The iOS Challenge: Safari View Controller and Beyond

For iOS users, things were even trickier. We explored using Safari View Controller (SFSafariViewController), a solution that offered some semblance of a native app experience within the Safari browser. However, Apple's App Store guidelines strictly prohibit hiding an SFSafariViewController behind a native app shell, and frankly, such a solution wouldn't have been ideal for user experience either. Building a fully native app for iOS became the clear path forward.

Swift UI's Appeal: A Short-Lived Dream

Initially, we considered building the iOS app with Swift UI, Apple's new framework for building native iOS interfaces. Swift UI had just been released a few weeks before we shipped our Android app, and its promise of a declarative and user-friendly development experience was enticing. However, after further evaluation, we decided against it. At the time, Swift UI was still very new, and its long-term stability and feature set were uncertain. Additionally, building separate codebases for Android (using custom tabs) and iOS (using Swift UI) would have introduced additional complexity for future maintenance and updates.

Choosing Flutter: A Responsive Framework for All Platforms

Maintaining a consistent experience across platforms became a priority. Since building a native app for iOS meant doing the same for Android, we needed a unified solution. Having experimented with both Flutter and React Native during my senior year, the choice became evident. Back then (in 2018), React Native had already been around for a few years (released in 2015), while Flutter was still very new (version 0.1.5). Despite its novelty, Flutter offered a significantly more responsive development experience compared to React Native, especially on my low-end computer and the abandoned low-end phone I borrowed from my uncle – limitations that mirrored the reality of many of our target users.

Early Flutter Days: Workarounds and Valuable Lessons

The early days with Flutter weren't without challenges. The framework was new, and some workarounds were necessary to achieve our goals – workarounds that later updates rendered obsolete. For instance, to get some functionalities working, we had to edit framework files outside the app code itself. While not ideal, this provided valuable insights into Flutter's inner workings and helped us adapt to the evolving framework.

Building a Sustainable Future: Codebase Rewrite and Improved Structure

Fast forward to 2022, and the app's codebase, burdened by years of continuous innovation, started showing signs of strain. Stability issues emerged, culminating in the dreaded "gray screen" crashes. These crashes were frustrating for users and a major roadblock for further development.

Combating the Gray Screen: Code Clean-up and Null Safety

We tackled the gray screen crashes head-on with a comprehensive code clean-up effort. This involved refactoring redundant code, removing unnecessary dependencies, and generally streamlining the codebase. Additionally, the introduction of null safety features in Flutter provided a significant boost to the app's stability after we migrated Pharst Care to be null safe. Null safety helps prevent crashes caused by accessing null values, a common culprit in app crashes.

The Decision to Rewrite: Building for the Future

By August 2023, while the code clean-up improved stability, the complex codebase, accumulated over years, posed challenges for implementing new features efficiently. To ensure long-term maintainability and efficient feature integration, we embarked on a full codebase rewrite in August 2023 through to February 2024. This rewrite incorporated key learnings from our journey. It wasn't just about fixing the old code; it was about building a foundation for future growth. We leveraged Riverpod for state management, and of course opting to avoid code generation for greater control over the codebase since we were not big fans of it or anything that had to do with build runner. We wrote all data models manually. Additionally, we implemented the new file structure inspired by the MVC paradigm, the gskinner team, and the NextJS framework, promoting better organization and maintainability for future development.

Under the Hood: Our New File Structure

For our rewritten codebase. This structure, categorizes code into distinct folders:

  • Model: This folder houses all data models representing the application's data entities (e.g., User, Order).
  • View: This folder contains the UI components responsible for displaying information and user interaction (e.g., LoginScreen, OrderList).
  • Command + Services: This combined folder groups functionalities related to application logic and data access, including business logic and communication with servers or APIs (e.g., AuthService, OrderService).
  • Local and Shared Components: This folder stores reusable components used across different parts of the app, promoting code reuse and consistency. Here's a breakdown of the local vs. shared components:
    • Local Components: These components are specific to a particular view or screen within the app. They reside in a subfolder within the corresponding view folder (e.g., views/login_screen/LocalComponents). This promotes tight coupling between the view and its specific UI elements.
    • Shared Components: These components are more generic and can be used across multiple views in the app. They reside in a dedicated shared_components folder at the root of the project. This promotes code reuse and a consistent user interface across the app.

The Road Ahead

The revamped Pharst Care app, built with a focus on long-term maintainability and a user-centric approach, is currently undergoing final testing and should hit the stores soon. The improved code structure and the lessons learned during the development process will empower us to continuously improve the app and deliver a seamless experience for our users on their health journeys. Thanks for reading 🫡.

I'm Aikins and — I love to build stuff.