1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Slack Circuitの`rememberRetained{}`のドキュメントとコードを読むメモ

Last updated at Posted at 2024-04-06

https://qiita.com/takahirom/items/8888b324b40bf5eb252b の続きです。

rememberRetained{}でViewModel相当のスコープを持てるということでしたが、それはどういうことなのか。どうやってnavigationとの連携をしているのかが気になったので見ていきます。

まずは公式ドキュメント。ViewModelを使っているとありますね。

rememberRetained – custom, remembers a value across recompositions and configuration changes. Can be any type, but should not retain leak-able things like Navigator instances or Context instances. Backed by a hidden ViewModel on Android. Note that this is not necessary in most cases if handling configuration changes yourself via android:configChanges.

android:configChangesすればあんまり必要ないはずだともありますね。

そもそもremember{}したときって、Navigationのバックスタックに保持されるんだっけ

っていうのが分からなくなったので、ちょっと実験します。

A->Bって画面遷移してからAに戻ったら、こんな感じのコードを入れて戻ってきたら時間は更新されました。

    val remembered = remember{ System.currentTimeMillis() }
    Toast.makeText(LocalContext.current, "MainScreen $remembered", Toast.LENGTH_SHORT).show()

やっぱり、rememberRetained{}必要ではとなりました。

いくつかCircuitにはサンプルがあって、タコスを注文するアプリと、犬を拾えるアプリがあるのですが、タコスの方ではPresenterを一つにして、すべての画面を一つのPresenterで管理することで、バックスタックなしにうまく実装しているんですが、現実的ではないような気がしています。
で、犬を拾えるアプリの方は、どうしているのかというと、保持するべき状態をrememberSaveable{} (OSに管理を任せる)としていました。が、一度バイナリデータに直せる必要があり、保持しているのはCommonParcelizeなどをつけてこんな感じになるのでつらそうだし容量制限もあったはずなので、つらいかもなとなりました。

@CommonParcelize
class Filters(
  @CommonTypeParceler<ImmutableSet<Gender>, ImmutableSetParceler>
  val genders: ImmutableSet<Gender> = Gender.entries.asIterable().toImmutableSet(),

rememberRetained{}のテストを読む

まあちょっとコード読んでいって普通にViewModelなどは見つけたんですが、結構登場人物多くて大変なので、テストから読もうと思いました。

KeyContentという関数がいろんなテストで使われているのでまずはそこから抑えましょう。

text1はrememberとmutableStateを使っている。
retainedTextはrememberRetainedを使っている。
それぞれ別々のTextFieldに設定されています。

@Composable
private fun KeyContent(key: String?) {
  var text1 by remember { mutableStateOf("") }
  // By default rememberSavable uses it's line number as its key, this doesn't seem
  // to work when testing, instead pass a key
  var retainedText: String by rememberRetained(key = key) { mutableStateOf("") }
  Column {
    TextField(
      modifier = Modifier.testTag(TAG_REMEMBER),
      value = text1,
      onValueChange = { text1 = it },
      label = {},
    )
    TextField(
      modifier = Modifier.testTag(TAG_RETAINED_1),
      value = retainedText,
      onValueChange = { retainedText = it },
      label = {},
    )
  }
}

一個目のテストはrecreateですね。setActivityContentを使っていて、これを使うと情報が保持されるLocalRetainedStateRegistryがprovideされます。

結果として、画面回転などでActivityが破棄された場合にTAG_REMEMBERは""になるのに対して、TAG_RETAINED_1は"Text_Retained"のままになっていて、保持されている事がわかります。

  @Test
  fun singleWithKey() {
    val content = @Composable { KeyContent("retainedText") }
    setActivityContent(content)
    composeTestRule.onNodeWithTag(TAG_REMEMBER).performTextInput("Text_Remember")
    composeTestRule.onNodeWithTag(TAG_RETAINED_1).performTextInput("Text_Retained")
    // Check that our input worked
    composeTestRule.onNodeWithTag(TAG_REMEMBER).assertTextContains("Text_Remember")
    composeTestRule.onNodeWithTag(TAG_RETAINED_1).assertTextContains("Text_Retained")
    // Restart the activity
    scenario.recreate()
    // Compose our content
    setActivityContent(content)
    // Was the text saved
    composeTestRule.onNodeWithTag(TAG_REMEMBER).assertTextContains("")
    composeTestRule.onNodeWithTag(TAG_RETAINED_1).assertTextContains("Text_Retained")
  }

  private fun setActivityContent(content: @Composable () -> Unit) {
    scenario.onActivity { activity ->
      activity.setContent {
        CompositionLocalProvider(
          LocalRetainedStateRegistry provides
            continuityRetainedStateRegistry(Continuity.KEY, vmFactory)
        ) {
          content()
        }
      }
    }
  }

こんな感じでテストからわかったことを箇条書きで書いていきます。

  • LocalRetainedStateRegistryをprovideしていれば、Activityの再構成でrememberRetained{}は残る
  • if文でrememberRetained{}が見えなくなると、保持される情報から消える
  • LocalRetainedStateRegistryをprovideしていないと、Activityの再構成でrememberRetained{}は消える
  • PopUpなどのコンテンツで、ネストされたRetainedStateRegistryがCompositionLocalProvider(LocalCanRetainChecker provides CanRetainChecker.Always)がprovideされていれば、PopUpがたとえ消えても、recreateされても生き残る
  • LocalCanRetainChecker provides { false }がProvideされていると、PopUpが消えたり、recreateされるとネストされたRetainedStateRegistryが消える
  • rememberRetained(ここ){}のここのキーが変わると、{}中が再実行されて初期化される

ちょっとここで https://github.com/takahirom/Rin というCircuitを使わずに同じようなことをできるライブラリを作り始めてしまい、色々読んだのですが記録は消失しています。


以下本当にメモなのですが、コードはないのですが、CircuitのrememberRetained{}のポイントをまとめておきます。

A -> Bへの画面移動がある場合で考えてください。

なぜA -> Bへの画面移動でAのStateが消えないのか?

rememberのonForgottonは呼ばれますがなぜ消えないのかというと、
Circuitのbackstackに居れば消さない処理があるからです。

configuration changeでなぜStateが消えないのか?

rememberのonForgottonは呼ばれますがなぜ消えないのかというと、
activityを使って、isChangingConfigurationsを使ってchanging中であれば消さないからです。

BからAに戻るボタンなどで戻ったときにBのStateはなぜ消えるのか?

onForgottonは呼ばれ、backstackにもいなくて、configuration changeでもないので消えます

画面回転時に他のComposableが設定されたときにStateがなぜ消えるのか?

onForgottonは呼ばれますが、configuration changeであるので、一度状態がそのまま残ってしまいます。
LaunchedEffectでframe後に今のCompositionに居ないstateを消す処理があるので消されます。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?