Flutter Add-To-App in practice: Onboarding revamp at EyeEm.

Łukasz Wiśniewski
Flutter Community
Published in
10 min readApr 27, 2020
Photo by Zanetta Mungro on EyeEm

Last year we started using Flutter at EyeEm. With a single codebase we managed to develop and integrate a brand new feature into our existing apps. Now we’re taking the next step and putting Flutter upfront with the new onboarding experience.

The Status Quo & A New Concept

Over years our Android & iOS apps drifted apart. One of the areas where this became visible was our onboarding flow. You only get one chance to make a first impression and we weren’t that great at it.

Former iOS flow wouldn’t show the Facebook option upfront and would make you pick a unique username.
Former Android flow was breaking the user sign up form into 3 separate screens and was missing the newsletter opt-in.
Once registered at EyeEm, new photographers can opt-in to sell their photos on our market. Again, the experience would differ slightly between the platforms.

Now if you think about it, our onboarding is a pretty self-contained. As such it’s a perfect candidate for the Flutter Add-To-App feature. We decided to take this opportunity and push forward with a new unified flow.

Behold the new unified flow. It’s pretty much identical on both platforms except for the authorisation providers. You don’t get Google Login on iOS — you’ll get Apple Sign-In instead though. More explanation in further paragraphs.

Once the conceptual phase was over, this is what our new onboarding designs would look like — all we had to do is just convert these specs into shiny Flutter widgets, maybe throw an A/B test here or there. Easy, right? Well, not quite.

Tech Kick-Off

Last year, when we started with Flutter, we established our design system and decided on the app architecture. Our app is essentially a loosely coupled collection of pages routed by Voyager with states managed by Bloc all grouped into individual feature packages. On top of everything, we have an overarching application bloc to share things like default theme, account, system services or settings.

Architecture Overview

Having all the needed infrastructure already in place, we kicked off tech planning by creating a navigation map of the feature (one you can see below):

“The Eagle View” of the Onboarding feature

We took key screens from Figma designs and organized them into a navigation flow using a tool called Miro. Once this was done we assigned names and paths to every screen which then we would use for routing with Voyager. We continued our planning with adding sticky notes to relevant pages to map out all possible tech tasks.

Converting sticky notes into JIRA tickets — via JIRA Cards integration

Finally, it was time to transfer those sticky notes to a sad place called JIRA. Luckily for us Miro, has this integration which allowed us to embed JIRA tickets directly on top of our navigation map. This created an “eagle” view of the project — a visual progress tracking method offering way more insight than traditional Kanban/Scrum boards.

Now let’s take a look at some of the challenges we encountered during the implementation phase.

Authorisation States & Multiple Flutter Entry/Exit Points.

MKT (Market Keywording Tool), our first Flutter feature, would only run in already authenticated environment — host app would launch Flutter engine and pass authorisation token using the platform channels — easy.

This suddenly starts to look a bit more complex with Onboarding. You are facing a full blow authorisation on the Flutter side — once you’re done, you need to make sure the host app (which is still not Flutter) works properly as if it was coming from the old onboarding flow.

On top of that, MKT and Onboarding features exist in the same app and have the same main.dart entry point. How do you handle it?

AppPathStrategy allows us to define what should be the App’s initial path given the preexisting state

In short, we have an abstract class which decides what Flutter’s next screen should be. We distinguish the entry point using initalRoute*. If our entry point is a MKT path we use different strategy implementation from one we would use for Onboarding… and if we use the Onboarding feature in e.g. standalone runner app environment we can provide yet another path strategy for this.

Anyway, once our user is finished with the onboarding we need to do the handover and return to the host app. It turns out having these two platform methods was just enough to make it all work.

These 2 methods would handle handover between Flutter Onboarding and the rest of the app.

First method passes account object we produce on the Flutter side to the host platform as serialized json map. Second method asks the host to kill the Flutter instance and move to the next screen.

* We actually don’t use initalRoute directly but have a custom platform method that returns initalRoute value — Flutter’s initalRoute is making weird assumptions that mess up your navigation stack.

Authorisation Plugins: Facebook, Google & Apple

The first step in the onboarding migration was getting the e-mail sign in method working and then integrating it with the rest of the app. Tackling one big problem at the time.

Next, Facebook and Google buttons were on the list (…and Apple very very soon). Now the question is — if you have e.g. Facebook SDK already integrated in your app — does it prevent you from using Flutter Facebook plugin?

No, it should work just fine— e.g. your existing native Facebook code will continue working along Flutter one. You should, however, make sure that the SDK versions used by the plugin and your host app match.

So, does it just work?

Kinda…

Ugly Plugins

Don’t get me wrong here — Flutter plugins are a great concept in general, they take away the pain of implementing platform specific code. You usually get a concise API to consume on the Flutter side. This is a massive relief to those (including myself) who can’t keep up with all the system updates and API changes. AndroidX, New Location Permissions, Next XCode version… name your pain here.

It takes one person to do the integration effort with plugins. Once it’s out there, others can simply profit from it without deep diving into platform code. It’s the open-source at its finest. For instance, unofficial Facebook Login Plugin by Iiro Krankka is an excellent example of a good job. It has exhaustive documentation, works great and enjoys well deserved popularity.

