Magic of Flutter, Provider, and Firestore

Muhammad Mashood Siddiquie
Analytics Vidhya
Published in
11 min readJun 11, 2020

Hello everyone today we will be learning flutter with firestore as a backend with Provider package in easy steps. Before starting into this article first, you should have already integrated a firebase in your application and also set up a firestore as a database with a testing mode. We will be building a simple app for products and apply CRUD operation on it.

Step 1: Let’s dive into the project structure first, you must have the same project structure as I am having so things will be more clear to you.

Step 2: Copy-paste the following dependencies in your pubspec.yaml file:

cloud_firestore: ^0.13.4+2
provider: ^4.0.5
uuid: ^2.0.4
  1. cloud_firestore: This gives access to use of firestore functions in our flutter application.
  2. provider: This will be used to manage the state of our app i.e differentiate the UI and logical parts.
  3. UUID: This package will be used to generate the id of a product.

Step 3: First goto models folder and create a product.dart class. The model class is basically referred to as a user-generated data type, in which data is classified according to the user requirements.

class Product{
String productId;
String productName;
double price;

Product({this.price,this.productId,this.productName});


Map<String,dynamic> createMap(){
return {
'productId': productId,
'productName': productName,
'productPrice': price
};
}

Product.fromFirestore(Map<String,dynamic> firestoreMap):
productId = firestoreMap['productId'],
productName = firestoreMap['productName'],
price = firestoreMap['productPrice'];
}

In this class we have three properties:

a) String productId.

b) String productName.

c)double price.

we created a named constructor i.e the order while passing data to the constructor is not necessary.

There are two functions that are defined in the product.dart model class:

  1. Map<String, dynamic> createmap(): This means it converts the product properties into a map and returns it as a Map.
  2. Product.fromFirestore(Map<String, dynamic firestoreMap): This means that the firestore returns the data in form of a map so this method is used to convert the map into the properties which means the vice-versa of create map function.

Step 4: Go to the provider folder and create a new class inside it with name products_provider.dart and copy-paste the following code inside it.

class ProductProvider with ChangeNotifier{
String _name;
String _productId;
double _price;

//getters:
String get getName => _name;
double get getPrice => _price;

//Setters:

void changeProductName(String val) {
_name = val;
notifyListeners();
}

void changeProductPrice(String val) {
_price = double.parse(val);
notifyListeners();
}

we created three private properties underscore defines the property as private and simply we created getters and setters for our properties. As we are not getting any product id and setting because it’ll be dynamically or auto-generated.

Step 5: Go to main.dart, remove all the code, and copy-paste the following code:

import 'package:flutter/material.dart';
import 'package:flutter_responsiveness/providers/product_provider.dart';
import 'package:flutter_responsiveness/screens/edit_add_product.dart';
import 'package:provider/provider.dart';
import './screens/product_list.dart';
import './services/firestore_service.dart';
void main()=>runApp(MyApp());


class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: ProductProvider()),
],
child: MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: Colors.blueAccent[700],
accentColor: Colors.indigoAccent[700],
canvasColor: Colors.white,
),
home: ProductList(),
),
);
}
}

Multi Providers enable us to use multiple providers in our app. it contains providers' property which takes an array of providers and a child property which contains a widget.

Step 6: Go to screens folder and create a new file with the name product_list.dart and copy-paste the following code:

import 'package:flutter/material.dart';

class ProductList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Product'),
actions: [
IconButton(
icon: Icon(
Icons.add,
),
onPressed: () {},
),
],
),
);
}
}

Simply we have an app bar widget with title and actions as IconButton. Actions are the widgets that are defined at the right side of the app bar.

Step 7: Create another class inside the screens folder with name edit_add_product.dart and copy-paste the following code:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/product_provider.dart';

class AddEditProduct extends StatefulWidget {
static const routeArgs = '/add-edit-screen';

@override
_AddEditProductState createState() => _AddEditProductState();
}

