DEV Community

Cover image for Tracking medication dosages made easy with Appwrite’s database
Femi-ige Muyiwa for Hackmamba

Posted on

2 1 1

Tracking medication dosages made easy with Appwrite’s database

Tracking medication dosages made easy with Appwrite’s database

Tracking medication dosages is essential for checking the compliance and consistency level in the prescription given by medical personnel (doctors, pharmacists, etc.). Likewise, it’s crucial for patients’ recovery from the illness or medical condition it was prescribed to treat. Appwrite’s database makes creating an efficient database system to track medication dosages easy.

By the end of this tutorial, we will have created a fully functional database system for tracking medication dosages.

Here is the GitHub repository containing all the code.

Prerequisites

This tutorial requires the reader to satisfy the following:

  • Xcode (with developer account for Mac users).
  • iOS Simulator, Android Studio, or Chrome web browser to run the application.
  • An Appwrite instance running on either Docker, DigitalOcean droplet, or Gitpod. Check out this article for the setup.

Setting up the Appwrite project

In this section, we’ll set up an Appwrite project and create a platform. Let’s start by doing the following:

  • Open your browser and enter the IP address or hostname.
  • Choose Create Project option and fill in the project name and ID. (ID can be automatically generated.)
    create project
    create project  2

  • Next, head to the databases section, select the Create Database option, and fill in the desired database name and ID.
    create database

  • Next, we will need to create two collections within the database. The first collection will hold the medication data. Thus, we will create eight attributes: four Boolean (isTrack, isInjection, isTablet, isSyrup), three strings (medicationname, userID, id), and one integer (dosages). This collection will hold the medication information.

    The second collection will hold the scheduled medications. Thus, we will need four string attributes (date, medicationname, userID, id) and a single Boolean attribute (isTrack).

create collection

  • Finally, set the collection level CRUD permission for both collections to Any and select all the options. collection permission collection permission

Getting started

In this section, we will clone a Flutter UI template, connect our Flutter app to Appwrite, and explain the functionality of our project.

Clone the UI template
This section uses a UI template containing user registration and login code. Let’s clone the repository specified in the prerequisites. Check out the official GitHub docs to learn more about cloning a repository.

clone app
clone app 2

After cloning the UI to local storage, open it in your preferred code editor and run the command below:

flutter pub get
Enter fullscreen mode Exit fullscreen mode

This command obtains all the dependencies listed in the pubspec.yaml file in the current working directory and their transitive dependencies. Next, run the command flutter run, and our application should look like the gif below:

clone result 1

clone result 2

clone result 3

The gif above shows the UI and functionality of the medication tracker, in which there is a section to add medication and track medication (overdue medication and currently tracking). These functionalities were achieved using Appwrite and Flutter_local_notification package.

Note: This program doesn’t use the Appwrite real-time API. Thus, check the resources section for references to its use.

The following sections provide a breakdown of how to implement the features shown above. Let’s start by showing how to connect Appwrite to a Flutter application and seed data to the Appwrite database collection.

Installing and connecting Appwrite to Flutter
First, install the Appwrite, Flutter_local_notification, and the Provider package for state management into the app. We can add them to the dependencies section of the pubspec.yaml file, like the image below:

pubspec.yaml

Alternatively, we can use a terminal by typing the command below:

flutter pub add appwrite

# and

flutter pub add Provider

# and

flutter pub add flutter_local_notifications
Enter fullscreen mode Exit fullscreen mode

Here’s how to connect a Flutter project to Appwrite for Android and iOS devices.

iOS
First, obtain the bundle ID by navigating to the project.pbxproj file (ios > Runner.xcodeproj > project.pbxproj) and searching for the PRODUCT_BUNDLE_IDENTIFIER.

Next, head to the Runner.xcworkspace folder in the application’s iOS folder in the project directory on Xcode. To select the runner target, choose the Runner project in the Xcode project navigator and find the Runner target. Next, select General and IOS 11.0 in the deployment info section as the target.

ios

Android
For Android, copy the XML script below and paste it below the activity tag in the Androidmanifest.xml file (to find this file, head to android > app > src > main).

<activity android:name="com.linusu.flutter_web_auth_2.CallbackActivity" android:exported="true">
<intent-filter android:label="flutter_web_auth_2">
<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>

Note: change [PROJECT-ID] to the ID you used when creating the Appwrite project.

