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:
- Basic understanding of Dart and Flutter
- Flutter SDK installed
- Xcode with a developer account (for Mac users)
- Either iOS Simulator, Android Studio, or Chrome web browser to run the application
- An Appwrite instance; check out this article on how to set up an instance locally or install it with one click on DigitalOcean or Gitpod
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
Running the project
First, we need to install the project dependencies by running the command below:
flutter pub get
Then, run the project using the following command:
flutter run
The command above will run the application on the selected device.
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 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.
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.
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 |
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.
Add platform support
To add support for our Flutter app, navigate to the Home menu and click the Flutter App button.
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
.
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.
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.
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>
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']),
);
}
}
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.
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";
Note: We can get the databaseId
, project
I
d
, 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!');
}
}
}
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
andgetMoodList
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
}
}
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 theMoodService().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,
),
),
),
),
],
),
),
),
);
}
}
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),
),
),
],
),
)),
);
}
}
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 theMoodService().getMoodList
service, set states accordingly, and use theinitState
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
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: