I am using one activity, the multiple fragment model, in my application. I have a sharedViewModel with the coroutine Channel to send and receive data from one fragment to another fragment. Also, I have a custom Dialog fragment as a popup across the app and its user action will observe the remaining fragments. My problem here is the event send from the custom dialog is not triggering in the fragment collect part frequently(2/5 Click). I am adding the codebase for your reference.
SharedView Model Part
private val _cornerDataChannel = Channel<CornerData>()
val cornerDataEvent: Flow<CornerData> = _cornerDataChannel.receiveAsFlow()
fun updateCornerDataEvent(data: CornerData) = viewModelScope.launch {
Log.e(TAG, "Event Added")
_cornerDataChannel.send(data)
}
Sending part in the Custom Dialog Fragment
private fun myToolSelectionCallBack(tool: ToolDTO) =
viewModel.updateCornerDataEvent(CornerData.ToolData(tool))
private fun systemStreamSelectionCallBack(stream: StreamDTO) =
viewModel.updateCornerDataEvent(CornerData.StreamData(stream))
Receiving Part inside fragments
private fun observeCornerItemSelectionCallBack() = lifecycleScope.launchWhenStarted {
viewModel.cornerDataEvent.collect { event ->
when (event) {
is StreamData -> streamSelectionCallBack(data= event.data)
is ToolData -> toolSelectionCallBack(data = event.data)
}
}
}
In the receiver part, some user clicks are missing freqently. But it's always getting the update part under the view model.
This is not a problem with clicks, this is because you're using regular channels for multiple observers/receivers but it is supposed to be single receiver with regular channels. You have to use BroadcastChannels for multiple receivers with single sender.
Now BrodcastChannels has been deprecated it is replaced with SharedFlow()
Note: This API is obsolete since 1.5.0. It will be deprecated with warning in 1.6.0 and with error in 1.7.0. It is replaced with SharedFlow.
Check: BroadcastChannels-OfficialDocs
Related
I am calling a signin method from a fragment using a viewmodel. I have been using a lot of callbacks in other areas but read that using MVVM I should not be communicating between the fragment and the viewmodel in this way. The Android documentation seems to use LiveData as well. Why is it ok to have listeners for components like adapters for recyclerview and not other components which are called from a view model.
The signin component is in the Splash fragment. Should I call it as a component outside the viewmodel and take advantage of the listeners?
I'm running into an error and want to give feedback to the user. Do I:
Take the component out of the viewmodel and call it directly from the fragment
Leave the component in the viewmodel and provide the feedback to the fragment/user by utilizing livedata?
Leave the signin component in the viewmodel and just use a callback/listener
UPDATE
Thank you for the feedback. I will provide more detail. I'm using Java, FYI. I am focused on the first run procedure, which is different from displaying a list or detail data. So, I need to have a lot of things happen to get the app ready for first use, and I need a lot of feedback in case things go wrong. I created a splash screen and have a way to record the steps in the process, so I can tell when something goes wrong, where it goes wrong. The user ends up seeing the last message, but the complete message is saved.
I have been adding a listener to the call and an interface to the component. If you haven't guessed, I'm somewhat of a novice, but this seemed to really be a good pattern, but I have been reading this is not the right way to communicate between the Fragment and the ViewModel.
Example, from the SplashFragment.java:
viewModel.signIn(getActivity(), getAuthHelperSignInCallback());
In the SplashViewModel.java:
public void signIn (Activity activity, AuthHelper.AuthHelperSignInListener listener) {
AuthHelper authHelper = new AuthHelper(activity.getApplication());
authHelper.signIn(activity,listener);
}
In the AuthHelper.java I have an interface:
public interface AuthHelperSignInListener {
void onSuccess(IAuthenticationResult iAuthenticationResult);
void onCancel();
void onError(MsalException e);
}
Using this method I can get information back that I need, so if I'm not supposed to use a callback/listener in the fragment like this, what is the alternative?
You can use channel to send these events to your activity or fragment, and trigger UI operation accordingly. Channel belongs to kotlinx.coroutines.channels.Channel package.
First, create these events in your viewModel class using a sealed class.
sealed class SignInEvent{
data class ShowError(val message: String) : SignInEvent()
data class ShowLoginSuccess(val message: String) : SignInEvent()
}
Define a channel variable inside viewModel.
private val signInEventChannel = Channel<SignInEvent>()
// below flow will be used to collect these events inside activity/fragment
val signInEvent = signInEventChannel.receiveAsFlow()
Now you can send any error or success event from viewModel, using the defined event channel
fun onSignIn() {
try {
//your sign in logic
// on success
signInEventChannel.send(SignInEvent.ShowLoginSuccess("Login successful"))
} catch(e: Exception){
//on getting an error.
signInEventChannel.send(SignInEvent.ShowError("There is an error logging in"))
}
}
Now you can listen to these events and trigger any UI operation accordingly, like showing a toast or a snackbar
In activity
lifecycleScope.launchWhenStarted {
activityViewModel.signInEvent.collect { event ->
when (event) {
//ActivityViewModel is your viewmodel's class name
is ActivityViewModel.SignInEvent.ShowError-> {
Snackbar.make(binding.root, event.message, Snackbar.LENGTH_SHORT)
.show()
}
is ActivityViewModel.SignInEvent.ShowLoginSuccess-> {
Snackbar.make(binding.root, event.message, Snackbar.LENGTH_SHORT)
.show()
}
}
}
In fragment
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
fragmentViewModel.signInEvent.collect { event ->
when (event) {
is FragmentViewModel.SignInEvent.ShowError-> {
Snackbar.make(requireView(), event.message, Snackbar.LENGTH_SHORT)
.show()
}
is FragmentViewModel.SignInEvent.ShowLoginSuccess-> {
Snackbar.make(requireView(), event.message, Snackbar.LENGTH_SHORT)
.show()
}
}
}
I am trying to display several download progress bars at once via a list of data objects containing the download ID and the progress value. The values of this list of objects is being updated fine (shown via logging) but the UI components WILL NOT update after their initial value change from null to the first progress value. Please help!
I see there are similar questions to this, but their solutions are not working for me, including attaching an observer.
class DownLoadViewModel() : ViewModel() {
...
private var _progressList = MutableLiveData<MutableList<DownloadObject>>()
val progressList = _progressList // Exposed to the UI.
...
//Update download progress values during download, this is called
// every time the progress updates.
val temp = _progressList.value
temp?.forEach { item ->
if (item.id.equals(download.id)) item.progress = download.progress
}
_progressList.postValue(temp)
...
}
UI Component
#Composable
fun ExampleComposable(downloadViewModel: DownloadViewModel) {
val progressList by courseViewModel.progressList.observeAsState()
val currentProgress = progressList.find { item -> item.id == local.id }
...
LinearProgressIndicator(
progress = currentProgress.progress
)
...
}
I searched a lot of text to solve the problem that List in ViewModel does not update Composable. I tried three ways to no avail, such as: LiveData, MutableLiveData, mutableStateListOf, MutableStateFlow
According to the test, I found that the value has changed, but the interface is not updated. The document says that the page will only be updated when the value of State changes. The fundamental problem is the data problem. If it is not updated, it means that State has not monitored the data update.
The above methods are effective for adding and deleting, but the alone update does not work, because I update the element in T, but the object has not changed.
The solution is to deep copy.
fun agreeGreet(greet: Greet) {
val g = greet.copy(agree = true) // This way is invalid
favourites[0] = g
}
fun agreeGreet(greet: Greet) {
val g = greet.copy() // This way works
g.agree = true
favourites[0] = g
}
Very weird, wasted a lot of time, I hope it will be helpful to those who need to update.
As far as possible, consider using mutableStateOf(...) in JC instead of LiveData and Flow. So, inside your viewmodel,
class DownLoadViewModel() : ViewModel() {
...
private var progressList by mutableStateOf(listOf<DownloadObject>()) //Using an immutable list is recommended
...
//Update download progress values during download, this is called
// every time the progress updates.
val temp = progress.value
temp?.forEach { item ->
if (item.id.equals(download.id)) item.progress = download.progress
}
progress.postValue(temp)
...
}
Now, if you wish to add an element to the progressList, you could do something like:-
progressList = progressList + listOf(/*item*/)
In your activity,
#Composable
fun ExampleComposable(downloadViewModel: DownloadViewModel) {
val progressList by courseViewModel.progressList
val currentProgress = progressList.find { item -> item.id == local.id }
...
LinearProgressIndicator(
progress = currentProgress.progress
)
...
}
EDIT,
For the specific use case, you can also use mutableStateListOf(...)instead of mutableStateOf(...). This allows for easy modification and addition of items to the list. It means you can just use it like a regular List and it will work just fine, triggering recompositions upon modification, for the Composables reading it.
It is completely fine to work with LiveData/Flow together with Jetpack Compose. In fact, they are explicitly named in the docs.
Those same docs also describe your error a few lines below in the red box:
Caution: Using mutable objects such as ArrayList or mutableListOf() as state in Compose will cause your users to see incorrect or stale data in your app.
Mutable objects that are not observable, such as ArrayList or a mutable data class, cannot be observed by Compose to trigger recomposition when they change.
Instead of using non-observable mutable objects, we recommend you use an observable data holder such as State<List> and the immutable listOf().
So the solution is very simple:
make your progressList immutable
while updating create a new list, which is a copy of the old, but with your new progress values
Hello I'm trying to use the new architecture components by jetpack.
So how the AsyncTask will be deprecated, how could I do a callback in android to get the result from a background thread. without my app lag
public void btnConfigurarClick(View v) {
btnConfigurar.setEnabled(false);
myViewModel.configurar(); // do in background resulting true or false
// how to get the result of it with a callback to set enable(true)
...
The concept of Callback gets converted to Subscribe/Publish in terms of ViewModels.
From Acvitity/Fragment, you would need to subscribe to a LiveData that exists inside the ViewModel.
The changes would be notified as you are observing.
Ex :
Class SomeActivity : Activity{
fun startObservingDataChange(){
yourViewModel.someLiveData.observe(viewLifecycleOwner) { data ->
// Whenever data changes in view model, you'll be notified here
// Update to UI can be done here
}
}
}
Class SomeViewModel{
// Observe to this live data in the View
val LiveData<String> someLiveData;
// Update happens to live data in view model
}
You can learn more about Architecture Components in this app.
I have Implemented MVVM architecture and it is working good when app is in foreground but when i minimize the app, repository send the data to view model and view model to activity but the callback is not called until i resume the application.
Method is Repository:
// Method for new order and its process, and listening the connects collection
fun connects(driverId: String, connectsSnapshot: (QuerySnapshot?) -> Unit) {
if (connectsRef!=null){
connectsRef?.remove()
connectsRef=null
}
this.connectsSnapshot = connectsSnapshot
connectsRef = FirebaseFirestore.getInstance().collection("connects").whereEqualTo("driver_id", UserDto.getInstance().id).whereEqualTo("status", "new").orderBy("created", Query.Direction.DESCENDING).limit(1)
.addSnapshotListener { snapshots, e ->
if (e != null) {
connectsSnapshot (null)
return#addSnapshotListener
}
System.out.println("==>ListnerHit")
connectsSnapshot (snapshots)
}
}
this one send data to view model
val mutableLiveDataForConnect = MutableLiveData<QuerySnapshot>()
val mObserverForConnect: Observer<QuerySnapshot> = Observer {
getView().connectListener(it)
}
fun getConnectsData(driverId: String)
{
repository.connects(driverId){
mutableLiveDataForConnect.postValue(it)
System.out.println("==>ViewModel")
}
}
this one also get called but i am not able to receive the data on activity when app is minimized. it works fine when i move the code to acitivty but i need to move the code to repository.
LiveData doesn't get new values without an active Observer, and when you minimise your app the Observer gets paused, because it is lifecycle dependent. Once the app gets back to the foreground, the Observer returns to the active state, the latest data gets posted to it and your activity is updated. This is how LiveData and Observers are supposed to work.
It's not entirely clear to me what you are trying to do. Why is it necessary to update the activity while it's not visible to anyone? It sounds like you are trying to do something in the activity/Observer that's not supposed to be happening there with MVVM.
In a word game app I share a model between an activity and a fragment:
public class MainViewModel extends AndroidViewModel {
private LiveData<List<Game>> mGames;
private final MutableLiveData<Game> mDisplayedGame = new MutableLiveData<>();
(please excuse the non-english text in the screenshot)
The activity observes mGames being currently played by the user and updates the navigational drawer menu (see the left side of the above screenshot).
The fragment observes mDisplayedGame and displays it in a custom view (see the right side of the above screenshot).
My problem is that when the list of the games is updated at the server (and the activity receives new list of games via Websocket and stores it in the Room), I need to post an update to the fragment: "Hey, the game you are displaying was updated, redraw it!"
Is it possible to do that from within the shared view model?
I know that I could observe mGames in the fragment too and add a code there iterating through them and then finding out if the displayed game was updated at the server.
But I would prefer to do it in the MainViewModel because I have a feeling that the fragment should only observe the one game it is displaying and that's it.
TL;DR
Whenever mGames is updated in the view model via Room, I need to notify the mDisplayedGame observers too!
You should use a MediatorLiveData for this.
The way it works is that
public class MainViewModel extends AndroidViewModel {
private final LiveData<List<Game>> mGames;
private final MutableLiveData<Game> mSelectedGame = new MutableLiveData<>();
private final MediatorLiveData<Game> mDisplayedGame = new MediatorLiveData<>();
{
mDisplayedGame.addSource(mGames, (data) -> {
// find the new value of the selected game in the list
mSelectedGame.setValue(newSelectedGame);
});
mDisplayedGame.addSource(mSelectedGame, (data) -> {
mDisplayedGame.setValue(data);
});
}
And then you expose mDisplayedGame as a LiveData<Game> and it should just work.
Use callback bro
-add a callback interface in your viewmodel and a setCallback method
-make your fragment implement it then call viewmodel.setCallback(fragment)
-call the callback in your obsever