Flutter for Android – Layout

The code for this article

We are going to focus on adding a native layout for an Android phone.

At this point it makes sense to make a broad distinction and go with a MaterialApp for all devices and platforms except iOS devices.

We can add the web and desktops platforms later if need, all platforms are stable now that Flutter 3 has been released.

This blog covers:

  • Device detection.
  • Adding a material app phone layout to the layout selector.
  • Supporting material styles.
  • Deciding on the components needed to give a native Android phone experience.
  • Creating adaptive wrappers for widgets with platform specific implementations.
  • Going test first with Gherkin feature tests and using them to confirm the correct application is run for each device and the appropriate layout is used.
  • Improving the automation to run and report on the Gherkin feature tests.

That’s quite a lot, but supporting new devices or platforms is not trivial.

Thankfully we don’t have endless options, but they do slowly change, folding phone and folding tablet usage is increasing and you can now change the application experience based on the hinge angle.

I wanted to able to easily add features tests and build the application test first through them, to do this I started by adding more automation, for running and reporting on the feature tests.

The next step was to create and automate the story to use a material application when we detect an android device.

Example map for Platform support feature
Example map for Platform support feature

In order to make the feature test pass I made some improvements to device detection on startup.

To cater for the different application theme implementations, I moved the theme state up into some new classes and wrote some extension methods to convert them into ThemeData for Cupertino and Material applications.

At this point the platform tests passed for both platforms but the recipe feature tests only passed on the iPhone and I needed to start work on swapping out platform specific widgets.

To do this I have created cross platform widgets where platform specific implementations are required.

The main difference is that we are replacing the tabbed navigation on iOS with a draw navigation for Android.

Android side draw navigation
Android side draw navigation

Once that was done I turned my attention to the content and AppBar.

With the CupertinoTabScaffold you passed in a list of widgets that had an app bar and content.

With Android you need to scaffold the full screens and pass the same draw navigation to each.

The Cupertino scaffold preserves the widget (screen) states when we switched tabs, but the Material scaffold does not, to achieve this I put an IndexedStack at the top level that switches the screens in and out and preserves the state.

Too some degree I’ve shortcut the process by creating three widgets to replace the original DigestTab widget used in the single platform CupertinoApp:

  • CpDigestTab (Selects iOS/Android version depending on client device).
  • IosDigestTab (iOS version)
  • AndroidDigestTab (Android version)

There was a temptation to abstract this more to CpTab with platform specific tab builders, but that would limit the implementations, so sticking with the ‘Simplest’ thing for now, until we know more, have built more tabs.

We are now little bit closer to having a native Android & iOS experience with a shared code base and good test coverage that will let us improve and optimise the code as we add features.

Ta Da

List recipes on Android & Apple phones
List recipes on Android & Apple phones
Display recipe on Android & Apple phones
Display recipe on Android & Apple phones
Android feature test report
Android feature test report

Next

UI Flow

We will be taking a look at the flow when a user starts the application and thinking about when to display the different start screens:

  • Splash
  • Login & Sign up.
  • Onboarding (Help on how to use the app, would be nice)
  • Dashboard

Shortcuts

New section, try to use a new shortcut once a week to make life easier, here are a few for Visual Studio Code on a Mac:

? + P Find/Open a file by name.
? + ? + P Run a command

[? + ? + Folds the innermost uncollapsed region

[? + ? + ]]() Unfolds the collapsed region at the cursor

? + + (K => 0) Folds all regions

? + + (K => J) Unfolds all regions

BackBurner

Native Android experience

We have only just started to implement a native Android UI by having a side draw menu, there is lots more to come as we tack more of the items like the floating action button detailed in iOS vs. Android App UI Design: The Complete Guide.

Add a Recipe on Android

Will add a feature test and Android implementation at a later date.

Api feature tests

The feature tests are just against the UI with a stubbed data source, we will need more feature tests to run against the API when we work on data access.

flutter_gherkin

Report on failed feature tests.

The Json reporter does not write the report when any of the feature tests fail, this needs to be fixed so we can report on test failures.

Inject device into the Tag Builder

If we can inject the device into the tag builder we can filter out device specific, tests, just difficult this early in the chain, needs research. Work around for now is to just make them pass if run on the wrong device.

Device specific feature test step workaround
Device specific feature test step workaround

Emulators

Extend the command line tools to automate running Android emulators.

For now you can just use the launch target to run it.

Launch agent to run Andriod Pixel emulator.
Launch agent to run Andriod Pixel emulator.

XP

Cross Platform Widgets

The key to cross platform work is to raise state up in the providers and then have widgets and adapters to build platform specific UI.

Cross platform widgets are now prefixed with ‘cp’ and they select the native implementation based on the device platform.

Cross platform widget - Bottom tab layout.
Cross platform widget – Bottom tab layout.
/*
  Cross platform Bottom Tab Layout, switches out the bottom tab layout dependent on the device platform e.g. Ios, Android...
*/

// Flutter imports:
import 'package:digestableme/model/config/platform.dart';
import 'package:digestableme/model/tab/tab_item.dart';
import 'package:digestableme/widget/android/tab/android_tabbed_layout.dart';
import 'package:digestableme/widget/ios/tab/ios_tabbed_layout.dart';
import 'package:flutter/material.dart';

// Package imports:
import 'package:provider/provider.dart';


class CpBottomTabLayout extends StatelessWidget {
  final List<TabItem> tabItems;
  CpBottomTabLayout(this.tabItems, {Key? key}) : super(key: key) {
    assert(tabItems.length > 1);
    assert(tabItems.length < 6);
  }

