サンプル として、
Screen-level と その中に Button 3つの composable と ViewModel でカウンターを作ります。
@HiltViewModel
class HomeViewModel @Inject constructor() : ViewModel() {
private val _countState = mutableStateOf(0)
val countState: State<Int> = _countState
private val _countStateFlow = MutableStateFlow(0)
val countStateFlow = _countStateFlow.asStateFlow()
fun incState() {
_countState.value += 1
}
fun incStateFlow() {
_countStateFlow.value += 1
}
}
@Composable
fun HomeScreen(
viewModel: HomeViewModel = hiltViewModel()
) {
Timber.d("### HomeScreen composed.")
var countA by remember { mutableStateOf(0) }
val countB by viewModel.countState
val countC by viewModel.countStateFlow.collectAsStateWithLifecycle()
val onClickA = { countA += 1 }
val onClickB = { viewModel.incState() }
val onClickC = { viewModel.incStateFlow() }
Text("${System.currentTimeMillis()}")
ButtonA(countA, onClickA)
Divider()
ButtonB(countB, onClickB)
Divider()
ButtonC(countC, onClickC)
}
@Composable
private fun ButtonA(count: Int, onClick: () -> Unit) {
Timber.d("### ButtonA(${count}, ${onClick.hashCode()}) composed.")
Button(onClick = onClick) {
Text("A: $count")
}
}
@Composable
private fun ButtonB(count: Int, onClick: () -> Unit) {
Timber.d("### ButtonB(${count}, ${onClick.hashCode()}) composed.")
Button(onClick = onClick) {
Text("B: $count")
}
}
@Composable
private fun ButtonC(count: Int, onClick: () -> Unit) {
Timber.d("### ButtonC(${count}, ${onClick.hashCode()}) composed.")
Button(onClick = onClick) {
Text("C: $count")
}
}
ボタンを順番に押していきます。
挙動とログを予想できますか。
■ 結果
クリックのたびに、
Screen が recompose されています。
クリックしてないボタンも recompose されていますね!
Recompose Highlighter でもやってみました。
VIDEO
そうなるんですか。
なんでかな。
■ 「Compose Compiler Reports」 を使う
ツールを使って書き出してみます。
The Compose Compiler plugin can generate reports / metrics around certain compose-specific concepts that can be useful to understand what is happening with some of your compose code at a fine-grained level.
👉 androidx/compiler-metrics.md at androidx-main · androidx/androidx
// build.gradle
kotlinOptions {
if (project.findProperty("composeCompilerReports") == "true") {
freeCompilerArgs += [
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
project.buildDir.absolutePath + "/compose_compiler"
]
}
if (project.findProperty("composeCompilerMetrics") == "true") {
freeCompilerArgs += [
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
project.buildDir.absolutePath + "/compose_compiler"
]
}
}
./gradlew assembleRelease -PcomposeCompilerReports=true
./gradlew assembleRelease -PcomposeCompilerMetrics=true
👉 Jetpack Compose Stability Explained | by Ben Trengrove | Android Developers | Jul, 2022 | Medium
// app_release-composables.txt
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun HomeScreen(
unstable viewModel: HomeViewModel? = @dynamic hiltViewModel(null, $composer, 0, 0b0001)
)
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun ButtonA(
stable count: Int
stable onClick: Function0<Unit>
)
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun ButtonB(
stable count: Int
stable onClick: Function0<Unit>
)
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun ButtonC(
stable count: Int
stable onClick: Function0<Unit>
)
ViewModel が untable で @dynamic です。
// app_release-classes.txt
unstable class HomeViewModel {
stable val _countState: MutableState<Int>
stable val countState: State<Int>
unstable val _countStateFlow: MutableStateFlow<Int>
unstable val countStateFlow: StateFlow<Int>
<runtime stability> = Unstable
}
Flow が unstable なので ViewModel として unstable となっているように見えます。
// app_release-composables.csv
name,composable,skippable,restartable,readonly,inline,isLambda,hasDefaults,defaultsGroup,groups,calls,
HomeScreen,1,1,1,0,0,0,1,0,2,2,
ButtonA,1,1,1,0,0,0,0,0,1,1,
ButtonB,1,1,1,0,0,0,0,0,1,1,
ButtonC,1,1,1,0,0,0,0,0,1,1,
Button たちは、すべて同じ数字。
Screen はそれらと違う。
なんとく、ViewModel が原因のような雰囲気。
しかし、気安く ViewModel に @Stable とかつけたくないし。
■ remember を使う
onClick* のラムダが毎回初期化されているというので、
// val onClickB = { viewModel.incState() }
// val onClickC = { viewModel.incStateFlow() }
val onClickB = remember { { viewModel.incState() } }
val onClickC = remember { { viewModel.incStateFlow() } }
とすべて remember 扱いにします。
イメージしていたログ出力になりました。
■ 「::」を使う
こんな記述はどう? というので書き換えてみます。
// val onClickB = { viewModel.incState() }
// val onClickC = { viewModel.incStateFlow() }
// val onClickB = remember { { viewModel.incState() } }
// val onClickC = remember { { viewModel.incStateFlow() } }
val onClickB = viewModel::incState
val onClickC = viewModel::incStateFlow
これもイメージしていたログ出力になりました。
少し驚きました。
■ State 取得部分はどうなの?
Screen 自体が recompose されてるのなら、こうしたほうがいいのでは?
// val countB by viewModel.countState
// val countC by viewModel.countStateFlow.collectAsStateWithLifecycle()
val countB by remember { viewModel.countState }
val countC by remember { viewModel.countStateFlow }.collectAsStateWithLifecycle()
...
もう、知らん...
(ここは後日検証します。)
■ まとめ
そもそも recompose の条件とタイミングをコード目視で把握できますかね。
「考えないほうがいい、きちんと行儀よくコードを書け」的な記事もよく見かけます。
しかし、それで SideEffect系 きちんと使える気がしません。
では、だれか decompose をお願いします。
👉 パフォーマンスに任せてコードの見通しを優先するほうが「recompose沼」にはまらなくていいのではという逆説。 - Qiita
👉 takahirom/decomposer: Gradle Plugin that allows you to decompile bytecode compiled with Jetpack Compose Compiler Plugin into Java and check it
👉 【Jetpack Compose】「Layout Inspector Recomposition counts」で re-compose 回数を確認する