Demo Proof of delivery

TL;DR

This year we delivered our first Line of business app to one of our customers that is built with Flutter.

The launch of the app was a success. The app went live without issues and the app was accepted by the users with ease.

We thought this app is a great way to demonstrate our app craftmanship of Line of business apps to a broader audience. That is why we decided to put a demo version of this app in the play store, app store, and on the web.

You can download the app for your device, or even try it out in your browser.

The creation of the demo app

Although the customer was using the app successfully in production, it was not ready yet to serve as a demo app.

We needed to make the ‘demo’ experience better:

Replace customer’s logo and icons

This was not the most fun part. Normally these assets are delivered to us by a designer, but now I had to figure it out by myself (again). Nothing too hard, but tedious as I remembered how it was in the past.

Add iOS and Web as a target

Now the fun started. Support for iOS was already there, or at least for the most part. Adding Web support was as easy as switching to Flutter’s beta branch.

The only major part that had to be rewritten for the web was the datastore. We like to have the power of SQL in our line of business apps, but SQL support on the web is just not there, or at least not in the way we like it. Because this application is not too difficult, we reworked the datastore from SQLite tables to hive tables. This took roughly 2 days but was straightforward and easy.

Internationalize the app

The original app was only in Dutch. To internationalize the app we used localizely. Localizely is an ide plugin that uses arb files for input and generates code that follows the same pattern to localize apps as the rest of the Flutter framework. Any literal string in the code can be easily extracted to the arb files by pressing ALT-ENTER.

After that, I created a Google sheet where I could import the English arb file (which is a JSON formatted file) and machine-translate the texts to other languages. With the help of some Google sheet plugins, the result can be extracted as arb/JSON files again. This was a bit sensitive to errors, but it got the job done. For larger projects, I would recommend a more professional workflow than Google sheets.

Use the camera as a barcode scanner

The original app was designed for a Zebra TC57 touch computer.

This is a ruggedized Android device with a dedicated barcode scanner. The barcode scanner of this device performs excellently.

On other devices, we wanted to use the camera as a fallback option. This is where things got a bit complicated.

The opensource ZXING barcode scan library is abandoned. Any plugin based on that is not future proof.

Commercial libraries are performing very well, but are very expensive and have a subscription model. You will have to pay for the number of users per year. This is far from ideal for a demo app.

We ended up implementing the barcode recognition with Google’s ML-Kit. At this time ML-Kit is still in beta, but there is a working Flutter sample from the firebase team. We used this sample as a starting point.

On Zebra devices, we normally enable ‘picklist mode’. When in picklist mode, the crosshairs projected on the target (red laser-lines) determine the barcode that is scanned. This makes it easier to scan the right barcode when more barcodes are visible at the same time. For example when barcodes are printed on a sheet of paper.

To make it easier for the user to scan the right barcode with the camera, the user can suspend the recognition by touching the screen You can then position the barcode under the square. When you release your finger, the barcode recognition is active and barcodes are scanned. A green square indicates that the recognition is suspended, a red square indicates that the recognition is active.

Because every type of phone has different camera characteristics like resolution and autofocus, the barcode scanning performance can be tweaked in the settings of the app.

I think that iPhone users will be most satisfied with the barcode scanning performance.

Mock the communication with the backend

The production app communicates with a backend to exchange data. Of course, the demo app has to be able to function without a backend. Therefore there is a setting ‘Demo mode’ in the app. For the demo app, this setting is enabled by default. When enabled, the communication functions inside the app are replaced with default reasonable responses. That’s why you can test the app without a backend.

Final words

We had a really good time, building this app in Flutter. I had great plans of explaining every detail of it on this blog. Unfortunately, COVID-19 made this impossible. Thankfully not because of illness, but because off lack-of-time.

Thank you all for reading and trying out the demo.

For questions about the app, you can contact me.
If you want a quotation for your Line of Business app, contact us here.

Flutter for Line Of Business apps – Logging

The importance and usage of logging is for Line Of Business apps different than for consumer apps.

Consumer apps

  • Collect application crashes/warnings
  • Narrow down problems to specific manufacturers, hardware, os versions
  • Get insights on how much the app is installed/used across the globe
  • Get insights on how the app is used
  • Get insights on how the monetization of the app can be improved

