Chapter 10. Working with data: HTTP, Firestore, and JSON

This chapter covers

  • Serializing JSON data
  • Using HTTP to talk to a backend
  • Using Firebase as a backend
  • Using a Firestore NoSQL database
  • Using dependency injection for reusable code

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.

Figure 10.1. Screenshot of the todo app built in this chapter

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.

10.1. HTTP and Flutter

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.

1

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:

  1. Adding the Flutter http package to your project
  2. Getting the todos with the http package from Typicode
  3. Converting the JSON response into Dart objects
  4. Displaying the data using a ListView

10.1.1. HTTP package

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.

Listing 10.1. Adding the HTTP dependency to your Flutter app
// backend/pubspec.yaml -- line ~19
dependencies:
  http: ^0.12.0+2           1

  • 1 This is the latest version of the package as of this writing. I highly suggest you use this version while following along with this book. I can’t guarantee that newer versions will work with the code in this book.

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.

10.1.2. GET requests

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.

Listing 10.2. An HTTP GET request
// 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
    }
  }
 }

  • 1 HttpServices is a custom class, not one provided by the package. It houses all calls over HTTP. More on this shortly!
  • 2 Client is a class included in the http package, which exposes methods to make HTTP requests.
  • 3 getTodos is the function that I wrote to make the GET request. It provides a list of Todo objects for Flutter to display.
  • 4 This line actually makes the HTTP request. You must await it, because network calls have to be asynchronous to work.
  • 5 This line ensures that the request was successful. If it was successful, then we have the data we need to proceed.
  • 6 This line turns data from the HTTP request into data we can use by de-serializing the body of the respond. JSON serialization is covered in a few pages, but this basically says, “Turn this String of JSON into a Dart object we can use in the app.”
  • 7 Returns the Todo objects in a list
  • 8 Else the call failed and should throw an error.

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]

2

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.

3

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.

10.2. JSON serialization

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 other options

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:

  • Manual serialization
  • Auto-generated serialization using a package

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.

10.2.1. Manual serialization

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.

Listing 10.3. JSON object from getPosts call
// 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.

Revisiting the ListView

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.

Listing 10.4. Using todos in the UI
// 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");
    }
  });

  • 1 todos is a List<Todo> type at runtime, so it should have a length, even if it’s 0. We provide a backup value of 1 because itemCount cannot be null in the ListView widget.
  • 2 In the builder method, the value and title of the CheckboxListTile corresponds to a single todo. If the todos have been fetched and converted into Dart objects, they can be used to configure the children of the ListView. Otherwise, we still need to fetch them, so display a button.
  • 3 Recall that the ListView.builder callback exposes the index of the todo in the list that we’re using to configure the children of ListView.

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:

  1. Fetch the data over HTTP, which returns a JSON blob.
  2. Parse the JSON into a generic Dart object (like a Map).
  3. Turn that object into a specific type (Todo).
About step 2

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.

Listing 10.5. Example of a todo from the Typicode JSONPlaceholder service
{
    "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.

Listing 10.6. Todo class
// 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,
  ); 
 
// ...

  • 1 Notice that each property reflects a key in the JSON map.

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.

Listing 10.7. fromJson factory methods
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
    );
  }
 }

  • 1 The argument name json is actually kind of misleading because, by this time, it’s already a Map. But that’s convention.
  • 2 For each property in the Todo class, pull the same property out of the JSON because, in this case, the property names are all the same.

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.

Listing 10.8. Parsing data out of an HTTP response
// 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),
        );
 // ...

  • 1 The important piece from this line is the json.decode method.

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.

10.2.2. Auto-generated JSON serialization

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

  • 1 json_annotation provides a simple way to tell the project to generate the JSON serializing methods for this class.
  • 2 build_runner is the package you use to run the json_serializable package. It includes running Dart web apps in development mode.
  • 3 json_serializable is the package that actually knows how to generate the code.

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.

10.2.3. 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.

Listing 10.9. Creating a serializable model
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
}

  • 1 Import the json_annotation package.
  • 2 Allows the Todo class to access private members in the generated file. Any file that ends in *.g.dart is a generated file.
  • 3 Tells the code generator that this class needs the logic to be generated
  • 4 This factory method will call a generated method, _$TodoFromJson(json).
  • 5 Creating JSON from a class also calls a generated method.

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.

Listing 10.10. Code generated by the json_serialization package
// 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
  };

  • 1 The generated code includes this method that turns JSON into a Todo ...
  • 2 ... and this, which does the opposite.

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.

10.2.4. Bringing it all together in the UI

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:

  • Todo controller
  • Updates to main.dart
  • The widgets in todo_page.dart
Todo controller

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.

