ClassCastException When Initialising Interface ViewModel Using by navGraphViewModels() [with Hilt] - android

I have a Fragment flow scoped with a navigation graph and want to scope each Fragment's ViewModel accordingly. However, I don't want each of the Fragments to have access to all methods and variables in the ViewModel, therefore each Fragment's ViewModel is an interface implemented in the base ViewModel.
I am using by navGraphViewModels() delegation in each of the Fragments but it seems to be unable to cast the interface to the base class.
The trace error is:
java.lang.ClassCastException: java.lang.Object cannot be cast to
androidx.lifecycle.ViewModel
Any advice on how to approach this problem??
In my Fragment it is defined as follows:
#AndroidEntryPoint
class ExampleFragment : Fragment() {
private val viewModel: ExampleViewModelController by
navGraphViewModels(R.id.nav_graph_example){defaultViewModelProviderFactory}
///
And the ViewModel is defined by:
#HiltViewModel
class ExampleViewModel #Inject constructor(
private val handle: SavedStateHandle,
private val useCases: ExampleUseCases,
) : ViewModel(), ExampleViewModelController {
override fun validateExampleInputs() {
// TODO("Not yet implemented")
}
}
And lastly, the interface:
interface ExampleViewModelController {
fun validateExampleInputs()
}

The ClassCastException happens because there's no type parameter passed to the delegate like by navGraphViewModels<ExampleViewModel>(). Thus, the delegate is wrongly trying to create a new instance of the interface ExampleViewModelController instead of ExampleViewModel.

Related

ViewModel injection failing on Dagger + Kotlin Android

