Update State outside the composable function. (Jetpack compose) - kotlin

I am trying to implement redux with Jetpack compose. The scenario looks like this:
I have a list view where I need to show data, in composable function.
#Composable
fun CreateListView(text: String) {
val listdata = state { store.state }
LazyColumn {
//some listview code here
}
}
above, I want to use the data that I got from the redux store. but the store. The subscription method is standalone, and outside the composable. where, though I am able to update the state through new data, but the changes are not reflecting back to composable listview:
// activity page outside composable
private fun storeSubscription(){
viewModel.storeSubscription = store.subscribe {
when (store.state) {
store.state = // list data from some source
}
}
}
Is it possible to update the composable, like above, from outside the function, and without sending any parameter? Since the redux store is a global one, so it should work I think.

You can use MutableLiveData outside of composable function. Use observeAsState() in composable to recompose when data changes.
private val myLive = MutableLiveData<String>()
fun nonComposableScope(){
myLive.postValue("hello")
}
#Composable
fun MyScreen(textLive:LiveData<String>){
val text: String? by textLive.observeAsState()
// use text here
}

Try something like,
#Composable
fun <T> Store<T>.asState(): State<T> {
val result = remember { mutableStateOf(store.state) }
DisposableEffect {
val unsubscribe = store.subscribe {
result.value = store.state
}
onDispose { unsubscribe() }
}
return result
}
#Composable
fun CreateListView(text: String) {
val listdata by store.asState()
LazyColumn {
//some listview code here
}
}
The exact code might differ as I don't know what redux implementation you are using.
This creates an observable state object that will be updated whenever the lambda passed to subscribe is called. Also, it will automatically unsubscribe when CreateListView is no longer part of the composition.

You have to follow the state hosting pattern
From Android Domcumentaiton
Key Term: State hoisting is a pattern of moving state up the tree to
make a component stateless.
When applied to composables, this often means introducing two
parameters to the composable:
value: T: the current value to display. onValueChange: (T) -> Unit: an
event that requests the value to change where T is the proposed new
value.
So in your case you will save the state in the upper Composable that needs to access it, and pass the value of the state and a lambda function to change it to the other Composable, you can learn more from the Official Documentation.

You could simply use a lambda like so:
(An example from an app I am working on.)
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun RumbleSearchResult(rumbleSearchResult: RumbleSearchResult, onClick: () -> Unit) {
ListItem(
headlineText = {
rumbleSearchResult.title?.let { title ->
Text(title)
}
},
supportingText = {
rumbleSearchResult.channel.let { creator ->
val text = when {
rumbleSearchResult.views > 0 -> {
"${creator.name}, ${rumbleSearchResult.views} views"
}
else -> {
creator.name ?: ""
}
}
Row {
Text(text)
if (creator.isVerified) {
Icon(
painter = painterResource(R.drawable.ic_baseline_verified_24),
tint = Color.Cyan,
contentDescription = stringResource(id = R.string.mainActivity_verified_content_description)
)
}
}
}
},
leadingContent = {
AsyncImage(
rumbleSearchResult.thumbnailSrc,
contentDescription = null,
modifier = Modifier.size(100.dp, 100.dp)
)
},
modifier = Modifier.clickable {
onClick.invoke()
}
)
Divider()
}
Main composable:
LazyColumn {
items(viewModel.searchResults) {
RumbleSearchResult(rumbleSearchResult = it) {
openDialog = true
}
}
}

Related

Jetpack Compose Navigation Is Triggering Recomposition

