I have the following implementation on my Fragment -
class HeroesDetailsFragment : Fragment() {
private val navArgs: HeroesDetailsFragmentArgs by navArgs()
private val heroesDetailsViewModel: HeroesDetailsViewModel by stateViewModel(state = { navArgs.toBundle() })
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentHeroDetailsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initListeners()
observeUiState()
observeUiAction()
}
}
My HeroesDetailsViewModel looks like this -
class HeroesDetailsViewModel(
private val savedStateHandle: SavedStateHandle,
private val heroesDetailsRepository: HeroesDetailsRepository
) : ViewModel() {
private fun getArgsModel() = HeroesDetailsFragmentArgs.fromSavedStateHandle(savedStateHandle)
init {
val navArgs = getArgsModel()
getAdditionalHeroDetails(navArgs.heroModel.id)
observeUiEvents()
}
}
And in my ViewModelModule I declare the following
val viewModelModule = module {
// ...
viewModel { params ->
HeroesDetailsViewModel(params.get(), get())
}
}
As you can see, I utilized the stateViewModel extension for Fragments that allows me to create a StateViewModel. The issue is that when trying to use the same functionality in Compose:
#Destination
#Composable
fun HeroDetailsScreen(
model: HeroesListModel,
viewModel: HeroesDetailsViewModel = getStateViewModel() //provides deprecation error
) {
}
I get the following deprecation message -
getStateViewModel will be merged to sharedViewModel - no need anymore of state parameter
I did not find any good references on this topic, and it seems weird for me because the Fragment extension stateViewModel is completely fine and not deprecated so I am missing information on what should I do to replace it.
My goal is to inject a ViewModel with state parameters that will initialize the SavedStateHandle object. Currently I am using Koin DI, will switch in the future to Dagger-Hilt so it would be also a nice bonus to see the solution both in Koin and in Dagger-Hilt.
So I finally found a way to inject the ViewModel with dynamic information coming from the Fragment. I was looking at the old Fragment / Activity way which includes handling bundles, but in Compose it's much easier as we don't need to use the SavedStateHandle object because we can handle process death by the rememberSaveable { } block, which decouples the need to inject a ViewModel with dynamic information and the need to save information for process death.
This leaves the ViewModel to only ask for the relevant model and not bother handling process death. Just pure information.
class HeroesDetailsViewModel(
heroListModel : HeroesListModel,
private val heroesDetailsRepositoryImpl: HeroesDetailsRepositoryImpl
) : ViewModel() {
init {
getAdditionalHeroDetails(heroListModel.id)
observeUiEvents()
}
}
So I added a model that will be injected when needed via the parameters field -
#Destination
#Composable
fun HeroDetailsScreen(
model: HeroesListModel,
viewModel: HeroesDetailsViewModel = koinViewModel(parameters = { ParametersHolder(mutableListOf(model)) })
) {
}
And in my DI module the implementation actually is left the same -
val viewModelModule = module {
viewModelOf(::HeroesViewModel)
viewModelOf(::HeroesListItemViewModel)
viewModel { params ->
HeroesDetailsViewModel(params.get(), get())
}
}
Hopefully this saves some time for other people in the future 💪🙂
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 am using hilt for dependency injection, I can't find a way to get a viewmodel inside composable directly as we can't use #AndroidEntryPoint annotation with compose, it works fine when I passed it from an activity..
You can inject your viewModel like this
#Composable fun MainScreen(viewModel: HomeScreenViewModel = hiltViewModel())
You can see my sample project in this link for more information github
Something like this..
#HiltViewModel
class MyVm #Inject constructor(
private val someDep: SomeDep,
) : ViewModel() {}
My view - considering its fragment in this case
#AndroidEntryPoint
class MYFragment : Fragment() {
private val viewModel: MyVm by viewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(requireContext()).apply {
setContent {
MYTheme {
MyTopLevelComposableScreen(
viewModel = viewModel
)
}
}
}
}
}
And my top level composable
#Composable
fun MyTopLevelComposableScreen(vm:MyVm){
}
or directly in the top level composable
#Composable
fun MyTopLevelComposableScreen(vm: MyVm = viewModel()){
}
https://developer.android.com/jetpack/compose/libraries#hilt
First, implementation this module:
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07")
And, you can use it like this:
#Composable
private fun MyLayout() {
val vm: MyViewModel = viewModel() // #HiltViewModel
...
[Android developer guideline]
Most likely a newbie question, as i'm fairly new to Android dev - I am having troubles preserving the state of AndroidView in my #Composable on configuration change/navigation , as factory block is called (as expected) and my chart gets reinstantiated.
#Composable
fun ChartView(viewModel:ViewModel, modifier:Modifier){
val context = LocalContext.current
val chart = remember { DataChart(context) }
AndroidView(
modifier = modifier,
factory = { context ->
Log.d("DEBUGLOG", "chart init")
chart
},
update = { chart ->
Log.d("DEBUGLOG", "chart update")
})
}
The DataChart is a 3rd party component with a complex chart, i would like to preserve the zoom/scrolling state. I know i can use ViewModel to preserve UI state across conf. changes, but given the complexity of saving zoom/scrolling state, i'd like to ask if there is any other easier approach to achieve this?
I tried to move the whole chart instance to viewModel, but as it's using context i get a warning about context object leaks.
Any help would be appreciated!
If you want to preserve state of AndroidView on configuration change, Use rememberSaveable instead remember.
While remember helps you retain state across recompositions, the state
is not retained across configuration changes. For this, you must use
rememberSaveable. rememberSaveable automatically saves any value that
can be saved in a Bundle. For other values, you can pass in a custom
saver object.
If the DataChart is a type of Parcelable, Serializable or other data types that can be stored in bundle :
val chart = rememberSaveable { DataChart(context) }
If the above way not working then create a MapSave to save data, zoom/scrolling states... I assume the DataChart has zoomIndex, scrollingIndex, values properties which your need to save:
fun getDataChartSaver(context: Context) = run {
val zoomKey = "ZoomState"
val scrollingKey = "ScrollingState"
val dataKey = "DataState"
mapSaver(
save = { mapOf(zoomKey to it.zoomIndex, scrollingKey to it.scrollingIndex, dataKey to it.values) },
restore = {
DataChart(context).apply{
zoomIndex = it[zoomKey]
scrollingIndex = it[scrollingKey]
values = it[dataKey]
}
}
)
}
Use:
val chart = rememberSaveable(stateSaver = getDataChartSaver(context)){ DataChart(context) }
See more Ways to store state
I'd say your instincts were correct to move the chart instance into the view model, but, as you noted, context dependencies can become a hassle when they are required for objects other than views. To me, this becomes a question of dependency injection where the dependency is the context or, in a broader sense, the entire data chart. I'd be interested in knowing how you source your view model, but I'll assume it relies on an Android view model provider (via by viewModels() or some sort of ViewModelProvider.Factory).
An immediate solution to this issue is to convert the view model into a subclass of an AndroidViewModel which provides reference to the application context via the view model's constructor. While it remains an anti-pattern and should used sparingly, the Android team has recognized certain use cases to be valid. I personally do not use the AndroidViewModel because I believe it to be a crude solution to a problem which could otherwise be solved with refinements to the dependency graph. However, it's sanctioned by the official documentation, and this is only my personal opinion. From experience, I must say its use makes testing a view model quite a nightmare after-the-fact. If you are interested in a dependency injection library, I'd highly recommend the new Hilt implementation which recently launched a stable 1.0.0 release just this past month.
With this aside, I'll now provide two possible solutions to your predicament: one which utilizes the AndroidViewModel and another which does not. If your view model already has other dependencies outside of the context, the AndroidViewModel solution won't save you much overhead as you'd likely already be instantiating a ViewModelProvider.Factory at some point. These solutions will be considering the scope of an Android Fragment but could easily be implemented in an Activity or DialogFragment as well with some tweaks to lifecycle hooks and whatnot.
With AndroidViewModel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
class MyViewModel(application: Application) : AndroidViewModel(application) {
val dataChart: DataChart
init {
dataChart = DataChart(application.applicationContext)
}
}
where the fragment could be
class MyFragment : Fragment() {
private val viewModel: MyViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View { ... }
}
Without AndroidViewModel
import androidx.lifecycle.ViewModel
class MyViewModel(args: Args) : ViewModel() {
data class Args(
val dataChart: DataChart
)
val dataChart: DataChart = args.dataChart
}
where the fragment could be
class MyFragment : Fragment() {
private lateinit var viewModel: MyViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val applicationContext: Context = requireContext().applicationContext
val dataChart = DataChart(applicationContext)
val viewModel: MyViewModel by viewModels {
ArgsViewModelFactory(
args = MyViewModel.Args(
dataChart = dataChart,
),
argsClass = MyViewModel.Args::class.java,
)
}
this.viewModel = viewModel
...
}
}
and where ArgsViewModelFactory is a creation of my own as shown below
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
class ArgsViewModelFactory<T>(
private val args: T,
private val argsClass: Class<T>,
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T = modelClass.getConstructor(
argsClass,
).newInstance(
args,
)
}
edit (via Hilt module):
#Module
#InstallIn(...)
object DataChartModule {
#Provides
fun provideDataChart(
#ApplicationContext context: Context,
): DataChart = DataChart(context)
}
Here's the simplest way I know of. This keeps the state and doesn't trigger a reload on the WebView when I rotate my phone. It should work with every View.
First create an application class
class MainApplication : Application() {
private var view: WebView? = null
override fun onCreate() {
super.onCreate()
LOG("started application")
}
fun loadView(context: Context): WebView {
if (view == null) {
view = WebView(context)
//init your view here
}
return view!!
}
}
Then add the application class to manifest.xml
<manifest>
...
<application
android:name=".MainApplication"
...
Finally add this to the composable
AndroidView(modifier = Modifier.fillMaxSize(), factory = { context ->
val application = context.applicationContext as MainApplication
application.loadView(context = context)
})
That's it. I'm not sure if this can lead to memory leaks but I haven't had problems yet.
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 am having some common logic which I currently have it in a Util class. Now, I want to move this logic to ViewModel class. As this util method is used in different fragments, is it a good practice to create a common view model (feature based view model) for multiple fragments. I know Google recommended to use 1 view model for 1 view. Please suggest.
If you've got common code, you could have several viewModels that inherit from a baseViewModel, which contains the shared code.
The advantage of this over a Util class is that the shared code is only visible to ViewModels that derive from the base, and can't get confused with anything else.
It's better to create a viewmodel per each fragment, but it is possible to create a single viewmodel for several fragments. According to the official documents:
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
})
}
}