How to Create a Favourite Product Function in Flutter

Demola Malomo - Aug 22 '22 - - Dev Community

Most digital platform services offer “Add to Favourite” functionality to their products. It allows users to save, streamline, and prioritise their experience using said platform.

In this post, we will learn how to create a favourite product feature in Flutter using Appwrite’s database for storage.

Prerequisites

To fully grasp the concepts presented in this tutorial, we require the following:

Getting Started

In this post, we will focus on implementations only. The project UI has already been set up.

To get started, we need to clone the project by navigating to the desired directory and running the command below:



    git clone https://github.com/Mr-Malomz/product_fav.git && cd product_fav


Enter fullscreen mode Exit fullscreen mode

The complete source code is also available on the dev branch of the same repository.

Running the Project

First, we need to install the project dependencies by running the command below:



    flutter pub get


Enter fullscreen mode Exit fullscreen mode

Then run the project using the command below:



    flutter run


Enter fullscreen mode Exit fullscreen mode

The command above will run the application on the selected device.

Home Screen
Favourite Screen

Creating a New Appwrite Project

To create a new project, start the Appwrite instance and navigate to the specified hostname and port http://localhost:80. Next, we need to log into our account or create an account if we don’t have one.

Appwrite Running

On the console, click on the Create Project button, input product_feature as the name, and click Create.

Create Project
Enter Project Name

Next, we need to create a database to save our product features. Navigate to the Database tab, click Add Database, input products as the database name, and click Create.

Click on Add Database
Enter Database Name & Enter

With that done, we need to create a collection for grouping our products. Click on Add Collection, input product_list as the collection name, and click on Create.

Click on Add Collection
Enter Collection Name & Create

Appwrite has an advanced yet flexible way to manage access to users, teams, or roles to access specific resources. We will modify the permission role:all to enable access from any application. Then click on Update to save changes.

Modify Permission

Add Attributes

Attributes are fields that our database will possess. Navigate to the Attributes tab, click on Add Attributes, add a New String Attribute and size of 1000 for both title and description fields with the required option marked, and New Boolean Attribute for isFav also marked as required, and click on Create.

Select Attribute
Create Field

Add Sample Data

To get a feel for our database, we can add sample data by navigating to the Documents tab, clicking on Add Document, inputting the required fields, and clicking on Create.

Add Document
Input Fields and Create

Sample Product Data

title description isFav
iPhone 9 An apple mobile which is nothing like apple false
iPhone X SIM-Free, Model A19211 6.5-inch Super Retina HD display with OLED technology A12 Bionic chip with ... false
Samsung Universe 9 Samsung's new variant which goes beyond Galaxy to the Universe false
OPPOF19 OPPO F19 is officially announced on April 2021. false
Huawei P30 Huawei’s re-badged P30 Pro New Edition was officially unveiled yesterday in Germany and now the device has made its way to the UK. false
MacBook Pro MacBook Pro 2021 with mini-LED display may launch between September, November false

Add Index

Indexes in a database improve the speed of data retrieval. Navigate to the Indexes tab, click on Add Index, input isFav as the index key, select isFav from the Attributes dropdown, and click on Create.

Connecting Appwrite to Flutter

To add support for our Flutter app, navigate to the Home menu, click on the Add Platform button, and select New Flutter App.

Add Platform
Select New Flutter App

Depending on the device we are running our Flutter application, we can modify it as shown below.

iOS
To obtain our Bundle ID, we can navigate using the path below:
ios > Runner.xcodeproj > project.pbxproj

Open the project.pbxproj file, and search for PRODUCT_BUNDLE_IDENTIFIER.

iOS

Next, open the project directory on Xcode, open the Runner.xcworkspace folder in the app's iOS folder, select the Runner project in the Xcode project navigator, select the Runner target in the main menu sidebar, and then select iOS 11 in the deployment info’s target.

Change Deployment Target

Android
To get our package name, we can navigate using the path below:
android > app > src > debug > AndroidManifest.xml

Open the AndroidManifest.xml file, and copy the package value.

Android

Next, we need to modify the AndroidManifext.xml as shown below:



    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.example.product_fav">
        <uses-permission android:name="android.permission.INTERNET"/>
        <application ...>
        <activity android:name="com.linusu.flutter_web_auth.CallbackActivity" android:exported="true">
          <intent-filter android:label="flutter_web_auth">
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.intent.category.DEFAULT" />
            <category android:name="android.intent.category.BROWSABLE" />
            <data android:scheme="appwrite-callback-[PROJECT_ID]" />
          </intent-filter>
        </activity>
      </application>
    </manifest>


