I’ve implemented a tabbed pane. How do I make the ...
# compose
a
I’ve implemented a tabbed pane. How do I make the state of each tab remembered across selected tab changes. The code is roughly
Copy code
var selectedTab by remember { mutableIntStateOf(0) }
Tabs(..., onTabClicked = { selectedTab = it })
Box {
    if (selectedTab == 0) {
        FirstTabContent()
    } else {
        SecondTabContent()
    }
}
Do I really need to provide my own
SaveableStateRegistry
and manually save/restore it on every tab change?
n
I think using
rememberSaveable
should be enough 🤔
a
It doesn’t appear so. This doesn’t work:
Copy code
Column(Modifier.fillMaxSize()) {
                var shownIndex by remember { mutableStateOf(0) }
                Row(
                    horizontalArrangement = Arrangement.Center,
                    verticalAlignment = Alignment.CenterVertically,
                    modifier = Modifier.fillMaxWidth()
                ) {
                    Button(onClick = { shownIndex = 0 }) { Text("Show First Panel")}
                    Button(onClick = { shownIndex = 1 }) { Text("Show Second Panel")}
                }

                Box(Modifier.fillMaxWidth().weight(1f)) {
                    if (shownIndex == 0) {
                        var text by rememberSaveable { mutableStateOf("") }
                        TextField(value = text, onValueChange = { text = it })
                    } else {
                        Text("Screen 2")
                    }
                }
            }
Switching between the panels clears the text in the textfield.
I have this, which does work, but it seems strange that I’d need to implement it myself:
Copy code
private class TabsStateProvider {

    private val stateByTabIndex = mutableMapOf<Int, Map<String, List<Any?>>>()

    @Composable
    fun provideSaveableTabStateRegistry(tabIndex: Int, content: @Composable () -> Unit) {
        val state = stateByTabIndex.getOrPut(tabIndex) { mutableMapOf() }
        val registry = remember(tabIndex) {
            SaveableStateRegistry(state) { true }
        }
        CompositionLocalProvider(LocalSaveableStateRegistry provides registry) {
            content()
        }

        DisposableEffect(tabIndex, registry) {
            onDispose {
                stateByTabIndex[tabIndex] = registry.performSave()
            }
        }
    }

}


fun main() = singleWindowApplication {
    Column(Modifier.fillMaxSize()) {
        var shownIndex by remember { mutableStateOf(0) }
        Row(
            horizontalArrangement = Arrangement.Center,
            verticalAlignment = Alignment.CenterVertically,
            modifier = Modifier.fillMaxWidth()
        ) {
            Button(onClick = { shownIndex = 0 }) { Text("Show First Panel")}
            Button(onClick = { shownIndex = 1 }) { Text("Show Second Panel")}
        }

        val tabState = remember { TabsStateProvider() }
        Box(Modifier.fillMaxWidth().weight(1f)) {
            tabState.provideSaveableTabStateRegistry(shownIndex) {
                if (shownIndex == 0) {
                    var text by rememberSaveable { mutableStateOf("") }
                    TextField(value = text, onValueChange = { text = it })
                } else {
                    Text("Screen 2")
                }
            }
        }
    }
}
a
I think this is what you want.
Copy code
val savableStateHolder = rememberSaveableStateHolder()
savableStateHolder.SaveableStateProvider(key = selectedTab) {
    Content()
}
n
Copy code
Column {
  var text by rememberSaveable { mutableStateOf("") }
  var shownIndex by remember { mutableIntStateOf(0) }
  Row(
    horizontalArrangement = Arrangement.Center,
    verticalAlignment = Alignment.CenterVertically,
    modifier = Modifier.fillMaxWidth()
  ) {
    IconButton(onClick = { shownIndex = 0 }) { Text("Show First Panel")}
    IconButton(onClick = { shownIndex = 1 }) { Text("Show Second Panel")}
  }

  Box(
    Modifier
      .fillMaxWidth()
      .weight(1f)) {
    if (shownIndex == 0) {
      TextField(value = text, onValueChange = { text = it })
    } else {
      Text("Screen 2")
    }
  }
}
there's a simpler way to do it in this case, by hoisting the TextState above it so that the shownIndex recomposition doesn't affect its state
a
here’s a simpler way to do it in this case
That doesn’t work for a generic
TabbedPane
widget. I can’t ask the user to hoist all the state outside the widget.
I think this is what you want.
Indeed, that does the trick. Thanks.
👍 3