Creating a custom Listview using the Firebase Realtime Database in Flutter

Author profile picture

@anurag-bannurAnurag bannur

A couple of weeks ago I started learning flutter and set out to create a Cookbook. This article explains the procedure of creating a Listview for the main feed of the cookbook in detail. The data is not stored locally, but on my all-time favorite Realtime Database of Firebase.
If you would like to refer to the code base you can find it here https://github.com/Anurag26/components
DisclosureIf you have any feedback/suggestions/criticism please let me know in the comments section, Thank you!
This article is divided into eight steps and there is a flowchart at the end for reference to the code flow.
The Cookbook Feed is a list and each item in the list has two parts:
1. The Food icon
2. The Veg or Non-Veg symbol, name and Preparation time
The data for the application is stored in a JSON tree in the form of an array of objects. Here each object has attributes which help describe a particular recipe. The figure below shows an array of six such objects.

Step 1:

Create a class containing the necessary attributes for the recipes and also declare a constructor for the same. This class is used to define the contents of each ListItem.
class RecipeDetailListItem {
  String foodtitle;
  String iconUrl;
  String PreptimeInMins;
  String uid;
  String vegOrNon;
  
  RecipeDetailListItem({this.foodtitle,this.iconUrl,this.PreptimeInMins,this.uid,this.vegOrNon});
}

Step 2:

As we will be working with a list, we need another class with a single attribute and a constructor. The attribute, in this case, will be a List of RecipeDetailListItem as shown below. I have declared both the classes in the same dart file:
class RecipeList{
  List<RecipeDetailListItem> recipeList;

  RecipeList({this.recipeList});
}

Step 3:

Let’s not forget to create the JSON tree with the data and upload it on the firebase. As the free plan allows a single tree in the Realtime Database, it is important to design a flexible architecture. As shown below, the recipes for the feed are stored in a single array called browseRecipes within the content object. This will allow me to add more objects in the future if needed within the content object.

Step 4:

After uploading JSON data, it is necessary to make calls from the application to populate the List for the feed. Create a new Dart file for this step. This file will hold the widgets of the feed.

4.1:

First, we need to make a reference to the fireplace project, The FirebaseDatabase instance, can be used to get a database reference. This is a reference to a specific point within the database tree at which read and write operations may be performed.
import 'package:firebase_database/firebase_database.dart';   #import the package

final databaseReference = FirebaseDatabase.instance.reference();
To use the fireplace in your flutter project refers to https://pub.dev/packages/firebase_database#-installing-tab-

4.2:

Second, once we make the call to firebase we will receive a JSON object which needs to be parsed to get the relevant data. Hence, in the two classes created for the ListView, we shall create methods which will help us in doing so.
As the initial JSON will be in the array format (array of ReceipeDetailListItem) we will pass the data to RecipeList class which has a List attribute.
In the RecipeList class add the following:
factory RecipeList.fromJSON(Map<dynamic,dynamic> json){
    return RecipeList(
      recipeList: parserecipes(json)
    );
  }
The factory constructor in the code above will help in returning the instance of the class RecipeList which is nothing but the List of RecipeDetailListItems. The factory ensures that a single instance of the class is in use.
The raw data received from firebase is like a map of dynamic: dynamic (key-value pair). The attribute recipeList will be populated with the help of parserecipes().

4.3:

static List<RecipeDetailListItem> parserecipes(recipeJSON){
    var rList=recipeJSON['browseRecipes'] as List;
   
  }
In the parserecipes() method we store the necessary list in a variable called rList, for our reference to enter into the browseRecipes array from the outer object content is shown on line 2 above.
However, if we return rList as it is we would get an error stating that the var is a List<dynamic> type and not List<RecipeDetailListItem>, hence we must map the above list to the RecipeDetailListItem class type. Before mapping, we must create a method in the RecipeDetailListItem class which will help in returning the instance of the class with all the attributes assigned to the data with parsed values.
This instance will be then passed to the list. Each recipe will have one instance stored in the list.

4.4:

 factory RecipeDetailListItem.fromJson(Map<dynamic,dynamic> parsedJson) {
    return RecipeDetailListItem(foodtitle:parsedJson['Name'],iconUrl: parsedJson['icon'],PreptimeInMins:parsedJson['PreptimeInMins'],uid: parsedJson['uid'],vegOrNon:parsedJson['vegOrNon']);
  }
The above code should be added to ReceipeDetailListItem class to help return the necessary objects.

4.5:

Now finish the parserecipes() method in RecipeList class. The third line helps in mapping the list items to the necessary type and the fourth line returns the list which can be used in the ListView widget.
static List<RecipeDetailListItem> parserecipes(recipeJSON){
    var rList=recipeJSON['browseRecipes'] as List;
    List<RecipeDetailListItem> recipeList=rList.map((data) => RecipeDetailListItem.fromJson(data)).toList();  //Add this
    return recipeList;                           //And this
  }

Step 5:

