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.
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
WillPopScopewidget wrapping the
onWillPopmethod. 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.
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
FlutterNativeViewcreated by the service. We didn’t want to deal with Isolates communication;
- We added code in the service that monitors if the
FlutterActivityis 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 🤞
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.