class MovieListFragment : Fragment() {
#Inject
lateinit var movieListView: MovieListViewModel
private lateinit var movieListAdapter: MovieListAdapter
private lateinit var binding: ListFragmentBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DaggerMovieComponent.builder().appComponent(MovieListApp.component()).fragmentModule(FragmentModule(this)).build().inject(this)
}
This is the class I'm trying to have my viewmodel injected.
#Module (includes = [FragmentModule::class])
class MovieListModule(fragment: Fragment) {
private lateinit var movieListView : MovieListViewModel
#Provides
fun getMovieListViewModel(fragment: Fragment): MovieListViewModel {
movieListView = ViewModelProvider(fragment).get(MovieListViewModel::class.java)
return movieListView
}
}
This is the class that has the module and lastly,
#Singleton
#Component(modules = [MovieModule::class,MovieListModule::class], dependencies = [AppComponent::class]))
interface MovieComponent {
fun inject(movieListViewModel : MovieListViewModel)
fun inject(movieDetailViewModel: MovieDetailViewModel)
fun inject(fragment : Fragment)
}
This is my component interface.
The app crashes, saying that the lateinit viewmodel that was supposed to be injected is not initialised. Is there a way around this?
Thank you in advance.
The error message:
2022-03-30 15:41:40.749 18607-18607/com.example.polyapp E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.polyapp, PID: 18607
java.lang.RuntimeException: Unable to create application com.example.polyapp.MovieListApp: java.lang.IllegalStateException: com.example.polyapp.movieDatabaseFeature.di.AppComponent must be set
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:7487)
at android.app.ActivityThread.access$1700(ActivityThread.java:310)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2283)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:226)
at android.os.Looper.loop(Looper.java:313)
at android.app.ActivityThread.main(ActivityThread.java:8641)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:567)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1133)
Caused by: java.lang.IllegalStateException: com.example.polyapp.movieDatabaseFeature.di.AppComponent must be set
at dagger.internal.Preconditions.checkBuilderRequirement(Preconditions.java:95)
at com.example.polyapp.movieDatabaseFeature.di.DaggerMovieComponent$Builder.build(DaggerMovieComponent.java:101)
at com.example.polyapp.MovieListApp.onCreate(MovieListApp.kt:15)
at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1211)
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:7482)
at android.app.ActivityThread.access$1700(ActivityThread.java:310) 
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2283) 
at android.os.Handler.dispatchMessage(Handler.java:106) 
at android.os.Looper.loopOnce(Looper.java:226) 
at android.os.Looper.loop(Looper.java:313) 
at android.app.ActivityThread.main(ActivityThread.java:8641) 
at java.lang.reflect.Method.invoke(Native Method) 
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:567) 
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1133) 
ViewModel injection on Android is tricky because ViewModels are created using ViewModelProvider to ensure they survive configuration changes. If they're created with ViewModelProvider, then how do you create it with Dagger? Luckily they both provide API's that can mesh together to solve your problem.
Dagger has Multibindings, and ViewModelProvider has it's ViewModelProvider.Factory API. Multibindings allow us to more finely tune when injection occurs by looking it up on a map first. The ViewModelProvider.Factory will tell the ViewModelProvider how to construct your ViewModel which allows for you to specify constructor parameters.
Here are the steps and explanations:
Annotate your MovieListViewModel constructor with #Inject. This will tell Dagger to put your MovieListViewModel on its' graph provided it can satisfy the constructor parameters(we won't be injecting it directly, we just need it on the Dagger graph). If there are no parameters, Dagger will handle it just fine.
import javax.inject.Inject
class MovieListViewModel #Inject constructor() {
...
}
Create a Multibinding for your MovieListViewModel. Instead of directly injecting your MovieListViewModel into the fragment, we want to wrap it in a special Dagger feature called a Multibinding. This will allow you to put your MovieListViewModel into a Map which can be injected and more finely manipulated at runtime(remember that ViewModelProvider.Factory I mentioned?).
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoMap
annotation class ViewModelKey(val value: KClass<out ViewModel>)
#Module
abstract class MovieListViewModelMultiBinder
{
#Binds // Tells Dagger to use the parameter value here, which extends ViewModel
#IntoMap // Tells Dagger to put this ViewModel implementation into a map. This requires you to provide a key which is known at compile time.
#ViewModelKey(MovieListViewModel::class) // Tells Dagger the key to use for this ViewModel. This is your ViewModel class.
// The parameter is the implementation to use when we request a `ViewModel`. Since this is a multibinding, multiple ViewModels can be bound.
fun bind(viewModel: MovieListViewModel): ViewModel
}
Create a ViewModelProvider.Factory that injects and uses the Map mentioned in step 2. This uses a special Dagger type called a Provider. Providers wrap your injected type and do not construct it until you call Provider.get() to retrieve your object.
class DaggerViewModelFactory #Inject constructor(
private val viewModelProviders: Map<Class<out ViewModel>, Provider<ViewModel>>
): ViewModelProvider.Factory
{
override fun <T: ViewModel?> create(modelClass: Class<T>): T
{
return viewModelProviders[modelClass]?.get() as T
}
}
Use your custom ViewModelProvider.Factory in your Fragments and Activities.
class MyActivity: Activity() {
private lateinit var viewModel: MovieListViewModel
#Inject lateinit var factory: DaggerViewModelFactory
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this, factory).get(MovieListViewModel::class.java)
}
}
For the sake of simplicity, I did not throw an error if the DaggerViewModelFactory returns null for the modelClass, but you should add one in case you forget to bind your ViewModel into the Multibinding.
Hope this helps.

Is it possible to inject a conditional class( based on a parameter from the previous fragment) into a view model?

