「Fragment を利用するか、しないか」という話

How often do you actually use fragments in your job? : androiddev

「どれくらいの頻度でFragmentを使っていますか。」というスレ。

興味深いコメントを多く見ることができます。

以前からFragmentについては否定的な意見を続けている Jake Warthon さんも登場しています。

5 years sober!

5年間使っていない。

Fragments don't represent anything overwhelmingly challenging. Creating re-usable pieces of UI tied to reusable controller/presenter/whatevers doesn't even require a library. Their problem stems from a lifecycle that's too complicated compounded with a menu system that's convoluted and obsolete compounded with trying to solve dialogs, UI, and retaining instances across rotation compounded with asynchronous transactions compounded with an opaque backstack.

Fragment はチャレンジするものではありません。再利用可能なUIのパーツ作成には、再利用可能なコントローラやプレセンターなどを使えば他のライブラリなど必要ありません。問題なのは、複雑すぎるメニューの仕組みとライフサイクルが根源で、不透明なバックスタックを抱えた非同期トランザクションを組み込んでおり、ローテーション時のインスタンス保持をダイアログやUIに対して解決しようとして複雑になりすぎています。

All of these things should have been decoupled: creating modular bits of UI + code with a simple lifecycle, navigation between conceptual destinations, workers that are retained across rotation, and multiple sources of contribution to a menu.

以下のように分離すべきです。

- シンプルなライフサイクルをもつ小分けされた UI+コード
- 概念的な遷移先への遷移
- ローテーション時のワーカーの保持
- メニューに必要な複数のソース

また、「別Activityに遷移する場合は起動に時間がかかる」ということに関しては以下のように答えています。

I only use a single activity. Activity transitions are a mess. I've never seen one that didn't jank or get screwed up by the result of scrolling or adapter changes behind the scenes. I'll use a second activity if there are vastly different window styles such as a pop-up as the result of a notification vs. the full-screen app. Granted, transitions in the same window are also a mess since the use of view overlay means elevation doesn't work... but at least when everything is in one window you actually have control of things.

一つの Activity とします。Activityのトランジションはややこしいです。スクロール処理やアダプターの変更結果で悩まされたり苦労してないものを見たことがありません。

通知の結果としてのポップアップやフルスクリーンのアプリなど、ウィンドウスタイルが大きく異なる場合は、2番目のアクティビティを使用します。 確かに、同じウィンドウ内での遷移は、表示オーバーレイ時にエレベーションが機能しないので混乱しますが、少なくともすべてが1つのウィンドウ内にあるときは、実質的には処理できます。

I've been doing it this way for the last 5 years. No problems. Or rather, all the problems were problems with how the screen was implemented and not with the mechanism itself. The fine grained control shouldn't be at the site requesting navigation anyway. It's either data in the arguments of the navigation action or an implementation of whoever is implementing the mechanism of animation between screens. Neither belongs inside the call site of screen initiating navigation nor the destination screen.

私は、このやり方で5年間やってきましたが何の問題もありません。メカニズム自体が問題になることはなく、むしろ、すべての問題は画面の実装方法によるものです。とにかく、遷移をリクエストする側にきめ細かいコントロール処理を置いてはいけません。遷移のアクションかアニメーションどちらかの利用する引数データだけを置きます。これらは、遷移元、遷移先のどちらにも属しません。

具体的にこんなやりとりもあります。

I'm not sure how he handles viewpager without fragments but for all other purposes he mostly uses custom views (at least that's what I've found from his github)

FragmentなしでViewPagerの使い方がわかりません。他はカスタムViewでやっています。

View pager.

「View」の Pager ですよ。

ネット上にもFragmentを利用したサンプルばかりが目に付きますが、

結局、Fragmentを使う理由は、

「Fragmentを使わない実装方法が分からない、分かりづらい」

というのが実質的なところでしょうか。


Android 8.0+ (Oreo) で ホーム画面にアイコンを