class _AddEditProductState extends State<AddEditProduct> {
final _formKey = GlobalKey<FormState>();
final nameController = TextEditingController();
final priceController = TextEditingController();
@override
void dispose() {
super.dispose();
nameController.dispose();
priceController.dispose();
}
@override
Widget build(BuildContext context) {
final productProvider = Provider.of<ProductProvider>(context);
return Scaffold(
appBar: AppBar(
title: Text('Add or Edit Product'),
),
body: Center(
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Column(
children: [
TextFormField(
controller: nameController, validator: (value) {
if (value.isEmpty) {
return 'Please Enter Name';
}
return null;
},
decoration: InputDecoration(
hintText: 'Enter Product Name',
),
onChanged: (val) => productProvider.changeProductName(val),
),
TextFormField(
controller: priceController,
validator: (value) {
if (value.isEmpty) {
return 'Please Enter Price';
}
return null;
},
decoration: InputDecoration(
hintText: 'Enter Product Price',
),
onChanged: (value) =>
productProvider.changeProductPrice(value),
),
SizedBox(
height: 20,
),
RaisedButton(
child: Text('Save'),
onPressed: () {

},
color: Theme.of(context).accentColor,
textColor: Colors.white,
),
RaisedButton(
child: Text('Delete'),
onPressed: () {
},
color: Colors.red,
textColor: Colors.white,
)
],
),
),
),
)),
);
}
}

onchanged method takes the parameterized function, which contains the string data and also we are using the setter of our products provider to change the name and price of a product.

As you can see we are using forms and I have already covered forms in flutter so you can check out my medium article of forms and its validation:

Also, the code has multiple useable widgets we can also do some code refactoring, this article of mine explains code refactoring in deep and simple steps check out here:

I won’t be doing any code refactoring or form explanation as the article would too long.

Now Let’s move towards the CRUD operation of flutter,

Step 8: Go to services and create a new file with name firestore_services.dart and copy-paste the following code inside it:

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_responsiveness/models/product.dart';

class FireStoreService {
Firestore _db = Firestore.instance;

Future<void> saveProduct(Product product) {
return _db
.collection('Products')
.document(product.productId)
.setData(product.createMap());
}

Stream<List<Product>> getProducts() {
return _db.collection('Products').snapshots().map((snapshot) => snapshot
.documents
.map((document) => Product.fromFirestore(document.data))
.toList());
}

Future<void> removeItem(String productId) {
return _db.collection('Products').document(productId).delete();
}
}

Firestore works on a collection and documents basis. Which is a different approach from SQL database.

  1. save product: This method saves data into our firestore. The code I have written means that as firestore works on collections and it’s a different approach from the SQL Database, we have created a firestore instance to use its functions. _db.collections(‘Products’) means that goto firestore and add a new collection with the Products and concatenation .documents means that we are passing our own custom id inside the collections and setData means the data we want to save inside this collection which should be in the form of Map.
  2. GetProducts: This method returns a stream. A stream is like a pipe, you put a value on the one end, and if there’s a listener on the other end that listener will receive that value. A Stream can have multiple listeners and all of those listeners will receive the same value when it’s put in the pipeline. Lines of code explanation:
_db.collection('Products').snapshots().map((snapshot) => snapshot
.documents
.map((document) => Product.fromFirestore(document.data))
.toList());

This means go to the products collection and gets snapshots which returns the stream of data and map it that means to map the data which you get from streams which takes a function in which we access the document inside the Products collection and also map that document as firestore returns data in form of Map so we need to convert it into the properties so we can populate them into our widgets as we have already created the method which converts map to properties inside our product model class i.e from firestore so we access it and pass the document.data which is a map also the return type of our method is a list so we add .toList() at the end of the method to return the data in the form of the list.

3. Remove Item Method: This method simply takes a Product id and deletes that from the app as well as the backend and since we are using streams we don't need to refresh the application to see the changes, Streams makes it easier for us to detect live changes within the app.

Step 9: Now go to products_provider.dart class:

