How can I launch recomposition when a specified Flow changed in Jetpack Compose? - kotlin

I know the Column(){...} will be recomposition when either b1 or b2 is changed.
If I hope that Column(){...} can be re-composed only when b2 is changed and Column(){...} doesn't be recomposed when b1 is changed, how can I do?
#Composable
fun ScreenDetail(
mViewMode: SoundViewModel
) {
val b1=mViewMode.a1.collectAsState(initial = 0)
val b2=mViewMode.a2.collectAsState(initial = 0)
Column() {
Text(" ${b1.value} ${b2.value}")
Text(Calendar.getInstance().time.toSeconds())
}
}
fun Date.toSeconds():String{
return SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.US).format(this)
}
class SoundViewModel(): ViewModel() {
var i = 0
val a1: Flow<Int> = flow {
while (true) {
emit(i++)
delay(1000)
}
}
val a2: Flow<Int> = flow {
while (true) {
emit(i)
delay(2000)
}
}
}

You need to create scopes if you need to have scoped recompositions. By scopes i mean creating a Composable that is not inline unlike Column, Row or Box. You can check answer and articles in this link.
Compose recomposes closest scope/function that States are read. If you read
Text(" ${b1.value} ${b2.value}")
your Column will be recomposed when any of these states changes. But as mentioned above even if you read any of the they should have changed because Column doesn't create a scope
#Composable
fun ScreenDetail2(
mViewMode: SoundViewModel
) {
val b1=mViewMode.a1.collectAsState(initial = 0)
val b2=mViewMode.a2.collectAsState(initial = 0)
Column(modifier= Modifier.background(getRandomColor()).fillMaxWidth()) {
Text("${b1.value}")
}
Column(modifier= Modifier.background(getRandomColor()).fillMaxWidth()) {
Text("${b2.value}")
}
}
But if you create a function such as
#Composable
private fun MyColumn(counter:Int){
Column(modifier= Modifier.background(getRandomColor()).fillMaxWidth()) {
Text("$counter")
}
}
you will be having scopes for each value you read
#Composable
fun ScreenDetail3(
mViewMode: SoundViewModel
) {
val b1=mViewMode.a1.collectAsState(initial = 0)
val b2=mViewMode.a2.collectAsState(initial = 0)
MyColumn(b1.value)
MyColumn(b2.value)
}
As you can see in the gif ScreenDetail2 recomposes each Column when b1 or b2 changes but ScreenDetail3 only recomposes respective scope of function. I changed delay time of b1 to 300 and b2 to 2000 to make recomposition easy to observe visually.
2 tolumns on top is from ScreenDetail2, and the bottom is from ScreenDetail3
Recomposition can be observer in many ways, i add 2
class Ref(var value: Int)
// Note the inline function below which ensures that this function is essentially
// copied at the call site to ensure that its logging only recompositions from the
// original call site.
#Composable
inline fun LogCompositions(msg: String) {
val ref = remember { Ref(0) }
SideEffect { ref.value++ }
println("$msg, recomposition: ${ref.value}")
}
or changing colors
fun getRandomColor() = Color(
red = Random.nextInt(256),
green = Random.nextInt(256),
blue = Random.nextInt(256),
alpha = 255
)

Related

why my data is coming as null or default in ui from view model in kotlin?

I set up mvvm structure and call two functions in viewmodel. Functions return values, but these values ​​always come as values ​​that I define as null or default.
Why can't I access my data in the view model in the UI?
I can see the data properly in the view model
hear is my code
my ui
if (viewModel.isDialogShown) {
AlertDialog(
onDismiss = {
viewModel.onDismissClick()
},
onConfirm = {
println("First"+viewModel.getFirstConversionRateByCurrency(viewModel.dropDownMenuItem1))
println("SECOND:"+viewModel.getSecondConversionRateByCurrency(viewModel.dropDownMenuItem2))
}
)
}
If the user says confirm in the alert dialog in my UI, these functions are called and I print the returned values ​​for testing purposes, but because I define null as default in the viewmodel, it always comes null.
my view model
#HiltViewModel
class ExchangeMainViewModel #Inject constructor(
private val getConversionRateByCurrencyUseCase: GetConversionRateByCurrencyUseCase
) : ViewModel() {
var second : Double?=null
var first : Double? = null
fun getFirstConversionRateByCurrency(currency:String) : String {
viewModelScope.launch {
first = getConversionRateByCurrencyUseCase.getConversionRateByCurrency(currency)
}
return first.toString()
}
fun getSecondConversionRateByCurrency(currency:String) :String {
viewModelScope.launch {
second = getConversionRateByCurrencyUseCase.getConversionRateByCurrency(currency)
}
return second.toString()
}
}
Also, I defined the viewmodel in ui as you can see the below like this, could it be because of this?
#Composable
fun DropDownMenu(
viewModel: ExchangeMainViewModel = hiltViewModel()
) {
The result in the console is as follows
All those functions your are calling are executing a coroutine from viewModelScope.launch{…} and at the time you launched them you immediately return a value from the method return.xxx.toString() where the xxx (first and second) doesn't have a value yet since what you are expecting is coming from a concurrent execution not a sequential one.
viewModelScope.launch { // launch me separately and wait for any value that will set the `first`
first = getConversionRateByCurrencyUseCase.getConversionRateByCurrency(currency)
}
// return from this function now with a null value, since it was initialized as null
return first.toString()
This is a rough implementation, though I'm not sure if this would work, but this should give you a headstart
Your new ViewModel
#HiltViewModel
class ExchangeMainViewModel #Inject constructor(
private val getConversionRateByCurrencyUseCase: GetConversionRateByCurrencyUseCase
) : ViewModel() {
var conversionValue by mutableStateOf<ConversionValues?>(null)
fun getFirstConversionRateByCurrency(currency:String) {
viewModelScope.launch {
val first = getConversionRateByCurrencyUseCase.getConversionRateByCurrency(currency)
val second = getConversionRateByCurrencyUseCase.getConversionRateByCurrency(currency)
conversionValue = ConversionValues(first, second)
}
}
}
the Data Class for first and second
data class ConversionValues(
val first : Double,
val second: Double
)
and your composable
#Composable
fun DropDownMenu(
viewModel: ExchangeMainViewModel = hiltViewModel()
) {
val conversionValue = vieModel.conversionValue
if (viewModel.isDialogShown) {
AlertDialog(
onDismiss = {
viewModel.onDismissClick()
},
onConfirm {
println("First" + conversionValue.first)
println("SECOND:"+conversionValue.second)
}
)
}
}

How do I update the UI before a turn in a game

I'm having trouble updating the UI in the middle of the turn. I'm currently trying to use a coroutine to choose a piece, but something is not going right. I want the player to click on the dice to roll them, so a new turn can begin, this works fine. The problem is the turn itself.
I have a function that sets an outside variable named "highLightedTiles" with every tile that is available for a move, it also creates a suspended coroutine and saves its continuation in another outside variable named "chosenTile".
suspend fun newTurn() {
currTurn++
dices.rollDices()
moveTurn() //Is a suspended function
endTurn()
}
private suspend fun moveTurn() {
currPlayer.pieces.forEach { p ->
if (p.info.isInPlay)
highlightedTiles.add(board.getTile(p.position!!))
}
selectedAlly = suspendCoroutine<Tile> { c -> chosenTile = c }.piece
setReachableTiles(dices.first * 2, selectedAlly!!)
}
The problem is the board, not updating itself, with the new highlights, and if it doesn't update itself the tiles are not clickable which means the continuation can't be resumed.
#Composable
suspend fun gameView(game: Game) {
paintBoard(game.board)
val dices = remember { game.dices }
paintHighlights(game.highlightedTiles, game.highlightType) { t ->
game.chosePiece(t)
}
diceView(dices) { game.newTurn() }
}
#Composable
suspend fun diceView(dices: Dices, onClick: suspend () -> Unit) {
val m1 = Modifier.clickable { onClick() }
val img1 = "dices/dice${if (dices.first == 0) "null" else dices.first}"
Image(painterResource("$img1.png"), img1, m1)
val m2 = Modifier.clickable { onClick() }
val img2 = "dices/dice${if (dices.second == 0) "null" else dices.second}"
Image(painterResource("$img2.png"), img2, m2)
}

Why needn't the author to wrap text.isNotBlank() with remember?

