This chapter covers
At this point in the book, if you’ve been following along in order, you’re ready to build a full, production-ready frontend in Flutter. Truly, you’re finished! If you work at a company that’s considering building a Flutter app, you have all the information you need to start that project or to convince your manager it’s worth it.
But there are an infinite number of topics that, although similar in Flutter to various other SDKs, are pertinent in writing applications. For the rest of the book, I’m going to depart from a Flutter focus on topics you need to leverage in any mobile app. Particularly in the app we’re going to build in this chapter, you probably want to know how to work with a backend or data store. And to talk to almost any backend, you’ll probably want to turn Dart objects into some universal data format, like JSON. That’s what this chapter is about: talking to backends.
With that in mind, the UI work for the remainder of the book is light. In fact, the app that I’m going to make in this chapter looks like the one shown in figure 10.1.
It’s very plain. That’s on purpose. For this chapter, there’s no reason to get bogged down in how an app looks, but rather in how it interacts with other pieces of software.
While writing this book and thinking about which apps to build as examples, I’ve tried my hardest to leave as much “setup” out of the apps as possible. I didn’t want to include sections on, for example, retrieving weather data from a specific public API. That information would only help if you happened to be writing a weather app.
That idea continues in this chapter and the following (to an extent). In the first half of this chapter, I want to discuss communication over HTTP, but I don’t want to focus on the backend itself. Thus, I’m using a free service from Typicode called JSONPlaceholder, which is used to mock arbitrary API calls over the network.[1] The point is that there’s no backend code to write or databases to set up. You can make HTTP calls to the Typicode service, and it will return pre-determined JSON objects.
The Typicode JSONPlaceholder service can be found at https://jsonplaceholder.typicode.com/.
The goal for this first part of the chapter is to make HTTP GET and POST requests in order to simulate using a “real” backend. Using Typicode, I’m going to fetch a list of todos, turn it into Dart objects we can use, and then render them to the screen. I’ll also write a POST request, which will update the todos when marked complete. In general, this requires four steps:
With Flutter, the http package makes it easy to communicate with other APIs via HTTP. To start, the package must be added to the pubspec.yaml file as shown in the following listing.
// backend/pubspec.yaml -- line ~19 dependencies: http: ^0.12.0+2 1
Once a dependency is added, you must run flutter packages get in your terminal from the project root, which downloads the package and makes its code available in your Flutter project. From here we can move on to using the package in a project.
Now that the http package is installed, you can see how it’s used in the services directory of the app. I suggest that you read this code and the remarks about it that follow before getting caught up in confusion. After I show you the example, I will be able to explain the concepts in depth, but the context will help you understand it.
// data_backend/lib/firestore/services.dart -- line ~13 class HttpServices implements Services { 1 Client client = Client(); 2 Future<List<Todo>> getTodos() async { 3 final response = await client.get( 4 'https://jsonplaceholder.typicode.com/todos', ); if (response.statusCode == 200) { 5 var all = AllTodos.fromJson( json.decode(response.body), ); 6 return all.todos; 7 } else { throw Exception('Failed to load todos '); 8 } } }
This might seem simple—and it is. There isn’t much to making HTTP requests. (Of course, this JSON server doesn’t require headers or authentication, so this is a basic example.)[2] The point is that making HTTP requests in Flutter (and Dart server-side apps) is fairly straight-forward.[3]
If you aren’t familiar with HTTP requests and need more information about headers, you can look to this general article about HTTP: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers.
You can also look at this repository for more examples of the http package: https://github.com/dart-lang/http.
To be fair, though, there is an essential piece of code that was the most glossed over in the example: using JSON serialization to turn the information gathered over the wire into Dart objects we can use. This part is actually more involved than the HTTP request itself, and I’ll cover that next.
First, what do we mean by serialization? In the context of making network calls, it’s a term that means something like “converting objects in a programming language to a lightweight, standard data format that can be sent over the network.” De-serialization is the opposite. It’s the act of converting that lightweight, standardized data into code, specific to the programming language you’re using. Usually, that standard data format is JSON, which I’ll focus on in this book.
There are, of course, other standardized data formats that can be sent over HTTP requests, such as XML. The fundamentals remain the same regardless of the format. In this book, I’m specifically talking about turning JSON into Dart objects and Dart objects back into JSON.
In Flutter apps, you have two options when it comes to serialization:
I want to talk about both. Manual serialization is worth doing in small apps with classes that aren’t too robust. And if you aren’t familiar with serialization, seeing it implemented manually helps clarify the concept. That said, auto-generating classes with serialization is far easier when writing Dart code, and you’ll almost always want to do that in the real world. If you’re a veteran app developer, you can probably skip to the section about auto-generation, because I’ll discuss the Dart (and Flutter) package that provides this functionality for you.
So far, I’ve only shown you one code listing in this chapter: making an HTTP GET request. That GET request returns a JSON object. That JSON object is actually just a string with specific formatting and placement of braces and semicolons. It looks like the following listing when we get it from the GET request.
// JSON '[ { "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false }, { "userId": 1, "id": 2, "title": "quis ut nam facilis et officia qui", "completed": false }, { "userId": 1, "id": 3, "title": "fugiat veniam minus", "completed": false }, // ... more todos ]
Notice the quotes on the very outside of this object. It is important to understand that this “map” is truly just a string, but the string happens to have very specific formatting. If you disregard the fact that this is actually a string, you’ll see that this data is a list of maps. (Importantly, it could be a Map at the highest level. This example just happens to be a List, but JSON, in general, provides a way to organize data using only maps, lists, numbers, booleans, and strings.)
Now, in order to use that data in a Flutter app, we can convert the JSON string into Dart objects. Once those todos are usable objects that our Dart code understands, we can use them to configure widgets in the UI.
First, let me show you how you might use these objects in widgets. Then, I’ll work backwards and show you how to turn the JSON into Dart objects.
The following example uses the ListView widget, which was discussed way back in chapter 3. Using this widget isn’t the focus right now, but if you don’t remember the API for that widget, it might be helpful to revisit it. That said, the new, more important parts all live in the if/else block.
// Imagine todos is a variable that's a list of Todo objects that have been // converted from JSON. A pseudo-code example would be: // List<Todo> todos = TodoController.getTodosAsObjects(); ListView.builder( itemCount: todos != null ? todos.length : 1, 1 itemBuilder: (ctx, idx) { if (todos != null) { 2 return CheckboxListTile( onChanged:(bool val) => updateTodo(todos[idx], val), value: todos[idx].completed, 3 title: Text(todos[idx].title), ); } else { return Text("Tap button to fetch todos"); } });
This code example is just showing how you’d configure your UI (read: widgets) from Dart objects. The point is to demonstrate that you’d likely use something like a ListView because you don’t know the length of the list of data before runtime.
Before you can use those objects in your UI, though, you have to fetch the data and then massage it into Dart objects. That’s the meat of what this section will be about. In general, that’s done in three steps:
Step 2 is a necessary interim step because Dart provides ways to turn a JSON string into a Map. Otherwise, you’d have to turn a string of JSON into a object (like a Todo) directly, and that would require a pretty gnarly algorithm.
Finally, as a side note, this app has an extra step because the JSON is actually a List of Map types. Dart’s JsonSerialization library (and most libraries for other languages) are specifically designed to process JSON that represents an object, as opposed to JSON that’s a list at the top level. (This makes sense if you consider an object-oriented language like Dart. Serialization is all about turning raw data into the building blocks of the language: objects. This will become more clear by the end of this section.) For now, though, let’s just look at turning a single todo from the JSON into a Todo. The JSON in this example will look like the next listing.
{ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false }
As you can see, it’s just a map. It’s a collection of keys and values. Perhaps those keys and values can be turned into the properties on an object. To start, the Todo class looks like the following code.
// shared/lib/src/todo_model.dart class Todo { final int userId; final int id; final String title; bool completed; Todo( this.userId, 1 this.id, this.title, this.completed, ); // ...
This may be obvious at this point, but I’d like to walk you through an example of converting that map into a Todo anyway. The first step is actually to write the method that will turn a Dart Map into the Todo. (We’ll worry about converting the string from the HTTP response into a Map next.)
In order to convert that Map, it’s convention to write a factory method for the class called fromJson(). This method will take in a Map as an argument and then create a new Todo from that Map. The Todo.fromJson looks like the next listing.
class Todo { final int userId; final int id; final String title; bool completed; Todo(this.userId, this.id, this.title, this.completed); factory Todo.fromJson( Map<String, dynamic> json, 1 ) { return Todo( json['userId'] as int, 2 json['id'] as int, json['title'] as String, json['completed'] as bool ); } }
This instance of a fromJson method is fairly simple. You’re literally just extracting properties from the map using square bracket notation. Using the as keyword ensures that the properties are the correct type, or it throws an error if they can’t be parsed into the right type.
That’s most of what’s required (by the developer) to convert JSON into an object. You can probably guess that a robust, complicated class would be much less fun to deserialize manually. (For example, imagine a class whose properties contain List objects and other custom objects. Imagine that instead of userId, there’s a property on the Todo that specifies a User object. You’d need to call User.fromJson within the Todo.fromJson method. I’ll show you a way to do that with ease in a bit.)
There’s one more step, though, and it happens in the GET request we looked at earlier, as shown in the next listing.
// backend/lib/services/todos.dart -- line 18 Future<List<Todo>> getTodos() async { final response = await client.get('https://jsonplaceholder.typicode.com/todos'); if (response.statusCode == 200) { // If the call to the server was successful, parse the JSON var all = AllTodos.fromJson( 1 json.decode(response.body), ); // ...
Recall that the Todo.fromJson factory method requires a Map<String, dynamic> type as an argument, but the data we get from the response is really a String. Part of the Dart standard library contains a nice JSON converter. Simply calling json.decode (String) will turn that into a Map for you. You technically could decode the JSON blob yourself, but there’s never any circumstance in Dart where you’d need to write that code. Therefore, in this situation, I won’t spend time showing you how to do that conversion manually.
There are multiple packages that will generate Dart classes for you. The simplest, and the one I like to use, is called json_serializable. When using this package, you write classes as you always have, and you also write a fromJson and toJson method on those classes. These two methods, fromJson and toJson, are just stubs that call out methods that this package generates for you. In short, you don’t have to write the cumbersome code of extracting every key-value pair yourself. This package is pretty slick.
To use json_serializable, you actually need to add three dependencies to your project:
// backend/package.yaml -- line ~9 dependencies: flutter: sdk: flutter http: ^0.12.0 json_annotation: ^2.0.0 1 dev_dependencies: flutter_test: sdk: flutter build_runner: ^1.0.0 2 json_serializable: ^2.0.0 3
Once those are installed and flutter packages get has been run, you can start writing the code needed to generate the fromJson and toJson methods. To show this, start by updating the Todo class.
The Todo class in the project, which uses json_serializable, actually looks like the following listing, which has everything you need to generate some code that will serialize and deserialize JSON.
import 'package:json_annotation/json_annotation.dart'; 1 part 'todo.g.dart'; 2 @JsonSerializable() 3 class Todo { final int userId; final int id; final String title; bool completed; Todo(this.userId, this.id, this.title, this.completed); factory Todo.fromJson(Map<String, dynamic> json) => _$TodoFromJson(json); 4 Map<String, dynamic> toJson() => _$TodoToJson(this); 5 }
If this was a new project, and the generated code didn’t exist yet, you’d have errors in that file because todo.g.dart doesn’t exist yet, nor do the methods that are being called from that file. (In fact, if you’re following along with the source code, you can delete the todo.g.dart file from the repo to watch the magic happen.) To generate that file, you need to go to your terminal and run
flutter packages pub run build_runner build
By running this command in the root of your project directory, build_runner finds all the classes that need code generated and generates it. (As a reminder, these classes are those that are annotated with @JsonSerializable()). The package creates (or overwrites) the todo.g.dart file, which has this logic in it.
// backend/lib/model/todo.g.dart // GENERATED CODE - DO NOT MODIFY BY HAND part of 'todo.dart'; // ************************************************************************ // JsonSerializableGenerator // ************************************************************************ Todo _$TodoFromJson(Map<String, dynamic> json) { 1 return Todo( json['userId'] as int, json['id'] as int, json['title'] as String, json['completed'] as bool, ); } Map<String, dynamic> _$TodoToJson(Todo instance) => 2 <String, dynamic>{ 'userId': instance.userId, 'id': instance.id, 'title': instance.title, 'completed': instance.completed };
That’s all there is to taking advantages of packages that generate serialization functionality for you. If you have a big app with robust classes, it’s much quicker to run a command in the terminal than to write the code that will parse the maps on your own.
Now that you know the app can grab data over the network, and you know that the data can be serialized into proper Dart classes, it’s time to bring it all together for its original purpose: to display that information in the UI. For the sake of focusing on the task at hand, I chose to use, basically, no state management pattern. The information is fetched from a controller right from the widgets. There are three pieces of code involved here:
This is a class that basically acts as a messenger between the HTTP services and the widgets. It’s responsible for telling the UI what the todos are, and what to render. That’s a bit abstract, so let’s look at the code in the next listing.
// backend/lib/controllers/todo.dart class TodoController { final Services services; 1 List<Todo> todos; 2 StreamController<bool> onSyncController = StreamController(); 3 Stream<bool> get onSync => onSyncController.stream; TodoController(this.services); Future<List<Todo>> fetchTodos() async { 4 onSyncController.add(true); 5 todos = await services.getTodos(); 6 onSyncController.add(false); 7 return todos; } }
This controller method, in human English, is saying, “Oh, UI, you want some data to render? Okay, then set your status to loading while I grab that data from the services.” Some time passes. Then it says, “Okay, I got your data, you aren’t loading anymore, you can render this.”
The point of this controller is basically to keep the UI as dumb as possible. The UI knows about this controller because it’s passed in from main.dart.
The main function can be used for any setup that needs to be done before the app renders. In this case, we need to create the controllers and services that the app will use.
void main() async { var services = HttpServices(); 1 var controller = TodoController(services); 2 runApp(TodoApp(controller: controller)); 3 } class TodoApp extends StatelessWidget { final TodoController controller; TodoApp({this.controller}); @override Widget build(BuildContext context) { return MaterialApp( home: TodoPage(controller: controller), 4 ); } }
There isn’t much to this. I just wanted to show you this so that when I show you the widget, you know where it got its reference to the controller.
The Todo page, shown in listing 10.13, is a StatefulWidget that just grabs the todos and displays them in a list (figure 10.2).
The state object of this widget has three aspects:
// backend/lib/todo_page.dart -- line ~14 class _TodoPageState extends State<TodoPage> { List<Todo> todos; bool isLoading = false; 1 void initState() { super.initState(); widget.controller.onSync.listen( (bool syncState) => setState(() { 2 isLoading = syncState; })); } void _getTodos() async { var newTodos = await widget.controller.fetchTodos(); 3 setState(() => todos = newTodos); 4 } // ...
That’s the first half of the _TodoPageState object. That’s the functionality so you have context for the widgets in the build method, shown in the following listing.
// backend/lib/todo_page.dart -- line ~37 Widget get body => isLoading 1 ? CircularProgressIndicator() 2 : ListView.builder( 3 itemCount: todos != null ? todos.length : 1, 4 itemBuilder: (ctx, idx) { if (todos != null) { return CheckboxListTile( 5 onChanged:(bool val) => updateTodo(todos[idx], val), value: todos[idx].completed, title: Text(todos[idx].title), ); } else { return Text("Tap button to fetch todos"); } }); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Http Todos"), ), body: Center(child: body), 6 floatingActionButton: FloatingActionButton( onPressed: () => _getTodos(), 7 child: Icon(Icons.add), ), ); }
This screen really has three states: Loading; Not loading, but there’s no data; and Not loading, and there is data. Figure 10.3 shows examples of these states.
This chapter, so far, might be fairly straightforward if you’re both comfortable with Flutter and a veteran app builder. Fetching JSON over HTTP and deserializing that JSON isn’t specific to Flutter. And what’s more, Flutter and Dart provide straightforward APIs to do so.
But this isn’t (necessarily) the “Flutter” way. The Flutter team seems to be all about using Firebase as a backend. (To be clear and not misrepresent anyone, they didn’t say that. I’m inferring that because such a large majority of official Google tutorials and docs use Firebase.) Because of that, the rest of this chapter will be devoted to using Firebase, rather than the HTTP package. Think of it as a different method of accomplishing the same thing: talking to external backends.
Firebase is a cloud platform by Google that provides a ton of features. In the simplest terms, it’s a backend-as-a-service. It can handle auth, it has a database, it can be used to store files like images, and it does more. It’s a pretty incredible product, really, because of how all-encompassing it is. In this book, though, I want to focus on a database service that’s part of Firebase, called Firestore.
Firestore is not, technically, only a database. It also handles the communication between your app and the data in Firestore, and does so in an opinionated way. It’s fair to think of it more as a complete solution to storing data and working with the data in your app.
Again, Firestore isn’t just a NoSql database. It also provides a way to reactively interact with its data. Specifically, you can subscribe to the data.
When code subscribes to data in Firestore, that code knows when the specified data changes, and your app can respond accordingly. We aren’t going to be concerned with that in this simple app, but it’s helpful to keep in mind that Firestore excels in reactive programming. (This is likely why the Flutter team uses Firestore in examples so often. They go together like peanut butter and jelly.)
Firestore is a NoSQL database, like Mongo (but it’s also much more). First, I have to make clear that NoSQL (and SQL) are outside of the scope of this book.
The short explanation of NoSQL databases is this: NoSQL databases store your data as nested objects, like a giant JSON blob. The data isn’t structured in a specific way, and there aren’t tables. There aren’t relationships, and you cannot join data tables like you can in SQL. Instead, there are collections and documents. Collections represent a List of documents, and documents are basically Map objects that represent records of data. Documents can have collections as properties. In a robust app, your data basically ends up as one giant key-value map.
This is how Firestore stores data. It provides a JSON-like blob of unstructured data. In fact, every document in a collection can have different properties than its siblings. This wouldn’t be ideal, but it is possible.
Unfortunately, I have to break the one rule I’ve tried to stick to throughout this book: avoid setup. Firebase can’t be used without doing some configuration in the android and iOS folders of the Flutter lib. This is the first (and only) time in this book that you’ll need to interact with platform-specific code. So please bear with me while I give you step-by-step instructions to do this monotonous task.
The good news is that it’s as easy as copying and pasting, because there’s no logic involved. If there are no issues, you can be set up in a couple of minutes. The other good news is you don’t have to write any Objective-C or Java; you only have to update the configuration.
In general, you need to follow these steps (the sections following this outline this process):
A giant disclaimer here is that this is (for some readers) a book. And a book doesn’t have WiFi or a data plan, so you can’t click links. That said, I’m going to tell you exactly which websites you need to go to for an in-depth guide to installing Firestore. And it’s all well documented. These steps should be sufficient if you don’t run into any issues. If you do run into a problem and my explanation isn’t getting you anywhere, Google has provided this thorough guide: https://codelabs.developers.google.com/codelabs/flutter-firebase/.
First, you have to go to firebase.google.com and set up an account. It’s a standard, quick process. Then, create a project. In Firebase, you’ll basically have a new project for every app you build. Here are some instructions directly from the Firebase docs:[4]
These steps come from https://firebase.google.com/docs/flutter/setup.
Now comes the fun part. Before we can use the Firebase packages in Flutter, we need to tell the native app platforms (iOS and Android) that we’re using those. The process is different for the two platforms, so you should follow the one that you use when developing. For example, I do all my testing for Flutter in iOS, so I wouldn’t bother adding Firebase to the Android app unless I plan on releasing the app to production.
I think Android is less cumbersome to set up because you don’t have to deal with any third-party application like Xcode:
apply plugin: 'com.google.gms.google-services'
buildscript { repositories { // ... } dependencies { // ... classpath 'com.google.gms:google-services:3.2.1' // new } }
And you’re done!
I’m sorry if that was painful. Configuration and setup is my least favorite part of making software. But now we can get back to Flutter. The last setup step is adding the right packages to your pubspec.yaml file. The next listing shows the finished dependencies list for this example app.
// backend/pubspec.yaml dependencies: flutter: sdk: flutter http: ^0.12.0 json_annotation: ^2.0.0 firebase_core: ^0.3.4 1 cloud_firestore: ^0.9.1 2
From a high level, there are two steps to using Firebase now. First, write the services that talk to Firestore. Then, call the services in the app. (Of course, this is an oversimplified explanation.)
First, let’s just talk about the services in the next listing. This is all found in the lib/services/todo.dart file.
import 'package:cloud_firestore/cloud_firestore.dart'; 1 class FirebaseServices implements Services { 2 // ... @override Future<List<Todo>> getTodos() async { QuerySnapshot snapshot = 3 await Firestore .instance .collection("todos") .getDocuments(); AllTodos todos = AllTodos.fromSnapshot(snapshot); 4 return todos.todos; } }
The important part of that code, for this section, is the line that deals with the object QuerySnapshot. There’s a lot going on there. Let me talk about it piece by piece.
QuerySnapshot is a class that represents some data from the database at any given moment. The term snapshot is used because Firestore is a real-time database, so the data is theoretically changing all the time. A snapshot says, “This is the data you wanted in the moment that you asked for it.” It’s a common term in NoSQL databases.
On the other side of the equals sign, the first important chunk is Firestore .instance. instance is a static getter on the Firestore package that represents the database itself. All calls to Firestore in your app will start by grabbing Firestore. instance.
Next, collection is a method that retrieves a collection from your database. There are two types of objects in Firestore: documents and collections (which are a Map of documents). The collection expects a path that corresponds to the data in your database. In this case, todos is a top-level collection in the database. If you were looking for sub-todos of a todo, the path might be todos/$id/subtodos, where id represents a specific todo, and subtodos is a collection on that document. (This app doesn’t deal with nested data, so that’s just an example.) And finally, getDocuments grabs all the documents from that collection and returns them as a QuerySnapshot.
So, from a high level, this function is basically saying, “Hey, Firestore, give me all the documents you have nested under the key todos at this exact moment.” Then the function passes that QuerySnapshot to AllTodos.fromSnapshot, which turns that snapshot into some Dart classes we care about. The AllTodos.fromSnapshot method looks like the following listing.
factory AllTodos.fromSnapshot(QuerySnapshot s) { List<Todo> todos = s.documents.map<Todo>( (DocumentSnapshot ds) { 1 return Todo.fromJson(ds.data); 2 }).toList(); return AllTodos(todos); }
That’s a simple example, and maybe it seems like there’s a lot more that I haven’t covered yet, but that’s what using Firestore is about. If you have a good understanding of streams, which you hopefully do from the previous chapter, and can work with Query-Snapshots and the Firestore.instance, that’s 99% of what you need to know to work with Firestore. Basically, using Firestore is all about making queries and then deserializing data into Dart objects.
Dependency injection is an important concept if you’re building multiple clients for the app (for example, a web app and a Flutter app). It makes your code highly reusable and lets you share it across many platforms. If that sounds abstract, consider this: you can write server-side apps, web apps, and Flutter apps in Dart. There are, however, two different packages that Dart uses to make HTTP requests—one for web apps because they compile into JavaScript to run in the browser, and one for server-side and Flutter apps because they don’t compile to JavaScript. So, if you have a Flutter app and a web app, the service that these two apps use to get the same data from Typicode is different.
Wouldn’t it be nice if you could write controllers that didn’t care about which platform is being used? By that I mean, wouldn’t it be nice if the UI could just call service.getTodos but didn’t really care what exactly the service is? That way, the web UI could call the same method as the Flutter UI, but the HTTP request that’s made would be different. This concept is called dependency injection, as figure 10.7 illustrates. It’s a way to share code between multiple apps (among other things).
In the backend app I wrote for this chapter, I used dependency injection to “inject” the service dependency into the controller of the app. Notice that when fetching todos, the _TodoPageState object calls widget.controller.fetchTodos. And that method calls services.getTodos, and the TodoController is passed in a Services object. So, does the controller really care about what services are called? No. It only cares that it gets back a list of Todo objects when it calls services.getTodos.
If this is confusing, consider the TodoController for a moment. It declares a member with the line final Services services. But the lib/services/todos.dart file has three classes in it, as shown in the following listing.
abstract class Services { Future<List<Todo>> getTodos(); Future<Todo> updateTodo(Todo todo); Future addTodo(); } class HttpServices implements Services { // ... class FirebaseServices implements Services { // ...
The first class is abstract. In some languages, this is similar to an interface. If you aren’t familiar with interfaces, that’s a class that you can’t create instances of directly, but will keep other classes you create honest.
The following two classes implement the abstract class, effectively saying, “I am of type Service, but my methods have logic that may be different than other classes that implement Service.”
To make that example concrete, let’s look at the main.dart file. At the very top of the main function, you can inject the proper service into the controller, as shown in the next listing.
//backend/lib/main.dart void main() async { // var services = HttpServices(); var services = FirebaseServices(); 1 var controller = TodoController(services); 2
The point is that the controller doesn’t care which instance of the Services class it gets, because it just calls Services.getTodos. Because the base Services class declares that it will have a function called getTodos, all classes that implement it are required to have that method and return the same type from those methods. So, the HttpServices and FirebaseServices classes will have a method called getTodos that returns a Future<List<Todo>.
In real life, this example is somewhat contrived. It’s unlikely that you’d have two different service implementations for the same client (such as a Flutter app), but you may have a different Services implementation for a web app and a Flutter app. Using dependency injection, both apps can use the same controller class, which means you don’t have to rewrite the logic layer (the controller) of the app.
For a concrete example, I wrote a web app that uses the same models and controllers as the Flutter app. All this app does is fetch the todos when the app starts and prints them in the console. But the example shows how dependency injection is useful for multiple clients.
I created two new projects to make this happen: shared, which is where the shared controllers and models live, and data_backend_web, which is a bare-bones Dart web app. This isn’t a book about writing web apps, so I’m just going to show the relevant code in the following listing. Recall that this is how the Flutter app is started up in the main.dart file.
void main() async { var services = FirebaseServices(); 1 var controller = TodoController(services); 2 runApp(TodoApp(controller: controller)); 3 }
And the FirebaseServices class is created like this:
class FirebaseServices implements Services {
Recall that implements means that this class guarantees that it will have all the same methods that Services declares. This is how the app is certain that it can call services .getTodos and that method will exist and return the same type, regardless of what that method does. Then, in the shared project, the WebHttp services is implemented in the same way:
class WebHttp implements Services { Client client = Client(); @override Future<List<Todo>> getTodos() async { // ...
The Services class is implemented to ensure that the WebHttp class can be used anywhere the Services class type is required. Finally, to drive it home, this is how you start the web app:
// data_backend_web/lib/main.dart import 'package:shared/shared.dart'; 1 void main() { var service = WebHttp(); 2 var controller = TodoController(service); 3 runApp(controller); 4 } runApp(TodoController controller) async { List<Todo> todos = await controller.services.getTodos(); todos.forEach((Todo t) => print(t.toString())); }
That’s the whole example, and the power lies in the fact that you got to reuse code for models and controllers. In a robust app, this pays off quickly for a couple of reasons:
This is a powerful approach to sharing code with Dart between web apps and mobile apps (which is now possible in Dart thanks to Flutter). If you’re building an app with multiple clients, this is an excellent way to guarantee all the apps are on the same page. At my day job, whenever we have a bug in our app, we can fix it once (usually in Flutter because the development environment is so wonderful), and it just works in both web and mobile. That saves a ton of time and resources.
52.15.55.18