Line Of Business apps

  • Collect application crashes/warnings
  • Narrow down issues with backend services
  • Narrow down issues reported by individual users
  • Enterprises do not like Line Of Business apps that leak info to Google, Apple or other Application Monitoring or Tracking Software

On the other hand, logging for Line Of Business apps is quite simple:

  • Logging is written to local persistent storage (text file or database)
  • Add logging to strategic points in the app, so you can follow the usage of the app
  • Each log line has a timestamp, loglevel, message and stacktrace (if available)
  • When the max amount of logging is reached, delete old logging
  • When a problem is reported, download the logging with an EMM and analyze it

Async challenges

The async nature of Flutter does make logging a bit more complicated than it was in the synchronous world.

We want to log things as soon as the application starts, but to store the logging, we are dependant on an async filesystem/database that needs time to initialize.

To be able to log events before it can be written, I split the logging service into two parts:

  • Logging service
  • Logging service file (persistent part)

The logging service is the first service that is started. This means that all other services can log messages. As long as the ‘Logging service file’ is not started, the ‘Logging service’ keeps the logging in memory.

As soon as the ‘Logging service file’ is started, it registers its ‘writer’ function in the Logging service. As soon as this is registered, the cached logging is purged to this ‘writer’ function. From now on, the ‘logging service’ uses this ‘writer’ function directly instead of its memory cache.

Initialization of the LoggingServiceFile and registering its writer function:

  Future<void> onStart() async {
    await Provider.of<VersionService>(context, listen: false).init();
    await Provider.of<SoundService>(context, listen: false).init();
    await Provider.of<ConfigurationBoxService>(context, listen: false).init();
    await Provider.of<ConfigurationLoader>(context, listen: false).init();
    await Provider.of<LoggingServiceFile>(context, listen: false).init();
    Provider.of<LoggingServiceFile>(context, listen: false).connectLogWriter();
    await Provider.of<DatawedgeService>(context, listen: false).init();
    await Navigator.pushReplacement(context, TimeRegistrationPage.Route);
  }

The implementation of LoggingService:

import 'package:enum_to_string/enum_to_string.dart';
import 'package:flutter/foundation.dart';

enum LogLevel {
  debug,
  information,
  warning,
  error,
  critical,
}

class LoggingService {
  LoggingService() {
    _start();
  }

  final int _maxCacheRows = 1000;

  var _cacheLog = List<Logging>();

  void _start() {
    log(LogLevel.information, "$runtimeType started");
  }

  void Function(Logging logging) _writer;

  void setWriter(void Function(Logging logging) writer) {
    _writer = writer;
    _cacheLog.forEach((a) => _writer(a));
  }

  void log(LogLevel logLevel, String message, {String stackTrace = ''}) {
    var logging = Logging(
      id: null,
      timestamp: DateTime.now().toUtc().toIso8601String(),
      logLevel: logLevel,
      message: message,
      stacktrace: stackTrace,
    );
    debugPrint(logging.toString());
    if (_writer == null) {
      _cacheLog.add(logging);
      if (_cacheLog.length > _maxCacheRows) {
        _cacheLog.removeRange(0, _cacheLog.length - _maxCacheRows);
      }
    } else {
      _writer(logging);
    }
  }
}

class Logging {
  Logging({
    this.id,
    this.timestamp,
    this.logLevel,
    this.message,
    this.stacktrace = '',
  });

  final int id;
  final String timestamp;
  final LogLevel logLevel;
  final String message;
  final String stacktrace;

  @override
  String toString() {
    var result = List<String>();
    result.add(
        '[LOGGING] ${EnumToString.parse(logLevel)} $timestamp Message: $message');
    if (stacktrace.isNotEmpty) {
      result.add('[LOGGING StackTrace: $stacktrace]');
    }
    return result.join('\n');
  }
}

The implementation of LoggingServiceFile:

import 'dart:io';

import 'package:flutter/cupertino.dart';
import 'package:intl/intl.dart';
import 'package:app/services/hive/configuration_box_service.dart';
import 'package:app/services/logging_service.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart';

const String externalLogFilename = "cc600_log.txt";
const String externalBackupLogFilename = "cc600_log_1.txt";

