How to build a mood tracker app in Flutter

Demola Malomo - Mar 29 '23 - - Dev Community

Over the years, founders, motivational speakers, and other experts have built tools and techniques to help people track their moods. It has helped people spot patterns, develop coping mechanisms, and answer some of their most profound questions about life.

In this post, we will learn how to build a personalized mood tracker using Appwrite’s database feature in a Flutter application. The project’s GitHub repository can be found here.

Prerequisites

To fully grasp the concepts presented in this tutorial, the following are required:

Getting started

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/mood_tracker.git
Enter fullscreen mode Exit fullscreen mode

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 following command:

    flutter run
Enter fullscreen mode Exit fullscreen mode

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

Home screen
Trend screen

Set up mood tracker service on Appwrite

To get started, we need to log into our Appwrite console, click the Create project button, input mood-tracker as the name, and then click Create.

Create project

Create a database, collection, and attributes

With our project created, we can set up our application database. First, navigate to the Database tab, click the Create database button, input trackers as the name, and then click Create.

Create database

Secondly, we need to create a collection for storing a user’s mood. To do this, click the Create collection button, input moods as the name, and then click Create.

Moods collection

Thirdly, we need to create attributes to represent our database fields. To do this, we need to navigate to the Attributes tab and create attributes for the collection, as shown below:

Attribute key Attribute type Size Required
rate integer YES
description string 5000 YES
createdAt datetime YES

Create attributes
Moods collection attributes

We also need to update our collection permissions to manage them accordingly. To do this, navigate to the Settings tab, scroll to the Update Permissions section, select Any, mark accordingly, and then click Update.

Select any
Mark permission and update

Add platform support

To add support for our Flutter app, navigate to the Home menu and click the Flutter App button.

Add platform

We can modify the Flutter application depending on the device used to run it, as shown below.

iOS

To obtain our Bundle ID, navigate to the path below:
ios > Runner.xcodeproj > project.pbxproj

Open the project.pbxproj file and search for PRODUCT_BUNDLE_IDENTIFIER.

Select new Flutter app

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 primary 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 to an XML file using the following path:
android > app > src > debug > AndroidManifest.xml

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

Android

Next, let’s modify the AndroidManifext.xml as shown below:

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.example.mood_tracker">
        <uses-permission android:name="android.permission.INTERNET"/>
        <application ...>
        <activity android:name="com.linusu.mobile_wallet.CallbackActivity" android:exported="true">
          <intent-filter android:label="mood_tracker">
            <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

Also, the highlighted [PROJECT_ID] must be replaced with the actual Appwrite project ID.

Building the mood tracker service

With all that done, let’s build the mood tracker service. First, we need to create models to convert the response sent from Appwrite to a Dart object. The models will also cater to JSON serialization. To do this, we need to create a utils.dart file in the lib folder and add the snippet below:

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

    class Mood {
      String? $id;
      int rate;
      String description;
      DateTime? createdAt;

      Mood({
        this.$id,
        required this.rate,
        required this.description,
        required this.createdAt,
      });

      Map<dynamic, dynamic> toJson() {
        return {
          "rate": rate,
          "description": description,
          "createdAt": createdAt!.toIso8601String(),
        };
      }

      factory Mood.fromJson(Map<dynamic, dynamic> json) {
        return Mood(
          $id: json['\$id'],
          rate: json['rate'],
          description: json['description'],
          createdAt: DateTime.tryParse(json['createdAt']),
        );
      }
    }
Enter fullscreen mode Exit fullscreen mode

We’ll need to modify the endpoint property to work with our system's local network address. We can adjust accordingly. Below are the instructions for both iOS and Android.

iOS

Navigate to the Network section and copy the IP address.

IP address

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

final String endpoint = "http://10.0.2.2/v1";
Enter fullscreen mode Exit fullscreen mode

Note: We can get the databaseId, projectId, and collectionId by navigating through the Appwrite console.

Next, we must create a service file to separate the application core logic from the UI. To do this, create a mood_service.dart file inside the lib directory. Then, add the snippet below:

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

    class MoodService {
      Client _client = Client();
      Databases? _db;

      MoodService() {
        _init();
      }

      //initialize the application
      _init() async {
        _client
            .setEndpoint(AppConstant().endpoint)
            .setProject(AppConstant().projectId);
        _db = Databases(_client);
        //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 createMood(int rate, String description) async {
        try {
          Mood newMood = Mood(
            rate: rate,
            description: description,
            createdAt: DateTime.now(),
          );
          var data = await _db?.createDocument(
            databaseId: AppConstant().databaseId,
            collectionId: AppConstant().collectionId,
            documentId: ID.unique(),
            data: newMood.toJson(),
          );
          return data;
        } catch (e) {
          throw Exception('Error creating mood!');
        }
      }

      Future<List<Mood>> getMoodList() async {
        try {
          var data = await _db?.listDocuments(
            databaseId: AppConstant().databaseId,
            collectionId: AppConstant().collectionId,
          );
          var moodList =
              data?.documents.map((mood) => Mood.fromJson(mood.data)).toList();
          return moodList!;
        } catch (e) {
          throw Exception('Error getting list of moods!');
        }
      }
    }
Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Imports the required dependencies
  • Creates a MoodService class with _client and _db properties to connect to the Appwrite instance and the database
  • Creates an _init method that configures Appwrite using the properties and also conditionally creates an anonymous user to access the Appwrite database
  • Creates a createMood and getMoodList methods that use the _db property to create and obtain a list of saved moods accordingly

