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
Related
I'm building a KMM app for retrieving news.
My app fetches news every 30 seconds and save it in a local database. User must be logged for use it. When user want to logout i need to stop refreshing news and delete the local database.
How do i stop a flow with an infinite loop properly without use static variabile?
I designed the app like follows:
ViewModel (separate for Android and iOS)
UseCase (shared)
Repository (shared)
Data source (shared)
Android Jetpack compose single activity
iOS SwiftUI
Android ViewModel:(iOS use ObservableObject, but logic is the same)
#HiltViewModel
class NewsViewModel #Inject constructor(
private val startFetchingNews: GetNewsUseCase,
private val stopFetchingNews: StopGettingNewsUseCase,
) : ViewModel() {
private val _mutableNewsUiState = MutableStateFlow(NewsState())
val newsUiState: StateFlow<NewsState> get() = _mutableNewsUiState.asStateFlow()
fun onTriggerEvent(action: MapEvents) {
when (action) {
is NewsEvent.GetNews -> {
getNews()
}
is MapEvents.StopNews -> {
//????
}
else -> {
}
}
}
private fun getNews()() {
startFetchingNews().collectCommon(viewModelScope) { result ->
when {
result.error -> {
//update state
}
result.succeeded -> {
//update state
}
}
}
}
}
UseCase:
class GetNewsUseCase(
private val newsRepo: NewsRepoInterface) {
companion object {
private val UPDATE_INTERVAL = 30.seconds
}
operator fun invoke(): CommonFlow<Result<List<News>>> = flow {
while (true) {
emit(Result.loading())
val result = newsRepo.getNews()
if (result.succeeded) {
// emit result
} else {
//emit error
}
delay(UPDATE_INTERVAL)
}
}.asCommonFlow()
}
Repository:
class NewsRepository(
private val sourceNews: SourceNews,
private val cacheNews: CacheNews) : NewsRepoInterface {
override suspend fun getNews(): Result<List<News>> {
val news = sourceNews.fetchNews()
//.....
cacheNews.insert(news) //could be a lot of news
return Result.data(cacheNews.selectAll())
}
}
Flow extension functions:
fun <T> Flow<T>.asCommonFlow(): CommonFlow<T> = CommonFlow(this)
class CommonFlow<T>(private val origin: Flow<T>) : Flow<T> by origin {
fun collectCommon(
coroutineScope: CoroutineScope? = null, // 'viewModelScope' on Android and 'nil' on iOS
callback: (T) -> Unit, // callback on each emission
) {
onEach {
callback(it)
}.launchIn(coroutineScope ?: CoroutineScope(Dispatchers.Main))
}
}
I tried to move the while loop inside repository, so maybe i can break the loop with a singleton repository, but then i must change the getNews method to flow and collect inside GetNewsUseCase (so a flow inside another flow).
Thanks for helping!
When you call launchIn on a Flow, it returns a Job. Hang on to a reference to this Job in a property, and you can call cancel() on it when you want to stop collecting it.
I don't see the point of the CommonFlow class. You could simply write collectCommon as an extension function of Flow directly.
fun <T> Flow<T>.collectCommon(
coroutineScope: CoroutineScope? = null, // 'viewModelScope' on Android and 'nil' on iOS
callback: (T) -> Unit, // callback on each emission
): Job {
return onEach {
callback(it)
}.launchIn(coroutineScope ?: CoroutineScope(Dispatchers.Main))
}
// ...
private var fetchNewsJob: Job? = null
private fun getNews()() {
fetchNewsJob = startFetchingNews().collectCommon(viewModelScope) { result ->
when {
result.error -> {
//update state
}
result.succeeded -> {
//update state
}
}
}
}
In my opinion, collectCommon should be eliminated entirely because all it does is obfuscate your code a little bit. It saves only one line of code at the expense of clarity. It's kind of an antipattern to create a CoroutineScope whose reference you do not keep so you can manage the coroutines running in it--might as well use GlobalScope instead to be clear you don't intend to manage the scope lifecycle so it becomes clear you must manually cancel the Job, not just in the case of the news source change, but also when the UI it's associated with goes out of scope.
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?
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 following project in Github : https://github.com/AliRezaeiii/TVMaze
I have started to using Koin as dependency injection framework in a sample app :
class TVMazeApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this#TVMazeApplication)
modules(networkModule)
modules(persistenceModule)
modules(repositoryModule)
modules(viewModelModule)
}
}
}
This is my repository class :
class ShowRepository(
private val dao: ShowDao,
private val api: TVMazeService,
private val context: Context
) {
/**
* A list of shows that can be shown on the screen.
*/
val shows = resultLiveData(
databaseQuery = {
Transformations.map(dao.getShows()) {
it.asDomainModel()
}
},
networkCall = { refreshShows() })
/**
* Refresh the shows stored in the offline cache.
*/
private suspend fun refreshShows(): Result<List<Show>> =
try {
if (isNetworkAvailable(context)) {
val shows = api.fetchShowList().await()
dao.insertAll(*shows.asDatabaseModel())
Result.success(shows)
} else {
Result.error(context.getString(R.string.failed_internet_msg))
}
} catch (err: HttpException) {
Result.error(context.getString(R.string.failed_loading_msg))
}
}
And my ViewModel :
class MainViewModel(
repository: ShowRepository
) : ViewModel() {
private val _shows = repository.shows
val shows: LiveData<Result<List<Show>>>
get() = _shows
}
And I observe LiveData in my Activity :
viewModel.shows.observe(this, Observer { result ->
when (result.status) {
Result.Status.SUCCESS -> {
binding.loadingSpinner.hide()
viewModelAdapter.submitList(result.data)
}
Result.Status.LOADING -> binding.loadingSpinner.show()
Result.Status.ERROR -> {
binding.loadingSpinner.hide()
Snackbar.make(binding.root, result.message!!, Snackbar.LENGTH_LONG).show()
}
}
})
When I click on Back button, Activity get destroyed ( but instance of app still exist as I can access it from recent apps). What I expect is a call to refreshShows() method when I start the app again, but it never get called.
But when I destroy instance of app by clearing from recent app and start the app, refreshShows() get called.
What should I do to have a call on refreshShows() every time onCreate() callback of Activity get called?
fun <T, A> resultLiveData(databaseQuery: () -> LiveData<T>,
networkCall: suspend () -> Result<A>): LiveData<Result<T>> =
liveData(Dispatchers.IO) {
emit(Result.loading<T>())
val source = databaseQuery.invoke().map { Result.success(it) }
emitSource(source)
val result = networkCall.invoke()
if (result.status == Result.Status.ERROR) {
emit(Result.error<T>(result.message!!))
emitSource(source)
}
}
Your refreshShows() in your repository is only get called when a new network request is done. The idea of your livedata is to provide the latest result when its fragment/activity is recreated, so when your screen rotates or you resume an activity it doesnt triggers another request as the livedata already have the latest result and you dont have a stateful connection with your network database/server (if you were observing data from Room it would receive the latest change if any).
The simpliest way I find to "fix" this, is to actually have your viewmodel val shows to be a fun, like this:
class MainViewModel(
repository: ShowRepository
) : ViewModel() {
private val _shows = repository.shows()
val shows: LiveData<Result<List<Show>>>
get() = _shows
}
However using like this, everytime the screen rotates a new network call will be made thus calling your refreshShows()
I am removing some data within a Room database table. After the data is removed I would like to call a function. The function should not be called until after the data has been removed. The problem is that the function to remove is asynchronous so the function is called before the db is updated.
onEndSession(){
myViewModel.removeAllData()
showSplashScreen()
}
In this example, I want to call showSplashScreen() after the data has been removed.
Create a new Event class like this -
open class Event<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
}
Inside your ViewModel -
private val showSplash = MutableLiveData<Event<Boolean>>()
fun getShowSplash(): LiveData<Event<Boolean>>{
return showSplash
}
// Assuming you are using asynchronous task to remove all data
fun removeAllData(){
// If you are using AsyncTask then in onComplete() call
showSplash.value = true
}
// If you are using Kotlin Co-routines then do this
fun removeAllData(){
viewModelScope.launch(Dispatchers.IO){
//Whatever implementation you have of removeAllData() then put this code
withContext(Dispatchers.Main){
showSplash.value = true
}
}
}
In your activity or fragment where you have showSplashScreen() put this code
mainViewModel.getShowSplash().observe(this, Observer {
it.getContentIfNotHandled()?.let {showSplash ->
if(showSplash != null && showSplash)
showSplashScreen()
}
})
with rxjava you can write this action whit completable like this
//viewModel
fun removeAllData(): Completable {
return Completable.fromAction {
removeData()
}
}
//in Activity
fun startDelete(){
mainViewModel()
.removeAllData()
.observeOn(AndroidMainThread).subscribe(
{
showSplashScreen()
},
{
e(it)
}
)
}