3つの画像読み込みライブラリ Glide / Picasso / Coil - JetpackCompose 対応の状況

どれが、今、旬なのか。

まずはリンクを列挙しておきます。

全部使ってみようと思います。

giide vs picasso vs coil

👉 android glide, android picasso, android coil - 調べる - Google トレンド hatena-bookmark

 

Glide


32.8k stars
Watchers 1.1k watching
Forks 6k forks

👉 bumptech/glide: An image loading and caching library for Android focused on smooth scrolling hatena-bookmark

Glide

👉 Glide v4 : Fast and efficient image loading for Android hatena-bookmark

@sjudd Hey, I believe you're one of the maintainers of Glide. Is it a planned feature? Does Glide's team want help from the community on this? Jetpack Compose is going to stable soon, I believe later this month or next month

👉 Jetpack Compose Support · Issue #4459 · bumptech/glide hatena-bookmark

 

Picasso


18.3k stars
Watchers 867 watching
Forks 4k forks

👉 square/picasso: A powerful image downloading and caching library for Android hatena-bookmark

Picasso

👉 Picasso hatena-bookmark

Nobody works on Picasso. If you want something in the next N years definitely use Coil. Or Glide. Or whatever. They're all fine.

Image loading is a terrible, horrible business to be in. It's been really nice not being in that business for the last few years. I don't see a reason to resume. I certainly have no intent to support it anymore. Picasso accomplished its goal of moving the ecosystem out of the painful image loaders of 2011/2012 to the fluent and extensible ones we know today. But it's filled with technical debt and the legacy of poor design (at least, in hindsight) and is currently very, very stuck between a major refactor and redesign with no end in sight.

👉 Consider providing Jetpack Compose support · Issue #2203 · square/picasso hatena-bookmark

 

Coil


8.4k stars
Watchers 98 watching
Forks 512 forks

👉 coil-kt/coil: Image loading for Android backed by Kotlin Coroutines. hatena-bookmark

Coil

👉 Coil hatena-bookmark

To add support for Jetpack Compose, import the extension library:

implementation("io.coil-kt:coil-compose:2.1.0")

👉 Jetpack Compose - Coil hatena-bookmark

 

まとめ

JetpackCompose への対応ライブラリ群も見逃せません。

👉 skydoves/landscapist: 🍂 Jetpack Compose image loading library that fetches and displays network images with Glide, Coil, and Fresco hatena-bookmark

👉 wasabeef/composable-images: The Composable Images is a library providing Jetpack Compose wrapper for Glide, Picasso, and Coil. hatena-bookmark

アーキテクチャー、フレームワーク、ライブラリの選定はそのプロダクトの安定感に直結します。

機能確認を中心に検証しながら、将来の本筋を外してはなりません。

👉 画像読み込みライブラリ「COIL」 hatena-bookmark


【Jetpack Compose】rememberCoroutineScope() vs LaunchedEffect

Jetpack Compose で非同期処理を行う場合、どっちを使うべきか。

👉 android - Using rememberCoroutineScope() vs LaunchedEffect - Stack Overflow hatena-bookmark

悩ましい問題です。

2度押しボタン対策3秒待ちで考えてみます。

 

rememberCoroutineScope()


val scope = rememberCoroutineScope()
var enabled1 by remember { mutableStateOf(true) }

Button(
  onClick = {
    scope.launch {
      enabled1 = false
      delay(3000)
      enabled1 = true
    }
  },
  enabled = enabled1
) {
  Text("Button1")
}


起動

 ↓

ボタンを表示

 ↓

ボタンをクリック

 ↓

CoroutineScope.launch が実行される

   ↓

  enabled1 が false に更新されて3秒開始

   ↓

  ボタンが更新され無効化される

   ↓

  3秒経ったら enabled1 が true に更新される

 ↓

ボタンが更新され有効化される

@Composable のスコープでは書けない。


Calls to launch should happen inside a LaunchedEffect and not composition
→ @SuppressLint("CoroutineCreationDuringComposition")

実行のタイミングをユーザー操作に直接的に紐付けれる。

 

