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

Surviving a discontinuous world

Surviving a discontinuous world

In this talk, I am showing some difficulties of Android app development in terms of "discontinuity". I have a theory that some difficulties come from "discontinuity" and they are hard to solve.

I am giving 4 types of the difficulties and show some solutions to them.

979d93b360f80486b121486a9d063ad5?s=128

Hiroshi Kurokawa

February 09, 2018
Tweet

Transcript

  1. SURVIVING A DISCONTINUOUS WORLD 2018.02.09 HIROSHI KUROKAWA
 FABLIC, INC. Cliffs

    of Moher by Archigeek CC BY-NC-ND 2.0
  2. WHAT’S DISCONTINUITY? ▸ Some difficulties of Android development derive from

    discontinuity
  3. WHAT’S DISCONTINUITY? Normal Program

  4. WHAT’S DISCONTINUITY? Callback Android Program

  5. … getDataA(dataA -> { final TextView result = … result.setText(dataA.getMsg());

    }); … WHAT’S DISCONTINUITY? might be on a different thread
  6. WHAT’S DISCONTINUITY? Background Thread ?

  7. WHY DISCONTINUITY IS HARD? ▸ Code is not intuitive ▸

    Coder always need to care about pre-conditions ▸ A bug derived from such code is usually hard to find out (a timing issue)
  8. WE HAVE TO JUMP! Santorin Klippenspringen cliff diving 
 by

    Falco Ermert CC BY 2.0
  9. 4 TYPES OF DISCONTINUITY 1. Asynchronous Execution 2. Screen Rotation

    3. Schrödinger's Activity (a.k.a Like Problem) 4. Relay Activities
  10. "TZODISPOPVT&YFDVUJPO

  11. ASYNCHRONOUS EXECUTION Background Thread ?

  12. ASYNCHRONOUS EXECUTION public void onBtnClick(View btn) { getDataA(dataA -> {

    Log.i(TAG, "Setting data A [" + dataA + "]"); final String msg = getString(R.string.fetched_data_a, dataA.getMsg()); final TextView result = … result.setText(msg); }); }
  13. public void onBtnClick(View btn) { getDataA(dataA -> { Log.i(TAG, "Setting

    data A [" + dataA + "]"); final String msg = getString(R.string.fetched_data_a, dataA.getMsg()); final TextView result = … result.setText(msg); }); } ASYNCHRONOUS EXECUTION might be on a different thread the activity might be
 already destroyed the activity might be
 leaked
  14. ASYNCHRONOUS EXECUTION ‣ Thread control ‣ Activity life cycle ‣

    Resource leak
  15. public void onBtnClick(View btn) { getDataA(dataA -> { runOnUiThread(() ->

    { if (!isDestroyed()) { Log.i(TAG, "Setting data A [" + dataA + "]"); final String msg = getString(R.string.fetched_data_a, dataA.getMsg()); final TextView result = … result.setText(msg); } }); }); ‌} ASYNCHRONOUS EXECUTION
  16. ASYNCHRONOUS EXECUTION ‣ Thread control ‣ Activity life cycle ‣

    Resource leak &
  17. SOLUTIONS ‣ RxJava ‣ Kotlin Coroutine

  18. RXJAVA ‣ Reactive Programming ‣ Data flow is defined, then

    changes are propagated ‣ Resource can be managed as a disposable
  19. RXJAVA public void onBtnClick(View btn) { disposable = getDataA() .subscribeOn(Schedulers.io())

    .observeOn(AndroidSchedulers.mainThread()) .subscribe(dataA -> { Log.i(TAG, "Setting data " + dataA); final String msg = getString(R.string.fetched_data_a, dataA.getMsg()); Toast.makeText(AsyncActivity.this, msg, Toast.LENGTH_LONG).show(); }, throwable -> { Log.e(TAG, "Failed to retrieve data A"); }); } Thread control Lifecycle aware and resource is manageable
  20. KOTLIN COROUTINE ‣ Code is executed from top to bottom

    ‣ Code is suspended at an asynchronous execution, which does not block the thread
  21. KOTLIN COROUTINE fun onBtnClick(btn: View) { launch(job + UI) {

    try { val dataA = getDataA() Log.i(TAG, "Setting data $dataA") val msg = getString(R.string.fetched_data_a, dataA.msg) Toast.makeText(this@AsyncCoroutineActivity, msg, Toast.LENGTH_LONG).show() } catch (e: Throwable) { Log.e(TAG, "Failed to retrieve data A"); } } } Thread control Lifecycle aware and resources manageable Suspension point
  22. 4DSFFO3PUBUJPO

  23. SCREEN ROTATION - W/O ROTATION

  24. SCREEN ROTATION - W/O ROTATION

  25. SCREEN ROTATION - W/O ROTATION

  26. SCREEN ROTATION - W/ ROTATION

  27. SCREEN ROTATION - W/ ROTATION

  28. SCREEN ROTATION - W/ ROTATION

  29. SCREEN ROTATION - W/ ROTATION

  30. SCREEN ROTATION Background Thread Activity A Activity A’

  31. SCREEN ROTATION ‣ If you want Activity A to receive

    the result even after a screen rotation, you have to deal with it ‣ e.g. a long list
  32. SOLUTIONS ‣ AsyncTaskLoader ‣ ViewModel

  33. ASYNCTASKLOADER public void onBtnClick(View btn) { setLoading(true); final LoaderManager manager

    = getSupportLoaderManager(); manager.initLoader(0, new Bundle(), this); } @Override public Loader<DataA> onCreateLoader(int id, Bundle args) { switch (id) { case 0: return new DataAAsyncTaskLoader(this); } return null; } @Override public void onLoadFinished(Loader<DataA> loader, DataA data) { final TextView result = findViewById(R.id.result); Log.i(TAG, "Setting data A [" + data + "]"); result.setText(data.getMsg()); setLoading(false); } The result is delivered even after
 the activity is recreated
  34. VIEWMODEL ‣ A part of Architecture Components (AAC) ‣ Just

    taking advantage of Fragment#setRetainInstance(true) ‣ See https://developer.android.com/guide/ topics/resources/runtime-changes.html
  35. VIEWMODEL model = ViewModelProviders.of(this) .get(DataAViewModel.class); model.getDataA().observe(this, dataA -> { final

    TextView result = findViewById(R.id.result); Log.i(TAG, "Setting data A [" + dataA + "]"); result.setText(dataA.getMsg()); setLoading(false); });
  36. VIEWMODEL public class DataAViewModel extends ViewModel { private MutableLiveData<DataA> dataA;

    public LiveData<DataA> getDataA() { if (dataA == null) { dataA = new MutableLiveData <>(); } return dataA; } public void load() { … dataA.postValue(response.body()); … }); }
  37. SCREEN ROTATION

  38. 4DISÖEJOHFST"DUJWJUZ

  39. SCHRÖDINGER'S ACTIVITY ‣ Suppose you have two Activities ‣ List

    Activity - Fetch and show a list of items ‣ Detail Activity - Fetch and show an item ‣ You can mark an item or unmark it ‣ Mark state should be preserved between the Activities
  40. SCHRÖDINGER'S ACTIVITY

  41. SCHRÖDINGER'S ACTIVITY

  42. SCHRÖDINGER'S ACTIVITY

  43. SCHRÖDINGER'S ACTIVITY

  44. SCHRÖDINGER'S ACTIVITY

  45. SCHRÖDINGER'S ACTIVITY ?

  46. SCHRÖDINGER'S ACTIVITY ‣ Two scenarios 1. List Activity is recreated

    ‣ The list of items are fetched from server 2. List Activity is not recreated ‣ The previous Activity is shown
  47. SCHRÖDINGER'S ACTIVITY Recreated Not Recreated

  48. SCHRÖDINGER'S ACTIVITY ‣ Activity may or may not be recreated

    ‣ If you want a state be consistent between Activities, you have to do bridge the gap
  49. PRO TIP ‣ Turn on/off “Don’t keep activities” in
 Developer

    options
  50. SOLUTIONS ‣ startActivityForResult ‣ EventBus ‣ Persistent repository

  51. ACTIVITY RESULT ‣ Call #startActivityForResult() at List Activity ‣ Call

    #setResult() at Detail Activity ‣ Handle #onActivityResult() at List Activity
  52. ACTIVITY RESULT (LIST ACTIVITY) @Override public void onClick(Item item) {

    final Intent intent = ItemDetailActivityResultActivity.newIntent(this, item); startActivityForResult(intent, REQ_ITEM_DETAIL); }
  53. ACTIVITY RESULT (DETAIL ACTIVITY) fab.setOnClickListener(view -> { item.checked = !item.checked;

    updateFab(item, fab); final Intent result = ListActivityResultActivity.createResult(item); setResult(RESULT_OK, result); });
  54. ACTIVITY RESULT (LIST ACTIVITY) @Override protected void onActivityResult(int requestCode, int

    resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { case REQ_ITEM_DETAIL: if (resultCode == RESULT_OK) { final Item item = data.getParcelableExtra(ARG_KEY_ITEM); for (int i = 0; i < items.length; i ++) { if (items[i].id == item.id) { items[i].checked = item.checked; adapter.notifyItemChanged(i); } } } } }
  55. EVENT BUS ‣ Pub/Sub Library ‣ An event can be

    sent/received between activities if they are alive
  56. EVENT BUS ‣ Basic strategy ‣ An event is sent

    from Detail Activity ‣ If List Activity is destroyed
 → List Activity does not received the event
 → Data is loaded from server when recreated ‣ If List Activity is alive
 → The event is received at Activity
  57. EVENT BUS (LIST ACTIVITY) @Subscribe public void onMessageEvent(
 ItemUpdateMessage message

    ) { final int id = message.item.id; for (int i = 0; i < items.length; i ++) { if (items[i].id == id) { items[i] = message.item; adapter.notifyItemChanged(i); } } }
  58. EVENT BUS (DETAIL ACTIVITY) fab.setOnClickListener(view -> { item.checked = !item.checked;

    EventBus.getDefault().post(new ItemUpdateMessage(item)); updateFab(item, fab); });
  59. PERSISTENT REPOSITORY ‣ Data is stored in a local repository

    ‣ UI is updated as the local repository is updated ‣ Data may be synchronised with the remote data on servers if necessary
  60. PERSISTENT REPOSITORY ‣ Straight forward but a bit complicated ‣

    Persistent repository ‣ Sync logic ‣ See Google I/O app.
 https://github.com/google/iosched
  61. 3FMBZ"DUJWJUJFT

  62. RELAY ACTIVITIES

  63. RELAY ACTIVITIES

  64. RELAY ACTIVITIES

  65. RELAY ACTIVITIES ‣ Draft data is built through some activities

    ‣ The draft is not sent to the server until it is completed
  66. I HAVE A GOOD IDEA! ‣ Why don’t you hold

    the draft in a singleton? public class DraftManager { public static final DraftManager INSTANCE = new DraftManager(); private Draft draft; private DraftManager() { this.draft = new Draft(); } public Draft getDraft() { return draft; } } Do Not Do This!
  67. WHY? ‣ When the app process is killed in the

    background, the state is lost ‣ When user reopens the app, the last activity is shown ‣ The state in the singleton is not restored
 → inconsistent
  68. WHY?

  69. WHY?

  70. WHY?

  71. WHY?

  72. WHY? The process might be
 restarted

  73. WHY?

  74. PRO TIP ‣ You can simulate process kill with ‣

    “Stop process” button on Device Monitor ‣ $ adb shell
 $ run-as <package name> kill <pid>
  75. SOLUTIONS ‣ startActivityResult & onActivityResult ‣ Local storage ‣ Fragments

    with an Activity instead of Activities
  76. WRAP UP ‣ Some difficulties in Android development come from

    discontinuity ‣ 4 types of discontinuities 1. Asynchronous Execution 2. Screen Rotation 3. Schrödinger's Activity 4. Relay Activities