こちら古い情報が記載されております。
# derivedStateOf とは
状態Aから状態Bを計算する場合に derivedStateOf を利用すると、状態Aが変化したときにのみ状態Bを再計算するように動作を保証してくれる。
```kotlin
/**
* Creates a [State] object whose [State.value] is the result of [calculation]. The result of
* calculation will be cached in such a way that calling [State.value] repeatedly will not cause
* [calculation] to be executed multiple times, but reading [State.value] will cause all [State]
* objects that got read during the [calculation] to be read in the current [Snapshot], meaning
* that this will correctly subscribe to the derived state objects if the value is being read in
* an observed context such as a [Composable] function.
*
* @sample androidx.compose.runtime.samples.DerivedStateSample
*
* @param calculation the calculation to create the value this state object represents.
*/
fun <T> derivedStateOf(calculation: () -> T): State<T> = DerivedSnapshotState(calculation)
特徴
- deriveStateOf を利用せずにある状態から他の状態を計算する処理をコンポーザブルに直書きしたとする。すると再コンポーズ時にある状態から他の状態を計算する処理が動作するようになる。
- deriveStateOf を利用するとある状態が変化したときにのみ他の状態を計算してくれる。そのため再コンポーズ時にある状態から他の状態を計算する処理が動作しないようになる。
サンプル
以下のようなニュースのリストを表示するデータ構造とコンポーザブル関数を用意する。これらのデータ構造とコンポーザブルを利用して「derivedStateOf を利用する場合」と「利用しない場合」ではどのように動作に差ができるのか検証してみる。
private data class News(
val title: String,
val content: String,
val pinned: Boolean
) {
companion object {
val SAMPLES = listOf(
News("ONE", "ONE ONE ONE", true),
News("TWO", "TWO TWO TWO", true),
News("THREE", "THREE THREE THREE", false),
News("FOUR", "FOUR FOUR FOUR", false),
News("FIVE", "FIVE FIVE FIVE", false),
)
}
}
@Composable
private fun NewsCard(news: News, onChangeStatus: () -> Unit, modifier: Modifier = Modifier) {
Card(modifier) {
Column {
Text(text = news.title)
Text(text = news.content)
Button(onClick = { onChangeStatus() }, modifier = Modifier.fillMaxWidth()) {
Text(text = if (news.pinned) "UNPIN" else "PIN")
}
}
}
}
derivedStateOf を利用しない場合
derivedStateOf を利用しない場合の動作を以下の動作をするサンプルを作って確認する。
- 全ニュース・ピン留めしているニュース・ピン留めしていないニュースのリストを表示する
- ニュースの PIN・UNPIN でピン留め・ピン留め解除ができる
- 右下の Change ボタンにてリスト表示のフィルターを変更できる
- 右下の Refresh ボタンにて右上の Unix 時間を更新できる
- リスト表示のフィルター処理は derivedStateOf を利用せずに直書きする
@Composable
fun DerivedStateOfSample() {
var filterType by remember { mutableStateOf(FilterType.NONE) }
var refreshTime by remember { mutableStateOf(Date().time) }
Box(modifier = Modifier.fillMaxSize()) {
NewsList(refreshTime, filterType)
Column(modifier = Modifier.align(Alignment.BottomEnd)) {
FloatingActionButton(
onClick = {
filterType = when (filterType) {
FilterType.PINNED -> FilterType.UNPINNED
FilterType.UNPINNED -> FilterType.NONE
FilterType.NONE -> FilterType.PINNED
}
}
) {
Text(text = "Change")
}
FloatingActionButton(
onClick = { refreshTime = Date().time }
) {
Text(text = "Refresh")
}
}
}
}
@Composable
private fun NewsList(refreshTime: Long, filterType: FilterType) {
Log.v("TEST", "enter pinned news list")
var newsList by remember { mutableStateOf(SAMPLES) }
Log.v("TEST", "calc pinned news list")
val filteredList = when (filterType) {
FilterType.PINNED -> newsList.filter { it.pinned }
FilterType.UNPINNED -> newsList.filter { !it.pinned }
FilterType.NONE -> newsList
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
item {
Surface(color = Color.LightGray, modifier = Modifier.fillMaxWidth()) {
Text(
text = "${filterType.name} $refreshTime",
style = MaterialTheme.typography.h6
)
}
}
items(filteredList) { news ->
NewsCard(
news = news,
onChangeStatus = {
newsList = buildList {
addAll(newsList)
val index = newsList.indexOf(news)
val newNews = news.copy(pinned = !news.pinned)
remove(news)
add(index, newNews)
}
},
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.padding(horizontal = 4.dp)
)
}
}
}
derviedStateOf を利用しない場合は以下のように動作する
- ニュースのフィルタが変更されたときにはフィルタ済みリストが再計算される
- ニュースのピン留め状態が変更されたときにはフィルタ済みリストが再計算される
- Unix 時間が変更されたときにはフィルタ済みリストが再計算される
- というようにニュースのリストに関係のない値が変化した場合にも再計算が動作してしまう
2022-02-25 22:28:36.080 8891-8891/jp.kaleidot725.sample V/TEST: enter pinned news list
2022-02-25 22:28:36.080 8891-8891/jp.kaleidot725.sample V/TEST: calc pinned news list
2022-02-25 22:28:39.096 8891-8891/jp.kaleidot725.sample V/TEST: enter pinned news list
2022-02-25 22:28:39.096 8891-8891/jp.kaleidot725.sample V/TEST: calc pinned news list
2022-02-25 22:28:40.464 8891-8891/jp.kaleidot725.sample V/TEST: enter pinned news list
2022-02-25 22:28:40.464 8891-8891/jp.kaleidot725.sample V/TEST: calc pinned news list
2022-02-25 22:28:41.497 8891-8891/jp.kaleidot725.sample V/TEST: enter pinned news list
2022-02-25 22:28:41.497 8891-8891/jp.kaleidot725.sample V/TEST: calc pinned news list
2022-02-25 22:28:42.951 8891-8891/jp.kaleidot725.sample V/TEST: enter pinned news list
2022-02-25 22:28:42.953 8891-8891/jp.kaleidot725.sample V/TEST: calc pinned news list
2022-02-25 22:28:44.315 8891-8891/jp.kaleidot725.sample V/TEST: enter pinned news list
2022-02-25 22:28:44.315 8891-8891/jp.kaleidot725.sample V/TEST: calc pinned news list
derivedStateOf を利用する場合
derivedStateOf を利用する場合の動作を以下の動作をするサンプルを作って確認する。
- 全ニュース・ピン留めしているニュース・ピン留めしていないニュースのリストを表示する
- ニュースの PIN・UNPIN でピン留め・ピン留め解除ができる
- 右下の Change ボタンにてリスト表示のフィルターを変更できる
- 右下の Refresh ボタンにて右上の Unix 時間を更新できる
- リスト表示のフィルター処理は derivedStateOf を利用して計算する
@Composable
fun DerivedStateOfSample() {
var filterType by remember { mutableStateOf(FilterType.NONE) }
var refreshTime by remember { mutableStateOf(Date().time) }
Box(modifier = Modifier.fillMaxSize()) {
NewsList(refreshTime, filterType)
Column(modifier = Modifier.align(Alignment.BottomEnd)) {
FloatingActionButton(
onClick = {
filterType = when (filterType) {
FilterType.PINNED -> FilterType.UNPINNED
FilterType.UNPINNED -> FilterType.NONE
FilterType.NONE -> FilterType.PINNED
}
}
) {
Text(text = "Change")
}
FloatingActionButton(
onClick = { refreshTime = Date().time }
) {
Text(text = "Refresh")
}
}
}
}
@Composable
private fun NewsList(refreshTime: Long, filterType: FilterType) {
Log.v("TEST", "enter pinned news list")
var newsList by remember { mutableStateOf(SAMPLES) }
val filteredList by remember(filterType) {
derivedStateOf {
Log.v("TEST", "calc pinned news list")
when (filterType) {
FilterType.PINNED -> newsList.filter { it.pinned }
FilterType.UNPINNED -> newsList.filter { !it.pinned }
FilterType.NONE -> newsList
}
}
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
item {
Surface(color = Color.LightGray, modifier = Modifier.fillMaxWidth()) {
Text(
text = "${filterType.name} $refreshTime",
style = MaterialTheme.typography.h6
)
}
}
items(filteredList) { news ->
NewsCard(
news = news,
onChangeStatus = {
newsList = buildList {
addAll(newsList)
val index = newsList.indexOf(news)
val newNews = news.copy(pinned = !news.pinned)
remove(news)
add(index, newNews)
}
},
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.padding(horizontal = 4.dp)
)
}
}
}
derivedStateOf を利用する場合は以下のように動作する
- ニュースのフィルタが変更されたときにはフィルタ済みリストが再計算される
- ニュースのピン留め状態が変更されたときにはフィルタ済みリストが再計算される(NewsList 自体は再コンポーズされない)
- Unix 時間が変更されたときにはフィルタ済みリストが再計算されない(NewsList 自体は再コンポーズされる)
- ニュースのリストに関係のない値が変化した場合にはフィルタ済みリストを再計算させないようにする、またニュースのリストの状態更新時にはコンポーザブル自体を再コンポーズさせないようにできる。
- フィルタが変更されたときにコンポーザブル自体が再コンポーズされるのは derivedStateOf を宣言している remember のキーにフィルタが設定されているからです。remember のキーがフィルタが変わったときにはフィルタ済みリストを破棄して再生するため再コンポーズされるようです。
2022-02-25 22:37:23.786 11595-11595/jp.kaleidot725.sample V/TEST: enter pinned news list
2022-02-25 22:37:23.787 11595-11595/jp.kaleidot725.sample V/TEST: calc pinned news list
2022-02-25 22:37:26.250 11595-11595/jp.kaleidot725.sample V/TEST: enter pinned news list
2022-02-25 22:37:26.250 11595-11595/jp.kaleidot725.sample V/TEST: calc pinned news list
2022-02-25 22:37:27.003 11595-11595/jp.kaleidot725.sample V/TEST: enter pinned news list
2022-02-25 22:37:27.003 11595-11595/jp.kaleidot725.sample V/TEST: calc pinned news list
2022-02-25 22:37:27.772 11595-11595/jp.kaleidot725.sample V/TEST: enter pinned news list
2022-02-25 22:37:27.774 11595-11595/jp.kaleidot725.sample V/TEST: calc pinned news list
2022-02-25 22:37:29.382 11595-11595/jp.kaleidot725.sample V/TEST: calc pinned news list
2022-02-25 22:37:30.666 11595-11595/jp.kaleidot725.sample V/TEST: enter pinned news list
参考文献
Discussion
間違ってたらすみません。
再コンポーズされるかどうかについては、
derivedStateOf
のおかげでなくてremember
が働いてるからだと思います。学習時にまとめたもので、正しく理解できていない箇所もありそうと思いますので、こちら再確認させていただきます。しばらく時間かかってしまうと思うのですが、上記の内容を確認させていただいて、更新したらまたコメントさせていただきます。
こちらメンテナンスが難しいと判断したのでアーカイブしてより、正しい情報が記載されている記事へのリンクを追加しました。(消してしまっても良いかなと思ったのですが、何かのタイミングでこの記事を参考にしてしまった場合に、後から見れなくなると困る方もいそうなので注意事項を記載した上でリンクを残す形にしました。)