LaunchedEffect


var enabled2 by remember { mutableStateOf(true) }

Button(
  onClick = {
    enabled2 = false
  },
  enabled = enabled2
) {
  Text("Button2")
}

LaunchedEffect(key1 = enabled2) {
  if (!enabled2) {
    delay(3000)
    enabled2 = true
  }
}


起動

 ↓

ボタンを表示

 ↓

LaunchedEffect が実行される

   ↓

  enable2 が true なので何もしない

 ↓

ボタンをクリック

 ↓

enabled2 が false に更新される

 ↓

ボタンが更新され無効化される

 ↓

LaunchedEffect が key1 の変更により実行される

   ↓

  enabled2 が false なので3秒開始

   ↓

  3秒経ったら enabled2 が true に更新される

   ↓

ボタンが更新され有効化される

 ↓

LaunchedEffect が key1 の変更により実行される

   ↓

  enable2 が true なので何もしない

@Composable スコープで書く。

re/compose か キーの変化 で実行される。

 

まとめ

SideEffect系 は、今後、苦労することになると思う。

ViewModel を使ったほうが直感的でないか?

ライフサイクルはも合ってるし、

どっちみち、実際は、ViewModel → Repository の経路をたどるし。


// Screen-level Composable

val enabled3 by viewModel.enabled

Button(
  onClick = {
    viewModel.click()
  },
  enabled = enabled3
) {
  Text("Button3")
}


// ViewModel

private val _enabled = mutableStateOf(true)
val enabled: State<Boolean> = _enabled

fun click() {
  viewModelScope.launch {
    _enabled.value = false
    delay(3000) // request to repository suspend function
    _enabled.value = true
  }
}

肥大化していますよね、その Composable。

StateHolder としての ViewModel も考慮に入れてみるのもいいかもしれません。

👉 【Jetpack Compose】NavBackStackEntry - Composable のライフサイクルと ViewModel の状態を確認する hatena-bookmark
👉 【Jetpack Compose】 「Layout Inspector Recomposition counts」で re-compose 回数を確認する hatena-bookmark
👉 【Jetpack Compose】よくあるボタンの有効化/無効化 hatena-bookmark


【Jetpack Compose】テーマデフォルトの色 (baseline palette) を確認する【AndroidStudio】

ここ、なぜ出ないの?

【Jetpack Compose】テーマデフォルトの色を確認したい【AndroidStudio】

コードを追っていけば分かるが面倒。

特に、デフォルト色をひと目で分かるやつがほしい。

Light も Dark も、こういう感じの。

【Jetpack Compose】テーマデフォルトの色を確認したい【AndroidStudio】

👉 Dark theme - Material Design hatena-bookmark

いいのだが実際の色があれば分かりやすいのに、の Material3一覧 は以下。


👉 Color theming with MDC-Android – Material Design 3 hatena-bookmark

しかも、ブラウザをいちいち開けるのもなんかだるい。(開けてるけど。)

色の視覚的な記憶、って出来なくない?

いい方法ないか。

 

リンクをコメントで書いておく

⌘(command) キーとクリックでブラウザ開きますから。

Color theming with MDC-Android – Material Design 3

どうせ、ブラウザは開けてるはず。

 

デフォルト色の記述を変えて書いておく

AndroidStudio エディタの左にも表示されるような記述で、敢えて再度書く。

【Jetpack Compose】テーマデフォルトの色 (baseline palette) を確認したい【AndroidStudio】

Light と Dark の比較が欲しくなってきました。

 

まとめ

もう全部書いてやりました。

視覚的に分かりやすく、Light/Dark も比較できます。

【Jetpack Compose】テーマデフォルトの色 (baseline palette) を確認したい【AndroidStudio】

【Jetpack Compose】テーマデフォルトの色 (baseline palette) を確認したい【AndroidStudio】

以下、色味を確認するためだけのコード。


