Flutter: enhance development and maintenance of mobile applications
I recently discovered that you can make shortcuts in Flutter. First, I thought this feature will be used for desktop and web Flutter applications.
Is It really the case ? Let’s see four usages that will enhance your productivity and the reliability of your (mobile) applications :
- Enable / Disable debug banner for taking screenshots
- Enable / Disable the performance overlay and the material grid, to check widgets implementations
- Switch between Dark and Light mode to ensure that UI design is well implemented
- Send a big amount of debug data (however you want), feature mainly for the testing team
All of these feature without compromising the UI Design and your application architecture!
Shortcut features are limited by your own imagination !
Introduction
Shortcuts are accessible anytime, but you may want to make them enabled only for debug builds. Also, you will have to connect a keyboard to your physical device or to your simulator/emulator.
On Android emulators and simulators, it’s a little bit more complex. Android Emulators coming from SDK don’t recognize special keys such as ‘shift’, ‘alt’, ‘ctrl’, ‘F12’,… at least on MAC OS. I only get results on common keys such as letters and digits.
Edit : you can also use adb commands to send key events to your emulator or your device. In my case, I tested on my device, sending F12 keycode with this command (in a terminal): adb shell input keyboard keyevent KEYCODE_F12.
A complete list of keyevent can be found here: https://developer.android.com/reference/android/view/KeyEvent
I got satisfying results using other android simulators, such as Genymotion (in my case). Precautions have to be made as some of their simulators doesn’t work with Flutter. Therefore I recommend you to test shortcuts using Google Pixel 2 API 28, after having defined, in Genymotion preferences, the Android SDK path used by Android studio.
On both Android and iOS devices, you can connect a USB and Bluetooth keyboard. Maybe with a dongle for iOS, if you don’t have the Magic Keyboard for iPad…
You can directly add shortcuts into your MaterialApp widget instance, through a combination of Shortcuts, Actions and Focus widgets, and by using FocusableActionDetector widget (that groups previous listed widgets together).
In this usecases, we will focus only on MaterialApp usage.
Implementing an action by pressing a keyboard key is decomposed in two steps:
- 1/ Define a shortcut : which key combination to press to throw a shortcut intent
- 2/ The action is associated to the shortcut intent
All these notions are defined in the MaterialApp, as shown below:
MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primaryColor: Colors.white,
),
darkTheme: ThemeData.dark(),
themeMode: themeMode,
showPerformanceOverlay: showPerformanceOverlay,
debugShowCheckedModeBanner: showDebugBanner,
debugShowMaterialGrid: debugShowMaterialGrid,
actions: {
DevelopmentIntent: DevelopmentAction(),
ToggleThemeIntent: ToggleThemeAction(),
SendDebugDataIntent: SendDebugDataAction(),
},
shortcuts: _shortcuts,
home: MyHomePage(),
);
Define shortcuts
In all cases, shortcuts as defined as a map of LocicalKeySet (keys) and Intent (values).
A LogicalKeySet define the keys to press simultaneously in order to throw an Intent.
You have to create your own Intents, as Intent class is abstract. Here is a simple implementation.
class DevelopmentIntent extends Intent {
final VoidCallback action;
DevelopmentIntent({this.action});
}class SendDebugDataIntent extends Intent {
const SendDebugDataIntent();
}class ToggleThemeIntent extends Intent {
final void Function(Brightness brightness) toggle;
ToggleThemeIntent({this.toggle});
}
You can put any information you want. In my example, I was very lazy so I implemented some generic Intent, named DevelopmentIntent, that stores a function that will be called later.
Why not call the action directly into the Intent ? Because you can manage actions policy, prevent them to execute in specific conditions for example. Also, you can have a proper access to a BuildContext in Actions (have a look to the SDK ContextAction class).
Define Actions
Actions are defined as a map of Intent types (as keys) and Action (as values). As for Intent, you have to create your own actions by creating classes inheriting from Action.
class DevelopmentAction extends Action<DevelopmentIntent> {
@override
Object invoke(covariant DevelopmentIntent intent) {
if (kDebugMode) {
intent.action?.call();
}
}
}class SendDebugDataAction extends ContextAction {
@override
Object invoke(covariant Intent intent, [BuildContext context]) {
if (!kDebugMode) {
return null;
}
showDialog(...);
}
}class ToggleThemeAction extends ContextAction {
@override
Object invoke(covariant Intent intent, [BuildContext context]) {
if (kDebugMode && intent is ToggleThemeIntent) {
intent.toggle?.call(Theme.of(context).brightness);
return true;
}
return false;
}
}
As you can see, you have the choice to implement a background process or something more visual, by inheriting ContextAction class.
Let’s see advantages to do that in your mobile application.
Usecase 1 : Debug banner toggle
In that scenario, the purpose is to make demonstration and screenshots of a debug version of your application, without creating a custom build for that. It’s even more maintainable if you have to do that frequently.
The only development is located around the MaterialApp which command the display of various overlays, banner, ThemeMode, et cetera… To perform that, you have to wrap it into a StatefullWidget handling these features.
Or you can also use SharedPreferences for that, if your features need to be saved. Or create your own ValueNotifier (through Provider for example)… There is tons of ways to implement it. I choose maybe the laziest one :D .
Usecase 2 : Display overlays to debug your widgets
When I implemented some custom widgets, such Wall layout (https://pub.dev/packages/flutter_wall_layout) and the cupertino listview (https://pub.dev/packages/cupertino_listview), I used a lot these two overlays to get some metrics about my libraries. It could have been more comfortable to use them directly on the device screen, instead of using Android studio panels.
Usecase 3 : Toggle light and dark mode
It’s a very simple feature, but it will save a lot of time! When I started developing my own Flutter applications, I may have spoil hours changing screen mode in device settings, to check that light and dark mode on both Android and iOS were fine. It can be a real pain to modify ThemeData properties in order to set the right color, and doing it without going out your application keep your focus!
It’s always good to create dev tools to ensure quality and maintainability of your app. And I think this one is a must have!
Usecase 4 : Export a large set of debug data
Some times ago I worked on an application handling a big set of data, up to 500Mo, packaged and distributed through a Gateway. When we had to fix bugs, most of the time we had a partial view of the problem, because we hadn’t access to local device data.
You know, that kind of bug you can’t reproduce, and only occurring on your client device. As you may understand, this is not a feature for you but for testers !
With Shortcuts, testers will be able to export these internal data, and bring it to you. In this example, we display a dialog to let the user send data by mail, but we could have exported data to a device public directory (for big data)!
I’m not sure that you can export a large amount of information using Crashlytics, as “Crashlytics supports a maximum of 64 key/value pairs. After you reach this threshold, additional values are not saved. Each key/value pair can be up to 1 kB in size.” (source: https://firebase.google.com/docs/crashlytics/customize-crash-reports-fabric-sdk)
Conclusion
My first idea was to discover Shortcuts for Flutter desktop and web applications, but I was astonished that it actually works for simulators.
Coupled to the fact that we can connect hardware keyboards on our devices, I could be possible to use this hidden features on real devices too.
Yes, please remind that it shall remain “hidden features”, because it’s not conventional on mobile applications development. And it could introduce a backdoor to your application if not well designed. That’s why I implemented it to be working in debug mode only.
Last but not least, I showed you some usecases that make sense to me, have you other ones to share ?
As always, you can get the source code used in that article here (Flutter beta-channel, to make Flutter web application available):