Phew…
Now that the classes are ready, we need to make calls to the firebase to retrieve the data, to do that I created a new class called MakeCall(). This call would use the firebase reference and make an asynchronous call to the cloud.
class MakeCall{
  List<RecipeDetailListItem> listItems=[];

  Future<List<RecipeDetailListItem>> firebaseCalls (DatabaseReference databaseReference) async{
    RecipeList recipeList;
        DataSnapshot dataSnapshot = await databaseReference.once();
        Map<dynamic,dynamic> jsonResponse=dataSnapshot.value[0]['content'];
        recipeList = new RecipeList.fromJSON(jsonResponse);
        listItems.addAll(recipeList.recipeList);
       
    return listItems;
    }
  }
The class above is fairly intuitive. The method firebaseCalls(databaseReference) returns the list to be used for the ListView.

Step 6:

Back in the Stateful widget class, I used the expanded widget with a container to house the ListView.
new Expanded(
            child:Container(
              child: ListView.builder()//ListView,
            ) ,
          ),
However, as the ListView is populated with data received from an Asynchronous call we will use the FutureBuilder. You can learn more about the FutureBuilder here:https://api.flutter.dev/flutter/widgets/FutureBuilder/FutureBuilder.html

Step 7:

Inside the stateful Widget declare a Future Builder which will call the asynchronous method firebaseCalls created in Step 5. As shown below, I have used the MakeCall object to call the method and passed the databasereference as the argument.
In the FureBuilder, the named parameter future: consists of the part which returns a future and the snapshot is the data which is received after a successful call.
We must also account for different cases of connectionstate, the case with a successful connection houses the ListView code as shown.
The code below also contains the design of each card. Each card has a icon, Title, Preparation time and Veg/Non-Veg symbol.
var futureBuilder=new FutureBuilder(
      future: makecall.firebaseCalls(databaseReference), // async work
      builder: (BuildContext context, AsyncSnapshot snapshot) {
        switch (snapshot.connectionState) {
          case ConnectionState.none: return new Text('Press button to start');
          case ConnectionState.waiting: return new Text('Loading....');
          default:
            if (snapshot.hasError)
              return new Text('Error: ${snapshot.error}');
            else
                return ListView.builder(
                itemCount: snapshot.data.length,
                itemBuilder: (BuildContext context, int index){
                   return Card(
                elevation: 0.0,
                child: Padding(
                  padding: const EdgeInsets.all(0.0),
                  child:  SizedBox(
                height: MediaQuery.of(context).size.height*0.15,
                       width: MediaQuery.of(context).size.width,
                       child:  Card(
                           elevation: 0,
                           child: Row(
                             children: <Widget>[
                               new Container(
                                     child: Image.network(snapshot.data[index].iconUrl,height: MediaQuery.of(context).size.width*0.3,width: MediaQuery.of(context).size.width*0.3,),
                               ),
                               Padding(
                                 padding: const EdgeInsets.only(left: 10,right: 5,top: 5),
                                 child: Column(
                                   mainAxisAlignment: MainAxisAlignment.start,
                                   children: <Widget>[
                                     new Container(
                                       child: Column(
                                         mainAxisAlignment: MainAxisAlignment.start,
                                         children: <Widget>[
                                           Column(
                                             mainAxisAlignment: MainAxisAlignment.start,
                                             crossAxisAlignment: CrossAxisAlignment.start,
                                             children: <Widget>[
                                               Row(
                                                 children: <Widget>[
                                                   condition('${snapshot.data[index].vegOrNon}')==true? new Image.asset('images/non_veg.png',height: 15,width: 15,): new Image.asset('images/veg.jpg',height: 15,width: 15),
                                                   SizedBox(width: 5),
                                                   Text(snapshot.data[index].foodtitle, style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20,fontFamily: 'Roboto-Black'),)
                                                 ],
                                               ),

                                               SizedBox(height:10.0),
                                               Row(
                                                 children: <Widget>[
                                                   new IconTheme(
                                                     data: new IconThemeData(
                                                         color: Colors.black26),
                                                     child: new Icon(Icons.timer,size: 20.0,),
                                                   ),
                                                   Text('${snapshot.data[index].PreptimeInMins} minutes',style: TextStyle(fontWeight: FontWeight.w700,color: Colors.black26),),
                                                 ],
                                               ),
                                             ],
                                           )
                                         ],
                                       ),
                                     ),
                                   ],
                                 ),
                               ),

                             ],
                           )
                       )


                   ),
                ),
              );
                },
                );
        }
      },
    );

Step 8:

Finally, we need to replace the ListView.builder inside the expanded widget with the futureBuilder:
new Expanded(
            child:Container(
              child: futureBuilder,
            ) ,
          ),
The flow chart below sums up the code flow:
It took me about two to three days to make this List and I am sure there are better practices for the same and hence I would appreciate any possible feedback.

If you enjoyed this story, please do share it! Also, feel free to leave a comment below.

Tags

The Noonification banner

Subscribe to get your daily round-up of top tech stories!