The Code A is from the official Sample project here.
1: I think I can wrap text.isNotBlank() withremember, so I think Code B is good, right?
BTW, I know the system will re-calculate when the text ( val (text, setText) = remember { mutableStateOf("") } ) is changed. So
2: In Code B, val (icon, setIcon) = remember { mutableStateOf(TodoIcon.Default)} and val iconsVisible = remember {text.isNotBlank() } will be re-launched when the text is changed (val (text, setText) = remember { mutableStateOf("") }) , right?
Code A
#Composable
fun TodoItemInput(onItemComplete: (TodoItem) -> Unit) {
val (text, setText) = remember { mutableStateOf("") }
val (icon, setIcon) = remember { mutableStateOf(TodoIcon.Default)}
val iconsVisible = text.isNotBlank()
Column {
Row( /* ... */ ) {
/* ... */
}
if (iconsVisible) {
AnimatedIconRow(icon, setIcon, Modifier.padding(top = 8.dp))
} else {
Spacer(modifier = Modifier.height(16.dp))
}
}
}
Code B
#Composable
fun TodoItemInput(onItemComplete: (TodoItem) -> Unit) {
val (text, setText) = remember { mutableStateOf("") }
val (icon, setIcon) = remember { mutableStateOf(TodoIcon.Default)}
val iconsVisible = remember {text.isNotBlank() } //I add remember
Column {
...
}
}
If you use remember as in Code B, iconsVisible will be calculated only once and the same value will be used across all recompositions and not get updates when text changes, which is not what we want here.
If you want to use remember here, you should pass text as a key to it, remember(text) { text.isNotBlank() }. But as this is not a time consuming calculation, you can just skip the remember block and use it as in Code A. The .isNotBlank() function will be invoked in every recomposition but that doesn't matter much here.

How Compose functions update themselves?

Lets suppose we have this compose function:
#Composable
fun Test() {
val foobar = remember { mutableStateOf("Foo") }
LaunchedEffect(true) {
delay(2000)
foobar.value = "Bar"
}
Text(text = foobar.value)
}
foobar state changes after 2 seconds, test function is recalled after foobar value has been changed (recomposition).
How can this be achieved? I mean how to recall/notify a function that one of its variables has been changed?

What is assigned to the variable updateSection in the offical sample code?

The Code A is from offical sample code here.
val (currentSection, updateSection) = rememberSaveable { mutableStateOf(tabContent.first().section) } is a destructuring declaration.
It's a clear that currentSection is assigned by tabContent.first().section
What is assigned to the variable updateSection? Will it be assigned by tabContent.first().content ?
Code A
#Composable
fun InterestsRoute(
...
) {
val tabContent = rememberTabContent(interestsViewModel)
val (currentSection, updateSection) = rememberSaveable {
mutableStateOf(tabContent.first().section)
}
...
}
#Composable
fun rememberTabContent(interestsViewModel: InterestsViewModel): List<TabContent> {
val uiState by interestsViewModel.uiState.collectAsState()
val topicsSection = TabContent(Sections.Topics) {
val selectedTopics by interestsViewModel.selectedTopics.collectAsState()
TabWithSections(
sections = uiState.topics,
selectedTopics = selectedTopics,
onTopicSelect = { interestsViewModel.toggleTopicSelection(it) }
)
}
...
return listOf(topicsSection, peopleSection, publicationSection)
}
enum class Sections(#StringRes val titleResId: Int) {
Topics(R.string.interests_section_topics),
People(R.string.interests_section_people),
Publications(R.string.interests_section_publications)
}
class TabContent(val section: Sections, val content: #Composable () -> Unit)
updateSection is a lambda here used to update the value of the mutable state.
Consider this example:
#Composable
fun MyButton() {
val (count, updateCount) = remember { mutableStateOf(0) }
Button(
onClick = { updateCount(count+1) }
) {
Text(text = "$count")
}
}
Here count is Int and updateCount is (Int) -> Unit. updateCount takes an Int and updates the value of the MutableState (count) to the provided value (here count+1).
The above code updates the count text by one everytime the button is clicked.
Coming back to your code, rememberSaveable { mutableStateOf (...) } creates a new MutableState. Its initial value will be tabContent.first().section. currentSection stores the value of this MutableState (initially it will be tabContent.first().section). Now if you want to update the value of this MutableState you can use the updateSection lambda. Just invoke that lambda with the new value and currentSection will be updated automatically.