“Architecture Components” - Flutter ft. RESTful API, SQLite, & Dependency Injection

Alfonso Cejudo
Jul 11 · 8 min read

If you’re like me, then you’ve read myriad examples and tutorials on Flutter and all the individual pieces needed to make a real, publishable app (some assembly required). And while we all now know how to display a counter that can not just increment but also decrement with the power of modern technology, we’re not really sure if that fully realizes the client’s vision of an app that does things. But I’m using BLoC pattern! Why u not impressed?!? Clients, amirite.

So it’s time to put it all together. We all already know about the popular patterns the cool kids are using in the streets: MVC, MVP, MVVM, Eminem, MGM Grand Hotel and Casino, etc. But in terms of app architecture that’s not just testable and scalable but also allows for seamless collaboration with multiple developers in a team, I’ve found the most success by building on top of a concrete example found in the Google Samples repo: Github Browser Sample. After I decided to pick up Flutter and started learning about streams and awaits and such, I quickly found myself hoping to play around with a comparably fleshed out starter pack for Flutter, to no avail. So I went ahead and made the initial version of what can hopefully be a useful one.

Organizational chart depicting Google’s recommended app architecture.
Google’s recommended app architecture

Here’s the organizational chart from the Android Jetpack guide that shows an overview of the different parts of the sample app, as well as the libraries used to carry out the functionality. If you’re coming from iOS, just know that LiveData is pretty much just RxSwift observables that are also lifecycle-aware (i.e. they know if the app is in the background and stuff). Room would be your Core Data, and Retrofit your Alamofire.

Parallel chart for an equivalent Flutter architecture.
Parallel Flutter app architecture

As luck would have it, it seems there are other devs who also wanted to see Flutter versions of libraries with which they were already familiar in Android. This greatly simplified two things: making a parallel Github Browser Sample app for Flutter, and stealing the Google chart and updating the libraries with their Flutter counterparts. In the rest of this article, we’ll go through each library one by one.

Before continuing, I should note that I’m not sure if there’s actually a catchy name like MVP or whatever used to refer to Google’s recommended app architecture. I always just said Android Architecture Components, and then immediately hated how cumbersome it always is to say. And I def don’t wanna say Android Architecture Components for Flutter in any of my remaining years. But making a Flutter version did inspire me to create an image that straps a jet pack onto Dash, the Dart mascot, thus making a bird even birder; Flutter even Fluterer. Alas, we have a simple name for our architecture. This is Fluttest.

Dash stole Bugdroid’s jet pack

If you’re already comfortable with Android Architecture Components and just want to dive into the Flutter code already, you can find the sample app here: https://github.com/alfonsocejudo/fluttest. I’ve scaled down what Google does in their Github Browser Sample app so all Fluttest demonstrates is querying the Github API to display someone’s real name given the input Github username. For the rest of you curious kittens, onwards and downwards.


Chopper, the Retrofit equivalent

