I never enter inside the LiveDataScope inside my ViewModel - android

I have developed 2 functions for the login.
The first "loginOne" works when I use the ViewModel scope.
The other one doesn't work when I use the LiveData scope.
Do you have an idea? I want to make "loginTwo" work.
API
interface LoginAPI {
#POST("login")
suspend fun getUser(#Body loginRequest: LoginRequest): User
}
Repository
class LoginRepository(private val loginAPI: LoginAPI) {
suspend fun getUser(loginRequest: LoginRequest) = loginAPI.getUser(loginRequest)
}
ViewModel
class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel() {
private var user: LiveData<User>? = null
fun loginOne(username: String, password: String) {
viewModelScope.launch {
// i can enter here and get the user :)
val user = loginRepository.getUser(LoginRequest(username, password))
user
}
}
fun loginTwo(username: String, password: String) {
user = liveData(Dispatchers.IO) {
// i never enter inside.. why ?
val user = loginRepository.getUser(LoginRequest(username, password))
emit(user)
}
}
fun getUser(): LiveData<User>? = user
}
Fragment, my viewModel is injected with Koin
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
loginViewModel.getUser()?.observe(this, Observer { user ->
Log.d(LoginFragment::class.java.name, "User : $user ")
})
loginViewModel.loginOne("user","pcw123")
loginViewModel.loginTwo("user","pcw123")
}

Make sure that you created Scope in the right way. Also, that you are using appropriate Dispatchers to achieve wanted results.
You can additionally check if the call is being executed when you wanted to postValue.
Check if Job is still alive.
Check this thing.
Your emmit call looks suspicious.
When using LiveData, you might need to calculate values asynchronously. For example, you might want to retrieve a user's preferences and serve them to your UI. In these cases, you can use the liveData builder function to call a suspend function, serving the result as a LiveData object.
Each emit() call suspends the execution of the block until the LiveData value is set on the main thread.
In the example below, loadUser() is a suspend function declared elsewhere. Use the liveData builder function to call loadUser() asynchronously, and then use emit() to emit the result:
val user: LiveData<User> = liveData {
val data = database.loadUser() // loadUser is a suspend function.
emit(data)
}
EDIT: MutableLiveData for user variable - resolved the issue.

From the documentation:
The liveData building block serves as a structured concurrency
primitive between coroutines and LiveData. The code block starts
executing when LiveData becomes active and is automatically canceled
after a configurable timeout when the LiveData becomes inactive.
So, in your case, the 'user' liveData is already activated when you observing it from fragment. Because you called loginTwo() after liveData has been observed, the emit function will not triggered anymore. Try to call loginTwo() before observing liveData to get emit value from liveData ktx.

Related

Confused about official Android documentation on Flow

I refer to the official Android documentation on using the Flow library in the recommended app architecture.
In the UserRepository class:
class UserRepository #Inject constructor(...) {
fun getUser(userId: String): Flow<User> {
refreshUser(userId)
// Returns a Flow object directly from the database.
return userDao.load(userId)
}
private suspend fun refreshUser(userId: String) {
...
}
...
}
I don't understand how refreshUser(), which is a suspending function, can be called in getUser(), which is a non-suspending function. Perhaps I'm missing something.
I'm trying to create something very similar to this class and, as expected, I get a compilation error stating that the suspending function can be called only in another suspending function. What is the minimal change required to make this work, such that in UserProfileViewModel, I can keep the LiveData<User> variable user as it already is:
val user = userRepository.getUser(userId).asLiveData()
You can't call a suspend function within non-suspend function, so the function getUser() has an error Suspend function 'refreshUser' should be called only from a coroutine or another suspend function. To make this error disappear add suspend keyword:
suspend fun getUser(userId: String): Flow<User> { ... }
To make your second code work you need to use liveData builder.
val user = liveData<User> {
emitSource(getUser(userId).asLiveData())
}
In liveData builder you can call suspend functions, in particular getUser(userId).

How to observe the return value from a Repository class in a ViewModel?

