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
Flutter
- 519