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]

    View full-size slide

  2. This talk
    Try & try & fail
    Architecture
    proposal
    Use case
    Show me the
    code

    View full-size slide

  3. TRY & TRY & FAIL
    Section 1

    View full-size slide

  4. 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?

    View full-size slide

  5. 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”

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  12. ARCHITECTURE PROPOSAL
    Section 2

    View full-size slide

  13. ARCHITECTURE PROPOSAL
    ACT LOCALLY
    SYNC GLOBALLY

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  19. Architecture
    proposal
    Job schedulers
    Loaders
    All together
    Read
    Write
    Repositories
    SDK
    Loader
    Loader
    Loader
    Overall diagram
    App Layer
    Android
    SDK
    Entry
    Entry
    Entry
    Entry

    View full-size slide

  20. Architecture
    proposal
    Job schedulers
    Loaders
    Read diagram
    All together
    Read
    Write
    Repositories

    View full-size slide

  21. Architecture
    proposal
    Job schedulers
    Loaders
    Write diagram
    All together
    Read
    Write
    Repositories
    online

    View full-size slide

  22. USE CASE
    Section 3

    View full-size slide

  23. Use case
    Online sample
    Models
    offline Sample
    Articles and comments

    View full-size slide

  24. Use case
    Online sample
    Models
    offline Sample

    View full-size slide

  25. Use case
    Online sample
    Models
    offline Sample

    View full-size slide

  26. SHOW ME THE CODE
    Section 4

    View full-size slide

  27. Classes
    PostActivity PostLoader PostRepository
    PostDAO
    CommentDAO
    PostService
    SyncService
    SynchronizeTask
    write

    View full-size slide

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

    View full-size slide

  29. Post Activity: load data
    @Override
    public Loader> onCreateLoader(int id, Bundle args) {
    return new PostLoader(this, DemosApplication.instance().demoSdk());
    }
    @Override
    public void onLoadFinished(Loader> loader, List data) {
    stopLoading();
    setAdapter(data);
    }
    @Override
    public void onLoaderReset(@NonNull Loader> loader) {
    //Do nothing
    }
    Views

    View full-size slide

  30. Post Activity: Refresh
    @Override
    public void onRefresh() {
    startLoading();
    PostLoader.getLoader(getSupportLoaderManager()).forceLoad();
    }
    Views

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  33. post loader
    @Override
    public List 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

    View full-size slide

  34. Post repository: all posts
    @WorkerThread
    public List posts() {
    Response> postsResponse = mPostService.posts().execute();
    if (postsResponse.isSuccessful()) {
    List posts = postsResponse.body();
    mPostDao.deleteAll();
    mPostDao.save(posts);
    }
    return mPostDao.posts();
    }
    datasource

    View full-size slide

  35. Post repository: Save
    @WorkerThread
    public void localCreate(@NonNull Post post) {
    mPostDao.save(post);
    SyncService.triggerSync(mContext);
    }
    @WorkerThread
    public void remoteCreate(@NonNull Post post) {
    Response 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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  39. 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;
    }

    View full-size slide

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

    View full-size slide

  41. Sync task: NOTIFY
    @Override
    protected void onPostExecute(JobParameters jobParameters) {
    super.onPostExecute(jobParameters);
    SyncService.notifyChange(mSyncService);
    mSyncService.jobFinished(jobParameters, mNeedsResync);
    }
    sync

    View full-size slide

  42. And there you go! your app works offline

    View full-size slide

  43. SOURCE Code available
    https://github.com/droidpl/offline-architecture
    @droidpl

    View full-size slide

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

    View full-size slide

  45. ……………………
    We are hiring!
    Ask

    View full-size slide

  46. ……………………
    Q&A

    View full-size slide

  47. ……………………
    Thank you!

    View full-size slide