I have an android application using an MVVM architecture. On a button click, I launch a coroutine that calls a ViewModel method to make a network request. In my ViewModel, I have a LiveData observable for the return of that request, but I'm not seeing it update. It seems that my repository method isn't being called and I'm not sure why.
UI Click Listener
searchButton.setOnClickListener{
CoroutineScope(IO).launch{
viewModel.getUser(username.toString())
}
}
ViewModel - Observables and invoked method
private var _user: MutableLiveData<User?> = MutableLiveData<User?>()
val user: LiveData <User?>
get() = _user
...
suspend fun getUser(userId:String) {
_user = liveData{
emit(repository.getUser(userId))
} as MutableLiveData<User?>
}
...
When I debug through, execution goes into the getUser method of the ViewModel but doesn't go into the liveData scope to update my _user MutableLiveData observable and I'm not sure what I'm doing wrong.
There is no need to use liveData coroutine builder because the getUser is a suspended function and you are already calling it in a coroutine. Just post the result simply on _user.
suspend fun getUser(userId: String) {
_user.postValue(repository.getUser(userId))
}
What you did on your code caused assigning a new instance of LiveData to _user, while the observer in the fragment is observing on previous LiveData which is instantiated by private var _user: MutableLiveData<User?> = MutableLiveData<User?>(). So, the update gets lost.
A better solution is to handle the creation of coroutines in your ViewModel class to keep track of them and prevent execution leak.
fun getUser(userId: String) {
viewModelScope.launch(IO) {
_user.postValue(repository.getUser(userId))
}
}
And in the fragment:
searchButton.setOnClickListener{
viewModel.getUser(username.toString())
}
It doesn't work because your "MVVM structure" is not following the MVVM recommendations, nor the structured concurrency guidelines provided for coroutines.
searchButton.setOnClickListener{
CoroutineScope(IO).launch{ // <-- should be using a controlled scope
viewModel.getUser(username.toString()) // <-- state belongs in the viewModel
}
}
Instead, it is supposed to look like this
searchButton.setOnClickListener {
viewModel.onSearchButtonClicked()
}
username.doAfterTextChanged {
viewModel.updateUsername(it)
}
And
class MyViewModel(
private val application: Application,
private val savedStateHandle: SavedStateHandle
): AndroidViewModel(application) {
private val repository = (application as CustomApplication).repository
private val username = savedStateHandle.getLiveData("username", "")
fun updateUsername(username: String) {
username.value = username
}
val user: LiveData<User?> = username.switchMap { userId ->
liveData(viewModelScope + Dispatchers.IO) {
emit(repository.getUser(userId))
}
}
}
Now you can do user.observe(viewLifecycleOwner) { user -> ... } and it should work. If you really do need to fetch only when the button is clicked, you might want to replace the liveData { with a regular suspend fun call, calling from viewModelScope.launch {, and save the value to a LiveData.

How to emit() LiveData results to an existing LiveData object?

The coroutine LiveData example in the official Android developer docs gives the following example using emit():
val user: LiveData<User> = liveData {
val data = database.loadUser() // loadUser is a suspend function.
emit(data)
}
Every example of emit() I have seen including this ProAndroidDev tutorial creates a new LiveData object when using emit(). How can I get a LiveDataScope from a LiveData object that has already been created and emit() values to it? E.g.
class MyViewModel : ViewModel() {
private val user: MutableLiveData<User> = MutableLiveData()
fun getUser(): LiveData<User> {
return user
}
fun loadUser() {
// Code to emit() values to existing user LiveData object.
}
Thanks so much and all help is greatly appreciated!
Something like
fun loadUser() {
user.value = User()
}
Listen to it via
myViewModel.getUser().observe(this, EventObserver { user ->
// do something with user
})
Hope it helps

Why is ViewModelScoped coroutine unusable after ViewModel onCleared() method called

I am sharing an ActivityScoped viewModel between multiple Fragments in my current Android application.
The viewModel employs Coroutine Scope viewModelScope.launch{}
My issue is the .launch{} only works until the owning ViewModel onCleared() method is called.
Is this how ViewModel scoped coroutines are supposed to work?
Is there an approach I can use to "Reset" the viewModelScope so that .launch{} works following the onCleared() method being called?
heres my code::
Fragment
RxSearchView.queryTextChangeEvents(search)
.doOnSubscribe {
compositeDisposable.add(it)
}
.throttleLast(300, TimeUnit.MILLISECONDS)
.debounce(300, TimeUnit.MILLISECONDS)
.map { event -> event.queryText().toString() }
.observeOn(AndroidSchedulers.mainThread())
.subscribe { charactersResponse ->
launch {
viewModel.search(charactersResponse.trim())
}
}
.
.
.
override fun onDetach() {
super.onDetach()
viewModel.cancelSearch()
compositeDisposable.clear()
}
ViewModel
suspend fun search(searchString: String) {
cancelSearch()
if (TextUtils.isEmpty(searchString)) {
return
}
job = viewModelScope.launch {
repository.search(searchString)
}
}
fun cancelSearch() {
job?.cancelChildren()
}
.
.
.
override fun onCleared() {
super.onCleared()
repository.onCleared()
}
What am I doing wrong?
UPDATE
If I amend my launch code to this
job = GlobalScope.launch {
repository.search(searchString)
}
It solves my issue, however is this the only way to achieve my desired result?
I was under the impression GlobalScope was "Bad"
following a cal to onCleared() my viewModelScoped cororoutine Launch stops executing
That's a feature, not a bug.
Once the ViewModel is cleared, you should not be doing anything in that ViewModel or whatever its LifecycleOwner was. All of that is now defunct and should no longer be used.
however is this the only way to achieve my desired result?
The correct solution is to get rid of the code from the ViewModel. If you are expecting some background work to go past the lifetime of an activity or fragment, then that code does not belong in the activity/fragment or its associated viewmodels. It belongs in something that has a matching lifetime to the work that you are trying to do.
repository.onCleared()
This method should not belong to the Repository.
In fact, the Repository should not be stateful.
If you check Google's samples, the Repository creates a LiveData that contains a Resource, and the reason why this is relevant is because the actual data loading and caching mechanic is inside this resource, triggered by LiveData.onActive (in this sample, MediatorLiveData.addSource, but technically that's semantically the same thing).
.subscribe { charactersResponse ->
launch {
viewModel.search(charactersResponse.trim())
The Fragment shouldn't be launching coroutines. It should say something like
.subscribe {
viewModel.updateSearchText(charactersResponse.trim())
}
and also
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java, factory)
viewModel.searchResults.observe(viewLifecycleOwner, Observer { results ->
searchAdapter.submitList(results)
})
}
Then ViewModel would
class MyViewModel(
private val repository: MyRepository
): ViewModel() {
private val searchText = MutableLiveData<String>()
fun updateSearchText(searchText: String) {
this.searchText.value = searchText
}
val searchResults: LiveData<List<MyData>> = Transformations.switchMap(searchText) {
repository.search(searchText)
}
}
And that's all there should be in the ViewModel, so then the question of "who owns the coroutine scope"? That depends on when the task should be cancelled.
If "no longer observing" should cancel the task, then it should be LiveData.onInactive() to cancel the task.
If "no longer observing but not cleared" should retain the task, then ViewModel's onCleared should indeed govern a SupervisorJob inside the ViewModel that would be cancelled in onCleared(), and the search should be launched within that scope, which is probably only possible if you pass over the CoroutineScope to the search method.
suspend fun search(scope: CoroutineScope, searchText: String): LiveData<List<T>> =
scope.launch {
withContext(Dispatchers.IO) { // or network or something
val results = networkApi.fetchResults(searchText)
withContext(Dispatchers.MAIN) {
MutableLiveData<List<MyData>>().apply { // WARNING: this should probably be replaced with switchMap over the searchText
this.value = results
}
}
}
}
Would this work? Not sure, I don't actually use coroutines, but I think it should. This example however doesn't handle the equivalent of switchMap-ing inside the LiveData, nor with coroutines.

Android LiveData Observer not active after first update

I'm trying out a basic implementation of Architecture Component's Live Data with Kotlin like this:
class MarketFragment : LifecycleFragment(){
......
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel=ViewModelProviders.of(this).get(MarketViewModel::class.java)
viewModel.book?.observe(this, Observer { book-> //updateUI })
....
My ViewModel class is created like this:
class MarketViewModel : ViewModel()
{
var book: MutableLiveData<Book>? =MutableLiveData()
var repository: Repository?= Repository()
init {
update("parameter")
}
fun update(s: String)
{
book=repository?.getBook(s)
}
}
And My Repository:
fun getBook(bookSymbol:String):MutableLiveData<Book>
{
val book=MutableLiveData<Book>()
......
call . enqueue (object : Callback<Book> {
override fun onResponse(call: Call<Book>?, response: Response<Book>?) {
book.value=response?.body()
}
.....
})
return book
}
}
And all of this works great and UI is updated as it should but only for the first time. If i try to make manual calls to update the viewModel from a UI action, the retrofit call still works as expected but the new data is not sent to the Observer in the Fragment:
//this doesn't work:
viewModel.update("string")
//This returns false:
viewModel.book.hasActiveObservers()
Is it the expected behaviour for an Observer to become inactive after the first trigger?
You are creating a new MutableLiveData instance each time you are calling getBooks
Hence your observer is not observing the right LiveData anymore.
To solve this
Your ViewModel should keep only one (immutable) LiveData instance
That immutable LiveData instance could either be:
A MediatorLiveData, which source is the repository's LiveData
A transformation of the repository's LiveData
That implies the repository method getBooks is only called once on initialization of the ViewModel, and that either refreshes itself OR you have to call another method on the repository to trigger a refresh.

Categories

Resources