Listing 10.11. Todo controller
// 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;
  }
 }

  • 1 The services are passed into the class.
  • 2 These todos are what will eventually be rendered.
  • 3 This stream tells the UI if the todos are currently loading. If they are, then the UI should show some sort of loading widget.
  • 4 This is the method that will talk to the services.
  • 5 Tells the app that the list is loading, so it knows not to try to display the list
  • 6 Makes the call to get the todos. This is an await call, so the function will pause until that’s finished.
  • 7 Now we have the todos, so the app can go ahead and render them where appropriate.

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.

Create the controller in the main function

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.

Listing 10.12. The root of the Flutter app
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
    );
  }
}

  • 1 Create an instance of the class that makes the actual HTTP calls.
  • 2 Create an instance of the controller that calls those services.
  • 3 Pass the controller into the app.
  • 4 Pass the controller further down in to the widget that needs it.

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.

Todo page UI

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).

Figure 10.2. Todo app screenshot for iOS app

The state object of this widget has three aspects:

  1. It displays a ListView of todos.
  2. It displays a CircularProgressIndicator instead, if the todos are loading.
  3. It fetches the todos when a button is tapped.

Listing 10.13. The Todo list page
// 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
  }
// ...

  • 1 By default, the todos aren’t loading.
  • 2 When this widget renders, tells the controller that it needs to know when the todos are loading
  • 3 This method makes a call to the controller.
  • 4 Use setState so that Flutter knows to re-render when you’ve grabbed the todos.

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.

Listing 10.14. The _TodoPageState build method
// 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),
      ),
    );
  }

  • 1 The body of this widget will change depending on isLoading, which changes in response to the controller.onSync stream.
  • 2 CircularProgressIndicator is a built-in Material widget that shows a spinner (seen later).
  • 3 The todos are rendered with our old pal, the ListView.builder.
  • 4 If there are no todos, I’d like to display a different widget than if there are.
  • 5 The CheckboxListTile is used to display individual todos.
  • 6 Display the value of the body widget (annotation 1 in this list).
  • 7 This app doesn’t fetch todos automatically; you have tell it to by tapping a button.

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.

Figure 10.3. Todo app progression

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.

10.3. Working with Firebase in Flutter

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.

The true potential of 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.)

NoSQL in two paragraphs

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.

10.3.1. Installing Firestore

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):

  1. Sign up for a (free) Firebase account.
  2. Start a new Firebase project.
  3. Add Firestore database to your project.
  4. Register your Android or iOS app with Firestore.
  5. Tinker with the native folders.
  6. Add Firebase and Firestore to your pubspec.yaml file.
  7. Use them.
A disclaimer about installing Firestore

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/.

10.3.2. Create a Firestore project

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]

4

  1. In the Firebase console, click Add Project; then select or enter a project name.
  2. (Optional) Edit the project ID. Firebase automatically assigns a unique ID to your Firebase project. After Firebase provisions resources for your Firebase project, you cannot change your project ID. To use a specific identifier, you must edit your project ID during this setup step.
  3. Follow the remaining setup steps in the Firebase console, and then click Create Project.
  4. Firebase automatically provisions resources for your Firebase project. When the process completes, you’ll be taken to the overview page for your Firebase project in the Firebase console.

10.3.3. Configure your app

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.

Configure iOS

  1. In the Firebase console, select Project Overview in the left navigation pane. Then click the iOS button under Get started by adding Firebase to your app. You’ll see the dialog shown in the following modal (figure 10.4).
    Figure 10.4. A screenshot from the web GUI for Firebase

  2. The important value to provide is the iOS bundle ID, which you’ll obtain using the following three steps.
  3. In the command-line tool, go to the top-level directory of your Flutter app.
  4. Run the command open ios/Runner.xcworkspace to open Xcode.
  5. In Xcode, click the top-level Runner in the left pane to show the General tab in the right pane as shown in figure 10.5. Copy the Bundle Identifier value.
    Figure 10.5. A screenshot from Xcode, showing where to configure your Firestore database

  6. Back in the Firebase dialog, paste the copied Bundle Identifier into the iOS bundle ID field, then click App.
  7. Continuing in Firebase, follow the instructions to download the GoogleService-Info.plist config file.
  8. Go back to Xcode. Notice that Runner has a subfolder also called Runner (as shown in figure 10.5).
  9. Drag the GoogleService-Info.plist file (that you just downloaded) into that Runner subfolder.
  10. In the dialog that appears in Xcode, click Finish.
  11. Go back to the Firebase console. In the setup step, click Next, then skip the remaining steps and go back to the main page of the Firebase console.
Configure Android

