team-supercharge / android-hilt-fragmentfactory

3 stars 1 forks source link

Unable to instantiate fragment, could not find Fragment constructor #1

Open Berki2021 opened 3 years ago

Berki2021 commented 3 years ago

Hello sir, I tried to implement your "improved fragmentfactory" as you had shown here.

The problem I have is, that I get the following error:

androidx.fragment.app.Fragment$InstantiationException: **Unable to instantiate fragment com.example0.app.presentation.documents.DocumentsFragment: could not find Fragment constructor**
        at androidx.fragment.app.FragmentFactory.instantiate(FragmentFactory.java:131)
        at com.example0.app.presentation.util.fragment.MainFragmentFactory.instantiate(FragmentFactory.kt:78)
        at androidx.navigation.fragment.FragmentNavigator.instantiateFragment(FragmentNavigator.java:132)
        at androidx.navigation.fragment.FragmentNavigator.navigate(FragmentNavigator.java:162)
        at androidx.navigation.fragment.FragmentNavigator.navigate(FragmentNavigator.java:58)
        at androidx.navigation.NavController.navigate(NavController.java:1066)
        at androidx.navigation.NavController.navigate(NavController.java:944)
        at androidx.navigation.NavController.navigate(NavController.java:877)
        at androidx.navigation.ui.NavigationUI.onNavDestinationSelected(NavigationUI.java:97)
        at androidx.navigation.ui.NavigationUI$5.onNavigationItemSelected(NavigationUI.java:531)
        at com.google.android.material.navigation.NavigationBarView$1.onMenuItemSelected(NavigationBarView.java:241)
        at androidx.appcompat.view.menu.MenuBuilder.dispatchMenuItemSelected(MenuBuilder.java:834)
        at androidx.appcompat.view.menu.MenuItemImpl.invoke(MenuItemImpl.java:158)
        at androidx.appcompat.view.menu.MenuBuilder.performItemAction(MenuBuilder.java:985)
        at com.google.android.material.navigation.NavigationBarMenuView$1.onClick(NavigationBarMenuView.java:110)
        at android.view.View.performClick(View.java:7125)
        at android.view.View.performClickInternal(View.java:7102)
        at android.view.View.access$3500(View.java:801)
        at android.view.View$PerformClick.run(View.java:27336)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7356)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
     Caused by: java.lang.NoSuchMethodException: com.example0.app.presentation.documents.DocumentsFragment.<init> []
        at java.lang.Class.getConstructor0(Class.java:2332)
        at java.lang.Class.getConstructor(Class.java:1728)
        at androidx.fragment.app.FragmentFactory.instantiate(FragmentFactory.java:121)
        at com.example0.app.presentation.util.fragment.MainFragmentFactory.instantiate(FragmentFactory.kt:78) 
        at androidx.navigation.fragment.FragmentNavigator.instantiateFragment(FragmentNavigator.java:132) 
        at androidx.navigation.fragment.FragmentNavigator.navigate(FragmentNavigator.java:162) 
        at androidx.navigation.fragment.FragmentNavigator.navigate(FragmentNavigator.java:58) 
        at androidx.navigation.NavController.navigate(NavController.java:1066) 
        at androidx.navigation.NavController.navigate(NavController.java:944) 
        at androidx.navigation.NavController.navigate(NavController.java:877) 
        at androidx.navigation.ui.NavigationUI.onNavDestinationSelected(NavigationUI.java:97) 
        at androidx.navigation.ui.NavigationUI$5.onNavigationItemSelected(NavigationUI.java:531) 
        at com.google.android.material.navigation.NavigationBarView$1.onMenuItemSelected(NavigationBarView.java:241) 
        at androidx.appcompat.view.menu.MenuBuilder.dispatchMenuItemSelected(MenuBuilder.java:834) 
        at androidx.appcompat.view.menu.MenuItemImpl.invoke(MenuItemImpl.java:158) 
        at androidx.appcompat.view.menu.MenuBuilder.performItemAction(MenuBuilder.java:985) 
        at com.google.android.material.navigation.NavigationBarMenuView$1.onClick(NavigationBarMenuView.java:110) 
        at android.view.View.performClick(View.java:7125) 
        at android.view.View.performClickInternal(View.java:7102) 
        at android.view.View.access$3500(View.java:801) 
        at android.view.View$PerformClick.run(View.java:27336) 
        at android.os.Handler.handleCallback(Handler.java:883) 
        at android.os.Handler.dispatchMessage(Handler.java:100) 
        at android.os.Looper.loop(Looper.java:214) 
        at android.app.ActivityThread.main(ActivityThread.java:7356) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930) 