Consuming the service

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

Creating moods

To get started, we need to modify the home.dart file in the screens directory and update it by doing the following:

First, we need to import the required dependencies and create a method to save the current mood to the database:

    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart';
    import 'package:mood_tracker/mood_service.dart';

    class Home extends StatefulWidget {
      @override
      State<Home> createState() => _HomeState();
    }

    class _HomeState extends State<Home> {
      List<int> list = <int>[1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
      final _formKey = GlobalKey<FormState>();
      final TextEditingController _description = TextEditingController();
      late int _rate;
      bool _isLoading = false;

      _createMood() {
        setState(() {
          _isLoading = true;
        });
        MoodService().createMood(_rate, _description.text).then((value) {
          setState(() {
            _isLoading = false;
          });
          _formKey.currentState!.reset();
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('Mood saved successfully!')),
          );
        }).catchError((_) {
          setState(() {
            _isLoading = false;
          });
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('Error saving mood!')),
          );
        });
      }

      @override
      Widget build(BuildContext context) {
        //UI code goes here
      }
    }
Enter fullscreen mode Exit fullscreen mode

The snippet above does the following, broken down by lines:

  • Lines 1-3: Import the required dependencies
  • Lines 13-15: Create the _description, _rate, and _isLoading properties to manage the application state
  • Lines 17-38: Create a _createMood method to save the current mood using the MoodService().createMood service, set states accordingly

