đ¤¸ââď¸ The Tricky Task of Keeping Flutter Running
Running Flutter apps on Android is more perilous than you might imagine
Not too long ago I published the first article of the Flutter series, explaining why our mobile team at Stuart had decided to go all-in with Flutter. And now, it is time to share our solution to our very first tricky challenge, which is running Dart code in Android for long periods of time.
The way Dart executes on Android is pretty straightforward: Dart code is executed so long as the FlutterActivity
it belongs to is running.
Unfortunately, if the FlutterActivity
is destroyed for some reason, the Dart code is no longer executed. On Android, there are two primary situations where an activity can be destroyed:
1. When you press the back button;
2. When you switch to another app, and the operating system requires more memory.
As a result, if you were partway through an activity (maybe downloading a picture, partially scrolled halfway down a certain screen, etc) then when you open the app again everything is gone because the activity has been destroyed.
When the FlutterActivity
is gone, Dart is gone too â°ď¸
A workaround for the back button
So, if pressing back button destroys the activityâŚdo not let users press the back button.
Weâre not going to cover the button with duct tape, though. Instead, weâll be doing the following:
- Adding a
WillPopScope
widget wrapping theScaffold
; - Implementing
onWillPop
method. If there are no more widgets to pop, we call native code, where the magic happens.
Now on the native code:
moveTaskToBack is going to be super helpful here. This will make the app run in the background but without destroying the Activity.
How do we prevent the system from killing the activity?
This is way trickier!
As you might know, if you need code to be executed for very long periods of time, and you cannot accept that the system kills it, all you have to do is create an Android Service. If this service is a STICKY_SERVICE, even better.
We can start the Android Service via Dart code, by making a call to native through the MethodChannel
, as we did before.
Now, weâll have a service running that prevents the system from killing the app completely, and we have the activity (hence the Dart code) available.
All good, right?
All that glitters is not gold
Thereâs a chance the service is killed (or crashes! đĽ). This could happen because of a whole host of different reasons.
Since we started the service as sticky, Android will helpfully restart the service after a crash. Starting the service, however, doesnât start the FlutterActivity
. So now weâll have a service running, but no Dart code being executed.
Remember, no FlutterActivity
, no party đđ
So, do we need a FlutterActivity
to execute Dart code? Well, actually we donât. All we need is a FlutterNativeView
. We can have a FlutterNativeView
in the service using the following snippet:
đĽ Dart is running on a service. Weâre done!
Well, not reallyâŚ
Situation Recap: We had an app running, which started a sticky service. Weâre now doing something else on the phone. Android decides to kill our app, and the service for some reason stops. Android relaunches the service and it instantiates a FlutterNativeView
that runs the Dart code.
So now, the user presses the app icon in order to get back to the app and the app starts.
As the app starts, the FlutterActivity
starts. Hence a new FlutterNativeView
starts. But wait⌠another one? How about the one running on the service?
Well⌠Now there are two different instances of FlutterNativeView
running. And, to make matters worse, both are running on different Isolates.
Two isolates mean two different memory spaces with no shared memory between them. So if the serviceâs FlutterNativeView
fetches some information and stores it in memory, the activityâs FlutterNativeView
cannot see it.
And the same vice versa. So what now?
Communicating between Isolates
Isolates communicate by using ports.
With our Repository Pattern, plugging in ports is not that complicated, but it adds some overhead and complexity weâd prefer to avoid. But there seems to be no workaroundâŚ
There is a workaround for us!
We could describe our app as an internal tool for those couriers using our platform to deliver packages around the city. So, because the app can be considered as an internal tool, we found a workaround that seems to work pretty well.
- We got rid of the
FlutterNativeView
created by the service. We didnât want to deal with Isolates communication; - We added code in the service that monitors if the
FlutterActivity
is running or not. In case itâs not running, the activity is started by it; - As the activity is started, it is presented to the user as the foreground app. Thereâs a high chance the user wonât understand whatâs going on here as it will interrupt them in another app, maybe messaging on WhatsApp or browsing a website. So, if we need to relaunch the activity, we decided to show a dialog explaining why. Hopefully, this wonât bug our users too much đ¤
Conclusions
I have the feeling that we bypassed one of the biggest potential issues we could face when developing our app in Dart with Flutter.
The next issue weâll probably face is app state management.
As I mentioned in my first article, thereâs no app state management in Flutter. If the app is destroyed, when you open it again it will show the main screen instead of the last screen the user was on.
Leave it with us! Weâll figure out how to deal with it, and weâll get back to youâŚ
Like what you see? Weâre hiring! đ Check out our open engineering positions.