@Suppress("UNUSED_VARIABLE")
val default = listOf(

  // material3
  // light             dark
  Color(0xff6750a4), Color(0xffd0bcff), // colorPrimary
  Color(0xffffffff), Color(0xff381e72), // colorOnPrimary
  Color(0xffeaddff), Color(0xff4f378b), // colorPrimaryContainer
  Color(0xff21005d), Color(0xffeaddff), // colorOnPrimaryContainer
  Color(0xffd0bcff), Color(0xff6750a4), // colorPrimaryInverse
  Color(0xff625b71), Color(0xffccc2dc), // colorSecondary
  Color(0xffffffff), Color(0xff332d41), // colorOnSecondary
  Color(0xffe8def8), Color(0xff4a4458), // colorSecondaryContainer
  Color(0xff1d192b), Color(0xffe8def8), // colorOnSecondaryContainer
  Color(0xff7d5260), Color(0xffefb8c8), // colorTertiary
  Color(0xffffffff), Color(0xff492532), // colorOnTertiary
  Color(0xffffd8e4), Color(0xff633b48), // colorTertiaryContainer
  Color(0xff31111d), Color(0xffffd8e4), // colorOnTertiaryContainer
  Color(0xffb3261e), Color(0xfff2b8b5), // colorError
  Color(0xffffffff), Color(0xff601410), // colorOnError
  Color(0xfff9dedc), Color(0xff8c1d18), // colorErrorContainer
  Color(0xff410e0b), Color(0xfff2b8b5), // colorOnErrorContainer
  Color(0xff79747e), Color(0xff938f99), // colorOutline
  Color(0xfffffbfe), Color(0xff1c1b1f), // android:colorBackground
  Color(0xff1c1b1f), Color(0xffe6e1e5), // colorOnBackground
  Color(0xfffffbfe), Color(0xff1c1b1f), // colorSurface
  Color(0xff1c1b1f), Color(0xffe6e1e5), // colorOnSurface
  Color(0xffe7e0ec), Color(0xff49454f), // colorSurfaceVariant
  Color(0xff49454f), Color(0xffcac4d0), // colorOnSurfaceVariant
  Color(0xff313033), Color(0xffe6e1e5), // colorSurfaceInverse
  Color(0xfff4eff4), Color(0xff313033), // colorOnSurfaceInverse

  // material
  // light             dark
  Color(0xFF6200EE), Color(0xFFBB86FC), // primary
  Color(0xFF3700B3), Color(0xFF3700B3), // primaryVariant
  Color(0xFFFFFFFF), Color(0xFF000000), // onPrimary
  Color(0xFF03DAC6), Color(0xFF03DAC6), // secondary
  Color(0xFF018786), Color(0xFF03DAC6), // secondaryVariant
  Color(0xFF000000), Color(0xFF000000), // onSecondary
  Color(0xFFFFFFFF), Color(0xFF121212), // background
  Color(0xFF000000), Color(0xFFFFFFFF), // onBackground
  Color(0xFFFFFFFF), Color(0xFF121212), // surface
  Color(0xFF000000), Color(0xFFFFFFFF), // onSurface
  Color(0xFFB00020), Color(0xFFCF6679), // error
  Color(0xFFFFFFFF), Color(0xFF000000), // onError

  // Color.Any
  Color(0xFF000000), // Color.Black,
  Color(0xFF444444), // Color.DarkGray
  Color(0xFF888888), // Color.Gray
  Color(0xFFCCCCCC), // Color.LightGray
  Color(0xFFFFFFFF), // Color.White
  Color(0xFFFF0000), // Color.Red
  Color(0xFF00FF00), // Color.Green
  Color(0xFF0000FF), // Color.Blue
  Color(0xFFFFFF00), // Color.Yellow
  Color(0xFF00FFFF), // Color.Cyan
  Color(0xFFFF00FF), // Color.Magenta
  Color(0x00000000), // Color.Transparent

)

プラグインとかねえかな。

👉 IntelliJ IDEA プラグイン「Rainbow Brackets」を使う hatena-bookmark
👉 @SuppressWarnings て使っていますか。 hatena-bookmark


【Jetpack Compose】NavBackStackEntry - Composable のライフサイクルと ViewModel の状態を確認する

