Mosius
Mosius

Reputation: 1682

Android Inject Dependency (Dagger 2) Inside Fragment Using @ContributesAndroidInjector

I'm using Dagger 2 for dependency injection in my project

The problem is when I'm trying to inject a ViewPageAdapter inside the fragment class which depend on FragmentManager like bellow:

@Inject
lateinit var mainTripsFragmentAdapter: MainTripsFragmentAdapter

I get the following build error:

error: [dagger.android.AndroidInjector.inject(T)] android.support.v4.app.FragmentManager cannot be provided without an @Provides-annotated method.
public abstract interface AppComponent extends dagger.android.AndroidInjector { ^ android.support.v4.app.FragmentManager is injected at com.example.presenters.main.trips.MainTripsFragmentAdapter.(fragmentManager, …) com.example.presenters.main.trips.MainTripsFragmentAdapter is injected at com.example.presenters.main.trips.MainTripsFragment.mainTripsFragmentAdapter com.example.presenters.main.trips.MainTripsFragment is injected at com.example.presenters.main.MainActivity.tripsFragment com.example.presenters.main.MainActivity is injected at dagger.android.AndroidInjector.inject(arg0) A binding with matching key exists in component: com.example.di.FragmentBuilder_BindMainTripsFragment$app_debug.MainTripsFragmentSubcomponent

I've researched for 3 days and I've read many articles but I couldn't figure out where the problem is!
I'll post the related code, sorry if it's too long

The Fragment which needs PagerAdapter:

class MainTripsFragment @Inject constructor() : BaseFragment() {

    @Inject
    lateinit var mainTripsFragmentAdapter: MainTripsFragmentAdapter

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        pager.adapter = mainTripsFragmentAdapter
    }
}

BaseFragment:

abstract class BaseFragment : DaggerFragment() {

    override fun onAttach(context: Context?) {
        AndroidSupportInjection.inject(this)
        super.onAttach(context)
    }

    override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
    ): View? = inflater.inflate(getLayout(), container, false)

    abstract fun getLayout(): Int
}

Here is the FragmentPagerAdapter:

class MainTripsFragmentAdapter @Inject constructor(
    fragmentManager: FragmentManager,
    private val mainCurrentTripsFragment: MainCurrentTripsFragment,
    private val mainHistoryTripsFragment: MainHistoryTripsFragment
) : FragmentPagerAdapter(fragmentManager) {

    override fun getItem(position: Int) = when (position) {
        0 -> mainCurrentTripsFragment
        1 -> mainHistoryTripsFragment
        else -> mainCurrentTripsFragment
    }

    override fun getCount() = 2
}

And here is DI related classes:

AppComponent:

@Singleton
@Component(modules = [
    AndroidInjectionModule::class,
    ActivityBuilder::class,
    FragmentBuilder::class
])

interface AppComponent : AndroidInjector<DaggerApplication> {

    fun inject(app: App)

    override fun inject(instance: DaggerApplication)

    @Component.Builder
    interface Builder {
        @BindsInstance
        fun application(app: Application): Builder

        fun build(): AppComponent
    }
}

FragmentBuilder:

@Module
abstract class FragmentBuilder {

    @ContributesAndroidInjector
    internal abstract fun bindMainHomeFragment(): MainHomeFragment

    @Module
    internal interface MainTripsFragmentModule {

        @Binds
        fun bindMainTripsFragment(fragment: MainTripsFragment): Fragment
    }

    @ContributesAndroidInjector(modules = [FragmentModule::class, MainTripsFragmentModule::class])
    internal abstract fun bindMainTripsFragment(): MainTripsFragment

    @ContributesAndroidInjector
    internal abstract fun bindMainCurrentTripsFragment(): MainCurrentTripsFragment
    @ContributesAndroidInjector
    internal abstract fun bindMainHistoryTripsFragment(): MainHistoryTripsFragment
}

FragmentModule:

@Module
class FragmentModule {

    @Provides
    fun provideFragmentManager(fragment: Fragment) = fragment.childFragmentManager
}

ActivityBuilder:

@Module
abstract class ActivityBuilder {

    @ContributesAndroidInjector
    internal abstract fun contributeMainActivity(): MainActivity
}

MainActivity:

class MainActivity : BaseActivity() {

     @Inject
     lateinit var tripsFragment: MainTripsFragment

     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)

         showFragment(tripsFragment)
     }

     private fun showFragment(fragment: Fragment) {
         supportFragmentManager
                 .beginTransaction()
                 .replace(R.id.layout_container, fragment)
                 .commit()
     }
 }

BaseActivity:

@SuppressLint("Registered")
abstract class BaseActivity : AppCompatActivity(), HasSupportFragmentInjector {

@Inject
lateinit var fragmentInjector: DispatchingAndroidInjector<Fragment>

    override fun onCreate(savedInstanceState: Bundle?) {
        AndroidInjection.inject(this)
        super.onCreate(savedInstanceState)
    }
    override fun supportFragmentInjector() = fragmentInjector
}

Application Class:

class App : Application(), HasActivityInjector {
    @Inject
    lateinit var activityInjector: DispatchingAndroidInjector<Activity>

    override fun activityInjector(): AndroidInjector<Activity> = activityInjector

    override fun onCreate() {
        super.onCreate()

        DaggerAppComponent.builder()
                .application(this)
                .build()
                .apply {
                    inject(this@App)
                }

    }
}

One point is that if I @Inject PagerAdapter Inside My Activity(I also should add a module for it) it works fine.
Which part is Implemented wrong?

Upvotes: 4

Views: 4217

Answers (1)

David Rawson
David Rawson

Reputation: 21407

There are a number of problems here:

class MainTripsFragmentAdapter @Inject constructor(
    fragmentManager: FragmentManager,
    private val mainCurrentTripsFragment: MainCurrentTripsFragment,
    private val mainHistoryTripsFragment: MainHistoryTripsFragment
) : FragmentPagerAdapter(fragmentManager) {

    override fun getItem(position: Int) = when (position) { 
        0 -> mainCurrentTripsFragment  //no!!
        1 -> mainHistoryTripsFragment
        else -> mainCurrentTripsFragment
    }

    override fun getCount() = 2
}

getItem in the PagerAdapter means "create item". When you use a PagerAdapter you delegate creation of Fragments to the adapter. You shouldn't write code to cache and store the Fragments, this is the job of the FragmentManager.

Instead your FragmentAdapter should look something like this:

    override fun getItem(position: Int) = when (position) { 
        0 -> MainCurrentTripsFragment.instantiate()
        1 -> MainHistoryTripsFragment.instantiate()
        else -> MainCurrentTripsFragment.instantiate()
    }

Where the instantiate method is declared inside a companion object inside your Fragments:

class MainCurrentTripsFragment: Fragment() {
    companion object {
        @JvmStatic
        fun instantiate(args: Bundle?) {
            val frag = MainCurrentTripsFragment()
            frag.arguments = args
        }
    }
}

That is the standard idiom for using a PagerAdapter with Fragments in Android. You shouldn't provide PagerAdapters or Fragments as dependencies in your object graph. In fact, many Android classes controlled by the OS like FragmentManager, Activity, Fragment, BroadcastReceiver, Service are not good candidates for becoming dependencies in your object graph using Dagger 2.

A common error when starting to use Dagger 2 is try and inject everything, as if newing up anything manually is wrong. This is just not possible in Android.

Please take the Fragments, the FragmentManager, the PagerAdapter etc out of your modules. Don't try to provide these or bind these using Dagger 2. Then use Dagger 2 to inject domain layer or data layer objects like repositories, Retrofit services etc. If you get stuck, you can always post another question.

And have a look at the offical Google Android Architecture blueprints. Your solution should, ideally, look something like them.

Upvotes: 3

Related Questions