Imagine following scenario:
I open Search View and SearchViewModel is initialized
class SearchViewModel(
usecase: Usecase
) : ViewModel() {
init {
viewModelScope.launch {
usecase.initialize()
}
}
fun search(query: String) = viewModelScope.launch {
usecase.search(query)
}
}
User start typing characters calling search
class UseCase(
private val dataSource: DataSource
private val store: Store
) {
private val searchQueryEmitter = MutableStateFlow<String>("") // 2 change to MutableSharedFlow
private val searchQuery = searchQueryEmitter
.mapLatest { query -> dataSource.search(query) }
.onEach { store.update(it) }
.launchIn(CoroutineScope(Dispatchers.Default)) // 1 comment
suspend fun search(query: String) {
searchQueryEmitter.emit(query)
}
override suspend fun initialize() {
// searchQuery.launchIn(CoroutineScope(Dispatchers.Default)) // > Emit only initial value
// searchQuery.collect() // > Emit only initial value
}
}
Flow emits first item "" and next items according to query value
PROBLEM:
I don't understand if we comment launchIn (1), and call it later in initialize() method, then exactly the same searchQuery.launchIn(...) or searchQuery.collect() cause issue - flow emits only first item "", but calling search with query doesn't trigger emission of next items.
If we change StateFlow to SharedFlow no items will be emitted in any case.
The problem was in ViewModel. I was using two different instances of usecase. The one passed in parameter was used in init while the second one came from import
so the desired case with
override suspend fun initialize() {
searchQuery.collect()
}
is working right now
Related
I have a common situation of getting data. I use the Kotlin Coroutines.
1 variant:
class SomeViewModel(
private val gettingData: GetDataUseCase
) : ViewModel() {
lateinit var data: List<String>
init {
viewModelScope.launch {
data = gettingData.get()
}
}
}
2 variant:
class SomeViewModel(
private val gettingData: GetDataUseCase
) : ViewModel() {
val data = MutableStateFlow<List<String>?>(null)
init {
viewModelScope.launch {
data.emit(gettingData.get())
}
}
}
How can I initialize a data field not delayed, but immediately, with the viewModelScope but without a lateinit or nullble field? And without LiveData, my progect uses Coroutine Flow
I can't return a result of viewModelScope job in .run{} or by lazy {}.
I cant return a result drom fun:
val data: List<String> = getData()
fun getData(): List<String> {
viewModelScope.launch {
data = gettingData.get()
}
return ???
}
Also I can't make suspend fun getData() because I can't create coroutineScope in initialisation'
You're describing an impossibility. Presumably, gettingData.get() is defined as a suspend function, meaning the result literally cannot be retrieved immediately. Since it takes a while to retrieve, you cannot have an immediate value.
This is why apps and websites have loading indicators in their UI.
If you're using Flows, you can use a Flow with a nullable type (like in your option 2 above), and in your Activity/Fragment, in the collector, you show either a loading indicator or your data depending on whether it is null.
Your code 2 can be simplified using the flow builder and stateIn with a null default value:
class SomeViewModel(
private val gettingData: GetDataUseCase
) : ViewModel() {
val data = flow<List<String>?> { emit(gettingData.get()) }
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
}
In your Activity or Fragment:
viewLifecycleOwner.lifecycleScope.launch {
viewModel.data
.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.collect { list ->
if(list == null) {
// Show loading indicator in UI
} else {
// Show the data
}
}
}
If your data loads pretty quickly, instead of making the type nullable, you can just make the default value emptyList(). Then your collector can just not do anything when the list is empty. This works if the data loads quickly enough that the user isn't going to wonder if something is wrong because the screen is blank for so long.
You have to use SharedFlow with replay 1 (to store last value and replay it for a new subscriber) to implement it.
My sample:
interface DataSource {
suspend fun getData(): Int
}
class DataViewModel(dataSource: DataSource): ViewModel() {
val dataField =
flow<Int> {
emit(dataSource.getData())
}.shareIn(viewModelScope, SharingStarted.WhileSubscribed(1000), 1)
}
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.
I want to debounce the items sent to a shared flow, and consume them after that. Something like this:
private var flow = MutableSharedFlow()
suspend fun search(query: String): Flow<Result> {
flow.emit(query)
return flow.debounce(1000).map{ executeSearch(it) }
}
The event that initiates the search is a user writing on a field. For each character, the search function is called. So I want to get a debounced result, to avoid many queries to the server.
It looks like the debounce operator returns a different flow instance each time, so that all the queries end up invoking the executeSearch() function, without dropping any of them as you could expect by using a debounce operator. How can I achieve a functionality like this, so that a client can invoke a function that returns a flow with debounced results?
You can try something like this:
private var flow = MutableSharedFlow()
init {
flow.debounce(1000)
.collect {
val result = executeSearch(it)
// Process the result (maybe send to the UI)
}
}
suspend fun search(query: String) {
flow.emit(query)
}
With two flows you could do it like this. One backing flow takes all the search inputs, and the second is a debounce version of it that runs the query. The search function doesn’t return a flow because the Flow is already available as a property and we aren’t creating new ones for each input.
private val searchInput = MutableSharedFlow<String>()
val searchResults = searchInput.debounce(1000)
.map { executeSearch(it) }
.shareIn(viewModelScope, SharingStarted.Eagerly)
fun submitSearchInput(query: String) {
searchInput.tryEmit(query)
}
You could alternatively do it with jobs that you extinguish when new inputs come in:
private val searchJob: Job? = null
private val _searchResults = MutableSharedFlow<SearchResultType>()
val searchResults = _searchResults.asSharedFlow()
fun submitSearchInput(query: String) {
searchJob?.cancel()
searchJob = viewModelScope.launch {
delay(1000)
_searchResults.emit(executeSearch(query))
}
}
I am trying to write tests for my Repository which provides access to my Room database.
For this, I wrote a Mock Database and a mock DAO:
My Database:
abstract class JoozdlogDatabase protected constructor(): RoomDatabase() {
abstract fun aircraftTypeDao(): AircraftTypeDao
// (...)
}
class MockDatabase: JoozdlogDatabase() {
override fun aircraftTypeDao(): AircraftTypeDao = MockAircraftTypeDao()
}
My DAO:
interface AircraftTypeDao {
#Query("SELECT * FROM AircraftTypeData")
suspend fun requestAllAircraftTypes(): List<AircraftTypeData>
#Query("SELECT * FROM AircraftTypeData")
fun aircraftTypesFlow(): Flow<List<AircraftTypeData>>
// etc etc //
}
class MockAircraftTypeDao: AircraftTypeDao {
private val simulatedDatabase = ArrayList<AircraftTypeData>()
private val simulatedFlow = MutableStateFlow<List<AircraftTypeData>>(listOf(AircraftTypeData("aap", "noot", false, multiEngine = false)))
override suspend fun requestAllAircraftTypes(): List<AircraftTypeData> = simulatedDatabase
override fun aircraftTypesFlow(): Flow<List<AircraftTypeData>> = simulatedFlow
override suspend fun save(vararg aircraftTypeData: AircraftTypeData) {
//println("${this::class.simpleName} Saving ${aircraftTypeData.size} type data")
simulatedDatabase.addAll(aircraftTypeData)
emit()
}
override suspend fun clearDb() {
println("${this::class.simpleName} Clear DB")
simulatedDatabase.clear()
emit()
}
private fun emit(){
println("emit() should emit ${simulatedDatabase.size} items")
simulatedFlow.update { simulatedDatabase.toList() } // also tried: simulatedFlow.value = simulatedDatabase.toList()
println("simulatedFlow.value is now ${simulatedFlow.value.size}")
}
My Test data:
object TestData {
val aircraftTypes = listOf(
AircraftType("Test Aircraft 1 (MP/ME)", "TAC1", multiPilot = true, multiEngine = true),
AircraftType("Test Aircraft 2 (SP/SE)", "TAC2", multiPilot = false, multiEngine = false)
)
}
and my test:
#Test
fun test() {
runTest {
var currentTypesList: List<AircraftType> = emptyList()
val aircraftRepository = AircraftRepository.mock(MockDatabase())
// DispatcherProvider.default() provides UnconfinedTestDispatcher(TestCoroutineScheduler()) for my test.
launch(DispatcherProvider.default()) {
aircraftRepository.aircraftTypesFlow.collect {
println("emitted ${it.size} flights: $it")
currentTypesList = it
}
}
aircraftRepository.replaceAllTypesWith(TestData.aircraftTypes)
delay(500)
println("Done waiting")
assertEquals (2, currentTypesList.size)
}
}
Expected result: Test passed.
received result: java.lang.AssertionError: expected:<2> but was:<1> for the single assert
received output:
emitted 1 flights: [AircraftType(name=aap, shortName=noot, multiPilot=false, multiEngine=false)]
MockAircraftTypeDao Clear DB
emit() should emit 0 items
simulatedFlow.value is now 0
emit() should emit 2 items
simulatedFlow.value is now 2
Done waiting
Now, I have been at this all morning and I just don't get why it won't collect anything but the first value.
Things I tried:
Making a flow object to test my collector -> collector is OK
Accessing the flow item in DAO directly -> Does not work
Setting value of MutableStateFlow with update and with value = -> neither works.
Making a different flow object the is exactly the same but not called from a suspend function: Works.
So, I guess something about the calling suspend function is doing something wrong, but the Flow object is being updated before the delay is over, and it just won't collect.
If anybody is much smarter than me and can explain what I am doing wrong, I would very much appreciate it.
I fixed this by using the suggestion posted here and switching to turbine for all my flow testing needs.
I'm using LiveData's version "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha05". Once my LiveData block executes successfully I want to explicitly trigger it to execute again, e.g.
I navigate to a fragment
User's data loads
I click delete btn while being in the same fragment
User's data should refresh
I have a fragment where I observe my LiveData, a ViewModel with LiveData and Repository:
ViewModel:
fun getUserLiveData() = liveData(Dispatchers.IO) {
val userData = usersRepo.getUser(userId)
emit(userData)
}
Fragment:
viewModel.getUserLiveData.observe(viewLifecycleOwner,
androidx.lifecycle.Observer {..
Then I'm trying to achieve desired behaviour like this:
viewModel.deleteUser()
viewModel.getUserLiveData()
According to the documentation below LiveData block won't execute if it has completed successfully and if I put a while(true) inside the LiveData block, then my data refreshes, however I don't want this to do since I need to update my view reactively.
If the [block] completes successfully or is cancelled due to reasons other than [LiveData]
becoming inactive, it will not be re-executed even after [LiveData] goes through active
inactive cycle.
Perhaps I'm missing something how I can reuse the same LiveDataScope to achieve this? Any help would be appreciated.
To do this with liveData { .. } block you need to define some source of commands and then subscribe to them in a block. Example:
MyViewModel() : ViewModel() {
val commandsChannel = Channel<Command>()
val liveData = livedata {
commandsChannel.consumeEach { command ->
// you could have different kind of commands
//or emit just Unit to notify, that refresh is needed
val newData = getSomeNewData()
emit(newData)
}
}
fun deleteUser() {
.... // delete user
commandsChannel.send(RefreshUsersListCommand)
}
}
Question you should ask yourself: Maybe it would be easier to use ordinary MutableLiveData instead, and mutate its value by yourself?
livedata { ... } builder works well, when you can collect some stream of data (like a Flow / Flowable from Room DB) and not so well for plain, non stream sources, which you need to ask for data by yourself.
I found a solution for this. We can use switchMap to call the LiveDataScope manually.
First, let see the official example for switchMap:
/**
* Here is an example class that holds a typed-in name of a user
* `String` (such as from an `EditText`) in a [MutableLiveData] and
* returns a `LiveData` containing a List of `User` objects for users that have
* that name. It populates that `LiveData` by requerying a repository-pattern object
* each time the typed name changes.
* <p>
* This `ViewModel` would permit the observing UI to update "live" as the user ID text
* changes.
**/
class UserViewModel: AndroidViewModel {
val nameQueryLiveData : MutableLiveData<String> = ...
fun usersWithNameLiveData(): LiveData<List<String>> = nameQueryLiveData.switchMap {
name -> myDataSource.usersWithNameLiveData(name)
}
fun setNameQuery(val name: String) {
this.nameQueryLiveData.value = name;
}
}
The example was very clear. We just need to change nameQueryLiveData to your own type and then combine it with LiveDataScope. Such as:
class UserViewModel: AndroidViewModel {
val _action : MutableLiveData<NetworkAction> = ...
fun usersWithNameLiveData(): LiveData<List<String>> = _action.switchMap {
action -> liveData(Dispatchers.IO){
when (action) {
Init -> {
// first network request or fragment reusing
// check cache or something you saved.
val cache = getCache()
if (cache == null) {
// real fecth data from network
cache = repo.loadData()
}
saveCache(cache)
emit(cache)
}
Reload -> {
val ret = repo.loadData()
saveCache(ret)
emit(ret)
}
}
}
}
// call this in activity, fragment or any view
fun fetchData(ac: NetworkAction) {
this._action.value = ac;
}
sealed class NetworkAction{
object Init:NetworkAction()
object Reload:NetworkAction()
}
}
First add implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" to your gradle file. Make your ViewModel as follows:
MyViewModel() : ViewModel() {
val userList = MutableLiveData<MutableList<User>>()
fun getUserList() {
viewModelScope.launch {
userList.postValue(usersRepo.getUser(userId))
}
}
}
Then onserve the userList:
viewModel.sessionChartData.observe(viewLifecycleOwner, Observer { users ->
// Do whatever you want with "users" data
})
Make an extension to delete single user from userList and get notified:
fun <T> MutableLiveData<MutableList<T>>.removeItemAt(index: Int) {
if (!this.value.isNullOrEmpty()) {
val oldValue = this.value
oldValue?.removeAt(index)
this.value = oldValue
} else {
this.value = mutableListOf()
}
}
Call that extension function to delete any user and you will be notified in your Observer block after one user get deleted.
viewModel.userList.removeItemAt(5) // Index 5
When you want to get userList from data source just call viewModel.getUserList() You will get data to the observer block.
private val usersLiveData = liveData(Dispatchers.IO) {
val retrievedUsers = MyApplication.moodle.getEnrolledUsersCoroutine(course)
repo.users = retrievedUsers
roles.postValue(repo.findRolesByAll())
emit(retrievedUsers)
}
init {
usersMediator.addSource(usersLiveData){ usersMediator.value = it }
}
fun refreshUsers() {
usersMediator.removeSource(usersLiveData)
usersMediator.addSource(usersLiveData) { usersMediator.value = it }
The commands in liveData block {} doesn't get executed again.
Okay yes, the observer in the viewmodel holding activity get's triggered, but with old data.
No further network call.
Sad. Very sad. "Solution" seemed promisingly and less boilerplaty compared to the other suggestions with Channel and SwitchMap mechanisms.
You can use MediatorLiveData for this.
The following is a gist of how you may be able to achieve this.
class YourViewModel : ViewModel() {
val mediatorLiveData = MediatorLiveData<String>()
private val liveData = liveData<String> { }
init {
mediatorLiveData.addSource(liveData){mediatorLiveData.value = it}
}
fun refresh() {
mediatorLiveData.removeSource(liveData)
mediatorLiveData.addSource(liveData) {mediatorLiveData.value = it}
}
}
Expose mediatorLiveData to your View and observe() the same, call refresh() when your user is deleted and the rest should work as is.