Enter fullscreen mode Exit fullscreen mode

Then, we need to navigate to the lib directory and create a utils.dart file and add the snippet below:



    class AppConstant {
      final String databaseId = "REPLACE WITH YOUR DATABASE ID";
      final String projectId = "REPLACE WITH YOUR PROJECT ID";
      final String endpoint = "REPLACE WITH YOUR ENPOINT";
      final String collectionId = "REPLACE WITH YOUR COLLECTION ID";
    }


Enter fullscreen mode Exit fullscreen mode

Navigate to the Database menu, click on the Products database, click on the Settings tab, and copy the Database ID.

Copy Database ID

Switch to the Collections tab, click on the product_list collection, click on the Settings tab, and copy the Collection ID.

Click on the product_list
Copy the Collection ID

Navigate to the Settings menu to copy the Project ID and API Endpoint.

Copy Project ID and API Endpoint

For the endpoint property, we need to modify it to work with our system's local network address. We can adjust accordingly:

iOS
Navigate to the Network section, copy the IP address, and modify it as shown below:



    class AppConstant {
      final String databaseId = "REPLACE WITH YOUR DATABASE ID";
      final String projectId = "REPLACE WITH YOUR PROJECT ID";
      final String endpoint = "http://192.168.1.195/v1";
      final String collectionId = "REPLACE WITH YOUR COLLECTION ID";
    }


Enter fullscreen mode Exit fullscreen mode

Android
We can connect our Android emulator to the system’s IP using the 10.0.2.2 IP address.



    class AppConstant {
      final String databaseId = "REPLACE WITH YOUR DATABASE ID";
      final String projectId = "REPLACE WITH YOUR PROJECT ID";
      final String endpoint = "http://10.0.2.2/v1";
      final String collectionId = "REPLACE WITH YOUR COLLECTION ID";
    }


Enter fullscreen mode Exit fullscreen mode

Building the Favourite Product Feature

To get started, we need to create a model to convert the response sent from Appwrite to a Dart object. The model will also cater to JSON serialization. To do this, add the snippet below in the same utils.dart file:



    class AppConstant {
      //code goes here
    }

    class Product {
      String? $id;
      String title;
      String description;
      bool isFav;

      Product({
        this.$id,
        required this.title,
        required this.description,
        required this.isFav,
      });

      factory Product.fromJson(Map<dynamic, dynamic> json) {
        return Product(
          $id: json['\$id'],
          title: json['title'],
          description: json['description'],
          isFav: json['isFav'],
        );
      }

      Map<dynamic, dynamic> toJson() {
        return {'title': title, 'description': description, 'isFav': isFav};
      }
    }


Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Creates a Product class with required properties
  • Adds a constructor with unrequired and required parameters
  • Creates a fromJson and toJson method for JSON serialization

