I have a LazyList of items, each item has layers of nested children wrapped in other components. For each item, there could be 2 or 3 focusables nested. From the list level, how to I request focus on any focusable child of nth item?
val autoFocusIndex = n
LazyList{
itemsIndex(data){ index, d->
ListItem{...}
if(index == n){
// focus on any child focus area of nth item
}
}
}
#Composable
fun ListItem(...) { ListItemLayer1{...} }
#Composable
fun ListItemLayer1(...) { ListItemLayer2{...} }
#Composable
fun ListItemLayerX(...) {
// 2 or 3 focusables,
Box(Modifier.focusable()) {...}
Box(Modifier.focusable()) {...}
if(...) Box(Modifier.focusable()) {...}
}
Related
I have a function like:
#Composable
fun LazyElementList(data: Flow<PagingData<Element>>) {
val scrollState = rememberLazyListState()
val elements = data.collectAsLazyPagingItems()
LazyColumn(state = scrollState) {
items(elements) {
DisplayElement(it)
}
}
}
I would like when navigating to another screen and back to maintain the place in the list.
Unexpectedly, the value of scrollState is maintained when visiting child screens. If it wasn't, it should be hoisted, probably into the ViewModel.
In the code in the question scrollState will be reset to the beginning of the list because there are no items in the list on the first composition. You need to wait to display the list until the elements are loaded.
#Composable
fun LazyElementList(data: Flow<PagingData<Element>>) {
val scrollState = rememberLazyListState()
val elements = data.collectAsLazyPagingItems()
if (elements.isLoading) {
DisplayLoadingMessage()
} else {
LazyColumn(state = scrollState) {
items(elements) {
DisplayElement(it)
}
}
}
}
fun LazyPagingItems.isLoading(): Boolean
get() = loadState.refresh is LoadState.Loading
I have a Text composable within a Box:
Box(modifier = Modifier)
) {
Text(text = "BlaBla" )
}
How can show the Box/Text for a few seconds, only?
You can use LaunchedEffect and delay with a boolean flag and set it false after time specified
#Composable
private fun TimedLayout() {
var show by remember { mutableStateOf(true) }
LaunchedEffect(key1 = Unit){
delay(5000)
show = false
}
Column(modifier=Modifier.fillMaxSize()) {
Text("Box showing: $show")
if(show){
Box{
Text(text = "BlaBla" )
}
}
}
}
There are tons of way you can achieve this, for instance:
You can use AnimatedVisibility in case you want to choose which animation you want.
Example:
AnimatedVisibility(visible = yourFlag) {
Box{
Text(text = "BlaBla")
}
}
If you are using a ViewModel or any pattern and you can observe you can create two states and then inside of the ViewModel / Presenter / Whatever you start the timer you want (doing this you can test it):
sealed class TextState {
object Visible: TextState()
object Invisible: TextState()
}
And then inside the #Composable you do this
val state by presenter.uiState.collectAsState()
when (val newState = state) {
Visible -> {}
Invisible -> {}
}
You can change it also playing with the Alpha
Modifier.alpha(if(isVisible) 1f else 0f)
Another valid option is what #Thracian commented, using Side-Effects
for example :
#Composable
fun YourComposable() {
LaunchedEffect(key1 = Unit, block = {
try {
initTimer(SECONDS_HERE) {
//Timer has passed so you can show or hide
}
} catch(e: Exception) {
//Something wrong happened with the timer
}
})
}
suspend fun initTimer(time: Long, onEnd: () -> Unit) {
delay(timeMillis = time)
onEnd()
}
I'm having a real strange issue here. I have a ViewModel that has a StateFlow. That ViewModel is recreated in specific circumstances and sets it's StateFlow value to 0.
I also have Compose view that reads value of this StateFlow and displays text according to it.
Then I change that state to 2, for example. And then recreate the whole Compose view and ViewModel.
But, when I recreate the whole view, and the ViewModel, for brief moment, StateFlow keeps it's old state (even though ViewModel is recreated altogether with the View and state is set to 0), and then switches to the new one, which is zero (this only works if you make a change mentioned below).
This can cause tha crash if we have lists that have different amout of items, and we pass them when view is recreated, because then we will read index value that does not exist and our app will crash.
Changing the list ViewModelTwo(mutableListOf("text4")) to the ViewModelTwo(mutableListOf("text4", "text5", "text6")) will stop the crash. But look at the logs, and you'll see what's going on. First it goes to 2, then to 0, which is default.
I have github repo setup for Compose-Jb. You can open it in Android Studio: https://github.com/bnovakovic/composableIssue
Sorry for using android compose tags, but I could not find Compose-JB tag.
And for convinience, here are the code snippets.
Any help is appreciated
Main.kt
#Composable
#Preview
fun App(viewModelOne: ViewModelOne) {
val showComposable by viewModelOne.stateOne.collectAsState()
MaterialTheme {
// Depending on the state we decide to create different ViewModel
val viewModelTwo: ViewModelTwo = when (showComposable) {
0 -> ViewModelTwo(mutableListOf("text1", "text2", "text3"))
1 -> ViewModelTwo(mutableListOf("text4"))
else -> ViewModelTwo(mutableListOf("blah1", "blah2", "blah3"))
}
// New composable is always created with new ViewModelTwo that has default index of 0, yet the app still crashes
TestComposableTwo(viewModelTwo)
Row {
Button(onClick = {
viewModelOne.changeState()
}) {
Text("Click button below, than me")
}
}
}
}
fun main() = application {
Window(onCloseRequest = ::exitApplication) {
val viewModelOne = ViewModelOne();
App(viewModelOne)
}
}
TestComposableView
#Composable
fun TestComposableTwo(viewModelTwo: ViewModelTwo) {
val currentIndex by viewModelTwo.currentListItem.collectAsState()
println("Index is: $currentIndex")
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
) {
// At the point where we recreate this composable view, currentIndex keeps it's old value, and then changes it
// to the new one causing the app to crash since new list does not have index of 1
Text(text = viewModelTwo.stringList[currentIndex])
Button(onClick = {
viewModelTwo.changeIndex()
}) {
Text("Click me before clicking button above")
}
}
}
ViewModel1
class ViewModelOne {
private val viewModelScope = CoroutineScope(Dispatchers.IO)
private val _stateOne = MutableStateFlow(0)
val stateOne = _stateOne.asStateFlow()
fun changeState() {
viewModelScope.launch {
val currentValue = stateOne.value + 1
_stateOne.emit(currentValue)
}
}
}
ViewModel2
class ViewModelTwo(val stringList: List<String>) {
private val viewModelScope = CoroutineScope(Dispatchers.IO)
private val _currentListItem = MutableStateFlow(0)
val currentListItem = _currentListItem.asStateFlow()
fun changeIndex() {
viewModelScope.launch {
_currentListItem.emit(2)
}
}
}
It seems overly complex to make a new ViewModelTwo in the Composable based on a value in the first ViewModel. Try to make your ViewModel take care of the logic and your Composables just show the data.
I would suggest using a single ViewModel with a single MutableStateFlow.
And you don't need the emit or the viewModelScope in this case.
Something like this should work:
class MainViewModel {
private val _state = MutableStateFlow(State(listOf("text1", "text2", "text3"), 0))
val state: StateFlow<State> = _state
fun onChangeStateClick() {
_state.value = State(listOf("text4"), 0) // Or other logic here if needed
}
fun onChangeIndexClick() {
_state.update {
if (it.currentIndex < it.stringList.count() - 1) {
it.copy(currentIndex = it.currentIndex + 1)
} else it.copy(currentIndex = 0)
}
}
data class State(
val stringList: List<String>,
val currentIndex: Int
)
}
#Composable
#Preview
fun App(viewModel: MainViewModel) {
val state by viewModel.state.collectAsState()
MaterialTheme {
TestComposableTwo(
state.stringList[state.currentIndex],
onButtonClick = { viewModel.onChangeIndexClick() }
)
Row {
Button(onClick = {
viewModel.onChangeStateClick()
}) {
Text("Click button below, than me")
}
}
}
}
#Composable
fun TestComposableTwo(text: String, onButtonClick: () -> Unit) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
) {
Text(text = text)
Button(onClick = onButtonClick) {
Text("Click me before clicking button above")
}
}
}
I want a graph like this.
The graph is composable in LazyColumn's item. And the lazyColumn get stuck when the graph shows and hides. How to optimize it?
code:
#Composable
fun List(){
LazyColumn(){
item{
Graph()
}
items{
// other items
}
}
}
#Composable
fun Graph(){
Row(Modifer.height(200.dp)) {
// other simple Composables,
if(isShow)
repeat(30){
Column() {
repeat(7) {
Box(Modifer.size(16.dp).background(Blue))
}
}
}
}
}
I don't know much about compose.I found a way to fixme it.
Use Side-effect.
#Composable
fun List(){
val isShow = remember { mutableStateOf(true) }
val listState = rememberLazyListState()
LaunchedEffect(listState) {
// Graph is first item
snapshotFlow { listState.firstVisibleItemIndex }.collect {
if (it != 0) {
isShow.value = false
}else{
isShow.value = true
}
}
}
LazyColumn(state = listState){
item{
Graph(isShow.value)
}
items{
// other items
}
}
}
#Composable
fun Graph(isShow:Boolean){
Row(Modifer.height(200.dp)) {
// other simple Composables,
if(isShow)
repeat(30){
Column() {
repeat(7) {
Box(Modifer.size(16.dp).background(Blue))
}
}
}
}
}
There are simple codes.
Is a good way?
You need to use LazyColumn's items(...) or item { ..composable..} to take advantage of its lazy-ness. For each item on your column, you need to surround it with item to tell compose that is a row inside that column.
Say I have a list of items in a LazyColumn and they can all be selected. How can I make sure when an item is selected, the others are deselected (so only 1 item can be selected at once)?
I was thinking about interactionSource but I'm not really sure how use it.
I'd extract this logic into a ViewModel, with your items being a specific UIModel that contains your current data of the items + their state, containing the isSelected state.
For ex:
data class YourUiModel(val id: Long, val isSelected: Boolean = false, ...)
private val _items = MutableStateFlow(emptyList<YourUiModel>())
val items: StateFlow<List<YourUiModel>>
get() = _items
private fun changeSelectionTo(uiModel: YourUiModel) {
_items.value = items.value.map { it.copy(isSelected = it.id == uiModel.id) }
}
...
Just store a variable holding the index of the selected variable
var sel by mutableStateOf(0)
Now you may want to use remember here if you are declaring it in your Composables, but I would highly recommend storing it inside a viewmodel. A simple example would be
class VM: ViewModel(){
var sel by mutableStateOf(0) //Assuming 0 is selected initially, change it to -1 if you please
//Create a setter optionally, for state-hoisting
fun setSel(index: Int){
sel = index
}
}
In the MainActivity,
class MainActivity{
val vm by viewmodels<VM>()
setContent{
MySelectableList(
list = ...,
selected = vm.sel,
onSelectionChange = vm::setSel // Just pass the viewmodel if you did not create the setter
)
}
#Composable
fun MySelectableList(
list: List<Any>,
selected: Int,
onSelectionChange: (int) -> Unit // or vm, if you did not create the setter
){
list.forEachIndexed {index, item ->
// Now create this Composable yourself
ItemComposable(
isSelected = index == selected,
onClick = { onSelectionChange(index) } // Just focus and have a look at what's happening here
)
}
}
}
I am using state-hoisting here, read this for reference. Def take this codelab if you haven't already
You will need to do an additional check if you set the initial value of sel to -1 earlier. Completely understand the code that is flowing through here.
You can try use remember {...}
#Composable
fun MyComposeList(data: List<MyItemData>,...) {
val (selectedItem, setSelectedItem) = remember { mutableStateOf(null // or maybe data.first() } // remember current selected by state delegate
lazyColumn() { // your lazy column
items (data) { item -> // item = each item
// your compose item view
MyComposeItemView (
data = item, // pass data by each item
isSelected = data == selectedItem // set is item selected by default
) {
// on item click
setSelectedItem(it)
}
}
}
}
#Composable
fun MyComposeItemView(
data: MyItemData,
isSelected: Boolean,
onSelect: (MyItemData) -> Unit = {}) {
// here set item selected state = isSelected maybe change color or something
// assume we have box as single item
Box(Modifier.clickable {
onSelect(data) // cal on click function and pass current data as parameter
})
}