Things, however, can get ugly when one of the plugins:

  • doesn’t quite work on one of the platforms you want to support
  • pulls massive dependency tree on the host side
  • requires you to add extra work on the platform side.
  • prevents you from using Flutter Web/Flutter Desktop
  • isn’t testable
  • hasn’t updated to add-to-app plugin API
  • has maintenance issues

The more plugins you add to your project, the more likely you are to run into one of the above issues.

For instance, adding Facebook plugin requires you to change Android manifest to include a Facebook ID of your app — otherwise the SDK crashes an App on start. It’s not a problem if you are writing only one app. However, if you have multiple feature packages with multiple runner apps (like our company) and you decide to make Facebook plugin a base dependency, you essentially make all of your runner apps crash.

Then there is Google Sign In Plugin by Google where you’d expect things to be rock solid. We most certainly thought we’d get Google integration for free on iOS —unfortunately the Cocoa Pods issues we’ve run into have successfully discouraged us from pursuing this goal. That’s not all of it. We were shocked to discover serverAuthCode wasn’t supported (PR opened in September 2019, still open at the time of writing this article — April 2020).

Pictured above: Delusion of self importance.

Good part, it’s all open-source so you can fix it yourself — I just copied the entire package folder from the official repo, removed any non Android code so that iOS would compile, applied changes from the aforementioned PR and finally added some of my own changes to get the serverAuthCode support.

The Power of Abstraction (and Mocks)

Anyway, it became apparent to us that plugins can be a liability blocking the development progress. Obvious question — is there anything we could do to mitigate it?

Stick to pure Dart/Flutter. Avoid plugins as long as you can.

In short, you can create an abstract class that will mirror the API of the plugin you want to use. You don’t need to mirror the entire API — just specify the parts that are going to be used, e.g. Facebook abstraction could be something like this:

an abstraction scoped to our needs

Now, when actually need to use Facebook, you provide a concrete implementation — in all other cases though, you can just use a mocked instance.

a mock in use — great success!

That’s some extra work added but there are immediate pay offs:

  • Your plugin dependency becomes programmatic and optional.
  • You can easily mock the plugin in widget tests.
  • You can use mocks in runner apps and avoid blowing your dependency tree.
  • You get flexibility in refining concrete implementation — e.g. you can use 2 different plugins for different platforms. If there is a better plugin you can switch to that without the need to refactor the rest of your code.

An important side note here — Flutter dependency system doesn’t appear to come with any “flavor” mechanism.

Flavors (or Variants) is a general technique that allows you to change dependencies during build time based on a target you are building for.

While this feature might be missing in Flutter — it’s quite easy though to write a script that will modify pubspec.yaml and put that in a CI pipeline:

Adding “flavors” to pubspec.yaml - ruby, 4 lines of code

Mitigating Heavy Assets Problem

If you haven’t noticed yet, our onboarding comes with some big hi-res background images — they are roughly 1.5MB large each in design assets.

Now, the app size has a direct impact on the app install rate. If you follow Google Play, in their article they state the following:

For every 6 MB increase to an APK’s size, we see a decrease in the install conversion rate of 1%.

Since our onboarding would ship with something like extra 3MB of assets, this would mean possible ~0.6–0.8% drop in installs. As a developer, you might suggest downloading the image from the internet. Remember though, this is our first screen and some network connections can be crappy. You might end up with a completely black background and your chance to make a first good impression is gone.

Did you know Flutter engine has built in WebP support by default? Well, now you do! With this knowledge we converted JPEG images to WEBP using ImageMagick CLI:

Play with compression parameters to get satisfying quality-to-size results.

This allowed us to shrink total assets size by around ~30%.

These were some major challenges we had to face during the onboarding milestone. Let’s now talk about the output our team has delivered.

Open Source Contributions

First off product is a library called SpanBuilder — it simplifies API around building text spans — we use it in the onboarding screen to highlight and link footer text.

https://pub.dev/packages/span_builder

The other tool we open sourced was a Dio Firebase Performance plugin for the excellent Dio package (HTTP client). Since we’re making more API calls from the Flutter side it’s worth knowing how do they perform.

https://pub.dev/packages/dio_firebase_performance

Release Summary

It was a successful release and we’ve got some numbers to back it up.

First, let’s talk about quality. The test code coverage for the feature is at 90% — we have auto generated test plans for all the screens listed in navigation map thanks to Voyager. Whenever we need to be super sure about implemented logic we add extra tests at Bloc level. Additionally, our mock environment makes things easy to test.

We A/B tested the new release against old iOS and Android versions. Here’s what we found out:

We didn’t make it worse.

Android sign up conversion rate didn’t change while on iOS it got much better. Also, the market sign-up numbers got better on both platforms.

The data also shows us iOS and Android users behave the same using the new flow — our design system brings a consistent look and feel across the product while staying true to the respective platforms.

Kudos to the amazing team that made this release possible — Adrienne, Anton, Dario, Ethan, Jędrzej & Vidu.

Thank you all for reading ❤️ If you want to stay connected, you can follow me on twitter/github or just reach out directly on fluttercommunity.slack. If you’re interested in EyeEm — wait no more and join 25M+ photographers today!

--

--