Dagger2.10+: Inject ViewModel in Fragment/Activity which has run-time dependencies - android

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.

Related

Hilt field injection android

here I am trying to inject the adapter in activity via field injection. Adapter has a parameter(list).
Can somebody assist me here? i am facing compile time error
cannot be provided without an #Provides-annotated method.
Please refer below code
#AndroidEntryPoint
class RecipeActivity() : PostLoginActivity() {
var TAG = MainActivity::class.java.simpleName
private lateinit var binding: ActivityRecipeBinding
private val viewModel: RecipeViewModel by viewModels()
#Inject lateinit var adapter: RecipeAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
}
class RecipeAdapter #Inject constructor(list: MutableList<RecipeModel> ) :
BaseAdapter<RecipeModel>(list) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<RecipeModel> {
return RecipeViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_recipe, parent, false), this
)
}
override fun onBindViewHolder(holder: BaseViewHolder<RecipeModel>, position: Int) {
holder.bindData(baseList[position])
}
}
data class RecipeModel(
var title: String,
var imageType: String,
var url: String
) : Item()
In order to Inject a class, Hilt/Dagger needs to understand exactly how to Inject it. In your project, you should have a 'Module' object. Within here, you can create #Provides methods, which tell Hilt/Dagger exactly what a class looks like so that it can be injected (find out more here).
For example, to provide a class that implements some Android Retrofit services, you might have a module that looks like:
#Module
#InstallIn(ActivityComponent::class)
object AnalyticsModule {
#Provides
fun provideAnalyticsService(
// Potential dependencies of this type
): AnalyticsService {
return Retrofit.Builder()
.baseUrl("https://example.com")
.build()
.create(AnalyticsService::class.java)
}
}
In this example, we can now use #Inject for an AnalyticsService, as Hilt/Dagger now knows how to make one!
In your scenario, it looks like your adapter needs to be constructed with a list of RecipeModels. As you will unlikely have access to this data at the Module level, I don't think you want to be injecting the Adapter like this? Simply creating it in the Activity should be sufficient for what you need!
Something like this:
private var adapter: RecipeAdapter? = null // OR
lateinit var adapter: RecipeAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adapter = RecipeAdapter(viewModel.recipeModels)
}
As a rule of thumb, it is generally more common to use injection for services, factories and view models rather than UI elements like adapters, as these UI elements need to often be constructed with actual data which isn't available in an application's Hilt/Dagger module.
Well as an error suggests you need to have a module in which you provide default construction of you adapter.
Example:
#Module
#InstallIn(ActivityComponent::class)
object AppModule {
#Provides
fun provideRecipeAdapter(
list: MutableList<RecipeModel>
): RecipeAdapter {
return RecipeAdapter(list)
}
}
This is just an example of what you are missing, not actual working code. For more details of how to create these modules look at the documentation

share data between fragments using ModelFactory

I've successfully implemented repository based MVVM. However I need to pass a class object between fragments. I've implemented a sharedViewModel between multiple fragments but the set value always gives null. I know this is due to me not passing the activity context to the initialization of the viewmodels in fragments. I am working with ModelFactory to make instances of my viewmodel yet I can't figure out a way to give 'applicationActivity()' .
Here's my modelFactory:
class MyViewModelFactory constructor(private val repository: MyRepository): ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return if (modelClass.isAssignableFrom(MyOwnViewModel::class.java)) {
MyOwnViewModel(this.repository) as T
} else {
throw IllegalArgumentException("ViewModel Not Found")
}
}
and this is how I intialize my viewmodel:
viewModel=ViewModelProvider(this, MyViewModelFactory(
MyRepository(MyServices() ) )).get(MyOwnViewModel::class.java)
fetching data and everything else works, but I need to be able to share data between fragments and i can't do that with this architecture. I'm not using dagger or Hilt.
Thank you for any pointers.
You can use by activityViewModels() and pass the factory
private val myViewModel: MyViewModel by activityViewModels(factoryProducer = {
MyViewModelFactory(<your respository instance>)
})
It would be good idea to get your repository instance from a singleton or from a field in Application class. If you choose to get from an Application class you can do it like this;
class MyApp: Application() {
val service by lazy { MyService() }
val repository by lazy { MyRepository(service) }
}
by defining them lazy, it ensures that they are not initialized until its necessary
With your application class, your viewmodel call should look like this
private val myViewModel: MyViewModel by activityViewModels(factoryProducer = {
MyViewModelFactory((activity?.application as MyApp).repository)
})
You can also write viewmodelfactory this way
class MyViewModelFactory(internal var viewModel: ViewModel) : ViewModelProvider.Factory {
override fun create(modelClass: Class): T {
return viewModel as T
}
}
And for share data between fragment you can use bundle

How to create separate ViewModels per list item when using Compose UI?