Android 8.0 では、アプリのショートカットが次のように変更されています。

com.android.launcher.action.INSTALL_SHORTCUT ブロードキャストは、プライベートで暗黙的なブロードキャストになったため、アプリに影響を与えることはなくなりました。代わりに、ShortcutManager クラスの requestPinShortcut() メソッドを使ってアプリのショートカットを作成する必要があります。

ACTION_CREATE_SHORTCUT インテントによって、ShortcutManager クラスを使用して管理するアプリ ショートカットを作成できるようになりました。このインテントでは、ShortcutManager とやり取りをしない以前のランチャーのショートカットも作成できます。これまで、このインテントでは以前のランチャーのショートカットしか作成できませんでした。

Extension Function で。コピペ用。


inline fun Context.createShortcutHomeScreen() {
  if (ShortcutManagerCompat.isRequestPinShortcutSupported(this)) {
    val shortcutInfo = ShortcutInfoCompat.Builder(this, "abc123")
        .setIntent(
            Intent(this, MainActivity::class.java).apply {
              action = Intent.ACTION_MAIN // for Oreo
            }
        )
        .setShortLabel("テストです")
        .setIcon(IconCompat.createWithResource(this, R.drawable.ic_favorite_black))
        .build()
    ShortcutManagerCompat.requestPinShortcut(this, shortcutInfo, null)
  } else {
    Timber.d("Shortcut is not supported by your launcher")
  }
}


Android Architecture Blueprints での コルーチンの使われ方

良い記事がたくさんあります。

Kotlin の Coroutine を概観する

入門Kotlin coroutines

読んでみましたが、きちんと理解できてる自信がありません!

MVPの中で、どのように使われているかAndroid Architecture Blueprintsで見てみましょう。

todo-mvp-kotlin-coroutines

すべての非同期処理をコルーチンで置き換えます。シンプルな非同期プログラミングとなり、直感的にコード書くことができます。

 

Presenter

非同期処置の記述はここが起点になります。


private fun loadTasks(forceUpdate: Boolean, showLoadingUI: Boolean) = launchSilent(uiContext) {
    // ...
    val result = tasksRepository.getTasks()
    // ...
}

TasksPresenter.kt#L69

メインのデータをリポジトリから取得する部分です。


launchSilent(uiContext) {
  // ...
}

CoroutineExt.kt#L18-L25

と見慣れない コルーチンビルダーがありますが、元の形に戻すと、


launch(
  context = UI,
  start = CoroutineStart.DEFAULT,
  parent = null) {
  // ...
}

AppExecutors.kt#L32-L35

です。

Presenterでは、launch() でコルーチンの起点を作っています。

Viewのメソッドを利用しないメソッドでは、コルーチンコンテクストは、DefaultDispatcher となり省略することができます。

 

Repository


override suspend fun getTasks(): Result<List<Task>> {
  // ...
  val result = tasksLocalDataSource.getTasks()
  // ...
 }

TasksRepository.kt#L49-L69


override suspend fun getTasks(): Result<List<Task>> = withContext(appExecutors.ioContext) {
  // ...
  Result.Success(tasksDao.getTasks())
  // ...
}

TasksLocalDataSource.kt#L34-L41

Repository -> LocalDataSource とサスベンドなfunctionが深く呼ばれていきます。

LocalDataSource では、Presenterでのlauch()時のコルーチンコンテキストから、withContext(appExecutors.ioContext) として、すべて、ioContext としての DefaultDispatcher に一時的に切り替えています。

 

まとめ

ざっくり、コルーチン関連の入れ子関係を書いてみると、


// presenter
launch(context = UI, parent = null) {

  // repository
  withContext(DefaultDispatcher) {
    // fetch data from database
  }

}

Presenter で launch() して、Repository末端(local/remote) で withContext() でコンテクスト切り替え。

もちろん、コルーチン内から呼ばれるのは、repository の suspend な functionず。