Disable Dagger injection in tests - android

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.

Related

Koin. Cannot inject to fragment with ScopeActivity

I'm trying to inject some dependency to both activity and fragment using Koin and I expect it to live as long as activity lives, but it turned out a headache for me.
I managed to create a module that resolves MainRouter, inject it into an activity, but it doesn't work for a fragment.
val appModule = module {
scope<MainActivity> {
scoped { MainRouter() }
}
}
MainActivity extends ScopeActivity, MyFragment extends ScopeFragment.
in MainActivity private val router : MainRouter by inject() works fine, but in MyFragment it throws org.koin.core.error.NoBeanDefFoundException: No definition found for class:'com.example.app.MainRouter'. Check your definitions!
Finally I managed to inject, but it doesn't look pretty
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val scopeId = scopeActivity!!.getScopeId()
scope.linkTo(getKoin().getScope(scopeId))
mainRouter = get()
...
I also don't like that scopeActivity can't be accessed in the init method. Does this mean that activity scoped dependencies cannot be resolved in fragment using by inject()?
As I can see in your code, you have to declare a Fragment instance, just declare it as a fragment in your Koin module and use constructor injection. Like below:
val appModule = module {
single { MyService() }
fragment { MyFragment(get()) }
}
Please refer link for more details.

Dagger Hilt - How do I inject the ViewModel into the Adapter?

I am trying to inject the ViewModel into the adapter. It works fine while injecting into Fragment.
ViewModel:
class HomeViewModel #ViewModelInject constructor(
): ViewModel()
Fragment:
#AndroidEntryPoint
class HomeFragment : BaseFragment<FragmentHomeBinding, HomeViewModel>(
R.layout.fragment_home
) {
private val viewModel: HomeViewModel by viewModels()
There is no problem so far. But problems arise when I try to inject into the adapter.
class HomeListAdapter #Inject constructor(
): BaseListAdapter<Users>(
itemsSame = { old, new -> old.username == new.username },
contentsSame = { old, new -> old == new }
) {
private val viewModel: HomeViewModel by viewModels() //viewModels() unresolved reference
UPDATE:
If I try to use constructor injection or field injection I get the following error:
error: [Dagger/MissingBinding] ***.home.HomeViewModel cannot be provided without an #Inject constructor or an #Provides-annotated method.
public abstract static class ApplicationC implements App_GeneratedInjector,
^
***.home.HomeViewModel is injected at
***.home.adapter.HomeListAdapter.viewModel
***.home.adapter.HomeListAdapter is injected at
***.home.HomeFragment.viewAdapter
***.home.HomeFragment is injected at
***.home.HomeFragment_GeneratedInjector.injectHomeFragment(***.home.HomeFragment) [***.App_HiltComponents.ApplicationC → ***.App_HiltComponents.ActivityRetainedC → ***.App_HiltComponents.ActivityC → ***.App_HiltComponents.FragmentC]
Adapter:
class HomeListAdapter #Inject constructor(
): BaseListAdapter<Users>(
itemsSame = { old, new -> old.username == new.username },
contentsSame = { old, new -> old == new }
) {
#Inject lateinit var viewModel: HomeViewModel;
Generally, you should not inject ViewModel into Adapter, because Adapter is a part of presentation layer and an Android-specific thing. ViewModel is something independent on Android in general, though ViewModel from AAC is tied to it.
You should get your data in ViewModel and pass it to Fragment via LiveData, and then populate your Adapter from within Fragment.
by viewModels() is not defined in Adapter since it is an extension function of Fragment and could be used only within Fragment. So, move your ViewModel away from Adapter back to Fragment. That will also fix your compilation error since Hilt doesn't inject into Adapters.

Hilt - EntryPoint in Fragment

I am using Hilt for DI and I have this class.
class ChatCore #Inject constructor()
This class needs to be injected in fragment , without marking the fragment as #AdroidEntryPoint as this fragment can be attached to activity which isn't marked as #AndroidEntryPoint
How can i achieve this. I tried using EntryPoint but i end up with error.
class MyFragment : Fragment() {
lateinit var chatCore: ChatCore
#EntryPoint
#InstallIn(FragmentComponent::class)
interface ChatCoreProviderEntryPoint{
fun chatCore():ChatCore
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val hiltEntryPoint = EntryPointAccessors.fromFragment(this, ChatCoreProviderEntryPoint::class.java)
chatCore = hiltEntryPoint.chatCore()
}
Solved it by adding it into the application container.
#EntryPoint
#InstallIn(ApplicationComponent::class)
interface ChatCoreProviderEntryPoint{
fun chatCore():ChatCore
}
val hiltEntryPoint = EntryPointAccessors.fromApplication(applicationContext,
ChatCoreProviderEntryPoint::class.java)
If you don't want to use AndroidEntryPoint for your Fragment you need to #Install your module (containing your dependency) within a different Component.
E.g. within the ApplicationComponent not the FragmentComponent.
Then you will also need to use the corresponding EntryPointAccessors.fromXyz(...) method. E.g. for a module installed in the ApplicationComponent you should be using EntryPointAccessors.fromApplication(...).

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.

HILT : lateinit property repository has not been initialized in ViewModel

I am facing this issue in multi module android project with HILT.
kotlin.UninitializedPropertyAccessException: lateinit property repository has not been initialized in MyViewModel
My modules are
App Module
Viewmodel module
UseCase Module
DataSource Module
'App Module'
#AndroidEntryPoint
class MainFragment : Fragment() {
private lateinit var viewModel: MainViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.main_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
viewModel.test()
}}
'ViewModel Module'
class MainViewModel #ViewModelInject constructor(private val repository: MyUsecase): ViewModel() {
fun test(){
repository.test()
}}
'UseCase Module'
class MyUsecase #Inject constructor() {
#Inject
lateinit var feature: Feature
fun doThing() {
feature.doThing()
}
#Module
#InstallIn(ApplicationComponent::class)
object FeatureModule {
#Provides
fun feature(realFeature: RealFeature): Feature = realFeature
}
}
'DataSource Module'
interface Feature {
fun doThing()
}
class RealFeature : Feature {
override fun doThing() {
Log.v("Feature", "Doing the thing!")
}
}
Dependencies are
MyFragment ---> MyViewModel ---> MyUseCase ---> DataSource
what i did wrong with this code pls correct it.
above your activity class you must add annotation #AndroidEntryPoint
as below:
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
In addition to moving all your stuff to constructor injection, your RealFeature isn't being injected, because you instantiate it manually rather than letting Dagger construct it for you. Note how your FeatureModule directly calls the constructor for RealFeature and returns it for the #Provides method. Dagger will use this object as is, since it thinks you've done all the setup for it. Field injection only works if you let Dagger construct it.
Your FeatureModule should look like this:
#Module
#InstallIn(ApplicationComponent::class)
object FeatureModule {
#Provides
fun feature(realFeature: RealFeature): Feature = realFeature
}
Or with the #Binds annotation:
#Module
#InstallIn(ApplicationComponent::class)
interface FeatureModule {
#Binds
fun feature(realFeature: RealFeature): Feature
}
This also highlights why you should move to constructor injection; with constructor injection, this mistake wouldn't have been possible.
The problem in the code is that #ViewModelInject doesn't work as #Inject in other classes. You cannot perform field injection in a ViewModel.
You should do:
class MainViewModel #ViewModelInject constructor(
private val myUseCase: MyUsecase
): ViewModel() {
fun test(){
myUseCase.test()
}
}
Consider following the same pattern for the MyUsecase class. Dependencies should be passed in in the constructor instead of being #Injected in the class body. This kind of defeats the purpose of dependency injection.
First, i think you are missing #Inject on your RealFeature class, so the Hilt knows how the inject the dependency. Second, if you want to inject into a class that is not a part of Hilt supported Entry points, you need to define your own entry point for that class.
In addition to the module that you wrote with #Provides method, you need to tell Hilt how the dependency can be accessed.
In your case you should try something like this:
#EntryPoint
#InstallIn(ApplicationComponent::class)
interface FeatureInterface {
fun getFeatureClass(): Feature
}
Then, when you want to use it, write something like this:
val featureInterface =
EntryPoints.get(appContext, FeatureInterface::class.java)
val realFeature = featureInterface.getFeatureClass()
You can find more info here:
https://dagger.dev/hilt/entry-points
https://developer.android.com/training/dependency-injection/hilt-android#not-supported
class MainViewModel #ViewModelInject constructor(private val repository: HomePageRepository,
#Assisted private val savedStateHandle: SavedStateHandle)
: ViewModel(){}
and intead of initializing the viewmodel like this :
private lateinit var viewModel: MainViewModel
viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
Use this directly :
private val mainViewModel:MainViewModel by activityViewModels()
EXplanation :
assisted saved state handle : will make sure that if activity / fragment is annotated with #Android entry point combined with view model inject , it will automatically inject all required constructor dependencies available from corresonding component activity / application so that we won't have to pass these parameters while initializing viewmodel in fragment / activity
Make sure you added class path and plugin
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.35'
in Project.gradle
apply plugin: 'dagger.hilt.android.plugin'
in app.gradle

Categories

Resources