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

Flutter in Practice

Flutter in Practice

In this talk, we’ll talk about some practical tips and tricks for writing your first production Flutter application – everything from setting up a cross-platform CI/CD system, organizing your application to be testable, as well as even how to change the framework itself *on the fly* and ship a custom version with your app.

This talk will be using the lessons learned while writing Sirene (https://github.com/anaisbetts/sirene), a production open-source Flutter app in the App Store + Play Store as its motivating example. Come out and learn some helpful tips for writing a great app!

Ana Betts

July 19, 2019
Tweet

More Decks by Ana Betts

Other Decks in Programming

Transcript

  1. Flutter in Practice Some tips and tricks for writing Flutter

    apps, from Start to Finish Ana Betts (@anaisbetts)
  2. I'm Ana, I'm a software developer, I worked for GitHub

    on GitHub Desktop, then at Slack on the Slack Desktop app, and now I'm working with Flutter A lot of what I've always worked on in my career has been about cross-platform applications, first with Xamarin and C#, then with Electron, and now with Flutter. hi!
  3. Today, I thought I'd talk about some practical advice that

    I learned, having written a full production application in Flutter. Since Flutter is super new, a lot of talks around it are about Getting Started; this talk is about various things I've learned while putting together a full, for Reals application, end to end Let's talk about fl utter!
  4. So, in April, I had surgery to talk more like

    this, and less like this. As part of that, for three weeks, I wasn't allowed to speak at all. ^ Of course, my solution is to write an app, because when you've got a hammer, everything around you looks like a thumb.
  5. There are lots and lots of apps that do text-to-speech,

    but none of them are usable. They're tech demos, or have thoughtless design. ^ Sirene was designed to be able to keep up in conversation Sirene: A text-to-speech app for day-to- day use
  6. Sirene is a cross-platform Flutter app that uses Cloud Firestore

    + Auth, MLKit, Sentry for error reporting, and Firebase Analytics. Wanted to build a Real App Phrases are stored in The Cloud
  7. The canned phrase list is sorted by usage and whether

    it's a reply ^ The opening list is generated from statistical mining of Friends ^ We do some cool stuff, like use MLKit to figure out what language you wrote text in. Here's why:
  8. Up front, I will say that this is not a

    Beacon of Software Design anaisbetts/sirene on GitHub
  9. I see a lot of people asking this question, because

    online, there are a lot of answers How do I choose what Design Pattern to use?
  10. This is what really excites me about Flutter - it

    so fundamentally changes things, that all of the patterns we're used to need to be rethought from First Principles You can Test widgets! Cheaply! As someone who loves thinking about The Zen of Designing Applications, this is Great. What's right for you, as always, depends on your team! Nobody knows! That's why Flutter is Cool.
  11. When we design things a certain way, our goal is

    that we pay something up- front, in order to get a benefit ^ Structure for Structure's Sake isn't usually a benefit! ^ "What are we making easier by making this other thing harder / more work" What makes a Good Design Pattern?
  12. I know it's hard to believe, but Code is a

    way for humans to describe things to each other ^ You know that a pattern is Good when your new hire reads it and understands the intent Design Patterns should Make Code Easier to Understand
  13. "Push complexity down" ^ Make it so most people write

    simple code, and only a few people write complicated code Design Patterns should Make Most Code Faster to Write
  14. A design pattern that makes stuff you do all the

    time filled with boilerplate and ceremony, is Not Good ^ We want Common Stuff to be Easy Design Patterns should Make Common Tasks Easier
  15. I don't know about you but I never agree with

    Computer, but even if you do... "Implicit is better than Explicit, if you and computer agree" - Yukihiro Matsumoto
  16. The bigger your team gets, the more that convention and

    Being Clever will slow you down ^ and the more that explicitness and straightforward code will benefit you "Implicit is better than Explicit, if you, your entire team, and computer, agree"
  17. Patterns like, "Just decorate this class to make it a

    Module" etc etc make it really difficult ^ to understand how the pieces fit together Putting similar code together is Good
  18. enum ApplicationMode { debug, production, test } class App extends

    State<AppWidget> { static GetIt locator; App() { locator = App.setupRegistration(GetIt()); } static GetIt setupRegistration(GetIt locator) { // ... } }
  19. static GetIt setupRegistration(GetIt locator) { final isTestMode = Platform.resolvedExecutable.contains('_tester'); var

    isDebugMode = false; // NB: Assert statements are stripped from release mode. Clever! assert(isDebugMode = true); final appMode = isTestMode ? ApplicationMode.test : isDebugMode ? ApplicationMode.debug : ApplicationMode.production; locator ..registerSingleton<ApplicationMode>(appMode) ..registerSingleton<Router>(setupRoutes(Router())) ..registerSingleton<StorageManager>(FirebaseStorageManager()); // ...
  20. Widget build(BuildContext context, {Router router, RouteObserver routeObserver}) { // Optional

    parameter for testing, default for production! routeObserver = routeObserver ?? App.locator<RouteObserver>(); router = router ?? App.locator<Router>(); return MaterialApp( title: 'Sirene', theme: ThemeMetrics.fullTheme(), initialRoute: '/', onGenerateRoute: router.generator, navigatorObservers: routeObserver != null ? [routeObserver] : [], ); }
  21. test('Make sure that setting up registrations ends up in test

    mode', () { var fixture = GetIt()..allowReassignment = true; App.setupRegistration(fixture); expect(fixture<ApplicationMode>(), ApplicationMode.test); });
  22. void withLocator(GetIt locator, VoidCallback block) { final oldLocator = App.locator;

    try { App.locator = locator; block(); } finally { App.locator = oldLocator; } } testWidget('Inject a dependency', (WidgetTester tester) { final locator = App.setupRegistration(GetIt()) ..allowReassignment = true ..registerSingleton<LoginProvider>(FakeLoginProvider()); withLocator(locator, () { var fixture = LoginWidget(); // Write your test! }); });
  23. Because GetIt at the end of the day is just

    a Map, the startup time is really fast GetIt is Super Fast™
  24. GetIt is a "small lego piece" - it's easy to

    build more complicated things GetIt is Super Dumb™
  25. GetIt makes it really easy to replace things in tests.

    That's the whole point! GetIt is Easy to Test
  26. In Dart, every class is an interface, whose definition is

    the public members of the class ^ This means, you don't have to wrap 3rd party classes as often Implicit Interfaces are Cool
  27. abstract class LoginManager { // This is a class from

    Firebase, but because // All Classes Are Interfaces, I can often just // Use It instead of making a silly wrapper. UserInfo get currentUser; Observable<UserInfo> getAuthState(); Future<UserInfo> login(); Future<void> logout(); Future<UserInfo> ensureNamedUser(); }
  28. class DebugFirebaseAnalytics implements FirebaseAnalytics { @override FirebaseAnalyticsAndroid get android =>

    null; @override Future<void> logEvent({String name, Map<String, dynamic> parameters}) async { debugPrint("Analytics: $name, $parameters"); } // ... }
  29. flutter_hooks class Example extends StatefulWidget { final Duration duration; const

    Example({Key key, @required this.duration}) : assert(duration != null), super(key: key); @override _ExampleState createState() => _ExampleState(); } class _ExampleState extends State<Example> with SingleTickerProviderStateMixin { AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: widget.duration); } @override void didUpdateWidget(Example oldWidget) { super.didUpdateWidget(oldWidget); if (widget.duration != oldWidget.duration) { _controller.duration = widget.duration; } } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Container(); } }
  30. flutter_hooks class Example extends HookWidget { final Duration duration; const

    Example({Key key, @required this.duration}) : assert(duration != null), super(key: key); @override Widget build(BuildContext context) { final controller = useAnimationController(duration: duration); return Container(); } }
  31. unawaited() BAD: // Returns Future, but nobody can tell sharedPrefs.setBool(INTRO_SEEN,

    true) GOOD: // We're explicitly saying, "I don't care about the result" unawaited(sharedPrefs.setBool(INTRO_SEEN, true));
  32. Since Widgets are immutable objects, we can reuse stuff return

    Container( color: Theme.of(context).backgroundColor, child: Padding( padding: EdgeInsets.all(8),
  33. return Flex( direction: Axis.vertical, children: <Widget>[ UsernameCard(), Expanded( child: InstagramImage()

    ), Flex( direction: Axis.horizontal, children: [LikeButton(), CommentButton(), ShareButton()], ), LikesText(), UsernameAndCaption(), Comments(collapsed: true), ], );
  34. Setting it up git submodule add https://github.com/flutter/flutter cd ./vendor/flutter git

    checkout v1.7.0 cd ../../ git add .gitmodules ./vendor git commit -m "Set Flutter to v1.7.0"
  35. Introducing flutterw macOS / Linux: #!/bin/bash set -euo pipefail DIR="$(

    cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" export PATH="$DIR/vendor/flutter/bin:$PATH" exec flutter $@