Firebase, Provider and Flutter

In this article, we’ll be covering how to make a super basic movie tracker app with a Firebase backend!

The hell is Flutter? Well I’m not going to explain it in depth here, but it’s a brush, and Dart (the programming language) is the paint, then you have a canvas which would be the screen you want to paint on! That’s that.

We’ll be focusing on how to use Firestore with Provider, so we’ll start with using the anonymous sign in!

Final thing before we jump into code, what is Provider? Well, provider is a state management “tool” that gives all of our widgets access to an object or several objects (depending on how big the app is)!

Dependencies

Firestore

Firebase auth

Provider

Setup

I’ll be doing most of the code for the web, however, very little changes between each platform, follow the setup steps to get everything working :) Make sure you are also on the beta channel for flutter if you want to build for the web!

Here is the repo for the initial code I’m going to start with:

https://github.com/karthikjn01/setup_firebase_provider_codelab

The only thing you need to change is the GoogleServices.json file/GoogleServices.plist/index.html (for web).

Code

We’ll be pretending like this is a massive project and setting up the folder structure to follow suit. Usually, for a small app such as this, we can use fewer files/folders.

Add this structure to the Lib folder of the app!

Movie Model

Let’s start with the task model, it’s going to contain a string title, and whether it is completed or not, that’s it. Also for the sake of simplicity, we will keep track of the id.

class Movie{
String title;
bool watched;
String id;
}

This is what it should look like so far, now let’s add in a constructor that will take a map (from firebase) and return a Task.

Movie.fromMap(Map<String, dynamic> data, this.id){
this.title = data["title"];
this.watched = data["watched"];
}

A small little function that will help us a tonne further down the line.

AppData Provider

This file will contain information about the app, such as a list of the todo items, but we can treat this as a general object and don’t need to really code differently!

Let’s start with the basic structure.

import 'package:flutter/widgets.dart';

class AppProvider with ChangeNotifier{
List<Movie> movies = [];
}

Now add in the provider to the main.dart file.

runApp(
ChangeNotifierProvider<AppProvider>(
create: (_) => AppProvider(),
child: MyApp(),
),
);

Let’s set up the auth and listeners, and create a sign-in method (replace this with your own sign in methods!)

FirebaseAuth auth;
User user; //null if not signed in
AppProvider() {
auth = FirebaseAuth.instance;
setupAuthListener();
}

Future<UserCredential> signIn(){
return auth.signInAnonymously();
}

setupAuthListener() {
auth.authStateChanges().listen((user) {
print("is user signed in: ${user != null} as ${user?.uid}");
this.user = user;
if(user != null){
load();
}
});
}

Now let’s sign the user in on app start. Change the “MyApp” widget to stateful and add this into the init state method,

@override
void initState() {
super
.initState();
Provider.of<AppProvider>(context, listen: false).signIn();
}

Now the provider is trying to sign the user in!

Once the provider has signed the user in, we want to retrieve the first 30 elements in Firestore! Let’s make a function called “load” which will handle loading and pagination of data.

load() async {
if (user == null || endReached || loading) return;
loading = true;

if (lastLoaded == null) {
movies.clear();
}

Query q = FirebaseFirestore.instance
.collection("Movies")
.where("uid", isEqualTo: user.uid)
.orderBy("title");

if (lastLoaded != null) {
print("Loading more: ${movies.length}");
q = q.startAfterDocument(lastLoaded);
}

QuerySnapshot query = await q.limit(5).get();

if(query.docs.length != 5){
endReached = true;
}

lastLoaded = query.docs.last;
query.docs.forEach((element) {
movies.add(Movie.fromMap(element.data(), element.id));
});
loading = false;
notifyListeners();
}

All this function does, it gets new data and adds it to the list of movies. Pagination is great to avoid loading unnecessary data.

UI

Great, we are done with the backend stuff, let’s get onto the frontend!

We want to build a basic listview to display the movies, and we also want a textfield to add a new movie to the list. Let’s start by replacing the MyHomepage widget with our own homepage!

class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(

);
}
}

A basic scaffold and nothing else, add in a Consumer<AppProvider>.

body: Consumer<AppProvider>(
builder: (context, app, child) {

},
),

Inside the builder place the column with a listview and a textfield.

return Column(
children: [
Expanded(
child: ListView.builder(
itemBuilder: (context, index) {
return;
},
itemCount: app.movies.length,
),
),
TextField(),
],
);

For the listview, we’ll use an item builder so that we can check if we are near the end of the list to load more.

if(index > app.movies.length - 2){
app.load();
}

Now let’s create a component for the movie.

class MovieView extends StatelessWidget {
Movie movie;
Function(bool) onChange;

MovieView(this.movie, this.onChange);

@override
Widget build(BuildContext context) {
return Container(
child: Row(
children: [
Checkbox(value: movie.watched, onChanged: onChange),
Expanded(child: Text(movie.title)),
],
),
);
}
}

A basic widget with a checkbox and a text that takes a Movie object and a function that will be called when the checkbox value is changed! Let’s add the return statement for the listview so that the user can actually see the movies.

return MovieView(app.movies[index], (v){
app.setWatched(app.movies[index].id, v);
});

Almost done, let’s add two more functions in the AppProvider class, one to set watched and one that will add a new entry in firestore!

void setWatched(String id, bool v) {
FirebaseFirestore.instance
.collection("Movies")
.doc(id)
.update({"watched": v});
movies.firstWhere((element) => element.id == id).watched = v;
notifyListeners();
}

void addMovie(String title) {
FirebaseFirestore.instance
.collection("Movies")
.add({"title": title, "watched": false, "uid": user.uid}).then((value){

movies.add(Movie(title, false, value.id));
notifyListeners();
});
}

And a couple more things, and we are set!

Let’s just update the TextField so that it looks nicer and is more functional!

Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
controller: _controller,
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10.0),
borderSide: BorderSide(
color: Colors.blue,
width: 2.0,
style: BorderStyle.solid))),
onSubmitted: (s) {
app.addMovie(s);
_controller.clear();
},
),
),

It’s not going to be turning heads but is definitely an improvement to a white box that can’t even be recognised as a textfield.

Done!

Awesome, so we are done! Provider is an awesome way to maintain state efficiently. Provider gives us a way to abstract away from the tree structure and maintains the state directly from an object, and it makes handling larger apps a lot easier.

Here is the link to the final repo :)

If you have any questions about how provider works, feel free to reach out to me. There is also a tonne of really good tutorials out there!

Flutter Developer, co-lead for GDG Southampton