I have a simple Jetpack Compose application that uses the Navigation Component.
My UI is comprised of the following composables (redacted UI to be concise):
NavHost:
#Composable
fun NavHost(
navController: NavHostController = rememberNavController(),
startDestination: String = "main"
) {
NavHost(
navController = navController,
startDestination = startDestination) {
composable("main") {
SomeList(onNavigateToItemView = {
navController.navigate("listItem")
})
}
composable("listItem") {
ItemView()
}
}
}
SomeList:
#Composable
fun SomeList(onNavigateToItemView: () -> Unit) {
Column {
Row ( Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
Text(text = Constants.APP_TITLE, fontSize = 30.sp, fontWeight = FontWeight.Bold)
}
Box(modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally
) {
items(items) { item->
ItemCard(item, onNavigateToItemView)
}
}
}
}
}
ItemCard:
#Composable
fun ItemCard(item: ItemModel, onNavigateToItemView: () -> Unit) {
Card(
border = BorderStroke(2.dp, Color.Cyan),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 5.dp)
.clickable {
onNavigateToItemView()
}
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
)
{
....
}
}
}
Now, whenever the user clicks on an ItemCard, he/she transitions to an ItemView.
What I am seeing is that the ItemView is being recomposed several times when navigating to it and also when navigating back from it.
According to the guide linked above,
The NavController's navigate function modifies the NavController's
internal state. To comply with the single source of truth principle as
much as possible, only the composable function or state holder that
hoists the NavController instance and those composable functions that
take the NavController as a parameter should make navigation calls.
Navigation events triggered from other composable functions lower in
the UI hierarchy need to expose those events to the caller
appropriately using functions.
And as you can see above, I am following that practice.
So, am I doing something wrong in my implementation or is this just how navigation and Jetpack Compose work together?
The multiple recomposition calls are affecting my UI unnecessarily.
Short Answer
Yes, navigating using compose navigation will recompose your composable each time you are navigating to it.
Workaround
When you are using navController.navigate() it will automatically recompose the targeted route. However, you can use an option to save the current state of the screen that you leave and restore the state of the screen that you target.
For example :
A->B->A->B
The first time you will load A & B you will recompose because you have no state saved yet. The moment you go from B to A (B->A) the state will be restored so you'll not recompose. Same things occurs the second time you go from A to B (A->B)
Use
Accord to documentation, you can make an extension function like this
fun NavController.popUpTo(destination: String) = navigate(destination) {
popUpTo(graph.findStartDestination().id) {
saveState = true
}
// Restore state when reselecting a previously selected item
restoreState = true
}
And use it like this
SomeList(onNavigateToItemView = {
navController.popUpTo("listItem")
})

viewModelScope blocks UI in Jetpack Compose

viewModelScope blocks UI in Jetpack Compose
I know viewModelScope.launch(Dispatchers.IO) {} can avoid this problem, but how to use viewModelScope.launch(Dispatchers.IO) {}?
This is my UI level code
#Composable
fun CountryContent(viewModel: CountryViewModel) {
SingleRun {
viewModel.getCountryList()
}
val pagingItems = viewModel.countryGroupList.collectAsLazyPagingItems()
// ...
}
Here is my ViewModel, Pager is my pagination
#HiltViewModel
class CountryViewModel #Inject constructor() : BaseViewModel() {
var countryGroupList = flowOf<PagingData<CountryGroup>>()
private val config = PagingConfig(pageSize = 26, prefetchDistance = 1, initialLoadSize = 26)
fun getCountryList() {
countryGroupList = Pager(config) {
CountrySource(api)
}.flow.cachedIn(viewModelScope)
}
}
This is the small package
#Composable
fun SingleRun(onClick: () -> Unit) {
val execute = rememberSaveable { mutableStateOf(true) }
if (execute.value) {
onClick()
execute.value = false
}
}
I don't use Compose much yet, so I could be wrong, but this stood out to me.
I don't think your thread is being blocked. I think you subscribed to an empty flow before replacing it, so there is no data to show.
You shouldn't use a var property for your flow, because the empty original flow could be collected before the new one replaces it. Also, it defeats the purpose of using cachedIn because the flow could be replaced multiple times.
You should eliminate the getCountryList() function and just directly assign the flow. Since it is a cachedIn flow, it doesn't do work until it is first collected anyway. See the documentation:
It won't execute any unnecessary code unless it is being collected.
So your view model should look like:
#HiltViewModel
class CountryViewModel #Inject constructor() : BaseViewModel() {
private val config = PagingConfig(pageSize = 26, prefetchDistance = 1, initialLoadSize = 26)
val countryGroupList = Pager(config) {
CountrySource(api)
}.flow.cachedIn(viewModelScope)
}
}
...and you can remove the SingleRun block from your Composable.
You are not doing anything that would require you to specify dispatchers. The default of Dispatchers.Main is fine here because you are not calling any blocking functions directly anywhere in your code.

Jetpack Compose Canvas drawPath doesn't work