  @override
  Widget build(BuildContext context) {
    var platform = Provider.of<Platform>(context);

    if (platform.isIos) {
      return IosTabbedLayout(tabItems);
    }

    return AndroidTabbedLayout(tabItems);
  }
}

When the number of cross platform widgets increases we can build a storybook of the widgets with design and usage examples.

Theme Changes

Theme files
Theme files

To cater for both the Cupertino and Material themes I moved the state up into a three classes:

  • ATheme
  • ATextTheme
  • AColorTheme

the’A’ prefix is purely too different from framework names.

I then change the theme files for light, dark, Whitelabels to use them rather than the Cupertino theme classes.

The I wrote a couple of Adapters as extensions to convert the theme classes into framework themes, CupertinoThemeData and ThemeData.

Thats good enough for now, but there is still a little discord between the classes and design system which will need clarification, shoring up.

Feature tests – Platforms and devices.

It took me a long time to settle on only having a single set of integration tests that can run on all devices.

I made the decision when device targeted feature tests started to get really complicated and required multiple launch agents rather than the standard one added by the Flutter extension.

Launch agent - Flutter integration tests
Launch agent – Flutter integration tests

The down side of a single set is it is hard to exclude device specific tests:

Gherkin ‘Tags’, partly solved that but not completely.

Platform support feature
Platform support feature
Tag builder function
Tag builder function

I cannot find a way yet to get the device information before the tests are filtered.

So it’s not perfect, but simple enough to work with for now.

Where I can’t filter out a device specific test, I simply make it pass when run on another device, which is information we have once the test is running.

Launch Agent - iPhone cucumber report
Launch Agent – iPhone cucumber report
Launch Agent - iPhone feature tests
Launch Agent – iPhone feature tests

flutter_gherkin

Automation

My first change was to automate the running and reporting. To do this I added a command line project under a new /tools directory and integrated the running of commands into launch.json using the ‘F5 Anything’ extension. It’s named dt.dart which stands for dart tools.

Debugging

You can set break points and debug the feature tests and code, to do this I added a new launch targets:

Launch Agent - iPhone debug feature tests
Launch Agent – iPhone debug feature tests
iPhone debug feature launch agent code
iPhone debug feature launch agent code
Debugging a feature step
Debugging a feature step

Gotcha

You need to specify where to find the features twice in the suite, once for the builder(featurePaths:) and once for the runner(..features =).

Flutter Gherkin config gotcha
Flutter Gherkin config gotcha

Speed

When developing the speed the tests run is critical, once they become too slow to run you stop using them to develop test first, to get feed back from the new code and that’s a big problem.

To some degree you can reduce the number of tests that run during development and run the full suite on integration but eventually you will still face the same issue.

There are two parts of running the integration tests that take significant time.

Launching the iPhone simulator took 7.11 seconds and building the project for the iPhone took 22.2 seconds and that will increase as the project grows in size.

Whilst the tests only took 2.52 seconds to run.

Feature test speed
Feature test speed

I thought I was snookered until I released that you can just launch the project once and then rerun many times.

So for development open the iPhone simulator and then use the new launch target ‘iPhone Debug Features’. This will take about 30 seconds to run but once it has your can just restart the project to rerun the tests without a full rebuild.

Regenerating the features after a wording change takes a bit of time, but if your are careful to only set it up once you have understood and agreed the requirements it will be a fairly limited activity.

Feature test step generation
Feature test step generation

Cucumber Report

There are two new launch targets for each platform that can be used to generate and view the cucumber report for the feature tests.

Launch Agent - iPhone cucumber report
Launch Agent – iPhone cucumber report
Launch Agent - iPhone feature tests
Launch Agent – iPhone feature tests

The first just generates the report from the last test run and the latter does the job lot, opens the simulator, runs the feature tests and then generates the report.

Automating commands

I added a command line project dt.dart under a new /tools directory and integrated the running of commands into launch.json using the ‘F5 Anything’ extension.

F5 Anything Visual Code packge
F5 Anything Visual Code packge

That gives us plenty of options to automate the running of commands, if they are simple just use F5 directly

F5 Launch agent code
F5 Launch agent code

If they are more complicated or composite you can extend the command line tooling and then run it using ‘F5’.

dt.dart commands

Just open a command prompt at the root of the project and use these commands:

// Refresh features

dart run tools/dt/bin/dt.dart -r /Users/simbu/Mobile/flutter/digestableme

// Run feature tests and generate/show cucumber report:

dart run tools/dt/bin/dt.dart -f /Users/simbu/Mobile/flutter/digestableme B7A5F50E-DA94-49D0-913F-698AF846365D

// Generate/Show cucumber report:

dart run tools/dt/bin/dt.dart -c /Users/simbu/Mobile/flutter/digestableme B7A5F50E-DA94-49D0-913F-698AF846365D

The guid is the simulator device id (uuid).

The easiest way to find out a uuid is to run the simulator and then at a terminal prompt, type:

flutter devices
Cmd to list devices
Cmd to list devices

If the process fails or hangs then try the following to find the problem:

//Check that the project builds and runs
flutter run

//Check the tests run
flutter drive --driver=test_driver/integration_test_feature_driver.dart --target=integration_test/gherkin_suite_test.dart             

Testing network images

Added an extension to allow network images requests to be stubbed out for unit and widget tests where the HTTP client is not available.

ORIGINAL:         Image.network(recipe.thumbnail),
WITH EXTENSION:   context.networkImage(recipe.thumbnail)

Relies on EnvironmentMode being set in the <StartUp> provider.

Sound & Vision

Squeeze - Spot the Difference
Squeeze – Spot the Difference
Florence + the Machine - Dance Fever
Florence + the Machine – Dance Fever

Links

Please follow and like us:

Leave a Reply

Your email address will not be published.