Performant cross-platform development using Flutter

A view on Flutter's motion, rendering, performance and web interop.

Hej, jeg er Den med fletning | The one with the braid

  • Dart developer in healthcare
  • [matrix]
  • Interested in FreeBSD, Linux, RISC architectures
  • Started Flutter back in 2018

Hvad er Flutter?

  • Dart platform
  • Flutter engine
  • Foundation library
  • Design-specific widgets
  • Dev tools

Why Dart & Flutter

  • natively compiles -> performant
  • single code base
  • adopts to platform
  • development experience
    • getting rid of legacy patterns

History of Dart & Flutter

  • Dart - initially released 2011
  • Aimed to replace JS with a type safe language
    • Dart JS interop still part of Dart core eight years later
    • UX nightmare
    • semi-implemented API
    • everything type dynamic -> we got JS with Dart syntax uwu
    • Absolutely failed
  • Flutter announced in 2015
  • More and more platforms added

Flutter heads-up

Flutter at a glace

  • Mostly UI framework
  • UI elements called Widget
  • Widgets nested into tree
  • Shipping high-level Widgets and basic layout Widgets

nuqneH tera'gngpu' - the Klingon hello world

 

void main() {
  runApp(const Text('Heghlu\'meH QaQ jajvam.'));
}

StatelessWidget

  • immutable rendering object
  • completely built around given surrounding parameters
    • constraining layout
    • constructor parameters
  • state inherited from next stateful widget

StatefulWidget

  • mutable, modifiable rendering object
  • rendered based on own and surrounding parameters
    • state change call
    • layout change
      • user-defined: constructor changes
  • state stored in a mutable companion class
  • capable of "self modification"

Beautiful and performant code

  • use stateless widgets wherever possible
  • check didUpdateWidget in order to minimize builds
  • address state from properly scoped state container / controller
  • use Keys to avoid unnecessary re-builds

Useful patterns

go_router

  • page routing
  • navigation
  • hooks into MaterialApp.router -> navigator provider
  • extensible
    • transitions
    • page builder
final GoRouter _router = GoRouter(
  routes: <RouteBase>[
    GoRoute(
      path: '/',
      builder: (BuildContext context, GoRouterState state) {
        return const HomeScreen();
      },
      routes: <RouteBase>[
        GoRoute(
          path: 'details',
          builder: (BuildContext context, GoRouterState state) {
            return const DetailsScreen();
          },
        ),
      ],
    ),
  ],
);

return MaterialApp.router(
  routerConfig: _router,
);

Storage

  • JSON is not a storage.

Storage solutions for Dart & Flutter

  • SQLite - the boring one
  • Hive - the native one
  • Isar - the rusty one
  • Shared Preferences - JSON is not a storage.

Hive - native Dart object store

  • type adapters
  • listenable
  • different backends
    • binary writer
    • Indexed DB
    • web worker
  • encryption
import 'package:hive/hive.dart';

part 'person.g.dart';

@HiveType(typeId: 1)
class Person {
  @HiveField(0)
  String name;

  @HiveField(1)
  int age;

  @HiveField(2)
  List<Person> friends;
}

How does rendering work under the hood?

Graphics from: https://docs.google.com/presentation/d/1cw7A4HbvM_Abv320rVgPVGiUP2msVs7tfGbkgdrTy0I/edit#slide=id.gbb3c3233b_0_162

Optimizing performance

Key feature : Keys

  • Communicating StatefulWidget to preserve State
  • Prevent rebuilding on StreamBuilder
  • Useful for animations (see animations section)

Caution with StreamBuilder

  • Never run a ListView.builder against a StreamBuilder
  • Use either Keys or AnimatedList / AnimatedSliverListDelegate

Animations and backdrops

  • Avoid Video and GIF/APNG (yes, finally supported !)
  • Shader performance awful on web
  • Use package:animations
  • Keys again

Web only : initial load

  • Deferred libraries (e.g. minimal splash screen)
  • Use browser-optimized CanvasKit
  • Defer in small pieces
  • Outsource to web workers

Performant animations

Implicit Animation

  • Value
  • Duration
  • Curve

Used for simple animations, low code effort

Curves

      Center(
        child: AnimatedContainer(
          width: selected ? 200.0 : 100.0,
          height: selected ? 100.0 : 200.0,
          color: selected ? Colors.red : Colors.blue,
          alignment:
              selected ? Alignment.center : AlignmentDirectional.topCenter,
          duration: Duration(seconds: 2),
          curve: Curves.fastOutSlowIn,
          child: FlutterLogo(size: 75),
        ),
      ),
        Center(
          child: TweenAnimationBuilder<double>(
            tween: Tween<double>(begin: 0, end: 2 * math.pi),
            duration: Duration(seconds: 2),
            builder: (BuildContext context, double angle, Widget child) {
              return Transform.rotate(
                angle: angle,
                child: Image.asset('assets/Earth.png'),
              );
            },
          ),
        ),

Explicit Animation

  • Ticker
  • Build for every single frame

Used for complex animations, high performance impact

Recepie

  • SingleTickerProviderStateMixin
  • AnimationController
  • addListener
  • setSate
