I'm trying to use MVVM with ViewModel, ViewBinding, Retrofit2 in Android/Kotlin.
I don't know how to move application logic from ViewModel. I can't just move methods with logic because they run on viewModelScope and put results into observable objects in my ViewModel.
Or maybe I can?
For example I have some ArrayList (to show on some ListView).
// items ArrayList
private val _stocktakings =
MutableLiveData<ArrayList<InventoryStocktakingWithCountsDto?>>(ArrayList())
val stocktakings : LiveData<ArrayList<InventoryStocktakingWithCountsDto?>> get() =
_stocktakings
// selected item
private val _selected_stocktaking = MutableLiveData<Int>>
val selected_stocktaking : LiveData<Int> get() = _selected_stocktaking
And a function that is called from my fragment:
public fun loadStocktakings() {
viewModelScope.launch {
Log.d(TAG, "Load stocktakings requested")
clearError()
try {
with(ApiResponse(ApiAdapter.apiClient.findAllStocktakings())){
if (isSuccessful && body != null){
Log.d(TAG, "Load Stocktakings done")
setStocktakings(body)
} else {
val e = "Can't load stocktakings, API error: {${errorMessage}}"
Log.e(TAG, e)
HandleError("Can't load stocktakings, API error: {${e}}") // puts error message into val lastError MutableLiveData...
}
}
} catch (t : Throwable) {
Log.e(TAG, "Can't load stocktakings, connectivity error: ${t.message}")
HandleError("Can't load stocktakings, API error: {${e}}") // puts error message into val lastError MutableLiveData...
}
}
}
Now I want to add another function that changes some field in one of stocktakings. Maybe something like:
public fun setSelectedStocktakingComplete() {
stocktakings.value[selected_stocktaking.value].isComplete = true;
// call some API function... another 15 lines of code?
}
How to do it properly?
I feel I have read wrong tutorials... This will end with fat viewmodel cluttered with viewModelScope.launch, error handling and I can't imagine what will happen when I start adding data/form validation...
Here, some tip for that
Make sure the ViewModel is only responsible for holding and managing
UI-related data.
Avoid putting business logic in the ViewModel. Instead, encapsulate
it in separate classes, such as Repository or Interactor classes.
Use LiveData to observe data changes in the ViewModel and update the
UI accordingly.
Avoid making network or database calls in the ViewModel. Instead,
use the Repository pattern to manage data operations and provide the
data to the ViewModel through a LiveData or other observable object.
Make sure the ViewModel does not hold context references, such as
Activity or Fragment.
Use a ViewModel factory to provide dependencies to the ViewModel, if
necessary.
you can ensure that your ViewModel is simple, easy to test,
and scalable. It also makes it easier to maintain your codebase, as
the business logic is separated from the UI logic.
hope you understand
Related
I have a question... sometimes, I need to get data from ViewModel directly. For example, Let's say there's a isChecked() method in ViewModel. And I want to use it in the if condition.
if(viewModel.isChecked()){
// TODO:
}
So, what I am doing right now is:
fun isChecked(): Boolean = runBlocking {
val result = dbRepo.getData()
val response = apiRepo.check(result)
return response.isSuccessful
}
It uses runBlocking. So, it runs on MainThread. I don't think it's a good way because it can freeze the screen. But yes, if the condition needs to run, it needs to wait it until it gets the data from DB and Network.
Another way that I can think of is using LiveData. However, I can't use it in the condition. So, I needs to move the condition in the observer block. But sometimes, this can't be done because there can be something before the condition. And it doesn't seem to look direct but writing code here and there and finally get that data.
So, Is there any simpler way than this?
Your best bet if you have something slow or blocking like that is to rethink how you are using the data entirely. Instead of trying to return it, use LiveData or callbacks to handle the response asynchronously without causing your UI to hang or become laggy. In these cases you really only have three options:
Use a callback to handle when the response is received
Use observable data like LiveData to handle when the response is received
Change the method to a suspend function and call it from a coroutine
Forcing a method to wait to return on the main thread without using one of these is going to cause the app to hang.
Callback to get state
It's hard to say definitely what the best solution for you is without more details about how you are using isChecked(), but one pattern that could work would be to use a callback to handle what you were formerly putting in the if statement, like this (in the ViewModel):
fun getCheckedState(callback: (Boolean)->Unit) {
viewModelScope.launch {
// do long-running task to get checked state,
// using an appropriate dispatcher if needed
val result = dbRepo.getData()
val response = apiRepo.check(result)
// pass "response.isSuccessful" to the callback, to be
// used as "isChecked" below
callback(response.isSuccessful)
}
}
You would call that from the activity or fragment like this:
viewModel.getCheckedState { isChecked ->
if( isChecked ) {
// do something
}
else {
// do something else
}
}
// CAUTION: Do NOT try to use variables you set inside
// the callback out here!
A word of caution - the code inside the callback you pass to getCheckedState does not run right away. Do not try to use things you set inside there outside the callback scope or you fall into this common issue
Simpler Callback
Alternately, if you only want to run some code when isChecked is true, you could simplify the callback like this
fun runIfChecked(callback: ()->Unit) {
viewModelScope.launch {
// do long-running task to get checked state,
// using an appropriate dispatcher if needed
val result = dbRepo.getData()
val response = apiRepo.check(result)
// only call the callback when it's true
if( response.isSuccessful ) {
callback()
}
}
}
and call it with
viewModel.runIfChecked {
// do something
}
// Again, don't try to use things from the callback out here!
Use lifecyclescope.launch(Dispatcher.IO) instead of runblocking
Try this code on your ViewModel class:
suspend fun isChecked(): Boolean {
val response: Response? = null
viewModelScope.launch(Dispatchers.IO) {
val result = dbRepo.getData()
response = apiRepo.check(result)
}.join()
return response?.isSuccessful
}
From Activity:
// Suppose you have a button
findViewById<Button>(R.id.btn).setOnClickListener({
CoroutineScope(Dispatchers.Main).launch {
if (viewModel.isChecked()) {
Log.d("CT", "Do your others staff")
}
}
})
Hope it work file. If no let me comment
I am trying to switch from LiveData to StateFlow in populating my ListAdapter.
I currently have a MutableLiveData<List<CustomClass>> that I am observing to update the list adapter as such:
viewModel.mutableLiveDataList.observe(viewLifecycleOwner, Observer {
networkIngredientAdapter.submitList(it)
}
This works fine. Now I am replacing the MutableLiveData<List<CustomClass>?> with MutableStateFlow<List<CustomClass>?> in the viewModel as such:
private val _networkResultStateFlow = MutableStateFlow<List<IngredientDataClass>?>(null)
val networkResultStateFlow : StateFlow<List<IngredientDataClass>?>
get() = _networkResultStateFlow
fun loadCustomClassListByNetwork() {
viewModelScope.launch {
//a network request using Retrofit
val result = myApi.myService.getItems()
_networkResultStateFlow.value = result
}
}
I am collecting the new list in a fragment as such:
lifecycleScope.launchWhenStarted {
viewModel.networkResultStateFlow.collect(){
list -> networkIngredientAdapter.submitList(list)}
}
However, the list Adapter does not update when I call loadCustomClassListByNetwork(). Why am I not able to collect the value
Try to replace code in fragment with below:
lifecycleScope.launch {
viewModel.networkResultStateFlow.flowWithLifecycle(lifecycle)
.collect { }
}
I haven't mentioned in the original question but there are two collect calls being made in the created coroutine scope as such:
lifecycleScope.launchWhenStarted {
viewModel.networkResultStateFlow.collect(){
list -> networkIngredientAdapter.submitList(list)}
viewModel.listOfSavedIngredients.collectLatest(){
list -> localIngredientAdapter.submitList(list)}
}
Previously only the first collect call was working so that only one list was updating. So I just created two separate coroutine scopes as such and it now works:
lifecycleScope.launchWhenStarted {
viewModel.networkResultStateFlow.collect(){
list -> networkIngredientAdapter.submitList(list)}
}
lifecycleScope.launchWhenStarted {
viewModel.listOfSavedIngredients.collectLatest(){
list -> localIngredientAdapter.submitList(list)}
}
Note: Using launch, launchWhenStarted or launchWhenCreated yielded the same results.
I'll edit my response once I figure out the reason for needing separate scopes for each call to collect.
EDIT:
So the reason only one listAdapter was updating was because I needed a separate CoroutineScope for each of my StateFlow since Flows by definition run on coroutines. Each flow uses its respective coroutine scope to collect its own value and so you cannot have flows share the same coroutine scope b/c then they would be redundantly collecting the same value. The answer provided by #ruby6221 also creates a new coroutine scope and so likely works but I cannot test it due to an unrelated issue with upgrading my SDK version, otherwise I would set it as the correct answer.
Is this good to put the collect latest inside observe?
viewModel.fetchUserProfileLocal(PreferencesManager(requireContext()).userName!!)
.observe(viewLifecycleOwner) {
if (it != null) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.referralDetailsResponse.collect { referralResponseState ->
when (referralResponseState) {
State.Empty -> {
}
is State.Failed -> {
Timber.e("${referralResponseState.message}")
}
State.Loading -> {
Timber.i("LOADING")
}
is State.Success<*> -> {
// ACCESS LIVEDATA RESULT HERE??
}}}}
I'm sure it isn't, my API is called thrice too as the local DB changes, what is the right way to do this?
My ViewModel looks like this where I'm getting user information from local Room DB and referral details response is the API response
private val _referralDetailsResponse = Channel<State>(Channel.BUFFERED)
val referralDetailsResponse = _referralDetailsResponse.receiveAsFlow()
init {
val inviteSlug: String? = savedStateHandle["inviteSlug"]
// Fire invite link
if (inviteSlug != null) {
referralDetail(inviteSlug)
}
}
fun referralDetail(referral: String?) = viewModelScope.launch {
_referralDetailsResponse.send(State.Loading)
when (
val response =
groupsRepositoryImpl.referralDetails(referral)
) {
is ResultWrapper.GenericError -> {
_referralDetailsResponse.send(State.Failed(response.error?.error))
}
ResultWrapper.NetworkError -> {
_referralDetailsResponse.send(State.Failed("Network Error"))
}
is ResultWrapper.Success<*> -> {
_referralDetailsResponse.send(State.Success(response.value))
}
}
}
fun fetchUserProfileLocal(username: String) =
userRepository.getUserLocal(username).asLiveData()
You can combine both streams of data into one stream and use their results. For example we can convert LiveData to Flow, using LiveData.asFlow() extension function, and combine both flows:
combine(
viewModel.fetchUserProfileLocal(PreferencesManager(requireContext()).userName!!).asFlow(),
viewModel.referralDetailsResponse
) { userProfile, referralResponseState ->
...
}.launchIn(viewLifecycleOwner.lifecycleScope)
But it is better to move combining logic to ViewModel class and observe the overall result.
Dependency to use LiveData.asFlow() extension function:
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0"
it certainly is not a good practice to put a collect inside the observe.
I think what you should do is collect your livedata/flows inside your viewmodel and expose the 'state' of your UI from it with different values or a combined state object using either Flows or Livedata
for example in your first code block I would change it like this
get rid of "userProfile" from your viewmodel
create and expose from your viewmodel to your activity three LiveData/StateFlow objects for your communityFeedPageData, errorMessage, refreshingState
then in your viewmodel, where you would update the "userProfile" update the three new state objects instead
this way you will take the business logic of "what to do in each state" outside from your activity and inside your viewmodel, and your Activity's job will become to only update your UI based on values from your viewmodel
For the specific case of your errorMessage and because you want to show it only once and not re-show it on Activity rotation, consider exposing a hot flow like this:
private val errorMessageChannel = Channel<CharSequence>()
val errorMessageFlow = errorMessageChannel.receiveAsFlow()
What "receiveAsFlow()" does really nicely, is that something emitted to the channel will be collected by one collector only, so a new collector (eg if your activity recreates on a rotation) will not receive the message and your user will not see it again
I'm writing my first app in Kotlin and am using Firebase for auth,db & storage. I have singleton object to wrap each Firebase service to reduce coupling. In my Authentication object, I hold the current user in a LiveData to allow ViewModels to reference it and Fragments to observe it, and it works great. I would like to observe it also from the Database object, to allow it to keep the current user's Firestore document loaded. My Firebase objects are not Lifecycle aware so observeForever is my option. The function's comment states that: "You should manually call {#link #removeObserver(Observer)} to stop observing this LiveData.", but I didn't find any "finalize"/"onCleanUp"/... handlers in Kotlin objects. Should I worry about it (in my case I kind of want to really observe forever)?
code:
object Authentication {
private val auth = FirebaseAuth.getInstance().apply {
addAuthStateListener { _currentUser.value = currentUser }
}
private val _currentUser: MutableLiveData<FirebaseUser?> =
MutableLiveData<FirebaseUser?>().apply { value = auth.currentUser }
val currentUser: LiveData<FirebaseUser?> = _currentUser
...
}
object Database {
init {
Authentication.currentUser.observeForever { switchUserDocument(it) }
}
...
}
Thanks!:)
It looks like you do not need to remove observer from your Singletone you should ignore android lint in your case.
I just started using MVVM architecture on Android. I have a service which basically fetches some data and updates the UI and this is what I understood from MVVM:
Activity should not know anything about the data and should take care of the views
ViewModels should not know about activity
Repository is responsible for getting the data
Now as ViewModels should not know anything about the activity and Activities should not do anything other than handling views, Can anyone please tell where should I start a service?
In MVVM, ideally, the methods to start a service should be defined in Repository since it has the responsibility to interact with Data Source. ViewModel keeps an instance of Repository and is responsible for calling the Repository methods and updating its own LiveData which could be a member of ViewModel. View keeps an instance of ViewModel and it observes LiveData of ViewModel and makes changes to UI accordingly. Here is some pseudo-code to give you a better picture.
class SampleRepository {
fun getInstance(): SampleRepository {
// return instance of SampleRepository
}
fun getDataFromService(): LiveData<Type> {
// start some service and return LiveData
}
}
class SampleViewModel {
private val sampleRepository = SampleRepository.getInstance()
private var sampleLiveData = MutableLiveData<Type>()
// getter for sampleLiveData
fun getSampleLiveData(): LiveData<Type> = sampleLiveData
fun startService() {
sampleLiveData.postValue(sampleRepository.getDataFromService())
}
}
class SampleView {
private var sampleViewModel: SampleViewModel
// for activities, this sampleMethod is often their onCreate() method
fun sampleMethod() {
// instantiate sampleViewModel
sampleViewModel = ViewModelProviders.of(this).get(SampleViewModel::class.java)
// observe LiveData of sampleViewModel
sampleViewModel.getSampleLiveData().observe(viewLifecycleOwner, Observer<Type> { newData ->
// update UI here using newData
}
}
As far as I know, Services are Android related so, they could be started from View (Activity/Fragment/Lifecycleowner).