Lastly, we need to modify the UI to use the method and states created to process the form.

    //import goes here

    class Home extends StatefulWidget {
      //code goes here
    }

    class _HomeState extends State<Home> {
      //states goes here

      _createMood() {
        //code goes here
      }

      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 30.0),
            child: Form(
              key: _formKey,
              child: Column(
                children: [
                  const Text('How are you feeling today',
                      style: TextStyle(
                        fontSize: 18.0,
                        fontWeight: FontWeight.bold,
                      )),
                  const SizedBox(height: 30.0),
                  Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      //title
                      Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          const Text(
                            'Rate your mood',
                            style: TextStyle(
                              color: Colors.grey,
                              fontSize: 14.0,
                            ),
                          ),
                          const SizedBox(height: 5.0),
                          DropdownButtonFormField(
                              items: list.map<DropdownMenuItem<int>>((int value) {
                                return DropdownMenuItem<int>(
                                  value: value,
                                  child: Text('$value'),
                                );
                              }).toList(),
                              validator: (value) {
                                if (value == null) {
                                  return 'Please rate your mood';
                                }
                                return null;
                              },
                              decoration: InputDecoration(
                                contentPadding: const EdgeInsets.symmetric(
                                    vertical: 10, horizontal: 20),
                                hintText: "select your mood",
                                focusedBorder: OutlineInputBorder(
                                  borderRadius: BorderRadius.circular(10),
                                  borderSide: const BorderSide(color: Colors.grey),
                                ),
                                enabledBorder: OutlineInputBorder(
                                  borderRadius: BorderRadius.circular(10),
                                  borderSide: const BorderSide(color: Colors.grey),
                                ),
                                errorBorder: OutlineInputBorder(
                                  borderRadius: BorderRadius.circular(10),
                                  borderSide: const BorderSide(color: Colors.red),
                                ),
                              ),
                              onChanged: ((value) {
                                setState(() {
                                  _rate = value!;
                                });
                              })),
                        ],
                      ),
                    ],
                  ),
                  const SizedBox(height: 30.0),
                  Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      const Text(
                        'Describing how you feel',
                        style: TextStyle(
                          color: Colors.grey,
                          fontSize: 14.0,
                        ),
                      ),
                      const SizedBox(height: 5.0),
                      TextFormField(
                        controller: _description,
                        validator: (value) {
                          if (value == null || value.isEmpty) {
                            return 'Please input your feeling';
                          }
                          return null;
                        },
                        inputFormatters: [LengthLimitingTextInputFormatter(70)],
                        decoration: InputDecoration(
                          contentPadding: const EdgeInsets.symmetric(
                              vertical: 10, horizontal: 20),
                          hintText: "describe your feeling",
                          fillColor: Colors.white,
                          focusedBorder: OutlineInputBorder(
                            borderRadius: BorderRadius.circular(10),
                            borderSide: const BorderSide(color: Colors.grey),
                          ),
                          enabledBorder: OutlineInputBorder(
                            borderRadius: BorderRadius.circular(10),
                            borderSide: const BorderSide(color: Colors.grey),
                          ),
                          errorBorder: OutlineInputBorder(
                            borderRadius: BorderRadius.circular(10),
                            borderSide: const BorderSide(color: Colors.red),
                          ),
                        ),
                        minLines: 6,
                        keyboardType: TextInputType.multiline,
                        maxLines: null,
                      ),
                    ],
                  ),
                  const SizedBox(height: 30.0),
                  SizedBox(
                    height: 45,
                    width: double.infinity,
                    child: TextButton(
                      onPressed: _isLoading
                          ? null
                          : () {
                              if (_formKey.currentState!.validate()) {
                                _createMood();
                              }
                            },
                      style: ButtonStyle(
                        backgroundColor:
                            MaterialStateProperty.all<Color>(Colors.blue),
                      ),
                      child: const Text(
                        'Save',
                        style: TextStyle(
                          color: Colors.white,
                          fontWeight: FontWeight.bold,
                          fontSize: 14.0,
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        );
      }
    }
Enter fullscreen mode Exit fullscreen mode

Get the list of moods

To obtain the list of moods, we need to modify the trends.dart file in the same screens directory as shown below:

    import 'package:fl_chart/fl_chart.dart';
    import 'package:flutter/material.dart';
    import 'package:mood_tracker/mood_service.dart';
    import 'package:mood_tracker/utils.dart';

    class Trends extends StatefulWidget {
      const Trends({Key? key}) : super(key: key);

      @override
      State<Trends> createState() => _TrendsState();
    }

    class _TrendsState extends State<Trends> {
      late List<Mood> moods;
      bool _isLoading = false;
      bool _isError = false;

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

      _getMoodList() {
        setState(() {
          _isLoading = true;
        });
        MoodService().getMoodList().then((value) {
          setState(() {
            moods = value;
            _isLoading = false;
          });
        }).catchError((e) {
          setState(() {
            _isLoading = false;
            _isError = true;
          });
        });
      }

      Widget build(BuildContext context) {
        return _isLoading
            ? const Center(
                child: CircularProgressIndicator(
                color: Colors.blue,
              ))
            : _isError
                ? const Center(
                    child: Text(
                      'Error getting list of transactions',
                      style: TextStyle(
                        color: Colors.red,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  )
                : Padding(
                    padding: const EdgeInsets.only(top: 30, bottom: 30, right: 30),
                    child: Container(
                        child: LineChart(
                      LineChartData(
                        minY: 0,
                        maxY: 10,
                        minX: 0,
                        maxX: 15,
                        borderData: FlBorderData(
                          show: true,
                          border: Border.all(),
                        ),
                        titlesData: FlTitlesData(
                          show: true,
                          topTitles: AxisTitles(
                            sideTitles: SideTitles(showTitles: false),
                          ),
                          rightTitles: AxisTitles(
                            sideTitles: SideTitles(showTitles: false),
                          ),
                          bottomTitles: AxisTitles(
                            sideTitles: SideTitles(showTitles: false),
                          ),
                        ),
                        lineBarsData: [
                          LineChartBarData(
                            spots: moods
                                .asMap()
                                .map((key, value) => MapEntry(key,
                                    FlSpot(key.toDouble(), value.rate.toDouble())))
                                .values
                                .toList(),
                            isCurved: true,
                            color: Colors.blueAccent,
                            barWidth: 2.5,
                            belowBarData: BarAreaData(
                              show: true,
                              color: Color.fromARGB(99, 142, 152, 169),
                            ),
                          ),
                        ],
                      ),
                    )),
                  );
      }
    }
Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Lines 1-4: Import the required dependencies
  • Lines 14-16: Create the moods, _isLoading, and _isError properties to manage the application state
  • Lines 18-40: Create a _getMoodList method to get the list of moods using the MoodService().getMoodList service, set states accordingly, and use the initState method to call the _getMoodList method when the object is inserted into the tree
  • Modifies the UI widgets to use the data saved on Appwrite to plot a graph showing the mood trend

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

    flutter run
Enter fullscreen mode Exit fullscreen mode

Conclusion

This post discussed how to use Appwrite’s database features to build a personalized mood tracker in a Flutter application. We built a base implementation to demonstrate Appwrite's support for scaffolding a working prototype. We can further extend the application's functionality by leveraging features like real-time communication, localization, and authentication.

These resources may also be helpful:

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