NavBackStackEntry Lifecylecle State

 

■ きっかけ

普通に、トップレベルの Composable内で State/Flow/StateFlow を返す ViewModel に接続します。


Composable ↔ ViewModel (↔ Repository)

👉 Hilt と Navigation

一般的な NavHostController を使う場合には、それは NavBackStackEntryインスタンス となり 一つの Activity の上にスタック上に積まれていく。

👉 Composable の LifecycleOwner は誰なのか - collectAsStateWithLifecycle


NavBackStackEntry3 - Composable3 ↔ ViewModel3 
NavBackStackEntry2 - Composable2 ↔ ViewModel2
NavBackStackEntry1 - Composable1 ↔ ViewModel1
Activity

ライフサイクル上、Activity と NavBackStackEntry との関係は良さそうです。

外側のActivityやFragmentがonStop状態になっていればこのNavBackStackEntryのlifecycleはCREATED状態になってくれていそうにみえます。

👉 ComposeNavigationで使えるNavBackStackEntryのlifecycleとは?
👉 NavBackStackEntryのLifecycleについて | by Kenji Abe | Medium

Flow を扱う ViewModel と、

スタックされる NavBackStackEntry (Screen-Level Composable) のライフサイクル。

気になります。

目視したいです。

遷移によっては同じ Composable がスタックに積まれていくのもキモいし。

 

■ 方法

ViewModel の生き死には、


class TodoViewModel @Inject constructor(
) : ViewModel() {

  init {
    // log
  }

  override fun onCleared() {
    // log
  }

}

で確認します。

👉 ViewModel はいつ生まれていつ死ぬか 【→ Jetpack Compose】

Composable - NavBackStackEntry のライフサイクルは、


LifecycleEventObserver { _, event ->
  // log event
}

を使います。hash 付けてます。

👉 DisposableEffect: クリーンアップが必要な作用

遷移先となる Composale に貼ります。


