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.
Related
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 💪🙂
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.
For ViewModels which has only compile-time dependencies, I use the ViewModelProvider.Factory from Architecture components like following:
class ViewModelFactory<T : ViewModel> #Inject constructor(private val viewModel: Lazy<T>) : ViewModelProvider.Factory {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T = viewModel.get() as T
}
And in my Activity or Fragment I get the ViewModel in following way:
#Inject
lateinit var viewModelFactory: ViewModelFactory<ProductsViewModel>
This is working fine until my ViewModel needs a dependency which is only available at run-time.
Scenario is, I have a list of Product which I am displaying in RecyclerView. For each Product, I have ProductViewModel.
Now, the ProductViewModel needs variety of dependencies like ResourceProvider, AlertManageretc which are available compile-time and I can either Inject them using constructor or I can Provide them using Module. But, along with above dependencies, it needs Product object as well which is only available at run-time as I fetch the list of products via API call.
I don't know how to inject a dependency which is only available at run-time. So I am doing following at the moment:
ProductsFragment.kt
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
productsAdapter = ProductsAdapter(context!!, products, R.layout.list_item_products, BR.productVm)
rvProducts.layoutManager = LinearLayoutManager(context)
rvProducts.addItemDecoration(RecyclerViewMargin(context, 10, 20))
rvProducts.adapter = productsAdapter
getProducts()
}
private fun getProducts() {
productsViewModel.getProducts()
.observe(this, Observer { productResponse: GetProductResponse ->
products.clear()
productsAdapter?.notifyDataSetChanged()
val productsViewModels = productResponse.data.map { product ->
// Here product is fetched run-time and alertManager etc are
// injected into Fragment as they are available compile-time. I
// don't think this is correct approach and I want to get the
// ProductViewModel using Dagger only.
ProductViewModel(product, resourceProvider,
appUtils, alertManager)
}
products.addAll(productsViewModels)
productsAdapter?.notifyDataSetChanged()
})
}
ProductsAdapter binds the ProductViewModel with the list_item_products layout.
As I mentioned in comments in the code, I don't want to create ProductViewModel my self and instead I want it from dagger only. I also believe the correct approach would be to Inject the ProductsAdapter directly into the Fragment, but then also, I need to tell dagger from where it can get Product object for ProductViewModel which is available at run time and it ends up on same question for me.
Any guide or directions to achieve this would be really great.
You are on the right direction in wanting to inject dependencies instead of creating them like you are doing with ProductViewModel. But, yes, you can't inject ProductViewModel as it needs a Product which is only available a runtime.
The solution to this problem is to create a Factory of ProductViewModel:
class ProductViewModel(
val product: Product,
val resourceProvider: ResourceProvider,
val appUtils: AppUtils,
val alertManager: AlertManager
) {
// ...
}
class ProductViewModelFactory #Inject constructor(
val resourceProvider: ResourceProvider,
val appUtils: AppUtils,
val alertManager: AlertManager
) {
fun create(product: Product): ProductViewModel {
return ProductViewModel(product, resourceProvider, appUtils, alertManager)
}
}
Then inject ProductViewModelFactory in your ProductsFragment class, and call productViewModelFactory.create(product) when the Product is available.
As your project start getting bigger and you see this pattern repeating, consider using AssistedInject to reduce the boilerplate.
I am using Koin library in Kotlin for DI
Koin providing by viewmodel() for get instance of ViewModel by sharedViewModel() to get same instance in fragments.
How can I get same instance of the ViewModel in activities ? I didn't find any way to achieve this.
you must use single{} instead of viewModel{} in module declaration.
single { SharedViewModel() }
And, you can use viewModel() in your views.
View1
private val viewModel: SharedViewModel by viewModel()
View2
private val viewModel: SharedViewModel by viewModel()
But you must load modules when view start by
loadKoinModules(module1)
The important point is that you must unload module in when destroy view.
unloadKoinModules(mainModule)
So, when unload modules your singleton ViewModel will be destroyed.
#EDIT
Now, you can use sharedViewModel declaration.
After some research or discussion on architecture level and also report and issue github Koin,i found solution for this
In this scenario,We should save that state/data into Repository which we need to share between multiple activities not in the viewModel and two or more different ViewModels can access same state/data that are saved in single instance of repository
you need to read more about ViewModel to understand it better.
https://developer.android.com/topic/libraries/architecture/viewmodel
ViewModel is connected to your Activity
so you can share your Activities ViewModel only between his Fragments ,
that is what mean sharedViewModel in koin
sharedViewModel is the same if you use ViewModel Factory with same context .
sharing any data between Activities can be done via Intent , there is no another way in Android,
or you can keep some static / global data and share it between Activities
I would suggest making the app a ViewModelStoreOwner and injecting the viewModels using as owner the app.
The code required would look like this
class App : Application(), ViewModelStoreOwner {
private val mViewModelStore = ViewModelStore()
override fun getViewModelStore(): ViewModelStore {
return mViewModelStore
}
}
You can define some extensions to easily inject the viewModels
val Context.app: App
get() = applicationContext as App
inline fun <reified T : ViewModel> Context.appViewModel(
qualifier: Qualifier? = null,
noinline state: BundleDefinition? = null,
noinline parameters: ParametersDefinition? = null
): Lazy<T> {
return lazy(LazyThreadSafetyMode.NONE) {
GlobalContext.get().getViewModel(qualifier, state, { ViewModelOwner.from(app, null) }, T::class, parameters)
}
}
inline fun <reified T : ViewModel> Fragment.appViewModel(
qualifier: Qualifier? = null,
noinline state: BundleDefinition? = null,
noinline parameters: ParametersDefinition? = null
): Lazy<T> {
return lazy(LazyThreadSafetyMode.NONE) {
GlobalContext.get().getViewModel(qualifier, state, { ViewModelOwner.from(requireContext().app, null) }, T::class, parameters)
}
}
You can then inject your viewModel like this
class MainActivity : AppCompatActivity() {
private val mAppViewModel: AppViewModel by appViewModel()
}
The advantage of this solution is that you don't need to recreate the view model and if you decide to save the state between app restarts, you can easily make the app an SavedStateRegistryOwner as well and using the SavedStateHandle save/restore your state from inside the viewModel, being now bound to the process lifecycle.
I know this is very very late but you can try this:
if you are extending a baseviewmodel, you need to declare the baseViewmodel as a single then in your respective activity inject the BaseViewModel.
Practical example:
val dataModule = module {
single { BaseViewModel(get(), get()) }
}
in your ViewModel
class LoginViewModel(private val param: Repository,
param1: Pref,
param2: Engine) : BaseViewModel(param1, param2)
Then in your activity class
val baseViewModel: BaseViewModel by inject()
Hope this help someone.
I am using android navigation that was presented at Google I/O 2018 and it seems like I can use it by binding to some view or by using NavHost to get it from Fragment. But what I need is to navigate to another specific view from ViewModel from my first fragment depending on several conditions. For ViewModel, I extend AndroidViewModel, but I cannot understand how to do next. I cannot cast getApplication to Fragment/Activity and I can't use NavHostFragment. Also I cannot just bind navigation to onClickListener because the startFragment contains only one ImageView. How can I navigate from ViewModel?
class CaptionViewModel(app: Application) : AndroidViewModel(app) {
private val dealerProfile = DealerProfile(getApplication())
val TAG = "REGDEB"
fun start(){
if(dealerProfile.getOperatorId().isEmpty()){
if(dealerProfile.isFirstTimeLaunch()){
Log.d(TAG, "First Time Launch")
showTour()
}else{
showCodeFragment()
Log.d(TAG, "Show Code Fragment")
}
}
}
private fun showCodeFragment(){
//??
}
private fun showTour(){
//??
}
}
My Fragment
class CaptionFragment : Fragment() {
private lateinit var viewModel: CaptionViewModel
private val navController by lazy { NavHostFragment.findNavController(this) }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
viewModel = ViewModelProviders.of(this).get(CaptionViewModel::class.java)
return inflater.inflate(R.layout.fragment_caption, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel.start()
}
}
I want to keep logic of navigation in ViewModel
How can I navigate from ViewModel?
The answer is please don't. ViewModel is designed to store and manage UI-related data.
New Answer
In my previous answers, I said that we shouldn't navigate from ViewModel, and the reason is because to navigate, ViewModel must have references to Activities/Fragments, which I believe (maybe not the best, but still I believe it) is never a good idea.
But, in recommended app architecture from Google, it mentions that we should drive UI from model. And after I think, what do they mean with this?
So I check a sample from "android-architecture", and I found some interesting way how Google did it.
Please check here: todo-mvvm-databinding
As it turns out, they indeed drive UI from model. But how?
They created an interface TasksNavigator that basically just a navigation interface.
Then in the TasksViewModel, they have this reference to TaskNavigator so they can drive UI without having reference to Activities / Fragments directly.
Finally, TasksActivity implemented TasksNavigator to provide detail on each navigation action, and then set navigator to TasksViewModel.
You can use an optional custom enum type and observe changes in your view:
enum class NavigationDestination {
SHOW_TOUR, SHOW_CODE_FRAGMENT
}
class CaptionViewModel(app: Application) : AndroidViewModel(app) {
private val dealerProfile = DealerProfile(getApplication())
val TAG = "REGDEB"
private val _destination = MutableLiveData<NavigationDestination?>(null)
val destination: LiveData<NavigationDestination?> get() = _destination
fun setDestinationToNull() {
_destination.value = null
}
fun start(){
if(dealerProfile.getOperatorId().isEmpty()){
if(dealerProfile.isFirstTimeLaunch()){
Log.d(TAG, "First Time Launch")
_destination.value = NavigationDestination.SHOW_TOUR
}else{
_destination.value = NavigationDestination.SHOW_CODE_FRAGMENT
Log.d(TAG, "Show Code Fragment")
}
}
}
}
And then in your view observe the viewModel destination variable:
viewModel.destination.observe(this, Observer { status ->
if (status != null) {
viewModel.setDestinationToNull()
status?.let {
when (status) {
NavigationDestination.SHOW_TOUR -> {
// Navigate to your fragment
}
NavigationDestination.SHOW_CODE_FRAGMENT -> {
// Navigate to your fragment
}
}
})
}
If you only have one destination you can just use a Boolean rather than the enum.
There are two ways I can recommend doing this.
Use LiveData to communicate and tell the fragment to navigate.
Create a class called Router and this can contain your navigation logic and reference to the fragment or navigation component. ViewModel can communicate with the router class to navigate.