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

Flutter at scale

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.

C887ad592770a197f114d0a1d3e3a5a7?s=128

Jorge Coca

August 27, 2019
Tweet

Transcript

  1. Flutter at scale @jcocaramos - Droidcon NYC 2019

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

    2019 - @jcocaramos - Droidcon NYC 2019
  3. What do you mean by Flutter at scale? @jcocaramos -

    Droidcon NYC 2019
  4. Complex problems, easy solutions. @jcocaramos - Droidcon NYC 2019

  5. @jcocaramos - Droidcon NYC 2019

  6. Goals before code @jcocaramos - Droidcon NYC 2019

  7. 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
  8. Engineering goals Create a platform that is developer friendly, developer

    scalable, and performant, which provides safe experimentation and continuos deployment. @jcocaramos - Droidcon NYC 2019
  9. @jcocaramos - Droidcon NYC 2019

  10. 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
  11. 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
  12. 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
  13. Developer friendly → Tests. Tons of tests. → Consistent coding

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

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

    improve → Tests, tests, tests → Easy to identify where are the problems @jcocaramos - Droidcon NYC 2019
  16. 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
  17. 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
  18. @jcocaramos - Droidcon NYC 2019

  19. How do we do this with Flutter? @jcocaramos - Droidcon

    NYC 2019
  20. Learn from the past @jcocaramos - Droidcon NYC 2019

  21. Our tools → High level architecture → flutter_bloc (@felangelov) →

    lumberdash (@bmw-tech) → unit, widget, and integration tests → ozzie (@bmw-tech) @jcocaramos - Droidcon NYC 2019
  22. Example Find more at felangel.github.io/ bloc @jcocaramos - Droidcon NYC

    2019
  23. High level architecture → Vertical modules (data, domain, UI &

    state) → Horizontal modules (feature composition) @jcocaramos - Droidcon NYC 2019
  24. Flutter is composable by nature @jcocaramos - Droidcon NYC 2019

  25. flutter_bloc A predictable state management library that helps implement the

    BLoC design pattern @jcocaramos - Droidcon NYC 2019
  26. UI = f (state) @jcocaramos - Droidcon NYC 2019

  27. 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
  28. 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
  29. UI Events Pure Dart classes (POJOs) used to describe actions

    triggered from the system/user enum CounterEvent { increment, decrement, } @jcocaramos - Droidcon NYC 2019
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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
  41. 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
  42. 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
  43. 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
  44. Easy, right? @jcocaramos - Droidcon NYC 2019

  45. Analytics and remote debugging, 100% free @jcocaramos - Droidcon NYC

    2019
  46. 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
  47. 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
  48. 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
  49. Open Source Lumberdash clients → SimpleClient → ColorizeClient → SentryClient

    → FirebaseClient @jcocaramos - Droidcon NYC 2019
  50. if (isDebug) { putLumberdashToWork(withClient: SimpleClient()); } else { putLumberdashToWork(withClient: FirebaseClient());

    } logMessage('Hello Message!'); @jcocaramos - Droidcon NYC 2019
  51. 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
  52. 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
  53. 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
  54. void main() { BlocSupervisor.delegate = CounterBlocDelegate( lumberdashClient: lumberdashClient, ); runApp(CounterApp());

    } @jcocaramos - Droidcon NYC 2019
  55. 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
  56. @jcocaramos - Droidcon NYC 2019

  57. What do we do in the core team to ensure

    quality? @jcocaramos - Droidcon NYC 2019
  58. 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
  59. Pipeline @jcocaramos - Droidcon NYC 2019

  60. 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
  61. Ozzie @jcocaramos - Droidcon NYC 2019

  62. 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
  63. 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
  64. 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
  65. 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
  66. 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
  67. 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
  68. 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
  69. 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
  70. 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
  71. 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
  72. @jcocaramos - Droidcon NYC 2019

  73. 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
  74. Questions, comments, feedback... ? → @jcocaramos & @ChicagoFlutter → Dart

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

  76. Happy coding! !" @jcocaramos - Droidcon NYC 2019