I am still pretty new to Daggers dependency injection. I am working on a new application utilizing Dagger 2 and Kotlin. I started with a basic starter app meant for building on. Within App.kt every activity is being injected automatically, which up until now is pretty cool. However I am running into an issue now with implementing Facebook and Google social logins.
When the app tries to launch Facebook or Googles sign in activities I get the error:
"No injector factory bound for Class<external.activities.classNameHere>"
I cannot #Provides those external classes since they do not implement the #Module annotation.
My temporary solution is to check the activity being injected before the automatic injection, and skip those external classes. This seems a little odd though, I am wondering if there is a better solution to this or if I am missing something. I can see this if statement getting pretty long over time.
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks() {
override fun onActivityCreated(p0: Activity?, p1: Bundle?) {
p0?.let {
if (p0 is FacebookActivity || p0 is CustomTabMainActivity || p0 is CustomTabActivity ) {
Log.d("KSULog", "App.kt is not injecting activity " + p0.toString())
}
else {
AndroidInjection.inject(p0)
}
}
}
})
}
Thanks for taking a look.
The way to do this is quite simple.
If You look at Google Samples You will have a clear direction. Like GitHubBrowserSample
So You will create an interface Injectable like this, basically a marker interface.
/**
* Marks an activity / fragment injectable.
*/
interface Injectable
Each activity or fragment will implement this interface for example like this (in Kotlin)
open class BaseActivity : AppCompatActivity(),Injectable {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
lateinit var baseActivityViewModel: BaseActivityViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidInjection.inject(this)
baseActivityViewModel = ViewModelProviders.of(this, viewModelFactory)
.get(BaseActivityViewModel::class.java)
}
}
Important lines are :
open class BaseActivity : AppCompatActivity(),Injectable
AndroidInjection.inject(this)
Create an Activity module to contribute Activity object
/**
* Module to contribute all the activities.
*/
#Module
abstract class ActivityModule {
#ContributesAndroidInjector
internal abstract fun contributeSplashActivity(): SplashActivity
}
and finally DaggerInjector to enable injection
/**
* Helper to inject all the activities and fragments that are marked Injectable.
*/
object DaggerInjector {
fun injectAll(application: TurtleApp) {
DaggerAppComponent.builder()
.application(application)
.build().inject(application)
application
.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
injectComponents(activity)
}
override fun onActivityStarted(activity: Activity) {
}
override fun onActivityResumed(activity: Activity) {
}
override fun onActivityPaused(activity: Activity) {
}
override fun onActivityStopped(activity: Activity) {
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
}
override fun onActivityDestroyed(activity: Activity) {
}
})
}
private fun injectComponents(activity: Activity) {
if (activity is Injectable) {
AndroidInjection.inject(activity)
}
// (activity as? FragmentActivity)?.supportFragmentManager?.registerFragmentLifecycleCallbacks(
// object : FragmentManager.FragmentLifecycleCallbacks() {
// override fun onFragmentCreated(fm: FragmentManager?, f: Fragment?,
// savedInstanceState: Bundle?) {
// if (f is Injectable) {
// AndroidSupportInjection.inject(f)
// }
// }
// }, true)
}
}
Uncomment the code to enable Fragment injection.
Your solution is fine but as you say it won't scale well.
You can have a look at one of the Google Samples where they implement a HasSupportFragmentInjector interface to determine whether they want to inject an Activity.
private fun handleActivity(activity: Activity) {
if (activity is HasSupportFragmentInjector) {
AndroidInjection.inject(activity)
}
if (activity is FragmentActivity) {
activity.supportFragmentManager
.registerFragmentLifecycleCallbacks(
object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentCreated(
fm: FragmentManager,
f: Fragment,
savedInstanceState: Bundle?
) {
if (f is Injectable) {
AndroidSupportInjection.inject(f)
}
}
}, true
)
}
}
You should be able to inject these as you would other classes. Providing the examples in Java. Assuming you have AppComponent and AppModule classes:
#Component(modules = AppModule.class)
public interface AppComponent {
....
void inject(App app);
....
}
#Module
public class AppModule {
#Provides
FacebookActivity providesFacebookActivity() {
return new FacebookActivity();
}
}
Then you can annotate the FacebookActivity to be injected into your main activity.
#Inject FacebookActivity mFacebookActivity;
So my external activity is injected into my main activity, which in turn implements the AndroidInjection.inject(this) defined in the AppComponent. The component links to the AppModule which has the #Provides for the FacebookActivity.
Related
Is there a way to mock ViewModel that's built is inside of a fragment? I'm trying to run some tests on a fragment, one of the fragment functions interacts with the ViewModel, I would like to run the test function and provided a mocked result for the ViewModel. Is this even possilbe?
MyFragment
class MyFragment : Fragment() {
#Inject
lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
(requireActivity().application as MyApplication).appComponent.inject(this)
super.onCreate(savedInstanceState)
}
}
Test
#RunWith(RoboeltricTestRunner::) {
#Before
fun setup() {
FragmentScenario.Companion.launchIncontainer(MyFragment::class.java)
}
}
Yeah, just mark your ViewModel open and then you can create a mock implementation on top of it.
open class MyViewModel: ViewModel() {
fun myMethodINeedToMock() {
}
}
class MockMyViewModel: MyViewModel() {
override fun myMethodINeedToMock() {
// don't call super.myMethodINeedToMock()
}
}
So, register your MockMyViewModel to the DI framework when testing.
I thought I would post this for anyone else struggling to find a solution. You'll want to use a Fragment Factory, that has a dependency on the ViewModel. Injecting the ViewModel into the fragments constructor allows the ViewModel to easliy be mocked. There are a few steps that need to be completed for a FragmentFactory but it's not that complicated once you do a couple of them.
Fragment Add the ViewModel into the constructor.
class MyFragment(private val viewModel: ViewModel) : Fragment {
...
}
FragmentFactory, allows fragments to have dependencies in the constructor.
class MyFragmentFactory(private val viewModel: MyViewModel) : FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
return when(className) {
MyFirstFragment::class.java.name -> {
MyFragment(viewModel)
}
// You could use this factory for multiple Fragments.
MySecondFragment::class.java.name -> {
MySecondFragment(viewModel)
}
// You also don't have to pass the dependency
MyThirdFragment::class.java.name -> {
MyThirdFragment()
}
else -> super.instantiate(classLoader, className)
}
}
}
Main Activity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Create your ViewModel
val viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
// create the FragmentFactory and the viewModel dependency.
supportFragmentManager.fragmentFactory = MainFragmentFactory(viewModel)
// FragmentFactory needs to be created before super in an activity.
super.onCreate(savedInstanceState)
}
}
Test
#RunWith(RobolectricTestRunner::class)
class MyFragmentUnitTest {
#Before
fun setup() {
val viewModel: MainViewModel = mock(MyViewModel::class.java)
...
}
}
I have a single activity application.
My MainActivity is referenced in a number of dependency injection modules, as the implementer of these interfaces. I currently have a work around, which is less than ideal.
class MainActivity : TransaktActivity(), RegistrationNavigator, IAuthPresenter,
IAuthButtonNavigator {
override fun navigateAwayFromAuth() {
navController.navigate(R.id.homeFragment)
}
override fun navigateToAuthPin(buttonId: Int) {
//todo navigate to auth with pin fragment
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_mainActivity = this
setContentView(R.layout.activity_main)
}
companion object {
private var _mainActivity: MainActivity? = null
fun getInstance() = _mainActivity
}
}
interface RegistrationNavigator {
fun navigateToCardDetails()
fun navigateToOtpCapture()
fun navigateToLoading()
fun navigateOutOfCardRegistration()
}
The appModule is a Koin Module
val appModule = module {
viewModel { SharedViewModel() }
single { MainActivity.getInstance() as RegistrationNavigator }
}
What is the preferred way of achieving this?
Android-lifecycled components such as activities should not be in koin modules.
For example you will have issues with e.g. configuration changes since the koin module would be serving references to stale activity after the activity is recreated.
I haven't really worked with NavController but rather rolled up my own navigation solution. As a generic approach I would refactor the RegistrationNavigator implementation to a separate class the instance of which you can provide from your koin module. If lifecycle-dependent params such as Context (or NavController) are needed, supply them as function args.
I'm trying to add Dagger 2 in one project. I have created 3 components
AppComponent (the primary one)
RetrofitComponent (Dependent component works fine)
ControllerComponent (Subcomponent, not injecting properly)
this is my AppComponent
#ApplicationScope
#Component(modules = [ApplicationModule::class, PrefModule::class])
interface AppComponent {
fun inject(instance: AppInstance)
fun getAppPref(): AppPreference
fun newControllerComponent(controllerModule: ControllerModule): ControllerComponent
}
this is my ControllerComponent
#UIScope
#Subcomponent(modules = [ControllerModule::class])
interface ControllerComponent {
fun injectActivity(activity: BaseActivity)
fun injectDialog(dialog: BaseDialog)
}
ControllerModule
#Module
class ControllerModule(activity: FragmentActivity) {
var mActivity: FragmentActivity
init {
mActivity = activity
}
#Provides
fun getContext(): Context {
return mActivity
}
#Provides
fun getActivity(): Activity {
return mActivity
}
#Provides
fun getFragmentManager(): FragmentManager {
return mActivity.supportFragmentManager
}
#Provides
fun getDialogManager(fragmentManager: FragmentManager): DialogsManager
{
return DialogsManager(fragmentManager)
}
#Provides
fun getDialogsFactory(): DialogsFactory {
return DialogsFactory()
}
}
Injecting the activity is working fine, but when I try to inject the Dialog its never injecting
controller.injectDialog(singleDialog)
My entire code is here Github link. Need some help. What am I not doing or doing wrong that I can't inject from subcomponent.
To use field injection on a given class, you need to specify the concrete type rather than its superclass.
#UIScope
#Subcomponent(modules = [ControllerModule::class])
interface ControllerComponent {
//fun injectActivity(activity: BaseActivity)
//fun injectDialog(dialog: BaseDialog)
fun inject(activity: SomeSpecificActivity)
fun inject(activity: SomeOtherActivity)
fun inject(dialog: SomeSpecificDialog)
fun inject(dialog: SomeOtherDialog)
}
The Injection is working, but you are injecting nothing inside SingleClickDialog class, remember than the method inject(class : Class) is for let it to know dagger that class will be injected, but you need to put the elements you need inject inside that class with the annotation #Inject, and those elements you need to add its provider in ControllerModule as well
I have a hard time figuring out how to set up Dagger component hierarchy to match my vision. Any help would be greatly appreciated and please feel free to criticise the vision if you find any holes in it.
Let's get to the point.
I'm trying to redesing part of my application related to user registration. Since registration is a self contained domain I assume that it's a good candidate for a separate dagger component(RegistrationSubcomponent). Within that registration domain I will show an activity(RegistrationActivity) and on top of it few fragments(RegistrationFragment1, RegistrationFragment2, ...). I already have an application component(AppComponent) with the life scope of an application which should be a parent for RegistrationSubcomponent. AppComponent is used to inject dependecies into other activites excluding RegistrationActivity but also to a common activity(CommonActivity) which is a parent for every activity in my app including RegistrationActivity. To inject dependencies into RegistrationActivity i want to use RegistrationSubcomponent.
The RegistrationSubcomponent should be instantiated shortly before displaying RegistrationActivity and should live until after the RegistrationActivity returns. I dont't want to bind RegistrationSubcomponent lifecycle with RegistrationActivity lifecycle. Mayby in this case it may have sense but in future I will probably would like to create components for other app domains which may not neceserralily match the lifecycle of activities they encompass.
In general the problem is injection of dependecies specfied in RegistrationComponent into RegistrationActivity as well as to RegistrationFragments.
The problem is that the app crashes when navigating to RegistrationActivity with following error:
Unable to start activity ComponentInfo{pl.kermit.diproblem/pl.kermit.diproblem.activities.RegistrationActivity}: java.lang.IllegalArgumentException: No injector factory bound for Class
Minimal project presenting the problem can be found at https://github.com/mariola3000/DIProblemStack01
Below I put the most important parts of code
// APP COMPONENT
#Singleton
#Component(modules = [AndroidInjectionModule::class, AppModule::class, AppAndroidBindingModule::class,
AndroidSupportInjectionModule::class])
interface AppComponent {
#Component.Builder
interface Builder {
#BindsInstance
fun application(application: Application): Builder
fun build(): AppComponent
}
fun inject(application: Application)
}
#Module(subcomponents = [RegistrationComponent::class])
class AppModule {
#Provides
fun provideGlobalDependency(): GlobalDependency {
return GlobalDependency("GlobalDependency")
}
#Provides
fun provideGlobal2Dependency(): Global2Dependency {
return Global2Dependency("Global2Dependency")
}
}
#Module
abstract class AppAndroidBindingModule {
#PerActivity
#ContributesAndroidInjector()
abstract fun bindMainActivity(): MainActivity
}
// REGISTRATION COMPONENT
#PerRegistration
#Subcomponent(modules = [RegistrationModule::class, RegistrationAndroidBindingModule::class])
interface RegistrationComponent {
#Subcomponent.Builder
interface Builder {
fun requestModule(module: RegistrationModule): Builder
fun build(): RegistrationComponent
}
}
#Module
class RegistrationModule {
#PerRegistration
#Provides
fun provideRegistrationDependency(): RegistrationDependency {
return RegistrationDependency("RegistrationDependency")
}
#PerRegistration
#Provides
fun provideRegistrationFragmentDependency(): RegistrationFragmentDependency {
return RegistrationFragmentDependency("RegistrationFragmentDependency")
}
}
#Module
abstract class RegistrationAndroidBindingModule {
#PerActivity
#ContributesAndroidInjector
abstract fun bindRegistrationActivity(): RegistrationActivity
#PerFragment
#ContributesAndroidInjector
abstract fun bindRegistrationFragment(): RegistrationFragment
}
// ACTIVITIES
abstract class CommonActivity : DaggerAppCompatActivity(){
#Inject
lateinit var globalDependency: GlobalDependency
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
Toast.makeText(this, "global message: " + globalDependency.message, Toast.LENGTH_LONG).show()
}
}
class MainActivity : CommonActivity() {
#set:Inject
lateinit var global2Dependency: Global2Dependency
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val text: TextView = findViewById(R.id.main_text)
text.text = global2Dependency.message
val button: Button = findViewById(R.id.main_button)
button.setOnClickListener {
startActivity(Intent(this, RegistrationActivity::class.java))
}
}
}
class RegistrationActivity : CommonActivity() {
#Inject
lateinit var registrationDependency: RegistrationDependency
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_registration)
val registrationMessage = findViewById<TextView>(R.id.registration_text)
registrationMessage.text = registrationDependency.message
val transaction = this.supportFragmentManager.beginTransaction()
val fragment = RegistrationFragment()
transaction.replace(R.id.registration_fragment_container, fragment)
transaction.addToBackStack(fragment.javaClass.simpleName)
transaction.commit()
}
}
I am a newbie to android and trying to use Dagger2. I spend whole night and still dont know why my dagger does not provide presenter. Here are my code (I use Kotlin)
AppComponent
#Singleton
#Component(modules = arrayOf(PresenterModule::class))
interface AppComponent {
fun inject(target: SplashActivity)
}
PresenterModule
#Module
class PresenterModule {
#Provides
#Singleton
fun provideSplashPresenter(): SplashPresenter {
return SplashPresenter()
}
}
App
class App: Application() {
companion object {
lateinit var appComponent: AppComponent
}
override fun onCreate() {
super.onCreate()
appComponent = initDagger()
}
private fun initDagger(): AppComponent {
return DaggerAppComponent.create()
}
}
This is the presenter
class SplashPresenter: BasePresenterImpl<SplashContract.View>(), SplashContract.Presenter {
override fun performToast(mess: String) {
logi("abc", "performToast")
logi("abc", "mess: " + mess)
mView?.showLoading()
if (mess.isNullOrBlank()) {
mView?.showTosat("this is empty mess") ?: logi("abc", "null")
} else {
mView?.showTosat(mess) ?: logi("abc", "null")
}
mView?.hideLoading()
}
}
And finally, this is my SplashActivity
class SplashActivity : BaseActivity(), SplashContract.View {
#Inject
lateinit var presenter: SplashPresenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_splash)
presenter.attachView(this)
//TODO: check log in
//TODO: If logged in => start main screen
//TODO: If not logged in => load login activity
button.setOnClickListener{
presenter.performToast(editText.text.toString())
logi("abc", "perform clicked")
}
}
}
When I run these code, I got this error
Lateinit property presenter has not been initialized, which means that "Inject" does not work
Since you're not using constructor injection here (which you can't, because you don't 'own' the activity's constructor) Dagger does not 'know' that it has to inject something into your Activity.
You have to manually inject like this:
(applicationContext as App).appComponent.inject(this)
in your SplashActivity's onCreate() method (before using the presenter, of course).
Second, your presenter needs a constructor that tells Dagger how to construct/'build' the presenter, which means a constructor annotated with the #Inject annotation, so:
class SplashPresenter #Inject constructor(): BasePresenterImpl<SplashContract.View>(), SplashContract.Presenter
You forgot to inject the SplashActivity:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
App.appComponent.inject(this)
setContentView(R.layout.activity_splash)
...