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.

Hiroshi Kurokawa

February 09, 2018
Tweet

More Decks by Hiroshi Kurokawa

Other Decks in Technology

Transcript

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

    }); … WHAT’S DISCONTINUITY? might be on a different thread
  2. 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)
  3. 4 TYPES OF DISCONTINUITY 1. Asynchronous Execution 2. Screen Rotation

    3. Schrödinger's Activity (a.k.a Like Problem) 4. Relay Activities
  4. 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); }); }
  5. 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
  6. 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
  7. RXJAVA ‣ Reactive Programming ‣ Data flow is defined, then

    changes are propagated ‣ Resource can be managed as a disposable
  8. 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
  9. KOTLIN COROUTINE ‣ Code is executed from top to bottom

    ‣ Code is suspended at an asynchronous execution, which does not block the thread
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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); });
  15. 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()); … }); }
  16. 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
  17. 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
  18. 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
  19. ACTIVITY RESULT ‣ Call #startActivityForResult() at List Activity ‣ Call

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

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

    updateFab(item, fab); final Intent result = ListActivityResultActivity.createResult(item); setResult(RESULT_OK, result); });
  22. 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); } } } } }
  23. EVENT BUS ‣ Pub/Sub Library ‣ An event can be

    sent/received between activities if they are alive
  24. 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
  25. 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); } } }
  26. EVENT BUS (DETAIL ACTIVITY) fab.setOnClickListener(view -> { item.checked = !item.checked;

    EventBus.getDefault().post(new ItemUpdateMessage(item)); updateFab(item, fab); });
  27. 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
  28. PERSISTENT REPOSITORY ‣ Straight forward but a bit complicated ‣

    Persistent repository ‣ Sync logic ‣ See Google I/O app.
 https://github.com/google/iosched
  29. RELAY ACTIVITIES ‣ Draft data is built through some activities

    ‣ The draft is not sent to the server until it is completed
  30. 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!
  31. 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
  32. PRO TIP ‣ You can simulate process kill with ‣

    “Stop process” button on Device Monitor ‣ $ adb shell
 $ run-as <package name> kill <pid>
  33. 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