@Composable
fun TodoScreen(
  viewModel: TodoViewModel = hiltViewModel()
) {

  LocalLifecycleStateLogger(" TodoScreen") // *


@Composable
fun PokeScreen(
  viewModel: TodoViewModel = hiltViewModel()
) {

  LocalLifecycleStateLogger(" PokeScreen") // *

ついでに、これらの親となる NavHostController をセットしている Composale にも貼ります。


@Composable
fun MainScreen() {
  SystemUi(rememberSystemUiController())

  LocalLifecycleStateLogger(screenName = "MainScreen") // *

  val navHostController = rememberNavController()

 

■ 結果

BottomNavigation をつかっていますが、スタックや saveState の設定はデフォルトのままなので、navigation を使っている場合は同様ではないかと思います。


アプリ起動

  ↓ ① click

TodoScreen

  ↓ ② click

PokeScreen

  ↓ ③ click

TodoScreen

  ↓ ④ back

PokeScreen

  ↓ ⑤ back

TodoScreen

  ↓ ⑥ back

アプリ終了



ログ出力結果。


D: @@@ MainScreen - MainActivity@a542ab9 -> ON_CREATE
D: @@@ MainScreen - MainActivity@a542ab9 -> ON_START
D: @@@ MainScreen - MainActivity@a542ab9 -> ON_RESUME
D: @@@  TodoViewModel@abae21d initialized.
D: @@@  TodoScreen - NavBackStackEntry@8ad648d7 -> ON_CREATE
D: @@@  TodoScreen - NavBackStackEntry@8ad648d7 -> ON_START
D: @@@  TodoScreen - NavBackStackEntry@8ad648d7 -> ON_RESUME
D: @@@  TodoScreen - NavBackStackEntry@8ad648d7 -> ON_PAUSE
D: @@@  TodoScreen - NavBackStackEntry@8ad648d7 -> ON_STOP
D: @@@  PokeViewModel@ad0cefd initialized.
D: @@@  PokeScreen - NavBackStackEntry@4049e1c3 -> ON_CREATE
D: @@@  PokeScreen - NavBackStackEntry@4049e1c3 -> ON_START
D: @@@  PokeScreen - NavBackStackEntry@4049e1c3 -> ON_RESUME
D: @@@  PokeScreen - NavBackStackEntry@4049e1c3 -> ON_PAUSE
D: @@@  PokeScreen - NavBackStackEntry@4049e1c3 -> ON_STOP
D: @@@  TodoViewModel@577b5b1 initialized.
D: @@@  TodoScreen - NavBackStackEntry@f1d51055 -> ON_CREATE
D: @@@  TodoScreen - NavBackStackEntry@f1d51055 -> ON_START
D: @@@  TodoScreen - NavBackStackEntry@f1d51055 -> ON_RESUME
D: @@@  TodoScreen - NavBackStackEntry@f1d51055 -> ON_PAUSE
D: @@@  TodoScreen - NavBackStackEntry@f1d51055 -> ON_STOP
D: @@@  PokeScreen - NavBackStackEntry@4049e1c3 -> ON_CREATE
D: @@@  PokeScreen - NavBackStackEntry@4049e1c3 -> ON_START
D: @@@  TodoScreen - NavBackStackEntry@f1d51055 -> ON_DESTROY
D: @@@  TodoViewModel@577b5b1 onCleared.
D: @@@  PokeScreen - NavBackStackEntry@4049e1c3 -> ON_RESUME
D: @@@  PokeScreen - NavBackStackEntry@4049e1c3 -> ON_PAUSE
D: @@@  PokeScreen - NavBackStackEntry@4049e1c3 -> ON_STOP
D: @@@  TodoScreen - NavBackStackEntry@8ad648d7 -> ON_CREATE
D: @@@  TodoScreen - NavBackStackEntry@8ad648d7 -> ON_START
D: @@@  PokeScreen - NavBackStackEntry@4049e1c3 -> ON_DESTROY
D: @@@  PokeViewModel@ad0cefd onCleared.
D: @@@  TodoScreen - NavBackStackEntry@8ad648d7 -> ON_RESUME
D: @@@  TodoScreen - NavBackStackEntry@8ad648d7 -> ON_PAUSE
D: @@@ MainScreen - MainActivity@a542ab9 -> ON_PAUSE
D: @@@  TodoScreen - NavBackStackEntry@8ad648d7 -> ON_STOP
D: @@@ MainScreen - MainActivity@a542ab9 -> ON_STOP

ログから 各Screen = NavBackStackEntry のスタック状態を考えます。


  -

  ↓ ① click

 TodoScreen1 ↔ TodoViewModel1
MainScreen1 - MainActivity

  ↓ ② click

 PokeScreen1 ↔ PokeViewModel1
 TodoScreen1 ↔ TodoViewModel1
MainScreen1 - MainActivity

  ↓ ③ click

 TodoScreen2 ↔ TodoViewModel2
 PokeScreen1 ↔ PokeViewModel1
 TodoScreen1 ↔ TodoViewModel1
MainScreen@1 - MainActivity

  ↓ ④ back

 PokeScreen1 ↔ TodoViewModel2
 TodoScreen1 ↔ TodoViewModel1
MainScreen1 - MainActivity

  ↓ ⑤ back

 TodoScreen1 ↔ TodoViewModel1
MainScreen1 - MainActivity

  ↓ ⑥ back

  -

それぞれの NavBackStackEntry のライフサイクルと ViewModel の状態が確認できました。

ホストの Composable のライフサイクルオーナーは、Activity であることも分かりました。

画面が遷移していくと、NavBackStackEntry はスタックされていき、 back (戻る操作) すると、NavBackStackEntry は ON_DESTROY され、スタックから取り除くかれ、それに対応する ViewModel も onCleard() されていきます。

なんとなく思うのは、今現在としては、ViewModel は、やっぱりあったほうがいいように思います。

👉 【Jetpack Compose】 ViewModel を捨てて Repository を Composable に直結する

そのうち、


hiltViewModel<ViewModel>()

のように、Reposiitory や DataProvider を注入する

androidx.hilt.navigation.compose メソッドやアノテーションが出れば嬉しいです。


👉 Jetpack Compose without ViewModel / Twitter hatena-bookmark

👉 Jim Sproch (@JimSproch) - Jetpack Composeのコンポーネントはなぜ返り値がないのか - Blog - Mori Atsushi hatena-bookmark

もうこれは「UIもマルチスレッドな時代になった」と言っていいんすかね。

👉 User interface frameworks typically are single-threaded. Why is this so and what are the performance implications of this single-threading? - Quora hatena-bookmark
👉 Are Multiple UI Threads Possible? | Apple Developer Forums hatena-bookmark

(つづく...)


【Jetpack Compose】Compose (androidx.compose.*) のバージョンが分かれている件

元は、こんな感じで問題ありませんでした。


ext.versions = [
  'kotlin'  : '1.6.21'
  'compose' : '1.2.0-rc02'
]

dependencies {
  classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}"
}

composeOptions {
  kotlinCompilerExtensionVersion versions.compose
}


implementation "androidx.compose.material:material:${versions.compose}"
implementation "androidx.compose.material:material-icons-extended:${versions.compose}"
implementation "androidx.compose.ui:ui:${versions.compose}"
implementation "androidx.compose.ui:ui-tooling-preview:${versions.compose}"
debugImplementation "androidx.compose.ui:ui-tooling:${versions.compose}"
debugImplementation "androidx.compose.ui:ui-test-manifest:${versions.compose}"
androidTestImplementation "androidx.compose.ui:ui-test-junit4:${versions.compose}"

アップデート通知が出たので、いつものように


1.3.0-alpha01

に上げました。


ext.versions = [
  'kotlin'  : '1.6.21'
  'compose': '1.3.0-alpha01'
]

...

ビルドできなくなりました。

なんでや。

 

■ kotlin と compose の関係

kotlin と compose には、お互いに対応するバージョンが決まっていましたね!

Compose to Kotlin Compatibility Map

👉 Compose to Kotlin Compatibility Map  |  Android Developers hatena-bookmark

あれ、compose「1.3.0-alpha01」がないよ!

 

■ 「Compose」 は1つではない

Jetpack Compose is multiple things under one name:

- A compiler plugin that helps efficiently calculate the difference between two in-memory tree data structures
- A new UI toolkit for Android
- A new UI toolkit for desktop apps (Compose Desktop)

👉 Drop "androidx" from Jetpack Compose package name, for multiplatform, before 1​.​0 release. · Change.org hatena-bookmark

どうやら、Android でいうと compose は

- compose compiler
- compose ui toolkit

の2つに分かれているようです

👉 This version (1.2.0-alpha08) of the Compose Compiler requires Kotlin version 1.6.20 but you appear to be using Kotlin version 1.6.21 which is not known to be compatible. hatena-bookmark

 

■ まとめ

上記2つのことを考慮して書き換えます。


ext.versions = [
  'kotlin'    : '1.7.0'
  'compose'   : '1.2.0',         // compose-compiler
  'composeUi' : '1.3.0-alpha01'  // compose-ui
]

dependencies {
  classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}"
}

composeOptions {
  kotlinCompilerExtensionVersion versions.compose // compose-compiler
}

// compose-ui
implementation "androidx.compose.material:material:${versions.composeUi}"
implementation "androidx.compose.material:material-icons-extended:${versions.composeUi}"
implementation "androidx.compose.ui:ui:${versions.composeUi}"
implementation "androidx.compose.ui:ui-tooling-preview:${versions.composeUi}"
debugImplementation "androidx.compose.ui:ui-tooling:${versions.composeUi}"
debugImplementation "androidx.compose.ui:ui-test-manifest:${versions.composeUi}"
androidTestImplementation "androidx.compose.ui:ui-test-junit4:${versions.composeUi}"

👉 This version (1.2.0-alpha08) of the Compose Compiler requires Kotlin version 1.6.20 but you appear to be using Kotlin version 1.6.21 which is not known to be compatible. hatena-bookmark