Should I include LifecycleOwner in ViewModel? - android

LifecycleOwner is currently needed in order for me to create an observer.
I have code which creates an Observer in the ViewModel so I attach the LifecycleOwner when retrieving the ViewModel in my Fragment.
According to Google's documentation.
Caution: A ViewModel must never reference a view, Lifecycle, or any class that may hold a reference to the activity context.
Did I break that warning and If I did, what way do you recommend me to move my creation of an observer for data return?
I only made an observer so I'm wondering if it's still valid. Since also in Google's documentation it also said.
ViewModel objects can contain LifecycleObservers, such as LiveData objects.
MainFragment
private lateinit var model: MainViewModel
/**
* Observer for our ViewModel IpAddress LiveData value.
* #see Observer.onChanged
* */
private val ipObserver = Observer<String> {
textIp.text = it
hideProgressBar()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
model = ViewModelProviders.of(this).get(MainViewModel::class.java)
model.attach(this)
}
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater?.inflate(R.layout.fragment_main, container, false)
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
buttonRetrieveIp.setOnClickListener {
showProgressBar()
model.fetchMyIp().observe(this, ipObserver) //Here we attach our ipObserver
}
}
override fun showProgressBar() {
textIp.visibility = View.GONE
progressBar.visibility = View.VISIBLE
}
override fun hideProgressBar() {
progressBar.visibility = View.GONE
textIp.visibility = View.VISIBLE
}
MainViewModel
private var ipAddress = MutableLiveData<String>()
private lateinit var owner: LifecycleOwner
fun attach(fragment: MainFragment) {
owner = fragment
}
/**
* For more information regarding Fuel Request using Fuel Routing and Live Data Response.
* #see Fuel Routing Support
* #see Fuel LiveData Support
* */
fun fetchMyIp(): LiveData<String> {
Fuel.request(IpAddressApi.MyIp())
.liveDataResponse()
.observe(owner, Observer {
if (it?.first?.statusCode == 200) {//If you want you can add a status code checker here.
it.second.success {
ipAddress.value = Ip.toIp(String(it))?.ip
}
}
})
return ipAddress
}
Update 1: Improved ViewModel thanks to #pskink suggestion for using Transformations.
private lateinit var ipAddress:LiveData<String>
/**
* Improved ViewModel since January 23, 2018 credits to pskink <a href="
*
* For more information regarding Fuel Request using Fuel Routing and Live Data Response.
* #see Fuel Routing Support
* #see Fuel LiveData Support
* */
fun fetchMyIp(): LiveData<String> {
ipAddress = Transformations.map(Fuel.request(IpAddressApi.MyIp()).liveDataResponse(), {
var ip:String? = ""
it.second.success {
ip = Ip.toIp(String(it))?.ip
}
ip
})
return ipAddress
}

No. If you wish to observe changes of some LiveData inside your ViewModel you can use observeForever() which doesn't require LifecycleOwner.
Remember to remove this observer on ViewModel's onCleared() event:
val observer = new Observer() {
override public void onChanged(Integer integer) {
//Do something with "integer"
}
}
...
liveData.observeForever(observer);
...
override fun onCleared() {
liveData.removeObserver(observer)
super.onCleared()
}
Very good reference with examples of observe LiveData.

Assumptions:
Fuel refers to your ViewModel
Fuel.request(IpAddressApi.MyIp()) is a method in your ViewModel
IpAddressApi.MyIp() does not have a reference to your LifecycleOwner,
If all are true,then you are not violating it. So long as you are not passing a LifecycleOwner reference to the ViewModel you are safe!
LifecycleOwner - relates to an Activity or Fragment as it owns the various Android Lifecycles e.g onCreate, onPause, onDestroy etc

in Kotlin this can be something like:
val mObserver = Observer<List<QueueTabData>> { myString->
// do something with myString
}