a) import the Firestore Service class and UUID:

import '../services/firestore_service.dart';
import 'package:uuid/uuid.dart';

b) Create a final field of products_provider inside ProductProvider and also UUID to get random ids

final service = FireStoreService();
var uuid = Uuid();

c) Now create a method inside Products Provider to save data into firestore:

void saveData() {
var newProduct = Product(productName: getName, productId:
uuid.v4(), price: getPrice);service.saveProduct(newProduct);}

As in our service class, our saveProduct takes a Product model class as a parameter so we need to first define a product then pass it into the parameter of saving product, where getName is the name which is set to the edit_add_product.dart class like this:

TextFormField(
validator: (value) {
if (value.isEmpty) {
return 'Please Enter Name';
}
return null;
},
controller: nameController,
decoration: InputDecoration(
hintText: 'Enter Product Name',
),
onChanged: (val) => productProvider.changeProductName(val),
),

Similarly for getPrice as well. product is the UUID.v4 which will be a unique id for every product we add in the firestore.

Step 10: Let's go back to the edit_add_product.dart class and go to the Save Raised Button compressed method and replace the code of Save Raised Button with this code:

RaisedButton(
child: Text('Save'),
onPressed: () {
if(_formKey.currentState.validate())
{
productProvider.saveData();
Navigator.of(context).pop();
}
},
color: Theme.of(context).accentColor,
textColor: Colors.white,
),

In this, we validate the form that if all fields are entered then save the data to the firebase, and pop means after adding data to the firestore immediately pop the screen from the stack of screens.

Now run the app, press the “+” button on the app bar, and then enter the product name and price, you’ll see that the data is added to the firestore.

Step 11: let's get back to the main. dart file as we are getting the product data in the form of a stream so we have to register it as well to it's my approach for using firestore in a flutter app. There are different approaches as well, you can find out more on documentation or googling it for different approaches. Let’s come back to the point, replace the main. dart code with this code:

import 'package:flutter/material.dart';
import 'package:flutter_responsiveness/providers/product_provider.dart';
import 'package:flutter_responsiveness/screens/edit_add_product.dart';
import 'package:provider/provider.dart';
import './screens/product_list.dart';
import './services/firestore_service.dart';
void main()=>runApp(MyApp());


class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final firestoreService = FireStoreService();

return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: ProductProvider()),
StreamProvider.value(value: firestoreService.getProducts()),
],
child: MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: Colors.blueAccent[700],
accentColor: Colors.indigoAccent[700],
canvasColor: Colors.white,
),
home: ProductList(),
routes: {
AddEditProduct.routeArgs:(ctx)=>AddEditProduct(),
},
),
);
}
}

we are using stream provider to get all the products from firebase to get updates of each action taken on our data.

Step 12: Now goto product_list.dart and copy-paste the following code:

import 'package:flutter/material.dart';
import 'package:flutter_responsiveness/models/product.dart';
import 'package:flutter_responsiveness/screens/edit_add_product.dart';
import 'package:provider/provider.dart';

class ProductList extends StatelessWidget {
@override
Widget build(BuildContext context) {
final productList = Provider.of<List<Product>>(context);
return Scaffold(
appBar: AppBar(
title: Text('Product'),
actions: [
IconButton(
icon: Icon(
Icons.add,
),
onPressed: () {
Navigator.of(context).pushNamed(AddEditProduct.routeArgs);
},
),
],
),
body: productList != null
? ListView.builder(
itemCount: productList.length,
itemBuilder: (ctx, i) => ListTile(
title: Text(productList[i].productName),
trailing: Text(productList[i].price.toString()),
onTap: () => Navigator.of(context)
.pushNamed(AddEditProduct.routeArgs, arguments: {
'productId': productList[i].productId,
'productName': productList[i].productName,
'productPrice': productList[i].price,
}),
),
)
: Center(
child: CircularProgressIndicator(),
),
);
}
}