Next, we need to create a service file to separate the application core logic from the UI. To do this, create a product_service.dart file inside the lib directory and update it by doing the following:



    import 'package:appwrite/appwrite.dart';
    import 'package:product_fav/utils.dart';

    class ProductService {
      Client client = Client();
      Databases? db;

      ProductService() {
        _init();
      }

      //initialize the application
      _init() async {
        client
            .setEndpoint(AppConstant().endpoint)
            .setProject(AppConstant().projectId);
        db = Databases(client, databaseId: AppConstant().databaseId);

        //get current session
        Account account = Account(client);

        try {
          await account.get();
        } on AppwriteException catch (e) {
          if (e.code == 401) {
            account
                .createAnonymousSession()
                .then((value) => value)
                .catchError((e) => e);
          }
        }
      }

      Future<List<Product>> getAllProducts() async {
        try {
          var data =
              await db?.listDocuments(collectionId: AppConstant().collectionId);
          var productList = data?.documents
              .map((product) => Product.fromJson(product.data))
              .toList();
          return productList!;
        } catch (e) {
          throw Exception('Error getting list of products');
        }
      }

      Future<List<Product>> getAllFavourites() async {
        try {
          var data = await db?.listDocuments(
              collectionId: AppConstant().collectionId,
              queries: [Query.equal('isFav', true)]);
          var favList =
              data?.documents.map((fav) => Product.fromJson(fav.data)).toList();
          return favList!;
        } catch (e) {
          throw Exception('Error getting list of favourites');
        }
      }

      Future addAsFav(
          String title, String description, String id, bool isFav) async {
        try {
          Product updateProduct =
              Product(title: title, description: description, isFav: isFav);
          var data = await db?.updateDocument(
            collectionId: AppConstant().collectionId,
            documentId: id,
            data: updateProduct.toJson(),
          );
          return data;
        } catch (e) {
          throw Exception('Error updating product');
        }
      }
    }


Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Imports the required dependencies
  • Creates a ProductService class with client and db properties to connect to the Appwrite instance and the database, respectively
  • Creates an _init method that configures the Appwrite using the property and also conditionally creates an anonymous user to access Appwrite
  • Creates a getAllProducts method that uses the configured database’s listDocuments function to get a list of products and converts the returned JSON to a list using the Product.fromJson method
  • Creates a getAllFavourites method that uses the configured database’s listDocuments function to get a list of products marked as favourite and converts the returned JSON to a list using the Product.fromJson method. This method also uses the isFav index we created earlier to retrieve products matching the query optimally.
  • Creates an addAsFav method that takes in the title, description, and isFav parameter and uses the updateDocument function to update a product; this creates an instance of the product using the title, description, and isFav parameter, passes in the id as the documentId, and passes in the updated note by converting the Dart object to JSON using the updateProduct.toJson() method

Consuming the Service

With that done, we can start using the service to perform the required operation.

Get the List of Products and Add to Favourites
To get the list of products, we need to navigate to the screens folder, open the home.dart and modify as shown below:



    import 'package:flutter/material.dart';
    import 'package:product_fav/product_service.dart'; //add this
    import 'package:product_fav/utils.dart'; //add this

    class Home extends StatefulWidget {
      const Home({Key? key}) : super(key: key);
      @override
      State<Home> createState() => _HomeState();
    }
    class _HomeState extends State<Home> {
      List<Product>? products;
      bool _isLoading = false;
      bool _isError = false;

      @override
      void initState() {
        _getProductList();
        super.initState();
      }

      _getProductList() {
        setState(() {
          _isLoading = true;
        });
        ProductService().getAllProducts().then((value) {
          setState(() {
            products = value;
            _isLoading = false;
          });
        }).catchError((e) {
          setState(() {
            _isLoading = false;
            _isError = true;
          });
        });
      }

      _addAsFavourite(
          String title, String description, String id, bool isFav, int index) {
        ProductService().addAsFav(title, description, id, isFav).then((value) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('favourites updated successfully!')),
          );
          setState(() {
            products![index].isFav = isFav;
          });
        }).catchError((e) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('Error adding favourite!')),
          );
        });
      }

      @override
      Widget build(BuildContext context) {
        //widget code goes here
      }
    }


Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Imports the required dependencies
  • Creates a products, _isLoading, and _isError properties to manage the application state
  • Creates a _getProductList method to get the list of products using the getAllProducts service and sets states accordingly
  • Uses the initState method to call the _getProductList method when the object is inserted into the tree
  • Creates a _addAsFavourite method that takes in the title, description, id, and index as a parameter and uses the addAsFav service to mark the selected product as a favourite, updates the UI using the setState method, and uses the snackbar to show the action performed