Should I include LifecycleOwner in ViewModel?
Ans: No
The purpose of viewmodel is to hold UI data, so that it survives across configuration changes.
And the reason for the following
Caution: A ViewModel must never reference a view, Lifecycle, or any class that may hold a reference to the activity context.
Is because the viewmodel survives configuration changes whereas activities don't. They are destroyed and re-created on configuration change. So, if you have any activity context references in viewmodel they would refer to previous activity that got destroyed.
So this leads to memory leak. And hence it is not recommended.
Furthermore,
If you have repositories that act as your data source we should avoid using LiveData for such purposes as mentioned here in the paragraph just above the code block.
This is because LiveData are handled on MainThread that may lead to UI freeze.
We should use kotlin flows for such purposes.

Related

How to prevent data duplication caused by LiveData observation in Fragment?

I'm subscribed to an observable in my Fragment, the observable listens for some user input from three different sources.
The main issue is that once I navigate to another Fragment and return to the one with the subscription, the data is duplicated as the observable is handled twice.
What is the correct way to handle a situation like this?
I've migrated my application to a Single-Activity and before it, the subscription was made in the activity without any problem.
Here is my Fragment code:
#AndroidEntryPoint
class ProductsFragment : Fragment() {
#Inject
lateinit var sharedPreferences: SharedPreferences
private var _binding: FragmentProductsBinding? = null
private val binding get() = _binding!!
private val viewModel: ProductsViewModel by viewModels()
private val scanner: CodeReaderViewModel by activityViewModels()
private fun observeBarcode() {
scanner.barcode.observe(viewLifecycleOwner) { barcode ->
if (barcode.isNotEmpty()) {
if (binding.searchView.isIconified) {
addProduct(barcode) // here if the fragment is resumed from a backstack the data is duplicated.
}
if (!binding.searchView.isIconified) {
binding.searchView.setQuery(barcode, true)
}
}
}
}
private fun addProduct(barcode: String) {
if (barcode.isEmpty()) {
return
}
viewModel.insert(barcode)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.start(args.documentId)
if (args.documentType == "Etichette") {
binding.cvLabels.visibility = View.VISIBLE
}
initUI()
observe()
}
private fun observe() {
observeBarcode()
observeProducts()
observeLoading()
observeLast()
}
}
Unfortunately, LiveData is a terribly bad idea (the way it was designed), Google insisted till they kinda phased it out (but not really since it's still there) that "it's just a value holder"...
Anyway... not to rant too much, the solution you have to use can be:
Use The "SingleLiveEvent" (method is officially "deprecated now" but... you can read more about it here).
Follow the "official guidelines" and use a Flow instead, as described in the official guideline for handling UI Events.
Update: Using StateFlow
The way to collect the flow is, for e.g. in a Fragment:
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { // or RESUMED
viewModel.yourFlow.collectLatest { ... } // or collect { ... }
}
}
For that in your ViewModel you'd expose something like:
Warning: Pseudo-Code
// Imagine your state is represented in this sealed class
sealed class State {
object Idle: State
object Loading: State
data class Success(val name: String): State
data class Failure(val reason: String): State
}
// You need an initial state
private val _yourFlow = MutableStateFlow(State.Idle)
val yourFlow: StateFlow<State> = _yourFlow
Then you can emit using
_yourFlow.emit(State.Loading)
Every time you call
scanner.barcode.observe(viewLifecycleOwner){
}
You are creating a new anonymous observer. So every new call to observe will add another observer that will get onChanged callbacks. You could move this observer out to be a property. With this solution observe won't register new observers.
Try
class property
val observer = Observer<String> { onChanged() }
inside your method
scanner.barcode.observe(viewLifecycleOwner, observer)
Alternatively you could keep your observe code as is but move it to a Fragment's callback that only gets called once fex. onCreate(). onCreate gets called only once per fragment instance whereas onViewCreated gets called every time the fragment's view is created.

