I am very new using Android architecture components, so I decided to base my application using GithubBrowserSample to achieve many of my use cases. But i have the problem that i don´t know what is the correct way to share viewmodels between fragments with this approach.
I want to share the view model because i have a fragment with a viewpager with 2 fragments that need to observe data of the parent fragment view model
I used this before to achieve it, based on google's documentation
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
model = activity?.run {
ViewModelProviders.of(this)[SharedViewModel::class.java]
} ?: throw Exception("Invalid Activity")
}
but with the lifecycle-extensions:2.2.0-alpha03 seems to be deprecated
In GithubBrowserSample they have something like this to create an instance of a view model, but with this it seems to be a different instance every time
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val userViewModel: UserViewModel by viewModels {
viewModelFactory
}
And i don't know where to pass the activity scope or if I should pass it.
I tried something like this
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var myViewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
myViewModel = activity?.run {
ViewModelProvider(this, viewModelFactory).get(MyViewModel::class.java)
} ?: throw Exception("Invalid Activity")
}
but im getting
kotlin.UninitializedPropertyAccessException: lateinit property viewModelFactory has not been initialized
I hope you can help me, i'm a little lost with this, sorry if my english it´s not that good
by viewModels() provides a ViewModel that is scoped to the individual Fragment. There's a separate by activityViewModels() that scopes the ViewModel to your Activity.
However, the direct replacement for ViewModelProviders.of(this) is simply ViewModelProvider(this) - you aren't required to switch to by viewModels() or by activityViewModels() when using lifecycle-extensions:2.2.0-alpha03
Related
What I would like to do is to remove at each fragment such lines:
val retrofitService = RetrofitService.getInstance(requireContext())
val mainRepository = MainRepository(retrofitService)
viewModel = ViewModelProvider(
this#SomeFragment,
AppVMFactory(mainRepository)
)[AppViewModel::class.java]
all these lines can be placed in onCreateView or some other lifecycle methods. I have a lot of fragment, so as a result a lot of such code scopes. I think it is not logical to have similar code inside every file. At first I thought about DI, but as I got to know (maybe I'm wrong) that it is not possible to use DI with ViewModel where I have some constructor parameters. So, I started thinking about creating some single CoreFragment or something like that for having such scope and then make implementation of this CoreFragment inside all other fragment. Let see some example: we have such SampleFragment with lines which I would like to optimize:
class SomeSampleFrg : Fragment(R.layout.fragment_sample) {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewBinding = FragmentSampleBinding.bind(view)
val retrofitService = RetrofitService.getInstance(requireContext())
val mainRepository = MainRepository(retrofitService)
viewModel = ViewModelProvider(
this#SampleFragment,
AppVMFactory(mainRepository)
)[AppViewModel::class.java]
....
}
...
}
then I created CoreFragment:
open class CoreFragment: Fragment() {
lateinit var viewModel: AppViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val retrofitService = RetrofitService.getInstance(requireContext())
val mainRepository = MainRepository(retrofitService)
viewModel = ViewModelProvider(
this#CoreFragment,
AppVMFactory(mainRepository)
)[AppViewModel::class.java]
}
}
and then the field viewModel will be accessible after implementation of this fragment class. Is it possible to do? Or it is very stupid question?)) The only problem which I can not even imagine how to do in addition is how to work with DataBinding in such situation) I mean that I will need to make some more fields and also pass layout to this CoreFragment. Maybe someone has any ideas how to do it?
I'm looking through the tutorial for Android room with a view, and trying to extend their model for using ViewModels to multiple fragments, but not really sure how.
MyApplication
class myApplication : Application() {
companion object {
var database: myDatabase? = null
var repository: myRepository? = null
}
override fun onCreate() {
super.onCreate()
database = MyDatabase.getInstance(this)
repository = MyRepository(database!!.myDatabaseDao)
}
}
MyViewModel
class MyViewModel(private val repository: MyRepository) : ViewModel() {
val allWords: LiveData<List<Words>> = repository.allWords.asLiveData()
fun insert(word: Word) = viewModelScope.launch {
repository.insert(word)
}
}
class MyViewModelFactory(private val repository: MyRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MyViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return MyViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
HomeFragment
class HomeFragment : Fragment() {
private val myViewModel: MyViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
var rootView = inflater.inflate(R.layout.fragment_home, container, false)
return rootView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
myViewModel.allWords.observe(viewLifecycleOwner) { words ->
// Update the cached copy of the words in the adapter.
words.let { Log.d("fragment", it.toString()) }
}
}
}
I have a couple of other fragments that will hopefully share the same ViewModel as HomeFragment. I've tried many different approaches, such as using
myViewModel = ViewModelProviders.of(activity!!).get(MyViewModel::class.java)
but all of them give me Caused by: java.lang.InstantiationException: java.lang.Class<com.example.tabtester.ViewModels.MyViewModel> has no zero argument constructor. I can't find any SO posts or documentation that shows me how to provide a constructor in Kotlin.
Also conceptually I can't find any description for what exactly is happening and how the viewmodel is being constructed (and by what). In the Room with a View tutorial, the example given is in MainActivity:
private val wordViewModel: WordViewModel by viewModels {
WordViewModelFactory((application as WordsApplication).repository)
}
This makes sense, to me; you're using the Factory to instantiate a ViewModel to use in the MainActivity. But for any description of how to use ViewModels in Fragments, I don't see where the ViewModel is being constructed. If you have multiple fragments who is constructing the ViewModel? If I use Fragments then does that mean I also need an activity to construct the ViewModel, then somehow share between the Fragments?
Would appreciate any help, or documentation that explains this more clearly.
The underlying APIs of by viewModels(), by activityViewModels() and the (now deprecated) ViewModelProviders.of() all feed into one method: the ViewModelProvider constructor:
ViewModelProvider(viewModelStore: ViewModelStore, factory: ViewModelProvider.Factory)
This constructor takes two parameters:
The ViewModelStore controls the storage and scoping of the ViewModel you create. For example, when you use by viewModels() in a Fragment, it is the Fragment which is used as the ViewModelStore. Similarly, by activityViewModels() uses the Activity as the ViewModelStore.
The ViewModelProvider.Factory controls the construction of the ViewModel if one has not already been created for that particular ViewModelStore.
Therefore if you need a custom Factory, you must always pass that Factory into all places that could create that ViewModel (remember, due to process death and recreation, there's no guarantee that your HomeFragment will be the first fragment to create your ViewModel).
private val myViewModel: MyViewModel by activityViewModels() {
MyViewModelFactory(MyApplication.repository!!)
}
As long as you're using activityViewModels(), the storage of your ViewModel will always be at the activity level, no matter what Factory you are using.
I have an app with one activity and two fragments, in the first fragment, I should be able to insert data to the database, in the second I should be able to see the added items in a recyclerView.
So I've made the Database, my RecyclerView Adapter, and the ViewModel,
the issue is now how should I manage all that?
Should I initialize the ViewModel in the activity and call it in some way from the fragment to use the insert?
Should I initialize the viewmodel twice in both fragments?
My code looks like this:
Let's assume i initialize the viewholder in my Activity:
class MainActivity : AppCompatActivity() {
private val articoliViewModel: ArticoliViewModel by viewModels {
ArticoliViewModelFactory((application as ArticoliApplication).repository)
}
}
Then my FirstFragments method where i should add the data to database using the viewModel looks like this:
class FirstFragment : Fragment() {
private val articoliViewModel: ArticoliViewModel by activityViewModels()
private fun addArticolo(barcode: String, qta: Int) { // function which add should add items on click
// here i should be able to do something like this
articoliViewModel.insert(Articolo(barcode, qta))
}
}
And my SecondFragment
class SecondFragment : Fragment() {
private lateinit var recyclerView: RecyclerView
private val articoliViewModel: ArticoliViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView = view.findViewById(R.id.recyclerView)
val adapter = ArticoliListAdapter()
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(activity)
// HERE I SHOULD BE ABLE DO THIS
articoliViewModel.allWords.observe(viewLifecycleOwner) { articolo->
articolo.let { adapter.submitList(it) }
}
}
}
EDIT:
My ViewModel looks like this:
class ArticoliViewModel(private val repository: ArticoliRepository): ViewModel() {
val articoli: LiveData<List<Articolo>> = repository.articoli.asLiveData()
fun insert(articolo: Articolo) = viewModelScope.launch {
repository.insert(articolo)
}
}
class ArticoliViewModelFactory(private val repository: ArticoliRepository): ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(ArticoliViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return ArticoliViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
Whether multiple fragments should share a ViewModel depends on whether they are showing the same data. If they show the same data, I think it usually makes sense to share a ViewModel so the data doesn't have to be pulled from the repository when you switch between them, so the transition is faster. If either of them also has significant amount of unique data, you might consider breaking that out into a separate ViewModel so it doesn't take up memory when it doesn't need to.
Assuming you are using a shared ViewModel, you can do it one of at least two different ways, depending on what code style you prefer. There's kind of a minor trade-off between encapsulation and code duplication, although it's not really encapsulated anyway since they are looking at the same instance. So personally, I prefer the second way of doing it.
Each ViewModel directly creates the ViewModel. If you use by activityViewModels(), then the ViewModel will be scoped to the Activity, so they will both receive the same instance. But since your ViewModel requires a custom factory, you have to specify it in both Fragments, so there is a little bit of code duplication:
// In each Fragment:
private val articoliViewModel: ArticoliViewModel by activityViewModels {
ArticoliViewModelFactory((application as ArticoliApplication).repository)
}
Specify the ViewModel once in the MainActivity and access it in the Fragments by casting the activity.
// In Activity: The same view model code you already showed in your Activity, but not private
// In Fragments:
private val articoliViewModel: ArticoliViewModel
get() = (activity as MainActivity).articoliViewModel
Or to avoid code duplication, you can create an extension property for your Fragments so they don't have to have this code duplication:
val Fragment.articoliViewModel: ArticoliViewModel
get() = (activity as MainActivity).articoliViewModel
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