class LoggingServiceFile {
  LoggingServiceFile(this._log, this._configurationBoxService) {
    _start();
  }

  final LoggingService _log;
  final ConfigurationBoxService _configurationBoxService;

  File _logFile;
  String _logFilename;
  File _backupLogFile;
  String _backupLogFilename;

  void _start() {
    _log.log(LogLevel.information, "$runtimeType started");
  }

  Future<void> init() async {
    var directory = await getExternalStorageDirectory();
    _log.log(LogLevel.information, 'External path reported: ${directory.path}');
    _logFilename = join(directory.path, externalLogFilename);
    _logFile = File(_logFilename);
    _backupLogFilename = join(directory.path, externalBackupLogFilename);
    _backupLogFile = File(_backupLogFilename);
  }

  void connectLogWriter() {
    _log.setWriter(_writer);
  }

  void _writer(Logging logging) {
    if (!_configurationBoxService.logging) {
      return;
    }
    try {
      if (_logFile.existsSync() && _logFile.lengthSync() > 1000000) {
        if (_backupLogFile.existsSync()) {
          _backupLogFile.deleteSync();
        }
        _backupLogFile = _logFile.renameSync(_backupLogFilename);
        _logFile = File(_logFilename);
      }
      _logFile.writeAsStringSync(
        '${DateFormat('dd-MM-yyyy HH:mm:ss').format(DateTime.now())} ${logging.message}\r\n',
        mode: FileMode.append,
        flush: true,
      );
    } catch (e) {
      debugPrint('error while writing to log ${e.toString()}');
    }
  }
}

Note that the LoggingServiceFile could be easily exchanged with a LoggingServiceDatabase. Only the signature of the writer function has to be the same.

Flutter for Line Of Business apps – Async initialization of services during startup

Startup phases:

  1. Bootstrapping the native platform code
  2. Bootstrapping the Flutter code
  3. Normal application state where user interaction is allowed

Bootstrapping the native platform code

The first thing that happens when a Flutter app starts, is the bootstrapping of the native platform code. This piece is responsible for showing the splash screen and creating a container where the Flutter app can live. As soon as everything is ready, the main function in the /lib folder is executed. This main function is responsible for bootstrapping the Flutter code.

Bootstrapping the Flutter code

The bootstrapping of the Flutter code is where things might get complicated due to the async nature of Flutter and the usage of Provider to provide services and state. The reason for this is:

  1. When using Provider for services and state, these services and state are part of the widget tree.
  2. Some services are initialized asynchronously. For example Sqflite and all other services that are using method channels to access the underlying platform.
  3. The widget tree must build to have a user interface
  4. The building of the user interface depends on services and state defined in step 1

To complicate things even further, some services are dependant on other services and the order of initialization of the services might matter.

One might try to initialize all the services before runApp(MyApp()) is called.

Future<void> main() async {
   await initAllServicesAndState();
   runApp(MyApp());
}

This has some major downsides:

  • There is no widget tree yet, getting the services into the widget tree from this point is hacky.
  • If something fails during initialization and you want the user to decide how to recover, there is no easy way to show some UI.

Another approach is to initially build a user interface that is not dependant on any services or state. The only responsibility of this page is to initialize things. In my project, this is called AppStartPage.

AppStartPage

I decided to take the following approach:

  1. Let Provider create the services and states
  2. Set AppStartPage as ‘home’ in the MaterialApp
  3. Let AppStartPage initialize the services
  4. When initialization is done, replace the AppStartPage with the HomePage of your app
class AppStartPage extends StatefulWidget {
  @override
  _AppStartPageState createState() => _AppStartPageState();
}

class _AppStartPageState extends State<AppStartPage> {
  @override
  void initState() {
    super.initState();
    onStart();
  }