android-implement ViewModel between 2 fragments without using getActivity()

I have 2 fragments in my app. when the user clicks on a button in the first fragment, the second fragment gets added so that the user can insert some data. and then it gets closed and gives the inserted data back to the first fragment. I've used ViewModels for this communication between fragments.
collectionsEditedViewModel = new ViewModelProvider(getActivity()).get(CollectionsEditedViewModel.class);
collectionsEditedViewModel.isEdited().observe(getViewLifecycleOwner(), new Observer<Bundle>() {
#Override
public void onChanged(Bundle bundle) {
}
});
the communication is working properly. but the point is that how can I define the scope of this communication within the fragments. currently I'm using getActivity() as ViewmodelStoreOwner which causes the set data to be redelivered to the first fragment whenever it is reopened. how can I solve this issue?
I believe for communication between Fragments, via the Activity is the way to go, so you're in the correct path.
One thing you could do is use a SingleLiveData class, which essentially is like LiveData but after its value is set, it's nullified, so only the first observer gets it:
class SingleLiveData<T> : MutableLiveData<T>() {
private val mPending = AtomicBoolean(false)
#MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<T>) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner, Observer { t ->
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
#MainThread
override fun setValue(t: T?) {
mPending.set(true)
super.setValue(t)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
#MainThread
fun call() {
value = null
}
companion object {
private val TAG = "SingleLiveData"
}
}
Then you can simply call:
singleLiveData.call() for a "set and destroy" and thus, forget your value after the first use! Class retrieved (and been using in my projects for years) from: https://stackoverflow.com/a/46862551/

How to observe ViewModel that makes a request for a call back

I am trying to make a request to a library that gives me a call back.
Manager.getInstance().request(new CallBack())
I want to put this in a ViewModel so that I can observe it from the Activity.
class RequestViewModel : ViewModel, CallBack {
fun request() {
Manager.getInstance().request(this)
}
override fun onFinished(result : List<String>?) {
}
override fun onFailed() {
}
}
How can I make it so that I can observe when this has finished? I know I could make my Activity implement this CallBack, but I don't want to couple Activity to this.
Ideally this would be a LiveData or Observable.
If I understand the question correctly, you can submit the data acquired in onFinished method to the LiveData instance that should be observed by a view component, e.g.
class RequestViewModel : ViewModel, CallBack {
private val _liveData = MutableLiveData<SomeResult<List<String>>>
val liveData: LiveData<SomeResult<List<String>>> get() = _liveData
fun request() {
Manager.getInstance().request(this)
}
override fun onFinished(result : List<String>?) {
if (result != null) {
_liveData.postValue(SomeResult.success(result))
} else {
_liveData.postValue(SomeResult.failure())
}
}
override fun onFailed() {
_liveData.postValue(SomeResult.failure())
}
}
And somewhere in your object that corresponds to a view component:
viewModel.liveData.observe(lifecycleOwner, Observer<List<String>> {
handleResponse(it)
})
whereas lifecycleOwner typically is your AppCompatActivity or android.support.v4.Fragment inheritor.
I would advise you to decouple requesting from ViewModel and create a class called Repository to handle all the requests. In this class you could have a MutableLiveData object which can be observed and whenever new requested data is retrieved, use mutableLiveData.postValue(retrievedData) for MutableLiveData which notifies the observes about the new changes.
To read more about repository, you can follow these links:
Google's Guide to App Architecture
Codelab tutorial with Repository pattern

How to use android navigation without binding to UI in ViewModel (MVVM)?

I am using android navigation that was presented at Google I/O 2018 and it seems like I can use it by binding to some view or by using NavHost to get it from Fragment. But what I need is to navigate to another specific view from ViewModel from my first fragment depending on several conditions. For ViewModel, I extend AndroidViewModel, but I cannot understand how to do next. I cannot cast getApplication to Fragment/Activity and I can't use NavHostFragment. Also I cannot just bind navigation to onClickListener because the startFragment contains only one ImageView. How can I navigate from ViewModel?
class CaptionViewModel(app: Application) : AndroidViewModel(app) {
private val dealerProfile = DealerProfile(getApplication())
val TAG = "REGDEB"
fun start(){
if(dealerProfile.getOperatorId().isEmpty()){
if(dealerProfile.isFirstTimeLaunch()){
Log.d(TAG, "First Time Launch")
showTour()
}else{
showCodeFragment()
Log.d(TAG, "Show Code Fragment")
}
}
}
private fun showCodeFragment(){
//??
}
private fun showTour(){
//??
}
}
My Fragment
class CaptionFragment : Fragment() {
private lateinit var viewModel: CaptionViewModel
private val navController by lazy { NavHostFragment.findNavController(this) }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
viewModel = ViewModelProviders.of(this).get(CaptionViewModel::class.java)
return inflater.inflate(R.layout.fragment_caption, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel.start()
}
}
I want to keep logic of navigation in ViewModel
How can I navigate from ViewModel?
The answer is please don't. ViewModel is designed to store and manage UI-related data.
New Answer
In my previous answers, I said that we shouldn't navigate from ViewModel, and the reason is because to navigate, ViewModel must have references to Activities/Fragments, which I believe (maybe not the best, but still I believe it) is never a good idea.
But, in recommended app architecture from Google, it mentions that we should drive UI from model. And after I think, what do they mean with this?
So I check a sample from "android-architecture", and I found some interesting way how Google did it.
Please check here: todo-mvvm-databinding
As it turns out, they indeed drive UI from model. But how?
They created an interface TasksNavigator that basically just a navigation interface.
Then in the TasksViewModel, they have this reference to TaskNavigator so they can drive UI without having reference to Activities / Fragments directly.
Finally, TasksActivity implemented TasksNavigator to provide detail on each navigation action, and then set navigator to TasksViewModel.
You can use an optional custom enum type and observe changes in your view:
enum class NavigationDestination {
SHOW_TOUR, SHOW_CODE_FRAGMENT
}
class CaptionViewModel(app: Application) : AndroidViewModel(app) {
private val dealerProfile = DealerProfile(getApplication())
val TAG = "REGDEB"
private val _destination = MutableLiveData<NavigationDestination?>(null)
val destination: LiveData<NavigationDestination?> get() = _destination
fun setDestinationToNull() {
_destination.value = null
}
fun start(){
if(dealerProfile.getOperatorId().isEmpty()){
if(dealerProfile.isFirstTimeLaunch()){
Log.d(TAG, "First Time Launch")
_destination.value = NavigationDestination.SHOW_TOUR
}else{
_destination.value = NavigationDestination.SHOW_CODE_FRAGMENT
Log.d(TAG, "Show Code Fragment")
}
}
}
}
And then in your view observe the viewModel destination variable:
viewModel.destination.observe(this, Observer { status ->
if (status != null) {
viewModel.setDestinationToNull()
status?.let {
when (status) {
NavigationDestination.SHOW_TOUR -> {
// Navigate to your fragment
}
NavigationDestination.SHOW_CODE_FRAGMENT -> {
// Navigate to your fragment
}
}
})
}
If you only have one destination you can just use a Boolean rather than the enum.
There are two ways I can recommend doing this.
Use LiveData to communicate and tell the fragment to navigate.
Create a class called Router and this can contain your navigation logic and reference to the fragment or navigation component. ViewModel can communicate with the router class to navigate.

Clean Architecture: ViewModel with multiple UseCases on Android

This is more of an Architecture question than a bug fixing one.
Let's assume this app lets users mark a Bus and/or Bus Stations as a favourite. My question is, should I have a ViewModel with both UseCases or should I build a UseCase that encapsulates the current logic?
Also for the question part, I'm not entirely sure the way I should expose the combined data to the UI layer (see favouritesExposedLiveData)
Thanks in advance any feedback is welcome, here's my ViewModel you can assume each UseCase is passing the correct data from the data source(s).
open class FavouritesViewModel #Inject internal constructor(
private val getFavouriteStationsUseCase: GetFavouriteStationsUseCase,
private val getFavouriteBusesUseCase: GetFavouriteBusesUseCase,
private val favouriteMapper: FavouriteMapper,
private val busMapper: BusMapper,
private val stationMapper: StationMapper) : ViewModel() {
private val favouriteBusesLiveData: MutableLiveData<Resource<List<BusView>>> = MutableLiveData()
private val favouriteStationsLiveData: MutableLiveData<Resource<List<StationView>>> = MutableLiveData()
private lateinit var favouritesMediatorLiveData: MediatorLiveData<List<FavouriteView>>
private lateinit var favouritesExposedLiveData: LiveData<Resource<List<FavouriteView>>>
init {
fetchFavourites()
}
override fun onCleared() {
getFavouriteStationsUseCase.dispose()
getFavouriteBusesUseCase.dispose()
super.onCleared()
}
fun getFavourites(): LiveData<Resource<List<FavouriteView>>> {
return favouritesExposedLiveData
}
private fun fetchFavourites() {
favouritesMediatorLiveData.addSource(favouriteStationsLiveData, { favouriteStationListResource ->
if (favouriteStationListResource?.status == ResourceState.SUCCESS) {
favouriteStationListResource.data?.map {
favouriteMapper.mapFromView(it)
}
}
})
favouritesMediatorLiveData.addSource(favouriteBusesLiveData, { favouriteBusesListResource ->
if (favouriteBusesListResource?.status == ResourceState.SUCCESS) {
favouriteBusesListResource.data?.map {
favouriteMapper.mapFromView(it)
}
}
})
getFavouriteStationsUseCase.execute(FavouriteStationsSubscriber())
getFavouriteBusesUseCase.execute(FavouriteBusesSubscriber())
}
inner class FavouriteStationsSubscriber : DisposableSubscriber<List<Station>>() {
override fun onComplete() {}
override fun onNext(t: List<Station>) {
favouriteStationsLiveData.postValue(Resource(ResourceState.SUCCESS, t.map { stationMapper.mapToView(it) }, null))
}
override fun onError(exception: Throwable) {
favouriteStationsLiveData.postValue(Resource(ResourceState.ERROR, null, exception.message))
}
}
inner class FavouriteBusesSubscriber : DisposableSubscriber<List<Bus>>() {
override fun onComplete() {}
override fun onNext(t: List<Bus>) {
favouriteBusesLiveData.postValue(Resource(ResourceState.SUCCESS, t.map { busMapper.mapToView(it) }, null))
}
override fun onError(exception: Throwable) {
favouriteBusesLiveData.postValue(Resource(ResourceState.ERROR, null, exception.message))
}
}
}
Note: Currently the MediatorLiveData (favouritesMediatorLiveData)is not binding the data back to the favouritesExposedLiveData since at this time, I'm not sure this is the correct way to go ;).
Ideally a ViewModel would only have the view state for its view. By using the MediatorLiveData you could aggregate all sources of state into one that represents the view state over time.
What you can have is a data class that represents your ViewState that you construct on your view model and is your exposed LiveData
data class FavouritesViewState(val favoriteStations: List<Station>, val favoritBuses: List<Bus>)
However you know depend on the ViewModel to construct the final ViewState which kinda breaks the single responsibility principle and also makes you dependent of an Android framework.
I would approach it using a composite UseCase that had both station and bus use cases and returns the composed data that you can then easily expose from the ViewModel.
The whole point of a ViewModel is that it is a model of what the view is using. It should be as close to that as possible.. Unless you are presenting stations and buses in the same view list (seems ugly), otherwise, they are separate views, and should get separate models.

Categories

Resources