About two years ago we decided to rewrite our native mobile apps using Flutter, then a year later we decided to use Flutter Web to convert our mobile app into our new web app. You can see the results of our efforts at invoiceninja.com/demo. This is an early pre-release demo but we’re really happy with the results so far.
While working on the app we’ve run into a few different challenges specific to Flutter Web, I thought it may be helpful to compile some of them into a post.
Checking if on Web
To start, to check if the app is running on the web you can use the kIsWeb constant.
It’s interesting to note how it’s implemented. The underlying differences can mean the runtimeType value of a variable may be different on web and mobile between int and double.
In the app we have two distinct navigation models: when the app is used on a mobile device the user can navigate between routes however in tablet/desktop layout a single ‘main’ route is used.
By default the app’s routes are integrated with the browser, when a user clicks the back button the current route is popped from the stack. This works on mobile but not tablet/desktop. To resolve this the app tracks it’s own history and uses a WillPopScope widget to ensure the right screen is shown after back is pressed.
Related to this you can use RendererBinding.instance.mouseTracker.mouseIsConnected to check if the user has a mouse.
An important difference between the mobile and web/desktop versions of the app is that users are likely to have a keyboard and mouse. Out of the the box when using the Tab key to move the focus the next element to the right will be selected.
Our app has three main columns so it’s important the focus is shifted down first and then to the right. We’re able to change the behavior by wrapping the UI in a DefaultFocusTraversal widget setting the policy parameter to WidgetOrderFocusTraversalPolicy().
NOTE: DefaultFocusTraversal has been replaced by FocusTraversalGroup #545
For some web features it can be useful to import the dart:html package, the problem is once the package is imported the mobile version of the app will fail to build with the message: Error: Not found: ‘dart:html’. This can be solved by using conditional imports.
We keep all web related functions in a file called utils/web.dart and then have another file called utils/web_stub.dart which has the same functions without the implementations. In a file where the web functionality is used we add the following import:
import 'package:invoiceninja/utils/web_stub.dart' if (dart.library.html) 'package:invoiceninja/utils/web.dart';
The web.dart file linked above has basic solutions for uploading/creating files as well as managing cookies.
Although we’ve made progress in most areas one feature we’re blocked on is enabling the browser to remember the username when a user logs in. We’re able to use local storage to persist the user’s data however we haven’t been able to get the browser’s password remember feature to catch the username along with the password when the user first logs in. If you have any ideas how to make this work please comment below.
Update: Here’s a possible solution…
Thanks for checking out the post, hope you found it useful! The source code for the Flutter Web app is available on the develop branch at github.com/invoiceninja/flutter-client. If you have any questions I’m happy to help, I’m reachable on Twitter at @hillelcoren.