This is a project designed to help you master RxJava2, Retrofit2, Room, Clean Architecture and MVVM. It’s a full app and can easily be used as a template for creation of projects involving one or all of the above technologies. It’s a Folktale app. You publish Tales to the server. Viewers with the app can then read those tales. You can update the tales, delete them, search them. The Tales are paginated so overtime the app can handle hundreds of thousands of tales. It all depends on your server, the app is very scalable and fast.

Here are the technologies you will learn:

  1. Java Programming Language.
  2. RxJava2
  3. Retrofit2
  4. Room
  5. Clean Architecture
  6. Model View ViewModel

Here are the concepts you will learn:

  1. MySQL CRUD from android app
  2. Room CRUD from MySQL data.
  3. Search mysql in realtime
  4. MySQL Pagination.
  5. Full app development.

Demo

Here is the demo of the project:

Video Demo and Introduction

Check out YouTube Video showing the demo:

Step 1 – Download Project

Go ahead and download the project using the link at the bottom of this page. Note that this is a premium project.

  1. You will find a zipped file.
  2. Extract it somewhere in your hard drive. It contains both the Kotlin Code and PHP code.
  3. Move to the next section.

Step 2 – Import into android studio and run

Go to android studio.

  1. Then go to File –> New –> Import Project
  2. Select the extracted project.
  3. It will import it into android studio.
  4. Now click the sync button to synchronize the dependencies. Basically we need to download the gradle dependencies before running this project for the first time.
  5. Once the sync is complete click run to run the project.

Now let’s move to explain project step by step.

Explanations

(a). Gradle Scripts

We have two build.gradle scripts:

  1. build.gradle(Project Level)
  2. build.gradle(App level)

In the first file we have added registered maven repository jitpack as one of those places we will be downloading our dependencies:

allprojects {
    repositories {
        google()
        jcenter()
        maven { url "https://jitpack.io" }
    }
}

In the second build.gradle files we have placed android configurations in the android closure. For example here are some of our configurations:

  1. Minimum SDK version as API level 16.
  2. Compile SDK version, build tools version etc.

We have also enabled java8:


    compileOptions {
        targetCompatibility 1.8
        sourceCompatibility 1.8
    }

We have also enabled data binding:

    dataBinding {
        enabled = true
    }

Androidx

Under the dependencies we have added basic androidx packages:

    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'androidx.cardview:cardview:1.0.0'
    implementation 'androidx.recyclerview:recyclerview:1.1.0'
    implementation 'com.google.android.material:material:1.0.0'

Lifecycle Packages

Then we have also added the following two lifecycle packages:

    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
    annotationProcessor 'androidx.lifecycle:lifecycle-common-java8:2.2.0'

The above will give us the following:

  1. AndroidViewModel
    public class MainViewModel extends AndroidViewModel {
    //
    }

    AndroidViewModel will allow us create a ViewModel for our main activity. The ViewModel classes can cache data despite configuration changes.

  2. ViewModelProvider

The ViewModelProvider is the class to use to instantiate our ViewModel class e.g:

        this.mv = new ViewModelProvider(this).get(MainViewModel.class);

Room

We will also have Room, a data abstraction layer on top of SQlite database. We are creating an offline first android app. Data will not only be stored in the server in mysql database but also locally and permanently in sqlite database. We use Room which makes working with SQLite database easier and is the modern and recommended way to work with sqlite database. Room also has official integrations with RxJava2.

@Database(entities = {Tale.class}, version = 1, exportSchema = false)
public abstract class MyRoomDB extends RoomDatabase {
    //...
}
@Entity(tableName = "TalesTB")
public class Tale implements Serializable {
    //...
}

The Room compiler will give us compile-time checks to any bit of sql statement we write. Mostly with room we don’t write sql statements but of course we will have stuff like the following:

    @Query("SELECT * FROM TalesTB WHERE title LIKE :searchTerm")
    Single<List<Tale>> search(String searchTerm);

Now Room comppiler is able to ensure that the above query is correct.

RxJava2

One of the main purposes if this class is to teach RxJava2. RxJava2 is a library built for the general JVM, so we not only need it but also the RxAndroid2. We are also installing the Room flavour to provide better workings with Room.

    implementation 'io.reactivex.rxjava2:rxjava:2.2.9'
    implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
    implementation 'androidx.room:room-rxjava2:2.2.5'

