<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Abraham Williams on Medium]]></title>
        <description><![CDATA[Stories by Abraham Williams on Medium]]></description>
        <link>https://medium.com/@abraham?source=rss-a0f937ae8409------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*BeAKcBNhqR7zRspM_uPYwQ.jpeg</url>
            <title>Stories by Abraham Williams on Medium</title>
            <link>https://medium.com/@abraham?source=rss-a0f937ae8409------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Wed, 08 Apr 2026 06:34:18 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@abraham/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[A month of Flutter: a look back]]></title>
            <link>https://medium.com/@abraham/a-month-of-flutter-a-look-back-edaa3454bac5?source=rss-a0f937ae8409------2</link>
            <guid isPermaLink="false">https://medium.com/p/edaa3454bac5</guid>
            <category><![CDATA[tutorial]]></category>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[blogging]]></category>
            <dc:creator><![CDATA[Abraham Williams]]></dc:creator>
            <pubDate>Mon, 31 Dec 2018 20:26:24 GMT</pubDate>
            <atom:updated>2018-12-31T20:26:24.576Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*QbWG4XcB4JnpXHTE.jpg" /></figure><p><em>Originally published on </em><a href="https://bendyworks.com/blog/a-month-of-flutter-a-look-back"><em>bendyworks.com</em></a><em>.</em></p><p>This is it. 31 blog posts in 31 days. Writing <a href="https://bendyworks.com/blog/a-month-of-flutter">a month of flutter</a> has been a ton of work but also lots of fun and a good learning experience. I really appreciate how supportive and and positive everyone as been.</p><h3>Publishing experience</h3><p>For the series I’ve been posting on <a href="https://bendyworks.com/blog/authors/abraham_williams">bendyworks.com</a>, <a href="https://dev.to/abraham">DEV</a>, <a href="https://blog.abrah.am/">my personal blog</a>, and <a href="https://medium.com/@abraham">Medium</a>. After publishing to these sites, I would put the Bendyworks link on <a href="https://twitter.com/abraham">Twitter</a>, <a href="https://www.reddit.com/user/abrahamwilliams/posts/">Reddit</a>, and the <a href="https://flutter.io/community">Flutter Study Group Slack</a>.</p><p>Posting to DEV was easy as they use Markdown just like the Bendworks blog. DEV also has built in support for a <a href="https://dev.to/ben/changelog-create-series-of-posts-4o63">series of posts</a> so it’s easy to read the entire series. I did have to manually upload any embedded images. DEV also has a number of <a href="https://dev.to/p/editor_guide">liquid tags</a> for embedding things like GitHub issues that I didn’t make as much use of as I should have.</p><p>Blogger is rich text so it was easy to copy/paste the rendered posts. This would hotlink all the images though so I had to remove them and manually re-upload them.</p><p>Medium will only support canonical links if you <a href="https://help.medium.com/hc/en-us/articles/214550207-Import-post">import from a URL</a> for some reason. They would grab any images and host them on their CDN so no manual uploading. Importing would lose all the code snippet formatting though so I would have to fix all of that.</p><h3>Engagement</h3><p>Bendyworks received the most page views which is not surprising since those were the URLs shared on Twitter/Reddit/Slack. DEV was second with with Medium and my blog getting negligible views.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/0*6pk_jiQUrkmb40AL.png" /></figure><p>For engagement DEV was the leader with lots of ❤️ and Twitter close behind. Reddit had some upvotes but not that many and only a few claps came from Medium.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/0*UD8pMeZOQ6wg0oQF.png" /></figure><p>The real breakout for me here was DEV. I had never posted anything or used it much but I found the community very friendly and engaging. I received a lot more activity from the 600+ followers up built up there than the almost 9000 followers I have on Twitter. If you’re not on DEV yet, sign up and <a href="https://dev.to/abraham">follow me</a>.</p><h3>Goals</h3><p>Kicking off I had three main goals</p><ul><li>Become a better Flutter developer</li><li>Practice writing concise blog posts</li><li>Practice writing project requirements</li></ul><p>I certainly become a <a href="https://bendyworks.com/blog/a-month-of-flutter-user-registration-refactor">better Flutter developer</a> and learned a lot along the way. I would like to try more architecture patterns but the need to always be publishing didn’t leave me with much room for exploration. Maybe I could have written more “I tried this and here why it didn’t work” posts but I felt pressure to always deliver progress.</p><p>I am much better at throwing words onto a page and coming out with a decent post. One aspect of blogging that I constantly struggle with is keeping posts small and digestible. It’s easy to get into the mindset that posts have to be long and perfect. Short and to the point is usually better though.</p><p>Working on improving project requirements kind of fell by the wayside as I tried to keep up with posting every day. It just wasn’t a priority.</p><h3>App</h3><p>The app isn’t done yet but it has <a href="https://bendyworks.com/blog/a-month-of-flutter-sign-in-with-google">Sign in with Google</a>, a <a href="https://bendyworks.com/blog/a-month-of-flutter-rendering-network-images">home stream</a>, and <a href="https://bendyworks.com/blog/a-month-of-flutter-hero-animation">fancy animations</a>.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2F2u-_GOWhWNc%3Ffeature%3Doembed&amp;url=http%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D2u-_GOWhWNc&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2F2u-_GOWhWNc%2Fhqdefault.jpg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube" width="640" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/813b7525842319d15a98ae8e017004c2/href">https://medium.com/media/813b7525842319d15a98ae8e017004c2/href</a></iframe><h3>AnnotatedRegion</h3><p><a href="https://github.com/Miguel-Herrero">Miguel</a> submitted a <a href="https://github.com/abraham/birb/pull/69">pull request</a> fixing <a href="https://github.com/abraham/birb/issues/51">a bug on Android</a> where the bottom navigation theme would break on sign in. He fixed it by wrapping the HomePage Scaffold in an <a href="https://docs.flutter.io/flutter/widgets/AnnotatedRegion-class.html">AnnotatedRegion widget</a>.</p><pre>AnnotatedRegion&lt;SystemUiOverlayStyle&gt;( value: lightSystemUiOverlayStyle, child: Scaffold(/* ... */), )</pre><h3>Thanks</h3><p>A huge thank you goes to <a href="https://twitter.com/pblatteier">Pearl</a> for proofreading every single post and reviewing all the code, sometimes late at night at the last minute. I couldn’t have done a month of Flutter without her help.</p><p>Thanks also to: <a href="https://unsplash.com/">Unsplash</a> for all the featured images. <a href="https://twitter.com/ninalimpi">@NinaLimpi</a> and <a href="https://undraw.co/">unDraw</a> for the <a href="https://bendyworks.com/blog/a-month-of-flutter-no-content-widget">fantastic illustrations</a>. <a href="https://twitter.com/tommy_emo_">@tommy_emo_</a> for the <a href="https://bendyworks.com/blog/a-month-of-flutter-awesome-adaptive-icons">awesome icon</a>.</p><p>All of you for reading along and your feedback and support.</p><h3>What’s next</h3><p>I’m going to keep developing Birb and writing about it as “a month+ of Flutter”. Just don’t expect posts every day. For example I’d like to finally get a <a href="https://github.com/abraham/birb/pull/45">readme written</a> and take a second look at something I <a href="https://github.com/abraham/birb/pull/63">didn’t get running</a> before.</p><p>Follow me on <a href="https://twittr.com/abraham">Twitter</a> and <a href="https://dev.to/abraham">DEV</a> for future posts and <a href="https://github.com/abraham/birb">GitHub</a> for code changes.</p><h3>Code changes</h3><ul><li><a href="https://github.com/abraham/birb/pull/69">#69 Apply AnnotatedRegion that fixes</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=edaa3454bac5" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A month of Flutter: the real hero animation]]></title>
            <link>https://medium.com/@abraham/a-month-of-flutter-the-real-hero-animation-4da206dc8500?source=rss-a0f937ae8409------2</link>
            <guid isPermaLink="false">https://medium.com/p/4da206dc8500</guid>
            <category><![CDATA[tutorial]]></category>
            <category><![CDATA[animation]]></category>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[navigation]]></category>
            <dc:creator><![CDATA[Abraham Williams]]></dc:creator>
            <pubDate>Sun, 30 Dec 2018 22:04:20 GMT</pubDate>
            <atom:updated>2018-12-30T22:04:20.370Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Sas6mVdkXY4QygKn.jpg" /></figure><p><em>Originally published on </em><a href="https://bendyworks.com/blog/a-month-of-flutter-hero-animation"><em>bendyworks.com</em></a><em>.</em></p><p>For the last post before the month’s wrap up tomorrow, I wanted to do something more fun: use a <a href="https://flutter.io/docs/development/ui/animations/hero-animations">hero animation</a> between the home page list and the individual post page.</p><p>When I first implemented the Hero animation it never worked going back from a PostPage to the HomePage. The reason was that HomePage would get rerendered and that would <a href="https://bendyworks.com/blog/a-month-of-flutter-real-faker-data">generate new fake posts</a>. So I moved the fake data generation up a level to MyApp and pass it into HomePage. This is more realistic as going to the HomePage shouldn&#39;t request the Posts every time.</p><pre>HomePage(<br>  title: &#39;Birb&#39;,<br>  posts: _loadPosts(context),<br>)</pre><p>The PostPage implementation is a simple StatelessWidget that takes Post and renders a PostItem. This will become more complex as things like comments and likes are implemented but works for now.</p><pre>class PostPage extends StatelessWidget {<br>  const PostPage({<br>    Key key,<br>    @required this.post,<br>  }) : super(key: key);<br><br>  final Post post;<br><br>  @override<br>  Widget build(BuildContext context) {<br>    return Scaffold(<br>      appBar: AppBar(<br>        title: const Text(&#39;Post&#39;),<br>        centerTitle: true,<br>        elevation: 0.0,<br>      ),<br>      body: SingleChildScrollView(<br>        child: Padding(<br>          padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 8.0),<br>          child: PostItem(post),<br>        ),<br>      ),<br>    );<br>  }<br>}</pre><p>With PostItem being used to render on the HomePage and on the PostPage, wrapping the Image in a Hero is handled in a single place. tag is how Hero knows what to transition between pages.</p><pre>Hero(<br>  tag: post.id,<br>  child: ClipRRect(<br>    child: Image.network(post.imageUrl),<br>    borderRadius: BorderRadius.circular(10.0),<br>  ),<br>)</pre><p>The last piece is navigating from PostList to PostPage when a user taps on a PostItem. I&#39;ll handle this with an <a href="https://docs.flutter.io/flutter/material/InkWell-class.html">InkWell widget</a> so there is a nice <a href="https://material.io/design/motion/understanding-motion.html#usage">Material ripple</a>.</p><pre>InkWell(<br>  onTap: () =&gt; _navigateToPost(context, post),<br>  child: PostItem(post),<br>)</pre><p>The navigation is more complex then <a href="https://bendyworks.com/blog/a-month-of-flutter-navigate-to-user-registration">opening the registration page</a> for two reasons. <a href="https://flutter.io/docs/cookbook/navigation/named-routes">Named routes</a> don’t support parameters and I wanted a simple <a href="https://docs.flutter.io/flutter/widgets/PageRouteBuilder/buildTransitions.html">transition</a> between the rest of the content on the page.</p><pre>void _navigateToPost(BuildContext context, Post post) {<br>  Navigator.of(context).push(<br>    PageRouteBuilder&lt;PostPage&gt;(<br>      pageBuilder: (<br>        BuildContext context,<br>        Animation&lt;double&gt; animation,<br>        Animation&lt;double&gt; secondaryAnimation,<br>      ) {<br>        return PostPage(post: post);<br>      },<br>      transitionsBuilder: (<br>        BuildContext context,<br>        Animation&lt;double&gt; animation,<br>        Animation&lt;double&gt; secondaryAnimation,<br>        Widget child,<br>      ) {<br>        return FadeTransition(<br>          opacity: animation,<br>          child: child,<br>        );<br>      },<br>    ),<br>  );<br>}</pre><p>Here I will <a href="https://docs.flutter.io/flutter/widgets/Navigator/push.html">push</a> a <a href="https://docs.flutter.io/flutter/widgets/PageRouteBuilder-class.html">PageRouteBuilder</a> onto the navigation stack. PageRouteBuilder has two key builders in use here. pageBuilder builds the widget that should be rendered as the new page and transitionBuilder specifies how to transition between the old and new pages. Note that this <a href="https://docs.flutter.io/flutter/widgets/FadeTransition-class.html">FadeTransition</a> is not related to implementing Hero earlier.</p><p>The tests for PostPage is simple and just checking that PostItem is rendered. I did update the PostItem test to expect that its Hero widget had the correct tag value.</p><pre>expect(tester.widget&lt;Hero&gt;(hero).tag, post.id);</pre><p>PostsList tests had to be wrapped in a MaterialApp as <a href="https://docs.flutter.io/flutter/material/InkWell-class.html">InkWell</a> must have a Material widget ancestor.</p><p>The navigation and animation from PostsList to PostPage is now doing more work so I replaced several <a href="https://docs.flutter.io/flutter/flutter_test/WidgetTester/pump.html">pump</a> pauses with <a href="https://docs.flutter.io/flutter/flutter_test/WidgetTester/pumpAndSettle.html">pumpAndSettle</a>.</p><p>Here is the fancy Hero animation:</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2F2u-_GOWhWNc%3Ffeature%3Doembed&amp;url=http%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D2u-_GOWhWNc&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2F2u-_GOWhWNc%2Fhqdefault.jpg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube" width="640" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/813b7525842319d15a98ae8e017004c2/href">https://medium.com/media/813b7525842319d15a98ae8e017004c2/href</a></iframe><h3>Code changes</h3><p><a href="https://github.com/abraham/birb/pull/68">WIP: Add post page with hero animation by abraham · Pull Request #68 · abraham/birb</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4da206dc8500" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A month of Flutter: user registration refactor with reactive scoped model]]></title>
            <link>https://medium.com/@abraham/a-month-of-flutter-user-registration-refactor-with-reactive-scoped-model-308199e0e256?source=rss-a0f937ae8409------2</link>
            <guid isPermaLink="false">https://medium.com/p/308199e0e256</guid>
            <category><![CDATA[refactoring]]></category>
            <category><![CDATA[tutorial]]></category>
            <category><![CDATA[firestore]]></category>
            <category><![CDATA[reactive]]></category>
            <category><![CDATA[flutter]]></category>
            <dc:creator><![CDATA[Abraham Williams]]></dc:creator>
            <pubDate>Sun, 30 Dec 2018 03:31:20 GMT</pubDate>
            <atom:updated>2018-12-30T03:31:20.676Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*uFtiWZXFwqi6Qvjp.jpg" /></figure><p><em>Originally published on </em><a href="https://bendyworks.com/blog/a-month-of-flutter-user-registration-refactor#"><em>bendyworks.com</em></a><em>.</em></p><p><a href="https://bendyworks.com/blog/a-month-of-flutter-wip-save-users-to-firestore">Yesterday I implemented</a> saving new users to Firestore but I wasn’t happy with the implementation. So today I refactored <strong>everything</strong>, well not everything but a lot. There are still areas for improvement but I like the general pattern being used now.</p><p>Now there is a top level <a href="https://pub.dartlang.org/packages/scoped_model">ScopedModel</a> that tracks the current authentication status. This sets up a listener on FirebaseAuth and Firestore and will get pushed changes when either have changed. Children widgets that need to change functionality based on authentication status will get rerendered as needed.</p><p>A couple of the less important changes that I’ll get out of the way now.</p><ul><li>Auth was renamed to AuthService</li><li>The Firestore rules were updated to allow a user to read their own user document</li><li>Registration/sign in success SnackBar was replaced with an unstyled welcome view</li><li>HomePage got routeName added</li></ul><p>In MyApp&#39;s build, MaterialApp has been wrapped in ScopedModel&lt;CurrentUserModel&gt;. I put ScopedModel at the top because authentication changing will touch almost all of the app.</p><pre>ScopedModel&lt;CurrentUserModel&gt;(<br>  model: CurrentUserModel.instance(),<br>  child: MaterialApp(<br>    debugShowCheckedModeBanner: false,<br>    title: &#39;Birb&#39;,<br>    theme: buildThemeData(),<br>    home: const HomePage(title: &#39;Birb&#39;),<br>    routes: &lt;String, WidgetBuilder&gt;{<br>      RegisterPage.routeName: (BuildContext context) =&gt;<br>          const RegisterPage(),<br>    },<br>  ),<br>)</pre><p>ScopedModel takes a model, which in this case is CurrentUserModel (I&#39;m not 100% in on that name yet) and acts similar to an InheritedWidget. Any children can find and interact with the CurrentuserModel instance or be conditionally rendered based on its state.</p><p>Coming from a background in web development, I thinkf of Models differently than ScopedModel does. Typically I think of models as single data objects like a blog post or tweet. In ScopedModel land, it&#39;s more like a <a href="https://redux.js.org/basics/store">store</a> or <a href="https://en.wikipedia.org/wiki/Finite-state_machine">state machine</a> and can manage the state of several things.</p><p>I have set the CurrentStatusModel to have one of three statues:</p><pre>enum Status {<br>  Unauthenticated,<br>  Unregistered,<br>  Authenticated,<br>}</pre><ul><li>Unauthenticated is the initial default</li><li>Unregistered the user has authenticated through Firebase</li><li>Authenticated is when the user has authenticated, agreed to the Terms of Service, and a User document has been saved to Firestore</li></ul><p>Here is the new CurrentUserModel definition:</p><pre>class CurrentUserModel extends Model {<br>  CurrentUserModel({<br>    @required this.firestore,<br>    @required this.firebaseAuth,<br>    @required this.userService,<br>  });<br><br>  CurrentUserModel.instance()<br>      : firestore = Firestore.instance,<br>        firebaseAuth = FirebaseAuth.instance,<br>        userService = UserService.instance(),<br>        authService = AuthService.instance() {<br>    firebaseAuth.onAuthStateChanged.listen(_onAuthStateChanged);<br>  }<br><br>  Status _status = Status.Unauthenticated;<br>  Firestore firestore;<br>  FirebaseAuth firebaseAuth;<br>  UserService userService;<br>  User _user;<br>  FirebaseUser _firebaseUser;<br>  AuthService authService;<br><br>  static CurrentUserModel of(BuildContext context) =&gt;<br>      ScopedModel.of&lt;CurrentUserModel&gt;(context);<br><br>  User get user =&gt; _user;<br>  Status get status =&gt; _status;<br>  FirebaseUser get firebaseUser =&gt; _firebaseUser;<br><br>  Future&lt;void&gt; signIn() {<br>    return authService.signInWithGoogle();<br>  }<br><br>  Future&lt;void&gt; signOut() {<br>    return firebaseAuth.signOut();<br>  }<br><br>  Future&lt;void&gt; register(Map&lt;String, String&gt; formData) async {<br>    await userService.createUser(_firebaseUser.uid, formData);<br>  }<br><br>  Future&lt;void&gt; _onAuthStateChanged(FirebaseUser firebaseUser) async {<br>    if (firebaseUser == null) {<br>      _firebaseUser = null;<br>      _user = null;<br>      _status = Status.Unauthenticated;<br>    } else {<br>      if (firebaseUser.uid != _firebaseUser?.uid) {<br>        _firebaseUser = firebaseUser;<br>      }<br>      _status = Status.Unregistered;<br>      if (firebaseUser.uid != _user?.id) {<br>        _user = await userService.getById(_firebaseUser.uid);<br>      }<br>      if (_user != null) {<br>        _status = Status.Authenticated;<br>      }<br>    }<br><br>    notifyListeners();<br>    _listenToUserChanges();<br>  }<br><br>  void _onUserDocumentChange(DocumentSnapshot snapshot) {<br>    if (snapshot.exists) {<br>      _user = User.fromDocumentSnapshot(snapshot.documentID, snapshot.data);<br>      _status = Status.Authenticated;<br>    } else {<br>      _user = null;<br>      _status = Status.Unregistered;<br>    }<br>    notifyListeners();<br>  }<br><br>  void _listenToUserChanges() {<br>    if (_firebaseUser == null) {<br>      return;<br>    }<br>    // TODO(abraham): Does this need any cleanup if uid changes?<br>    firestore<br>        .collection(&#39;users&#39;)<br>        .document(_firebaseUser.uid)<br>        .snapshots()<br>        .listen(_onUserDocumentChange);<br>  }<br>}</pre><p>I have created a <a href="https://www.dartlang.org/guides/language/language-tour#named-constructors">named constructor</a> so that I can call CurrentUserModel.instance() and it will use the default services. The number of services this relies on is large and I think can be cleaned up in the future.</p><p>The first neat bit is firebaseAuth.onAuthStateChanged.listen(_onAuthStateChanged). This adds a <a href="https://pub.dartlang.org/documentation/firebase_auth/latest/firebase_auth/FirebaseAuth/onAuthStateChanged.html">listener</a> to FirebaseAuth and anytime the user signs in or signs out the callback will be called.</p><p>There is a static of method for the convenience of being able to call CurrentUserModel.of(context) for easy access to the state.</p><pre>static CurrentUserModel of(BuildContext context) =&gt;<br>    ScopedModel.of&lt;CurrentUserModel&gt;(context);</pre><p>The signIn, signOut, and register methods perform as they are named. They do consolidate a number of service dependencies into CurrentUserModel but I&#39;m not sure this is the best place to have them.</p><p>_onAuthStateChanged is the work horse of this class. Anytime FirebaseAuth changes, this gets called and has to figure out what&#39;s going on. In essence if there is no user, it clears all the state, if the user is new or different it tries to get the User document. Lastly it will notify children that the state has changed and will start listening to changes to the User document.</p><p>_listenToUserChanges will listen to Firestore for changes to the authenticated user&#39;s document. One neat aspect is it can start listening before the document even exists and will get notified when it&#39;s created (from registering).</p><p>Lastly in CurrentUserModel is _onUserDocumentChange. If the document exists the user is authenticated and registered.</p><p>I also added a User model. It doesn&#39;t do much yet but handles taking a Firestore document and and turning it into a more manageable class instance.</p><p>To make implementation easier, I added a SignOutAction widget to the AppBar in HomePage. This simply renders an Icon and calls CurrentUserModel.of(context).signOut() on tap.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*y6kT6xZwgZb5V3FS.png" /></figure><p>Another change in HomePage is to wrap the SignInFab widget in a ScopedModelDescendant&lt;CurrentUserModel&gt;. This will cause it to get rebuilt when CurrentUserModel notifies its children of state changes. The builder callback has to return a Widget so I just return an empty Container if the user is authenticated.</p><pre>Widget _floatingActionButton() {<br>  return ScopedModelDescendant&lt;CurrentUserModel&gt;(<br>    builder: (<br>      BuildContext context,<br>      Widget child,<br>      CurrentUserModel model,<br>    ) =&gt;<br>        model.user == null ? const SignInFab() : Container(),<br>  );<br>}</pre><p>RegisterPage similarly gets updated with a ScopedModelDescendant&lt;CurrentUserModel&gt; wrapper. This use is being a little smarter and will show the form if the user is authenticated but not registered and a welcome message once the user finishes registering. This message will need to be <a href="https://github.com/abraham/birb/issues/67">improved</a>.</p><pre>ScopedModelDescendant&lt;CurrentUserModel&gt;(<br>  builder: (<br>    BuildContext context,<br>    Widget child,<br>    CurrentUserModel model,<br>  ) {<br>    if (model.status == Status.Unregistered) {<br>      return const RegisterForm();<br>    } else if (model.status == Status.Authenticated) {<br>      return const Center(<br>        child: Text(&#39;Welcome&#39;),<br>      );<br>    } else {<br>      return const CircularProgressIndicator();<br>    }<br>  },<br>)</pre><p>I haven’t fleshed out all the tests yet but a lot of the code now has dependencies on CurrentUserModel so I added a simple appMock to makes this easier.</p><pre>ScopedModel&lt;CurrentUserModel&gt; appMock({<br>  @required Widget child,<br>  @required CurrentUserModel mock,<br>}) {<br>  return ScopedModel&lt;CurrentUserModel&gt;(<br>    model: mock,<br>    child: MaterialApp(<br>      home: Scaffold(<br>        body: child,<br>      ),<br>      routes: &lt;String, WidgetBuilder&gt;{<br>        RegisterPage.routeName: (BuildContext context) =&gt; const RegisterPage(),<br>      },<br>    ),<br>  );<br>}</pre><h3>Code changes</h3><ul><li><a href="https://github.com/abraham/birb/pull/66">#66 Create user in Firestore</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=308199e0e256" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A month of Flutter: WIP save users to Firestore]]></title>
            <link>https://medium.com/@abraham/a-month-of-flutter-wip-save-users-to-firestore-8f0a2e0085d8?source=rss-a0f937ae8409------2</link>
            <guid isPermaLink="false">https://medium.com/p/8f0a2e0085d8</guid>
            <category><![CDATA[refactoring]]></category>
            <category><![CDATA[tutorial]]></category>
            <category><![CDATA[firestore]]></category>
            <category><![CDATA[flutter]]></category>
            <dc:creator><![CDATA[Abraham Williams]]></dc:creator>
            <pubDate>Sat, 29 Dec 2018 04:53:55 GMT</pubDate>
            <atom:updated>2018-12-29T04:53:55.457Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*slgGYymvGqerdTlK.jpg" /></figure><p><em>Originally published on </em><a href="https://bendyworks.com/blog/a-month-of-flutter-wip-save-users-to-firestore"><em>bendyworks.com</em></a><em>.</em></p><p>Today was supposed to be simple. Take <a href="https://bendyworks.com/blog/a-month-of-flutter-user-registration-form">form values</a>, <a href="https://bendyworks.com/blog/a-month-of-flutter-firestore-create-user-rules-and-tests">save them in Firestore</a>. It works but the current implementation is messy so I’m going to walk through the work in progress (WIP) code and refactor it tomorrow.</p><p>The larger architectural change was creating a UserService to handle getting and creating users. This approach works but creates a complex dependency injection pattern that requires a lot of duplicate code and mocking in test. These are some of the current changes and what I don&#39;t like about the implementation:</p><p>RegisterPage now takes a UserService which in turn takes FirebaseAuth and Firestore instances. Doing this once wouldn&#39;t be so bad but I&#39;ve had to instantiate UserService in several places. I&#39;d like to find a better approach than UserService.</p><pre>RegisterPage(<br>  userService: UserService(<br>    firebaseAuth: FirebaseAuth.instance,<br>    firestore: Firestore.instance,<br>  ),<br>)</pre><p>RegisterForm was updated to call <a href="https://docs.flutter.io/flutter/widgets/FormState/save.html">FormState.save</a> on submit if the form is valid. _submit will also grab photoUrl and set it directly on _formData.</p><pre>Future&lt;void&gt; _submit() async {<br>  if (_formKey.currentState.validate()) {<br>    _formKey.currentState.save();<br>    _formData[&#39;photoUrl&#39;] = widget.firebaseUser.photoUrl;<br><br>    final bool result =<br>        await widget.userService.addUser(widget.firebaseUser.uid, _formData);<br>    if (result) {<br>      _showSnackBar(context, &#39;Welcome ${_formData[&#39;fullName&#39;]}&#39;);<br>      Navigator.pushNamed(context, HomePage.routeName);<br>    } else {<br>      _showSnackBar(context, &#39;Error submitting form&#39;);<br>    }<br>  }<br>}</pre><p>_formData[&#39;key&#39;] is clumsy so I&#39;d like to refactor it to use _formData.key instead. The <a href="https://docs.flutter.io/flutter/widgets/FormField/onSaved.html">onSaved callback</a> on TextFormField takes the input value and sets it on _formData.</p><pre>TextFormField(<br>  initialValue: widget.firebaseUser.displayName,<br>  decoration: const InputDecoration(<br>    labelText: &#39;Full name&#39;,<br>  ),<br>    validator: (String value) {<br>      if (value.trim().isEmpty) {<br>      return &#39;Full name is required&#39;;<br>    }<br>  },<br>  onSaved: (String value) =&gt; _formData[&#39;fullName&#39;] = value,<br>)</pre><p>RegisterForm now takes a FirebaseUser and a UserService. You might notice the repetitiveness repetitiveness of UserService. The FirebaseUser is used to pre-fill the form name fields so users can just submit the form if they want to use their Google registered names.</p><p>Here is how RegisterForm is called in RegisterPage:</p><pre>Widget _formWhenReady() {<br>  return _firebaseUser == null<br>      ? const CircularProgressIndicator()<br>      : RegisterForm(<br>          firebaseUser: _firebaseUser,<br>          userService: UserService(<br>            firestore: Firestore.instance,<br>            firebaseAuth: FirebaseAuth.instance,<br>          ),<br>        );<br>}</pre><p>RegisterPage has a new _getCurrentUser method that will set the _firebaseUser state. Until _firebaseUser is set, <a href="https://docs.flutter.io/flutter/material/CircularProgressIndicator-class.html">CircularProgressIndicator</a> is displayed. Checking if there is a current user is going to happen a lot in the application so this needs to be much simpler to do.</p><pre>Future&lt;void&gt; _getCurrentUser() async {<br>  final FirebaseUser user = await widget.userService.currentUser();<br>  setState(() {<br>    _firebaseUser = user;<br>  });<br>}</pre><p>UserService itself is fairly simple.</p><pre>class UserService {<br>  UserService({<br>    @required this.firestore,<br>    @required this.firebaseAuth,<br>  });<br><br>  final Firestore firestore;<br>  final FirebaseAuth firebaseAuth;<br><br>  Future&lt;FirebaseUser&gt; currentUser() {<br>    return firebaseAuth.currentUser();<br>  }<br><br>  Future&lt;bool&gt; addUser(String uid, Map&lt;String, String&gt; formData) async {<br>    try {<br>      await firestore<br>          .collection(&#39;users&#39;)<br>          .document(uid)<br>          .setData(_newUserData(formData));<br>      return true;<br>    } catch (e) {<br>      return false;<br>    }<br>  }<br><br>  Map&lt;String, dynamic&gt; _newUserData(Map&lt;String, String&gt; formData) {<br>    return &lt;String, dynamic&gt;{}<br>      ..addAll(formData)<br>      ..addAll(&lt;String, dynamic&gt;{<br>        &#39;agreedToTermsAt&#39;: FieldValue.serverTimestamp(),<br>        &#39;createdAt&#39;: FieldValue.serverTimestamp(),<br>        &#39;updatedAt&#39;: FieldValue.serverTimestamp(),<br>      });<br>  }<br>}</pre><p>It takes Firestore and FirebaseAuth instances so that it can get the currentUser or create a new document with setData. I don&#39;t like having to inject UserService in multiple places and I would like to cleanup the formData type.</p><p>FieldValue.serverTimestamp() is a <a href="https://firebase.google.com/docs/reference/js/firebase.firestore.FieldValue#.serverTimestamp">special Firestore value</a> that will use the server timestamp when the document gets saved.</p><p>In the tests I’m now having to mock a lot more stuff. This is making the tests more verbose and harder to read and change. Come back tomorrow to see the exciting conclusion of the refactor.</p><h3>Code changes</h3><ul><li><a href="https://github.com/abraham/birb/pull/66/commits/9a631f70d532e8a175a717bd7d28c63b70231456">#66 9a631f7 Create user in Firestore</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8f0a2e0085d8" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A month of Flutter: Firestore create user rules and tests]]></title>
            <link>https://medium.com/@abraham/a-month-of-flutter-firestore-create-user-rules-and-tests-c02ddae30ffb?source=rss-a0f937ae8409------2</link>
            <guid isPermaLink="false">https://medium.com/p/c02ddae30ffb</guid>
            <category><![CDATA[testing]]></category>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[tutorial]]></category>
            <category><![CDATA[firestore]]></category>
            <category><![CDATA[firebase]]></category>
            <dc:creator><![CDATA[Abraham Williams]]></dc:creator>
            <pubDate>Fri, 28 Dec 2018 02:40:38 GMT</pubDate>
            <atom:updated>2018-12-28T02:40:38.273Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*s7gJQiPKfX4Q-W-u.jpg" /></figure><p><em>Originally published on </em><a href="https://bendyworks.com/blog/a-month-of-flutter-firestore-create-user-rules-and-tests"><em>bendyworks.com</em></a><em>.</em></p><p>When a user <a href="https://bendyworks.com/blog/a-month-of-flutter-sign-in-with-google">signs in with Google</a> I’m going to create a user document in Firestore. Each authenticated user should only be able to create one user document. These documents will eventually be readable by other users so Firestore needs to have <a href="https://firebase.google.com/docs/firestore/security/get-started">server(less)-side validation</a> to keep the data as correct as possible.</p><p>Right now I’m working on <a href="https://bendyworks.com/blog/a-month-of-flutter-user-registration-form/">registering users</a> so I’m only going to implement create rules, not read, update, etc.</p><pre>match /users/{userId} {<br>  allow create: if isOwner(userId) &amp;&amp;<br>                  // TODO: enable after bug fix https://github.com/firebase/firebase-tools/issues/1073<br>                  //  validCreateTimestamps() &amp;&amp;<br>                  validCreateUser();<br>}</pre><p>This will allow user documents to be created if the owner has the same ID and the document being created passes validation.</p><p>Validation of timestamps is being skipped because of a <a href="https://github.com/firebase/firebase-tools/issues/1073">bug in the emulator</a> that causes tests to hang. I’ve also been considering moving createdAt and updatedAt timestamps into <a href="https://firebase.google.com/docs/functions/">Cloud Functions</a> but I&#39;m not ready to make that commitment yet.</p><p>User document creation is primarily validated with two tests. One to make sure an authenticated user can create their own document and one to make sure a user can’t create a document for someone else.</p><pre>@test<br>async &#39;can create self&#39;() {<br>  const uid = this.user().uid;<br>  const user = this.db({ uid }).collection(&#39;users&#39;).doc(uid);<br>  await firebase.assertSucceeds(user.set(this.validUser));<br>}<br><br>@test<br>async &#39;can not create someone else&#39;() {<br>  const user = this.db(this.user()).collection(&#39;users&#39;).doc(uuid.v4());<br>  await firebase.assertFails(user.set(this.validUser));<br>}</pre><p>Additionally, there are several tests that iterate over a number of invalid values and assert they fail.</p><p>There are a number of helper methods I’ve defined in firestore.rules:</p><pre>function isOwner(userId) {<br>  return request.auth != null &amp;&amp;<br>          request.auth.uid == userId;<br>}</pre><p>The isOwner helper checks to see if the user document ID matches that of the current authenticated user.</p><pre>function validString(key) {<br>  return data()[key].trim() == data()[key] &amp;&amp;<br>          data()[key].size() &gt; 0;<br>}</pre><p>The validString helper checks to see that a required string has a value.</p><pre>function validUrl(url) {<br>  return url.matches(&#39;https://[a-zA-Z].+&#39;);<br>}</pre><p>The validUrl helper checks that a string starts with https://. I&#39;m pretty sure this will not correctly validate some URLs but this value should generally be set to a Google CDN so I don&#39;t think invalid hosts will come up.</p><pre>function validCreateTimestamps() {<br>  return data().updatedAt == request.time &amp;&amp;<br>          data().createdAt == request.time;<br>}</pre><p>validCreateTimestamps checks that the updatedAt and createdAt values match the time on the request. This works because the client will set that value with a constant that Firestore will replace with the current time.</p><pre>function validUser() {<br>  // TODO: prevent extra fields<br>  return validString(&#39;nickname&#39;) &amp;&amp;<br>          validString(&#39;fullName&#39;) &amp;&amp;<br>          validString(&#39;photoUrl&#39;) &amp;&amp;<br>          validUrl(data().photoUrl);<br>}<br><br>function validCreateUser() {<br>  return validUser() &amp;&amp;<br>          data().agreedToTermsAt == request.time;<br>}</pre><p>The final two helpers are to validate a user document leveraging all the other helpers. One TODO I&#39;ve left for the future is to make sure all fields on the document are on an <a href="https://github.com/abraham/birb/issues/65">allowed list</a>.</p><p>I’m not completely happy with how the server tests are currently organized. I will probably do some cleanup in the future to try and make everything more elegant. I plan on adding <a href="https://github.com/marak/Faker.js/">faker.js</a> for fun too.</p><h3>Code changes</h3><ul><li><a href="https://github.com/abraham/birb/pull/64">#64 Add Firestore rules for creating users</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c02ddae30ffb" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A month of Flutter: set up Firestore rules tests]]></title>
            <link>https://medium.com/@abraham/a-month-of-flutter-set-up-firestore-rules-tests-d2ac7fec97bd?source=rss-a0f937ae8409------2</link>
            <guid isPermaLink="false">https://medium.com/p/d2ac7fec97bd</guid>
            <category><![CDATA[firebase]]></category>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[tutorial]]></category>
            <category><![CDATA[testing]]></category>
            <dc:creator><![CDATA[Abraham Williams]]></dc:creator>
            <pubDate>Thu, 27 Dec 2018 03:07:46 GMT</pubDate>
            <atom:updated>2018-12-27T03:07:46.372Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*vJrEUw3Hb1Y2eitZ.jpg" /></figure><p><em>Originally published on </em><a href="https://bendyworks.com/blog/a-month-of-flutter-set-up-firestore-rules-tests"><em>bendyworks.com</em></a><em>.</em></p><p>One aspect of using Firestore for my data backend means I need to be certain my <a href="https://firebase.google.com/docs/firestore/security/get-started">security rules</a> are configured correctly. Otherwise users might be able to read or write date they shouldn’t have access to.</p><p>A few days ago I <a href="https://bendyworks.com/blog/a-month-of-flutter-setting-up-firebase-firestore">set up Firestore</a> in the server directory. I&#39;m going to continue that work and configure tests to run on the <a href="https://firebase.google.com/docs/firestore/security/test-rules-emulator">Firestore emulator</a> based off of the <a href="https://github.com/firebase/quickstart-nodejs/tree/master/firestore-emulator/typescript-quickstart">typescript-qickstart</a> example.</p><p>In package.json I&#39;ll add some devDependencies and define several scripts. <a href="https://docs.npmjs.com/misc/scripts">Node package </a><a href="https://docs.npmjs.com/misc/scripts">scripts</a> can be run with npm run &lt;name&gt;.</p><ul><li>postinstall will set up the Firestore emulator after npm install is run in the scripts directory</li><li>start-emulator will will do just that</li><li>pretest will compile the project&#39;s <a href="https://www.typescriptlang.org/">TypeScript</a> files before the test script is run</li><li>test will run the actual tests within the test directory using <a href="https://mochajs.org/">mocha test runner</a></li><li>posttest will cleanup the test *.js and *.js.map files created during pretest</li><li>ci uses a handy <a href="https://www.npmjs.com/package/start-server-and-test">Node package </a><a href="https://www.npmjs.com/package/start-server-and-test">start-server-and-test</a> to start the emulator, wait for it to be ready, run the tests, and then shut down</li></ul><p>I created a new tsconfig.json file with npx tsc --init. The two main changes I made were to target es6 instead of es5 and enable experimentalDecorators for the <a href="https://www.npmjs.com/package/mocha-typescript">mocha-typescript package</a>.</p><p>Within test/firestore.ts I&#39;m defining a FirestoreTest class that will handle loading the rules, and setting up and tearing down test databases. mocha-typescript will use a new instance of this class for each test. Each instance will use a different projectId to avoid different test runs from interfering with each other.</p><blockquote><em>The Cloud Firestore emulator persists data. This might impact your results. To run tests independently, assign a different project ID for each, independent test. When you call firebase.initializeAdminApp or firebase.initializeTestApp, append a user ID, timestamp, or random integer to the projectID.</em></blockquote><blockquote><a href="https://firebase.google.com/docs/firestore/security/test-rules-emulator">Test your security rules</a></blockquote><p>I changed firestore.rules so there was an allowed rule and a denied rule. These will be updated with real rules before the next deploy.</p><pre>service cloud.firestore {<br>  match /databases/{database}/documents {<br>    match /{document=**} {<br>      allow read: if true;<br>      allow write: if false;<br>    }<br>  }<br>}</pre><p>The initial tests for the user collection in user_rules_test.ts look like this:</p><pre>@suite<br>class Users extends FirestoreTest {<br>  @test<br>  async &#39;can read&#39;() {<br>    const user = this.db().collection(&#39;users&#39;).doc(&#39;alice&#39;);<br>    await firebase.assertSucceeds(user.get());<br>  }<br><br>  @test<br>  async &#39;can not write&#39;() {<br>    const user = this.db().collection(&#39;users&#39;).doc(&#39;alice&#39;);<br>    await firebase.assertFails(user.set({ nickname: &#39;alice&#39; }));<br>  }<br>}</pre><p>The @suite, @test, and class style is supported by mocha-typescript. One of the reasons I chose TypeScript instead of JavaScript is because the types are similar to Dart much of the time.</p><p>I created a success test and a failure test to as proof of concept while setting up all the tooling. They get a database handle and assert that it can be read or written to.</p><p>I can now run npm run ci and see the following:</p><pre>  Users<br>    ✓ can read (162ms)<br>    ✓ can not write (95ms)<br>  2 passing (462ms)</pre><p>The next step will be to get these tests running along with the <a href="https://bendyworks.com/blog/a-month-of-flutter-configuring-continuous-integration">existing CI</a>.</p><h3>Code changes</h3><ul><li><a href="https://github.com/abraham/birb/pull/62">#62 Set up Firestore rules tests</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d2ac7fec97bd" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A month of Flutter: awesome adaptive icons]]></title>
            <link>https://medium.com/@abraham/a-month-of-flutter-awesome-adaptive-icons-205fdbb0917a?source=rss-a0f937ae8409------2</link>
            <guid isPermaLink="false">https://medium.com/p/205fdbb0917a</guid>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[design]]></category>
            <category><![CDATA[tutorial]]></category>
            <dc:creator><![CDATA[Abraham Williams]]></dc:creator>
            <pubDate>Tue, 25 Dec 2018 21:07:45 GMT</pubDate>
            <atom:updated>2018-12-25T21:07:45.524Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*PsjahoU6t4_s1vrk.png" /></figure><p><em>Originally published on </em><a href="https://bendyworks.com/blog/a-month-of-flutter-awesome-adaptive-icons"><em>bendyworks.com</em></a><em>.</em></p><p>For this festive day of Christmas, I’m going to do something more fun and add a fancy icon. For android I will be using an <a href="https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive">adaptive icon</a> so that it looks good in any shape and has a nice wiggle of movement.</p><p>A big thank you goes to <a href="https://twitter.com/tommy_emo_">@tommy_emo_</a> for creating this awesome design.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/320/0*2Rf2-v3mMhPSaKV-.png" /></figure><p>There is a nice <a href="https://pub.dartlang.org/packages/flutter_launcher_icons">flutter_launcher_icons package</a> that makes the technical implementation easy.</p><p>To start, require flutter_launcher_icons as a dev_dependency in pubspec.yaml. Then there will be a new flutter_icons key added with several values.</p><pre>flutter_icons:<br>  android: true<br>  ios: true<br>  image_path_android: assets/icon/ic_launcher_xxxhdpi.png<br>  image_path_ios: assets/icon/ios.png<br>  adaptive_icon_background: assets/icon/ic_background.png<br>  adaptive_icon_foreground: assets/icon/ic_foreground.png</pre><p>android and ios set to true specifies that the tool should build icons for both Android and iOS.</p><p>Then there are four images defined:</p><ul><li>image_path_android will be used for the Android app and should be a full icon with transparent edges. This will get several sizes generated by the tool so I use the xxxhdpi size as the source image.</li><li>image_path_ios will be used for the iOS app and <a href="https://stackoverflow.com/questions/26014461/black-border-on-my-ios-icon">should not have any transparent edges</a>. It is the same as the adaptive icon but with both layers in one image.</li><li>adaptive_icon_background is the background scene of the adaptive icon. For Birb it&#39;s the sun and hills.</li><li>adaptive_icon_foreground is the forground of the adaptive icon. For Birb it&#39;s the bird.</li></ul><p>Once those four images were added to the assets/icon directory I ran the flutter_launcher_icons tool to generate the correct assets and added all the changes to git.</p><pre>$ flutter packages pub run flutter_launcher_icons:main<br>Android minSdkVersion = 16<br>Creating default icons Android<br>Overwriting the default Android launcher icon with a new icon<br>Creating adaptive icons Android<br>Overwriting default iOS launcher icon with new icon</pre><p>The new icon on Android’s home screen with a rounded square shape:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*esLm05JZmFd0432F.png" /></figure><p>The new icon in Android’s app switcher with a circle shape:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*eskB4JELgk2OjLX1.png" /></figure><p>The new icon on iOS’s home screen:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Ypa4EPi__z2QQQFd.png" /></figure><p>The new icon in iOS’s app switcher:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*5Ia1b7kG0MRrajT2.png" /></figure><h3>Code changes</h3><ul><li><a href="https://github.com/abraham/birb/pull/61">#61 Add adaptive icons</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=205fdbb0917a" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A month of Flutter: setting up Firebase Firestore]]></title>
            <link>https://medium.com/@abraham/a-month-of-flutter-setting-up-firebase-firestore-735e1eeb3535?source=rss-a0f937ae8409------2</link>
            <guid isPermaLink="false">https://medium.com/p/735e1eeb3535</guid>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[firestore]]></category>
            <category><![CDATA[tutorial]]></category>
            <category><![CDATA[firebase]]></category>
            <dc:creator><![CDATA[Abraham Williams]]></dc:creator>
            <pubDate>Mon, 24 Dec 2018 23:46:17 GMT</pubDate>
            <atom:updated>2018-12-24T23:46:17.876Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*ckuMJqLc0gkLbCZz.jpg" /></figure><p><em>Originally published on </em><a href="https://bendyworks.com/blog/a-month-of-flutter-setting-up-firebase-firestore"><em>bendyworks.com</em></a><em>.</em></p><p>After a user <a href="https://bendyworks.com/blog/a-month-of-flutter-sign-in-with-google">signs in with Google</a> and <a href="https://bendyworks.com/blog/a-month-of-flutter-user-registration-form">registers</a>, their info needs to be saved to a databasee. I’m going to use <a href="https://firebase.google.com/docs/firestore/">Firebase Firestore</a> as my backend. Within the birb codebase I&#39;m going to create a server directory and initialize a Firestore project inside it using <a href="https://www.npmjs.com/package/firebase-tools">firebase-tools</a>.</p><pre>$ firebase init</pre><pre>######## #### ########  ######## ########     ###     ######  ########<br>     ##        ##  ##     ## ##       ##     ##  ##   ##  ##       ##<br>     ######    ##  ########  ######   ########  #########  ######  ######<br>     ##        ##  ##    ##  ##       ##     ## ##     ##       ## ##<br>     ##       #### ##     ## ######## ########  ##     ##  ######  ########</pre><pre>You&#39;re about to initialize a Firebase project in this directory:</pre><pre>/home/abraham/Development/birb</pre><pre>? Which Firebase CLI features do you want to setup for this folder? Press Space to select features, then Enter to confirm your choices.<br> ◯ Database: Deploy Firebase Realtime Database Rules<br>❯◉ Firestore: Deploy rules and create indexes for Firestore<br> ◯ Functions: Configure and deploy Cloud Functions<br> ◯ Hosting: Configure and deploy Firebase Hosting sites<br> ◯ Storage: Deploy Cloud Storage security rules</pre><p>I choose the <a href="https://bendyworks.com/blog/a-month-of-flutter-configure-sign-in-with-google-android">same Firebase project</a> being used for authentication, the default Firestore Rules file, and the default Firestore indexes file. By default .firebaserc is not .gitignored. I have added my .firebasrc to .gitignore because this is an open source project. Anyone who forks Birb will need to set up their own Firebase project.</p><p>In the Firebase console I will now enable Firestore for the project.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*l74Xni8Ey6aRQicL.png" /></figure><p>Here are the default firestore.rules that just say don&#39;t allow reads or writes.</p><pre>service cloud.firestore {<br>  match /databases/{database}/documents {<br>    match /{document=**} {<br>      allow read, write: if false;<br>    }<br>  }<br>}</pre><p>Deploying the rules is handled with the <a href="https://www.npmjs.com/package/firebase-tools">firebase-tools Node package</a>.</p><pre>$ npx firebase deploy<br><br>=== Deploying to &#39;birb-app-dev&#39;...<br><br>i  deploying firestore<br>i  firestore: checking firestore.rules for compilation errors...<br>i  firestore: reading indexes from firestore.indexes.json...<br>✔  firestore: rules file firestore.rules compiled successfully<br>i  firestore: uploading rules firestore.rules...<br>✔  firestore: deployed indexes in firestore.indexes.json successfully<br>✔  firestore: released rules firestore.rules to cloud.firestore<br><br>✔  Deploy complete!<br><br>Project Console: https://console.firebase.google.com/project/birb-app-dev/overview</pre><p>Installing the <a href="https://pub.dartlang.org/packages/cloud_firestore">cloud_firestore package</a> in pubspec.yaml happens last.</p><p>Before integrating with the Flutter code, I’m going to write some rules with so come back soon for that article.</p><h3>Code changes</h3><ul><li><a href="https://github.com/abraham/birb/pull/57">#57 Add Firestore</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=735e1eeb3535" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A month of Flutter: testing forms]]></title>
            <link>https://medium.com/@abraham/a-month-of-flutter-testing-forms-341a674c2150?source=rss-a0f937ae8409------2</link>
            <guid isPermaLink="false">https://medium.com/p/341a674c2150</guid>
            <category><![CDATA[forms]]></category>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[test]]></category>
            <category><![CDATA[tutorial]]></category>
            <dc:creator><![CDATA[Abraham Williams]]></dc:creator>
            <pubDate>Sun, 23 Dec 2018 20:32:52 GMT</pubDate>
            <atom:updated>2018-12-23T20:32:52.185Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*WjlW_YI_OgGhOCig.jpg" /></figure><p><em>Originally published on </em><a href="https://bendyworks.com/blog/a-month-of-flutter-testing-forms"><em>bendyworks.com</em></a><em>.</em></p><p>With the new <a href="https://bendyworks.com/blog/a-month-of-flutter-user-registration-form">user registration form</a> in place, it’s time to make sure the form is <a href="https://flutter.io/docs/cookbook/testing/widget/tap-drag">tested</a> and will work as expected.</p><p>There are basically five different states that need to be tested.</p><h3>Default state</h3><p>This is the view users will first arrive to and here I’m testing that all the components are present as expected.</p><pre>testWidgets(&#39;Renders&#39;, (WidgetTester tester) async {<br>  await tester.pumpWidget(app);<br><br>  expect(find.text(&#39;Register&#39;), findsOneWidget);<br>  expect(find.text(&#39;I agree to the Terms of Services and Privacy Policy&#39;),<br>      findsOneWidget);<br>  expect(find.byType(TextFormField), findsNWidgets(2));<br>  expect(find.byType(OutlineButton), findsOneWidget);<br>  expect(find.byType(Checkbox), findsOneWidget);<br>});</pre><p>To succesfully submit the form, I require the user to provide a nickname and a full name. In the test those values will be provided with <a href="https://docs.flutter.io/flutter/flutter_test/WidgetTester/enterText.html">enterText</a>. After filling out the two TextFormFields and submitting the form, I wait a tick for the success SnackBar to render.</p><pre>testWidgets(&#39;Form can be submitted&#39;, (WidgetTester tester) async {<br>  await tester.pumpWidget(app);<br>  final Finder nickname = find.widgetWithText(TextFormField, &#39;Nickname&#39;);<br>  final Finder fullName = find.widgetWithText(TextFormField, &#39;Full name&#39;);<br>  final Finder submit = find.widgetWithText(OutlineButton, &#39;Register&#39;);<br><br>  expect(find.text(&#39;Form submitted&#39;), findsNothing);<br><br>  await tester.enterText(nickname, &#39;Jess&#39;);<br>  await tester.enterText(fullName, &#39;Jess Sampson&#39;);<br><br>  await tester.tap(submit);<br>  await tester.pump();<br><br>  expect(find.text(&#39;Form submitted&#39;), findsOneWidget);<br>});</pre><p>The success SnackBar is a temporary placeholder so that I have an in-app confirmation the form was submitted. Once user registration logic is in place this messaging to the user will change.</p><pre>void _submit() {<br>  if (_formKey.currentState.validate()) {<br>    const SnackBar snackBar = SnackBar(content: Text(&#39;Form submitted&#39;));<br><br>    Scaffold.of(context).showSnackBar(snackBar);<br>  }<br>}</pre><h3>Required fields</h3><p>Next there are two tests to make sure the nickname and full name fields are required. This checks that the required message was displayed and that the success message was not displayed.</p><pre>testWidgets(&#39;Form requires nickname&#39;, (WidgetTester tester) async {<br>  await tester.pumpWidget(app);<br>  final Finder submit = find.widgetWithText(OutlineButton, &#39;Register&#39;);<br>  await tester.tap(submit);<br>  await tester.pump();</pre><pre>expect(find.text(&#39;Nickname is required&#39;), findsOneWidget);<br>  expect(find.text(&#39;Form submitted&#39;), findsNothing);<br>});</pre><p>There are some improvements that could be made this error display. If a user focuses an errored TextFormField and enters a letter, the error message should disappear. I&#39;ve <a href="https://github.com/abraham/birb/issues/55">created an issue</a> to implement this in the future.</p><p>If a user disables the Terms of Service/Privacy Policy checkbox, they are not longer permitted to register. Here I am testing that the submit button is disabled if the checkbox is unchecked.</p><p>The <a href="https://docs.flutter.io/flutter/flutter_test/WidgetController/widget.html">WidgetTester#widget method</a> is a way to get a reference to a finder&#39;s actual widget. This is how I&#39;m testing to see if the submit button is disabled. Usually you should test UI that the user can see but in this case the state of the button is conveyed to the user through its styling.</p><pre>testWidgets(&#39;Submit disabled if TOS unchecked&#39;, (WidgetTester tester) async {<br>  await tester.pumpWidget(app);<br>  final Finder submit = find.widgetWithText(OutlineButton, &#39;Register&#39;);<br>  final Finder tos = find.byType(Checkbox);<br><br>  expect(tester.widget&lt;OutlineButton&gt;(submit).enabled, isTrue);<br><br>  await tester.tap(tos);<br>  await tester.tap(submit);<br>  await tester.pump();<br><br>  expect(tester.widget&lt;OutlineButton&gt;(submit).enabled, isFalse);<br>  expect(find.text(&#39;Form submitted&#39;), findsNothing);<br>});</pre><h3>Code changes</h3><ul><li><a href="https://github.com/abraham/birb/pull/54">#54 Add registration form</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=341a674c2150" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A month of Flutter: user registration form]]></title>
            <link>https://medium.com/@abraham/a-month-of-flutter-user-registration-form-f57da77853a5?source=rss-a0f937ae8409------2</link>
            <guid isPermaLink="false">https://medium.com/p/f57da77853a5</guid>
            <category><![CDATA[material-design]]></category>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[forms]]></category>
            <category><![CDATA[tutorial]]></category>
            <dc:creator><![CDATA[Abraham Williams]]></dc:creator>
            <pubDate>Sun, 23 Dec 2018 03:53:40 GMT</pubDate>
            <atom:updated>2018-12-23T16:30:41.776Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*WW0gW7Ibne2od4Hf.jpg" /></figure><p><em>Originally published on </em><a href="https://bendyworks.com/blog/a-month-of-flutter-user-registration-form"><em>bendyworks.com</em></a><em>.</em></p><p>After a user <a href="https://bendyworks.com/blog/a-month-of-flutter-navigate-to-user-registration">navigates to the registration page</a>, they should be able to enter their name and agree to the Terms of Service/Privacy Policy.</p><p>I’ll start by updating RegisterPage to render a RegisterForm widget that I&#39;ll create in a minute. Wrapped around the form is a <a href="https://docs.flutter.io/flutter/widgets/SingleChildScrollView-class.html">SingleChildScrollView</a>. This scroll view is for when the keyboard in open and the form can&#39;t fit in the visible space.</p><pre>Scaffold(<br>  appBar: AppBar(<br>    title: const Text(&#39;Register&#39;),<br>    centerTitle: true,<br>    elevation: 0.0,<br>  ),<br>  body: const SingleChildScrollView(<br>    child: Padding(<br>      padding: EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 8.0),<br>      child: RegisterForm(),<br>    ),<br>  ),<br>);</pre><p>The majority of the work is going to be handled in the new RegisterForm. The initial StatefulWidget structure is based on Flutter&#39;s <a href="https://flutter.io/docs/cookbook/forms/validation">building a form with validsation</a> recipe.</p><pre>class RegisterForm extends StatefulWidget {<br>  const RegisterForm({Key key}) : super(key: key);<br><br>  @override<br>  _RegisterFormState createState() =&gt; _RegisterFormState();<br>}<br><br>class _RegisterFormState extends State&lt;RegisterForm&gt; {<br>  final GlobalKey&lt;FormState&gt; _formKey = GlobalKey&lt;FormState&gt;();<br>  bool _agreedToTOS = true;<br><br>  @override<br>  Widget build(BuildContext context) {<br>    return Form(<br>      key: _formKey,<br>      child: Column(<br>        crossAxisAlignment: CrossAxisAlignment.start,<br>        children: &lt;Widget&gt;[<br>          TextFormField(<br>            decoration: const InputDecoration(<br>              labelText: &#39;Nickname&#39;,<br>            ),<br>            validator: (String value) {<br>              if (value.trim().isEmpty) {<br>                return &#39;Nickname is required&#39;;<br>              }<br>            },<br>          ),<br>          const SizedBox(height: 16.0),<br>          TextFormField(<br>            decoration: const InputDecoration(<br>              labelText: &#39;Full name&#39;,<br>            ),<br>            validator: (String value) {<br>              if (value.trim().isEmpty) {<br>                return &#39;Full name is required&#39;;<br>              }<br>            },<br>          ),<br>          Padding(<br>            padding: const EdgeInsets.symmetric(vertical: 16.0),<br>            child: Row(<br>              children: &lt;Widget&gt;[<br>                Checkbox(<br>                  value: _agreedToTOS,<br>                  onChanged: _setAgreedToTOS,<br>                ),<br>                GestureDetector(<br>                  onTap: () =&gt; _setAgreedToTOS(!_agreedToTOS),<br>                  child: const Text(<br>                    &#39;I agree to the Terms of Services and Privacy Policy&#39;,<br>                  ),<br>                ),<br>              ],<br>            ),<br>          ),<br>          Row(<br>            children: &lt;Widget&gt;[<br>              const Spacer(),<br>              OutlineButton(<br>                highlightedBorderColor: Colors.black,<br>                onPressed: _submittable() ? _submit : null,<br>                child: const Text(&#39;Register&#39;),<br>              ),<br>            ],<br>          ),<br>        ],<br>      ),<br>    );<br>  }<br><br>  bool _submittable() {<br>    return _agreedToTOS;<br>  }<br><br>  void _submit() {<br>    _formKey.currentState.validate();<br>    print(&#39;Form submitted&#39;);<br>  }<br><br>  void _setAgreedToTOS(bool newValue) {<br>    setState(() {<br>      _agreedToTOS = newValue;<br>    });<br>  }<br>}</pre><p>It starts out with a <a href="https://docs.flutter.io/flutter/widgets/GlobalKey-class.html">GlobalKey</a> to uniquely identify the form. This will be used later on to validate the state of the form.</p><p>There is also _agreedToTOS, this is a boolean property that is updated to match if the TOS/PP checkbox is checked. If a user unchecks the checkbox, this boolean will turn to false and the submit button will be disabled.</p><p>That brings up an intersting API design for <a href="https://docs.flutter.io/flutter/material/OutlineButton-class.html">OutlinedButton</a>:</p><blockquote><em>If the onPressed callback is null, then the button will be disabled and by default will resemble a flat button in the disabledColor.</em></blockquote><p>So if _agreedToTOS is true, _submittable will be true and the button will have a onPressed callback. If the value is false, the callback will be set to null and the button will be in a disabled state`.</p><pre>OutlineButton(<br>  highlightedBorderColor: Colors.black,<br>  onPressed: _submittable() ? _submit : null,<br>  child: const Text(&#39;Register&#39;),<br>)</pre><p>The <a href="https://docs.flutter.io/flutter/material/TextFormField-class.html">TextFormFields</a> are pretty straightforward. They get some decoration with a labelText since you should always label your inputs. They also get a validator that just checks to see if it has a value or not.</p><pre>TextFormField(<br>  decoration: const InputDecoration(<br>    labelText: &#39;Nickname&#39;,<br>  ),<br>  validator: (String value) {<br>    if (value.trim().isEmpty) {<br>      return &#39;Nickname is required&#39;;<br>    }<br>  },<br>),</pre><p>I choose to go with nickname and full name because I don&#39;t want to make <a href="https://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/">assumptions about names</a>.</p><p>TextFormFields are by default <a href="https://material.io/design/components/text-fields.html#filled-text-field">filled</a> but I like <a href="https://material.io/design/components/text-fields.html#outlined-text-field">outlined</a> so I&#39;m updating the theme. With the addition of these theme changes, the ThemeData definition was growing pretty large so I moved it to its own theme.dart file.</p><pre>ThemeData(<br>  brightness: Brightness.light,<br>  primaryColor: Colors.white,<br>  accentColor: Colors.white,<br>  scaffoldBackgroundColor: Colors.white,<br>  textSelectionHandleColor: Colors.black,<br>  textSelectionColor: Colors.black12,<br>  cursorColor: Colors.black,<br>  toggleableActiveColor: Colors.black,<br>  inputDecorationTheme: InputDecorationTheme(<br>    border: const OutlineInputBorder(<br>      borderSide: BorderSide(color: Colors.black),<br>    ),<br>    enabledBorder: OutlineInputBorder(<br>      borderSide: BorderSide(color: Colors.black.withOpacity(0.1)),<br>    ),<br>    focusedBorder: const OutlineInputBorder(<br>      borderSide: BorderSide(color: Colors.black),<br>    ),<br>    labelStyle: const TextStyle(<br>      color: Colors.black,<br>    ),<br>  ),<br>);</pre><p>The important change is the addition of <a href="https://docs.flutter.io/flutter/material/InputDecorationTheme-class.html">inputDecorationTheme</a>. This sets the border style to be outlined and customizes the color based on the state of the input.</p><p>There are a couple of other theme changes:</p><ul><li>toggleableActiveColor changes the color of the checkbox.</li><li>cursorColor changes the color of the blinking cursor in an input.</li><li>textSelectionColor changes the highlight color when text is selected.</li><li>textSelectionHandleColor changes the color of the handlers to select more or less text.</li></ul><p>One addition I made was to wrap the text for the checkbox in a <a href="https://docs.flutter.io/flutter/widgets/GestureDetector-class.html">GestureDetector</a> so that if a user taps on the label, it will toggle the checkbox value.</p><pre>GestureDetector(<br>  onTap: () =&gt; _setAgreedToTOS(!_agreedToTOS),<br>  child: const Text(<br>    &#39;I agree to the Terms of Services and Privacy Policy&#39;,<br>  ),<br>)</pre><p>Here are some screenshots of the registration form in several different states:</p><p>Empty form:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*pAggndwTALzZlSci.png" /></figure><p>Filled out form:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*4dDhZTRZ0SbQ7U94.png" /></figure><p>Disabled form:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*E3PLjoU9ASa3y-33.png" /></figure><p>Form with errors</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*64X4v7cIaioGOr6n.png" /></figure><p>Come back tomorrow to see how I’ll test the various states of the form.</p><h3>Code changes</h3><ul><li><a href="https://github.com/abraham/birb/pull/54">#54 Add registration form</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f57da77853a5" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>