I'm using Hilt. I want to inject a subclass of Foo into my hilt view model.
All subclasses of Foo depend on different class that is already using an #Inject constructor and can be injected into view models, activities, etc. But not into my subclass, so I'm using EntryPoints to inject them.
Also, which subclass gets injected depends upon a property I'm getting from the previous fragment via the SavedStateHandle Hilt provides the view model.
Is it possible to create a Factory (or another solution) that somehow gets the param from the previous fragment and injects the correct Foo object?
I have a solution that doesn't use Hilt to get the Foo object, it just instantiates the right object using a conditional on the param. This solution is not testable and I don't like it.
// in the view model I would like to do this
//
// #Inject
// lateinit var thatFooINeed: Foo
//
// But thatFooINeed could be the Foo with Dependency1 or Dependency2
// It depends on the param sent from the previous fragment
interface Foo {
fun doThis()
fun doThat()
}
class Bar1(context: Context): Foo {
private val dependencyInterface =
EntryPoints.get(context, DependencyInterface::class.java)
val dependency1: Dependency1 = dependencyInterface.getDependency1()
// override doThis() and doThat() and use ^ dependency
...
}
class Bar2(context: Context): Foo {
private val dependencyInterface =
EntryPoints.get(context, DependencyInterface::class.java)
val dependency2: Dependency2 = dependencyInterface.getDependency2()
// override doThis() and doThat() and use ^ dependency
...
}
#EntryPoint
#InstallIn(SingletonComponent::class)
interface DependenciesInterface {
fun getDependency1(): Dependency1
fun getDependency2(): Dependency2
}
class Dependency1 #Inject constructor(val yetAnotherDep: ButWhosCounting)
class Dependency2 #Inject constructor(val yetAnotherDep: ButWhosCounting)```
You have to adopt #AssistedInject, assisted injection will work as factory for your case. Inject the factory and then use the factory to lazily create the instance of the interface.

How to Bind/Provide Activity or Fragment with Hilt?

I'm trying to implement Hilt on an Android App, while It's pretty easy to implement and removes a lot of the boilerplate code when comparing with Dagger, there are some things I miss, Like building my own components and scoping them myself so i'll have my own hirerchy.
To the point: Example: let's say I have a simple App with a RecyclerView, Adapter, Acitivity, and a Callback nested in my Adapter that I pass into my Adapter constructor in order to detect clicks or whatever, and I'm letting my activity implement that Callback, and of course I want to inject the adapter.
class #Inject constructor (callBack: Callback): RecyclerView.Adapter...
When I let Hilt know that I want to inject my adapter I need to let Hilt know how to provide all the Adapter dependencies - the Callback.
In Dagger I was able to achieve this by just binding the Activity to the Callback in one of my modules:
#Binds fun bindCallback(activity: MyActivity): Adapter.Callback
Dagger knew how to bind the Activity(or any Activity/Fragment) and then it was linked to that Callback, but with Hilt it does'nt work.
How can I achieve this? How can I provide or Bind Activity or Fragment with Hilt?
The solution is quite simple.
So a few days ago I came back to look at my question only to see that there was still no new solution, so I tried Bartek solution and wasn't able to make it work, and even if it did work, the clean Hilt code was becoming too messy, so I did a little investigation and played a little and discovered that the solution is actually stupidly easy.
It goes like this:
App:
#HiltAndroidApp
class MyApp: Application()
Activity: (implements callback)
#AndroidEntryPoint
class MainActivity : AppCompatActivity(), SomeClass.Callback {
#Inject
lateinit var someClass: SomeClass
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun onWhatEver() {
// implement
}
}
SomeClass: (with inner callback)
class SomeClass #Inject constructor(
private val callback: Callback
) {
fun activateCallback(){
callback.onWhatEver()
}
interface Callback{
fun onWhatEver()
}
}
SomeModule: (providing/binding the activity to the callback)
#Module
#InstallIn(ActivityComponent::class)
object SomeModule{
#Provides
fun provideCallback(activity: Activity) =
activity as SomeClass.Callback
}
And that's all we need.
We cannot bind the activity to the callback with #Bind because it needs to be explicitly provided and cast to the callback so that the app can build.
The module is installed in ActivityComponent and is aware of a generic 'activity', if we cast it to the callback, Hilt is content and the activity is bound to the callback, and Hilt will know how to provide the callback as long as its in the specific activity scope.
Multiple Activities/Fragments
App:
#HiltAndroidApp
class MyApp: Application()
BooksActivity:
#AndroidEntryPoint
class BooksActivity : AppCompatActivity(), BooksAdapter.Callback{
#Inject
lateinit var adapter: BooksAdapter
...
override fun onItemClicked(book: Book) {...}
}
}
AuthorsActivity:
#AndroidEntryPoint
class AuthorsActivity : AppCompatActivity(), AuthorsAdapter.Callback{
#Inject
lateinit var adapter: AuthorsAdapter
...
override fun onItemClicked(author: Author) {...}
}
BooksAdapter
class BooksAdapter #Inject constructor (
val callback: Callback
) ... {
...
interface Callback{
fun onItemClicked(book: Book)
}
}
AuthorsAdapter
class AuthorsAdapter #Inject constructor (
val callback: Callback
) ... {
...
interface Callback{
fun onItemClicked(auhtor: Auhtor)
}
}
AuhtorsModule
#Module
#InstallIn(ActivityComponent::class)
object AuthorsModule {
#Provides
fun provideCallback(activity: Activity) =
activity as AuthorsAdapter.Callback
}
BooksModule
#Module
#InstallIn(ActivityComponent::class)
object BooksModule {
#Provides
fun provideCallback(activity: Activity) =
activity as BooksAdapter.Callback
}
The Modules can be joined to one module with no problem, just change the names of the functions.
This is offcourse applicable for more activities and/or multiple fragments.. for all logical cases.
Hilt can provide an instance of the generic Activity (and Fragment for that matter) as a dependency within the ActivityComponent (and FragmentComponent respectively). It's just not able to provide an instance of your specific MyActivity.
You can still create your own Component in Hilt. You will just have to manage the component instance on your own. Add the MyActivity as the seed data for the component builder and you should be able to #Bind your Callback with no problem.

