I like to keep notes. This is how I record ideas that flash through my mind at any time of the day. As a professional developer, my job isn’t necessarily about typing code but more importantly coming up with solutions and expressing them in high quality code. So is yours if you are reading this post.

So I decided that instead of installing some third party app, what if I just create my own. I started by creating a simple personal notes app. Then I decided to enhance it to become a SAAS app, a full Notes Management System. SAAS stands for Software As A Service.

Here are the features i decided to incorporate in the app:

  1. SAAS App – Owner of the app can have users from all over the world creating accounts then keeping both private and public notes. Private Notes are your own personal notes and aren’t visible to other users. Public Notes are notes you don’t mind other people viewing.
  2. MySQL/MariaDB database – Notes as well as User Account are stored in MySQL database. MySQL is a fast relational database.
  3. PHP – PHP will be our server side programming language. It will run on the server and interact with the MySQL database then return a response to the client. The client in this case is an android app. We will write Object Oriented PHP and use RedbeanPHP as our ORM(Object Relational Mapper).
  4. ROLES AND PERMISSIONS– To create a SAAS App, you have to incorporate a User Management Capability in the app. User Management involves spliting roles and capability based on a given user’s capability. For users to user this app, they have to create an account. This is a onestep process and they have to type only a pin. We can programmatically pick their phone number well as their names. Then the notes they can view/edit/delete will be determined by their privileges.
  5. PHONE AUTHENTICATION– This app includes a super-fast and highly reliable yet easy to understand mode of authentication. We use Phone Numbers and PINS rather than email/password. Email and Passwords can be faked. On the other hand we can directly pick phone numbers from the user’s device provided we are granted the permission. We can also pick a name. This saves users from having to type several fields even during account creation. Interestingly, all the user needs to type is the PIN only.
  6. PRIVATE and PUBLIC NOTES – There are two types of notes, private and public notes. Public Notes are notes visible to all users. However they can be edited only by the creator. Private Notes on the other hand are notes private to only the creator. Only the creator can view/edit/delete these notes. The private notes can further be divided into DRAFTS and PUBLISHED. DRAFTS are notes you haven’t published yet. However be aware that drafts are also stored in the server just like PUBLISHED notes.
  7. SERVER SIDE PAGINATION – Public Notes will be paginated at the server. Pagination involves loading data in small chunks as opposed to loading everything from the server. Public Notes are paginated as they are likely to be many since they are being posted by many users. On the other hand private notes do not need pagination. However we will load all my private notes only once then split into DRAFTS and PUBLISHED categories.
  8. FULL CRUD CAPABILITY – The app involves full CRUD capability. Users can CREATE notes, READ notes, UPDATE notes and DELETE notes. Not only that but they can also Create Accounts and Login. All these operations involve performing CRUD operations against MySQL database.
  9. BEAUTIFUL UI – The app involves some really beautiful UI involving PIN-AUTHENTICATION Activity, Dashboard Fragment with Collasping Toolbar Layout, Private Notes Fragment involving both vertical and horizontal recyclerview. The horizontal recyclerview shows our drafts and uses LinearLayoutaManager. The vertical recyclerview on the other hand will show our published notes. We have swipeable tabs based on viewpager. We also have a detail activity with collapsing toolbar layout. We have a CRUD page for creating/updateing/deleting notes.
  10. USER-FRIENDLY PROGRESS MESSAGES – When user is communicating with the server, we will not only be showing a beautiful progress indicator, but also a title and message. This tells the user of status of the operation, whether it is starting,in progress, completed with failure or succeded. The messages are being shown in cardview at the top of the page. The message is automatically hidden after 10 seconds or whatever time you specify in code. Users can also manually close the card.
  11. CLEAN ARCHITECTURE – We use MVVM(Model View ViewModel) in this project, making it easy to understand and maintain.
  12. OFFLINE-FIRST – Users can use can view both their own as well as public notes and search through them as well when completely offline. When they want to post/edit/delete notes however, we will have to connect to the server. They post/edit/delete the notes and when they come back to the viewing page, we auto-refresh the notes and save the updates in locally in an NOSQL database called PaperDB. Users can refresh a list by just pulling it down, a technique we call Pull To Refresh.
  13. NoSQL Local database – We use a fast but super-simple local database instead of Room. This database is NoSQL and completely alleviates us from having to write SQL statements. It is also fast and extremely simple. So simple that you can save/fetch data via only a single line of code. And yes it does these operations in the background thread by default unlike Room. It is also reliable ad well maintained in Github.
  14. SharedPreferences – We implement a one-time login for users using SharedPreferences. This is the same thing that browsers like Chrome usualy do, you login only once in a given website and then the next time you come, they auto-login you. Browsers typically use cookies. For us we will use SharedPreferences. Through SharedPreferences, we can store simple key-value pairs reliably and securly in the device. We will use it to store a given user’s account credentials locally.