  Future<void> onStart() async {
    await Provider.of<VersionService>(context, listen: false).init();
    await Provider.of<SoundService>(context, listen: false).init();
    await Provider.of<ConfigurationBoxService>(context, listen: false).init();
    await Provider.of<ConfigurationLoader>(context, listen: false).init();
    await Provider.of<LoggingServiceFile>(context, listen: false).init();
    Provider.of<LoggingServiceFile>(context, listen: false).connectLogWriter();
    await Provider.of<DatawedgeService>(context, listen: false).init();
    await Navigator.pushReplacement(context, HomePage.Route);
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

In this sample, the AppStartPage is an empty container. In a real app, you might want to show a page that informs the user what is going on, like ‘Loading….’.

A downside is that the app now has two splash screens. The first is coming from the Android/iOS project. The second one is AppStartPage.

But the advantages of an AppStartPage obvious:

  1. Everything is initialized properly and in the right order when the app starts
  2. No user interaction is possible before the initialization is done, so no-undefined state.
  3. When there is a problem during initialization, you can ask the user in a pop-up what to do.
  4. It is super simple….

Flutter for Line Of Business apps – Local datastore, SQLite

History

At the time I was programming apps for Windows CE and Windows Mobile, we used SQL CE. It integrated nicely with the Visual Studio tooling at that time and it was easy to work with. The downside of SQL CE was the likelihood of database corruption. The corruption could occur if the battery was pulled, forced reboot, or if the SD card was ejected. On some hardware, the SD Card was ejected (in software) if the device went to sleep. So this was a nightmare.

Can you imagine what happens to customer satisfaction when he loses a whole day of work?

When looking for alternatives, I found SQLite. There was no compiled executable for Windows CE at that time, but I found a source, ready for compilation to Windows CE.

SQLite has proved its value since that day, more than 10 years ago. I never encountered a corrupt database anymore.

Why a local SQL datastore

In today’s connected world, one might ask why there is a need for a local SQL datastore on the device. There is a difference between the requirements of a consumer app and a Line of Business app.

Network connectivity

The work that needs to be done with Line of Business apps is not always at places with a fast or even present network connection. Think of basements, old warehouses, container yards or the countryside. A company wants to use the app reliably at 100% of the locations. Therefore, a lot of Line of Business apps are batch applications.

Batch applications

Batch applications work in cycles. A sample of a cycle is here below:

#Network availableAction
1yesAt company: download master data
2Perform work (user collects data)
3maybeIf connection detected, upload collected data
4Goto 2 until finished
5yesAt company: upload collected data

Speed

With manual data entry, the speed of a lookup is not very important. If it took a user 8 seconds to type a number, it does not matter much if checking the number by the app takes 2 seconds.

But most Line of Business apps that I write, run on hardware with dedicated barcode scanners. Scanning a barcode is almost instant. Waiting 2 seconds for the app to respond is not acceptable in this use case.

Looking up data in an SQLite database is very fast and generally a matter of milliseconds. Even a million rows of master data is accessed without a noticeable delay.

This kind of speed is needed when an employee scans items in a retail shop to order at their supplier.

Large datasets

The consequence of a batch application is that you have to download all the needed master data to the device. Sometimes this is just a few lines, but in some cases, it might be a million rows of data.

An approach is to download the dataset as a zip file. A zip file has the following advantages:

  • Size of the transfer
    Improves speed over slower network connections
  • Detect data corruption
    When the zip files extract successfully, you have a high degree of certainty that the data is complete and not corrupted
  • Have a coherent dataset
    With a zip file, you can download several files at once and be certain to have a coherent dataset.

It may take a minute to (batch) insert large datasets into a SQLite database. Normally this is not an issue, as it only happens once a day. You can also automate this process in software by starting the download automatically before the workers start their day.

Complex queries

A big advantage of using an SQLite datastore is that you can run complex queries against it.

An application might start simple with a few tables, relations and statuses. But as the customer explores the power and benefits of the app in their logistical process, it is almost certain that the customer will ask for more functionality. With the power of SQL and SQLite, you can be certain that there will be no limitation on the complexity of the queries.

Data integrity

Data integrity is as important is the integrity of the source code of the app. If the data is corrupted, the program might get in a loop, behave unexpectedly or even crash your application. The size of the database might also get out of control.

A good data model with the right (composite) primary keys and foreign keys will help you to keep your data valid.

Database versioning

As the requirements of the customer changes, it will also influence your data model. It is good to know that SQLite can help you to upgrade and/or downgrade the version of your data model.

With the onUpgrade and onDowngrade methods, you can update or downgrade the data model. Although this is something that you have to write manually, you can take measures that the data in the tables are transformed in the right way.

Downsides

Of course there are also downsides to a local SQLite datastore:

  • Skills needed
    You need to know how to design and query sql databases. This can be a complex and challenging subject and can be done wrong in more than a thousand ways.
  • Async data access
    Every call to the database is asynchronously. Be prepared that when you navigate to a page, the data will always come later. This will make your application more complex.
  • No web support
    There is no direct SQLite support for the web at this time. There are some proof of concepts, like Moor which might be useful for demo purposes. But I don’t expect that serialising the database after each update does anything good for performance and reliability.

Using Flutter for Line Of Business apps – State management with provider

History

When I started exploring Flutter, there was (and there still is) a lot of discussion about state management. At that time, Google promoted Inherited Widget as the way to propagate information down the widget tree. I tried Inherited Widget, and indeed with some extra code, you could use it to make your state accessible to your widgets. But it involved quite some boilerplate, which was not easy to read and understand. It felt overly complex and likely to introduce errors that are difficult to spot.

After that, I tried get_it. That was easy and simple. It looked a lot that I was used to when I was working in C#. But I was not sure. I found a lot of happy users of get_it. But there were also a lot of people calling the foundation of get_it an anti-pattern. Although I did not really understand the discussion at that time, the choice was made for me by Google. They explicitly recommended Provider in their talk Pragmatic State Management in Flutter (Google I/O’19)

Provider

With Provider, you can wrap your class with one statement into an Inherited Widget.

In the code below, a Provider of an S080StateService is created. When this S080StateService is created, it’s dependencies are injected via its constructor. Provider is used to find these dependencies in the tree above.

  Provider<S080StateService>(
    create: (BuildContext context) {
      return S080StateService(
        Provider.of<LoggingService>(context, listen: false),
        Provider.of<LadenLossenBoxService>(context, listen: false),
        Provider.of<ConfiguratieBoxService>(context, listen: false),
        Provider.of<UnloadingStateService>(context, listen: false),
        Provider.of<CommunicatieBoxService>(context, listen: false),
        Provider.of<NotInteractiveCommunicationService>(context, listen: false),
        Provider.of<NavigatorService>(context, listen: false),
      );
    },
  )

Below this provider, the instance of the S080StateService is accessible by:

var state = Provider.of<S080StateService>(context);

The way this works is readable and easy to understand until you have a lot of services, that depends on a lot of other services.

Avoid super-nesting

When your code is dependent on many services, the code gets easily unreadable. Every Provider creates a parent-child relationship and therefore indents your code.

The indentation can easily be solved by using the MultiProvider widget. When you supply a list of widgets to the MultiProvider widget, the MultiProvider will take care of the parent-child relationship at runtime.

In code:

  • MultiProvider
    • Provider1
    • Provider2
    • Provider3
    • Provider4

At runtime:

  • MultiProvider
    • Provider1
      • Provider2
        • Provider3
          • Provider4

This improves readability a lot.

Another improvement can be made to move the list of Providers that you feed to the MultiProvider to a separate file.

class S080UnloadingSignaturePage extends StatefulWidget {
  static get Route => RouteEx<S080UnloadingSignaturePage>(
        builder: (context) => S080UnloadingSignaturePage(),
      );
  @override
  _S080UnloadingSignaturePageState createState() =>
      _S080UnloadingSignaturePageState();
}

class _S080UnloadingSignaturePageState
    extends State<S080UnloadingSignaturePage> {
  final SignatureController _controller =
      SignatureController(penColor: CompanyColor.companyBlue);

  @override
  Widget build(BuildContext context) {
    double bottomHeight = 80.0;
    return MultiProvider(
      providers: providers,
      child: Builder(builder: (BuildContext context) {
        var unloadingStateService = Provider.of<UnloadingStateService>(context);
        var state = Provider.of<S080StateService>(context);
        return Scanner(

Here the list of providers is just one line, on line 19. The actual list of providers is in a separate file:

List<SingleChildWidget> providers = [
  Provider<DialogService>(
    create: (BuildContext context) {
      return DialogService(
        context,
        Provider.of<ValidatorService>(context, listen: false),
        Provider.of<DatawedgeService>(context, listen: false),
        Provider.of<SoundService>(context, listen: false),
      );
    },
  ),
  Provider<S080StateService>(
    create: (BuildContext context) {
      return S080StateService(
        Provider.of<LoggingService>(context, listen: false),
        Provider.of<LadenLossenBoxService>(context, listen: false),
        Provider.of<ConfiguratieBoxService>(context, listen: false),
        Provider.of<UnloadingStateService>(context, listen: false),
        Provider.of<CommunicatieBoxService>(context, listen: false),
        Provider.of<NotInteractiveCommunicationService>(context, listen: false),
        Provider.of<NavigatorService>(context, listen: false),
      );
    },
  ),
];

Although I am not convinced that this is the very best way, it gets the job done. The code is readable and understandable. And, if you make a mistake, the error message is usually so well detailed, that finding and fixing the issue is easy.

Flutter for Line of Business apps – Splash screens and launch icons

Adding splash screens and launch icons is always a pain. It must be done, and, it must be done right. You only get one chance to make a first impression.

Luckily I had a UX designer that provided all the assets in the right sizes.

Because I had all assets in the right sizes, I decided to do it manually for Android and iOS and not to use a package like https://pub.dev/packages/flutter_launcher_icons.

Splash screens and launch icons are platform-specific. Therefore I had to do the changes in the native solutions in Android Studio and Xcode.

Splash screen on iOS

For iOS, I choose to just adjust what the default iOS project already provided. In Assets.xcassets, there is a section LaunchImage with three empty spots with the names “1x”, “2x” and “3x”.

To add an image to the existing splash screen you have to add these three images, in my case these were:

ic_launcher.png   (393x137)
ic_launcher-1.png (524x182)
ic_launcher-2.png (786x273)
Before adding the files
After adding the files

The splash screen now looks like this:

Before changing the background color

After changing the background color, the splash screen looks fine.

After changing the background color

Launch icons on iOS

To add the launch icons in iOS, you have to open the section AppIcon in Assets.xcassets. With a default Flutter project, it will look like this in Xcode:

Before changing the launch icons

After carefully dragging and dropping all icons to the right places, it will look like this:

After changing the launch icons

At last, you would like to change the CFBundleName in the info.plist to something that you want to have as text below the launch icon.

Splash screen on Android

The process of adding a splash screen felt a bit easier on the Android side.

First I replaced all existing ic_launcher.png files with the ones that I received from my UX designer:

<flutter_project>/android/app/src/main/res/mipmap-mdpi/ic_splash.png
<flutter_project>/android/app/src/main/res/mipmap-hdpi/ic_splash.png
<flutter_project>/android/app/src/main/res/mipmap-xhdpi/ic_splash.png
<flutter_project>/android/app/src/main/res/mipmap-xxhdpi/ic_splash.png
<flutter_project>/android/app/src/main/res/mipmap-xxxhdpi/ic_splash.png

Add the file that specifies the background color of the splash screen:

<flutter_project>/android/app/src/main/res/values/colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="splash_background">#012141</color>
</resources>

Edit the file that defines the splash screen to include the background color and the ic_launcher image:

<flutter_project>/android/app/src/main/res/drawable/launch_background.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@color/splash_background" />

    <!-- You can insert your own image assets here -->
    <item>
        <bitmap
            android:gravity="center"
            android:src="@mipmap/ic_splash" />
    </item>
</layer-list>

That’s it. The splash screen on Android is ready.

Launch icons on Android

I placed all the received assets in the following folder:

<flutter_project>/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
<flutter_project>/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
<flutter_project>/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
<flutter_project>/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
<flutter_project>/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
<flutter_project>/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
<flutter_project>/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
<flutter_project>/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
<flutter_project>/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
<flutter_project>/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
<flutter_project>/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
<flutter_project>/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
<flutter_project>/android/app/src/main/res/drawable/ic_launcher_foreground.xml
<flutter_project>/android/app/src/main/ic_launcher-web.png

At last, you would like to change the android:label in the AndroidManifest.xml to something that you want to have as text below the launch icon.

Flutter for Line Of Business app – Theming

When you receive a design from a UX designer, from for example Zeplin, everything is specified:

  • Colors
  • Fonts (Name, Size, Style)
  • Margins and padding of the elements

As in the Flutter counter-sample project, I started in my project with changing the primary color:

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // This is the theme of your application.
        //
        // Try running your application with "flutter run". You'll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // "hot reload" (press "r" in the console where you ran "flutter run",
        // or simply save your changes to "hot reload" in a Flutter IDE).
        // Notice that the counter didn't reset back to zero; the application
        // is not restarted.
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

But then I quickly realized that the primary color is a specific value in a range of colors, a ColorSwatch.

 static const MaterialColor blue = MaterialColor(
    _bluePrimaryValue,
    <int, Color>{
       50: Color(0xFFE3F2FD),
      100: Color(0xFFBBDEFB),
      200: Color(0xFF90CAF9),
      300: Color(0xFF64B5F6),
      400: Color(0xFF42A5F5),
      500: Color(_bluePrimaryValue),
      600: Color(0xFF1E88E5),
      700: Color(0xFF1976D2),
      800: Color(0xFF1565C0),
      900: Color(0xFF0D47A1),
    },
  );
  static const int _bluePrimaryValue = 0xFF2196F3;

Such a simple task, changing the primary color, was just not so simple anymore. How do I generate a range of colors from one specific color that was specified by the UX designer?

After some googling, I found the website: http://mcg.mbitson.com/. Here you can delegate the burden of creating a ColorSwatch. If you specify the primary color, this website will generate the color variants for you. Great!

I ended up copying the code of Colors.blue and changed it with the values from that website. Together with some other specific colors, I put that into a class:

class CompanyColor {
  static const MaterialColor primaryColor = MaterialColor(
    _primaryColor,
    <int, Color>{
      50: Color(0xffe1e4e8),
      100: Color(0xffb3bcc6),
      200: Color(0xff8090a0),
      300: Color(0xff4d647a),
      400: Color(0xff27425e),
      500: Color(_primaryColor),
      600: Color(0xff011d3b),
      700: Color(0xff011832),
      800: Color(0xff01142a),
      900: Color(0xff000b1c),
    },
  );

  static const int _primaryColor = 0xff012141;

  static const Color companyBlue = Color(0xff063a65);
  static const Color companyOrange = Color(0xfff98c1b);
  static const Color companyWhite = Color(0xffffffff);
  static const Color companyBlueLight = Color(0xff8fa1b7);
  static const Color companyBlueText = Color(0xff124370);
  static const Color companyRed = Color(0xffcd3631);
  static const Color companyGreen = Color(0xff008a57);
  static const Color companyYellow = Color(0xfffcc634);
  static const Color companyGrey = Color(0xffe4e8ed);
}

This was good. Now I had one place to put all the custom colors. But this was still not good enough. The UX designer has designed a consistent user interface, but I was repeating myself all over the place.

Referencing the colors by hand did not feel like applying the D.R.Y. principle. There had to be a better way. And, of course, there was: “Make a custom theme”. And the good news is that a theme is not only for colors but also for fonts, padding, etc.

Changing the theme is quite easy. Identify the object that you want to customize application-wide. Drill down to the Flutter source code.

For example for changing the cursor color:

  /// The color to use when painting the cursor.
  ///
  /// Defaults to [ThemeData.cursorColor] or [CupertinoTheme.primaryColor]
  /// depending on [ThemeData.platform].
  final Color cursorColor;

You can change that in a custom theme like this:

ThemeData companyTheme(BuildContext context) {
  var theme = Theme.of(context);
  return theme.copyWith(
    primaryColor: CompanyColor.primaryColor,
    iconTheme: theme.iconTheme.copyWith(
      color: CompanyColor.companyWhite,
    ),
    cursorColor: CompanyColor.companyOrange,
    inputDecorationTheme: theme.inputDecorationTheme.copyWith(
        contentPadding: EdgeInsets.only(bottom: 7.0, top: 4.0),
        enabledBorder: UnderlineInputBorder(
          borderSide: BorderSide(
            color: CompanyColor.underLineInputColor,
          ),
        ),
        focusedBorder: UnderlineInputBorder(
          borderSide: BorderSide(
            color: CompanyColor.underLineInputColor,
          ),
        ),
        focusedErrorBorder: UnderlineInputBorder(
          borderSide: BorderSide(
            color: CompanyColor.companyPink,
          ),
        ),
        errorBorder: UnderlineInputBorder(
          borderSide: BorderSide(
            color: CompanyColor.companyPink,
          ),
        ),
        errorStyle: TextStyle(
          color: CompanyColor.companyRed,
          fontFamily: 'Lato-Bold',
          fontSize: 14.0,
        )),
  );
}

Apply this theme in your MaterialApp:

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: providers(isZebra: isZebra, scanWithCamera: scanWithCamera),
      child: Builder(
        builder: (BuildContext context) => MaterialApp(
          localizationsDelegates: [
            GlobalMaterialLocalizations.delegate,
            GlobalWidgetsLocalizations.delegate,
            GlobalCupertinoLocalizations.delegate,
          ],
          supportedLocales: [const Locale('nl')],
          title: 'Ferwerda BV',
          theme: companyTheme(context),
          navigatorObservers: [
            Provider.of<RouteObserver>(
              context,
              listen: false,
            ),
          ],
          home: S005StartupPage(),
        ),
      ),
    );
  }

That’s it. Now you have all theming in one place.

Flutter for Line Of Business apps – Assets, where to put what

I am good at programming, but not good at drawing and designing icons. Also, it is better to have the theming of the app and layout of the screens to be done by a visual designer. A professional, who knows how to create a consistent look and feel, and to make the app visually attractive.

So when the visual designer has done its job, I have a bunch of screens and assets. Normally we get the designs in Zeplin. This is very handy because it allows seeing the exact amount of pixels the visual designer used for spacing and elements.

With Zeplin, it is also possible to download the assets as a zip file. The contents are in the following folders:

  • drawable-mdpi
  • drawable-hdpi
  • drawable-xhdpi
  • drawable-xxhdpi
  • drawable-xxxhdpi

From the Flutter documentation, I learned that assets could be resolution aware if you have the right naming of the folders. But it was not obvious for me where I should put what.

After some testing I used the following scheme:

  • Project Folder
    • assets
      [drawable-mdpi]
      • 1.5x
        [drawable-hdpi]
      • 2.0x
        [drawable-xhdpi]
      • 3.0x
        [drawable-xxhdpi]
      • 4.0x
        [drawable-xxxhdpi]
    • sounds
    • fonts

Although I did not find this in the Flutter documentation, it looked logical to me. And testing it on the TC57 showed the size of the images were as intended by the designer.

Flutter for Line Of Business apps – Development tools and target device

When I started on this project I had to choose the development environment to work with. The two obvious development environments for creating Flutter apps are:

  • Android Studio
  • Visual Studio Code

I did not want to introduce too many variables and decided to stick with Android Studio. Both Android Studio and Flutter come from Google, so it looked like a safe bet.

The device that the app has to run on is the Zebra TC57 Touch Computer. This is a ruggedized Android device with a built-in barcode scanner.

Zebra TC57

More information about this device can be found on:
https://www.zebra.com/us/en/products/spec-sheets/mobile-computers/handheld/tc52-tc57.html

Flutter for Line Of Business apps – Design phase

The design phase defines for a large part if a project will be successful.

  • If the design has flaws, the implementation will have flaws.
  • If the design is not clear, the customer’s vision will almost always be different than yours.
  • If the design is not complete, the customer will argue that the missing parts must be part of the solution. Delivering an incomplete product could not have been your goal, is it?

The design phase is the most important part of a project. Once everything is clear, documented and agreed, it only has to be implemented.

By saying that it “only has to be implemented”, I on purpose minify the importance of the implementation phase.


If you have a good design, you only have to write it out in code. The design will be the guide how to structure your code, classes, database tables, and folders.

I am used to writing my designs in Microsoft Word. It may not be the best tool, but with some macro’s it gets the job done.

The design can be split up in the following parts:

  1. Interview with the customer
  2. Write design with wireframes
  3. Send a wireframe version of the design to the customer
  4. Adjust the design with the customer’s feedback
  5. Send the design to a screen designer
  6. Replace the wireframes with the designed screens
  7. Send the definitive version for approval to the customer
  8. When the customer agrees, let’s start coding!