Building a Seamless Offline Experience with WatermelonDB and AdonisJs

Laying the Foundation

To get started, clone the repository and check out the v1 branch. Once you have the code and its dependencies installed, launch the app on your platform of choice using either your device or an emulator.

Syncing Data with WatermelonDB

WatermelonDB provides an out-of-the-box sync feature that allows us to keep our data in sync across devices. We’ll create a sync.ts file inside the data folder and define three essential functions: synchronize, pullChanges, and pushChanges. These functions will enable our app to communicate with the server API and exchange data.


// sync.ts
import { Database } from '@nozbe/watermelondb';

const synchronize = async (database: Database) => {
  // synchronization logic
};

const pullChanges = async (database: Database) => {
  // pulling changes logic
};

const pushChanges = async (database: Database) => {
  // pushing changes logic
};

Generating the AdonisJs API App

Using AdonisJs, we’ll generate a new API app by running yarn create adonis-ts-app api. This will create a directory named api with the necessary boilerplate code. We’ll then set up our database using MySQL and Lucid as the ORM.


yarn create adonis-ts-app api

Defining the Data Structure

To replicate the data structure from WatermelonDB, we’ll create a migration to define the weights table in our MySQL database. We’ll also generate a Weight model to interact with the data.


// migration.ts
import {Migration} from '@adonisjs/lucid';

export default class CreateWeightsTable extends Migration {
  public async up() {
    this.schema.createTable('weights', (table) => {
      table.increments('id');
      table.dateTime('date');
      table.decimal('weight');
    });
  }

  public async down() {
    this.schema.dropTable('weights');
  }
}

Building the REST API Endpoints

Next, we’ll create two REST API endpoints: one for GET requests to pull data from the server and another for POST requests to push data to the server. These endpoints will be triggered by the pullChanges and pushChanges functions from WatermelonDB.


// WeightController.ts
import { Controller, Get, Post } from '@adonisjs/core';
import Weight from 'App/Models/Weight';

@Controller('weights')
export default class WeightController {
  @Get()
  public async pullWeights() {
    // pulling weights logic
  }

  @Post()
  public async pushWeights() {
    // pushing weights logic
  }
}

Implementing the Sync Indicator UX

To provide a seamless user experience, we’ll create a sync indicator component that displays the syncing status to the user. This component will trigger the sync function when the app is opened and display a success or failure message accordingly.


// SyncIndicator.js
import React, { useState, useEffect } from 'eact';
import { synchronize } from '../data/sync';

const SyncIndicator = () => {
  const [syncing, setSyncing] = useState(false);
  const [syncError, setSyncError] = useState(null);

  useEffect(() => {
    synchronize().then(() => {
      setSyncing(false);
    }).catch((error) => {
      setSyncError(error);
      setSyncing(false);
    });
  }, []);

  return (
{syncing? (Syncing…

) : ({syncError? `Sync failed: ${syncError}` : ‘Sync successful’}

)}


  );
};

export default SyncIndicator;

Testing the Offline Sync Behavior

To test our offline sync behavior, we’ll simulate an offline scenario by turning on airplane mode, adding a new weight entry, and then reconnecting to the internet. We should see the sync indicator display a success message, and the new entry should be visible in the server database.

Where to Go from Here

Congratulations! You now have a weight tracking app with full offline support. However, there’s still room for improvement. Consider adding features like:

  • detailed error viewers
  • data export and import capabilities
  • partial sync failure handling

to take your app to the next level.

Leave a Reply