Offline First Reactive Android Apps

What we will be Learning?

Aditya Ladwa
11 min readOct 25, 2016
  • 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 rootbuild.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
  • dagger-compiler 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.
dagger-2-gradle-sync.jpg
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
  • Also, I have 3 packages dagger, data, and mainscreen
  • Inside mainscreen I have MainscreenContract, Presenter and Activity class
  • Inside dager package I have component and module 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.
  • Further, I have defined two more packages local and remote that have classes AppLocalDataStore and AppRemoteDataStore that implement methods from AppDataStore interface
  • Inside local package there is another package models 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

[gallery ids="745,744,746" type="rectangular"]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
  • View interface holds all the methods which we will implement in our MainScreen View (i.e in our case MainActivity)
  • Presenter interface has all the methods that we will implement in ourMainScreenPresenter
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 providespublic interface AppDataStore {
Observable getPost();
}
  • We have only one method getPost 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
Screenshot_20161025-190810.png

Github

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.

--

--