alex-townsend / FragmentFactoryDaggerSample

Sample using AndroidX's FragmentFactory and Dagger to allow for constructor injection of Fragments.
MIT License
16 stars 2 forks source link

Not working when used with automatic injection #1

Open matpag opened 5 years ago

matpag commented 5 years ago

Hi, thank you for your amazing tutorial. I've tried to implement this pattern using the automatic injection took from GithubBrowserExample provided by Google. I'm referring about this class

object AppInjector {
    fun init(app: App) {
        DaggerAppComponent.builder().application(app).build()
            .inject(app)
        app.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
                override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
                    handleActivity(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 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
                )
        }
    }

But using this approach (which probably moved the injection after the super.onCreate call in the MainActivity) everything is broken and I get the error Caused by: java.lang.IllegalArgumentException: No injector factory bound for Class<com.atownsend.fragmentfactorysample.ui.MainFragment>

Is there a way to let this work without manually injecting in every activity?

Thank you :)

alex-townsend commented 5 years ago

The error seems to suggest your Dagger setup is incorrect -- did you change anything with the setup there? Also, I believe these calls will be triggered after onCreate finishes, meaning your custom FragmentFactory will not be injected and set before your Activity has called super.onCreate(savedInstanceState). This will break the restoration of your Fragments during configuration changes or process death, as the FragmentFactory will not be set before the fragments are created.

matpag commented 5 years ago

This is my Application class:

class AppApplication : Application(), HasActivityInjector {

    @Inject
    lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Activity>

    override fun onCreate() {
        super.onCreate()
        AppInjector.init(this)
    }

    override fun activityInjector() = dispatchingAndroidInjector
}

AppComponent

@Singleton
@Component(
    modules = [
        AndroidSupportInjectionModule::class,
        AppModule::class,
        MainActivityModule::class
    ]
)
interface AppComponent {
    @Component.Builder
    interface Builder {
        @BindsInstance
        fun application(application: Application): Builder

        fun build(): AppComponent
    }

    fun inject(app: AppApplication)
}

FragmentFactory

@PerActivity
class DefaultFragmentFactory @Inject constructor(
    private val creators: Map<Class<out Fragment>, @JvmSuppressWildcards Provider<Fragment>>
) : FragmentFactory() {
    override fun instantiate(classLoader: ClassLoader, className: String, args: Bundle?): Fragment {
        val fragmentClass = loadFragmentClass(classLoader, className)
        val creator = creators[fragmentClass]
            ?: throw java.lang.IllegalArgumentException("Unknown fragment class $fragmentClass")
        try {
            val fragment = creator.get()
            fragment.arguments = args
            return fragment
        } catch (e: Exception) {
            throw RuntimeException(e)
        }
    }
}

FragmentModule

@Module
abstract class FragmentModule {

    @Binds
    @IntoMap
    @FragmentKey(SearchFragment::class)
    abstract fun bindSearchFragment(searchFragment: SearchFragment): Fragment

    @Binds
    abstract fun bindFragmentFactory(factory: DefaultFragmentFactory): FragmentFactory
}

ActivityModule

@Suppress("unused")
@Module
abstract class MainActivityModule {

    @PerActivity
    @ContributesAndroidInjector(modules = [FragmentModule::class])
    abstract fun contributeMainActivity(): MainActivity
}

I omitted AppModule because I don't think the problem can reside there.

Sincerely I don't know what I'm doing wrong... Probably it's my fault because I don't have a big experience with Dagger

alex-townsend commented 5 years ago

You have no multibinding mapping for MainFragment in your FragmentModule, only for SearchFragment -- this is causing your error.

matpag commented 5 years ago

Can you help me a bit more Alex? What should I add in FragmentModule? Thank you

matpag commented 5 years ago

I think I've found the problem. Now is working fine. The only problem that I still have (even with your example) is that if I try to use DaggerFragment the app crash because internally it still searches for android.support.v4.app.Fragment and not androidx.fragment.app.Fragment so the injector doesn't recognize the correct class to inject

BTW I'm on Dagger 2.21 and still have the problem

Thanks Alex 👍

alex-townsend commented 5 years ago

Try adding these two lines to your project's gradle.properties file:

android.useAndroidX=true
android.enableJetifier=true
matpag commented 5 years ago

Yeah, the first thing I tried following some advices found online but it's not working. I invalidated and restarted AS too without success

At the end I had to convert the library manually, I've added an answer here: https://stackoverflow.com/a/54705489/2910520

alex-townsend commented 5 years ago

You could also try changing the version of jetifier your build is using -- add this to your root build.gradle's script:

classpath("com.android.tools.build.jetifier:jetifier-processor:1.0.0-beta03")
matpag commented 5 years ago

I've tried but didn't work with those 2 lines:

classpath 'com.android.tools.build.jetifier:jetifier-core:1.0.0-beta03'
classpath 'com.android.tools.build.jetifier:jetifier-processor:1.0.0-beta03'
matpag commented 5 years ago

Hi Alex, i've created a question here: https://stackoverflow.com/questions/55043890/dagger-2-how-to-bind-fragment-map-in-parent-component-from-subcomponent to be able to scope your fragment factory implementation with sub-component.

If you have time I'll be glad to have an help from you. In the question I added a link to your post because it's really useful :)

alex-townsend commented 5 years ago

I have not worked through this scenario yet, but it is possible to have subcomponents add to the multibinding maps of their parent components. FragmentFactory is still in alpha, but once it has been fully released Dagger itself will probably have better support for it to more easily allow for scoping like this. I believe there is already an issue for this on Dagger's Github.

matpag commented 5 years ago

Thank you Alex. I will search the issue tonight and maybe directly ask to ronshapiro for help

matpag commented 5 years ago

Currently they have deprecated the instantiate method with the Bundle argument in fragment-ktx 1.1.0-alpha06, so new FragmentFactory should be like this or will crash at runtime (discovered this while updating Navigation library to 2.1.0-alpha02)

class DefaultFragmentFactory @Inject constructor(
    private val creators: Map<Class<out Fragment>, @JvmSuppressWildcards Provider<Fragment>>
) : FragmentFactory() {
    //override this if you were using the bundle before or remove it if you were not
    override fun instantiate(classLoader: ClassLoader, className: String, args: Bundle?): Fragment {
        return instantiate(classLoader, className).apply { arguments = args }
    }
    override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
        val fragmentClass = loadFragmentClass(classLoader, className)
        val found = creators.entries.find { fragmentClass.isAssignableFrom(it.key) }
        val provider = found?.value
        //if we don't find a match in the map, proceed with the default empty constructor, for example if used with
        //navigation component this is the case of NavHostFragment or others system/libraries generated Fragments
        return if (provider != null) {
            provider.get()
        } else {
            fragmentClass.newInstance()
        }
    }
}