Creating Our Data Objects

We have to start by creating our data object class. This class will define the realworld object that we are basing our app. For example in this case we are creating a Notes Management System App. That will imply that we will need:

  1. Note Item
  2. User Item.

Such that we will be having users managing notes.

 

 

Let’s start with the Note class:

public class Note implements Serializable {
    /**
     * Let' now come define instance fields for this class. We decorate them with
     *
     * @SerializedName attribute. Through this we are specifying the keys in our json data.
     */
    @SerializedName("id")
    private String id;
    @SerializedName("title")
    private String title;
    @SerializedName("content")
    private String content;
    @SerializedName("category")
    private String category;
    @SerializedName("date_created")
    private String dateCreated;
    @SerializedName("status")
    private String status;
    @SerializedName("user_id")
    private String userID;

That implies our Note will have the following properties:

  1. Id – String(Android,But integer in MySQL) – Autogenerated by MySQL.
  2. Title – String(varchar) – EditText
  3. Content – String(text) – Multi-Line EditText
  4. Category – String(varchar) – Single-Choice Dialog
  5. Date Created – String(Date) – Single-Choice Dialog
  6. Status – String(varchar) – Single-Choice Dialog
  7. UserID – String – Auto-picked from Current User Credentials.

 

Then we will also have a user object:

public class User implements Serializable {
    @SerializedName("id")
    private String id;
    @SerializedName("name")
    private String name;
    @SerializedName("phone_number")
    private String phoneNumber;
    @SerializedName("pin")
    private String pin;
    @SerializedName("status")
    private String status;

The properties are:

  1. Id – Autogenerated by MySQL
  2. Name – Autopicked from the device.
  3. Phone Number – Autopicked from the device.
  4. PIN – User enters PIN using PinView.
  5. Status – Can be changed by admin from the server.

Because most of the properties of the user are being autopicked, a user needs to type only the PIN to either create an account or login.

What happens when a User types his PIN and presses the Login/Signup button?

  1. First we connect to the server.
  2. We then send the user details to the server.
  3. In the server we check if the user exists in the database, based on the Phone Number.
  4. If the user exists we sign him in using the phone number and pin.
  5. If the user exists but the pin doesn’t match the one in the mysql database, we tell the user that credentials are wrong and ask him to try again with a proper PIN.
  6. If the pin matches then the login is successful.
  7. If the user doesn’t exist in the database then we create an account for him and auto-sign him in.

Programmatically Repesenting Our JSON Response

We will be communicating between the client and the server via HTTP requests, by sending encoded JSON data between the two endpoints. Let’s therefore come and create a class that will represent a single response.

public class ResponseModel {
    /**
     * Our ResponseModel attributes
     */
    @SerializedName("code")
    private String code;
    @SerializedName("message")
    private String message;
    @SerializedName("notes")
    private List<Note> notes;
    @SerializedName("user")
    private User user;

The properties are:

  1. code – Our custom response code. e.g 1 to represent success, 2 to represent failure.
  2. message – Our custom response message e.g User Account Succesfully Created.
  3. notes – Notes we download from the server. It maybe be private or public notes.
  4. user – Our logged in User.

Programmatically Representing Our HTTP Request

We will be making HTTP requests to our server. All these requests have some common properties that can be abstracted into a single simple data object class.

Here are those properties:

  1. Response Code
  2. Response Message.
  3. Notes associated with the call.
  4. User associated with the call.
public class RequestCall {
    private int status;
    private String message;
    private List<Note> notes;
    private User user;
    private ResponseModel responseModel;

Defining our HTTP Methods

Let’s come define an interface to contain our HTTP methods:

public interface RestApi {

Fetching All Notes

You can fetch all notes using the following method. This involves making a HTTP request to our base url. No data is sent to the server using this method.

    @GET("index.php")
    Call<ResponseModel> getNotes();

 

Fetching Paginated,Queried Notes

Basically we want to:

  1. Fetch only a limited number of notes, starting at a given row upto a given row.
  2. Filtered down using a Query.
  3. Belonging to a given user.

Well to send those parameters we peform a HTTP POST request.

    @FormUrlEncoded
    @POST("index.php")
    Call<ResponseModel> fetchNotes(@Field("action") String action,
                                   @Field("query") String query,
                                   @Field("start") String start,
                                   @Field("limit") String limit,
                                   @Field("user_id") String userId);

 

 

CREATING NOTE

Users will be able to create a note. This is the method that defines parameters that will be sent to the server.

    @FormUrlEncoded
    @POST("index.php")
    Call<ResponseModel> createNote(@Field("action") String action,
                                   @Field("title") String name,
                                   @Field("content") String bio,
                                   @Field("category") String category,
                                   @Field("date_created") String date_created,
                                   @Field("status") String status,
                                   @Field("user_id") String userId
    );

 

UPDATING NOTE

The app will also have the capability to update a given note. We will need the id of the note to be updated. Again we use a HTTP POST request.

    @FormUrlEncoded
    @POST("index.php")
    Call<ResponseModel> updateNote(@Field("action") String action,
                                   @Field("id") String id,
                                   @Field("title") String name,
                                   @Field("content") String bio,
                                   @Field("category") String category,
                                   @Field("date_created") String date_created,
                                   @Field("status") String status,
                                   @Field("user_id") String userId
    );

 

DELETING NOTE

We can use a HTTP POST request for deletes as well. All we need is to send the id of row to be deleted, as well as the action to specify the operation we are trying to accomplish

    @FormUrlEncoded
    @POST("index.php")
    Call<ResponseModel> deleteNote(@Field("action") String action,
                                   @Field("id") String id);

CREATE ACCOUNT

Well users can also create accounts as we said. The below is the method responsible for defining properties to be sent.


    @FormUrlEncoded
    @POST("index.php")
    Call<ResponseModel> createAccount(@Field("action") String action,
                                      @Field("name") String name,
                                      @Field("phone_number") String phoneNumber,
                                      @Field("pin") String pin,
                                      @Field("status") String status);
    @FormUrlEncoded
    @POST("index.php")
    Call<ResponseModel> login(@Field("action") String action,
                                      @Field("phone_number") String phoneNumber,
                                      @Field("pin") String pin);

Caching Simple Variables and Data

We will be caching some variables and even notes statically. This saves us from having to fetch them every time we visit a given page. Moreover it provides a simple way of maintaining an application state.

public class CacheManager {
    public static String SEARCH_STRING = "";
    public static List<Note> MY_NOTES_MEM_CACHE = new ArrayList<>();
    public static List<Note> PUBLIC_LIST_MEM_CACHE = new ArrayList<>();
    public static boolean DISK_CACHE_DIRTY = false;
    public static boolean TURN_ON_AGGRESSIVE_CACHING = true;
    public static User CURRENT_USER = null;

}

Here are what they do:

  1. SEARCH_STRING – The search term the user types in the searchbox. We cache it since we will be using it in the adapter class and we don’t want to be injecting it via constructor everytime we are instantiating that adapter class.
  2. MY_NOTES_MEM_CACHE – We will hold user’s notes in this arraylist after we’ve fetched those notes from local database.
  3. PUBLIC_LIST_MEM_CACHE – Another arraylist to hold all public notes we’ve downloaded before saving them in bulk to local database.
  4. DISK_CACHE_DIRTY – This is a boolean. We can set it to true after successfully making an edit/delete/addition. Then the next time we come to the listing page, we auto-refresh our data from the server.
  5. TURN_ON_AGGRESSIVE_CACHING – We are implementing a load more pagination technique. Through this type of pagination, data is automatically downloaded as we scroll down the recyclerview. However this maybe too much if you have data that rarely changes. Instead you’d want a situation whereby after you’ve reached the end of a page and there is no more data, then the next scrolls will be ignored until the user refreshes data by pulling down on the recyclerview, or exits the fragment and comes back. To enable that, we turn on this variable.
  6. CURRENT_USER – The current user, that is the logged in user will be cached not only in the SharedPreferences but also in this variable.

Handling Permissions

We will need to implement the ability to grant/deny resources to a user based on his capability. For example you are only granted access to this app once you’ve logged in:

    public static boolean isLoggedIn() {
        return CURRENT_USER != null && !Utils.notNull(CURRENT_USER.getId()).isEmpty();
    }

You can only create a new note when logged in:

    public static boolean canCreateNote() {
        if (!isLoggedIn()) return false;
        return true;
    }

You can only edit/delete your own notes:

    public static boolean canEditOrDeleteNote(Note note) {
        if (!isLoggedIn()) return false;
        if (CURRENT_USER.getId() == note.getUserID()) {
            return true;
        }
        return false;
    }

We place these in the PermissionManager class:

public class PermissionManager {}

Our Application Constants

We will place all our static final fields, otherwise known as constants in our Const class:

public class Const {
    public static final String DATE_FORMAT = "yyyy-MM-dd";
    //public  static  final String BASE_URL = "https://camposha.info/php/user/bigthinkers/";

    //To use Localhost use your ip address e.g
    //public static  final String BASE_URL = "http://YOUR_IP_ADDRESS/php/user/bigthinkers/";
    public static  final String BASE_URL = "http://192.168.43.91/php/productivity/notesapp/";

    public static final int IN_PROGRESS = 0;
    public static final int SUCCEDED = 1;
    public static final int FAILED = -1;

    public static final int ERROR_STATE = -1;
    public static final int PROGRESS_STATE = 0;
    public static final int SUCCESS_STATE = 1;

    public static final String DO_CREATE_NOTE="DO_CREATE_NOTE";
    public static final String DO_UPDATE_NOTE="DO_UPDATE_NOTE";
    public static final String DO_DELETE_NOTE="DO_DELETE_NOTE";
    public static final String DO_FETCH_NOTES ="DO_FETCH_NOTES";
    public static final String DO_CREATE_ACCOUNT="DO_CREATE_ACCOUNT";
    public static final String DO_LOGIN="DO_LOGIN";

    public static final String USER_ACTIVE = "USER_ACTIVE";
    public static final String USER_INACTIVE = "USER_INACTIVE";
    public static final String USER_SUSPENDED = "USER_SUSPENDED";

    public static final String NOTE_PUBLISHED_PRIVATE="NOTE_PUBLISHED_PRIVATE";
    public static final String NOTE_PUBLISHED_PUBLIC="NOTE_PUBLISHED_PUBLIC";
    public static final String NOTE_DRAFT="NOTE_DRAFT";

    public static final String CACHE_KEY = "CACHE_KEY";
}
//end

PaperDB – Saving our Notes Locally

As we said earlier on, PaperDB is a fast and simple NoSQL database. We will use it to save our notes locally so that we can create an offline first app. Once data has been downloaded from our server, we will save them in the user’s device. Then even if the user closes the device and comes back, we will just load our data from the device. We don’t have to be connecting to the server everytime.

However if the user refreshes data, then we will also update our local database as well. At the end of day we are saving user alot of bandwith while also making the app fast and smooth.

Creating a PaperDB Key

PaperDB is NoSQL. Thus we will use unique keys to identify different books(tables). For example we need to differentiate the book that stores public notes, from that stores private notes and also from one that stores drafts. Hence we will pass the itemType, a string that tells us the type of item we are storing. We then generate a unique key based on that type.

    public static String getKey(String itemType) {
        if (CURRENT_USER == null) {
            return "";
        }
        if (CURRENT_USER.getId() == null || CURRENT_USER.getId().isEmpty()) {
            return "";
        }
        if (itemType == Const.NOTE_PUBLISHED_PRIVATE) {
            return  CURRENT_USER.getId()+"_published_notes";
        }else if (itemType == Const.NOTE_PUBLISHED_PUBLIC){
            return  "public_notes";
        }else if (itemType == Const.NOTE_DRAFT){
            return  CURRENT_USER.getId()+"_drafts";
        }
        return  "";

    }

Saving Data in PaperDB

Because we will be dumping a whole list into our database as opposed to saving one item at a time, we need a method that receives the list to be saved and the item type we are attempting to save.

    public static void save(List<Note> notes,String ITEM_TYPE) {
        deleteAll(ITEM_TYPE);
        Paper.book().write(getKey(ITEM_TYPE),notes);
    }

We’ve started by removing items so that we don’t have duplicates.

If you wanted to save a single item, for example saving single draft, this is the way you would do it:

    public static void saveDraft(Note note) {
        List<Note> drafts = fetch(Const.NOTE_DRAFT);
        drafts.add(note);
        Paper.book().write(getKey(Const.NOTE_DRAFT),drafts);
    }

Fetching From PaperDB

Fetching from PaperDB is supersimple:

    public static List<Note> fetch(String itemType) {
        return Paper.book().read(getKey(itemType), new ArrayList<>());
    }

You can see we are passing the item type so that we use it to get a key that will point us to the book from which you are fetching data. If no data is found then we return an empty arraylist.

Deleting From PaperDB

Well you can also delete data in a very simple manner:

    public static void deleteAll(String itemType) {
        Paper.book().delete(getKey(itemType));
    }

Saving Logged In User in SharedPreferences

We said earlier we want a one-time login app. There is no need for the user to be logging in everytime to the device. With a class like SharedPreferences, we can achieve this capability very easily.

We start by creating a class we call PrefUtils:

public class PrefUtils {

(a). Initializing SharedPreferences

To use SharedPreferences, you have to initialize it. To initialize it you need a Context object.

Here is the method that will do that initialization for us:

    private static SharedPreferences getPreferences(Context context){
        return context.getSharedPreferences("CURRENT_USER_DETAILS", Context.MODE_PRIVATE);
    }

 

(b). Saving a User into SharedPreferences

Let’s define public static method that will be used to save to sharedpreferences. All you need to pass it is a context object as well as a User object. The context will be used initialize the SharedPreferences.

    public static void save(Context context, User user) {

Then we prepare our Sharedpreferences editor:

        SharedPreferences.Editor editor = getPreferences(context).edit();

Now we can simply put items into our sharedpreferences, we put keys and values.

        editor.putString("ID", user.getId());
        editor.putString("NAME", user.getName());
        editor.putString("PHONE_NUMBER", user.getPhoneNumber());
        editor.putString("PIN", user.getPin());
        editor.putString("STATUS", user.getStatus());

Then to persist you can either invoke commit() or apply(). The latter is recommended as it utilises a background thread.

        editor.apply();
    }

(c). Retrieving From SharedPreferences

You can safely attempt to fetch from sharedpreferences using the following method.

    public static User load(Context context) {
        SharedPreferences sp = getPreferences(context);
        if(!sp.contains("ID")){
            return null;
        }
        User user=new User();
        user.setId(sp.getString("ID",""));
        user.setName(sp.getString("NAME",""));
        user.setPhoneNumber(sp.getString("PHONE_NUMBER",""));
        user.setName(sp.getString("PIN",""));
        user.setStatus(sp.getString("STATUS",""));
        return user;
    }

It is safe because first we attempt to search if atleast we have an id present. If it isn’t then we return null. Otherwise we proceed, instantiate a User then obtain the value from our sharedpreferences. You can also see that we are providing default values.

(d). Deleting From SharedPreferences

You can delete a user from shared preferences. This methid will be invoked if the user signs or logs out.

    public static void delete(Context context) {
        SharedPreferences.Editor editor = getPreferences(context).edit();
        editor.remove("ID");
        editor.remove("EMAIL");
        editor.remove("PASSWORD");
        editor.remove("NAME");
        editor.remove("PHONE_NUMBER");
        editor.remove("PIN");
        editor.remove("IMAGE_URL");
        editor.apply();

    }

Then the next time the user comes to the app, he/she will have to login again.

 

Continous Development

This project isn’t a one-time development app. We are continuing its development and adding more features as well as fxing bugs. We will be emailing buyers those updates.