Observe StateFlow emission in ViewModel initialization - android

I have a view model that takes in an initial ViewState object and has a publicly accessible state variable which can be collected.
class MyViewModel<ViewState>(initialState: ViewState) : ViewModel() {
val state: StateFlow<ViewState> = MutableStateFlow(initialState)
val errorFlow: SharedFlow<String> = MutableSharedFlow()
init {
performNetworkCall()
}
private fun performNetworkCall() = viewModelScope.launch {
Network.makeCall(
"/someEndpoint",
onSuccess = {
(state as MutableStateFlow).tryEmit(<some new state>)
},
onError = {
(errorFlow as MutableSharedFlow).tryEmit("network failure")
}
)
}
}
When observing this state from a fragment, I can see the initial state (loading for example) and I collect the change when the network call completes successfully (for example, to a loaded state.)
However, I am at a loss as to how to observe this emission from my ViewModelUnitTest.
I use kotlin turbine to test emissions for my state and shared flows, but I can only observe emissions that occur after I call viewModel.state.test or viewModel.errorFlow.test.
Since I cannot reference viewModel.state or viewModel.errorFlow prior to initialization of the ViewModel, how can I write a test to validate that my initialization logic performs correctly and emits the expected result based on the mocked behavior of Network.makeCall - whether it be a new emission of state or an errorFlow emission?

Since your network class doesn't have any reference in the view model, it isn't possible to mock/fake Network class' behavior. So, create a reference for Network. To capture what's going on in the init block, you can lazy initialize your view model in the test class. It is described in here as the second way. Roughly, your classes look similar to:
class MyViewModel<ViewState>(
initialState: ViewState,
network: Network
) : ViewModel() {
val state: StateFlow<ViewState> = MutableStateFlow(initialState)
val errorFlow: SharedFlow<String> = MutableSharedFlow()
init {
performNetworkCall()
}
private fun performNetworkCall() = viewModelScope.launch {
network.makeCall(
"/someEndpoint",
onSuccess = {
(state as MutableStateFlow).tryEmit(<some new state>)
},
onError = {
(errorFlow as MutableSharedFlow).tryEmit("network failure")
}
)
}
}
#ExperimentalCoroutinesApi
class MyViewModelTest {
#get:Rule
var coroutinesTestRule = CoroutinesTestRule()
val viewModel by lazy { MyViewModel(/*pass arguments*/) }
}
CoroutineTestRule can be found in here.

Related

Test Android Compose State values

I've implemented Android view model which exposes Compose State<T> instance. For instance, view model implementation:
class MyViewModel<S> {
private val _viewState: MutableState<S> = mutableStateOf(initialState)
val viewState: State<S> = _viewState
...
protected fun setState(newState: S) {
_viewState.value = newState
}
}
I'd like to test in unit tests what values/state it will get set. Just a brief example of what I'd like to achieve:
class MyViewModelTest {
#Test
fun `when view model initialized then should emit initial state first`() {
val viewModel = MyViewModel()
assertEquals(InitialState(), viewModel.viewState.value)
}
#Test
fun `when view model interacted then should emit result state`() {
val viewModel = MyViewModel()
val expectedState = NewState()
viewModel.setState(expectedState)
assertEquals(expectedState, viewModel.viewState.value)
}
}
Is it possible to test State<T>? How do you guys unit test compose states values if you store them in view model side?
For my specific use case, I'm using coroutines and unidirectional data flow, but exposing state via mutableStateOf. Was looking to Unit Test initial state + effect of events as is your case.
View Model:
class MyViewModel(
// For injecting test dispatcher
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) : ViewModel() {
// State exposed via delegate
var state by mutableStateOf(State.Show())
private set
fun onEvent(event: Event) {
when (event) {
is Event.Greet -> greet(event)
else -> TODO("Other Stuff")
}
}
private fun greet(greet: Event.Greet) {
viewModelScope.launch(dispatcher) {
// Do some, ya know, API stuff!
state = State.Show("Hello ${greet.name}")
}
}
}
state is exposed via delegate, with a private setter. The initial value would be Show("Hello"). We can also set updates directly with the imports:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
The injection of the dispatcher is essential for controlling the progression.
States & Events, for completeness
// Encapsulates possible states
sealed class State {
// Data class calculates equality from the constructor
// aiding with assertEquals
data class Show(
val greeting: String = "Hello"
) : State()
}
// ... and possible Events
sealed class Event {
data class Greet(val name: String) : Event()
}
With this setup, we can get to the unit testing:
#ExperimentalCoroutinesApi
#RunWith(MockitoJUnitRunner::class)
class MyViewModelTest {
val dispatcher = StandardTestDispatcher()
#Test
fun `viewModel shows initial state`() = runTest {
val vm = MyViewModel(dispatcher)
val expectedState = State.Show()
// Get the initial state
val state = vm.state
assertEquals(expectedState, state)
}
#Test
fun `viewModel shows updated state`() = runTest {
val vm = MyViewModel(dispatcher)
val expectedState = State.Show("Hello Robertus")
// Trigger an event
vm.onEvent(Event.Greet("Robertus"))
// Await the change
dispatcher.scheduler.advanceUntilIdle()
// Get the state
val state = vm.state
assertEquals(expectedState, state)
}
}
The val dispatcher = StandardTestDispatcher() lets us pass in a dispatcher we can control for awaiting the coroutine. We pass this to the ViewModel in the tests and when needing to await them to run and update the state.
Note: the tests are within the expression = runTest { }.
For the first test, viewModel shows initial state, we get the initial value the mutableStatOf was initialized with.
In the second test, viewModel shows updated state, we await the coroutine to complete (dispatcher to idle) by calling dispatcher.scheduler.advanceUntilIdle() between passing the Event and getting the state.
Without it, we will get the initial state.

How can I update the flow source data in android coroutine?

I am developing an android app using Coroutine.
Because I have an experience with the Rx, I can understand Coroutine Flow's concept.
But I don't know about emitting new data to the existing flow.
In Rx, I can use BehaviorSubject and it's onNext() function.
Producer:
class ProfileRepository(
private val profileDao: ProfileDao
) {
init {
val profile = profileDao.getMyProfile()
myProfile.onNext(profile)
}
val myProfile: PublishSubject<Profile> = BehaviorSubject.create()
fun updateMyProfile(profile: Profile) {
myProfile.onNext(profile)
}
}
Subscriber:
class ProfileViewModel(
private val profileRepo: ProfileRepository
) {
val myProfile = MutableLiveData<Profile>()
fun loadMyProfile() {
profileRepo.subscribe {
myProfile.postValue(it)
}
}
}
And other UI (like Dialog) can update the source data like:
class SignInDialog(
private val profileRepo: ProfileRepository
) {
fun onSignInSuccess() {
profileRepo.updateMyProfile(profile)
}
}
I want to do this using Coroutine Flow, Is this possible on Flow?
You are using BehaviorSubject, which emits the most recently item at the time of subscription and then continues the sequence until complete. So basically it is a hot stream of data.
The similar behavior has SharedFlow:
A hotFlow that shares emitted values among all its collectors in a broadcast fashion, so that all collectors get all emitted values. A shared flow is called hot because its active instance exists independently of the presence of collectors.
For your case it will look something like the following:
class ProfileRepository(
private val profileDao: ProfileDao
) {
init {
val profile = profileDao.getMyProfile()
_myProfile.tryEmit(profile)
}
private val _myProfile = MutableSharedFlow<String>(replay = 1, extraBufferCapacity = 64)
val myProfile: SharedFlow<Profile> = _myProfile
fun updateMyProfile(profile: Profile) {
_myProfile.tryEmit(profile)
}
}
class ProfileViewModel(
private val profileRepo: ProfileRepository
) {
val myProfile = MutableLiveData<Profile>()
fun loadMyProfile() = viewModelScope.launch {
profileRepo.myProfile.collect {
myProfile.postValue(it)
}
}
}
viewModelScope - Any coroutine launched in this scope is automatically canceled if the ViewModel is cleared. For ViewModelScope, use
androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0 or higher.

Transformed LiveData's value is always null in unit testing even when observed, but works fine in the UI

I want to test a LiveData's value. This LiveData's value depends on another LiveData in the ViewModel. When I run the app, the UI is working as intended. However, when I create a unit test to test the dependent LiveData, the test always fails with an AssertionError saying that the value is always null.
I have the following LiveData in my ViewModel.
class MovieViewModel #ViewModelInject constructor(
private val movieRepository: MovieRepository
) : ViewModel() {
private val state = movieRepository.getMoviesState()
val errorLoading: LiveData<Boolean>
get() = Transformations.map(state) { it == State.ERROR }
val isLoading: LiveData<Boolean>
get() = Transformations.map(state) { it == State.LOADING }
...
}
The LiveData I want to test are errorLoading and isLoading. To do that, I use Mockk to mock objects and have the following code to test them both.
#Test
fun ensure_isLoading_is_true_when_state_is_loading() {
...
every { movieRepository.getMoviesState() } returns MutableLiveData(State.LOADING)
every { booleanObserver.onChanged(any()) } just runs
viewmodel = MovieViewModel(movieRepository)
verify { movieRepository.getMoviesState() }
viewmodel.isLoading.observeForever(booleanObserver)
verify { booleanObserver.onChanged(true) }
assertEquals(true, viewmodel.isLoading.value)
viewmodel.isLoading.removeObserver(booleanObserver)
}
So far, the test can verify that onChanged on the mocked Observer is called and the value of the new change is also correct. However, when I want to access the value of the LiveData I want to test, it always returns null.
I am aware that the LiveData needs to be observed and I have made a mock Observer to do that. However, the test still fails. Why does it always return null?
Simply use this :
#Rule
public InstantTaskExecutorRule instantTaskExecutorRule = new
InstantTaskExecutorRule();
I had the same issue and discovered that the problem is with backing property.
When you use it with get() like this:
val isLoading: LiveData<Boolean>
get() = Transformations.map(state) { it == State.LOADING }
then when calling isLoading you get a new (empty) LiveData instance.
Simply get rid of backing property and use it that way:
val _isLoading = MutableLiveData<Boolean>
val isLoading: LiveData<Boolean> = Transformations.map(state) { it == State.LOADING }

liveData with coroutines only trigger first time

I have a usecase:
Open app + disable network -> display error
Exit app, then enable network, then open app again
Expected: app load data
Actual: app display error that meaning state error cached, liveData is not emit
Repository class
class CategoryRepository(
private val api: ApiService,
private val dao: CategoryDao
) {
val categories: LiveData<Resource<List<Category>>> = liveData {
emit(Resource.loading(null))
try {
val data = api.getCategories().result
dao.insert(data)
emit(Resource.success(data))
} catch (e: Exception) {
val data = dao.getCategories().value
if (!data.isNullOrEmpty()) {
emit(Resource.success(data))
} else {
val ex = handleException(e)
emit(Resource.error(ex, null))
}
}
}
}
ViewModel class
class CategoryListViewModel(
private val repository: CategoryRepository
): ViewModel() {
val categories = repository.categories
}
Fragment class where LiveDate obsever
viewModel.apply {
categories.observe(viewLifecycleOwner, Observer {
// live data only trigger first time, when exit app then open again, live data not trigger
})
}
can you help me explain why live data not trigger in this usecase and how to fix? Thankyou so much
Update
I have resolved the above problem by replace val categories by func categories() at repository class. However, I don't understand and can't explain why it works properly with func but not val.
Why does this happen? This happens because your ViewModel has not been killed yet. The ViewModel on cleared() is called when the Fragment is destroyed. In your case your app is not killed and LiveData would just emit the latest event already set. I don't think this is a case to use liveData builder. Just execute the method in the ViewModel when your Fragment gets in onResume():
override fun onResume(){
viewModel.checkData()
super.onResume()
}
// in the viewmodel
fun checkData(){
_yourMutableLiveData.value = Resource.loading(null)
try {
val data = repository.getCategories()
repository.insert(data)
_yourMutableLiveData.value = Resource.success(data)
} catch (e: Exception) {
val data = repository.getCategories()
if (!data.isNullOrEmpty()) {
_yourMutableLiveData.value = Resource.success(data)
} else {
val ex = handleException(e)
_yourMutableLiveData.value = Resource.error(ex,null)
}
}
}
Not sure if that would work, but you can try to add the listener directly in onResume() but careful with the instantiation of the ViewModel.
Small advice, if you don't need a value like in Resource.loading(null) just use a sealed class with object
UPDATE
Regarding your question that you ask why it works with a function and not with a variable, if you call that method in onResume it will get executed again. That's the difference. Check the Fragment or Activity lifecycle before jumping to the ViewModel stuff.

