I'm using Redux and somewhere in application I need to check if returning data is not null, then run a function.
as you know in redux we should initialize the states like this:
data class ApplicationState(
val movieByGenre : List<DomainMovieModel> = emptyList()
)
the problem is when I check the data ,at the very beginning level, data has been set to empty list, and it satisfies the IF statement
by the way here is the implementation inside view model
fun getMovieByGenre(genreId: Int){
viewModelScope.launch {
val response=repository.getMovieByGenre(genreId)
store.update { currentState->
return#update currentState.copy(movieByGenre = response)
}
}
}
and here is the implementation in fragment:
viewModel.store.stateFlow.map { it.movieByGenre }.distinctUntilChanged().asLiveData().observe(viewLifecycleOwner){
if (it.isEmpty() ){
....
}else{
.....
}
}
Related
I currently have a piece of logic as follows:
interface anotherRepository {
fun getThings(): Flow<List<String>>
}
interface repository {
suspend fun getSomeThings(): AsyncResult<SomeThings>
}
when (val result = repository.getSomeThings()) {
is AsyncResult.Success -> {
anotherRepository.getThings().collectLatest {
// update the state
}
else -> { }
}
}
The problem I am having is that, if repository.getSomeThings has been triggered multiple times before, anotherRepository.getThings is getting triggered for the amount of all the pre-loaded values from repository.getSomeThings. I was wondering what is the proper way to use these repositories, one a suspend function, the other a Flow together. The equivalent behaviour that is combineLatest{} in Rx.
Thank you.
There are a couple of ways to solve your problem. One way is just to call
repository.getSomeThings() in the collectLatest block and cache last result:
var lastResult: AsyncResult<SomeThings>? = null
anotherRepository.getThings().collectLatest {
if (lastResult == null) {
lastResult = repository.getSomeThings()
}
// use lastResult and List<String>
}
Another approach is to create a Flow, which will be calling repository.getSomeThings() function, and combine two Flows:
combine(
anotherRepository.getThings(),
flow {emit(repository.getSomeThings())}
) { result1: List<String>, result2: AsyncResult<SomeThings> ->
...
}
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
}
}
}
The basic idea of the app is the following: fetch data from server using retrofit whenever its up, fall back on local room database whenever the server is unreachable. I have an issue with the way I'm saving data in my view models however. I'm using LiveData for the data fetched from Room and MutableLiveData for the data fetched from the server. Not sure how to have a single source for the data though. The Retrofit API returns my entities (Recipe) as List<Recipe>. Only way I know to persist that is using MutableLiveData:
var readAllIngredients = MutableLiveData<List<Ingredient>>().apply { value = emptyList() }
...
readAllIngredients.postValue(NetworkService.service.getIngredients())
Changing readAllIngredients to LiveData<List<Ingredient>> and then trying to set the value field of the list doesn't work since apparently value is not assignable to.
Can't make the DAO return MutableLiveData<List<Ingredient>> either (getting some compile errors). And I heard trying to cast one of these types to the other isn't exactly best practice. So I'm not sure what else I could try.
class InventoryViewModel(application: Application): AndroidViewModel(application) {
var readAllIngredients = MutableLiveData<List<Ingredient>>().apply { value = emptyList() }
private val ingredientRepository: IngredientRepository
init {
val ingredientDAO = ShoppingAppDatabase.getDatabase(application).ingredientDAO()
ingredientRepository = IngredientRepository(ingredientDAO)
viewModelScope.launch(Dispatchers.IO) {
// logic moved from repository in order to make example more concise
if(NetworkService.serverReachable()) {
readAllIngredients.postValue(NetworkService.service.getIngredients())
}
else {
//save the data from the database somehow; used to do it like
//readAllIngredients = ingredientRepository.allIngredients
//when readAllIngredients was of type `LiveData<List<Ingredient>>`
}
}
}
MediatorLiveData is used when you want to combine multiple sources. Example:
class InventoryViewModel(application: Application): AndroidViewModel(application) {
var readAllIngredients = MediatorLiveData<List<Ingredient>>().apply { value = emptyList() }
private val ingredientRepository: IngredientRepository
private var gotNetworkIngredients = false
init {
val ingredientDAO = ShoppingAppDatabase.getDatabase(application).ingredientDAO()
ingredientRepository = IngredientRepository(ingredientDAO)
readAllIngredients.addSource(ingredientRepository.allIngredients) { repoValue ->
if (!gotNetworkIngredients) {
readAllIngredients.value = repoValue
}
}
viewModelScope.launch(Dispatchers.IO) {
// logic moved from repository in order to make example more concise
if(NetworkService.serverReachable()) {
gotNetworkIngredients = true
readAllIngredients.postValue(NetworkService.service.getIngredients())
}
else {
//save the data from the database somehow; used to do it like
//readAllIngredients = ingredientRepository.allIngredients
//when readAllIngredients was of type `LiveData<List<Ingredient>>`
}
}
}
}
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
}
}
The following code is from the project.
The function of tasksRepository.refreshTasks() is to insert data from remote server to local DB, it's a time consuming operation.
In class TasksViewModel, asksRepository.refreshTasks() is wrapped with viewModelScope.launch{}, it means launch and careless.
1: How can I guarantee tasksRepository.observeTasks().distinctUntilChanged().switchMap { filterTasks(it) } to return the latest result?
2: I don't know how distinctUntilChanged() work, will it keep listening to return the latest result in whole Lifecycle ?
3: What's happened if I use tasksRepository.observeTasks().switchMap { filterTasks(it) } instead of tasksRepository.observeTasks().distinctUntilChanged().switchMap { filterTasks(it) }
Code
class TasksViewModel(..) : ViewModel() {
private val _items: LiveData<List<Task>> = _forceUpdate.switchMap { forceUpdate ->
if (forceUpdate) {
_dataLoading.value = true
viewModelScope.launch {
tasksRepository.refreshTasks()
_dataLoading.value = false
}
}
tasksRepository.observeTasks().distinctUntilChanged().switchMap { filterTasks(it) }
}
...
}
class DefaultTasksRepository(...) : TasksRepository {
override suspend fun refreshTask(taskId: String) {
updateTaskFromRemoteDataSource(taskId)
}
private suspend fun updateTasksFromRemoteDataSource() {
val remoteTasks = tasksRemoteDataSource.getTasks()
if (remoteTasks is Success) {
tasksLocalDataSource.deleteAllTasks()
remoteTasks.data.forEach { task ->
tasksLocalDataSource.saveTask(task)
}
} else if (remoteTasks is Result.Error) {
throw remoteTasks.exception
}
}
override fun observeTasks(): LiveData<Result<List<Task>>> {
return tasksLocalDataSource.observeTasks()
}
}
switchMap - The returned LiveData delegates to the most recent LiveData created by calling switchMapFunction with the most recent value set to source, without changing the reference. Doc
Yes, it'll keep listening to return the latest result in whole Lifecycle. distinctUntilChanged creates a new LiveData object that does not emit a value until the source LiveData value has been changed. The value is considered changed if equals() yields false.
Yes you can use that too but it'll keep emitting the values even the values are the same as the last emitted value.
e.g. first emitted value is ["aman","bansal"] and the second is the same ["aman","bansal"] which you don't want to emit since the values are same. So you use distinctUntilChanged to make sure it won't emit the same value until changed.
I hope this helped.