Introduction
Hi, my name is Cherry. I’m so excited to be part of this blog series!
Let me introduce myself first. I joined Mercari in October this year and am now a Flutter engineer on the Mercari Hallo mobile team. Before this, I was a native Android app developer and spent about two years migrating a native app to Flutter using the add-to-app embedded app approach. Since the Mercari Hallo app is a standalone Flutter app, I’ve faced significant challenges in transitioning to the different development focus, and the new project architecture that comes with it.
In this blog, I will demonstrate the challenge and highlight a few points about how Mercari Hallo’s onboarding process and documentation helped me overcome. I hope this blog will offer insights for readers considering starting Flutter or migrating native apps to Flutter.
Challenge: The New Development Focus
During onboarding, I noticed two major differences compared to my previous experience.
- Simpler project architecture
- Deeper focus on Flutter-specific development
Simpler Project Architecture
Overall Architecture
In embedded development, we can either fully replace a page or replace only a specific part of a page with Flutter. However, the latter requires precise control over the native page’s lifecycle via bridges, making it unsuitable as a solution for large-scale apps. Therefore, I will focus on the first approach.
An embedded Flutter project is created as a module, with the host iOS and Android apps that reference this module as a dependency. This setup requires separate maintenance for the Flutter module and the host projects. While a standalone Flutter project eliminates the need for separate management.
Business Logic Complexity
Routing
In an embedded app, handling mixed stacks of Flutter and native pages is an unavoidable challenge. I have encountered two solutions:
- Routing managed by the native side
- Routing managed by both native and Flutter
This method requires stack information synced via bridges. For typical apps that use a navigation bar the sync could be complex, as each navigation item usually holds an independent stack.
On the other hand, a standalone Flutter app rarely needs to deal with this complexity. Mercari Hallo app uses go_router to manage page routing. It also leverages StatefulShellRoute
to build StatefulShellBranch
, enabling easy management of the stack for each tab in the bottom navigation bar.
This is a sample routing structure of Mercari Hallo:
routing root
|__ homeRoute: StatefulShellRoute
|// branches corresponding to bottom navigation
|__ timelineBranch: StatefulShellBranch
|__ favoriteBranch: StatefulShellBranch
|__ offerRoute: GoRoute
// switch tabs by statefulNavigationShell.goBranch()
Bridges
In an embedded app, a large amount of bridging is often required to handle data sharing between multiple page engines. But a standalone Flutter app only needs to define custom bridges in a few cases, such as handling deep links or interacting with native interfaces for creating files in the file system.
Deeper Focus on Flutter-Specific Development
In my previous experience working on both Android and Flutter, the focus was on tackling the hybrid architecture. But development for the Mercari Hallo app pays major attention to Flutter and Dart
Encouraging to Use the Best Practices of Dart
Here’s an example I encountered during a code review. In Kotlin, the common approach is to construct a list using MutableList<T>()
then update elements and apply transforming or filtering through methods like map
, filter
, and finally use toList()
to gather the results into a new list. This became a habitual way of writing code for me:
return myList
.map((element) => element.copyWith(property: newValue))
.toList();
return myList
.whereType<MyFilteredListType>()
.toList();
However, Dart provides a collection literal <ListType>[]
syntax and allows using the spread operator (…) to insert other lists directly into the content of the list. It also supports embedded for loops or if-else control flows. As a result, I refactored the code to follow Dart’s preferred style:
return <MyListType>[
for (final element in myList)
element.copyWith(
property: newValue,
),
];
return <MyFilteredListType>[
...myList.whereType<MyFilteredListType>(),
];
Emphasizing UI Testing
After joining Mercari Hallo, more attention was placed on UI (widgets). This is because the code structure minimizes the focus on the data and business logic layers, which I’ll discuss in the next section. With the focus shift, unit testing for widgets became essential. Actually, most of the tests are concentrated on widget tests.
Widget Testing
When business logic is tied to UI states, it needs to be covered within widget tests. The WidgetTester has functions such as tap and drag, which are used to simulate user interactions and trigger different UI states. The displayed data is then used to verify the underlying logic.
Golden Testing
Mercari Hallo app uses golden test to check UI intuitively. The flutter test --update-goldens --tags=golden
command generates golden images, and the matchesGoldenFile function checks for differences. These images cover both light and dark modes, as well as large and small screen sizes.
Adopting React-Like Architecture
When doing native Android development, I used the model–view–viewmodel (MVVM) architecture, keeping View, ViewModel, Repository, and Data layers separate. Among Flutter’s state management solutions, BLoC is probably closest to MVVM, as it updates the state through events from the UI and populates backend data to UI as well. This is similar to the ViewModel’s two-way binding.
However, Mercari Hallo adopts a React-like architecture with flutter_hooks:
- Components compose a page
- Hooks manage state, with heavy use of custom hooks
A typical page architecture in Mercari Hallo might look like this:./lib/src/screens/ |__ hoge_screen/ |__ components/ --> The UI components for the page |__ hoge_header.dart |__ hoge_content.dart |__ hoge_footer.dart |__ hoge_error.dart |__ hooks/ --> The custom hook |__ use_hoge_screen_state |__ gen/
This structure organizes logic around pages and components, rather than separating it into distinct layers. It also ensures a unidirectional flow of state passing down the Widget tree.
Work through
As for how I’ve been working through the above challenge, the following factors greatly helped.
During Onboarding
Comprehensive README Documentation
- Clearly lists every possible step, avoiding omissions, including directory movements, environment variable setup, etc.
- Provides separate steps for different shell environments. (e.g. bash, zsh)
- Highlights any project-specific, recommended, or non-standard practices compared to official documentation.
- Maintains a troubleshooting section.
- Encourages team members to update the documentation actively.
Monorepo Flexibility
With the monorepo, engineers can freely set up the environments for other ends based on the documentation, significantly reducing the cost of understanding the entire project.
During Development
- Actively Adding Custom Linter Rules:
We not only adopt many Dart linter rules but also have ahallo_linter
package for custom linter rules to enforce specific guidelines in certain scenarios.
These extra rules help enforce the use of standardized Dart code across the team. - Actively improving CI/CD processes
- Emphasizes best practices in code reviews
Conclusion
Shifting from embedded Flutter development to a standalone project like Mercari Hallo was both challenging and rewarding. It required adapting to new architectures and focusing more on Flutter-specific features.
This experience helped me grow technically and showed the value of good documentation, monorepo flexibility, and clear coding standards. I hope my journey offers helpful insights to others exploring Flutter or migrating native apps. Thanks for reading!
We hope this article has been helpful to your projects and technical explorations. We will continue to share our technical insights and experiences through this series, so stay tuned.
Also, be sure to check out the other articles in the Mercari Advent Calendar 2024.
We look forward to seeing you in the next article!