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.

Avatar for Hiroshi Kurokawa

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