What we will be Learning?

Architecture an Android app to support Offline caching of data using RxJava, SQLite and ContentProvider

Use Repository Architecture to decouple Local and Remote Data Store

Use dagger 2 to provide dependency

Use MVP design pattern to architecture app in a clean way and decouple business logic

Our Local Datastore will be maintained using SQlite and we will be using Retrofit and OkHttp and Gson to request data from remote RESTful API service

A content provider will be used to fetch data from SQLite database

On top of ContentProvider we will be using StorIO to add Reactivity to our Database

Use RxJava and its awesome operators to observe changes in our data store and update the UI.

Step 1: Create a project with a Blank Activity template

Step 2: Add the necessary dependency

We will be using Retrofit, OkHttp, Gson, RxJava, RxAndroid Dagger 2 and StorIO to your apps build.gradle file

//Retrofit compile 'com.squareup.retrofit2:retrofit:2.0.2' //OkHttp compile 'com.squareup.okhttp3:okhttp:3.2.0' compile 'com.squareup.okio:okio:1.7.0' //Gson compile 'com.google.code.gson:gson:2.6.2' compile 'com.squareup.retrofit2:converter-gson:2.0.1' //RxJava compile 'io.reactivex:rxjava:1.1.2' compile 'io.reactivex:rxandroid:1.1.0' compile 'com.squareup.retrofit2:adapter-rxjava:2.0.1' //StorIO compile "com.pushtorefresh.storio:sqlite:1.9.0" compile "com.pushtorefresh.storio:content-resolver:1.9.0" compile "com.pushtorefresh.storio:sqlite-annotations:1.9.0" compile "com.pushtorefresh.storio:content-resolver-annotations:1.9.0" apt "com.pushtorefresh.storio:sqlite-annotations-processor:1.9.0" apt "com.pushtorefresh.storio:content-resolver-annotations-processor:1.9.0"

Android Studio by default will not recognize a lot of generated Dagger 2 code as legitimate classes, but adding the android-apt plugin will add these files into the IDE class path and enable you to have more visibility. Add this line to your root build.gradle :