class _MyWidgetState extends State<MyWidget>
    with SingleTickerProviderStateMixin<MyWidget> {
  
  AnimationController? _controller;
  
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this);
    _controller?.addListener(_update);
  }
  
  void _update() {
    setState(() {
      // TODO
    });
  }
  
  @override
  Widget build(BuildContext context) {
  	print('I\'m re-built very often.');
    return Container();
  }
}
class _MyWidgetState extends State<MyWidget>
    with SingleTickerProviderStateMixin<MyWidget> {
  
  AnimationController? _controller;
  
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this, 
      duration: const Duration(seconds: 1),
    );
    _controller?.addListener(_update);
    _controller?.forward();
  }
  
  @override
  void dispose() {
    _controller?.dispose();
    super.dispose();
  }
  
  void _update() {
    setState(() {
      // TODO
    });
  }
  
  @override
  Widget build(BuildContext context) {
    prin('Still getting re-built...')
    return Container();
  }
}

AnimatedBuilder

  • AnimationController
  • Builder

Used for custom animations, easy to implement


    _controller = AnimationController(
      duration: const Duration(seconds: 10),
      vsync: this,
    )..repeat();
    
    // [...]

	return AnimatedBuilder(
      animation: _controller,
      child: Container(
        width: 200.0,
        height: 200.0,
        color: Colors.green,
        child: const Center(
          child: Text('Whee!'),
        ),
      ),
      builder: (BuildContext context, Widget child) {
        return Transform.rotate(
          angle: _controller.value * 2.0 * math.pi,
          child: child,
        );
      },
    );

Hero animations

  • Hero class
  • mostly known from FAB

Used for simple animations, easy to implement

Hero transitions

Container(
	child: Hero(
    	tag: "SomeObject",
    	child: whatever    
    )
)

// [...]
//
// On any other page
//

Card(
	child: Hero(
    	tag: "SomeObject",
    	child: whatever    
    )
)

`animations` package

  • Prebuild animations
  • Common cases

Used for UI transitions, easy to implement

Container transform

    return OpenContainer<bool>(
      transitionType: transitionType,
      openBuilder: (BuildContext context, VoidCallback _) {
        return const _DetailsPage();
      },
      onClosed: onClosed,
      tappable: false,
      closedBuilder: closedBuilder,
    );

Axis transitions

Types

  • SharedAxisPageTransitionsBuilder
  • SharedAxisTransition

Flutter - the bad, the ugly

  • Engine build - not possible without BLOBs
  • Your free Google Fonts connection by CanvasKit

Introduction into Dart JS interop

Cursed part, powered by an alpaca horse

A journey into deep insights of Dart you actually don't want to know about.

  • Interops any Dart into JS
  • null safety LOL
  • syntax lol

History of Dart

  • Replace JS with a type safe language -> failed
  • Dart JS interop still part of Dart core eight years later
  • UX nightmare
  • semi-implemented API
  • everything type dynamic -> we got JS with Dart syntax uwu

Use cases for Dart JS interop

  • Expose APIs to embedding website
  • Accessing native JS APIs in Flutter / Dart apps
  • Writing pure Dart web apps
  • Flutter's web engine

JS & util JS

  • exposing links between Dart and JavaScript runtime
  • Examples include:
    • jsify
    • allowInterop
    • constructor calls
    • promiseToFuture
    • access type unsafe JS objects in an array API
final completer = Completer<MyType>();
final promiseProto = getProperty(window, 'Promise');
final promise = callConstructor(
  promiseProto,
  jsify([
    allowInterop((resolve, reject) {
      completer.future.then((value) => resolve(value));
    })
  ]),
);
completer.complete(MyFuture());
return promise;
 

Flutter testing

Kinds of tests

  • Widget tests
  • Golden tests
  • Unit tests
  • Integration tests
  • Plugin tests
  • Code coverage tests

Testing in common

  • Dart has a pretty advanced built-in test framework
  • Clean environment
    • Initial state mockable
    • Always reproducible device state
  • Tests run in dedicated isolate from application
  • Tests run in Dart VM / browser
    • Flutter plugins require native tests
    • No canonical way for Flutter native device tests
    • See Test Drivers later
  • Mock implementations for APIs

Unit, widget and integration tests in common

  • "Pumping" widget or app
  • Mocking required APIs
    • Alternatively, connect to test services for sure
  • simple find-expect syntax

Golden tests

  • Ensures UI components' appearance
  • Matches against abstracted bitmap
  • Uses abstracted platform specific UI
  • Easy to use interface
  • Can be generated using a single command

Testing the web

  • Browsers have no canonical debug protocol
  • Web drivers it is
  • Flutter takes test driver running web driver as argument

Useful links

  • https://pub.dev/packages/mockito
  • https://github.com/flutter/flutter/wiki/Running-Flutter-Driver-tests-with-Web#examples-from-flutter-project
    • https://github.com/flutter/web_installers/tree/master/packages/web_drivers/lib
  • https://pub.dev/packages/coverage
  • https://github.com/flutter/flutter/wiki/Plugin-Tests

Questions left?

Flutter

By The one with the braid