Does auto-generating source code based on annotations excite you? Well you hit the jackpot, mofo. The Chopper repo (https://github.com/lejard-h/chopper) by Hadrien Lejard ports the ease of Retrofit’s system to Flutter land:

Like. That’s it. That’s all we need to tell Chopper to GET user information from Github in a modular way. Bro grabs.

Bro grabs.
https://www.youtube.com/watch?v=K7y2xPucnAo

After you’ve defined the Github API with a class, you can switch to your third-favorite command line and enter:

flutter packages pub run build_runner build

Success of the above command creates the actual implementation that will be used to talk to the ChopperClient, an instance of which we pass into the generated _$GithubService() class in lines 7 and 8 above. Now to get this client object you might be tempted to just create a ChopperClient from any old place where you want to make an API call, as in this quick example:

final chopper = ChopperClient(
baseUrl: "http://localhost:8000",
services: [
// the generated service
MyService.create()
],
converter: JsonConverter(),
);

final myService = MyService.create(chopper);

final response = await myService.getMapResource("1");

But remember, we’re trying to make an app that’s scalable and testable. We don’t want to instantiate any new objects in place. We want to inject them via the constructors of classes that want to use those objects. So let’s wrap the ChopperClient into a GithubClient class that can be passed via dependency injection and only concerns itself with talking to anything with Github’s baseUrl. If at some point down the line we want the app to hit another API at a different URL, that API would have its own WhateverClient wrapper class. Separation of concerns, you know?

So I guess we’re done with annotations and source generation now. But wait! There’s Moor…


Moor, the Room equivalent

One of the more annoying things about interfacing with a database in years past was needing to create some POJO class that simply maps the data from a SQL row to properties of an object just so you can use that object in the rest of your app. Room db mitigated this boring routine by killing two birds with one stone in that the class used to describe a database table can also be its Value Object class or Data class. I was relieved to find that Simon Binder brought this functionality over to Flutter with his backwardly-spelled Moor package: https://github.com/simolus3/moor.

So not only can we describe a table with a class:

We can also receive an instance of this class directly as a result of a SQLite query, or even create an instance to insert (or replace) into the db:

I do recommend coding up the database tables and DAOs earlier than later when following Fluttest, as code will have to be generated by build_runner before the IDE stops complaining about missing code. So after you’ve created github_db.dart and user_dao.dart, you can once again run:

flutter packages pub run build_runner build

and the corresponding github_db.g.dart and user_dao.g.dart files will be generated in the same directory and your IDE will stop yelling at you.


The Repository Pattern

At this point we have a way of fetching user information from the Github API and a way of saving User objects to persistent storage, which is important as these days people want to be able to continue to seamlessly use their apps in a cached, offline mode while in the subway or trapped in a basement. We are now ready to coordinate functionality between the API and the database in a UserRepository. However, I do want to reiterate that we’re building the foundation of an enterprise level app.

So when creating the repository class, DON’T:

And DO:

At some point, you are definitely going to want to run tests against your repository, and things aren’t going to go very well if you’re using real, actually running instances of UserDao and GithubService. If instead you pass these into UserRepository via the constructor, then you can use mocks to simulate all the different scenarios you want to test, like receiving a 404 response, for example. But how can we reap such benefits? Ritual sacrifice? Winning Bingo? How do we accomplish this, what was it called? Dependency injection?


Provider, the Dagger substitute

If you’re not already familiar with the concept of dependency injection, or with Dagger for that matter, that’s actually more information than we would want to cover here. But if you can trust that DI can help minimize coupling between different modules (thus allowing for cleaner code reuse) and keep things testable, then we can overlook the why in this article and instead focus on the how.

Remi Rousselet’s Provider (https://github.com/rrousselGit/provider), in collaboration with Google devs themselves, has given us a way to allow for classes to be provided the objects on which they depend as long as those dependencies exist above them in the Flutter widget tree. Which is why we have the following at the tippy top of our sample app:

import 'di/global_providers.dart';class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: globalProviders,
child: MaterialApp(
title: 'Fluttest',
...
onGenerateRoute: Router.generateRoute,
initialRoute: Home.route,
)
);
}
...
}

What’s in our globalProviders? Pretty much the same things you’ll see in Dane Mackier’s article, “Flutter Provider v3 Architecture using ProxyProvider for Injection”.

So we can see now in lines 18–20 how we provide both the UserDao and GithubService to UserRepository. First, we create instances of GithubDb and GithubClient, which require no dependencies for themselves, and are thus independentServices. The entities that depend on these two bad boys (as well as the dependencies they produce) reside in the dependentServices list. We can use a ProxyProvider to pass in anything we’ve already provided (i.e. any code already written above the current line we’re looking at) and produce a new dependency that we want to inject into a constructor somewhere in our MaterialApp.

Another job well done, international Flutter community. 🍻


Set A Screen, BLoC The Shot

Basketball blocked shot.
Trying to avoid the BLoC.

A convenient feature of the Fluttest architecture is that you can (theoretically) easily replace any and all components with something more suitable for you and your use case. Don’t want to have the BLoC pattern between your screen’s widgets and your repository? Use a View Model. Use Redux. Go to town.

Whatever you end up implementing, you’ll likely have to inject it into your screen’s state. But even though we’re using a BLoC for the sample app, you may have noticed that such a class was not included in the globalProviders. Well, that’s because we don’t want the BLoC to be globally available.

If we have a Home StatefulWidget, the corresponding HomeBloc should only be provided to Home and nothing else. So where can we construct Home() and simultaneously make available a HomeBloc() object in isolation? We can actually use the onGenerateRoute feature of our MaterialApp instance where we passed in Router.generateRoute, as shown previously in the MyApp sample. And inside our Router class:

class Router {
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case Home.route:
return MaterialPageRoute(
builder: (_) => ProxyProvider<UserRepository, HomeBloc>(
builder: (context, userRepository, homeBloc) =>
HomeBloc(userRepository: userRepository),
dispose: (context, homeBloc) => homeBloc.dispose(),
child: Home(),
),
);
...
}
}
}

So we instantiate and provide HomeBloc (which takes UserRepository as a dependency for itself), give it a dispose directive for when the time comes, and make it available for Home and Home alone. That’s scope, baby.

Macaulay Culkin in Home Alone.
Home Alone

Where do we go from here?

So that’s the state of Fluttest today. Insert setState() joke. Insert database insert joke. If you have any suggestions for how we can make it an even better jumping off point from which to make awesome, robust Flutter apps, please leave a comment. Until the next installment (perhaps when Google decides to release inject.dart?), keep Fluttering, guys and gals.

The Fluttest sample app repo: https://github.com/alfonsocejudo/fluttest

Jet Set Digital

…and you will know us by the trail of read.

Alfonso Cejudo

Written by

developist. humorer. cash him inside making iOS and Android apps.

Jet Set Digital

…and you will know us by the trail of read.