We will also need to set up a platform within the Appwrite console. Follow the steps below to do so.

  • Within the Appwrite console, select Create Platform and choose Flutter for the platform type.
  • Specify the operating system: in this case, Android.
  • Finally, provide the application and package names (found in the app-level build.gradle file).

create platform 1
create platform 2
create platform 3

Creating the providers
Since we will use the provider package for state management, here are the changeNotifier classes to be utilized:

  1. UserRegProvider

    class UserRegProvider extends ChangeNotifier {
    Client client = Client();
    late Account _account;
    late bool _isloading;
    late bool _isloggedin;
    late bool _signedin;
    User? _user;
    bool get isloading => _isloading;
    bool get isloggedin => _isloggedin;
    bool get signedin => _signedin;
    User? get user => _user;
    UserRegProvider() {
    _isloading = true;
    _isloggedin = false;
    _signedin = false;
    _user = null;
    initializeclass();
    }
    initializeclass() {
    client
    ..setEndpoint(Appconstants.endpoint)
    ..setProject(Appconstants.projectid);
    _account = Account(client);
    _checksignin();
    }
    // check if there is an existing user
    _checksignin() async {
    try {
    _user = await _getUseraccount();
    notifyListeners();
    } catch (e) {
    _isloggedin = false;
    _isloading = false;
    // print("hello");
    notifyListeners();
    }
    }
    // map user details to the model class created earlier
    Future<User?> _getUseraccount() async {
    try {
    final response = await _account.get();
    if (response.status == true) {
    final jsondata = jsonEncode(response.toMap());
    final json = jsonDecode(jsondata);
    // print(json);
    _isloggedin = true;
    _isloading = false;
    return User.fromJson(json);
    } else {
    return null;
    }
    } catch (e) {
    // print("get user: hello");
    rethrow;
    }
    }
    // login method
    login(String email, String password) async {
    try {
    final result = await _account.createEmailSession(
    email: email,
    password:
    password); // create a new email session with respect to the parameters provided (if an account exist, then we will be able to create a new session)
    _isloading = true;
    notifyListeners();
    if (result.userId.isNotEmpty) {
    _user = await _getUseraccount();
    _isloading = false;
    _isloggedin = true;
    notifyListeners();
    // print("login provider: ${_isloggedin}");
    }
    notifyListeners();
    } catch (e) {
    rethrow;
    }
    }
    // sign up method
    signup(String name, String email, String password) async {
    try {
    final result = await _account.create(
    userId: 'unique()',
    name: name,
    email: email,
    password:
    password); // create a new account with respect to the parameters given
    _isloading = true;
    notifyListeners();
    if (result.status == true) {
    _signedin = true;
    _isloading = false;
    // _checklogin();
    }
    notifyListeners();
    } catch (e) {
    rethrow;
    }
    }
    }
  2. AppProvider class

    class AppProvider extends ChangeNotifier {
    Client client = Client();
    late Databases _databases;
    late bool _isloading;
    List<Medications>? _meditem;
    bool get isloading => _isloading;
    List<Medications>? get meditem => _meditem;
    AppProvider() {
    _isloading = true;
    initializeclass();
    }
    initializeclass() {
    client
    ..setEndpoint(Appconstants.endpoint)
    ..setProject(Appconstants.projectid);
    _databases = Databases(client);
    _isloading = false;
    }
    // load medication method
    loadMedication(String userid) async {
    try {
    final response = await _databases.listDocuments(
    collectionId: Appconstants.collectionID,
    databaseId: Appconstants.dbID,
    queries: [
    // Query.orderAsc('date'),
    Query.equal('userID', [userid])
    ]);
    _meditem = response.documents
    .map((meditem) => Medications.fromJson(meditem.data))
    .toList();
    notifyListeners();
    } catch (e) {
    print(e);
    }
    }
    // add medication method
    addMedication(String medicationname, String userID, int dosages, bool isInjection,
    bool isTablet, bool isSyrup) async {
    try {
    final response = await _databases.createDocument(
    collectionId: Appconstants.collectionID,
    data: {
    'id': '',
    'medicationname': medicationname,
    'userID': userID,
    'dosages': dosages,
    'isInjection': isInjection,
    'isTablet': isTablet,
    'isSyrup': isSyrup,
    'check':false,
    'isTrack':false
    },
    databaseId: Appconstants.dbID,
    documentId: 'unique()',
    );
    final String documentId = response.$id;
    // update the ID of the medication.
    await _databases.updateDocument(
    databaseId: Appconstants.dbID,
    collectionId: Appconstants.collectionID,
    documentId: documentId,
    data: {'id': documentId});
    if (response.$id.isNotEmpty) {
    _isloading = false;
    }
    notifyListeners();
    } catch (e) {
    _isloading = false;
    notifyListeners();
    print(e);
    }
    }
    }
  3. ReminderProvider

    class ReminderProvider extends ChangeNotifier {
    Client client = Client();
    late Databases _databases;
    late bool _isloading;
    List<ReminderModel>? _reminderitemtrue;
    List<ReminderModel>? _reminderitem;
    bool get isloading => _isloading;
    List<ReminderModel>? get reminderitem => _reminderitem;
    List<ReminderModel>? get reminderitemtrue => _reminderitemtrue;
    ReminderProvider() {
    _isloading = true;
    initializeclass();
    }
    initializeclass() {
    client
    ..setEndpoint(Appconstants.endpoint)
    ..setProject(Appconstants.projectid);
    _databases = Databases(client);
    }
    // load reminder
    loadReminders(String userid) async {
    try {
    final response = await _databases.listDocuments(
    collectionId: Appconstants.collectionID2,
    databaseId: Appconstants.dbID,
    queries: [
    Query.orderDesc('date'),
    Query.equal('userID', userid),
    Query.equal('isTrack', false)
    ]);
    _reminderitem = response.documents
    .map((reminder) => ReminderModel.fromJson(reminder.data))
    .toList();
    _isloading = false;
    // print("this is ${_reminderitem![0].date}");
    notifyListeners();
    } catch (e) {
    print(e);
    }
    }
    // load reminder with item false
    loadReminderstrue(String userid) async {
    try {
    final response = await _databases.listDocuments(
    collectionId: Appconstants.collectionID2,
    databaseId: Appconstants.dbID,
    queries: [
    Query.orderDesc('date'),
    Query.equal('userID', userid),
    Query.equal('isTrack', true)
    ]);
    _reminderitemtrue = response.documents
    .map((reminder) => ReminderModel.fromJson(reminder.data))
    .toList();
    _isloading = false;
    notifyListeners();
    } catch (e) {
    print(e);
    }
    }
    }
