Flutter – Lottie Animation

A picture paints an thousand words, an animation more…

A Lottie is a JSON-based animation file format that enables designers to ship animations on any platform as easily as shipping static assets. They are small files that work on any device and can scale up or down without pixelation. https://lottiefiles.com

I want to start with a typical case where we are waiting for something to load, in this case the animation will repeat and let the user know we are working on their request.

I’ve abstracted this into a new widget in our DigestableLego library called DlLottie.

In order to allow designers to test the look and feel of any Lottie animation, I’ve added a Knob to the Widgetbook use case that accepts a Json string.

Lottie Json Knob
Lottie Json Knob

If you are new to Widgetbook you can find links to the posts and a quick start guide below.

Ta Da

Lottie Animation Widget
Lottie Animation Widget

Xp

Quick Start

Just download the component library DigestableLego.

Open in Visual Studio code and run the following two launch agents Visual Studio Launch.

Builder launch agent
Builder launch agent
Widgetbook Launch Agent
Widgetbook Launch Agent

Then download any Lottie Json files https://lottiefiles.com and just copy the contents and paste it into the knob:

Lottie Json Knob
Lottie Json Knob

Links

Flutter – Widgetbook (Storybook) – Part 2

Widgetbook (Storybook) – Part 1

Just a follow-up to part 1 to talk about Knobs.

Knobs allow the users of WidgetBook to supply their own values and see the changes the values make to the widgets displayed.

For the list widget I added knobs to allow changes to list item title, subtitle and selected image.

Ta Da

Knobs to specify Title, subtitle and image for a list item.
Knobs to specify Title, subtitle and image for a list item.
Tabbed Layout
Tabbed Layout

BackBurner

I only moved one more widget iPhone tabbed layout widget to DigestableLego.

You can already see the difference, DigestableMe is getting simpler to use again. I will those remaining over the course of the next few blogs, it just takes time to do it right.

Xp

Knobs

.knobs is an extension on the build context:

@WidgetbookUseCase(name: 'Avatar, Title and Subtitle', type: DlList)
Widget dlListAvatarTitleAndSubtitle(BuildContext context) {
  return buildItem(DlListItem(
    leading: DlAvatar(
      imageOptionsKnob(context),
      radius: 22,
    ),
    title: Text(context.knobs.text(label: 'Title', initialValue: "Title")),
    subtitle:
        Text(context.knobs.text(label: 'SubTitle', initialValue: "SubTitle")),
  ));
}

You can specify multiple images for the user to choose from:

Image imageOptionsKnob(BuildContext context) {
  return context.knobs.options(
    label: 'Logo',
    options: [
      Option<Image>(
        label: 'Flutter logo',
        value: Image.asset("images/flutter.png"),
      ),
      Option(
        label: 'Widgetbook logo',
        value: Image.asset("images/widgetbook_logo.jpeg"),
      )
    ],
  );
}

See the Widgetbook documentation for details.

Simplifying DigestableMe startup

Removing code, simplifying and finding clarity, always feels good, it’s something worth fighting for.

There is a tendency for complexity to creep in organically.

As part of the process of separating and moving widgets, I got the chance to clean up and strip away some complexity from application providers.

Application Providers before the clean up.
Application Providers before the clean up.
Application Providers after the clean up.
Application Providers after the clean up.

This puts us in a good position to start work on the application flow on startup.

Excluding unused import warnings on generated files

Turned out that the syntax to exclude a file from the analyser analysis_options.yaml was a comma separated list of files:

Exclude widgetbook generated file from the analyzer
Exclude widgetbook generated file from the analyzer

So back to no warnings now we have excluded the file generated by WidgetBook.

No problems in the project
No problems in the project

It is important to target no warnings or errors b4 checkin, once you let it slip then its harder to spot issue you introduce.

Shared Stuff

Some of the widgets I moved into DigestableLego used provider state e.g.

Providers in widgets
Providers in widgets

Too avoid the issue of creating shared classes required by both DigestableMe and DigestableLego I removed the provider state and added the necessary information to the widget constructors.

Widget data now in the constructor
Widget data now in the constructor

It then meant that I need additional mapping code in DigestableMe to map its types to those in DigestableLego:

New mapping files
New mapping files

Which are just simple extension methods on the types in DigestableMe.

extension TabItemMap on List<TabItem> {
  List<DlTabItem> toDlTabItems() {
    return map((item) => DlTabItem(
          label: item.label,
          icon: item.icon,
          content: item.content,
          globalKey: item.globalKey,
        )).toList();
  }
}

Now we have a new rule in the Done Done Done checklist.

No provider state in widgets, pass in the required info on the widget constructor

As part of this I removed provider: from pubspec.yaml in DigestableLego.

Removing the shared state simplifies the widget development and widget tests.

There will be more to come on this but we just need to avoid coupling things together, we don’t want to create monoliths.

The whole point of microservices is that they are independent. You can create a service that has countries (for example so that you can display a list of available countries), but I would avoid binding that service to your other ones. Copy the country information into those other services. This allows these services to evolve independently. For example, if a person has nationalities that are at a finer grain model than buildings country (because subtleties like that really happen in the real world).

Services can call other services, coarser business services should caller finer-grained ones, but do not make a network, make sure that you strongly define your hierarchy, and make high-level services call low-level ones. I don’t think “Building” or “Person” services are those coarse-grained services, you probably want a layer above them that is integrating those with preferences. Building and Person probably should be on their own, but Microservices design is not an exact science, and a lot depends on your situation. If User Preferences is deeply integrated into the behavior of Person, it might make sense to make Person your coarse-grained service. Just make sure that it’s an explicit decision, and you don’t do something horrible like make them co-dependent on each other.

Finally, do not create a “Shared Stuff” assembly. It will become your Monolith in very short order, and will bind everything together into an unruly mess. You will end up with all the disadvantages of a monolith, combined with all the overhead of microservices. Rob Conklin

Sound & Vision

Moby Essentials
Moby Essentials

Power is not shared, power is taken

Links

Flutter – Widgetbook (Storybook)

Component driven design is a great way to safe guard your brand/s and make it easy to quickly develop new screens and UI experiences from existing components, or to build new ones.

It was made popular by Storybook.js for good reasons:

  • Prevents the same components from been rebuilt in different parts of the UI.
  • Provides a toolbox of components, a bit like Lego.
  • Components can be built and modified in isolation so they are not affected by flakey date, business logic or unfinished UI’s
  • Makes it easy to focus on difficult use cases
  • Allows for collaboration with the design team and other stakeholders

Flutter is less mature than React and there is no clear alternative to storybook.

After a little research I selected Widgetbook because it is been actively developed and used.

Flutter gems storybook

Widgetbook
Widgetbook

And followed their instructions set it up in the new components project DigestableLego.

Ta Da

Widgetbook in action

Xp

Widgetbook Approach

I decide to use the widget book generator rather than the manual approach to avoid having to setup:

  • Categories
  • Folders
  • Widgets
  • Use cases
Widgetbook Manual Configuration Example
Widgetbook Manual Configuration Example

When you use the generator you just need to write the use cases and annotate them.

@WidgetbookUseCase(name: 'List', type: DlList)

There is a little more upfront, you need to add config for the Widgetbook application and for themes using two other annotations:

  • @WidgetbookApp
  • @WidgetbookTheme

Widgetbook Setup

I added Widgetbook to the /example project because it needs the macOS applications files to run and it meant that the examples created by the package code be reused by the scenarios.

I started by adding the package references for the generator recommend by the Widgetbook.

Then the App widget and annotated it with @WidgetbookApp.

// Flutter imports:
import 'package:flutter/cupertino.dart';

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

@WidgetbookApp.cupertino(
  name: 'Digestable Lego',
  devices: [
    Apple.iPhone13Mini, Apple.iPhone13, Apple.iPhone13Pro, Apple.iPhone13ProMax
  ],
  textScaleFactors: [
    1,
    2,
    3,
  ],
  foldersExpanded: true,
  widgetsExpanded: true,
)
class CupertionApplication extends StatelessWidget {
  const CupertionApplication({Key? key}) : super(key: key);

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

And the first use case:

// Flutter imports:
import 'package:flutter/cupertino.dart';

// Package imports:
import 'package:digestable_lego/widget/dl_list.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart';

// Project imports:
import 'package:example/dl_list.dart';

@WidgetbookUseCase(name: 'List', type: DlList)
Widget listItemRecipe(BuildContext context) {
  return const CupertinoPageScaffold(
    navigationBar: CupertinoNavigationBar(
      middle: Text(
        "Example List Items",
      ),
    ),
    child: DlListExample(),
  );
}

Next I ran the builder:

flutter pub run build_runner build

// Optionally to automatically rerun when files change
flutter pub run build_runner watch

That generated the .widgetbook.dart file.

Widgetbook files
Widgetbook files

Then just run it:

flutter run -t lib/widgetbook/cupertino_application.widgetbook.dart -d macos

To make this step easier I added it as a new launch agent.

Widgetbook launch agent
Widgetbook launch agent

Widgetbook Themes

To support themes in widget book I had moved them to the DigestableLego package, they could potentially become their own package if the code and assets grows.

Upgrade a Git package

Whenever we make changes to DigestableLego and want to use those changes in DigestableMe, we need to update the package from the git repository.

To do this run

flutter pub upgrade digestable_lego
pub upgrade Digestable Lego
pub upgrade Digestable Lego

BackBurner

MaterialApp support for Widgetbook

The Widgetbook application runs widgets in a CupertinoApp, you get to choose as part of the application config.

@WidgetbookApp.cupertino

I would like a version of Widgetbook for Android devices, Ideally widget book would allow a dynamic switch of application types.

There does not seem an easy way to make @WidgetbookTheme work with both Material and Cupertino Apps in the same application so even if I have two @WidgetbookApp configurations each picks up all the @WidgetbookTheme themes and then one configuration complains that the ThemeData is the wrong type.

Excluding unused import warnings on generated files

Unable to ignore unused import warnings for the widget book generated file cupertino_application.widgetbook.dart

I was able to exclude other linting rules in the analysis_options.yaml file.

ignore:
      - lib/widgebook/cupertino_application.widgetbook.dart

Sound & Vision

One flew over the cuckoo's nest

Links

Flutter – Packages

The number of files in the project has grown to a point where it is more difficult to work with. I know because I now need to use ? + P to find files rather than the file explorer.

It now makes sense to start to break up the solution into packages to help make it easier to understand and maintain, and help to ensure each part has a single responsibility.

I started by pulling out the UI widgets as a precursor for creating a storybook to show case them to the stakeholders.

To do this I created a new project called DigestableLego and hosted it on GitLab.

I then moved in the Listview and Listitem widgets, built the package and included it back in DigestableMe Application.

Ta Da

Digestable Lego: List item examples
Digestable Lego: List item examples
Using the DigestableLego list widget in the project.
Using the DigestableLego list widget in the project.

Xp

Creating and using a package

Create package.

flutter create --template=package digestablelego

Create a new Git repository.

git init
git add --all
git commit -m "initial commit"
git remote add origin https://gitlab.com/simbu-mobile/DigestableLego.git
git push -u origin master

Add code and examples to the project.

Check Package is ready to publish.

flutter packages pub publish --dry-run

Once the code passes the checks, check it in.

For now we are going to use it directly from the Git repository e.g.

dependencies:
  digestable_lego:
    git: https://gitlab.com/simbu-mobile/digestablelego.git

It is likely that in the future I move to a privately hosted pub.dev repository to get the version and update benefits. You can find more details here:

Application Types

We are now up to four:

  • MaterialApp
  • CupertinoApp
  • FluentApp
  • MacosApp

They reflect the different design systems/guidelines, Material on Android, Google, Linux and the Web, Cupertino on iOS, Fluent on windows and MacOs on macs.

Currently we are only targeting iOS & Android and extending to the web, so that narrows it down to two:

  • MaterialApp
  • CupertinoApp

For simplicities sake I’ve now started to move away from device detection and use the application type to differentiate in the widgets that require cross platform support.

import 'package:flutter/cupertino.dart';

extension BuildContextExtension on BuildContext {
  bool isCupertinoApp() {
    var cupertinoApp = findAncestorWidgetOfExactType<CupertinoApp>();
    return cupertinoApp != null;
  }
}

// And usage in the cross platform widget

return context.isCupertinoApp()
              ? const CupertinoListTile()
              : const ListTile();

NB: findAncestorWidgetOfExactType is powerful stuff and has a number of uses.

Testing & Development Rhythm

Packaging the UI widgets has given me a moment of clarity with regard to test types.

I would now expect widget and unit tests in the DigestableLego project and feature and unit tests in the DigestableMe project.

Whilst there could possible be the need for widgets tests in DigestableMe it is unlikely, so we shall see how that pans out.

Just looking to build a development rhythm e.g. new UI, build widgets in DigestibleLego and showcase, agree on look and feel and style. Then move to API data side calls and mapping etc… Finally run through the Dev done checklist. More on this later on.

Sound and Vision

Better call Saul (Netflix)
Better call Saul (Netflix)

Links