Next, we can modify the UI widgets to show the products coming from Appwrite.



    //imports goes here

    class Home extends StatefulWidget {
      const Home({Key? key}) : super(key: key);
      @override
      State<Home> createState() => _HomeState();
    }
    class _HomeState extends State<Home> {
      //properties goes here

      @override
      void initState() {
        _getProductList();
        super.initState();
      }

      _getProductList() {
        //code goes here
      }

      _addAsFavourite(
         //code goes here
      }

      @override
      Widget build(BuildContext context) {
        return _isLoading
            ? const Center(
                child: CircularProgressIndicator(
                color: Colors.blue,
              ))
            : _isError
                ? const Center(
                    child: Text(
                      'Error loading products',
                      style: TextStyle(
                        color: Colors.red,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  )
                : ListView.builder(
                    itemCount: products?.length,
                    itemBuilder: (context, index) {
                      return InkWell(
                        onTap: () {
                          var isFav =
                              products![index].isFav == false ? true : false;
                          _addAsFavourite(
                            products![index].title,
                            products![index].description,
                            products![index].$id!,
                            isFav,
                            index,
                          );
                        },
                        child: Container(
                          decoration: const BoxDecoration(
                            border: Border(
                              bottom: BorderSide(width: .5, color: Colors.grey),
                            ),
                          ),
                          padding: EdgeInsets.fromLTRB(10, 20, 10, 20),
                          child: Row(
                            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                            children: [
                              Expanded(
                                flex: 7,
                                child: Column(
                                  crossAxisAlignment: CrossAxisAlignment.start,
                                  children: [
                                    Text(
                                      products![index].title,
                                      style: TextStyle(
                                          color: Colors.blue,
                                          fontWeight: FontWeight.w800),
                                    ),
                                    SizedBox(height: 10.0),
                                    Text(products![index].description)
                                  ],
                                ),
                              ),
                              Icon(
                                products![index].isFav
                                    ? Icons.favorite
                                    : Icons.favorite_border,
                                color: products![index].isFav
                                    ? Colors.red
                                    : Colors.grey,
                              )
                            ],
                          ),
                        ),
                      );
                    },
                  );
      }
    }


Enter fullscreen mode Exit fullscreen mode

Get the List of Favourite Products
To get the list of products added as a favourite, we need to open the favourite.dart file in the same screens folder and modify as shown below:



    import 'package:flutter/material.dart';
    import 'package:product_fav/product_service.dart'; //add this
    import 'package:product_fav/utils.dart'; //add this

    class Favourites extends StatefulWidget {
      const Favourites({Key? key}) : super(key: key);
      @override
      State<Favourites> createState() => _FavouritesState();
    }
    class _FavouritesState extends State<Favourites> {
      List<Product>? favourites;
      bool _isLoading = false;
      bool _isError = false;

      @override
      void initState() {
        _getFavList();
        super.initState();
      }

      _getFavList() {
        setState(() {
          _isLoading = true;
        });
        ProductService().getAllFavourites().then((value) {
          setState(() {
            favourites = value;
            _isLoading = false;
          });
        }).catchError((e) {
          setState(() {
            _isLoading = false;
            _isError = true;
          });
        });
      }

      @override
      Widget build(BuildContext context) {
        return _isLoading
            ? const Center(
                child: CircularProgressIndicator(
                color: Colors.blue,
              ))
            : _isError
                ? const Center(
                    child: Text(
                      'Error loading products',
                      style: TextStyle(
                        color: Colors.red,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  )
                : favourites!.isEmpty
                    ? const Center(
                        child: Text(
                          'No items added to favourites yet',
                          style: TextStyle(
                            color: Colors.grey,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                      )
                    : ListView.builder(
                        itemCount: favourites?.length,
                        itemBuilder: (context, index) {
                          return Container(
                            decoration: const BoxDecoration(
                              border: Border(
                                bottom: BorderSide(width: .5, color: Colors.grey),
                              ),
                            ),
                            padding: EdgeInsets.fromLTRB(10, 20, 10, 20),
                            child: Row(
                              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                              children: [
                                Expanded(
                                  flex: 7,
                                  child: Column(
                                    crossAxisAlignment: CrossAxisAlignment.start,
                                    children: [
                                      Text(
                                        favourites![index].title,
                                        style: TextStyle(
                                            color: Colors.blue,
                                            fontWeight: FontWeight.w800),
                                      ),
                                      SizedBox(height: 10.0),
                                      Text(favourites![index].description)
                                    ],
                                  ),
                                ),
                              ],
                            ),
                          );
                        },
                      );
      }
    }


Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Imports the required dependencies
  • Creates the favourites, _isLoading, and _isError properties to manage the application state
  • Creates a _getFavList method to get the list of products marked as favourite using the getAllFavourites service and sets states accordingly
  • Uses the initState method to call the _getProductList method when the object is inserted into the tree
  • Modifies the UI widgets to show the products marked as favourite coming from Appwrite

With that done, we restart the application using the code editor or run the command below:



    flutter run


Enter fullscreen mode Exit fullscreen mode

We can also validate the entries by navigating to the Database section of Appwrite’s management console.

Data on Appwrite

Conclusion

This post discussed how to create a favourite product feature in Flutter using Appwrite’s database for storage. The Appwrite platform ships with services that speed up development processes. Try it out today and focus on what matters while Appwrite takes care of the tricky part.

These resources might be helpful:

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .