I have a problem with my MVVM structure. I create apps and pass data between fragments. Now it works fine, but I need to add this logic to my ViewModel.
This is my NotesClickFragment:
#AndroidEntryPoint
class NotesClickFragment : Fragment(R.layout.fragment_click_notes) {
private val args by navArgs<NotesClickFragmentArgs>()
private val viewModel: NotesClickViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentClickNotesBinding.bind(view)
binding.apply {
textViewTitleClick.setText(args.notesClickArgs.titleNotes)
textViewContentNotesClick.setText(args.notesClickArgs.contentNotes)
textViewHistoryClick.setText(args.notesClickArgs.createdNotesDateFormat)
}
}
}
This is my NotesClickViewModel:
class NotesClickViewModel #ViewModelInject constructor(
private val notesDao: NotesDao
) : ViewModel() {
}
I'm trying to add a private argument val navArgs: NotesClickFragmentArgs and create the other functions to set data from fragments but it doesn't work. What is good practice? Thanks in advance for your tips.
NotesDao has no place in ViewModel and instead should be inside Repository.
For more information on MVVM, please read the following: Guide to app architecture
As far as I know, ViewModel cannot be used here. Navigation should be handled inside Fragment itself.
In order to use SafeArgs you first need to declare parameters inside NavGraph and then you can use Directions to navigate to a fragment where you can pass your necessary data.
For example. Let's say I have list of movies in RecyclerView. When I click on any of the movies I want to see id of the movie I clicked in a new fragment.
Step 1: Declare information in NavGraph
This is how Fragment is declared in my NavGraph. The tag argument means that this Fragment accepts Int(id).
<fragment
android:id="#+id/movieDetailsFragment"
android:name="my.test.MovieDetailsFragment"
android:label="fragment_movie_details"
tools:layout="#layout/fragment_movie_details">
<argument
android:name="id"
app:argType="integer" />
</fragment>
Then, in any other Fragment connected to MovieDetailsFragment I simply use Directions and pass id that I need.
val direction = MoviesPopularFragmentDirections.actionPopularFragmentToMovieDetailsFragment(id = movie.id)
this.findNavController().navigate(direction)
And that should be it.
Again, more information in official documentation here Navigation Safe Args
Related
I have an app that has a main activity and fragments depend on it, so this is normal.
Now, two of my 10 fragments need to communicate, which I use the example given here
https://developer.android.com/topic/libraries/architecture/viewmodel.html#sharing
class SharedViewModel : ViewModel() {
val selected = MutableLiveData<Item>()
fun select(item: Item) {
selected.value = item
}
}
class MasterFragment : Fragment() {
private lateinit var itemSelector: Selector
// Use the 'by activityViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
itemSelector.setOnClickListener { item ->
// Update the UI
}
}
}
class DetailFragment : Fragment() {
// Use the 'by activityViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.selected.observe(viewLifecycleOwner, Observer<Item> { item ->
// Update the UI
})
}
}
Now, if MasterFragment and DetailFragment dies (both does a popBackStack()) does that instance of the viewmodel keep active untill I finish the MainActivity containing this Fragments ? Because now I dont need anymore that viewmodel instance, but as per documentation says, this instance will be retained from the Activity that contains these fragments
This is not what I'm looking for to communicate between fragments since now a new instance of that viewmodel will be the same as the past one I have created, I mean, it will reuse the instance that I used with the already poped fragments, in which I will need to extra handling a deletion or reset of all the data inside this viewmodel instead of getting a new fresh viewmodel.
Does it works this way or that instance automatically dies when no fragments depending on it are in the stack anymore ?
Now, if MasterFragment and DetailFragment dies (both does a popBackStack()) does that instance of the viewmodel keep active untill I finish the MainActivity containing this Fragments ?
Correct. While it so happens that only two of your fragments use it, that ViewModel is scoped to the activity.
I mean, it will reuse the instance that I used with the already poped fragments, in which I will need to extra handling a deletion or reset of all the data inside this viewmodel instead of getting a new fresh viewmodel.
Then perhaps you should not be using activityViewModels(). For example, you could isolate these two fragments into a nested navigation graph and set up a viewmodel scoped to that graph.
Does it works this way or that instance automatically dies when no fragments depending on it are in the stack anymore ?
The ViewModel system does not know about what is or is not "depending on it". It is all based on the ViewModelStore and the ViewModelStoreOwner that supplies it. activityViewModels() uses the activity as the ViewModelStoreOwner, so viewmodels in that ViewModelStore are tied to the activity.
I'm trying to share a ViewModel between my activity and my fragment. My ViewModel contains a report, which is a complex object I cannot serialize.
protected val viewModel: ReportViewModel by lazy {
val report = ...
ViewModelProviders.of(this, ReportViewModelFactory(report)).get(ReportViewModel::class.java)
}
Now I'm trying to access the viewmodel in a fragment, but I don't want to pass all the factory parameters again.
As stated by the ViewModelProvider.get documentation:
Returns an existing ViewModel or creates a new one in the scope
I want to access the ViewModel instance defined in the activity, so I tried the following but it logically crashes as the model doesn't have an empty constructor:
protected val viewModel: ReportViewModel by lazy {
ViewModelProviders.of(requireActivity()).get(ReportViewModel::class.java)
}
How one should access its "factorysed" ViewModels in a fragment? Should we pass the factory to the fragment?
Thanks!
A little late but I had this question myself. What I found is you can do the following:
In your activity override getDefaultViewModelProviderFactory() like so:
override fun getDefaultViewModelProviderFactory(): ReportViewModelFactory {
return ReportViewModelFactory(report)
}
now in your fragments you can do
requireActivity().getDefaultViewModelProviderFactory()
to get the factory.
Or simply instantiate your viewModel like:
private val viewModel: ReportViewModel by activityViewModels()
In my app I have some data that will be used across all app in the different fragments. According to the Official Android Guides we should use LiveData and SharedViewModel
That documentations shows just how to use data from SharedViewModel in fragment. But ...
How to use that data in the FragmentViewModel?
Use case #1: using the SharedInfo from SharedViewModel I need to make some request to the server and to do smth with response from server in the FragmentViewModel
Use case #2: I have some screen (fragment) that shows info both from FragmentVM and SharedVM
Use case #3: When user click on SomeButton I need to pass some data from SharedViewModel to the ViewModel
I have found two possibles ways how to do it (maybe their are very similar), but I seems that it can be done more clearly
1) Subscribe to LiveData from SharedViewModel in the fragment and call some method in the ViewModel
2) Use the "CombineLatest" approach like in the RX ( thanks for https://github.com/adibfara/Lives )
Some example to reproduce:
class SharedViewModel(app: Application) : ViewModel(app) {
val sharedInfo = MutableLiveData<InfoModel>()
}
class MyFragmentViewModel(app: Application) : ViewModel(app) {
val otherInfo = MutableLiveData<OtherModel>()
}
class StartFragment : Fragment(){
lateinit var viewModel: MyFragmentViewModel
lateinit var sharedViewModel: SharedViewModel
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
// Create Shared ViewModel in the Activity Scope
activity?.let {
sharedViewModel = ViewModelProviders.of(it).get(SharedViewModel::class.java)
}
// Create simple ViewModel forFragment
viewModel = ViewModelProviders.of(this).get(MyFragmentViewModel::class.java)
// Way #1
sharedViewModel.sharedInfo.observe(this, Observer{
viewModel.toDoSmth(it)
})
viewModel.otherInfo.observe(this, Observer{
sharedViewModel.toDoSmth(it)
})
// Way #2
combineLatest(sharedViewModel.sharedInfo, viewModel.otherInfo){s,o -> Pair(s,o)}.observe(this, Observe{
viewModel.doSmth(it)
// or for example
sharedViewModel.refreshInfo(it)
})
}
}
I expect to found some clear way to access to LiveData from SharedVM from FragmentVm and vise versa. Or maybe I think wrong and this is a bad approach to do that and I shouldn't do it
I found the question but does not have solution in code
I want to have data when backpress/manual back happens. I am using navigateUp() to go back. How can I pass data to previous fragment? navigateUp() does not have any facility to pass data to previous fragment. Even I did not find solution using Safe Args. It's passing data forward. I want to have in backward Frad B -> Frag A.
My code to go back to previous fragment
Navigation.findNavController(view).navigateUp()
My question is, How can i get data in previous fragment. I can navigate to Frag A from Frag B using
According to developer.android.com, you can use common for fragments where you want to share data ViewModel using their activity scope.
Here are steps:
Create view model which will keep the data:
class SharedViewModel : ViewModel() {
val dataToShare = MutableLiveData<String>()
fun updateData(data: String) {
dataToShare.value = data
}
}
Observe data changes in Fragment1:
class Fragment1 : Fragment() {
private lateinit var viewModel: SharedViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProviders.of(activity!!).get(SharedViewModel::class.java)
viewModel.dataToShare.observe(this, Observer<String> { dataFromFragment2 ->
// do something with data
})
}
}
Update data in Fragment2 and if you're handling navigation properly, now, you should be able to receive data changes on Fragment1:
class Fragment2 : Fragment() {
private lateinit var viewModel: SharedViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProviders.of(activity!!).get(SharedViewModel::class.java)
updateDataButton.setOnClickListener { v ->
viewModel.updateData("New data for fragment1")
}
}
}
I hope answer helps.
You can use NavigationResult library. Basically it's startActivityForResult but for Fragments in Navigation component.
Please use the OFFICIAL androidx's components. setFragmentResultListener() and setFragmentResult() methods:
implementation "androidx.fragment:fragment-ktx:1.3.5"
Cheers ;)
You should use static variables/companion objects, because it is better than shared viewmodel as it is not simple/nice architecture. As it it not straightforward, I think it is the best way.
To navigateUp From FragmentB to FragmentA
FragmentB:
isBackpressed = true
findNavController().navigateUp()
FragmentA:
onViewCreated() {
// todo
if(isBackpressed) {
isBackpressed = false
// do whatever you want
}
}
To pop destinations when navigating from one destination to another, add an app:popUpTo attribute to the associated <action> element.
To navigate from fargment2 to Fragment1 with arguments, specify in the navigation graph the action of the caller fragment and the arguments of the destination fragment :
<fragment
android:id="#+id/fragment2"
android:name="com.example.myapplication.Fragment2"
android:label="fragment_2"
tools:layout="#layout/fragment_2">
<action
android:id="#+id/action_2_to_1"
app:destination="#id/fragment1"
app:popUpTo="#+id/fragment1"/>
</fragment>
<fragment
android:id="#+id/fragment1"
android:name="com.example.myapplication.Fragment1"
android:label="fragment_1"
tools:layout="#layout/fragment_1">
<argument
android:name="someArgument"
app:argType="string"
app:nullable="false"
android:defaultValue="Hello Word"/>
</fragment>
In your Fragment2 class, you call your action and pass your argument:
val action = Fragment2Directions.action2To1("MY_STRING_ARGUMENT")
findNavController().navigate(action)
You can just call
findNavController().navigate(R.id.fragment1, args)
where args is your bundle.
In fragment1, fetch the data from the arguments
I have an architectural question about the android ViewModels:
Let's say that in my App I have an Activity with two Fragments inside (using a Viewpager). The two fragments do different things (therefore may have their own ViewModel?), but they also both need various data that is similar.
This is for example the state if a network connection is available or not (and both fragments show different error UIs if there is no connection), or some user setting that comes via a Push from a server and affects both fragments equally.
This looks something like this:
Now my question is how to deal with that situation when using ViewModels?
Is it good that a view observes multiple ViewModels, like it would be if I have a ViewModel for the Activity (holding the state that both need equally) and one for each fragment, like this:
This was hinted here for example, but it is not a good practice, as the relationship in MVVM generally is
View n - 1 ViewModel n - 1 Model
But I am not sure where the right place for such shared LiveData is in my case?
Late answer but I asked myself the same question and found the answer in Google guide.
Especially for fragments, it is mentioned on Google Documentations explicitly here
class SharedViewModel : ViewModel() {
val selected = MutableLiveData<Item>()
fun select(item: Item) {
selected.value = item
}
}
class MasterFragment : Fragment() {
private lateinit var itemSelector: Selector
// Use the 'by activityViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
itemSelector.setOnClickListener { item ->
// Update the UI
}
}
}
class DetailFragment : Fragment() {
// Use the 'by activityViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.selected.observe(viewLifecycleOwner, Observer<Item> { item ->
// Update the UI
})
}
}
I think the concept behind the ViewModel was that it is supposed to be related to a single "Screen" rather than a "View". So going by that logic, I think you can use the same ViewModel if multiple fragments reference the same ViewModel because they technically belong to the same "Screen".
In the fragments, you could request the activity for the ViewModel which holds the instance of LiveData and could give you the updates as needed.
Hope this answers your question.
Update: I found a link to a sample fragment in Google samples. Check out onCreateView() method. Pasting code below for reference:
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
final View root = inflater.inflate(R.layout.addtask_frag, container, false);
if (mViewDataBinding == null) {
mViewDataBinding = AddtaskFragBinding.bind(root);
}
mViewModel = AddEditTaskActivity.obtainViewModel(getActivity());
mViewDataBinding.setViewmodel(mViewModel);
setHasOptionsMenu(true);
setRetainInstance(false);
return mViewDataBinding.getRoot();
}
P.S. If you have found a better solution/answer/practice, lemme know.