I am currently building an app and I have added Dagger Hilt in order to define a single class to access data. the injection seems working fine but I am not able to store a value in the data class I use.
I have created a Singleton first, which is used by the code to set/get value from a data structure.
#Singleton
class CarListMemorySource #Inject constructor() : CarListInterface {
private var extendedCarList: ExtendedCarList? = null
override fun setListOfVehicles(listOfVehicles: List<item>)
{
extendedCarList?.listOfVehicles = listOfVehicles
}
}
When I am calling setListOfVehicles the listOfVehicules contains 10 items but
The data structure ExtendedCarList is defined as below:
data class ExtendedCarList(
var listOfVehicles: List<item>
)
The Singleton in passed using Hilt like for example in the viewModel below:
#HiltViewModel
class HomeScreenViewModel #Inject constructor(
private val carList: CarListMemorySource
): ViewModel() {
fun getList() {
--> DO SOMETHING TO Get A ListA
carList.setListOfVehicles(ListA)
}
}
And in the activity, using the viewModel, I am just doing this:
#AndroidEntryPoint
class HomeScreenActivity: AppCompatActivity() {
private val viewModel: HomeScreenViewModel by viewModels()
....
viewModel.getList()
....
}
Any idea why and how to fix it ?
Thanks
you never initialize extendedCarList.
extendedCarList?.listOfVehicles = listOfVehicles
above line is exactly the same as
if (extendedCarList != null) extendedCarList.listOfVehicles = listOfVehicles
But it never passes the null check.
I think just changing
private var extendedCarList: ExtendedCarList? = null
to
private val extendedCarList = ExtendedCarList()
might solve it
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.
Probably it has a simple solution that I cant see. I have a fragment with a ViewModel, The Viewmodel has a method inside of it that I want to call from my fragment and supply the arguments for. but when I try to call the method it shows an error "Unsolved Reference"
class DetailFragmentViewModel : ViewModel() {
private val repo = Crepository.get()
private val itemIdlivedata = MutableLiveData<UUID>()
var crimeLiveDate: LiveData<Crime?> = Transformations.switchMap(itemIdlivedata){ it ->
repo.getitem(it) }
fun LoadItem(itemuuid:UUID){
itemIdlivedata.value = itemuuid
}
}
Fragment Class:
private val crimeDetailVM : ViewModel by lazy {
ViewModelProvider(this).get(DetailFragmentViewModel::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
crimeDetailVM.LoadItem <- Unsolved Reference
}
Thanks for the help!
EDIT:IT HAS A SIMPLE SOLUTION, I DID NOT CAST THE VIEW MODEL TO THE VIEW MODEL CLASS,THANKS FOR THE HELP EVERYONE
You are doing downcasting DetailFragmentViewModel to ViewModel. That is why you are not accessing to DetailFragmentViewModel methods.
Use
private val crimeDetailVM : DetailFragmentViewModel by lazy {
ViewModelProvider(this).get(DetailFragmentViewModel::class.java)
}
Instead of
private val crimeDetailVM : ViewModel by lazy {
ViewModelProvider(this).get(DetailFragmentViewModel::class.java)
}
Also this way is not idiomatic i suggest you to use kotlin extension
val viewModel by viewModels<DetailFragmentViewModel>()
But before do that you need to add the dependency which is Fragment KTX to your app gradle file.
https://developer.android.com/kotlin/ktx
You need activity context
try:
ViewModelProvider(requireActivity()).get(DetailFragmentViewModel::class.java)
you can use also extend view model by ActivityViewModel
eg.-> class DetailFragmentViewModel(application:Application) : AndroidViewModel(applivation){}
I'm trying to create an extension function that return a viewmodel by lazy, but i get an error about viewmodelFactory in't initialized, when i use the by lazy in the same Fragment works fine,
Example (Works fine):
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val listViewModel by lazy {
ViewModelProvider(this, viewModelFactory)[ListViewModel::class]
}
But when I extract it to an Extension function this fails
Example (Error):
inline fun <reified VM : ViewModel> Fragment.provideViewModel(
viewModelFactory: ViewModelProvider.Factory
): Lazy<VM> = lazy {
ViewModelProvider(this, viewModelFactory)[VM::class.java]
}
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val listViewModel by provideViewModel<ListViewModel>(viewModelFactory)
Having a look at what happens behind the scenes, it seems like inline functions are the cause. When you decompile the Kotlin code into Java you see that Kotlin treats an inline function with a special wrapper class which unfortunately takes in the function arguments as constructor parameters:
this.viewModel$delegate = LazyKt.lazy((Function0)(new ActivityMain$$special$$inlined$provideViewModel$1(this, viewModelFactory$iv)));
so it requires viewModelFactory$iv which represents the lateinit factory to be initialised:
Factory var10001 = this.fac;
if (var10001 == null) {
Intrinsics.throwUninitializedPropertyAccessException("fac");
}
androidx.lifecycle.ViewModelProvider.Factory viewModelFactory$iv = (androidx.lifecycle.ViewModelProvider.Factory)var10001;
So this is clearly an issue with Kotlin and Dagger's interoperability and I doubt has any solution other than to change the way you're implementing things.
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