RxJava, with RxJava2 being the latest version, is an implementation of Reactive Programming. Reactive Programming is an abstraction on top of High-Level Imperative Programming, which itself is an abstraction on top of the underlying assembly and binary instructions. RxJava2’s main purpose is to allow us create asynchronous and event driven computer programs in an easier manner than it would otherwise be, especially across threads and over network. RxJava helps in solving concurrency and parallelism problems which traditionally are difficult to implement.

What is Termima?

Termima is the name of the app we are creating. It’s a beautiful single page app designed to teach the following concepts:

  1. RxJava2 Usage with Room
  2. Room CRUD with classic pagination and search.
  3. Clean Architecture
  4. Model View ViewModel(MVVM)
  5. Single-Page full app design.

The app will all users to keep a record of terms and definitions. We writers and sharers of information, especially non-native speakers have to keep widening our vocabularies repository. I created this app to help in doing that and am sharing it for you to create your offline apps based on it. Basically you can use it as a template to create your applications based on RxJava2 and Rooom.

Common Scenarios to Use Reactive Programming

The following are the situations where Reactive Programming is especially important:

  1. Situations involving Producer/Consumer pattern. For example when data or events are being pushed to an application by another application part that we have no control over. Examples include server events, reacting to hardware signals, reacting to sensor data etc.
  2. Situations requiring asynchrony e.g latency bound IO events say from network, disk etc. Asynchronous operations involve making a request, waiting, getting a response or error.
  3. Situations involving processing common user events like mouse click, typing etc.

What is an Observable? What is an Observer?

The Observable is the center of RxJava,it’s a representation of a stream of data or events. It can be used for both pushing and pulling events/data.

An Observer is the entity that subscribes to the observable to receive the stream.

What is a Subscription?

This is the connection between an Observable and Observer. To receive the stream of data or events that the Observable represents, an observer has to subscribe to that stream.

interface Observable<T> {
    Subscription subscribe(Observer s)
}

What happens after the Subscription?

After an Observer subscribes to the Observable, three events get pushed to it:

  1. Data via the onNext() function.
  2. Errors via the onError() function.
  3. Completion signal via the onCompleted() function.

Here is the definition of the Observer interface:

interface Observer<T> {
    void onNext(T t)
    void onError(Throwable t)
    void onCompleted()
}

onNext(),onError(),onCompleted()

Of these three onNext() gets called first. This event can be called any number of times. It might also never be called. The onError() and onCompleted() are terminal events. This means only one of them gets called. It it happens only once. Either of them(the terminal events) signals that the Observable stream is finished and no further events will be sent. However be aware that theoretically terminal events might never occur suppose the stream is infinite and doesn’t fail.

Let’s start our lessons.

Demo

Here is the demo of what we create:

Lesson 1 – Creating Our Project

Create android studio project.

Lesson 2 – Enabling Java8 and Data Binding

In this class we will enavle Java8 as well as data Binding. These two allow us to utilize latest tools that allow us massively reduce our boilerplate code. Java8 will give us the lambda expressions rather than the annonymous classes to use. Let’s look at a usage example in this project. Below is the method that will allow us perform our search/filter against our Room Database with the help of RxJava2. If we write with annonynous class this is what we have:

    @Override
    public void search(String searchTerm,ICallbacks.IFetchListener iFetchListener) {
        termsDAO.search("%"+searchTerm+"%").subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread()).subscribe(new Consumer<List<Term>>() {
            @Override
            public void accept(List<Term> terms) throws Exception {
                iFetchListener.onDataFetched(terms);
            }
        });
    }

Now we can replace that method with a Java8 lamda expression and this is what we have:

    @Override
    public void search(String searchTerm,ICallbacks.IFetchListener iFetchListener) {
        termsDAO.search("%"+searchTerm+"%").subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread()).subscribe(terms -> iFetchListener.onDataFetched(terms));
    }

Is it worth it for you? Yeah it seems worth it as we are writing about half or even less the amount of code we would have written.

Now to enable Java8 go to the app level build.gradle, inside the android{} closure and add the following compile options:

    compileOptions {
        targetCompatibility 1.8
        sourceCompatibility 1.8
    }

What about data binding? Well let’s see it’s usage. Normally you need to write code like this to access a widget from our layout and use it:

        RecyclerView rv = findViewBy(R.id.rv);
        rv.setAdapter(adapter).

With data binding you don’t explicitly reference the widget, you can simply access it like below:

        b.rv.setAdapter(adapter);

Now imagine if you have lots of UI widgets like edittexts that need to be referenced, data binding would definitely make it a piece of cake as it eliminates those findViewBydIds. However not just that, you can also use it to bind data directly on widgets.

To enable data binding, go below the compileOptions closure in our build.gradle and add the following:

    dataBinding {
        enabled = true
    }

Finally through data binding and a beautiful library called easyadapter, we can write our recyclerview code in as little as three lines of code:

        adapter = new EasyAdapter<Term, ModelGridBinding>(R.layout.model_grid) {
            @Override
            public void onBind(@NonNull ModelGridBinding mb, @NonNull Term t) {
                mb.contentTV.setText(t.getMeaning());
            }
        }