dependencies { // other classpath definitions here classpath ' com.neenbedankt.gradle.plugins:android-apt:1.8 ' }

Then make sure to apply the plugin in your app/build.gradle :

// add after applying plugin: 'com.android.application' apply plugin : ' com.neenbedankt.android-apt '

Add these three lines to your app/build.gradle file after this apply statement:

dependencies { // apt command comes from the android-apt plugin apt ' com.google.dagger:dagger-compiler:2.2 ' compile ' com.google.dagger:dagger:2.2 ' provided ' javax.annotation:jsr250-api:1.0 ' }

In dependencies I added:

dagger library

library dagger-compiler for code generation

for code generation javax.annotation for additional annotations required outside Dagger

After updating Dagger’s configuration, you can synchronize the project with the gradle files by clicking the button at the top.

Also add INTERNET permission to your manifest

<uses-permission android:name="android.permission.INTERNET" />

Step 3: Create packages to architecture our app in a clean way

At top level com.ladwa.aditya.offlinefirstapp we have App , BasePresenter , BaseView files

we have , , files Also, I have 3 packages dagger , data , and mainscreen

, , and Inside mainscreen I have MainscreenContract , Presenter and Activity class

, and class Inside dager package I have component and module as packages that have the respective module and component files

package I have and as packages that have the respective module and component files Our data package is more complex at first glance however if we take a close look at and scrutinize it become self-explanatory.

Inside data package, we have an interface AppDataStore that has abstract methods of our Repository.

that has abstract methods of our Repository. Further, I have defined two more packages local and remote that have classes AppLocalDataStore and AppRemoteDataStore that implement methods from AppDataStore interface

and that implement methods from interface Inside local package there is another package models that holds all the POJO classes.

that holds all the POJO classes. There is another file in data AppRepository that implements methods from AppDataStore interface. This class will be used by the Presenter to make request to data and this is the essence of Repository Architecture

This is how my app structure looks

Step 4: Create BasePresenter and BaseView

BaseView will be inherited by every Activity or Fragment of our app

BasePresenter will be inherited by every Presenter of our app

public interface BasePresenter { void subscribe(); void unsubscribe(); }

public interface BaseView { void setPresenter(T presenter); }

Note that the BaseView takes a Presenter of a Generic type and has a method setPresenter that will be called in the View that implements it.

Step 5: In mainscreen package create an interface called MainScreenContract

This interface will have two more inner interface called View and Presenter

called and View interface holds all the methods which we will implement in our MainScreen View (i.e in our case MainActivity )

(i.e in our case ) Presenter interface has all the methods that we will implement in our MainScreenPresenter

public class MainScreenContract { interface View extends BaseView { void showPosts(List posts); void showError(String message); void showComplete(); } interface Presenter extends BasePresenter { void loadPost(); void loadPostFromRemoteDatatore(); } }

Step 6: Define the methods that our Repository provides

public interface AppDataStore { Observable getPost(); }

We have only one method getPost that returns an Observable of List of Post

that returns an Observable of List of Post This interface will be implemented by AppLocalDataStore, AppRemoteDataStore, and AppRepository

Step 7: Define the Database Contract

We have a table “post” that has 4 columns ID, USER_ID, TITLE and BODY

This is the JSON response we will be receiving

public class DatabaseContract { public static final String CONTENT_AUTHORITY = "com.ladwa.aditya.offlinefirstapp"; private static final String CONTENT_SCHEME = "content://"; public static final Uri BASE_CONTENT_URI = Uri.parse(CONTENT_SCHEME + CONTENT_AUTHORITY); public static final String PATH_POST = "post"; public DatabaseContract() { } public static abstract class Post implements BaseColumns { @NonNull public static final String CONTENT_URI_STRING = "content://" + CONTENT_AUTHORITY + "/" + PATH_POST; public static final Uri CONTENT_URI = Uri.parse(CONTENT_URI_STRING); public static final String CONTENT_USER_TYPE = "vnd.android.cursor.dir/" + CONTENT_AUTHORITY + "/" + PATH_POST; public static final String CONTENT_USER_ITEM_TYPE = "vnd.android.cursor.item/" + CONTENT_AUTHORITY + "/" + PATH_POST; public static final String TABLE_NAME = "post"; public static final String COLUMN_ID = "id"; public static final String COLUMN_USER_ID = "user_id"; public static final String COLUMN_TITLE = "title"; public static final String COLUMN_BODY = "body"; public static String getPostCreateQuery() { return "CREATE TABLE " + TABLE_NAME + " (" + COLUMN_ID + " LONG NOT NULL PRIMARY KEY, " + COLUMN_USER_ID + " LONG , " + COLUMN_TITLE + " TEXT NOT NULL, " + COLUMN_BODY + " TEXT NOT NULL" + ");"; } public static String getUserDeleteQuery() { return "DROP TABLE IF EXISTS " + TABLE_NAME; } public static Uri buildUserUri(long id) { return ContentUris.withAppendedId(CONTENT_URI, id); } } }

Step 8: Create a Database helper class

This class will be used to initialize the database in our Content Provider

public class DatabaseHelper extends SQLiteOpenHelper { public static final String DATABASE_NAME = "OfflineFirstApp.db"; public static final int DATABASE_VERSION = 1; public DatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase sqLiteDatabase) { sqLiteDatabase.execSQL(DatabaseContract.Post.getPostCreateQuery()); } @Override public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) { sqLiteDatabase.execSQL(DatabaseContract.Post.getUserDeleteQuery()); onCreate(sqLiteDatabase); } }

Step 9: Create the ContentProvider of the app

This may seem a little intimidating to grasp but ContentProviders are awesome once you master them

We have two URI matcher POST_ITEM and POST_DIR

Note : Dont forget to declare the ContentProvider in the AppMainfest file

<provider android:name=".data.local.Provider" android:authorities="com.ladwa.aditya.offlinefirstapp" android:exported="false" android:syncable="true" />