we create a product list of provider. As ‘of ‘ is a generic type so we need to pass the data type in <> brackets. so we passed the list<products> as we want to get all the products. creating a body part we are checking with ternary operator that if products are not null then create a listview.builder in which item count is the length of list items and builder takes the context and index of each item. We use fat arrow notation to return the list tile widget with title and trailing means it will be at the most right of the screen, also we used on tap on the list tile widget in which we are going to the Add_Edit_product class and arguments means what data we want to pass to this class we are passing map data to add_edit_product, it’s not always necessary to use arguments until unless you need to pass the data to the ongoing class, also after this ‘:’ we return circular progress indicator and when we get data this will automatically hide and our list view will be populated with data.

Lets hot restart the app and check that your first screen will be populated with data you entered.

Step 13: It’s time to update the data, first go to products provider and inside save data copy-replace the following code:

void saveData() {
if (_productId == null) {
var newProduct =
Product(productName: getName, productId: uuid.v4(), price: getPrice);
service.saveProduct(newProduct);
} else {
var updatedProduct =
Product(productName: getName, price: getPrice, productId: _productId);
service.saveProduct(updatedProduct);
}
}

we are checking here if the product id is null so that means we will add new data to the firestore if we found any product id that means we will update that data to Firestone.

Step 14: Let’s move towards the add_edit_products and create init state inside it:

@override
void initState() {
super.initState();
new Future.delayed(Duration(microseconds: 10), () {
routeData =
ModalRoute.of(context).settings.arguments as Map<String, dynamic>;
}).then((_) => routeData == null
? new Future.delayed(Duration.zero, () {
clearData();
final productProvider =
Provider.of<ProductProvider>(context, listen: false);
productProvider.loadValues(Product());
})
: Future.delayed(Duration.zero, () {
print(routeData);
nameController.text = routeData['productName'];
priceController.text = routeData['productPrice'].toString();

final productProvider =
Provider.of<ProductProvider>(context, listen: false);
Product p = Product(
productId: routeData['productId'],
price: routeData['productPrice'],
productName: routeData['productName'],
);
productProvider.loadValues(p);
}));
}

We passed the data from the product list to add_edit_product class as map argument so we accept it like this:

routeData =
ModalRoute.of(context).settings.arguments as Map<String, dynamic>;

since init, the state has no access to context directly because its the first lifecycle method of the stateful widget, so to access context inside init state we use Future. delayed which takes duration and a method inside it and then means that when the given operation is completed inside future then we want run these lines of code, so inside then we are checking if the route data is null then we are entering new data into text fields else we are updating the data from text field which will be populated automatically for edit purpose.

Since there will be a bug because the data will always be there if we edit or add a product so we need to load tell the provider too about it so we will create a new method inside products_provider.dart class:

loadValues(Product product) {
_name = product.productName;
_price = product.price;
_productId = product.productId;
}

This method makes our state up to date according to add or update data.

Now run the app and tap on the list item and you’ll see the data is populated inside the text field try to change the data inside the field and hit save you’ll see the data is updated inside the app due to streams and also to firestore as well.

Step 15: Now let's remove the item from Firestore, goto to the products_provider class and add this method:

void removeData() {
service.removeItem(_productId);
}

This method will remove the item from the firestore.

Step 16: Let’s go to the add_edit_product class and inside the second raised button for delete replace it with this code:

RaisedButton(
child: Text('Delete'),
onPressed: () {
productProvider.removeData();
Navigator.of(context).pop();
},
color: Colors.red,
textColor: Colors.white,
)

This will remove the following data and pop the screen from the stack of the screen.

Run the app from scratch and check out by adding, update and deleting, and reading data.

For any queries, Leave me a note or comment on the article.

That’s all for today. It was really a deep dive and a big article of flutter with firestore and provider.

I have made a lot of effort to write this article in the simplest way so don’t forget to give a clap and a follow.

Github Repo Link:

Don’t forget to start the repo and follow me.

Thank you.

--

--