I'm working on a trading app. I need to list the user stocks and their value (profit or loss) among with the total value of the portfolio.
For the holdings list, in an MVP architecture I would create a presenter for each list item but for this app I decided to use MVVM (Compose, ViewModels and Hilt ). My first idea was to create a different ViewModel for each list item. I'm using hiltViewModel() in the composable method signature to create instances of my ViewModel, however this gives me always the same instance and this is not what I want. When using MVVM architecture, is what I'm trying to do the correct way or I should use a single ViewModel? Are you aware about any project I could have a look at? The image below is a super simplification of my actual screen, each cell is complex and that's why I wanted to use a different ViewModel for each cell. Any suggestion is very welcome.
Hilt doesn't support keyed view models. There's a feature request for keyed view models in Compose, but we had to wait until Hilt supports it.
Here's a hacky solution on how to bypass it for now.
You can create a plain view model, which can be used with keys, and pass injections to this view model through Hilt view model:
class SomeInjection #Inject constructor() {
val someValue = 0
}
#HiltViewModel
class InjectionsProvider #Inject constructor(
val someInjection: SomeInjection
): ViewModel() {
}
class SomeViewModel(private val injectionsProvider: InjectionsProvider) : ViewModel() {
val injectedValue get() = injectionsProvider.someInjection.someValue
var storedValue by mutableStateOf("")
private set
fun updateStoredValue(value: String) {
storedValue = value
}
}
#Composable
fun keyedViewModel(key: String) : SomeViewModel {
val injectionsProvider = hiltViewModel<InjectionsProvider>()
return viewModel(
key = key,
factory = object: ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
#Suppress("UNCHECKED_CAST")
return SomeViewModel(injectionsProvider) as T
}
}
)
}
#Composable
fun TestScreen(
) {
LazyColumn {
items(100) { i ->
val viewModel = keyedViewModel("$i")
Text(viewModel.injectedValue.toString())
TextField(value = viewModel.storedValue, onValueChange = viewModel::updateStoredValue)
}
}
}
Unfortunately, HiltViewModelFactory is not a KeyedFactory. So as of now it does not support same viewModel with multiple instances.
Tracking: https://github.com/google/dagger/issues/2328
You have to use Dagger version 2.43 (or newer), it includes the feature/fix to support keys in Hilt ViewModels
https://github.com/google/dagger/releases/tag/dagger-2.43
From the release description:
Fixes #2328 and #3232 where getting multiple instances of #HiltViewModel with different keys would cause a crash.

Why do we need ViewModelProvider.Factory to pass view model to a screen?

I am new to Android development. Currently, I am using Jetpack Compose to build Android apps. I am also learning with MVVM architecture.
One thing I don't understand with this architecture is why we need to use ViewModelProvider.Factory to pass view model to a screen.
For example,
Instead of this,
#Composable
fun HomeScreen() {
val factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
val repository = InMemoryPlantService()
#Suppress("UNCHECKED_CAST")
return HomeViewModel(
plantRepository = repository
) as T
}
}
val homeViewModel: HomeViewModel = viewModel(null, factory)
val currentState: State<HomeViewState> = homeViewModel.viewState.collectAsState()
HomeScreenScaffold(currentState.value)
}
Can't we do this,
#Composable
fun HomeScreen() {
val repository = InMemoryPlantService()
val homeViewModel: HomeViewModel = HomeViewModel(
plantRepository = repository
)
val currentState: State<HomeViewState> = homeViewModel.viewState.collectAsState()
HomeScreenScaffold(currentState.value)
}
Please help.
Full source code can be found here: https://github.com/adammc331/bloom
HomeScreen can be found here: https://github.com/AdamMc331/Bloom/blob/development/app/src/main/java/com/adammcneilly/bloom/HomeScreen.kt
When you call:
val homeViewModel: HomeViewModel = viewModel(null, factory)
The function viewModel(...) will create a new HomeViewModel if it's the first time you request the ViewModel, or it will return the previous instance of HomeViewModel if it already exists. That's one of the advantages of using ViewModels, because on configuration change (or on recomposition) your ViewModel should be reused, not created again. And the way it works is by using a ViewModelProvider.Factory to create the ViewModel when it's necessary. Your ViewModel has a parameter on its constructor, there's no way the default Android classes would know how to create your ViewModel and pass that parameter (i.e. the repository) without you providing a custom ViewModelProvider.Factory. If your ViewModel doesn't have any parameters, the default ViewModelProvider.Factory uses reflection to create your class by using the no-argument constructor.
If you do this:
val homeViewModel: HomeViewModel = HomeViewModel(
plantRepository = repository
)
Your ViewModel will be created many times and won't be reused across configuration changes or recompositions because you're always creating it there - instead of asking for it to be created or reusing it if it already exists, which is what the viewModel(...) function does.
As per a codelab in Room,
By using viewModels and ViewModelProvider.Factory,the framework will take care of the lifecycle of the ViewModel. It will survive configuration changes and even if the Activity is recreated, you'll always get the right instance of the WordViewModel class.
You do not have to use ViewModelProvider.Factory to instantiate your ViewModel.
Lets assume you have an Entity:
#Entity(tableName = "user")
data class User(
#PrimaryKey(autoGenerate = true) #ColumnInfo(name = "user_id") val userId: Long)
And a DAO for that entity:
#Dao
interface UserDao {//some methods}
Without using a repository you can instantiate your ViewModel with the help of android.app.Application like so:
class UserViewModel(
application: Application
) : AndroidViewModel(application) {
val dao = AppDatabase.getDatabase(application, viewModelScope).userDao()
}
And then later in a Fragment create your ViewModel which you can later pass into your composable:
private val userViewModel: userViewModel by viewModels()

Jetpack Compose - preserve state of AndroidView on configuration change

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.

Categories

Resources