public class Provider extends ContentProvider { private static final int POST_ITEM = 100; private static final int POST_DIR = 101; private static final UriMatcher sUriMatcher = buildUriMatcher(); private DatabaseHelper mDbHelper; private static UriMatcher buildUriMatcher() { final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); final String authority = DatabaseContract.CONTENT_AUTHORITY; matcher.addURI(authority, DatabaseContract.PATH_POST + "/#", POST_ITEM); matcher.addURI(authority, DatabaseContract.PATH_POST, POST_DIR); return matcher; } @Override public boolean onCreate() { mDbHelper = new DatabaseHelper(getContext()); return true; } @Nullable @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { Cursor retCursor; switch (sUriMatcher.match(uri)) { case POST_ITEM: retCursor = mDbHelper.getReadableDatabase().query( DatabaseContract.Post.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder ); break; case POST_DIR: retCursor = mDbHelper.getReadableDatabase().query( DatabaseContract.Post.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder ); break; default: throw new UnsupportedOperationException("Unknown Uri " + uri); } retCursor.setNotificationUri(getContext().getContentResolver(), uri); return retCursor; } @Nullable @Override public String getType(Uri uri) { final int match = sUriMatcher.match(uri); switch (match) { //Case for user case POST_ITEM: return DatabaseContract.Post.CONTENT_USER_ITEM_TYPE; case POST_DIR: return DatabaseContract.Post.CONTENT_USER_TYPE; default: throw new UnsupportedOperationException("Unknown URI " + uri); } } @Nullable @Override public Uri insert(Uri uri, ContentValues contentValues) { final SQLiteDatabase db = mDbHelper.getWritableDatabase(); Uri returnUri; switch (sUriMatcher.match(uri)) { //Case for Post case POST_DIR: long _id = db.insert(DatabaseContract.Post.TABLE_NAME, null, contentValues); if (_id > 0) returnUri = DatabaseContract.Post.buildUserUri(_id); else throw new SQLException("Failed to insert row " + uri); break; default: throw new UnsupportedOperationException("Unknown URI " + uri); } getContext().getContentResolver().notifyChange(uri, null); return returnUri; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { final SQLiteDatabase db = mDbHelper.getWritableDatabase(); int rowsDeleted; switch (sUriMatcher.match(uri)) { case POST_DIR: rowsDeleted = db.delete(DatabaseContract.Post.TABLE_NAME, selection, selectionArgs); break; default: throw new UnsupportedOperationException("Unknown URI " + uri); } if (selection == null || 0 != rowsDeleted) getContext().getContentResolver().notifyChange(uri, null); return rowsDeleted; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { final SQLiteDatabase db = mDbHelper.getWritableDatabase(); int update; switch (sUriMatcher.match(uri)) { //Case for User case POST_DIR: update = db.update(DatabaseContract.Post.TABLE_NAME, values, selection, selectionArgs); break; default: throw new UnsupportedOperationException("Unknown URI " + uri); } if (update > 0) getContext().getContentResolver().notifyChange(uri, null); return update; } }

Step 10: Create the POJO class for Post

Notice the Annotations that I’m using provided by StorIO library

These annotations are used to create Get, Put and Delete resolvers to Content Provider that. Refer StorIO documentation for more info

@StorIOSQLiteType(table = DatabaseContract.Post.TABLE_NAME) @StorIOContentResolverType(uri = DatabaseContract.Post.CONTENT_URI_STRING) public class Post { @StorIOSQLiteColumn(name = DatabaseContract.Post.COLUMN_ID, key = true) @StorIOContentResolverColumn(name = DatabaseContract.Post.COLUMN_ID, key = true) public Integer id; @StorIOSQLiteColumn(name = DatabaseContract.Post.COLUMN_USER_ID) @StorIOContentResolverColumn(name = DatabaseContract.Post.COLUMN_USER_ID) public Integer userId; @StorIOSQLiteColumn(name = DatabaseContract.Post.COLUMN_TITLE) @StorIOContentResolverColumn(name = DatabaseContract.Post.COLUMN_TITLE) public String title; @StorIOSQLiteColumn(name = DatabaseContract.Post.COLUMN_BODY) @StorIOContentResolverColumn(name = DatabaseContract.Post.COLUMN_BODY) public String body; public Post(Integer id, Integer userId, String title, String body) { this.id = id; this.userId = userId; this.title = title; this.body = body; } public Post() { } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public Integer getUserId() { return userId; } public void setUserId(Integer userId) { this.userId = userId; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getBody() { return body; } public void setBody(String body) { this.body = body; } }

Step 11:Use dagger 2 to provide dependency

AppModule provides context of the Application

DatModule provides depencency such as Retrofit, LocalRepository, RemoteRepository

Component

@Module public class AppModule { Application mApplication; public AppModule(Application mApplication) { this.mApplication = mApplication; } @Provides @Singleton Application provideApplication() { return mApplication; } }

@Module public class DataModule { String mBaseUrl; public DataModule(String mBaseUrl) { this.mBaseUrl = mBaseUrl; } @Provides @Singleton SharedPreferences providesSharedPreferences(Application application) { return PreferenceManager.getDefaultSharedPreferences(application); } @Provides @Singleton Cache provideHttpCache(Application application) { int cacheSize = 10 * 1024 * 1024; Cache cache = new Cache(application.getCacheDir(), cacheSize); return cache; } @Provides @Singleton Gson provideGson() { GsonBuilder gsonBuilder = new GsonBuilder(); gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES); return gsonBuilder.create(); } @Provides @Singleton OkHttpClient provideOkhttpClient(Cache cache) { OkHttpClient.Builder client = new OkHttpClient.Builder(); client.cache(cache); return client.build(); } @Provides @Singleton Retrofit provideRetrofit(Gson gson, OkHttpClient okHttpClient) { Retrofit retrofit = new Retrofit.Builder() .addConverterFactory(GsonConverterFactory.create(gson)) .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .baseUrl(mBaseUrl) .client(okHttpClient) .build(); return retrofit; } @Provides @Singleton AppLocalDataStore porvidesAppLocalDataStore(Application context) { return new AppLocalDataStore(context); } @Provides @Singleton AppRemoteDataStore providesRepository() { return new AppRemoteDataStore(); } }

The methods will be injected in MainActivity and RemoteDatastore

@Singleton @Component(modules = {AppModule.class, DataModule.class}) public interface AppComponent { void inject(MainActivity activity); void inject(AppRemoteDataStore appRemoteDataStore); }

Step 12: Implement AppDataStore in AppLocalDataStore class

We use @Inject in the Constructor so that dagger provides the context

We have a member variable of type StorIOContentResolver

We add the Type maping for Post POJO class and the Put, Get, Delete resolvers that were generated by StorIO

Note : Rebuild your project to generate these class

public class AppLocalDataStore implements AppDataStore { private StorIOContentResolver mStorIOContentResolver; @Inject public AppLocalDataStore(@NonNull Context context) { mStorIOContentResolver = DefaultStorIOContentResolver.builder() .contentResolver(context.getContentResolver()) .addTypeMapping(Post.class, ContentResolverTypeMapping.builder() .putResolver(new PostStorIOContentResolverPutResolver()) .getResolver(new PostStorIOContentResolverGetResolver()) .deleteResolver(new PostStorIOContentResolverDeleteResolver()) .build() ).build(); } @Override public Observable getPost() { Log.d("LOCAL","Loaded from local"); return mStorIOContentResolver.get() .listOfObjects(Post.class) .withQuery(Query.builder().uri(DatabaseContract.Post.CONTENT_URI).build()) .prepare() .asRxObservable(); } public void savePostToDatabase(List posts) { mStorIOContentResolver.put().objects(posts).prepare().executeAsBlocking(); } }

Step 13: Implement AppDataStore in AppRemoteDataStore class

We have 2 member variables injected

In getPost() we make a request to our remote RESTful API and when we get the result we use RxJava’s doOnNext() operator to save the posts in the database

public class AppRemoteDataStore implements AppDataStore { @Inject Retrofit retrofit; @Inject AppLocalDataStore appLocalDataStore; public AppRemoteDataStore() { App.getAppComponent().inject(this); } @Override public Observable getPost() { Log.d("REMOTE","Loaded from remote"); return retrofit.create(PostService.class).getPostList().doOnNext(new Action1() { @Override public void call(List posts) { appLocalDataStore.savePostToDatabase(posts); } }); } private interface PostService { @GET("/posts") Observable getPostList(); } }

Step 14: Implement AppDataStore in AppRepository class

We use @Inject so that dagger provides the Local and Remote repository

In getPost() we use RxJava’s concat operator to concat local and remote repository

The operator first() will return the observable from the repository that has posts

So if we have posts in local database they will be returned first. If not a GET request will be made to the Remote RESTful data service

public class AppRepository implements AppDataStore { private AppLocalDataStore mAppLocalDataStore; private AppRemoteDataStore mAppRemoteDataStore; @Inject public AppRepository(AppLocalDataStore mAppLocalDataStore, AppRemoteDataStore mAppRemoteDataStore) { this.mAppLocalDataStore = mAppLocalDataStore; this.mAppRemoteDataStore = mAppRemoteDataStore; } @Override public Observable getPost() { return Observable.concat(mAppLocalDataStore.getPost(), mAppRemoteDataStore.getPost()) .first(new Func1<List, Boolean>() { @Override public Boolean call(List posts) { return posts != null; } }); } }

Step 15: Create an App class that extends Application and add it to manifest

We create the Dagger component in onCreate()

getAppComponent() will return the AppComponent wherever we need variables to be injected

public class App extends Application { private static AppComponent mAppComponent; @Override public void onCreate() { super.onCreate(); mAppComponent = DaggerAppComponent.builder() .appModule(new AppModule(this)) .dataModule(new DataModule("http://jsonplaceholder.typicode.com/")) .build(); } public static AppComponent getAppComponent() { return mAppComponent; } }

Add this class name in the tag of AppMainfest

<application android:allowBackup="true" android:name=".App" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme">

Step 16: Implement the Presenter

The presenter calls loadPost() immediately when the view subscribes to the Presenter

loadPostFromRemote() is called when the user whats the date to be explicitly loaded from the remote REST API, which automaticlly updates the data in our database also

in unsubscrive() the Subscriptions are unsubscribed to avoid memory leak

public class MainScreenPresenter implements MainScreenContract.Presenter { private static final String TAG = MainScreenPresenter.class.getSimpleName(); private Subscription mSubscription; private AppRepository mAppRepository; private MainScreenContract.View mView; public MainScreenPresenter(AppRepository mAppRepository, MainScreenContract.View mView) { this.mAppRepository = mAppRepository; this.mView = mView; mView.setPresenter(this); } @Override public void loadPost() { mSubscription = mAppRepository.getPost() .observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.newThread()) .subscribe(new Observer() { @Override public void onCompleted() { Log.d(TAG, "Complete"); mView.showComplete(); } @Override public void onError(Throwable e) { Log.d(TAG, e.toString()); mView.showError(e.toString()); } @Override public void onNext(List posts) { mView.showPosts(posts); } }); } @Override public void loadPostFromRemoteDatatore() { new AppRemoteDataStore().getPost().observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.newThread()) .subscribe(new Observer() { @Override public void onCompleted() { Log.d(TAG, "Complete"); mView.showComplete(); loadPost(); } @Override public void onError(Throwable e) { Log.d(TAG, e.toString()); mView.showError(e.toString()); } @Override public void onNext(List posts) { } }); } @Override public void subscribe() { loadPost(); } @Override public void unsubscribe() { //Unsubscribe Rx subscription if (mSubscription != null && mSubscription.isUnsubscribed()) mSubscription.unsubscribe(); } }

Step 17: Create activity_main layout

I have a Listview inside a SwipeRefreshLayout

<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.ladwa.aditya.offlinefirstapp.mainscreen.MainActivity"> <android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/swipeContainer" android:layout_width="match_parent" android:layout_height="match_parent"> <ListView android:id="@+id/my_list" android:layout_width="match_parent" android:layout_height="match_parent" /> </android.support.v4.widget.SwipeRefreshLayout> </RelativeLayout>

Step 18: Code MainActivity

MainActivity implements MainScreenContract.View interface

AppRepository is injected by using @Inject annotation

We override onResume() and onStop() where we subscribe and unsubscribe respectively to the Presenter

When we receive the data showPost() is called by presenter and the result is displayed

When there is an error onError() is called and a Toast is show to user

public class MainActivity extends AppCompatActivity implements MainScreenContract.View, SwipeRefreshLayout.OnRefreshListener { private MainScreenContract.Presenter mPresenter; private ListView listView; private ArrayList list; private ArrayAdapter adapter; @Inject AppRepository repository; SwipeRefreshLayout swipeContainer; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //Inject dependency App.getAppComponent().inject(this); listView = (ListView) findViewById(R.id.my_list); swipeContainer = (SwipeRefreshLayout) findViewById(R.id.swipeContainer); swipeContainer.setOnRefreshListener(this); list = new ArrayList<>(); new MainScreenPresenter(repository, this); } @Override public void showPosts(List posts) { for (int i = 0; i < posts.size(); i++) { list.add(posts.get(i).getTitle()); } //Create the array adapter and set it to list view adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, list); listView.setAdapter(adapter); } @Override public void showError(String message) { Toast.makeText(this, "Error loading post", Toast.LENGTH_SHORT).show(); if (swipeContainer != null) swipeContainer.post(new Runnable() { @Override public void run() { swipeContainer.setRefreshing(false); } }); } @Override public void showComplete() { Toast.makeText(this, "Completed loading", Toast.LENGTH_SHORT).show(); if (swipeContainer != null) swipeContainer.post(new Runnable() { @Override public void run() { swipeContainer.setRefreshing(false); } }); } @Override protected void onResume() { super.onResume(); mPresenter.subscribe(); } @Override protected void onPause() { super.onPause(); mPresenter.unsubscribe(); } @Override public void setPresenter(MainScreenContract.Presenter presenter) { mPresenter = presenter; } @Override public void onRefresh() { mPresenter.loadPostFromRemoteDatatore(); } }

ScreenShots

Github

Source code of the app can be found here OfflineFirstReactive Android App

Conclusion

We used Repository architecture to make your apps Offline first

We used MVP design pattern to decouple business logic from implementation

We used Dagger 2 for dependency injection

We also created a local cache i.e SQLite database and a ContentProvider and wrapped it with StorIO and its Reactive features

We learnt a few RxJava operator.