Howdy everyone hope all is well and swell, so I am having an issue with storing the live data I retrieve from my repository into my view Model so that my fragment can observe it. The scenario is as follows:
I have a suspended repository call like this
suspend fun getProfiles(profileId: Int): Resource<LiveData<List<Profile>>?>
{
return if(profileCaching()){
Resource.success(profileDao.getProfiles())
}else {
val result = fetchProfilesDataSource(profileId)//Suspend func API call
when (result.status) {
Status.SUCCESS -> when (result.data) {
null -> Resource.noContent()
else -> Resource.success(profileDao.getProfiles())
}
Status.LOADING -> Resource.loading(null)
Status.ERROR -> Resource.error(result.message!!, null)
}
}
}
The problem I am having is trying to structure my view Model so that a copy of this can be saved on it (To be observed by a fragment). I have tried calling it directly like this
val profiles = repo.getProfiles(10)
, but because it is suspended I have to wrap it in a viewModelScope. Additionally, I have tried using MediatorLiveData to try the copy the live data, but it didn't seem to retrieve it
var source: MediatorLiveData<List<Profile>> = MediatorLiveData()
fun processProfiles(){
viewModelScope.launch(Dispatchers.IO) {
val results = repo.getProfiles(10)
if (results.status == Status.SUCCESS && results.data != null) {
source.addSource(results.data, Observer{
source.value = it
})
} else {
//Set the empty list or error live data to true
}
}
}
Wanted to know if I was doing something wrong, or should I try a different approach?
Related
as invers to the question asked here How to convert Flow<List<Object>> to Flow<Object> I want to convert my Flow<Object> to Flow<List<Object>>.
At least I think I want that, so I try to explain what I want to achieve and give some background. I am working on an Android application that uses bluetooth to scan and connect to BLE devices. I'm fairly new to the Android platform and kotlin so I haven't quite grasped all the details despite all the many things I've already learnt.
My repository has a method which returns a Flow of ScanResults from the bluetooth adapter:
fun bluetoothScan(): Flow<ScanResult> {
return bluetoothStack.bluetoothScan()
}
My ViewModel consumes that function, maps the data to my BleScanResult and returns it as LiveData.
val scanResults: LiveData<BleScanResult> =
scanEnabled.flatMapLatest { doScan ->
if (doScan) {
repository.bluetoothScan().map { BleScanResult(it.device.name, it.device.address) }
} else {
emptyFlow()
}
}.asLiveData()
In my activity I want to observer on that data and display it in a RecyclerView:
val adapter = ScanResultListAdapter()
binding.rcBleScanResults.adapter = adapter
viewModel.scanResults.observe(this) { result ->
//result.let { adapter.submitList(it) }
}
The problem is that scanResults is from type Flow<BleScanResult> and not Flow<List<BleScanResult>>, so the call to adapter.submitList(it) throws an error as it is expected to be a list.
So, how do I convert Flow to Flow<List> (with additional filtering of duplicates)? Or is there something I miss about the conception of Flow/LiveData?
You can try to use a MutableList and fill it with the data you get form a Flow, something like the following:
val results: MutableList<BleScanResult> = mutableListOf()
val scanResults: LiveData<List<BleScanResult>> =
scanEnabled.flatMapLatest { doScan ->
if (doScan) {
repository.bluetoothScan().map {
results.apply {
add(BleScanResult(it.device.name, it.device.address))
}
}
} else {
emptyFlow()
}
}.asLiveData()
You can also use a MutableSet instead of MutableList if you want to have a unique list of items (assuming BleScanResult is a data class).
You could use the liveData builder to collect the Flow's values into a MutableList.
Here I copy the MutableList using toList() before emitting it since RecyclerView Adapters don't play well with mutable data sources.
val scanResults: LiveData<List<BleScanResult>> = liveData {
val cumulativeResults = mutableListOf<BleScanResult>()
scanEnabled.flatMapLatest { doScan ->
if (doScan) {
repository.bluetoothScan().map { BleScanResult(it.device.name, it.device.address) }
} else {
emptyFlow()
}
}.collect {
cumulativeResults += it
emit(cumulativeResults.toList())
}
}
If you want to avoid duplicate entries and reordering of entries, you can use a set like this:
val scanResults: LiveData<List<BleScanResult>> = liveData {
val cumulativeResults = mutableSetOf<BleScanResult>()
scanEnabled.flatMapLatest { doScan ->
if (doScan) {
repository.bluetoothScan().map { BleScanResult(it.device.name, it.device.address) }
} else {
emptyFlow()
}
}.collect {
if (it !in cumulativeResults) {
cumulativeResults += it
emit(cumulativeResults.toList())
}
}
}
i have a list from firebase database,
i'm getting my values in viewmodel as mutableState and show in compose components.
But if i navigate in my app, my list is gone because of (i think) my viewmodel instance recreated again and erase the list. I dont want to firabase call again. How can i solve that.
ViewModel:
var acceptedFriendRequestList = mutableStateOf<List<FriendListRow>>(listOf())
private set
fun loadAcceptedFriendRequestListFromFirebase(){
viewModelScope.launch {
useCases.loadAcceptedFriendRequestListFromFirebase.invoke().collect { response ->
when(response){
is Response.Loading -> {
}
is Response.Success -> {
if(response.data.size != 0){
acceptedFriendRequestList.value = response.data
}
delay(1000)
isRefreshing.value = false
}
is Response.Error -> {}
}
}
}
}
View:
#Composable
fun UserListScreen(){
val acceptedFriendRequestList = userListViewModel.acceptedFriendRequestList
}
i am using this variable in lazyColumn.
How can i prevent acceptedFriendRequestList value when i'am navigate and coming back in app.
I have this observer inside an activity and I want to make part of it run only when we have new data and not when the activity is recreated (like when you rotate screen)
signInViewModel.signInResponse.observe(this, {
response-> when(response){
is NetworkResult.Success ->{
response.data?.let {
signInViewModel.saveJwt(it)
}
}
is NetworkResult.Error ->{
errorMsg.text = response.message.toString()
}
is NetworkResult.Loading -> {
}
}
})
I want to run this signInViewModel.saveJwt(it) only when we get new data and other part run when we get new data and when we recreate the activity.
override onChange method, that should only run when the data changes.
By far this should solve your problem
As I understand, you want to observe your NetworkResult only once when it's in (Success) state, and multiple times for other status as your replay for #Gouse Mohiddin answer. I'll recommend for your same #Gouse Mohiddin answer to use StateFlow with some different changes...
First of all you need to create some method in your ViewModel to reset your stateFlow to your default.
ViewModel
private val _signInResponse = MutableStateFlow<NetworkResult(NetworkResult.Loading)
val signInResponse : StateFlow<NetworkResult> = _signInResponse.asStateFlow()
fun resetSignInResponse(){
_signInResponse.value = _signInResponse.value.copy(NetworkResult.Default)
}
When you receive your new data use same
_signInResponse.value = _signInResponse.value.copy(NetworkResult.Success(jwt))
Activity
signInViewModel.signInResponse.collect { response ->
when (response) {
is NetworkResult.Success -> {
response.data?.let {
signInViewModel.saveJwt(it)
//After receiving success data, reset your state to default
//this will never run again unless you receive new data
signInViewModel.resetSignInResponse()
}
}
is NetworkResult.Error -> {
//this will run always if you hold an error in state
errorMsg.text = response.message.toString()
}
is NetworkResult.Default -> {
//Ignore Or Apply the default actions you want
}
}
}
and don't forget to reset your state to default before any updates (emits).
resetSignInResponse()
You should use Kotlin StateFlow instead of liveData
StateFlow will emit new data only (not any duplicate)
Only change required is
private val _signInResponse = MutableStateFlow<NetworkResult>(NetworkResult.Loading)
val signInResponse : StateFlow<NetworkResult> = _signInResponse
When you have the data simply call
_signInResponse.value = NetworkResult.Success(jwt)
And Handle like this
signInViewModel.signInResponse.collect{ response->
when(response){
is NetworkResult.Success ->{
response.data?.let {
signInViewModel.saveJwt(it)
}
}
is NetworkResult.Error ->{
errorMsg.text = response.message.toString()
}
is NetworkResult.Loading -> {
}
}
}
For start I must say I am begginer in RxJava.
Data class:
#Entity(tableName = "google_book")
data class GoogleBook (
#PrimaryKey(autoGenerate = true) val id: Int=0,
val items: ArrayList<VolumeInfo>)
data class VolumeInfo(val volumeInfo: BookInfo){
data class BookInfo(val title: String, val publisher: String, val description: String, val imageLinks: ImageLinks?)
data class ImageLinks(val smallThumbnail: String?)
}
Function which helps me save data to database:
fun searchBooks(query: String) {
searchJob?.cancel()
searchJob = viewModelScope.launch {
val text = query.trim()
if (text.isNotEmpty()) {
bookRepository.getBooksFromApi(query)
.map { t ->
t.items.map {
it.volumeInfo.imageLinks?.smallThumbnail?.filter { x -> x != null }
}
t
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { x ->
x?.let { googleBook ->
searchJob?.cancel()
searchJob = viewModelScope.launch {
bookRepository.deleteGoogleBook()
bookRepository.insertGoogleBook(googleBook)
}
} ?: kotlin.run {
Log.d(TAG, "observeTasks: Error")
}
}
}
}
}
As seen I want to filter list within GoogleBook object by image parameter but It doesnt work. I cannot add filtering for data class ImageLinks so I have no Idea how can I make it right
I am asking mostly about this part:
.map { t ->
t.items.map {
it.volumeInfo.imageLinks?.smallThumbnail?.filter { x -> x != null }
}
t
}
Thanks for reading
welcome to RxJava, you gonna love it.
As far as I can tell the issue with your filtering simply relies here:
.map { t ->
t.items.map {
it.volumeInfo.imageLinks?.smallThumbnail?.filter { x -> x != null })
} // this returns you a new list filtered list here, but does not modify the original one
t // but you return the same data object here, it is not modified at all
}
// also consider naming it bookInfo if it is actually a bookInfo
What you should do is make a copy of your object with the filtered elements, something like this:
fun filterGoogleBookBySmallThumbNail(googleBook: GoogleBook): GoogleBook {
val filteredItems = googleBook.items.filter { it.volumeInfo.imageLinks?.smallThumbnail == null }
return googleBook.copy(items = ArrayList(filteredItems)) // now a new googleBook item is created with the filtered elements
}
// snippet to adjust then
bookRepository.getBooksFromApi(query)
.map { googleBook -> filterGoogleBookBySmallThumbNail(googleBook) }
//...
Some additional notes / suggestions I have:
I don't see you actually disposing of the subscription of the Observable.
bookRepository.getBooksFromApi(query) If this line returns an Observable, even if you cancel the job, you will be still observing that Observable. If it returns a Single then you are in luck, because after one element it is disposed.
To properly dispose, in cancellation you would have to do something like this(still i would recommend the other two rather, just wanted to note the not disposing):
searchJob = viewModelScope.launch {
val text = query.trim()
if (text.isNotEmpty()) {
val disposable = bookRepository.getBooksFromApi(query)
//...
.subscribe { x ->
//...
}
try {
awaitCancellation() // this actually suspends the coroutine until it is cancelled
} catch (cancellableException: CancellationException) {
disposable.dispose() // this disposes the observable subscription
// that way the coroutine stays alive as long as it's not cancelled, and at that point it actually cleans up the Rx Subscription
}
Seems wasteful that you start a new coroutine job just to do actions
If you want to go the Rx way, you could make the
bookRepository.deleteGoogleBook() and bookRepository.insertGoogleBook(googleBook) Completable, and setup the observable as:
bookRepository.getBooksFromApi(query)
//..
.flatMap {
bookRepository.deleteGoogleBook().andThen(bookRepository.insertGoogleBook(it)).andThen(Observable.just(it))
}
//..subscribeOn
.subscribe()
Seems weird you are mixing coroutine and RX this way
if you don't want to go full Rx, you may consider converting your Observable into a kotlin coroutine Flow, that would be easier to handle with coroutine cancellations and calling suspend functions.
I hope it's helpful
I have a viewmodel that receives flow as livedata from scenario
val state get () = syncScenario.state.asLiveData ()
In the activity, we subscribe to this livedata, some logic happens and used the activityResult
private val resultLauncher = registerForActivityResult (activityResult ()) {result ->
when (result.resultCode) {
Activity.RESULT_OK -> sync()
Activity.RESULT_CANCELED -> return
}
}
when we return, we have an observer triggered with the last state and the previous logic with navigation is performed again
private val syncStateObserver = Observer<StateInfo?> {
it?: return#Observer
when (it) {
is Guest -> doWhenUserIsGuest()
is Authorized -> doWhenUserIsAuthorized()
}
}
How can you ignore an observer trigger with the same value on return?
There is a popular answer for this. You can wrap your StateInfo with SingleEvent class:
open class SingleEvent<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
So your observer looks like below:
private val syncStateObserver = Observer<SingleEvent<StateInfo>> {
it.getContentIfNotHandled()?: return#Observer
when (it.peek()) {
is Guest -> doWhenUserIsGuest()
is Authorized -> doWhenUserIsAuthorized()
}
}
this url is help me - https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150
but doesn't work for livedata.ktx -> liveData{ syncScenario.state.collect { emit(Wrapper(it))} }
I solved this by making a method in which I collect data from the flow and put it in my mutable livedata with wrapper from url