The above code is the only code you need to write for a full recycler adapter. You will never find an easier way.

Take Away

  1. Data Binding and Java8 allow us to write less code by eliminating boilerplates that we would otherwise need to write. Less code means easier readability as well as less chances of bugs.

Lesson 3 – Installing Dependencies

When creating a full android app these days, you will always need to add dependencies into your project. These are third party or otherwise code that solve a given problem in easier manner. Remember we said earlier that the less code you write the less chances of bugs. Also using other’s people’s code makes you more productive as you focus on the big picture rather than the minute implementation details.

Repositories are where these dependencies are hosted. By default android studio has two repositories registered:

    repositories {
        google()
        jcenter()
    }

We will add a third one as some libraries get hosted here:

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

Let’s start by installing androidx dependencies:

    //AndroidX
    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'
    //try to use material version 1.0.0 or a bug causes TextInputLayout to crash with newer versions
    implementation 'com.google.android.material:material:1.0.0'

We promised to use Model View ViewModel. Luckily a few years ago google introduced the architecture components that allow us to be more productive while at the same time building high quality, maintainable apps. Among the packages introduced then were lifecycle extension:

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

Through the above we will get the ViewModel class that allow our view models to be cached across lifecycle changes.

Another tool introduced with the architecture components was Room, an abstraction on top of SQLite database. In short it makes it easier to work with SQLite databases. It does it in a manner that clearly promotes writing high quality testable code. We also get access to compile time verification of any sql statements we write, something that is magical.

    // Room components
    implementation 'androidx.room:room-runtime:2.2.5'
    annotationProcessor 'androidx.room:room-compiler:2.2.5'

Then we have RxJava2, which we had introduced earlier. RxJava2 is not specific to android. To make it work better with android, we need RxAndroid. There are also adapters that make it work more intuitively with Room.

    //RxJava & RxAndroid
    implementation 'io.reactivex.rxjava2:rxjava:2.2.9'
    implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
    implementation 'androidx.room:room-rxjava2:2.2.5'

Because we are creating a single-page app, we will use dialogs to provide editing and inputs. We’ve chosen one of the best dialog libraries in LovelyDialogs:

    //Beautiful material dialogs
    implementation 'com.yarolegovich:lovely-dialog:1.1.0'

The app we are creating intends to be filtering down terms on a daily basis, based on the date the user selects. This reduces the number of items we request from the database which makes the app use less memory and quickly load our terms from the database. Remember that these are terms, so at the end of the day you may be having thousands of terms stored in your database. It wouldn’t be a good idea to be loading all these at once. Filtering as well as pagination makes it efficient. To filter we will use the horizontal date picker library, which is a favorite of mine:

    //Horizontal DatePicker
    implementation 'com.github.jhonnyx2012:horizontal-picker:1.0.6'

Earlier on while discussing data binding, I mentioned how we can create an adapter with as little as three lines of code. Well that happens only courtesy of the easy adapter library:

    //Adapter using data binding. Rids of much boilerplate code  >= API 16
    implementation 'com.dc.easyadapter:easyadapter:2.0.3'

You’ve seen those beautiful expandable items in our recyclerview. The trick is that we are using doublelift. It has the capability to give us views that can be in two states: collapsed or expanded. It knows how to hide or show it’s contents based on those state:

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

Lesson 4 – Designing our Layouts

Because as we said this is a single page app, we will have only three layouts:

No. Layout Role
1. activity_main.xml To be inflated into our main activity.
2. dialog_edit.xml To be inflated into the Edit Dialog layout
3. model.xml To be inflated into our recyclerview view items

(a). activity_main.xml

This layou will contain:

  1. RecyclerView for rendering our terms.
  2. HorizontalDatePicker – For picking dates
  3. TextViews – For showing app title and subtitle
  4. Floating Action Buttons – For initiating certian actions.

Here is the layout code:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorPrimaryDark">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:paddingLeft="@dimen/activity_horizontal_margin"
            android:paddingTop="@dimen/activity_vertical_margin"
            android:paddingRight="@dimen/activity_horizontal_margin"
            android:paddingBottom="@dimen/activity_vertical_margin">
            <TextView
                android:id="@+id/titleTxt"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_gravity="center_horizontal"
                android:text="Termima"
                android:textAlignment="center"
                android:textAppearance="@style/TextAppearance.AppCompat.Large"
                android:textColor="@color/white"
                android:layout_marginBottom="5dp"
                android:textStyle="bold" />
            <TextView
                android:id="@+id/subTitleTxt"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_gravity="center_horizontal"
                android:text="A Terms and Meanings App"
                android:textAlignment="center"
                android:textAppearance="@style/TextAppearance.AppCompat.Medium"
                android:textColor="@color/white"
                android:layout_marginBottom="5dp"
                android:textStyle="italic" />

            <androidx.appcompat.widget.SearchView
                android:id="@+id/searchViewTxt"
                android:visibility="gone"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:defaultQueryHint="Search" />
            <com.github.jhonnyx2012.horizontalpicker.HorizontalPicker
                android:id="@+id/datePicker"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />

            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/rv"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_marginBottom="70dp"
                android:background="@drawable/planner" />

        </LinearLayout>

        <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/closeBtn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|left"
            android:layout_margin="16dp"
            android:src="@android:drawable/ic_delete"
            android:tint="@color/white" />

        <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/switchBtn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|center"
            android:layout_margin="16dp"
            android:src="@drawable/up_bottom"
            android:tint="@color/white" />

        <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/addBtn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|right"
            android:layout_margin="16dp"
            android:src="@drawable/ic_add_24dp" />
    </androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

(b). dialog_edit.xml

This layout will be inflated into our dialog UI. This dialog will be used for creating a new term or editing/updating an existing one. We use material components inside this layout. This is an example of creation of custom materail dialog using the lovelydialog library. We can still use the functionalities provided by the library while we use our own custom layout.

Here is the code for this layout:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_gravity="center"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <androidx.cardview.widget.CardView
                android:layout_width="match_parent"
                android:layout_height="match_parent">

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:orientation="vertical">

                    <com.google.android.material.textfield.TextInputLayout
                        style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:layout_marginStart="4dp"
                        android:layout_marginLeft="4dp"
                        android:layout_marginTop="8dp"
                        android:layout_marginEnd="4dp"
                        android:layout_marginRight="4dp">

                        <EditText
                            android:id="@+id/termTxt"
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:hint="Enter Term"
                            android:padding="@dimen/text_padding"
                            android:text="" />
                    </com.google.android.material.textfield.TextInputLayout>

                    <com.google.android.material.textfield.TextInputLayout
                        style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:layout_marginStart="4dp"
                        android:layout_marginLeft="4dp"
                        android:layout_marginTop="8dp"
                        android:layout_marginEnd="4dp"
                        android:layout_marginBottom="4dp"
                        android:layout_marginRight="4dp">

                        <EditText
                            android:id="@+id/meaningTxt"
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:hint="Enter Meaning"
                            android:minLines="3"
                            android:padding="@dimen/text_padding"
                            android:text="" />
                    </com.google.android.material.textfield.TextInputLayout>

                </LinearLayout>

            </androidx.cardview.widget.CardView>

            <LinearLayout
                android:orientation="horizontal"
                android:layout_width="match_parent"
                android:layout_height="wrap_content">
                <Button
                    android:id="@+id/cancelBtn"
                    android:text="Cancel"
                    android:textColor="@color/white"
                    android:background="@color/colorPrimary"
                    android:layout_width="0dp"
                    android:layout_weight="0.45"
                    android:layout_height="wrap_content"/>
                <View
                    android:layout_width="0dp"
                    android:layout_weight="0.05"
                    android:layout_height="match_parent"/>
                <Button
                    android:id="@+id/saveBtn"
                    android:text="Save"
                    android:layout_width="0dp"
                    android:layout_weight="0.45"
                    android:textColor="@color/white"
                    android:background="@color/colorPrimary"
                    android:layout_height="wrap_content"/>
            </LinearLayout>

        </LinearLayout>
    </androidx.core.widget.NestedScrollView>

</androidx.coordinatorlayout.widget.CoordinatorLayout><!--end-->

(c). model_grid.xml

This layout will be inflated into our recyclerview items. These items are that expandable/collapsable items in our recyclerview. One of the widgets we use is a third party library called DoubleLift and it helps us achieve the expandability/collapsibility states.

Here is the layout:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.cardview.widget.CardView
        android:id="@+id/mCardView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="5dp"
        android:layout_marginLeft="10dp"
        android:layout_marginRight="10dp"
        app:cardCornerRadius="5dp"
        app:cardElevation="5dp">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:padding="10dp"
                android:background="@color/colorPrimary"
                android:orientation="horizontal">

                <TextView
                    android:id="@+id/headerTV"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center_horizontal"
                    android:layout_weight="0.8"
                    android:text="Lesson"
                    android:textStyle="bold"
                    android:textAppearance="@style/TextAppearance.AppCompat.Medium"
                    android:textColor="@color/white"/>

                <ImageButton
                    android:id="@+id/toggleBtn"
                    android:background="@color/colorPrimary"
                    android:layout_width="0dp"
                    android:layout_height="match_parent"
                    android:layout_weight="0.2"
                    android:src="@drawable/ic_keyboard_arrow_down_white_24dp" />
            </LinearLayout>

            <com.skydoves.doublelift.DoubleLiftLayout
                android:id="@+id/doubleLift"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@color/colorAccent"
                app:doubleLift_animation="bounce"
                app:doubleLift_cornerRadius="4dp"
                app:doubleLift_foldedHeight="0dp"
                app:doubleLift_foldedWidth="50dp"
                app:doubleLift_horizontalDuration="400"
                app:doubleLift_startOrientation="horizontal"
                app:doubleLift_verticalDuration="300">

                <LinearLayout
                    android:id="@+id/contentSection"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="vertical">

                    <TextView
                        android:id="@+id/contentTV"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:layout_gravity="center_horizontal"
                        android:background="@color/white"
                        android:ellipsize="end"
                        android:minLines="5"
                        android:padding="10dp"
                        android:text="Content"/>

                    <LinearLayout
                        android:layout_width="match_parent"
                        android:layout_height="40dp"
                        android:layout_gravity="bottom"
                        android:background="@color/colorAccent"
                        android:orientation="horizontal">

                        <ImageButton
                            android:id="@+id/editBtn"
                            android:layout_width="0dp"
                            android:layout_height="match_parent"
                            android:layout_weight="0.2"
                            android:background="@color/colorPrimary"
                            android:src="@drawable/ic_edit" />

                        <View
                            android:layout_width="2dp"
                            android:layout_height="match_parent"
                            android:background="@color/white" />

                        <ImageButton
                            android:id="@+id/deleteBtn"
                            android:layout_width="0dp"
                            android:layout_height="match_parent"
                            android:layout_weight="0.2"
                            android:background="@color/colorPrimary"
                            android:src="@drawable/m_delete" />
                    </LinearLayout>
                </LinearLayout>
            </com.skydoves.doublelift.DoubleLiftLayout>

        </LinearLayout>
    </androidx.cardview.widget.CardView>
</layout>

Lesson 5 – Clean Architecture

Our aim has been to build an app using the best industry standards. Google has recommended using clean architecture to construct an app. Through this architecture we can organize our project into layers that aim to be as independent of each other as possible.With Clean Architecture, the recommended base layers should be the following:

  1. Domain Layer – To contain our core business classes. Should be as independent as possible
  2. Infrastructure – Contains classes that are likely to change especially as we change frameworks,data sources etc.

(a). Domain Layer

This, as we said will contain our core business classes. Our model classes will be located right here. Moreover you can include abstract methods that define the actions that will happen against those models. The methods need to be abstract unless they work without needing any external dependencies. The important point is that classes here shouldn’t have external dependencies from another layer. This makes them portable across projects and subject to very little change across a project’s lifetime.

In this layer we have the following classes

  1. Entity -> Term.java
  2. Usecase -> ICallbacks.java

(a). Term.java

Our main business object. This is what our app is about.It’s simple yet it’s our most important class. When your through with the development phase, this class should change the least. If you want to use this project as a template, this is the first class you to change. This class defines what we mean by a Term in a programmatic sense. It’s properties as well as how to access those properties:

import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.PrimaryKey;

import java.io.Serializable;

/**
 * Our Term Class. We specify the table name in our entity attribute
 */
@Entity(tableName = "TermsTB")
public class Term implements Serializable {

    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    private int id;
    @ColumnInfo(name = "term")
    private String term;
    @ColumnInfo(name = "meaning")
    private String meaning;
    @ColumnInfo(name = "date")
    private String date;
    private boolean isExpanded = false;

    public int getId() {
        return id;
    }

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

    public String getTerm() {
        return term;
    }

    public void setTerm(String term) {
        this.term = term;
    }

    public String getMeaning() {
        return meaning;
    }

    public void setMeaning(String meaning) {
        this.meaning = meaning;
    }

    public String getDate() {
        return date;
    }

    public void setDate(String date) {
        this.date = date;
    }

    public boolean isExpanded() {
        return isExpanded;
    }

    public void setExpanded(boolean expanded) {
        isExpanded = expanded;
    }

}
//end

The decorations we’ve applied on our instance fields will assist the Room compiler in generating the sqlite database schema, the tables, columns,record etc.

(b). ICallbacks

Because we are working on our model class, basically. We have to define the methods that define what those works are. Basically methods that define the operations that need to be performed against our model class. Because we strive to achieve independence from external layers(infrastructure layer), we will make these methods abstract. Thus the methods won’t have a body. They will be defined in interfaces rather than classes. This makes the methods independent and can be used across different databases for example. They are not tied down to a single infrastructure.

Here are the interfaces:

import java.util.List;

import info.camposha.terms.domain.entity.Term;

public class ICallbacks {
    /**
     * Our CRUD Methods
     */
    public interface ICrud {
        void insert(final ISaveListener iSaveListener, final Term term);
        void update(final ISaveListener iSaveListener, final Term term);
        void delete(final ISaveListener iSaveListener, final Term term);
        void selectAll(final IFetchListener iFetchListener);
        void selectAndPaginate(int limit,int start,final IFetchListener iFetchListener);
        void search(String searchTerm,final IFetchListener iFetchListener);
        void selectByDate(String searchTerm,final IFetchListener iFetchListener);
    }
    /**
     * Our DataSave Listener
     * Will be raised once our data is saved.
     */
    public interface ISaveListener {
        void onSuccess(String message);
        void onError(String error);
    }

    /**
     * Our DataLoad Listener
     * Will be raised once our data is loaded. A List of loaded items will
     * passed to us
     */
    public interface IFetchListener {
        void onDataFetched(List<Term> terms);
    }
}

(2). Infrastructure Layer

This is the implementation specific layer. The classes here are likely to change based on the APIs,databases etct you are targeting. Still however, they are fairly independent as we are also using Model View ViewModel design pattern. Through MVVM we will split this layer into the following sub-layers:
1.data -> db -> MyRoom,TermsDAO
-> repository -> TermsRepository
2.view -> MainActivity
3.viewmodel -> MainViewModel

That’s MVVM. Model View ViewModel. Let’s look at these classes one by one.

(a). TermsDAO

DAO stands for Data Access Object. The instance of this interface will be our Data Access Object. We define operation operations to manipulate our SQLite data right here. We define our SQLite CRUD methods here. They are simple and self-explanatory:

package info.camposha.terms.infrastructure.data.db;

import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;

import java.util.List;

import info.camposha.terms.domain.entity.Term;
import io.reactivex.Flowable;

@Dao
public interface TermsDAO {
    //NB= Methods annotated with @Insert can return either void, long, Long, long[],
    // Long[] or List<Long>.
    @Insert
    void insert(Term term);
    //Update methods must either return void or return int (the number of updated rows).
    @Update
    void update(Term term);
    //Deletion methods must either return void or return int (the number of deleted rows).
    @Delete
    void delete(Term term);

    /**
     * Select all tasks and order them by dates
     * @return
     */
    @Query("SELECT * FROM TermsTB ORDER BY date")
    Flowable<List<Term>> selectAll();

    /**
     * Select all tasks and order them by dates
     * @return
     */
    @Query("SELECT * FROM TermsTB ORDER BY id DESC LIMIT :limit OFFSET :start")
    Flowable<List<Term>> selectAndPaginate(int limit,int start);

    @Query("SELECT * FROM TermsTB WHERE date LIKE :thisDate")
    Flowable<List<Term>> selectByDate(String thisDate);

    /**
     * Select all tasks and order them by dates
     * @return
     */
    @Query("SELECT * FROM TermsTB WHERE term LIKE :searchTerm")
    Flowable<List<Term>> search(String searchTerm);

    //NB= Deletion methods must either return void or return int (the number of deleted
    // rows).
    @Query("delete from TermsTB")
    void deleteAll();

}

For us insert,update and delete methods will return voids. However luckily through RxJava2, we will be able to know whether these methods completed successfuly, in which case we show a sucess message, or whether a failure occured.

The following method will select all terms from our room database and order them by dates:

    /**
     * Select all tasks and order them by dates
     * @return
     */
    @Query("SELECT * FROM TermsTB ORDER BY date")
    Flowable<List<Term>> selectAll();

The following method will select terms from room database, order them by dates while paginating those terms. Rather than selecting everything, we select our data progressively in chunks:

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

The following method will filter out terms from the room database based on a given date:

    /**
     * Select terms filtering them by dates
     * @param thisDate
     * @return
     */
    @Query("SELECT * FROM TermsTB WHERE date LIKE :thisDate")
    Flowable<List<Term>> selectByDate(String thisDate);

The following method will search terms in our room database based on a supplied query:

    /**
     * Search terms based in a query
     * @return
     */
    @Query("SELECT * FROM TermsTB WHERE term LIKE :searchTerm")
    Flowable<List<Term>> search(String searchTerm);

(b). MyRoomDB

This class is an abstract representation of our room database, which itself is an abstraction over the SQLite database. Therefore we need define the basic properties of that database:

  1. Database Name
  2. Database Tables
  3. Database Version

Here is how we define those using the @Database attribute:

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

Here is the full class:

import android.content.Context;

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

import info.camposha.terms.domain.entity.Term;

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

    private static MyRoomDB myRoomDB;

    public abstract TermsDAO termsDAO();

    /**
     *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

You can see we have a static factory method to return the instance of this class. That method will be used in our repository class for instantiating this class.

(c). TermsRepository

This is our repository class. We will write our logic for working on our Room database right here. This is where we mostly use RxJava2 features. This class is nothing without implementing ICallbacks.Icrud interface. It is in that interface where the methods are defined abstractly. This class now becomes responsible for providing implementations of those methods.

public class TermsRepository implements ICallbacks.ICrud {

You may wonder, why are we implementing the methods in the first place? Wouldn’t it not make any difference if we just created the methods without creating the interface in the first place? Well in a small project like this it certainly doesn’t make a difference. But imagine if we wanted to work with several database, like an API call, as well as Room. In that case we would have defined the actions that can be made on our model class in one place inside our interface. Then we would have several repositories implementing that same interface. Our domain layer would have no idea of the number of databases that are used. And it wouldn’t care. We would chop and change databases as we like but our model and actions that occur on that model remain un-changed. It’s in other words independent of the implementation.

Making the repository class implement that interface makes sure that the repository obeys actions that have been stipulated on the contact(interface), which itself is independent of implementations. Another repository class, targeting a different database would also obey the same contract. Both repository classes would be of that interface’s type. That’s what’s called Polymorphism, which is an object oriented pillar. For example, ArrayList and LinkedList take advantage of Polymorphism by implementing the List<T> interface.

Now in our constructor, we will initialize our TermsDAO:

    private TermsDAO termsDAO;

    /**
     * Let's receive a context that will help with the instantiation of our MyRoomDB
     * @param context
     */
    public TermsRepository(Context context) {
        MyRoomDB myRoomDB = MyRoomDB.getInstance(context);
        termsDAO = myRoomDB.termsDAO();
    }

Here is how insert into Room database using RxJava2:

    /**
     * Insert into Room database
     * @param dataCallback
     * @param term
     */
    @Override
    public void insert(final ICallbacks.ISaveListener dataCallback, final Term term) {
        Completable.fromAction(() -> termsDAO.insert(term)).observeOn(AndroidSchedulers.mainThread()).subscribeOn(Schedulers.io())
                .subscribe(new CompletableObserver() {
                    @Override
                    public void onSubscribe(Disposable d) {
                    }
                    @Override
                    public void onComplete() {
                        dataCallback.onSuccess("INSERT SUCCESSFUL");
                    }
                    @Override
                    public void onError(Throwable e) {
                        dataCallback.onError(e.getMessage());
                    }
                });
    }

Here is how update our room database using RxJava2:

    /**
     * Update our data
     * @param dataCallback
     * @param term
     */
    @Override
    public void update(final ICallbacks.ISaveListener dataCallback, final Term term) {
        Completable.fromAction(() -> termsDAO.update(term)).observeOn(AndroidSchedulers.mainThread()).subscribeOn(Schedulers.io())
                .subscribe(new CompletableObserver() {
                    @Override
                    public void onSubscribe(Disposable d) {
                    }
                    @Override
                    public void onComplete() {
                        dataCallback.onSuccess("UPDATE SUCCESSFUL");
                    }
                    @Override
                    public void onError(Throwable e) {
                        dataCallback.onError(e.getMessage());
                    }
                });

    }

Here is how delete from our Room database using RxJava2:

    /**
     * Delete from our Room database
     * @param dataCallback
     * @param term
     */
    @Override
    public void delete(final ICallbacks.ISaveListener dataCallback, final Term term) {
        Completable.fromAction(() -> termsDAO.delete(term)).observeOn(AndroidSchedulers.mainThread()).subscribeOn(Schedulers.io())
                .subscribe(new CompletableObserver() {
                    @Override
                    public void onSubscribe(Disposable d) {
                    }
                    @Override
                    public void onComplete() {
                        dataCallback.onSuccess("DELETE SUCCESSFUL");
                    }
                    @Override
                    public void onError(Throwable e) {
                        dataCallback.onError(e.getMessage());
                    }
                });
    }

Here is how we select all data from our room database using RxJava2:

    /**
     * Select or retrieve our data
     * @param dataCallback
     */
    @Override
    public void selectAll(final ICallbacks.IFetchListener dataCallback) {
        termsDAO.selectAll().subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread()).subscribe(terms -> dataCallback.onDataFetched(terms));
    }

Here is how select our room database data using RxJava2 while paginating:

    /**
     * This method will allow us to select while paginating data at the SQLite level
     * @param start - where pagination is starting
     * @param limit - number of rows to fetch
     * @param iFetchListener
     */
    @Override
    public void selectAndPaginate(int start,int limit,ICallbacks.IFetchListener iFetchListener) {
        termsDAO.selectAndPaginate(start,limit).subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread()).subscribe(terms -> iFetchListener.onDataFetched(terms));
    }

 

Here’s how search against our Room database based on a query, using RxJava2:

    /**
     * Search Terms based on a Query
     * @param searchTerm
     * @param iFetchListener
     */
    @Override
    public void search(String searchTerm,ICallbacks.IFetchListener iFetchListener) {
        termsDAO.search("%"+searchTerm+"%").subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread()).subscribe(terms -> iFetchListener.onDataFetched(terms));
    }

Here is how filter our room database data based on a date using RxJava2:

    /**
     * Select Terms by Date
     * @param date
     * @param iFetchListener
     */
    @SuppressLint("CheckResult")
    @Override
    public void selectByDate(String date, ICallbacks.IFetchListener iFetchListener) {
        termsDAO.selectByDate(date).subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread()).subscribe(terms -> iFetchListener.onDataFetched(terms));
    }

That’s it. That’s our repository class.

Lesson 6 : MainViewMdel – Our ViewModel Class

Even though we are using Clean Architecture, as we said earlier, we will use MVVM(Model View ViewModel) design pattern. An important cog in the MVVM machine is the ViewModel class. This class respects the lifecycle of android activities and fragments. Thus it’s perfect to use to expose data to the UI.

Here is how create a ViewModel class:

public class MainViewModel extends AndroidViewModel {

For example this class will hold these data will be important to us even when the user rotates the device:

    public String SELECTED_DATE = "";
    public boolean IS_DAILY_VIEW = true;

Here is the whole clas:

package info.camposha.terms.infrastructure.viewmodel;

import android.app.Application;

import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;

import info.camposha.terms.domain.entity.Term;
import info.camposha.terms.domain.usecase.ICallbacks;
import info.camposha.terms.infrastructure.data.repository.TermsRepository;

public class MainViewModel extends AndroidViewModel {
    private TermsRepository termsRepository;
    public String SELECTED_DATE = "";
    public boolean IS_DAILY_VIEW = true;

    public MainViewModel(@NonNull Application application) {
        super(application);
        termsRepository =new TermsRepository(application);
    }

    /**
     * This method will:
     * 1. Insert our lesson into our SQLite database
     */
    public void insert(ICallbacks.ISaveListener iSaveListener, Term term) {
        termsRepository.insert(iSaveListener, term);
    }
    /**
     * This method will:
     * 1. Update a Term
     * @param term
     */
    public void update(ICallbacks.ISaveListener iSaveListener, Term term) {
        termsRepository.update(iSaveListener, term);
    }
    /**
     * This method will:
     * 1. Delete a Term
     * @param term
     */
    public void delete(ICallbacks.ISaveListener iSaveListener, Term term) {
        termsRepository.delete(iSaveListener, term);
    }
    public void fetchAll(ICallbacks.IFetchListener iFetchListener){
        termsRepository.selectAll(iFetchListener);
    }

    public void selectAndPaginate(int start,int limit,ICallbacks.IFetchListener iFetchListener){
        termsRepository.selectAndPaginate(limit, start, iFetchListener);
    }
    public void search(String searchTerm,ICallbacks.IFetchListener iFetchListener){
        termsRepository.search(searchTerm, iFetchListener);
    }
    public void selectByDate(String date,ICallbacks.IFetchListener iFetchListener){
        termsRepository.selectByDate(date, iFetchListener);
    }
}

Lesson 7 – Our MainActivity

This is our only activity. We had already seen how to design it. We create it by extending the AppCompatActivity. We also implement a couple of interfaces:

public class MainActivity extends AppCompatActivity implements DatePickerListener,
        ICallbacks.ISaveListener, ICallbacks.IFetchListener {

These will be our instance fields:

    private ActivityMainBinding b;
    private EasyAdapter<Term, ModelGridBinding> adapter;
    private MainViewModel mv;
    private EditText termTxt, meaningTxt;
    private boolean reachedEnd = false;
    private boolean isScrolling;
    private int ITEMS_PER_PAGE = 10;
    private boolean isAfterUpdate = false;

We said we are using Data binding. mv will be our MainViewModel instance. You can see we’ve defined several pagination related fields.

Our init method will be responsible for initializing several stuff needed when activity is created, including our MainViewModel:

    private void init() {
        this.setupDatePicker();
        this.setupAdapter();
        this.handleEvents();
        this.listenToRecyclerViewScroll();
        this.mv = new ViewModelProvider(this).get(MainViewModel.class);
    }

This is the method needed to setup our horizontal datepicker:

    /**
     * This method will:
     * 1. Load our material colors into an array
     * 2. Setup our horizontal datepicker
     */
    private void setupDatePicker() {
        b.datePicker.setListener(this)
                .setDays(360)
                .setOffset(7)
                .setDateSelectedColor(Color.DKGRAY)
                .setDateSelectedTextColor(Color.WHITE)
                .setMonthAndYearTextColor(Color.DKGRAY)
                .setTodayButtonTextColor(getResources().getColor(R.color.colorPrimary))
                .setTodayDateTextColor(getResources().getColor(R.color.colorPrimary))
                .setTodayDateBackgroundColor(Color.GRAY)
                .setUnselectedDayTextColor(Color.DKGRAY)
                .setDayOfWeekTextColor(Color.DKGRAY)
                .setUnselectedDayTextColor(getResources().getColor(R.color.primaryTextColor))
                .showTodayButton(true)
                .init();
        b.datePicker.setBackgroundColor(Color.LTGRAY);
        b.datePicker.setDate(new DateTime());
    }

And this is the method needed to setup our adapter using easyadapter:

    /**
     * This method will:
     * 1. setup our adapter for us
     * 2.Setup our recyclerview
     */
    private void setupAdapter() {
        adapter = new EasyAdapter<Term, ModelGridBinding>(R.layout.model_grid) {
            @Override
            public void onBind(@NonNull ModelGridBinding mb, @NonNull Term t) {
                mb.headerTV.setText(String.format(Locale.getDefault(),"%d. %s", getData().indexOf(t) + 1, t.getTerm()));
                mb.contentTV.setText(t.getMeaning());

                mb.doubleLift.collapse();

                mb.headerTV.setOnClickListener(view -> {
                    mb.toggleBtn.performClick();
                });
                mb.toggleBtn.setOnClickListener(view -> {
                    if (mb.doubleLift.isExpanded()) {
                        mb.toggleBtn.setImageResource(R.drawable.ic_keyboard_arrow_down_white_24dp);
                        mb.doubleLift.collapse();
                    } else {
                        mb.toggleBtn.setImageResource(R.drawable.ic_keyboard_arrow_up_white_24dp);
                        mb.doubleLift.expand();
                    }

                });
                mb.editBtn.setOnClickListener(v -> createCustomDialog(true, t));
                mb.deleteBtn.setOnClickListener(v -> mv.delete(MainActivity.this, t));
            }

        };
        GridLayoutManager glm = new GridLayoutManager(this, 1);
        b.rv.setLayoutManager(glm);
        b.rv.setAdapter(adapter);

    }

This is the method needed to create our custom material lovely dialog with edittexts:


    /**
     * This method will:
     * 1. Create our input dialog. We can use this dialog for either update or delete
     *
     * @param forEdit
     */
    private void createCustomDialog(Boolean forEdit, Term t) {
        Term term = forEdit ? t : new Term();
        LovelyCustomDialog d = new LovelyCustomDialog(this);
        d.setTopColorRes(R.color.colorPrimary)
                .setView(R.layout.dialog_edit)
                .setTitle("Terms Editor")
                .setTitleGravity(Gravity.CENTER_HORIZONTAL)
                .setMessage("Enter term and meaning and click save")
                .setMessageGravity(Gravity.CENTER_HORIZONTAL)
                .setIcon(R.drawable.flip_page)
                .setCancelable(false)
                .configureView(rootView -> {
                    termTxt = rootView.findViewById(R.id.termTxt);
                    meaningTxt = rootView.findViewById(R.id.meaningTxt);
                    Button saveBtn = rootView.findViewById(R.id.saveBtn);

                    saveBtn.setText(forEdit ? "UPDATE" : "ADD");
                    termTxt.setText(forEdit ? term.getTerm() : "");
                    meaningTxt.setText(forEdit ? term.getMeaning() : "");

                    saveBtn.setOnClickListener(view -> {
                        term.setTerm(termTxt.getText().toString());
                        term.setMeaning(meaningTxt.getText().toString());
                        term.setDate(mv.SELECTED_DATE);
                        if (forEdit) {
                            mv.update(MainActivity.this, term);
                            d.dismiss();
                        } else {
                            mv.insert(MainActivity.this, term);
                        }
                    });
                })
                .setListener(R.id.cancelBtn, view -> d.dismiss())
                .show();
    }

Here is what happens when user selects a date via the datepicker:

    /**
     * When our datepicker is selected, we obtain the selected date and
     * filter our data
     *
     * @param dateSelected
     */
    @Override
    public void onDateSelected(DateTime dateSelected) {
        adapter.clear(true);
        String year = String.valueOf(dateSelected.getYear());
        String month = String.valueOf(dateSelected.getMonthOfYear());
        String day = String.valueOf(dateSelected.getDayOfMonth());
        if (dateSelected.getMonthOfYear() < 10) {
            mv.SELECTED_DATE = year + "-0" + month + "-" + day;
        } else {
            mv.SELECTED_DATE = year + "-" + month + "-" + day;
        }
        mv.IS_DAILY_VIEW = true;
        mv.selectByDate(mv.SELECTED_DATE, this);
    }

Here is what we do when data is successfully saved,updated or deleted:


    /**
     * When our data is saved
     *
     * @param message - success message
     */
    @Override
    public void onSuccess(String message) {
        isAfterUpdate = true;
        if (termTxt != null && meaningTxt != null) {
            termTxt.setText("");
            meaningTxt.setText("");
        }
        show(message);
        adapter.notifyDataSetChanged();
    }

Here is what we do when an error occurs:

    /**
     * When an error occurs
     *
     * @param error
     */
    @Override
    public void onError(String error) {
        show(error);
    }

Here is what we do when data is successfully fetched, be it for the first time or via pagination:

    /**
     * When our data is loaded
     *
     * @param terms
     */
    @Override
    public void onDataFetched(List<Term> terms) {
        if(!isAfterUpdate){
            adapter.addAll(terms, true);
        }else{
            adapter.clear(true);
        }
        isAfterUpdate=false;

        adapter.notifyDataSetChanged();

        if (!mv.IS_DAILY_VIEW) {
            if (terms.size() > 0) {
                reachedEnd = false;
            } else {
                reachedEnd = true;
                show("No More data. Reached End");
            }
        }
    }

Here is how we listen to recyclerview scroll events, which is important as we are using the endless scroll pagination technique:

    /**
     * We will listen to scroll events. This is important as we are implementing scroll to
     * load more data pagination technique
     */
    private void listenToRecyclerViewScroll() {
        b.rv.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView rv, int newState) {
                //when scrolling starts
                super.onScrollStateChanged(rv, newState);
                //check for scroll state
                if (newState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
                    isScrolling = true;
                }
            }

            @Override
            public void onScrolled(RecyclerView rv, int dx, int dy) {
                // When the scrolling has stopped
                super.onScrolled(rv, dx, dy);

                //getChildCount() returns the current number of child views attached to the
                // parent RecyclerView.
                int current = b.rv.getLayoutManager().getChildCount();
                //getItemCount() returns the number of items in the adapter bound to the
                // parent RecyclerView.
                int total = b.rv.getLayoutManager().getItemCount();
                //findFirstVisibleItemPosition() returns the adapter position of the first
                // visible view.
                int scrolledOut = ((LinearLayoutManager) b.rv.getLayoutManager()).findFirstVisibleItemPosition();

                if (isScrolling && (current + scrolledOut == total)) {
                    isScrolling = false;

                    if (dy > 0) {
                        // Scrolling up
                        if (!reachedEnd && !mv.IS_DAILY_VIEW) {
                            mv.selectAndPaginate(b.rv.getLayoutManager().getItemCount(), ITEMS_PER_PAGE, MainActivity.this);
                        }
                    } else {
                        // Scrolling down
                    }
                }
            }
        });
    }

Conclusion

We’ve create a full app based on RxJava2 and Room. This is an app that you can customize and upload to Google Play. We’ve learned alot about RxJava2, Clean Architecture, Room CRUD, Pagination and search. We’ve used minimal design by using just a single activity but with dialogs.

Download the app below. Cheers.