class ReminderProvider extends ChangeNotifier {
// the previous code
// addreminder method
addReminder(
String medname,
String medid,
String userid,
String date,
) async {
try {
var response = await _databases.createDocument(
collectionId: Appconstants.collectionID2,
data: {
'id': "",
'medicationname': medname,
'userID': userid,
'date': date,
'isTrack': false,
},
databaseId: Appconstants.dbID,
documentId: ID.unique(),
);
final String documentId = response.$id;
await _databases.updateDocument(
databaseId: Appconstants.dbID,
collectionId: Appconstants.collectionID2,
documentId: documentId,
data: {'id': documentId});
print("done for adding");
notifyListeners();
} catch (e) {
_isloading = false;
if (e is AppwriteException) {
_databases.updateDocument(
collectionId: Appconstants.collectionID2,
databaseId: Appconstants.dbID,
documentId: medid,
data: {
'date': date,
});
print('done');
} else {
print('Unknown error: $e');
}
notifyListeners();
}
}
updateisTrack(String medid) async {
try {
await _databases.updateDocument(
databaseId: Appconstants.dbID,
collectionId: Appconstants.collectionID2,
documentId: medid,
data: {'isTrack': true});
} catch (e) {
rethrow;
}
}
void removeReminder(String documentID) async {
try {
final response = await _databases.deleteDocument(
databaseId: Appconstants.dbID,
collectionId: Appconstants.collectionID2,
documentId: documentID);
print('done');
notifyListeners();
} catch (e) {
rethrow;
}
}
void removeReminderfromList(int index) async {
try {
_reminderitem!.removeAt(index);
notifyListeners();
} catch (e) {
rethrow;
}
}
void removeReminderfromListtrue(int index) async {
try {
_reminderitemtrue!.removeAt(index);
notifyListeners();
} catch (e) {
rethrow;
}
}
}

We used queries for the userID in the lists in case there is a need to add a super admin who will have access to all the items in the list. Also, we queried the isTrack attribute in the ReminderProvider to segregate the currently tracked processes to prevent a rerun in the instance when we run our program again.

Lastly, using JSON serialization, we can map the list of documents from Appwrite to a class and get our items in a list. Thus, we have created three model classes for each ChangeNotifier class.

Next, let’s head to the app_constants.dart file to store important constants within a class, such as the projectID, database, collection ID, and so on.

class Appconstants {
  static const String projectid = "<projectID>";
  static const String endpoint = "<Hostname or IP address>";
  static const String dbID = "<database ID>";
  static const String collectionID = "<collection ID 1>";
  static const String collectionID2 = "<Collection ID 2>";
}
Enter fullscreen mode Exit fullscreen mode

Adding medication details
This project uses three Notifier classes — UserRegProvider, AppProvider, ReminderProvider — to map out the user registration, medication document, and reminder document object. Thus, declare the MultiProvider class inside the runApp function of the main.dart file. This MultiProvider class will assist us in building a tree of providers like this:

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(MultiProvider(
    providers: [
      ChangeNotifierProvider(
        create: (context) => UserRegProvider(),
        lazy: false,
      ),
      ChangeNotifierProvider(
        create: (context) => AppProvider(),
        lazy: false,
      ),
      ChangeNotifierProvider(
        create: (context) => ReminderProvider(),
        lazy: false,
      ),
    ],
    child: const MyApp(),
  ));
}
Enter fullscreen mode Exit fullscreen mode

This allows us to use methods, setters, and getters from the provider classes around our project via the Consumer widget or by calling the provider class. First, use the routerclass.dart to create navigation routes between the Add Medication Information and Track Medication information pages.

Thus, in the Add Medication Information page (register_medication.dart file), we have a stateful widget in which the body property of the scaffold widget takes a condition. This condition checks whether state1 (UserRegProvider) via the consumer is true; it should show a circularloader widget, and if it is false, it should show the appcontainer widget. This condition is to prevent the UI from loading before the provider. If that happens, we will get a null error, as some UI properties vary depending on the provider.

The appcontainer widget contains a column of the logged-in username (retrieved from the UserRegProvider class) and a form field consisting of a string text field, integer text field checkboxes, and a submit button. We can add the medication information to our database collection created earlier with this form field, and its implementation is in the [**medicine_details.dart**](https://gist.github.com/muyiwexy/5d6b638092d6472d39423f807ca09010) file.

Tracking the medication
To track the medication, we will use the Appwrite database collection to track our medication by attaching a Boolean value to each tracked medication. We will then use the flutter_local_notifications package to schedule a notification for each tracked medication.

Thus, in the Track Medication Information page (track_medication.dart file), we have a column of a button and a list area (divided into a row of Overdue and Currently tracking). When we click the button, it shows an alertdialog containing a checklist of all the medications. This checklist allows checking only one item at a time, and once we check an item, it shows a dropdown list to select the date form field to schedule the medication.

If a date or time before the current date is added, and we click done, then the item will be added to the overdue section. However, if a date is after the current date, then it will run the notification function.

class NewAlertDialog extends StatefulWidget {
const NewAlertDialog(
{required this.user,
required this.meditem,
required this.reminderstate,
super.key});
final User? user;
final List<Medications>? meditem;
final ReminderProvider reminderstate;
@override
State<NewAlertDialog> createState() => _NewAlertDialogState();
}
class _NewAlertDialogState extends State<NewAlertDialog> {
TextEditingController dateinput = TextEditingController();
final NotificationService _notificationService = NotificationService();
bool get isAtLeastOneChecked =>
widget.meditem!.any((element) => element.check == true);
void _selectOption(int index) {
if (mounted) {
setState(() {
for (int i = 0; i < widget.meditem!.length; i++) {
if (i == index) {
widget.meditem![i].check = true;
} else {
widget.meditem![i].check = false;
}
}
});
}
}
void submitButtonAction(int index) async {
int idMaker = widget.reminderstate.reminderitem!.length + 1;
print(idMaker);
await widget.reminderstate.addReminder(
widget.meditem![index].medicationname!,
widget.meditem![index].id!,
widget.meditem![index].userID!,
dateinput.text);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text("Medication List"),
content: widget.meditem != null
? medicationcontent()
: const Center(
child: Text(
"You have not Registered any user or You are not connected\nCheck your connection settings!"),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
// if (mounted) {
// }
},
child: const Text(
"Close",
style: TextStyle(fontSize: 20),
)),
TextButton(
onPressed: isAtLeastOneChecked
? () async {
// _addTimer();
await widget.reminderstate.loadReminders(widget.user!.id!);
if (mounted) {
await _notificationService.scheduleNotifications(
context, widget.reminderstate.reminderitem!);
}
await widget.reminderstate.loadReminders(widget.user!.id!);
await widget.reminderstate.loadReminderstrue(widget.user!.id!);
if (mounted) {
Navigator.pop(context);
}
}
: null,
child: const Text("Done", style: TextStyle(fontSize: 20)),
)
],
);
}
// add medicationcontent widget here
}

The done button triggers the _notificationService.scheduleNotifications(); while passing context, widget.reminderstate.reminderitem! as parameters. These parameters will assist in building the notification function. Here is the code for the notification function:

class NotificationService {
//NotificationService a singleton object
static final NotificationService _notificationService =
NotificationService._internal();
factory NotificationService() {
return _notificationService;
}
NotificationService._internal();
static const channelId = '123';
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
Future<void> init() async {
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher');
var initializationSettingsIOS = const DarwinInitializationSettings();
var initializationSettings = InitializationSettings(
android: initializationSettingsAndroid, iOS: initializationSettingsIOS);
tz.initializeTimeZones();
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
}
final AndroidNotificationDetails _androidNotificationDetails =
const AndroidNotificationDetails(
'channel ID',
'channel name',
playSound: true,
priority: Priority.high,
importance: Importance.high,
);
Future<void> scheduleNotifications(
BuildContext context, List<ReminderModel> reminders) async {
// Initialize the time zone database
tz.initializeTimeZones();
for (var i = 0; i < reminders.length; i++) {
var reminder = reminders[i];
// Convert the reminder's scheduled time to the local time zone
DateTime dateTime = DateFormat("MM/dd/yyyy H:mm").parse(reminder.date!);
int secondsDifference = dateTime.difference(DateTime.now()).inSeconds;
if (secondsDifference < 0) {
reminders.removeAt(i);
i--;
continue;
}
// Schedule a notification for the reminder
flutterLocalNotificationsPlugin.zonedSchedule(
0, // Use the reminder's ID as the notification ID
"Take Your Medication\nMedication Name: ${reminder.medicationname}",
"Medication Dosage: ${reminder.date}",
tz.TZDateTime.now(tz.local).add(Duration(seconds: secondsDifference)),
NotificationDetails(android: _androidNotificationDetails),
androidAllowWhileIdle: true,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime);
// Print a message to the console indicating that the notification was scheduled
print("Scheduled notification for reminder with ID ${reminder.id}");
ReminderProvider state3 =
Provider.of<ReminderProvider>(context, listen: false);
await state3.updateisTrack(reminder.id.toString());
}
}
Future<void> cancelNotifications(String id) async {
await flutterLocalNotificationsPlugin.cancel(int.parse(id));
print("object done");
}
}

The code above defines a NotificationService class that handles scheduling and canceling local notifications using FlutterLocalNotificationsPlugin. The NotificationService class follows the singleton pattern, ensuring that only one class instance can exist simultaneously.

Next, we create the init() method to initialize the FlutterLocalNotificationsPlugin, which is required before scheduling any notifications. The scheduleNotifications() method schedules notifications for a list of reminders (the parameter passed earlier). It converts the scheduled time of each reminder to the local time zone and schedules a notification using flutterLocalNotificationsPlugin.zonedSchedule().

Lastly, the cancelNotifications() method cancels a previously scheduled notification using flutterLocalNotificationsPlugin.cancel().

So, to use this code properly, we will need to call the init() method in the void main function, like so:

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  NotificationService().init();
  // add runapp method
}
Enter fullscreen mode Exit fullscreen mode

The earlier results were on a web platform, and flutter_local_notifications does not support the web yet. So here, for the moment of truth:

Image description

Conclusion

Taking medications isn’t the most pleasant task of the day, but failing to do so can pose a risk to a patient and cause problems for their caregivers or healthcare providers. Thus, a scheduler can come in handy, as we can now set a future time and receive a notification when it is time. The Appwrite database assists in sorting out those lists, and with a package like the flutter_local_notifications, we are guaranteed to follow our medication schedule.

Check out these additional resources that might be helpful:

Sentry mobile image

Is your mobile app slow? Improve performance with these key strategies.

Improve performance with key strategies like TTID/TTFD & app start analysis.

Read the blog post

Top comments (1)

Collapse
 
tomcarran profile image
tomcarran

This tutorial is incredibly insightful and well-detailed, guiding users through the process of setting up Appwrite's database for efficient medication dosage tracking in Flutter. The integration of Flutter_local_notifications adds a practical touch to medication management, showcasing the power of combining Appwrite's capabilities with Flutter's user interface. I have seen this concept already on openingtoclosehours i think. Well Great job on providing a comprehensive guide for developers interested in creating a medication tracking system! 👏🚀

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay