Building a Notes App with Offline Support - Advanced

A tutorial to build an Ionic app with offline support.

Introduction

This is part 2 and advanced part of the Building a Notes App with Offline Support tutorial. This part starts where the basic tutorial ended. Make sure you first complete the basic tutorial.

Step 10: Showing Network Status

In this step, you will add a label to show network status (online/offline) in the app header.
To show network state:

  1. Open Index page.
  2. Specify Text of Header Status: {{status}}.
  3. Add showState scope function.
Apperyio.get("AppClientGetState")({}).then(
function(success){
    $scope.status = success.state;
    $scope.$apply();
},
function(error){
    console.log(error);
},
function(notify){
    console.log(error);
});
  1. Edit init scope function.
$scope.visionStatus = {};
$scope.showState();

You can run now the app with the Internet on and without an Internet connection. The State of the network should be displayed in Header properly. Try to change the network status when the application is working. Status is not changing. Let’s add this capability.

  1. Add state change notification code at end of showState scope function:
Apperyio.get("AppClientGetState")({}).then(
    function(success) {
        $scope.status = success.state;
        $scope.$apply();
    },
    function(error) {
        console.log(error);
    },
    function(notify) {
        console.log(error);
    });

Apperyio.get("mssdk")().then(function(AppClientInstance) {
    //subscribe to AppClient state updates
    AppClientInstance.on("statechange", function(currentState) {
        $scope.status = currentState;
        if (currentState == "sync_failed") {
            Apperyio.navigateTo("Conflict");
        }
        $scope.$apply();
    });
});

Download and Resources

  • Download Ionic app backup file from this step.

📘

Checking Network Status

If you look at the Network tab in the Chrome browser Developer Tools, you see all the service requests when going from Notes to Note page.

Step 11: Adding Auto-login

In this step, you are going to add auto-login functionality. If you logged into the app before and launching the app after a period of no activity, the app will perform and auto-login without prompting the user for credentials.

  1. In the project tree open Services, and AppClientSettings making sure that autoLogin setting equals true.
  2. Create a new page and name it Loading.
  3. Mark Loading page as default in Routing.
  4. Add load scope function.
Apperyio.get("AppClientService").isUserLoggedIn().then(function(result) {
  	var page = result ? "Notes" : "Login";
    Apperyio.navigateTo(page);
    $scope.$apply();
});
  1. Edit init scope function of Loading page.
$scope.load();
  1. Auto-login with social networks

To enable auto-login with social-login together to use following code in success social login callback where token is social network login token

Apperyio.get("AppClientLogin")({
        "token": token
    }).then(
      	function(success) { // success callback 
    		},
        function(error) { // callback to handle request error
        });

Result in This Step

The app now supports logging in without prompting for user credentials.

Download and Resources

  • Download Ionic app backup file from this step.

Step 12: Adding a Logout

In this step, you are going to add a logout functionality.

To add a menu to app with logout option:

  1. Add a button to left part of Header of the index page.
  2. Remove the Text value.
  3. Set icon to ionic-navigation-round.
  4. Add the menu-toggle property with empty value.
  5. In the navigation bar (above the canvas), click the Page link.
  6. Set the Side menu attribute to True.
  7. Add a button inside menu:
  • Text Logout.
  • Add ng-click with the logout() value.
  • Add the menu-close attribute with an empty value.
  1. Add the logout scope function to index page:
Apperyio.get("AppClientLogout")({}).then( 
    function(success) {
        Apperyio.navigateTo("Login");
        $scope.$apply();

    },
    function(error) {
        console.log(error);
    },
    function(notify) {
        console.log(notify);
    });
  1. Open the Login page in Design view.
  2. Set Show Header to False.

Result in This Step

The app now has login and logout functionality.

Download and Resources

  • Download Ionic app backup file from this step.

📘

Emulating Offline State

To emulate offline state in Chrome browser, you can use Chrome Offline mode feature.

Step 13: Adding a Conflict Page

In this step, you will add a conflict page to the app which will display conflict situation which can happen after moving from offline to online state.

To add the conflict page and make it display the actions performed offline:

  1. Add page Conflict.
  2. Add a text
  • Text: Error: {{errorMessage}}.
  1. Add a text
  • Text: Operation : {{operation}}.
  1. Add a text
  • Text: Previous content: {{prevContent}}.
  • ng-show: operation != "CREATE".
  1. Add a text
  • Text: Server content: {{serverContent}}.
  • ng-show: conflictCode == "AE104".
  1. Add an input
  • ng-model: newContent.
  • ng-show: operation != "DELETE".
  1. Add a button:
  • Text: Resolve.
  • ng-click: resolve().
  1. Add the resolve scope function
Apperyio.get("AppClientResolveConflict")({
    "action": "UPDATE",
    "data": {
        "content": $scope.newContent
    }
}).then( 
    function(success) {
        alert("Conflict was resolved successfully");
    },
    function(error) {
        console.log(error)

    },
    function(notify) {
    });
  1. Add a button:
  • Text: Delete Conflict.
  • ng-click: deleteConflict().
  1. Add the deleteConflict scope function
Apperyio.get("AppClientResolveConflict")({
    "action": "DELETE"
}).then(
    function(success) {
        alert("Conflict was deleted");

    },
    function(error) {
        console.log(error)

    },
    function(notify) {
    });
  1. Add a button:
  • Text: Retry.
  • ng-click: retry().
  1. Add the retry scope function
Apperyio.get("AppClientRetrySync")({}).then( 
    function(success) {
        Apperyio.navigateTo("Notes");
        $scope.$apply();

    },
    function(error) {
        console.log(error)
    },
    function(notify) {
    });
  1. Add a button:
  • Text: Reset.
  • ng-click: reset().
  1. Add the reset scope function
Apperyio.get("AppClientResetFailedSync")({}).then( 
    function(success) {
        Apperyio.navigateTo("Notes");
        $scope.$apply();
    },
    function(error) {
        console.log(error);
    },
    function(notify) {}
    );
  1. Add a button:
  • Text: Notes.
  • navigate-to: Notes.

Result in This Step

  • The app now has a history page to show all operations performed when offline.

Download and Resources

  • Download Ionic app backup file from this step.

Step 14: Conflict sutation

To emulate conflict situation in ApperyNote app the unique index will be created for content field and after that, the attempt to insert a duplicate value into the content field will be prohibited (create rest service will return 400 HTTP status with the corresponding message).

  1. Open Databases > ApperyNoteDB > Notes > Manage indexes.
  2. Check unique.
  3. Check note (After that it will not be possible to insert two records with the same note into collection Notes ).
  4. Insert two records with the same note into collection Notes.

Result in This Step

If you try to add two records with the same note text into the Notes collection or update already existing note to the text of another existing one, the app will show the message that non-unique values are not allowed.

Step 15: Creating a Conflict in Offline Mode

To create a conflict in the offline mode and to make sure that information is not lost:

  1. Go offline.
  2. Create a duplicating note.
  3. Go online.

📘

Resetting Local Changes

Revert operation reverts all local changes made in offline mode without clearing all cached data. Possible reason for this can be failed synchronization, conflicted objects etc. It is recommended to use this operation in general cases.

Reset operation clears everything: cache and local history. It is recommended to use this only when absolutely necessary.

Step 16: Incremental Synchronization

The algorithm to implement incremental synchronization mechanism.

  1. Set sorting for the model by modifiedAt field.
  2. Add new field deleted in the Notes collection. Now the records will never be deleted from the DB: they will just be marked as deleted. Actually, all business fields after the delete operation can be empty (we need only _id, modifiedAt and deleted fields).
  3. Remove the Delete operation for the model.
  4. Add filtering in App for records which are marked as deleted and do not display them.

To add this feature to the app:

  1. Add a new column(type Boolean) in Notes collection, call it deleted.
  2. Open API Express and the ApperyNoteProject project.
  3. Click edit link notes.
  4. Press Include for deleted field.
  5. Set sorting (Sort column) for _updatedAt field (asc).
  6. Update remove scope function of Notes page.

📘

Tip

If some note(s) are deleted (not marked as deleted) from the database it can because of out of sync. data and malfunction of the app. To solve such issues, use reset, clearCache and revert operations.

Step 17: Example on Notes Table

This section provides examples of Notes table for various databases.

MySQL

CREATE TABLE Notes
(
    `_id` INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT,
    content VARCHAR(100),
    deleted TINYINT(1),
    `_updatedAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);
CREATE UNIQUE INDEX Notes__id_uindex ON Notes (`_id`);

Postgres

CREATE TABLE "Notes"
(
  _id SERIAL PRIMARY KEY NOT NULL,
  content VARCHAR(100),
  deleted BOOLEAN,
  "_updatedAt" TIMESTAMP DEFAULT now() NOT NULL
);
CREATE UNIQUE INDEX "notes__id_uindex" ON "Notes" (_id);

CREATE OR REPLACE FUNCTION update_updatedAt_column()
  RETURNS TRIGGER AS $$
BEGIN
  NEW."_updatedAt" = now();
  RETURN NEW;
END;
$$ language 'plpgsql';

CREATE TRIGGER update_Notes_updatedAt BEFORE UPDATE
  ON "Notes" FOR EACH ROW EXECUTE PROCEDURE
  update_updatedAt_column();

MSSQL

CREATE TABLE Notes
(
    _id INT PRIMARY KEY NOT NULL IDENTITY,
    content VARCHAR(100),
    deleted BIT,
    _updatedAt DATETIME DEFAULT getdate()
);
CREATE UNIQUE INDEX Notes__id_uindex ON Notes (_id);
	
CREATE TRIGGER update_Notes_updatedAt  ON Notes
AFTER UPDATE
AS
  UPDATE f set _updatedAt=GETDATE()
  FROM
    Notes AS f
    INNER JOIN inserted
    AS i
      ON f._id = i._id;

Oracle

CREATE TABLE "Notes"
(
	"_id" NUMBER(*) PRIMARY KEY NOT NULL,
	"content" VARCHAR2(100),
	"deleted" NUMBER(*) DEFAULT 0,
	"_updatedAt" TIMESTAMP(6)
);

CREATE SEQUENCE notes_id_seq START WITH 1;

CREATE OR REPLACE TRIGGER notes_bir
	BEFORE INSERT ON "Notes"
	FOR EACH ROW
	  BEGIN
		SELECT notes_id_seq.NEXTVAL
		INTO   :new."_id"
		FROM   dual;
	  END;

CREATE OR REPLACE TRIGGER update_Notes_updatedAt
	BEFORE INSERT OR UPDATE ON "Notes"
	FOR EACH ROW
		BEGIN
			:new."_updatedAt" := SYSTIMESTAMP;
		END;