Offline-First Reactive Android Apps (Repository Pattern + MVP + Dagger 2 + RxJava + ContentProvider)

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

 

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 provides

public 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.

 

Advertisements

10 thoughts on “Offline-First Reactive Android Apps (Repository Pattern + MVP + Dagger 2 + RxJava + ContentProvider)

  1. Hi I’m trying to achieve something similar in my App. The problem that I’m dealing with is that my server work with etags, and the response from server-side can be either a 200 (Success) or a 304 (Not-Modified), when the response is a 200 I want to store the data in my database cache, but if the response is a 304 I don’t, because is the same data that I already have and It’s a waste of time saving it again.

    Here’s a full example: http://stackoverflow.com/questions/43960203/handling-different-flows-with-rxjava-and-retrofit

    Could you give me some advice?

    Thanks!

    Like

    1. Hey you can use ResponseBody that Okhttp provides and check the result code and based on the result code if its a 200 you can trigger a database save or else you can skip it

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s