Flutter @ amaysim - Challenges and Learnings

We recently released a new self-service web app for amaysim customers to self-manage their mobile services, and account. If you’re an amaysim customer, and have used our iOS or Android app, it might look familiar. Leveraging Flutter, we are now providing the same self-service experience across 3 platforms; iOS, Android and web, which bring many benefits, but also some challenges.

In this second of two posts, we’ll talk through some of those challenges and learnings adopting Flutter, and what you might want to be aware of, if you’re planning to go the same route.

Flutter for Native Apps

Foundation

The first challenge we had when adopting Flutter was that it was so new to the team. All but one developer had experience with it. It was important to have an engineering team that embrace new technologies and are up for a challenge. Our team got involved early with spikes and experimentation, to evaluate Flutters’ fitness for purpose. We also established some foundational elements, to support them through adoption.

Typically when starting a new project, you might build a skeleton app; no features, but something that can be deployed to a production-like environment in an automated way, to establish a working codebase. With our new Flutter app we went a bit further, to help lower the learning curve for developers.

We established some patterns for developers to follow with a foundation app. We implemented our design system using Flutter widgets, that allowed UI to be composed from existing elements. Since we were building like-for-like, we knew (mostly) what UI we would need and therefore built out the UI library upfront, which developers new to Flutter could leverage, to get started quicker.

Our app architecture made use of custom dart packages. We built packages that wrapped HTTP clients, implemented JSON parsers, and consumed amaysim APIs. These packages allowed developers to focus on UI development of features.

Together with an automated safety net in the form of our continuous integration setup, these foundations helped to guide and onboard developers, who were able to build a complete cross-platform replacement of the amaysim native apps, within a few months.

Evolving toolchain

The next challenge was working with Flutter as an evolving toolchain.

When we first adopted Flutter in 2020 it was fairly mature, but not as well established as it is today. Both Flutter and Dart have undergone some significant changes since we built our app. For example, when Dart added null safety support, our codebase was over ~180k lines of code, and required significant refactoring. 

Updates to Flutter itself often involve dealing with deprecated APIs, upgrading several dependencies. Flutter has a healthy release cadence, and the Flutter team are great at providing heads up on upcoming deprecations, but it’s still a challenge to keep up to date. 

We did a couple of things to mitigate this. First, we rely less on 3rd party plugins. We have native iOS/Android skillset in the team, and we leverage that skillset to get deeper insight into the quality of plugins that we adopt. Sometimes, we implement our own plugins instead. Secondly, we don’t rush to adopt the latest Flutter SDK releases. We’ve noticed that most “stable” versions require a number of hot fixes post release, so we allow time for those to be resolved before upgrading.

Flutter Web

Lack of Hot Reload

As a developer, one of the first things you'll notice when building a Flutter app for web, is the lack of hot reload. 

Hot reload is one of the key features of the Flutter toolchain. It enables immediate feedback when developing a UI, where code changes are reflected in a running app. This feedback loop is one of the main features that make Flutter development so rapid.

Unfortunately, Flutter web only supports hot restart, so code changes are reflected on the browser, but the app state is not preserved. 

This feels like a step back in productivity, but in practice we found that web specific changes were the exception not the norm. Most development would continue with an iOS simulator which has full hot reload support, and the fast development cycle we were used to. Then a quick check on the browser to confirm things work as expected.

Browser Navigation

Flutter provides two navigation APIs. An imperative API, where navigation is done on demand, and a declarative API. We've been using the imperative API on our native apps without issue, but web browsers bring more navigational concepts that now needed to be considered - the address bar, browser back/forward buttons, bookmarking and URL schemes. 

The declarative API (Navigator 2.0) provides this support out of the box. But migrating a large Flutter app to this new scheme is a big task.

In the interim, we were able to extend our existing approach to support deep links and browser navigation. We have more work to do to support the address bar and bookmarking, which might mean moving to the declarative API.

DOM Inspection

We found that most 3rd party tools and Javascript libraries could be integrated with our Flutter app without issue. However, tools that depend on heavy DOM manipulation or inspection, would not work.

Initially we thought this was due to the rendering approach we'd taken. Flutter supports both HTML rendering, and Canvas rendering. Canvas incurs a slightly longer load time, but provides pixel perfect rendering of the UI and is more performant. We switched back to HTML rendering, but found the markup Flutter generates was not sufficient to support these tools either.

Multi-platform

Delivery mindset

Working with a multi-platform toolchain requires a shift in thinking for delivery teams. When a single code change now effects iOS, Android and web, we don’t need multiple delivery teams, and in fact, the challenge now is how can we enable multiple streams of work, without developers stepping on each others changes, given the single code base.

Application architecture plays a big role here. We use a mono-repo, for our Flutter apps with well defined modules in the form of a design system for UI, and packages for services like API consumers, parsers, etc. Each module is decoupled from the main application code, which makes it easier for developers to work independently.

Another approach that’s worked well for us, is to spread this additional capacity to other codebases. Taking more ownership of APIs our Flutter app consumes, gives developers space and also encourages better design decisions, where less logic is embedded in the client app, and a richer backend for frontend (BFF) evolves. 

For stakeholders, we reinforce the effect of a single change with showcases running web and app side by side. This reminds the whole team to consider all platforms when designing features or requesting changes.

Developer velocity

With developers now capable of building features across 3 platforms, theres an additional ask on supporting roles.

There's an increase demand on stories to be ready for development, from our business analysts and designers. Designers also need to ensure that designs are responsive and adaptive to each platform.

Our QA now have 3 platforms to test. We would rely even more heavily on automation, with Flutters golden tests providing confidence in preventing UI regressions. At amaysim, we consider QA a shared role, where our QAs support developers and others in testing. This model has helped share the load, and alleviate the additional pressure.

Releases

Native apps have a staggered release process, where a manual review is required by Apple or Google, before release. Regression testing a native app also incurs more manual testing, on real devices, which doesn’t fit well with a continuous delivery model. Extra care is taken with native app releases, as once it’s released, there is no guarantee users will upgrade.

In contrast, web supports a continuous delivery model, in that there are no mandatory manual gates, like app reviews. Changes are also applied to all users when they next visit the app, there are no legacy versions.

With these constraints, keeping web and app releases in sync can be difficult. Initially, we held back web releases to align with native apps, and maintain full feature parity. Later, we decided to trade off some feature parity, and release more frequently on web. This allows us to gather feedback on web app features, before committing them to a native app release. 

Wrap-up

Overall our experience with Flutter has been a positive one, but as with any technology there are challenges, and tradeoffs need to be made. 

For us, the past 4+ years of using Flutter at amaysim has shown that those tradeoffs are worth it; resulting in high quality apps, more control over feature parity, complete design consistency, getting features out to customers faster, on their preferred platform, with significantly less development cost.


If you like the sound of this and are interested in being part of our apps engineering team, check our our Careers @ amaysim and our LinkedIn.

The views expressed on this blog post are mine alone and do not necessarily reflect the views of my employer, Optus Administration Pty Ltd.

Previous
Previous

Android Dev Tips: Kotlin get

Next
Next

Unleashing the Power of Generative AI: Building Practical Applications