Reusing ViewModel between fragments - android

I am using Navigation Component to write an app that is one activity multiple fragment architecture and passing the data(class object) by using ShareViewModel. My issue is passing data between fragment works fine, however when I back to the previous fragment the data change to the latest object store in ShareViewModel.
The flow would be like:
characterListFragment -> characterDetailFragmentA -> episodeFragment -> characterDetailFragmentB
Start to back navigation:
characterDetailFragmentB -> episodeFragment -> characterDetailFragmentA (The shown data is same as the characterDetailFragmentB one) -> characterListFragment
Here is my navGraph: ​
characterListFragmentwhich show a list of data.
characterDetailFragmentwhich show the selected data from characterListFragment.
episodeFragment which show the all the data in selected episode.
I am using a shareViewModel to pass data between characterListFragment and characterDetailFragment. Also I reuse the shareViewModel to pass data between episodeFragment and characterDetailFragment
Here is the shareViewModel code
class ShareSelectedCharacterViewModel : ViewModel() {
val selected = MutableLiveData<Character>()
fun select(item: Character) {
selected.value = item
}
}
And I hook the shareViewModel to the activity lifecycle like so
private val shareViewModel: ShareSelectedCharacterViewModel by activityViewModels()
When I selected a data in characterDetailFragment or episodeFragment I will set the data to shareViewModel and get the data in characterDetailFragment by using the same lifecycle scope shareViewModel.
// Set selected data
shareViewModel.select(character)
sharedModel.selected.observe(viewLifecycleOwner, Observer {
// Observe the data
})
My issue is passing data between fragment works fine, however when I back to the previous fragment the data change to the latest object store in ShareViewModel. I know the data will be change due to the using the same viewModel. So I am wonder is there any way to handle this kind of scenario?

I find a way to solve this problem. Adding a stack structure in ShareViewModel and detect the back navigation in characterDetailFragment .
/**
* Using stack to store the sharedData and pop the stored data
* when user pressBack.
*/
class ShareSelectedCharacterViewModel : ViewModel() {
private val stack = Stack<Character>()
val selected = MutableLiveData<Character>()
fun select(item: Character) {
selected.value = item
stack.add(item)
}
fun pop() {
stack.pop()
if(stack.isNotEmpty()) {
selected.value = stack.peek()
}
}
}
When I press back button in characterDetailFragment I call the viewModel.pop method to set the previous data into selected.
/**
* Implement Custom Back Navigation:
* when user pressBack in CharacterDetailFragment, we also need to pop out the data
* that stored in the ShareSelectedCharacterViewModel to retrieve previous one.
*/
requireActivity().onBackPressedDispatcher.addCallback(this) {
findNavController().popBackStack()
sharedModel.pop()
}
Is there a better way to handle this situation?

Related

Add to favourites functionality using MVVM Best Practice