How dynamically inject data with dagger?

I have SetupActivity and there in it’s viewModel I fetch data and then that data I need to pass it to MainActivity and it’s Fragment’s ViewModels , I am wondering instead of passing it is it possible to inject dynamically that settingsData in ViewModel's constructor's
settingsViewModel.settingsMutableData.observe(this, Observer {settingsData->
startActivity(MainActivity.getStartInent(this,settingsData))
})
#IntoMap
#ViewModelKey(PageViewModel::class)
abstract fun bindHomeViewModel(pageViewModel: PageViewModel): ViewModel```
Here is one of my ViewModel
```class PageViewModel #Inject constructor( homeRepository: HomeRepository) : ViewModel(), Injectable {
}```

Disable Dagger injection in tests

I have the following LoginFragment that uses Dagger to inject its fields:
class LoginFragment : DaggerFragment() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(LoginViewModel::class.java)
}
I also have a corresponding test that mocks the LoginViewModel according to the documentation from Google: "You can create the fragment and provide it a mock ViewModel."
#MediumTest
#RunWith(AndroidJUnit4::class)
class LoginFragmentTest {
#Mock
private lateinit var viewModel: LoginViewModel
#Before
fun setUp() {
loginFragment = LoginFragment()
loginFragment.viewModelFactory = createMockViewModelFactory(viewModel)
activityRule.activity.setFragment(loginFragment)
}
}
The problem is that when the onAttached method of the fragment is invoked, Dagger overrides the viewModelFactory with its own object, thus replacing my mock.
How can I prevent Dagger from overriding my mock object?
In the android-architecture-components samples on Github Google have an interesting solution.
They inject the activities trough ActivityLifecycleCallbacks. For instrumented tests they use a TestApp that does not register ActivityLifecycleCallbacks so it injects nothing.
Just like in your example the ViewModel.Factory is package private so in the test you can assign it yourself.
For Fragments there is FragmentManager.FragmentLifecycleCallbacks class which can be used. Instead of the Fragment injecting itself in onActivityCreated your production activity injects the Fragment using FragmentLifecycleCallbacks. You can create a testing activity which does not inject the fragment and you can create a mock factory yourself.

Categories

Resources