As you probably know, we have an app in Flutter that has been successfully running in production for more than 2 years now.
But we also have another app, a Kiosk application written natively in Kotlin and available for Android only. It was available, actually, as we decided to migrate it to Flutter as well. And here’s a report on why and how.
What’s the application about?
This is a Kiosk app for self-check-in and checkout in hotels. One of the core ideas behind it is simple maintenance: instead of buying a standalone (and very expensive) Kiosk and integrating it with your system, you just grab an Android tablet, scan the QR code from the PMS, and voilà: you have it ready. By the way, this is what's called BYOD (Bring Your Own Device) in the mobile enterprise world. And it's very flexible: you can choose what key cutter you need or whether you want to integrate it with the payment terminal.
So, initially, the app was only available for Android. The reason behind it was pretty simple: since the app is always in full-screen mode, and the tablet itself is most probably in a special bounce pad, who cares if this is an iPad or some cheap Android tablet?
Some properties do care, as it turned out. For some, it's a matter of brand, and they prefer having iPads. Others already have a fleet of iPad devices and want to use them for Kiosk. Anyway, the feature request was sitting for some time, but it still wasn’t popular enough to develop and maintain a separate iOS version.
And here comes Flutter:
- we already have another app in Flutter, and it proved to be useful for us;
- we have our own design system with Flutter implementation – we can reuse it instead of maintaining a native Android version;
- we can unify architecture and share some code;
- we can maintain a single codebase for Android, iOS and maybe even Web/Desktop.
There were 2 ways to migrate the app: either we could do it gradually and add Flutter to the existing app, or we could just rewrite it from scratch. Both options have pros and cons.
Gradual rewriting can be done in parallel with maintaining the current app, but there's a significant overhead, and some functionality will be inevitably duplicated. On the other hand, rewriting is more straightforward, and we don't need to plug into existing architecture, but it's all or nothing – if the app is 90% ready, it's not ready at all.
Eventually, we decided on the second route and rewrote the app from scratch. A dream for a developer and a nightmare for a tech lead. Being in both roles, I'm not sure whether I was more excited or scared 😁
Significant points (yeah, they’re expected, but it hurts anyway):
- the deadline was pretty optimistic (it always is);
- since it's a different language, you cannot just use the same tests, so there were regression bugs;
- be ready for "what the hell is this doing here?" reactions.
The first and foremost step of the migration process was to make it work in the same (or better) way on Android tablets. We were concentrated on that, so we didn't even test the app on iPads. After the Android version was released, we could put more effort into the iOS part, and of course, there were some problems specific to the iOS version (although, I was afraid there would be more of them).
And, naturally, most of the problems were at the juncture between the native platform and Flutter: webviews and camera.
We use webview for displaying things like Terms & Conditions or House Rules. They come from the backend in HTML, and we apply some CSS to them. At first, we used webbview_flutter as it’s maintained by the official flutter.dev publisher and it worked well for Android. But on iOS, it was just giving us a white screen, and we didn't find a simple way to fix this. So we decided to switch to flutter_inappwebview, and so far, it works well for both Android and iOS.
The camera is a common source of pain in native development, especially in Android, where the possible hardware and software range is too broad. camera package does a great job of abstracting over all these discrepancies and the somewhat cumbersome native Camera API, but there are some problems. For example, we ran into this issue and had to downgrade to version
0.8.1+7 for now.
Apart from achieving the initial goals (such as unifying architecture and sharing code or going cross-platform), there are some more minor improvements that we acquired after migration:
The kiosk codebase is now 2x smaller. Of course, it's not just about Flutter being so tremendous and concise (although it is). We could achieve some code reduction even after rewriting to the same language and framework, as it was basically a major refactoring. But it's still pretty cool to know that we have more platforms to support and less code to maintain.
Custom keyboard implementation is much easier. We use a custom keyboard in the app rather than an external system keyboard: for granular control, security and a consistent look and feel. I remember we spent a lot of time explaining to native Android that the input field (despite being enabled and writable) doesn't need to have the keyboard. In Flutter, it's just a matter of 2 parameters (readOnly: true, showCursor: true) and providing a TextEditingController with custom functionality. Take a look at this excellent article for inspiration if you need to integrate a custom keyboard into your app.
A declarative and functional approach to UI. Without exaggeration, I think this is the best trend in front-end and mobile development of the last few years:
Yes, now we have Jetpack Compose, so native Android development goes in the same direction. But back then, for our pretty complex registration card screen (that has a lot of dynamic form fields based on the nationality of the guest, the legal environment of a hotel, and a ton of other stuff), we ended up with a scary piece of code that no one wanted to touch. It's still pretty complex in Flutter, but it is, at least, maintainable.
There's one part that we definitely cannot make cross-platform: installation and management. First, it's low-level. Second, it's totally different for iOS and Android.
Currently, for Android, we have our own solution – we've developed a separate DPC-app that you can install as part of the enrolment process using the QR-code provisioning method – it automatically gets device owner permissions, so it can install and update other applications, and grant permissions to them. Furthermore, it means that we don't even need Google Play for the deployment, as we can simply upload a new app version to cloud storage (Azure Storage in our case), and the DPC-app periodically checks, downloads and installs a new version automatically.
- we fully control the deployment process;
- we can do the "real" CI/CD without waiting for Google Play approval;
- we can do some intelligent deployment. This "smart deployment" means that we don't just force an update while a user is in the middle of the session, nor do we wait for the app to be closed (which doesn't make sense for a single-app kiosk mode). Instead, our Kiosk app and DPC-app communicate with each other. DPC-app notifies Kiosk if the new version is available, and Kiosk notifies DPC-app when it's ready to be updated (e.g. the current session has ended).
- we need to maintain the code for the DPC app;
- we lack some features like remote rebooting and don't have resources to implement them;
- as I mentioned, for iOS, we need to implement all this functionality from scratch.
That's why, while launching iOS beta, we decided to try some 3rd-party MDM solutions. If everything goes smoothly, we're thinking about migrating Android devices to the MDM as well, so it's both beta for the iOS app and for a distribution model in general.
So, expect another article on integrating an MDM solution sometime in the future :)
Bonus: useful libraries
As a bonus, here are some valuable libraries we've discovered (or written) for this app.
- wakelock allows preventing the screen from sleeping on Android, iOS, macOS, Windows, and web.
- hand_signature will enable you to draw smooth signatures with a variety of draw and export settings – and also supports SVG.
- native_device_orientation – reading device's native orientation, either from UI orientation or from sensors.
- kiosk_mode – this is our library for working with Lock Task / Guided Access modes on Android and iOS.