I think Android is less cumbersome to set up because you don’t have to deal with any third-party application like Xcode:

  1. In the Firebase Console, select Project Overview in the left nav, then click the Android button under Get started by adding Firebase to your app. You’ll see the dialog shown in figure 10.6.
    Figure 10.6. A screenshot from the web GUI for Firebase

  2. The important value to provide is the Android package name, which you’ll obtain using the following two steps.
  3. In your Flutter app directory, open the file android/app/src/main/AndroidManifest.xml.
  4. In the manifest element, find the string value of the package attribute. This value is the Android package name (something like com.yourcompany.yourproject). Copy this value.
  5. In the Firebase dialog, paste the copied package name into the Android package name field.
  6. Click App.
  7. Continuing in Firebase, follow the instructions to download the google-services .json config file.
  8. Go to your Flutter app directory; then move the google-services.json file (that you just downloaded) into the android/app directory.
  9. Back in the Firebase console, skip the remaining steps and go back to the main page of the Firebase console.
  10. Finally, you need the Google Services Gradle plugin to read the google-services .json file that was generated by Firebase. In your IDE or editor, open android/ app/build.gradle and add the following line as the last line in the file:
    apply plugin: 'com.google.gms.google-services'
  11. Open android/build.gradle. Then, inside the buildscript tag, add a new dependency:
    buildscript {
       repositories {
           // ...
       }
       dependencies {
           // ...
           classpath 'com.google.gms:google-services:3.2.1'   // new
       }
    }

And you’re done!

10.3.4. Add Firebase to your pubspec

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.

Listing 10.15. The finished pubspec file
// 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

  • 1 Firebase core is needed for all Firebase features.
  • 2 Cloud Firestore is the package specific to the database, which we’ll use.

10.3.5. Using Firestore

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.

Listing 10.16. Implement Firebase services
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;
  }
}

  • 1 Import the Firestore package so we can use the API.
  • 2 This class implements the same interface as the http package so we can use dependency injection. If that seems like nonsense, put a pin in your questions for a couple of pages. This chapter closes with a section on dependency injection.
  • 3 Firebase uses objects called snapshots, which represent the database records in a single moment. I will cover this in depth later.
  • 4 AllTodos.fromSnapshot is a method I wrote, which I’ll cover later.

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.

Listing 10.17. Convert a Firestore QuerySnapshot into a Dart object
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);
  }

  • 1 Iterate over all the individual documents (that are of the type DocumentSnapshot) ...
  • 2 ... and turn them into Todo objects, which our UI knows how to render

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.

10.4. Dependency injection

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).

Figure 10.7. Dependency injection diagram

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.

Listing 10.18. Use abstract classes for dependency injection
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.

Listing 10.19. Using dependency injection
//backend/lib/main.dart
 
void main() async {
//  var services = HttpServices();
  var services = FirebaseServices();           1
  var controller = TodoController(services);   2

  • 1 Create an instance of Services that’s specifically set up to use Firebase.
  • 2 Pass the Services object into the controller.

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.

Listing 10.20. Starting up the Flutter app
void main() async {
  var services = FirebaseServices();            1
  var controller = TodoController(services);    2
 
  runApp(TodoApp(controller: controller));      3
}

  • 1 Create a new instance of FirebaseServices, which are specific to Flutter. These services live in the Flutter project.
  • 2 Create a new instance of TodoController and pass it the services. This is a cross-platform class. It doesn’t care what services it gets, just that it’s some implementation of the Services class.
  • 3 Run the app with the controller.

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()));
}

  • 1 Import the shared library, which houses the shared code
  • 2 Create a new instance of WebHttp services, which are specific to the web app.
  • 3 Create a new instance of TodoController and pass it the services. This is a cross-platform class. It doesn’t care what services it gets, just that it’s some implementation of the Services class.
  • 4 Run the app with the controller.

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:

  • Anytime there’s a bug in the logic layer, fixing it once fixes it everywhere.
  • Your clients remain consistent. When you add a new feature to the app, you only write it once, and then write the UI twice. The UI is generally less prone to bugs, so this is nice.
  • Adding to the previous point, it kind of forces you to make your UI as “dumb” as possible, which makes it easier to reason about when there are bugs.

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.

Summary

  • Google has provided packages for HTTP if you want to use a traditional backend.
  • There’s also Firebase packages, which let you use Firebase as a backend with ease. Firebase provides a reactive, NoSql database called Firestore, which is a great combination to use with Flutter.
  • Breaking the controller logic out of your UI and making it a middleman between the UI and services makes your logic layer highly reusable.
  • You can share code between a web app and mobile app using dependency injection.
  • Regardless of the backend you’re using, JSON serialization lets you gather data from external sources and turn it into proper Dart objects.
  • JSON serialization can be done manually or with packages that generate code automatically.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.15.55.18