Retrofit2

To talk to our server we need a http client. That client will be able to make our http requests to the server and provide us a response. Probably the most popular thrid party http client for android is Retrofit. We will install it alongside a converter. The converter will be responsible for mapping our json data to a model class that we can work with easily in java. We will also install a Rxjava2-Retrofit adapter to allow retrofit to work smoothly with rxjava2:

    implementation 'com.squareup.retrofit2:retrofit:2.5.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
    implementation "com.squareup.retrofit2:adapter-rxjava2:2.5.0"

Nb/= Am using retrofit version 2.5.0 which is the second latest at the time of writing this article. The latest is retrofit 2.7.1 but this would force us to increment our minimum sdk version to API level 21 which we refuse to do. After all the 2.5.0 can do all we are interested in this project.

Material Dialogs

Dialogs are incredily important to us in this project because we are creating a single page app. We intentionally don’t want to create more than one activity or fragments. This allows students to focus on learning RxJava2,Retrofit and Room rather than writing tons of UI code. Dialogs will allow us to input and reading views which are hosted within the main activity. We choose lovely dialogs even though there are a couple of beautiful dialog libraries in github.

    implementation 'com.yarolegovich:lovely-dialog:1.1.0'

Easy Adapter

EasyAdapter is a library for recyclerview. It provides a much easier,boilerplate-free adapter and works smoothly with data binding. Let’s install it:

    implementation 'com.dc.easyadapter:easyadapter:2.0.3'

DoubleLift

This library will allow us create an expandable recyclerview easily. This is important to our app design as we want to show only tale titles and allow us to expand to view the tale content:

    implementation "com.github.skydoves:doublelift:1.0.2"

Clean Architecture

We will structure our app based on clean architecture principals. The end game is to provide better seperation of concerns thus improving testability as well as maintenance. To achieve that we will start by dividing the app into two layers:

  1. Domain Layer
  2. Infrastructure Layer

The domain layer will contain code that are at the core of the app and that are bound to be subject minimal changes. The infrastructure layer will contain code that will change frequently, basically implementation code.

Model View ViewModel

Not only will we use clean architecture guidelines to architect our app, but we will also use Model View ViewModel to design the app. We will have models basically our data and their manipulations, Views and ViewModels. Views are the UI parts of the app. The ViewModel will connect our models to the views and vice versa. This not only improves separation of concerns and its benefits but also allows our UI to respect the android system lifecycle.

Let’s now proceed and look at our indvidual classes and interfaces.

Our Model Classes

These will be under the domain layer. We have two model classes:

  1. Tale
  2. ResponseModel
  3. RequestCall

(a). Tale

Tale is to represent a single tale or story. That story will have properties like:

  1. Id – To identify the tale from other tales. The id will be autogenerated at mysql side.
  2. Title – Title of the tale.
  3. Content – The story.
@Entity(tableName = "TalesTB")
public class Tale 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.
     */
    @PrimaryKey(autoGenerate = false)
    @ColumnInfo(name = "id")
    @SerializedName("id")
    private int id;
    @ColumnInfo(name = "title")
    @SerializedName("title")
    private String title;
    @ColumnInfo(name = "content")
    @SerializedName("content")
    private String content;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    @Override
    public String toString() {
        return getTitle();
    }
}
//end

(b). ResponseModel

This model class will represent the response we receive from our server. We want to easily work with this response and it’s much easier to work with it as a java class than a blob of json. Thankfully Retrofit2 is responsible for the mapping from the json data to java class as long as we define appropriate types and keys:

public class ResponseModel {
    /**
     * Our ResponseModel attributes
     */
    @SerializedName("code")
    private String code = "-1";
    @SerializedName("message")
    private String message = "UNKNOWN MESSAGE";
    @SerializedName("tales")
    private List<Tale> tales = new ArrayList<>();
    @SerializedName("id")
    private String id = "-1";
    private String action = "";
    private int eventSource = -1;

    /**
     * Generate Getter and Setters
     */
    public List<Tale> getTales() {
        return tales;
    }
    public void setTales(List<Tale> tales) {
        this.tales = tales;
    }
    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getAction() {
        return action;
    }

    public void setAction(String action) {
        this.action = action;
    }

    public int getEventSource() {
        return eventSource;
    }

    public void setEventSource(int eventSource) {
        this.eventSource = eventSource;
    }
}

(c). RequestCall

We will also create a model class to represent a single request we make to our server. By abstracting the process into a class, we are able to encapsulate several process details into a single entity. For example the process state, process message and associated response model can also be placed under one roof:

public class RequestCall {
    private int status;
    private String message;
    private List<Tale> tales;
    private ResponseModel responseModel;

    public int getStatus() {
        return status;
    }
    public void setStatus(int status) {
        this.status = status;
    }
    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
    public List<Tale> getTales() {
        return tales;
    }
    public void setTales(List<Tale> tales) {
        this.tales = tales;
    }

    public ResponseModel getResponseModel() {
        return responseModel;
    }

    public void setResponseModel(ResponseModel responseModel) {
        this.responseModel = responseModel;
    }
}

API

We will define two interfaces to smoothen how we work with our API:

  1. ICallbacks
  2. RestAPI

(a). ICallbacks

This interface will define signatures for our callbacks. The callbacks will be raised whenever we make a request.

public class ICallbacks {
    /**
     * Our OnFinished Listener
     * Will be raised once we finish our request
     */
    public interface OnFinished {
        void onSuccess(ResponseModel responseModel);
        void onError(int source,String error);
    }

}

(b). RestAPI

This interface will contain methods to represent to communicate with our RestAPi endpoints. These methods will represent requests we make to the server. Mostly we will be returing RxJava2 Observables which can then be subscribed to receive a response from the server.

package info.camposha.cuckoos.domain.usecase;

/**
 * Let's define our imports
 */

import info.camposha.cuckoos.domain.entity.ResponseModel;
import io.reactivex.Observable;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.POST;

/**
 * Let's Create an interface
 */
public interface RestApi {
    /**
     * This method will allow us perform a HTTP GET request to the specified url
     * .The response will be a ResponseModel object.
     */
    @GET("index.php")
    Observable<ResponseModel> retrieve();

    /**
     * This method will allow us to search our data while paginating the search results. We
     * specify the search and pagination parameters as fields.
     */
    @FormUrlEncoded
    @POST("index.php")
    Observable<ResponseModel> search(@Field("action") String action,
                                     @Field("query") String query,
                                     @Field("start") String start,
                                     @Field("limit") String limit);

    /**
     * This method will allow us perform a HTTP POST request to the specified url.In the process
     * we will upload data to mysql and return a ResponseModel object
     * <p>
     * Multipart Denotes that the request body is multi-part. Parts should be declared as parameters
     * and annotated with @Part.
     */
    @FormUrlEncoded
    @POST("index.php")
    Observable<ResponseModel> write(@Field("action") String action,
                                    @Field("title") String title,
                                    @Field("content") String content);

    /**
     * This method will allow us update our mysql data by making a HTTP POST request.
     * After that
     * we will receive a ResponseModel model object
     */

    @FormUrlEncoded
    @POST("index.php")
    Observable<ResponseModel> update(@Field("action") String action,
                                             @Field("id") int id,
                                             @Field("title") String title,
                                             @Field("content") String content);

    /**
     * This method will allow us to remove or delete from database the row with the
     * specified
     * id.
     */
    @FormUrlEncoded
    @POST("index.php")
    Observable<ResponseModel> delete(@Field("action") String action, @Field("id") int id);
}
//end

The generic parameters of these methods is the ResponseModel class, which is the representatioon of our server response.

Our Common Infrastructure Classes

In this category we have only one class:

  1. Constants

(a). Constants

Constants, these are variables that don’t change throughout the application lifetime. We have a few of them and instead of rubbishing our activities with these fields, we will collect them in one class and access them easily wherever we need them.

public class Constants {

    //supply your ip address. Type ipconfig while connected to internet to get your
    //ip address in cmd. Read the readme file for more details.
    // For example below I have used my ip address
    //private  static  final String base_url = "http://192.168.43.91/php/stars/";
    //The below line points to our hosted online demo database
    public static final String BASE_URL = "https://camposha.info/php/tales/cuckoo/";
    //The following will work for majority of emulators like
    // android studio emulator, bluestacks etc
    //private  static  final String base_url = "http://10.0.2.2/php/stars/";
    //if you use genymotion emulator then use below
    //private  static  final String base_url = "http://10.0.3.2/php/stars/";
    //If you change the base_url don't forget to change the images url

    //If you are using phone and hosting your database locally then use
    //your ip address
    //private  static  final String base_url = "http://YOUR_IP_ADDRESS/php/stars/";

    //These will help identify the source of the event
    public static final int ROOM = 0;
    public static final int SERVER = 1;

    //These variables will be sent to the server to identify the operations
    //we are performing
    public static final String PUBLISH ="PUBLISH";
    public static final String UPDATE ="UPDATE";
    public static final String DELETE ="DELETE";
    public static final String SELECT_ALL ="SELECT_ALL";
    public static final String SELECT_WITH_PAGINATION ="SELECT_WITH_PAGINATION";
    public static final String SEARCH_WITH_PAGINATION ="SEARCH_WITH_PAGINATION";

}

Our Local Database

Our local database is Room. Room is an abstraction on top of SQLite database. The actual database in fact is actually SQLite. But Room makes it easier to work with SQLite database, saving us from writing tons of SQL statements. We will create two classes to allow us work with Room:

  1. MyRoomDB
  2. TalesDAO

(a). MyRoomDB

This class will represent our Room database.

In the MyRoomDB you will find code like this:

import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;

import info.camposha.cuckoos.domain.entity.Tale;

@Database(entities = {Tale.class}, version = 1, exportSchema = false)
public abstract class MyRoomDB extends RoomDatabase {

    private static MyRoomDB myRoomDB;

    public abstract TalesDAO talesDAO();

    /**
     *This factory method will instantiate for us our MyRoomDB class
     * @param c - A Context Object
     * @return
     */
    public static MyRoomDB getInstance(Context c) {
        if (myRoomDB == null) {
            myRoomDB = Room.databaseBuilder(c, MyRoomDB.class,
                    "MyRoomDB")
                    .fallbackToDestructiveMigration()
                    .build();
        }
        return myRoomDB;
    }
}
//end

The MyRoomDB you can see is abstract thus it can contain an abstract method. The class also extends the RoomDatabase. We have annotated it with the @Database attribute and provided our entities. This is like addding tables to a database. Our table to store tales will be created based on attributes and properties of Tale class.

We have provided our abstract talesDAO method which will return an instance of our TalesDAO. The getInstance() will return the MyRoomDB instance. You change the database name by providing a custom string as the third parameter in the databaseBuilder() method.

(b). TalesDAO

This is an interface which is our DAO interface. DAO stands for Data Access Object. We will define our CRUD methods here.

@Dao
public interface TalesDAO {

The following method will allow us insert a single tale into Room database:


    //NB= Methods annotated with @Insert can return either void, long, Long, long[],
    // Long[] or List<Long>.
    @Insert
    void insert(Tale tale);

The following method will allow us a list of tales into room database. If a conflict occurs, we will replace existing data:

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    void saveAll(List<Tale> tales);

The following method will allow us update a single tale in our Room database:

    //Update methods must either return void or return int (the number of updated rows).
    @Update
    void update(Tale tale);

The following method will return us an io.reactivex.Single object. Like an Observable, Single can be observed for emissions. However unlike an Observable, it can only emit one between success or error, it
doesn’t have an onComplete().
Below we are returning all tales stored in our sqlite database. The sql statements are specified using the @Query attribute.

    /**
     * Select all cuckoos and order them by dates
     * @return
     */
    @Query("SELECT * FROM TalesTB ORDER BY id")
    Single<List<Tale>> selectAll();

The following method will allow ys fetch our tales, order them by date and paginate them at the database level. We are returning a Single as well.

    /**
     * Select cuckoos,order them by dates and paginate them
     * @return
     */
    @Query("SELECT * FROM TalesTB ORDER BY id DESC LIMIT :limit OFFSET :start")
    Single<List<Tale>> selectAndPaginate(int limit,int start);

The following method will allow us search tales stored in our sqlite database. All we receive is a search term and we do return yet again a Single.

    /**
     * Search cuckoos based in a query
     * @return
     */
    @Query("SELECT * FROM TalesTB WHERE title LIKE :searchTerm")
    Single<List<Tale>> search(String searchTerm);

The following method will allow us delete a tale based on an id.

    //NB= Deletion methods must either return void or return int (the number of deleted
    // rows).
    @Query("delete from TalesTB WHERE id =:id")
    void delete(String id);

}

Our RestAPI

We will have an interfacke known as RestAPI. This interface will have abstract methods representing our HTTP requests. These methods will be returning Observables.

The method to retrieve all tales from database without pagination or filter:

    /**
     * This method will allow us perform a HTTP GET request to the specified url
     * .The response will be a ResponseModel object.
     */
    @GET("index.php")
    Observable<ResponseModel> retrieve();

The following method will allow us search tales from our mysql database while paginating the search results:

    /**
     * This method will allow us to search our data while paginating the search results. We
     * specify the search and pagination parameters as fields.
     */
    @FormUrlEncoded
    @POST("index.php")
    Observable<ResponseModel> search(@Field("action") String action,
                                     @Field("query") String query,
                                     @Field("start") String start,
                                     @Field("limit") String limit);

The following method will allow us publish our Tale online. The Tale gets stored in our mysql database:

    /**
     * This method will allow us perform a HTTP POST request to the specified url.In the process
     * we will upload data to mysql and return a ResponseModel object
     */
    @FormUrlEncoded
    @POST("index.php")
    Observable<ResponseModel> write(@Field("action") String action,
                                    @Field("title") String title,
                                    @Field("content") String content);

The following method will allow us update an already published Tale:

    /**
     * This method will allow us update our mysql data by making a HTTP POST request.
     * After that
     * we will receive a ResponseModel model object
     */

    @FormUrlEncoded
    @POST("index.php")
    Observable<ResponseModel> update(@Field("action") String action,
                                             @Field("id") int id,
                                             @Field("title") String title,
                                             @Field("content") String content);

The following method will allow us delete an already published Tale from our server:

    /**
     * This method will allow us to remove or delete from database the row with the
     * specified
     * id.
     */
    @FormUrlEncoded
    @POST("index.php")
    Observable<ResponseModel> delete(@Field("action") String action, @Field("id") int id);

Our Repositories.

Under the data/repositories folder, you will find two classes:

  1. LocalRepository – Logic for performimg CRUD operations against Room Database.
  2. RemoteRepository – Logic for performing CRUD operations against MySQL database in our server.

We use the following techologies in both classes:

  • Retrofit2
  • RxJava2

Our ViewModel

For us to use MVVM as our design pattern, we need a ViewModel class. This class should ideally be lifecycle conscious. The class is to expose our functionalities and data to the UI. Data will flow through this class from our UI to our Model and vice versa.

Here is our ViewModel class:

public class MainViewModel extends AndroidViewModel {
    private LocalRepository localRepository;
    private RemoteRepository remoteRepository;

    public MainViewModel(@NonNull Application application) {
        super(application);
        localRepository = new LocalRepository(application);
        remoteRepository = new RemoteRepository();
    }

    public void performServerRequest(Tale tale, String action, String start, String limit, ICallbacks.OnFinished onFinished) {
        remoteRepository.performRequest(tale, action, "", start, limit, onFinished);
    }

    public void performRoomCRUD(String action, int id, Tale tale, List<Tale> tales, ICallbacks.OnFinished onFinished) {
        localRepository.performCRUD(action,id,tale ,tales, onFinished);
    }

    public void performRoomFetch(String action, String query, int limit, int start, ICallbacks.OnFinished onFinished) {
        localRepository.performFetch(action, query, limit, start, onFinished);
    }

}

Conclusion

This is a project designed to teach you RxJava2, Retrofit2, Room, Clean Architecture and MVVM. It is designed to be as beginner friendly as possible yet high quality. You can use it as a template in creating your future projects. It gives you a blueprint to master these technologies and use them without having to read any manual. The download contains both Java and PHP code.