This init block is in my ViewModel:
init {
viewModelScope.launch {
userRepository.login()
userRepository.user.collect {
_uiState.value = UiState.Success(it)
}
}
}
This is very similar to what's actually written on the app, but even this simple example doesn't work. After userRepository.login(), user which is a SharedFlow emits a new user state. This latest value DOES get collected within this collect function shown above, but when emitting a new uiState containing the result, the view does not get such update.
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
Doing this for some reason, does not work. I suspect the issue is related to the lifecycle of the viewmodel, because when I treat the viewmodel as a singleton, this doesn't happen. It happens only when the viewmodel gets destroyed and then created a 2nd (or more) time(s).
What I'm trying to achieve is that the screen containing the view model is aware of the user state. Meaning that when I navigate to the screen, I want it to collect the latest user state, and then decide which content to show.
I also realize this is not the best pattern, most likely. I'm currently looking into a solution that holds the User as part of the app state and collecting per screen (given that it basically changes all or many screens and functionalities) so if you have any resources on an example on such implementation I'd be thankful. But I can't get my head around why this current implementation doesn't work so any light shed on the situation is much appreciated.
EDIT
This is what I have in mind for the repository
private val _user = MutableSharedFlow<User>()
override val user: Flow<User> = _user
override suspend fun login() {
delay(2000)
_user.emit(LoggedUser.aLoggedUser())
}
override suspend fun logout() {
delay(2000)
_user.emit(GuestUser)
}
For your case better to use this pattern:
ViewModel class:
sealed interface UserUiState {
object NotLoggedIn : UserUiState
object Error : UserUiState
data class LoggedIn(val user: User) : UserUiState
}
class MyViewModel #Inject constructor(
userRepository: UserRepository
) : ViewModel() {
val userUiState = userRepository.login()
.map { user ->
if (user != null)
UserUiState.LoggedIn(user)
else
UserUiState.Error
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = UserUiState.NotLoggedIn
)
}
Repository class:
class UserRepository {
fun login(): Flow<User?> = flow {
val user = TODO("Your code to get user")
if (isSuccess) {
emit(user)
} else {
emit(null)
}
}
}
Your screen Composable:
#Composable
fun Screen() {
val userUiState by viewModel.userUiState.collectAsStateWithLifecycle()
when (userUiState) {
is UserUiState.LoggedIn -> { TODO("Success code") }
UserUiState.NotLoggedIn -> { TODO("Waiting for login code") }
UserUiState.Error -> { TODO("Error display code") }
}
}
How it works: login() in repository returns autorized user flow which can be used in ViewModel. I use UserUiState sealed class to handle possible user states. And then I convert User value in map {} to UserUiState to display it in the UI Layer. Then Flow of UserUiState needs to be converted to StateFlow to obtain it from the Composable function, so I made stateIn.
And of course, this will solve your problem
Tell me in the comments if I got something wrong or if the code does not meet your expectations
Note: SharedFlow and StateFlow are not used in the Data Layer like you do.
EDIT:
You can emiting flow like this if you are working with network:
val user = flow of {
while (true) {
// network call to get user
delay(2000)
}
}
If you use Room you can do this in your dao.
#Query(TODO("get actual user query"))
fun getUser(): Flow<User>
It is a better way and it recommended by android developers YouTube channel
Related
A have a screen where I display 10 users. Each user is represented by a document in Firestore. On user click, I need to get its details. This is what I have tried:
fun getUserDetails(uid: String) {
LaunchedEffect(uid) {
viewModel.getUser(uid)
}
when(val userResult = viewModel.userResult) {
is Result.Loading -> CircularProgressIndicator()
is Result.Success -> Log.d("TAG", "You requested ${userResult.data.name}")
is Result.Failure -> Log.d("TAG", userResult.e.message)
}
}
Inside the ViewModel class, I have this code:
var userResult by mutableStateOf<Result<User>>(Result.Loading)
private set
fun getUser(uid: String) = viewModelScope.launch {
repo.getUser(uid).collect { result ->
userResult = result
}
}
As you see, I use Result.Loading as a default value, because the document is heavy, and it takes time to download it. So I decided to display a progress bar. Inside the repo class I do:
override fun getUser(uid: String) = flow {
try {
emit(Result.Loading)
val user = usersRef.document(uid).get().await().toObject(User::class.java)
emit(Result.Success(user))
} catch (e: Exception) {
emit(Result.Failure(e))
}
}
I have two questions, if I may.
Is there something wrong with this code? As it works fine when I compile.
I saw some questions here, that recommend using collectAsState() or .collectAsStateWithLifecycle(). I tried changing userResult.collectAsState() but I cannot find that function. Is there any benefit in using collectAsState() or .collectAsStateWithLifecycle() than in my actual code? I'm really confused.
If you wish to follow Uncle Bob's clean architecture you can split your architecture into Data, Domain and Presentation layers.
For android image below shows how that onion shape can be simplified to
You emit your result from Repository and handle states or change data, if you Domain Driven Model, you store DTOs for data from REST api, if you have db you keep database classes instead of passing classes annotated with REST api annotation or db annotation to UI you pass a UI.
In repository you can pass data as
override fun getUser(uid: String) = flow {
val user usersRef.document(uid).get().await().toObject(User::class.java)
emit(user)
}
In UseCase you check if this returns error, or your User and then convert this to a Result or a class that returns error or success here. You can also change User data do Address for instance if your business logic requires you to return an address.
If you apply business logic inside UseCase you can unit test what you should return if you retrieve data successfully or in case error or any data manipulation happens without error without using anything related to Android. You can just take this java/kotlin class and unit test anywhere not only in Android studio.
In ViewModel after getting a Flow< Result<User>> you can pass this to Composable UI.
Since Compose requires a State to trigger recomposition you can convert your Flow with collectAsState to State and trigger recomposition with required data.
CollectAsState is nothing other than Composable function produceState
#Composable
fun <T : R, R> Flow<T>.collectAsState(
initial: R,
context: CoroutineContext = EmptyCoroutineContext
): State<R> = produceState(initial, this, context) {
if (context == EmptyCoroutineContext) {
collect { value = it }
} else withContext(context) {
collect { value = it }
}
}
And produceState
#Composable
fun <T> produceState(
initialValue: T,
key1: Any?,
key2: Any?,
#BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
val result = remember { mutableStateOf(initialValue) }
LaunchedEffect(key1, key2) {
ProduceStateScopeImpl(result, coroutineContext).producer()
}
return result
}
As per discussion in comments, you can try this approach:
// Repository
suspend fun getUser(uid: String): Result<User> {
return try {
val user = usersRef.document(uid).get().await().toObject(User::class.java)
Result.Success(user)
} catch (e: Exception) {
Result.Failure(e)
}
}
// ViewModel
var userResult by mutableStateOf<Result<User>?>(null)
private set
fun getUser(uid: String) {
viewModelScope.launch {
userResult = Result.Loading // set initial Loading state
userResult = repository.getUser(uid) // update the state again on receiving the response
}
}
I'm trying to show a user information in DetailActivity. So, I request a data and get a data for the user from server. but in this case, the return type is Flow<User>. Let me show you the following code.
ServiceApi.kt
#GET("endpoint")
suspend fun getUser(#Query("id") id: Int): Response<User>
Repository.kt
fun getUser(id: Int): Flow<User> = flow<User> {
val userResponse = api.getUser(id = id)
if (userResponse.isSuccessful) {
val user = userResponse.body()
emit(user)
}
}
.flowOn(Dispatchers.IO)
.catch { // send error }
DetailViewModel.kt
class DetailViewModel(
private val repository : Repository
) {
val uiState: StateFlow<User> = repository.getUser(id = 369).stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = User() // empty user
)
}
DetailActivity.kt
class DetailActivity: AppCompatActivity() {
....
initObersevers() {
lifecycleScope.launch {
// i used the `flowWithLifecycle` because the data is just a single object.
viewModel.uiState.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect { state ->
// show data
}
}
}
...
}
But, all of sudden, I just realized that this process is just an one-shot operation and thought i can use suspend function and return User in Repository.kt.
So, i changed the Repository.kt.
Repository.kt(changed)
suspend fun getUser(id: Int): User {
val userResponse = api.getUser(id = id)
return if(userResponse.isSuccessful) {
response.body()
} else {
User() // empty user
}
}
And in DetailViewModel, i want to convert the User into StateFlow<User> because of observing from DetailActivity and I'm going to use it the same way as before by using flowWithLifecycle.
the concept is... i thought it's just one single data and i dind't need to use Flow in Repository. because it's not several items like List.
is this way correct or not??
Yeap, this one-time flow doesn't make any sense - it emits only once and that's it.
You've got two different ways. First - is to create a state flow in your repo and emit there any values each time you're doing your GET request. This flow will be exposed to the use case and VM levels. I would say that it leads to more difficult error handling (I'm not fond of this way, but these things are always arguable, haha), but it also has some pros like caching, you can always show/get the previous results.
Second way is to leave your request as a simple suspend function which sends a request, returns the result of it back to your VM (skipping error handling here to be simple):
val userFlow: Flow<User>
get() = _userFlow
private val _userFlow = MutableStateFlow(User())
fun getUser() = launch(viewModelScope) {
_userFlow.value = repository.getUser()
}
This kind of implementation doesn't provide any cache out of scope of this VM's lifecycle, but it's easy to test and use.
So it's not like there is only one "the-coolest-way-to-do-it", it's rather a question what suits you more for your needs.
My repo has the following function:
override fun getTopRatedMoviesStream(): Flow<List<Movie>>
I have the following Result wrapper:
sealed interface Result<out T> {
data class Success<T>(val data: T) : Result<T>
data class Error(val exception: Throwable? = null) : Result<Nothing>
object Loading : Result<Nothing>
}
fun <T> Flow<T>.asResult(): Flow<Result<T>> {
return this
.map<T, Result<T>> {
Result.Success(it)
}
.onStart { emit(Result.Loading) }
.catch { emit(Result.Error(it)) }
}
And finally, my ViewModel has the following UiState logic:
data class HomeUiState(
val topRatedMovies: TopRatedMoviesUiState,
val isRefreshing: Boolean
)
#Immutable
sealed interface TopRatedMoviesUiState {
data class Success(val movies: List<Movie>) : TopRatedMoviesUiState
object Error : TopRatedMoviesUiState
object Loading : TopRatedMoviesUiState
}
class HomeViewModel #Inject constructor(
private val movieRepository: MovieRepository
) : ViewModel() {
private val topRatedMovies: Flow<Result<List<Movie>>> =
movieRepository.getTopRatedMoviesStream().asResult()
private val isRefreshing = MutableStateFlow(false)
val uiState: StateFlow<HomeUiState> = combine(
topRatedMovies,
isRefreshing
) { topRatedResult, refreshing ->
val topRated: TopRatedMoviesUiState = when (topRatedResult) {
is Result.Success -> TopRatedMoviesUiState.Success(topRatedResult.data)
is Result.Loading -> TopRatedMoviesUiState.Loading
is Result.Error -> TopRatedMoviesUiState.Error
}
HomeUiState(
topRated,
refreshing
)
}
.stateIn(
scope = viewModelScope,
started = WhileUiSubscribed,
initialValue = HomeUiState(
TopRatedMoviesUiState.Loading,
isRefreshing = false
)
)
fun onRefresh() {
viewModelScope.launch(exceptionHandler) {
movieRepository.refreshTopRated()
isRefreshing.emit(true)
isRefreshing.emit(false)
}
}
The issue is the TopRatedMoviesUiState.Loading state is only emitted once on initial load but not when user pulls to refresh and new data is emitted in movieRepository.getTopRatedMoviesStream(). I understand that it is because .onStart only emits first time the Flow is subscribed to.
Do I somehow resubscribe to Flow when refresh is performed? Refresh does not always return new data from repo so how in this case, how do I avoid duplicate emission?
You emit TopRatedMoviesUiState.Loading in onStart. So what you describe is totally expected. Loading is emitted when the stream starts. I.e. when you start collecting. In your case by stateIn.
Looking at it from another perspective, how should your result wrapper know that the repository is currently loading?
And that's also the answer. Only two places in your code know that you are reloading.
Either subscribe to a fresh flow whenever you call refreshTopRated and complete the flow after emitting the final result.
Or emit a loading state right from the repository before you start loading.
I'd prefer the later one.
Neither solution will save you from emitting the same result again and again. For that, you'd need a new state like 'Unchanged' that your repository emits when it finds no new data. But please evaluate if this optimization is required. What is the cost of an extra emission?
That being said, here are some more questions, challenging your code. Hopefully guiding you to a solid implementation:
Why do you need the isRefreshing StateFlow? Is there a difference in the UI wether you initially load or wether you refresh?
Why do you need topRated to be a StateFlow? Wouldn't a regular Flow do?
Emitting two items to a StateFlow in succession (i.e. isRefreshing.emit(true); isRefreshing.emit(false) ) might loose the first emission [1]. As for state, only the most recent value is relevant. States are not events!
[1]: StateFlow has a replay buffer of 1 and a buffer overflow policy of DROP_OLDEST. See State flow is a shared flow in kotlinx-coroutines-core/kotlinx.coroutines.flow/StateFlow
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.
In my current Android project I have a dialog that retrieves a list of objects from a webservice and show these in a list. It has a problem though. The webservice (outside my control) is not the fastest, so the process takes a while and often the user changes the orientation of the device while this process is ongoing. For now the orientation change results in the original webservice call to be cancelled and a new one is created, but this is not how it should be done I know. I would like the dialog to able to continue the loading from the webservice when an orientation change happens, but I just can't get my head around how to do this. How is it possible to hook into an ongoing call and display this state in the view (the dialog)? Any recommendations are appreciated. I've played around with Android Architecture Components and kotlins sealed classes saving the viewstate in a livedata object that the view observes, but have not found a solution that I like.
I believe a lot of developers use RxJava for this kind of issue. Is this my only option?
Btw. at the moment I use MVP architecture in my project.
Edit
This is where I cancel the job - if the listener is null its cancelled (I'm using kotlin coroutines):
override fun getWorklist() {
job = onWorkerThread {
val result = repository.getResult().awaitResult()
onMainThread {
when (result) {
is Result.Ok -> listener?.onResult(result.getOrDefault(emptyList())) :? job.cancel()
// Any HTTP error
is Result.Error -> listener?.onHttpError(result.exception) :? job.cancel()
// Exception while request invocation
is Result.Exception -> listener?.onException(result.exception) :? job.cancel()
}
}
}
}
Edit 2
I've tried controlling the viewstate using this Android Arch ViewModel, but the MediatorLiveData object isn't triggered on changes of the viewstate object. And the view stays in Loading state:
class MainModel : ViewModel() {
private var job: Job? = null
val viewState = MutableLiveData<MainViewState>().apply { value = Loading("Warsaw") }
val dataGetter = MediatorLiveData<MainViewState>().apply {
addSource(viewState, {
Log.d("MainModel", "Viewstate is: " + viewState.value.toString() + ")...")
when(it) {
is Loading -> {
Log.d("MainModel", "Launching coroutine...")
job = launch(UI) {
Log.d("MainModel", "Loading...")
val items = Repository.getWorklist()
Log.d("MainModel", "Charts retrieved...")
this#MainModel.viewState.postValue(Success(it.items))
Log.d("MainModel", "Posted update to viewstate...")
}
}
}
})
}
override fun onCleared() {
Log.d("MainModel", "Clearing ViewModel")
job?.cancel()
}
}
interface ViewState
sealed class MainViewState : ViewState
class Loading() : MainViewState()
class Error(val error: String) : MainViewState()
class Success(val items: List<WorklistItem>) : MainViewState()