I have this composable that is supposed to allow drawing with the pointer to a canvas but when I move the pointer nothing happens
#ExperimentalComposeUiApi
#Composable
fun CanvasDraw() {
val path = remember { Path() }
Canvas(modifier = Modifier
.fillMaxSize()
.pointerInteropFilter {
when (it.action) {
MotionEvent.ACTION_DOWN -> {
path.moveTo(it.x, it.y)
}
MotionEvent.ACTION_MOVE -> {
path.lineTo(it.x, it.y)
}
else -> false
}
true
}) {
drawPath(
path = path,
color = Color.Blue,
style = Stroke(10f)
)
}
}
remember { Path() } is caching lambda content value for next recompositions, but it cannot trigger recomposition when content on this object is changed. To make some state, which will trigger recomposition on changes in Compose, you need to use a mutable state of some kind - it's a new thing made for Compose.
You can find more info about state in Compose in documentation, including this youtube video which explains the basic principles.
Storing points inside Path will not be clean, as it's not a basic type. Instead I'm using a mutable state list of points, like this:
val points = remember { mutableStateListOf<Offset>() }
Canvas(modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectDragGestures { change, _ ->
points.add(change.position)
}
}
) {
drawPath(
path = Path().apply {
points.forEachIndexed { i, point ->
if (i == 0) {
moveTo(point.x, point.y)
} else {
lineTo(point.x, point.y)
}
}
},
color = Color.Blue,
style = Stroke(10f)
)
}

Kotlin Compose Search Bar

I have some doubts how to realize Search Bar in my Compose application. I call my SeachView function from ContactContent function where I pass state value.
#Composable
fun ContactContent(navigateToProfile: (Contact) -> Unit) {
val contacts = remember { DataProvider.contactList }
val textState = remember { mutableStateOf(TextFieldValue("")) }
Column(){
SearchView(textState)
LazyColumn() {
items(
items = contacts,
itemContent = {
ContactListItem(contact = it, navigateToProfile)
}
...
}
In SearchView I not sure how should I call onImeActionPerformed search state as mine search state is not be recognized.
#Composable
fun SearchView(state: MutableState<TextFieldValue>) {
Surface(){
TextField(
value = state.value,
onValueChange = { value -> state.value = value},
leadingIcon = {...},
keyBoardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Search
),
onImeActionPerformed = {action, softKeyboardController -> if (action == ImeAction.Search){
>HERE IS WHERE I AM NOT SURE WHAT TO DO<
DataProvider.newSearch(textState)
}
...
}
newSearch function snippet
fun newSearch (textState: MutableState<String>){
val result = repository.search(
token = token,
page = 1,
query = "chicken"
)
DataProvider.value = result
}
Maybe you have a different solution how to realize the search bar from the list with with Kotlin Compose.
Do not use it directly in the parenthesis of the onImeActionPerformed, extract it in the parent Composable right before calling the TextField. Store it in a val, then use that val inside your onImeActionPerformed. Alogside, I assume you are creating that parameter with something like () -> Unit, ok so I'm not sure of this, but I think changing that to #Composable () -> Unit, you can access it directly without extracting it in a val first. Try it out

Change source Flow for LiveData

I try to to use Flow instead of LiveData in repos.
In viewModel:
val state: LiveData<StateModel> = stateRepo
.getStateFlow("euro")
.catch {}
.asLiveData()
Repository:
override fun getStateFlow(currencyCode: String): Flow<StateModel> {
return serieDao.getStateFlow(currencyCode).map {with(stateMapper) { it.fromEntityToDomain() } }
}
It works fine if currCode if always the same during viewModel's lifetime, for example euro
but what to do if currCode is changed to dollar?
How to make state to show a Flow for another param?
You need to switchMap your repository call.
I imagine you could dosomething like this:
class SomeViewModel : ViewModel() {
private val currencyFlow = MutableStateFlow("euro");
val state = currencyFlow.switchMap { currentCurrency ->
// In case they return different types
when (currentCurrency) {
// Assuming all of these database calls return a Flow
"euro" -> someDao.euroCall()
"dollar" -> someDao.dollarCall()
else -> someDao.elseCall()
}
// OR in your case just call
serieDao.getStateFlow(currencyCode).map {
with(stateMapper) { it.fromEntityToDomain() }
}
}
.asLiveData(Dispatchers.IO); //Runs on IO coroutines
fun setCurrency(newCurrency: String) {
// Whenever the currency changes, then the state will emit
// a new value and call the database with the new operation
// based on the neww currency you have selected
currencyFlow.value = newCurrency
}
}