I am working with MVVM architecture and trying to use best practices in documentation.
Let me explain what I have done and what is my problem.
I have a fragment and a parent fragment. When user enters a string to searchview which is in parent fragment, I collect the data and fetch the list to recyclerview.
I have a list adapter which takes care of favourite button for each elements in recyclerview. And I implemented a callback to listadapter:
val onFavouriteChanged: (id, isFavourited) -> Unit
In my viewmodel, I have MutableStateFlow to update and collect UI data. Something like this:
#HiltViewModel
class SearchViewModel #Inject constructor(private val repository: Repository) :
ViewModel() {
private var _ViewState =
MutableStateFlow(ViewState(onFavouriteChanged = { Id, isFavorite ->
viewModelScope.launch {
if (isFavorite) {
repository.unmarkAsFavorite(Id)
} else {
repository.markAsFavorite(Id)
}
searchContent(ViewState.value.searchText)
}
}))
val ViewState: StateFlow<ViewState> = _ViewState
...
When user clicked a favourite button, I immediately let backend to know user is favourited that item. Then I refetch the data from callback function using same query text:
searchContent(ViewState.value.searchText)
In my viewmodel i check the new updated data, if user favourited item i change the color of favourite button. It works pretty good.
But the problem is that, when user clicked a favourite button it takes time to update backend, then refetching updated data. So there is a pretty good delay since user clicked the button and saw the button's color change.
My question is, what and how is the best practice to bypass that delay. Should I update the favourited ui state before I even inform the backend? If so where and which layer should i do that? Is it possible to update a list's one item's favourited state in viewmodel?
This is my UIState
data class ViewState(
val list: List<WallItemCampaignResponse> = listOf(),
val onFavouriteChanged: (Long, Boolean) -> Unit,
var searchText: String = ""
)

Pass data to previous composable in Android Compose

I will take a simple sample.
I have 2 Screens: Screen A and Screen B. From Screen A, I open Screen B. And when I return Screen B to Screen A, I want to transfer data back to Screen A.
With Android Fragment, I can use Shared ViewModel or Fragment Result API to do this.
But with Android Compose, the Fragment Result Api is not in Compose. With using Shard ViewModel, what lifecycle do I have to attach Shared ViewModel so it can keep alive? Activity, ... or something else.
Or is there another way to do this?
If you use jetpack navigation, you can pass back data by adding it to the previous back stack entry's savedStateHandle. (Documentation)
Screen B passes data back:
composable("B") {
ComposableB(
popBackStack = { data ->
// Pass data back to A
navController.previousBackStackEntry
?.savedStateHandle
?.set("key", data)
navController.popBackStack()
}
)
}
Screen A Receives data:
composable("A") { backStackEntry ->
// get data passed back from B
val data: T by backStackEntry
.savedStateHandle
.getLiveData<T>("key")
.observeAsState()
ComposableA(
data = data,
navToB = {
// optional: clear data so LiveData emits
// even if same value is passed again
backStackEntry.savedStateHandle.remove("key")
// navigate ...
}
)
}
Replace "key" with a unique string, T with the type of your data and data with your data.
All of your compose composition operations happens within a single activity view hierarchy thus your ViewModel lifecycle will inevitably be bound to that root activity. It can actually be accessed from your composition through LocalLifecycleOwner.current.
Keep in mind that Compose is a totally different paradigm than activity/fragment, you can indeed share ViewModel across composables but for the sake of keeping those simple you can also just "share" data simply by passing states using mutable values and triggering recomposition.
class MySharedViewModel(...) : ViewModel() {
var sharedState by mutableStateOf<Boolean>(...)
}
#Composable
fun MySharedViewModel(viewModel: MySharedViewModel = viewModel()) {
// guessing you already have your own screen display logic
// This also works with compose-navigator
ComposableA(stateResult = viewModel.sharedState)
ComposableB(onUpdate = { viewModel.sharedState = false })
}
fun ComposableA(stateResult: Boolean) {
....
}
fun ComposableB(onUpdate: () -> Unit) {
Button(onClick = { onUpdate() }) {
Text("Update ComposableA result")
}
}
Here you'll find further documentation on managing states with compose
Let's say there are two screens.
1 - FirstScreen it will receive some data and residing on bottom in back stack user will land here from Second screen by press back button.
2 - SecondScreen it will send/attach some data to be received on previous first screen.
Lets start from second screen sending data, for that you can do something like this:
navController.previousBackStackEntry
?.savedStateHandle
?.set("key", viewModel.getFilterSelection().toString())
navController.popBackStack()
Now lets catch that data on first screen for that you can do some thing like this:
if (navController.currentBackStackEntry!!.savedStateHandle.contains("key")) {
val keyData =
navController.currentBackStackEntry!!.savedStateHandle.get<String>(
"key"
) ?: ""
}
Worked perfectly for me.

ViewModel observed every back to fragment

I have MainActivity which is containing 5 Fragments (FragmentA, FragmentB, FragmentC....E) with ViewPager. FragmentA has viewModel and observing a MutableLiveData called "showPopupSuccess" which set to true after doing a task.
The problem is when i go to FragmentC and then get back to FragmentA. The popup is showing again because the observer looks like "reactivated". How to get rid of this? I want the mutableLiveData get resetted. So it has no value in it and not showing a popup
This is the video of the bug if you want to see further
https://www.youtube.com/watch?v=Ay1IIQgOOtk
The easiest way to solve your issue: Use the Event wrapper
Instead of "resetting" the LiveData, you can mark its content as handled when you observe it for the first time. Then your repeated observations know it has already been handled and can ignore it.
To create a better answer according to the guidelines I'm copying over the relevant information from the linked article:
The wrapper:
/**
* Used as a wrapper for data that is exposed via a LiveData that represents an event.
*/
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
In the ViewModel:
// Instead of Boolean the type of Event could be popup
// parameters or whatever else.
private val _showSuccess = MutableLiveData<Event<Boolean>>()
val showSuccess : LiveData<Event<Boolean>>
get() = _showSuccess
In the Fragment:
myViewModel.showSuccess.observe(viewLifecycleOwner, Observer {
it.getContentIfNotHandled()?.let {
// This is only executed if the event has never been handled
showSuccess(...)
}
})

Should livedata be always used in ViewModel?

It seems like recommended pattern for fields in viewmodel is:
val selected = MutableLiveData<Item>()
fun select(item: Item) {
selected.value = item
}
(btw, is it correct that the selected field isn't private?)
But what if I don't need to subscribe to the changes in the ViewModel's field. I just need passively pull that value in another fragment.
My project details:
one activity and a bunch of simple fragments replacing each other with the navigation component
ViewModel does the business logic and carries some values from one fragment to another
there is one ViewModel for the activity and the fragments, don't see the point to have more than one ViewModel, as it's the same business flow
I'd prefer to store a value in one fragment and access it in the next one which replaces the current one instead of pass it into a bundle and retrieve again and again manually in each fragment
ViewModel:
private var amount = 0
fun setAmount(value: Int) { amount = value}
fun getAmount() = amount
Fragment1:
bnd.button10.setOnClickListener { viewModel.setAmount(10) }
Fragment2:
if(viewModel.getAmount() < 20) { bnd.textView.text = "less than 20" }
Is this would be a valid approach? Or there is a better one? Or should I just use LiveData or Flow?
Maybe I should use SavedStateHandle? Is it injectable in ViewModel?
To answer your question,
No, It is not mandatory to use LiveData always inside ViewModel, it is just an observable pattern to inform the caller about updates in data.
If you have something which won't be changed frequently and can be accessed by its instance. You can completely ignore wrapping it inside LiveData.
And anyways ViewModel instance will be preserved and so are values inside it.
And regarding private field, MutableLiveData should never be exposed outside the class, as the data flow is always from VM -> View which is beauty of MVVM pattern
private val selected = MutableLiveData<Item>()
val selectedLiveData : LiveData<Item>
get() = selected
fun select(item: Item) {
selected.value = item
}

Android Kotlin Passing Data From Callback Class To Activity

Hi what is the best way of passing data from a callback function defined in a different class to your activity back to your activity. I am new to android development so sorry if some of this is obvious. I am using an SDK provided by Xsens and a bit of background basically they provide sensors which connect to your device via bluetooth and then stream data back to your device such as acceleration, orientation etc...
The way my code is written is I scan for the sensors, they are then listed on my app and I can press connect on each sensor. When the connected button is clicked the callback class is defined (mine is called ConnectScannedDevice())
Inside this ConnectScannedDevice Class I have overridden the following function and written the below code
override fun onXsensDotDataChanged(address: String, XsensDotData: XsensDotData) {
XsensDotData.acc.forEachIndexed() { index, value ->
Log.d("Sensor Data Acceleration $index", value.toString())
}
XsensDotData.dq.forEachIndexed { index, value ->
Log.d("Sensor Data Orientation $index", value.toString())
}
}
This callback function is hit when I start measuring on the device by using the following code connectedDevice.startMeasuring() this is when the callback function is hit.
I have a setOnClickListener in my activity which then runs the above code to make the device start measuring.
What I now need to do is pass the data the callback function is logging to logcat back to the activity. What is the best way of passing the data the callback function is logging to my activity where the button was pressed.
In the SDK documentation it mentions The XsensDotData object has implemented the Parcelable object from Java, so this object can be passed to another class by Broadcast event.
When the device starts measuring it is a constant stream of data until I stop it from measuring, I need to pass all this data back to the activity. I am trying to display this data onto a graph.
The following is not tested but it outlines the logic of creating a LiveData stream.
In your ConnectScannedDevice class add a private MutableLiveData<XsensDotData> property (that you will update as data changes) and a LiveData<XsensDotData> that you will expose to the ViewModel.
// This is changed internally (never expose a MutableLiveData)
private var xSensorDataMutableLiveData = MutableLiveData<XsensDotData>()
// Changed switchMap() to map()
var xSensorDataLiveData: LiveData<XsensDotData> =
Transformations.map(xSensorDataMutableLiveData) { data ->
data
}
When the data changes you want to update your xSensorMutableDataLiveData in the onXsensDotDataChanged function
override fun onXsensDotDataChanged(address: String, xSensDotData: XsensDotData) {
xSensorDataMutableLiveData.value = XsensDotData
}
Now implement you ViewModel class as follows
// Uncomment if you want to use savedState
//class DeviceDataViewModel(private val savedState: SavedStateHandle): ViewModel() {
class DeviceDataViewModel(): ViewModel() {
// define and initialize a ConnectScannedDevice property
val connectScannedDevice = ConnecScannedDevice() // or however you initialize it
// get a reference to its LiveData
var xSensorDataLiveData = connectScannedDevice.xSensorDataLiveData
}
In your activity get hold of the ViewModel like this
private val deviceDataViewModel by lazy {
ViewModelProvider(this).get(DeviceDataViewModel::class.java)
}
Register your activity to observe the LiveData coming from the ViewModel in the onCreate(Bundle?) function and define how to respond
deviceDataViewModel.xSensorDataLiveData.observe(
this,
Observer { xSensDotData ->
xSensDotData.acc.forEachIndexed() { index, value ->
Log.d("Sensor Data Acceleration $index", value.toString())
}
xSensDotData.dq.forEachIndexed { index, value ->
Log.d("Sensor Data Orientation $index", value.toString())
}
}
)
Note
Google recommends the use of a repository pattern, which means you should add a singleton repository class that does what your ViewModel is currently doing. Your ViewModel should then get hold of the repository instance and simply get its LiveData and pass it on to the activity.
The repository's job is to gather all data from all sources and pass them on to various ViewModels. The ViewModel encapsulates the data needed by activities/fragments and helps make the data survive configuration changes. Activities and fragments get their data from ViewModels and display them on screen.

Categories

Resources