Making stateful components in Android

I am using MVVM in my app. When you enter a query and click search button, the chain is as follows: Fragment -> ViewModel -> Repository -> API -> Client. The client is where HTTP requests are made. But there is one thing here, the client needs to make a call and get a key from the server at initialization. Therefore, to prevent any call before it this first call completes, I need to be able to observe it from Fragment so that I can disable search button. Since each component in the chain can communicate with adjacent components, all components should have a state.
I am thinking to implement a StatefulComponent class and make all components to extend it:
open class StatefulComponent protected constructor() {
enum class State {
CREATED, LOADING, LOADED, FAILED
}
private val currentState = MutableLiveData(State.CREATED)
fun setState(newState: State) {
currentState.value = newState
}
val state: LiveData<State> = currentState
val isLoaded: Boolean = currentState.value == State.LOADED
val isFailed: Boolean = currentState.value == State.FAILED
val isCompleted: Boolean = isLoaded || isFailed
}
The idea is that each component observers the next one and updates itself accordingly. However, this is not possible for ViewModel since it is already extending ViewModel super class.
How can I implement a solution for this problem?
The most common approach is to use sealed class as your state, so you have any paramaters as you want on each state case.
sealed class MyState {
object Loading : MyState()
data class Loaded(data: Data) : MyState()
data class Failed(message: String) : MyState()
}
On your viewmodel you will have only 1 livedata
class MyViewModel : ViewModel() {
private val _state = MutableLiveData<MyState>()
val state: LiveData<MyState> = _state
fun load() {
_state.postCall(Loading)
repo.loadSomeData(onData = { data ->
_state.postCall(Loaded(data))
}, onError = { error -> _state.postCall(Failed(error.message)) })
}
// coroutines approach
suspend fun loadSuspend() {
_state.postCall(Loading)
try {
_state.postCall(Loaded(repo.loadSomeDataSupend()))
} catch(e: Exception) {
_state.postCall(Failed(e.message))
}
}
}
And on the fragment, just observe the state
class MyFragment : Fragment() {
...
onViewCreated() {
viewModel.state.observer(Observer {
when (state) {
// auto casts to each state
Loading -> { button.isEnabled = false }
is Loaded -> { ... }
is Failed -> { ... }
}
}
)
}
}
As João Gouveia mentioned, we can make stateful components quite easily using kotlin's sealed classes.
But to make it further more useful, we can introduce Generics! So, our state class becomes StatefulData<T> which you can use pretty much anywhere (LiveData, Flows, or even in Callbacks).
sealed class StatefulData<out T : Any> {
data class Success<T : Any>(val result : T) : StatefulData<T>()
data class Error(val msg : String) : StatefulData<Nothing>()
object Loading : StatefulData<Nothing>()
}
I've wrote an article fully explaining this particular implementation here
https://naingaungluu.medium.com/stateful-data-on-android-with-sealed-classes-and-kotlin-flow-33e2537ccf55
If you are using the composable ... You can use produce state
#Composable
fun PokemonDetailScreen(
viewModel: PokemonDetailVm = hiltViewModel()
) {
/**
* This takes a initial state and with that we get a coroutine scope where we can call a API and assign the data into the value
*/
val pokemonInfo = produceState<Resource<Pokemon>>(initialValue = Resource.Loading()) {
value = viewModel.getPokemonInfo(pokemonName)
}.value
}

Categories

Resources