Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Droidcon ES 16': How to fail going offline

Droidcon ES 16': How to fail going offline

Architecture proposal to work offline using mainly android components and a single dataflow

Javier de Pedro López

October 25, 2016
Tweet

More Decks by Javier de Pedro López

Other Decks in Programming

Transcript

  1. Javier de Pedro López Strengths Experience Senior Android Developer 3

    Years professionally 2 Different companies Free time architecture Design Patterns Gradle Code quality @droidpl [email protected] [email protected]
  2. Try & try & fail Pojos Services Sync Adapters Why

    I did investigate? • I work on an SDK similar to firebase • Should work offline • Should have a simple API • Should be reliable and user friendly Why This matters?
  3. Try & try & fail Why This matters? Pojos Services

    Sync Adapters Why I did investigate? • Offline improves user experience • No loading times (most of the cases) • App always accesible • Just like magic
 • Makes your app less error prone
 • Forces the developer to think more mobile
 • Say no to: “you always have a good internet connection”
  4. Try & try & fail Why This matters? Pojos Services

    Sync Adapters Why I did investigate? Views/MVP/ Interactors Using POJOS Database Callback Network Callback Pojo Callback
  5. Try & try & fail Why This matters? Pojos Services

    Sync Adapters Why I did investigate? Using POJOS • Action cancellation • Lifecycle management • Single responsibility principle broken • Messy thread management • Hard to read code
 • Fast to implement
  6. Try & try & fail Why This matters? Pojos Services

    Sync Adapters Why I did investigate? Views/MVP/ Interactors Using Services Service ResultReceiver/Binder Threading Database Sync Network Sync Pojo Callback
  7. Try & try & fail Why This matters? Pojos Services

    Sync Adapters Why I did investigate? Problems using Services • Action cancellation is still a pain • Easy to leak memory
 • Easy to split in many services • Export to other apps • Easier to handle threading • Possibility to have many processes • Simplified callback system
  8. Try & try & fail Why This matters? Pojos Services

    Sync Adapters Why I did investigate? Views/ MVP/ Interactors Using Sync Adapters Network Sync Sync adapter Sync trigger Database (content provider) Sync Content observer
  9. Try & try & fail Why This matters? Pojos Services

    Sync Adapters Why I did investigate? Problems using Services • Hard disconnection errors • Sync adapter documentation • Sync adapter configuration (many files) • Too linked to accounts
 • Uses system tools • Content change notifications • All the benefits from services
  10. Loaders Architecture proposal Job schedulers All together Read Write Loaders

    Repositories Views/ MVP/ Interactors Loader inits provides data gets notified android lifecycle asks for data Data source
  11. Loader states Architecture proposal Job schedulers All together Read Write

    Loaders Repositories Started Stopped Reset Can load Can observe Can deliver Can load Can observe Can deliver Can load Can observe Can deliver Stop/Reset Start/Reset Start
  12. Job schedulers Architecture proposal Job schedulers Loaders All together Read

    Write Repositories Pojo notifies notifies System Scheduler schedule job Preconditions check Sync Service trigger met Sync task run task
  13. Repositories Architecture proposal Job schedulers Loaders All together Read Write

    Repositories Data Repository notifies Database Repository Read Write Other sources Network repository Read Write
  14. Architecture proposal Job schedulers Loaders Overall diagram All together Read

    Write Repositories Data Repository Notifies Asks for data Loader Inits Provides data System Scheduler Schedule sync Sync Service Sync task Conditions met Run task Changes data Read Write Read Write Views/ MVP/ Interactors Start App SDK write
  15. Architecture proposal Job schedulers Loaders All together Read Write Repositories

    SDK Loader Loader Loader Overall diagram App Layer Android SDK Entry Entry Entry Entry
  16. Post Activity: init private PostRepository mRepository; private Executor mExecutor; @Override

    protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mRepository = DemosApplication.instance() .demoSdk() .postRepository(); mExecutor = Executors.newSingleThreadExecutor(); startLoading(); PostLoader.init(getSupportLoaderManager(), this); } Views
  17. Post Activity: load data @Override public Loader<List<Post>> onCreateLoader(int id, Bundle

    args) { return new PostLoader(this, DemosApplication.instance().demoSdk()); } @Override public void onLoadFinished(Loader<List<Post>> loader, List<Post> data) { stopLoading(); setAdapter(data); } @Override public void onLoaderReset(@NonNull Loader<List<Post>> loader) { //Do nothing } Views
  18. Post Activity: Create post @Override public void onPostCreated(@NonNull final Post

    post) { startLoading(); mExecutor.execute(() -> { try { mRepository.localCreate(post); } catch (RepositoryException e) { showError(); } }); } Views
  19. Post Activity: Delete post @Override public void onDeleted(@NonNull final Post

    post) { startLoading(); mExecutor.execute(() -> { try { mRepository.localDelete(post); } catch (RepositoryException e) { showError(); } }); } Views
  20. post loader @Override public List<Post> loadInBackground() { setData(mDemoSdk.postRepository().posts()); return getData();

    } @Override public void registerListener() { if (mObserver == null) { mObserver = SyncService.listenForUpdates(this); } } @Override public void unregisterListener() { if (mObserver != null) { SyncService.removeUpdateListener(this, mObserver); } } loader
  21. Post repository: all posts @WorkerThread public List<Post> posts() { Response<List<Post>>

    postsResponse = mPostService.posts().execute(); if (postsResponse.isSuccessful()) { List<Post> posts = postsResponse.body(); mPostDao.deleteAll(); mPostDao.save(posts); } return mPostDao.posts(); } datasource
  22. Post repository: Save @WorkerThread public void localCreate(@NonNull Post post) {

    mPostDao.save(post); SyncService.triggerSync(mContext); } @WorkerThread public void remoteCreate(@NonNull Post post) { Response<Post> postResponse = mPostService.create(Post.builder(post) .internalId(null) .needsSync(false) .build()).execute(); if (postResponse.isSuccessful()) { mPostDao.save(Post.builder(postResponse.body()) .internalId(post.internalId()) .build()); } } datasource
  23. Post repository: DELETE @WorkerThread public void localDelete(@NonNull Post post) {

    long now = new Date().getTime(); if (post.isNew() && post.isStoredLocally()) { mPostDao.delete(post.internalId()); SyncService.notifyChange(mContext); } else { mPostDao.save(Post.builder(post) .deletedAt(now) .updatedAt(now) .needsSync(true).build()); SyncService.triggerSync(mContext); } } @WorkerThread public void remoteDelete(@NonNull Post post) { if (mPostService.deletePost(post.id()).execute().isSuccessful() && post.isStoredLocally()) { for (Comment comment : mCommentDao.comments(post.internalId())) { remoteDelete(comment); } mPostDao.delete(post.internalId()); } } datasource
  24. Sync service: trigger public static void triggerSync(@NonNull Context context) {

    SyncService.notifyChange(context); ComponentName component = new ComponentName(context, SyncService.class); JobInfo info = new JobInfo.Builder(SYNC_SERVICE_ID, component) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) .build(); getScheduler(context).schedule(info); } sync
  25. Sync service: Listen for updates public static BroadcastReceiver listenForUpdates(@NonNull Loader

    loader) { IntentFilter filter = new IntentFilter(CHANGE_SYNC_INTENT_ACTION); SyncServiceReceiver receiver = new SyncServiceReceiver(loader); LocalBroadcastManager.getInstance(loader.getContext()) .registerReceiver(receiver, filter); return receiver; } public static void removeUpdateListener(@NonNull Loader loader, @NonNull BroadcastReceiver observer) { LocalBroadcastManager.getInstance(loader.getContext()) .unregisterReceiver(observer); } public static void notifyChange(@NonNull Context context) { LocalBroadcastManager.getInstance(context).sendBroadcast(getCompletionIntent()); } sync
  26. Sync service: Do the JOB @Override public boolean onStartJob(JobParameters params)

    { DemoSdk sdk = DemoSdk.Factory.instance(); boolean willExecute = true; if (sdk != null) { mRunningSyncTask = new SynchronizeTask(sdk, this); mRunningSyncTask.execute(params); } else { willExecute = false; } return willExecute; } sync @Override public boolean onStopJob(JobParameters params) { boolean reschedule = false; if (mRunningSyncTask != null) { mRunningSyncTask.cancel(true); reschedule = true; } return reschedule; }
  27. Sync task: Sync @Override protected JobParameters doInBackground(JobParameters... params) { syncPosts();

    syncComments(); return params[0]; } sync private void syncPosts() { List<Post> posts = mSdk.postRepository().localPendingPosts(); for (Post post : posts) { if (post.isNew()) { mSdk.postRepository().remoteCreate(post); } else if (post.isDeleted()) { mSdk.postRepository().remoteDelete(post); } } }
  28. Sync task: NOTIFY @Override protected void onPostExecute(JobParameters jobParameters) { super.onPostExecute(jobParameters);

    SyncService.notifyChange(mSyncService); mSyncService.jobFinished(jobParameters, mNeedsResync); } sync
  29. Conclusions Offline matters because UX matters It is part of

    the architecture Not so much effort with the right choice Unique dataflow Don’t reinvent the wheel - Android has it -