Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Flutter at scale

Jorge Coca
August 27, 2019

Flutter at scale

Flutter has been out of beta for less than a year. It is an amazing framework UI that lets you run your code pretty much anywhere. There's a lot of expectations around it, incredible apps running at 60fps, but... is it ready to be adopted at scale? At BMW, we faced that question a while ago, and the answer was YES! We tried it, we loved it, and we collected all the necessary information to prove that Flutter was the right choice for us. In this talk, let me show you our process to make Flutter our preferred tool for developing applications, and how we enable multiple teams around the world to deliver incredible experiences fast and safely.

Jorge Coca

August 27, 2019
Tweet

More Decks by Jorge Coca

Other Decks in Programming

Transcript

  1. Before we start Our Journey to Flutter - Android Summit

    2019 - @jcocaramos - Droidcon NYC 2019
  2. Business and design goals We want to be able to

    regularly release our products across all brands, all platforms, and all regions simultaneously, with the same feature capabilities @jcocaramos - Droidcon NYC 2019
  3. Engineering goals Create a platform that is developer friendly, developer

    scalable, and performant, which provides safe experimentation and continuos deployment. @jcocaramos - Droidcon NYC 2019
  4. Release regularly → Train releases: every X weeks, whether you

    are ready or not → Trunk-based development: all code is on master @jcocaramos - Droidcon NYC 2019
  5. All brands, regions, and platforms → BMW, BMWi, MINI, Rolls

    Royce → Global presence, but different legal/market requirements → One single Flutter codebase, multiple apps with different design libraries @jcocaramos - Droidcon NYC 2019
  6. Same feature capabilities → Feature flags to support different integrations,

    markets, and configurations → Cloud centric: reduce number of decisions on mobile, it is just a rendering client @jcocaramos - Droidcon NYC 2019
  7. Developer friendly → Tests. Tons of tests. → Consistent coding

    style → Tech debt is addressed soon @jcocaramos - Droidcon NYC 2019
  8. Developer scalable → Assume 1 to N developers → Natural

    onboarding → Keep it simple! @jcocaramos - Droidcon NYC 2019
  9. Performant → 60fps → 99.9% crash free → Measure, analyze,

    improve → Tests, tests, tests → Easy to identify where are the problems @jcocaramos - Droidcon NYC 2019
  10. Safe experimentation → Feature flags, A/B → Complexity out of

    the client → Predictable APIs, natural language → Variants are forced to be tested on integration @jcocaramos - Droidcon NYC 2019
  11. Continuos deployment → Merge a commit, you are in production!

    → Same philosophy on backend and client → Percentage rollout → Monitoring, alerting, and remote debugging @jcocaramos - Droidcon NYC 2019
  12. Our tools → High level architecture → flutter_bloc (@felangelov) →

    lumberdash (@bmw-tech) → unit, widget, and integration tests → ozzie (@bmw-tech) @jcocaramos - Droidcon NYC 2019
  13. High level architecture → Vertical modules (data, domain, UI &

    state) → Horizontal modules (feature composition) @jcocaramos - Droidcon NYC 2019
  14. flutter_bloc A predictable state management library that helps implement the

    BLoC design pattern @jcocaramos - Droidcon NYC 2019
  15. UI = f (state) → UI is the layout of

    the screen → f is the build method of your widgets → state is the different scenarios that your application can handle @jcocaramos - Droidcon NYC 2019
  16. States Pure Dart classes used to describe a particular moment

    in the life cycle of the application. States can be tied to a UI screen (counter value) or not (user is authenticated) int counter; @jcocaramos - Droidcon NYC 2019
  17. UI Events Pure Dart classes (POJOs) used to describe actions

    triggered from the system/user enum CounterEvent { increment, decrement, } @jcocaramos - Droidcon NYC 2019
  18. bloc Pure Dart class that receives events, and produces states.

    → Can be used in other Dart projects as well (Angular, CLI...) → Takes repositories as dependencies → Provides an initialState and a mapEventToState @jcocaramos - Droidcon NYC 2019
  19. flutter_bloc → Flutter widgets to build, provide, and manage bloc

    dependencies in the widget tree → There's a version for AngularDart @jcocaramos - Droidcon NYC 2019
  20. class CounterBloc extends Bloc<CounterEvent, int> { @override int get initialState

    => 0; @override Stream<int> mapEventToState(CounterEvent event) async* { switch (event) { case CounterEvent.decrement: yield currentState - 1; break; case CounterEvent.increment: yield currentState + 1; break; } } } @jcocaramos - Droidcon NYC 2019
  21. class CounterBloc extends Bloc<CounterEvent, int> { @override int get initialState

    => 0; @override Stream<int> mapEventToState(CounterEvent event) async* { switch (event) { case CounterEvent.decrement: yield currentState - 1; break; case CounterEvent.increment: yield currentState + 1; break; } } } @jcocaramos - Droidcon NYC 2019
  22. class CounterBloc extends Bloc<CounterEvent, int> { @override int get initialState

    => 0; @override Stream<int> mapEventToState(CounterEvent event) async* { switch (event) { case CounterEvent.decrement: yield currentState - 1; break; case CounterEvent.increment: yield currentState + 1; break; } } } @jcocaramos - Droidcon NYC 2019
  23. class CounterBloc extends Bloc<CounterEvent, int> { @override int get initialState

    => 0; @override Stream<int> mapEventToState(CounterEvent event) async* { switch (event) { case CounterEvent.decrement: yield currentState - 1; break; case CounterEvent.increment: yield currentState + 1; break; } } } @jcocaramos - Droidcon NYC 2019
  24. Flutter class CounterApp extends StatelessWidget { @override Widget build(BuildContext context)

    { return MaterialApp( title: 'Flutter Demo', home: BlocProvider<CounterBloc>( builder: (context) => CounterBloc(), child: CounterPage(), ), ); } } @jcocaramos - Droidcon NYC 2019
  25. Flutter class CounterApp extends StatelessWidget { @override Widget build(BuildContext context)

    { return MaterialApp( title: 'Flutter Demo', home: BlocProvider<CounterBloc>( builder: (context) => CounterBloc(), child: CounterPage(), ), ); } } @jcocaramos - Droidcon NYC 2019
  26. class CounterPage extends StatelessWidget { @override Widget build(BuildContext context) {

    final CounterBloc counterBloc = BlocProvider.of<CounterBloc>(context); return Scaffold( appBar: AppBar(title: Text('Counter')), body: BlocBuilder<CounterBloc, int>( builder: (context, count) { return Text('$count'); }, ), floatingActionButton: Column( children: [ FloatingActionButton( child: Icon(Icons.add), onPressed: () { counterBloc.dispatch(CounterEvent.increment); }, ) FloatingActionButton( child: Icon(Icons.remove), onPressed: () { counterBloc.dispatch(CounterEvent.decrement); }, ), ], ), ); } } @jcocaramos - Droidcon NYC 2019
  27. class CounterPage extends StatelessWidget { @override Widget build(BuildContext context) {

    final CounterBloc counterBloc = BlocProvider.of<CounterBloc>(context); return Scaffold( appBar: AppBar(title: Text('Counter')), body: BlocBuilder<CounterBloc, int>( builder: (context, count) { return Text('$count'); }, ), floatingActionButton: Column( children: [ FloatingActionButton( child: Icon(Icons.add), onPressed: () { counterBloc.dispatch(CounterEvent.increment); }, ) FloatingActionButton( child: Icon(Icons.remove), onPressed: () { counterBloc.dispatch(CounterEvent.decrement); }, ), ], ), ); } } @jcocaramos - Droidcon NYC 2019
  28. class CounterPage extends StatelessWidget { @override Widget build(BuildContext context) {

    final CounterBloc counterBloc = BlocProvider.of<CounterBloc>(context); return Scaffold( appBar: AppBar(title: Text('Counter')), body: BlocBuilder<CounterBloc, int>( builder: (context, count) { return Text('$count'); }, ), floatingActionButton: Column( children: [ FloatingActionButton( child: Icon(Icons.add), onPressed: () { counterBloc.dispatch(CounterEvent.increment); }, ) FloatingActionButton( child: Icon(Icons.remove), onPressed: () { counterBloc.dispatch(CounterEvent.decrement); }, ), ], ), ); } } @jcocaramos - Droidcon NYC 2019
  29. class CounterPage extends StatelessWidget { @override Widget build(BuildContext context) {

    final CounterBloc counterBloc = BlocProvider.of<CounterBloc>(context); return Scaffold( appBar: AppBar(title: Text('Counter')), body: BlocBuilder<CounterBloc, int>( builder: (context, count) { return Text('$count'); }, ), floatingActionButton: Column( children: [ FloatingActionButton( child: Icon(Icons.add), onPressed: () { counterBloc.dispatch(CounterEvent.increment); }, ) FloatingActionButton( child: Icon(Icons.remove), onPressed: () { counterBloc.dispatch(CounterEvent.decrement); }, ), ], ), ); } } @jcocaramos - Droidcon NYC 2019
  30. class CounterPage extends StatelessWidget { @override Widget build(BuildContext context) {

    final CounterBloc counterBloc = BlocProvider.of<CounterBloc>(context); return Scaffold( appBar: AppBar(title: Text('Counter')), body: BlocBuilder<CounterBloc, int>( builder: (context, count) { return Text('$count'); }, ), floatingActionButton: Column( children: [ FloatingActionButton( child: Icon(Icons.add), onPressed: () { counterBloc.dispatch(CounterEvent.increment); }, ) FloatingActionButton( child: Icon(Icons.remove), onPressed: () { counterBloc.dispatch(CounterEvent.decrement); }, ), ], ), ); } } @jcocaramos - Droidcon NYC 2019
  31. class CounterPage extends StatelessWidget { @override Widget build(BuildContext context) {

    final CounterBloc counterBloc = BlocProvider.of<CounterBloc>(context); return Scaffold( appBar: AppBar(title: Text('Counter')), body: BlocBuilder<CounterBloc, int>( builder: (context, count) { return Text('$count'); }, ), floatingActionButton: Column( children: [ FloatingActionButton( child: Icon(Icons.add), onPressed: () { counterBloc.dispatch(CounterEvent.increment); }, ) FloatingActionButton( child: Icon(Icons.remove), onPressed: () { counterBloc.dispatch(CounterEvent.decrement); }, ), ], ), ); } } @jcocaramos - Droidcon NYC 2019
  32. lumberdash Do you need logs? Lumberdash is the answer! void

    main() { putLumberdashToWork(withClient: SimpleClient()); logWarning('Hello Warning'); logFatal('Hello Fatal!'); logMessage('Hello Message!'); logError(Exception('Hello Error')); } @jcocaramos - Droidcon NYC 2019
  33. lumberdash Do you need logs? Lumberdash is the answer! void

    main() { putLumberdashToWork(withClient: SimpleClient()); logWarning('Hello Warning'); logFatal('Hello Fatal!'); logMessage('Hello Message!'); logError(Exception('Hello Error')); } @jcocaramos - Droidcon NYC 2019
  34. lumberdash Do you need logs? Lumberdash is the answer! void

    main() { putLumberdashToWork(withClient: SimpleClient()); logWarning('Hello Warning'); logFatal('Hello Fatal!'); logMessage('Hello Message!'); logError(Exception('Hello Error')); } @jcocaramos - Droidcon NYC 2019
  35. flutter_bloc ❤ lumberdash class CounterBlocDelegate extends BlocDelegate { final LumberdashClient

    lumberdashClient; CounterBlocDelegate(this.lumberdashClient); @override void onTransition(Transition transition) { super.onTransition(transition); lumberdashClient.logMessage(transition.toString()); } @override void onError(Object error, StackTrace stacktrace) { super.onError(error, stacktrace); lumberdashClient.logError(error, stacktrace); } } @jcocaramos - Droidcon NYC 2019
  36. flutter_bloc ❤ lumberdash class CounterBlocDelegate extends BlocDelegate { final LumberdashClient

    lumberdashClient; CounterBlocDelegate(this.lumberdashClient); @override void onTransition(Transition transition) { super.onTransition(transition); lumberdashClient.logMessage(transition.toString()); } @override void onError(Object error, StackTrace stacktrace) { super.onError(error, stacktrace); lumberdashClient.logError(error, stacktrace); } } @jcocaramos - Droidcon NYC 2019
  37. flutter_bloc ❤ lumberdash class CounterBlocDelegate extends BlocDelegate { final LumberdashClient

    lumberdashClient; CounterBlocDelegate(this.lumberdashClient); @override void onTransition(Transition transition) { super.onTransition(transition); lumberdashClient.logMessage(transition.toString()); } @override void onError(Object error, StackTrace stacktrace) { super.onError(error, stacktrace); lumberdashClient.logError(error, stacktrace); } } @jcocaramos - Droidcon NYC 2019
  38. Feature development is With these two simple tools, our feature/market

    teams can develop features with a consistent pattern, that is well documented, 100% testable, and provides automation for logs and analytics. @jcocaramos - Droidcon NYC 2019
  39. What do we do in the core team to ensure

    quality? @jcocaramos - Droidcon NYC 2019
  40. Tests, automation, and pipelines → Unit and widget tests with

    100% coverage → Integration tests to ensure 60fps at all times → Visual dependency graph with validation → Consistent lint and code formatting in every module → Project generators with Docker and pipelines @jcocaramos - Droidcon NYC 2019
  41. Ozzie With Ozzie, during integration tests, we can capture screenshots

    of every frame we want, and we can also measure the performance of a feature, breaking the build if necessary @jcocaramos - Droidcon NYC 2019
  42. void main() { FlutterDriver driver; Ozzie ozzie; setUpAll(() async {

    driver = await FlutterDriver.connect(); ozzie = Ozzie.initWith(driver, groupName: 'counter'); }); tearDownAll(() async { if (driver != null) driver.close(); ozzie.generateHtmlReport(); }); test('initial counter is 0', () async { await ozzie.profilePerformance('counter0', () async { await driver.waitFor(find.text('0')); await ozzie.takeScreenshot('initial_counter_is_0'); await driver.tap(find.byType('FloatingActionButton')); await driver.waitFor(find.text('1')); await ozzie.takeScreenshot('counter_is_1'); }); }); } @jcocaramos - Droidcon NYC 2019
  43. void main() { FlutterDriver driver; Ozzie ozzie; setUpAll(() async {

    driver = await FlutterDriver.connect(); ozzie = Ozzie.initWith(driver, groupName: 'counter'); }); tearDownAll(() async { if (driver != null) driver.close(); ozzie.generateHtmlReport(); }); test('initial counter is 0', () async { await ozzie.profilePerformance('counter0', () async { await driver.waitFor(find.text('0')); await ozzie.takeScreenshot('initial_counter_is_0'); await driver.tap(find.byType('FloatingActionButton')); await driver.waitFor(find.text('1')); await ozzie.takeScreenshot('counter_is_1'); }); }); } @jcocaramos - Droidcon NYC 2019
  44. void main() { FlutterDriver driver; Ozzie ozzie; setUpAll(() async {

    driver = await FlutterDriver.connect(); ozzie = Ozzie.initWith(driver, groupName: 'counter'); }); tearDownAll(() async { if (driver != null) driver.close(); ozzie.generateHtmlReport(); }); test('initial counter is 0', () async { await ozzie.profilePerformance('counter0', () async { await driver.waitFor(find.text('0')); await ozzie.takeScreenshot('initial_counter_is_0'); await driver.tap(find.byType('FloatingActionButton')); await driver.waitFor(find.text('1')); await ozzie.takeScreenshot('counter_is_1'); }); }); } @jcocaramos - Droidcon NYC 2019
  45. void main() { FlutterDriver driver; Ozzie ozzie; setUpAll(() async {

    driver = await FlutterDriver.connect(); ozzie = Ozzie.initWith(driver, groupName: 'counter'); }); tearDownAll(() async { if (driver != null) driver.close(); ozzie.generateHtmlReport(); }); test('initial counter is 0', () async { await ozzie.profilePerformance('counter0', () async { await driver.waitFor(find.text('0')); await ozzie.takeScreenshot('initial_counter_is_0'); await driver.tap(find.byType('FloatingActionButton')); await driver.waitFor(find.text('1')); await ozzie.takeScreenshot('counter_is_1'); }); }); } @jcocaramos - Droidcon NYC 2019
  46. ozzie.yaml integration_test_expectations: should_fail_build_on_warning: true should_fail_build_on_error: true performance_metrics: missed_frames_threshold: warning_percentage: 5.0

    error_percentage: 10.0 frame_build_rate_threshold: warning_time_in_millis: 14.0 error_time_in_millis: 16.0 frame_rasterizer_rate_threshold: warning_time_in_millis: 14.0 error_time_in_millis: 16.0 @jcocaramos - Droidcon NYC 2019
  47. ozzie.yaml integration_test_expectations: should_fail_build_on_warning: true should_fail_build_on_error: true performance_metrics: missed_frames_threshold: warning_percentage: 5.0

    error_percentage: 10.0 frame_build_rate_threshold: warning_time_in_millis: 14.0 error_time_in_millis: 16.0 frame_rasterizer_rate_threshold: warning_time_in_millis: 14.0 error_time_in_millis: 16.0 @jcocaramos - Droidcon NYC 2019
  48. ozzie.yaml integration_test_expectations: should_fail_build_on_warning: true should_fail_build_on_error: true performance_metrics: missed_frames_threshold: warning_percentage: 5.0

    error_percentage: 10.0 frame_build_rate_threshold: warning_time_in_millis: 14.0 error_time_in_millis: 16.0 frame_rasterizer_rate_threshold: warning_time_in_millis: 14.0 error_time_in_millis: 16.0 @jcocaramos - Droidcon NYC 2019
  49. ozzie.yaml integration_test_expectations: should_fail_build_on_warning: true should_fail_build_on_error: true performance_metrics: missed_frames_threshold: warning_percentage: 5.0

    error_percentage: 10.0 frame_build_rate_threshold: warning_time_in_millis: 14.0 error_time_in_millis: 16.0 frame_rasterizer_rate_threshold: warning_time_in_millis: 14.0 error_time_in_millis: 16.0 @jcocaramos - Droidcon NYC 2019
  50. ozzie.yaml integration_test_expectations: should_fail_build_on_warning: true should_fail_build_on_error: true performance_metrics: missed_frames_threshold: warning_percentage: 5.0

    error_percentage: 10.0 frame_build_rate_threshold: warning_time_in_millis: 14.0 error_time_in_millis: 16.0 frame_rasterizer_rate_threshold: warning_time_in_millis: 14.0 error_time_in_millis: 16.0 @jcocaramos - Droidcon NYC 2019
  51. ozzie.yaml integration_test_expectations: should_fail_build_on_warning: true should_fail_build_on_error: true performance_metrics: missed_frames_threshold: warning_percentage: 5.0

    error_percentage: 10.0 frame_build_rate_threshold: warning_time_in_millis: 14.0 error_time_in_millis: 16.0 frame_rasterizer_rate_threshold: warning_time_in_millis: 14.0 error_time_in_millis: 16.0 @jcocaramos - Droidcon NYC 2019
  52. Recap → Complex problems require simple solutions → Compliment business

    goals with engineering goals → If you don't test it, you don't ship it → Automate all the things! → Developer experience is key to success @jcocaramos - Droidcon NYC 2019
  53. Questions, comments, feedback... ? → @jcocaramos & @ChicagoFlutter → Dart

    2 In Action → jorgecoca.dev ( ) → https://speakerdeck.com/jorgecoca/flutter-at- scale @jcocaramos - Droidcon NYC 2019