Here are my fragments:

DocumentFragment

@AndroidEntryPoint
class DocumentsFragment @Inject constructor(
    private val fileValidator: FileValidator,
    private val snackbarUtils: InternetErrorSnackbar,
) : BusEventFragment<FragmentDocumentsBinding>(), SwipeRefreshLayout.OnRefreshListener {
    private val documentViewModel: DocumentViewModel by viewModels()
    @Inject
    lateinit var documentListAdapter: DocumentListAdapter

    override val bindingInflater: (LayoutInflater) -> FragmentDocumentsBinding
        get() = FragmentDocumentsBinding::inflate

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        bindObjects()
    }

    private fun bindObjects() {
        with(binding) {
            adapter = documentListAdapter
            refreshListener = this@DocumentsFragment
        }
    }

    override val onDestroyView: () -> Unit
        get() = {
            snackbarUtils.onDestroyView()
            binding.rvDocuments.adapter = null
        }
}

When I delete my dependencies from my fragment-constructor, everything works fine. Does that mean, that we are not able to constructor inject with your "improved" method??

Berki2021 commented 3 years ago

Okay, I found the actual problem. It has nothing to do with constructor injecting my fragments, but with my base class BusEventFragment. Because loadFragmentClass tries to load a fragment of type androidx.fragment.Fragment and not my special desired BusEventFragment, this does not work. I will try to work around and do this with my custom class

Berki2021 commented 3 years ago

Edit

Your FragmentFactory has one big flaw: If one wants to inject dependencies that need a fragment instance, it is not possible, because you are installing your FragmentFactoryModule inside SingletonComponent::class and therefore you can't get lifecycleaware dependencies into your fragments, because they have to be injected inside FragmentComponent::class

Berki2021 commented 3 years ago

Okay, here is the final fix for the problem and the correct way to instantiate all objects:

FragmentFactoryModule

@Module
@InstallIn(FragmentComponent::class) // NOT SINGLETONCOMPONENT::CLASS
abstract class FragmentFactoryModule {
    @Binds
    @IntoMap
    @FragmentKey(FirstFragment::class)
    abstract fun bindFirstFragment(fragment: FirstFragment): Fragment

    @Binds
    @IntoMap
    @FragmentKey(SecondFragment::class)
    abstract fun bindSecondFragment(fragment: SecondFragment): Fragment

    @Binds
    @IntoMap
    @FragmentKey(ThirdFragment::class)
    abstract fun bindThirdFragmentt(fragment: ThirdFragment): Fragment
}

@MapKey
@Retention(AnnotationRetention.RUNTIME)
annotation class FragmentKey(val value: KClass<out Fragment>)

NavHostModule

@Module
@InstallIn(FragmentComponent::class) // Since fragments are only available in FragmentComponent::class
abstract class NavHostModule {
    @Binds
    abstract fun provideNavHostFragment(fragment: MainFragmentFactory): FragmentFactory
}

FragmentFactoryProviderModule

@Module
@InstallIn(FragmentComponent::class)
object FragmentModule {

    @Provides
    fun provideMainFragmentFactory(providerMap: Map<Class<out Fragment>, @JvmSuppressWildcards Provider<Fragment>>)
        = MainFragmentFactory(providerMap)
}

Fragment Factory

@Singleton
class MainFragmentFactory (
    private val providerMap: Map<Class<out Fragment>, @JvmSuppressWildcards Provider<Fragment>>
) : FragmentFactory() {
    override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
        val fragmentClass = loadFragmentClass(classLoader, className)

        val creator = providerMap[fragmentClass] ?: providerMap.entries.firstOrNull {
            fragmentClass.isAssignableFrom(it.key)
        }?.value

        return creator?.get() ?: super.instantiate(classLoader, className)
    }
}

NavHostFragment

@AndroidEntryPoint
class MainNavHostFragment : NavHostFragment() {
    @Inject lateinit var mainFragmentFactory: FragmentFactory

    override fun onAttach(context: Context) {
        super.onAttach(context)
        childFragmentManager.fragmentFactory = mainFragmentFactory
    }
}