有缺陷的androidxfragmentfactory

In this blog post I would like to express my disappointment in the AndroidX FragmentFactory. I will briefly describe what the FragmentFactory is, and why I think it is defective.

在此博客文章中,我想对AndroidX FragmentFactory表示失望。 我将简要描述FragmentFactory是什么,以及为什么我认为它有缺陷。

I already tried to bring Google’s attention to this problem by opening an issue a year ago. But after quite a long discussion the issue was closed without any reasonable solution to the problem.

一年前,我已经尝试通过打开一个问题来引起Google对这个问题的关注。 但是经过长时间的讨论,该问题没有任何合理的解决方案而被关闭。

The purpose of this blog post is to bring attention of the community.

这篇博客的目的是引起社区的关注。

历史 (History)

Historically, fragments are usually created by the FragmentManager automatically. Just like activities are automatically created by the system. Every fragment is expected to have a constructor without arguments. Once a fragment is added to the FragmentManager, the latter takes care of recreating the fragment after configuration changes and/or process death. The FragmentManager remembers classes of all fragments in the back stack, and calls empty constructors via reflection when needed.

从历史上看,片段通常由FragmentManager自动创建。 就像系统自动创建活动一样。 每个片段都应具有不带参数的构造函数。 将片段添加到FragmentManager后,后者将负责在配置更改和/或进程终止后重新创建片段。 FragmentManager记住后堆栈中所有片段的类,并在需要时通过反射调用空的构造函数。

Suppose we have the following sample UserFragment:

假设我们有以下示例UserFragment:

This fragment is responsible for loading and displaying a user’s profile with the given id. This is not necessarily a full screen fragment. It might just be a small piece, like avatar, name and possibly some additional information. So the UserFragment is a resuable component, it should not make any assumptions about the use case.

该片段负责加载和显示具有给定ID的用户个人资料。 这不一定是全屏片段。 它可能只是一小块,例如头像,名称以及其他一些信息。 因此,UserFragment是可重用的组件,不应对用例做任何假设。

Because of this, we want to use inversion of control (IoC), so that the actual user loading is beyond UserFragment’s responsibility. For this we introduced the UserRepository interface, its implementation should be provided by clients of the UserFragment. This approach also allows us to put this fragment into a separate Gradle module.

因此,我们要使用控制反转 (IoC),以便实际的用户加载超出UserFragment的责任。 为此,我们引入了UserRepository接口,该接口的实现应由UserFragment的客户端提供。 这种方法还使我们可以将此片段放入单独的Gradle模块中

Here is just one of the ways how we can retrieve the dependencies:

这只是我们检索依赖项的方法之一:

class UserFragment : Fragment() {
    private lateinit var userId: String
    private lateinit var userRepository: UserRepository


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)


        userId = requireArguments().getString(KEY_USER_ID)!!


        userRepository = 
            (requireParentFragment() as UserRepositoryProvider)
                getUserRepository()
    }


    fun withArguments(userId: String): UserFragment =
        apply { arguments = bundleOf(KEY_USER_ID to userId) }


    private companion object {
        private const val KEY_USER_ID = "USER_ID"
    }


    interface UserRepositoryProvider {
        fun getUserRepository(): UserRepository
    }
}

We can pass userId via fragment arguments. The UserRepository can be taken from the UserFragment’s parent fragment.

我们可以通过片段参数传递userId 。 可以从UserFragment的父片段中获取UserRepository。

This is indeed a solution, but there are some disadvantages:

这确实是一个解决方案,但是有一些缺点:

  • There is no type safety when passing data via arguments in this way.

    以这种方式通过参数传递数据时,没有类型安全性。
  • There is no compile time safety when integrating the UserFragment into a parent fragment — compiler won’t fail if the UserRepositoryProvider interface is not implemented.

    将UserFragment集成到父片段中时,没有编译时的安全性-如果未实现UserRepositoryProvider接口,则编译器不会失败。
  • We have to use lateinit var because we can’t access arguments and parentFragment in the init section.

    我们必须使用lateinit var因为我们无法在init部分中访问参数和parentFragment

FragmentFactory (The FragmentFactory)

So how would you normally pass dependencies to an object? There is a proper way of doing this — dependency injection (DI). And (I believe) exactly because of this reason we now have the FragmentFactory. So what is the FragmentFactory?

那么,通常如何将依赖关系传递给对象? 有一种适当的方法可以执行此操作- 依赖项注入 (DI)。 并且(我相信)正是由于这个原因,我们现在有了FragmentFactory。 那么,FragmentFactory是什么?

It is a factory that we can implement and apply to the FragmentManager. After that it is our responsibility to create fragments. Now we can use DI in our UserFragment as follows:

这是一个我们可以实现并将其应用于FragmentManager的工厂。 之后,创建片段是我们的责任。 现在我们可以在UserFragment中使用DI,如下所示:

class UserFragment(
    private val userRepository: UserRepository
) : Fragment() {
    private lateinit var userId: String


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)


        userId = requireArguments().getString(KEY_USER_ID)!!
    }


    fun withArguments(userId: String): UserFragment =
        apply { arguments = bundleOf(KEY_USER_ID to userId) }


    private companion object {
        private const val KEY_USER_ID = "USER_ID"
    }
}

And this is how we can use the UserFragment in another fragment:

这就是我们可以在另一个片段中使用UserFragment的方法:

class ParentFragment : Fragment() {
    private val factory = ParentFragmentFactory()


    override fun onCreate(savedInstanceState: Bundle?) {
        childFragmentManager.fragmentFactory = factory
        super.onCreate(savedInstanceState)
    }


    private fun openUser(userId: String) {
        childFragmentManager.commit {
            replace(
                R.id.content,
                factory.userFragment().withArguments(userId = userId)
            )
        }
    }
}


internal class ParentFragmentFactory : FragmentFactory() {
    override fun instantiate(classLoader: ClassLoader, className: String): Fragment =
        when (loadFragmentClass(classLoader, className)) {
            UserFragment::class.java -> userFragment()
            else -> super.instantiate(classLoader, className)
        }


    fun userFragment(): UserFragment = UserFragment(DefaultUserRepository())
}

Now we create the UserFragment ourselves and pass the UserRepository via constructor.

现在,我们自己创建UserFragment并通过构造函数传递UserRepository。

有缺陷的FragmentFactory (The defective FragmentFactory)

So why do I think the FragmentFactory is defective? The DI problem is solved, but not completely. With the current approach we are still very limited in what we can pass via constructor.

那么,为什么我认为FragmentFactory有缺陷? DI问题已解决,但还没有完全解决。 使用当前方法,我们仍然可以通过构造函数进行传递。

传递UserRepository的不同实现 (Pass different implementations of the UserRepository)

We can’t pass different implementations of the UserRepository. For example, what if in the ParentFragment we need to display user profiles loaded from either local phone book or from a remote server.

我们无法传递UserRepository的不同实现。 例如,如果在ParentFragment中我们需要显示从本地电话簿或从远程服务器加载的用户配置文件,该怎么办?

Consider the following example:

考虑以下示例:

class ParentFragment : Fragment() {
    private val factory = ParentFragmentFactory()


    override fun onCreate(savedInstanceState: Bundle?) {
        childFragmentManager.fragmentFactory = factory
        super.onCreate(savedInstanceState)
    }


    private fun openLocalUser(userId: String) {
        openUser(userId, factory::localUserFragment)
    }


    private fun openRemoteUser(userId: String) {
        openUser(userId, factory::remoteUserFragment)
    }
    
    private fun openUser(userId: String, factory: () -> UserFragment) {
        childFragmentManager.commit {
            replace(
                R.id.content,
                factory().withArguments(userId = userId)
            )
        }
    }
}


internal class ParentFragmentFactory : FragmentFactory() {
    override fun instantiate(classLoader: ClassLoader, className: String): Fragment =
        when (loadFragmentClass(classLoader, className)) {
            // Is it a local or a remote user?
            UserFragment::class.java -> TODO()
            else -> super.instantiate(classLoader, className)
        }


    fun localUserFragment(): UserFragment = UserFragment(LocalUserRepository())


    fun remoteUserFragment(): UserFragment = UserFragment(RemoteUserRepository())
}

When the UserFragment is recreated there is no way to determine which implementation of the UserRepository we should use.

重新创建UserFragment时,无法确定我们应使用UserRepository的哪种实现。

将不同的数据传递到UserRepository (Pass different data to the UserRepository)

Currently our UserFragment loads user profiles based on userId . But what if we want to make it the implementation details of the UserRepository? So the UserFragment won’t care about how user profiles are loaded. For example, we could load a remote user by userId and a local user by phone number.

当前,我们的UserFragment基于userId加载用户配置文件。 但是,如果我们要使其成为UserRepository的实现细节,该怎么办? 因此,UserFragment不会在乎如何加载用户配置文件。 例如,我们可以通过userId加载远程用户,并通过电话号码加载本地用户。

Our UserFragment could just accept the UserRepository:

我们的UserFragment可以只接受UserRepository:

class UserFragment(
    private val userRepository: UserRepository
) : Fragment() {
    // Some logic here
}

And here is an example of the ParentFragment:

这是ParentFragment的示例:

class ParentFragment : Fragment() {
    private val factory = ParentFragmentFactory()


    override fun onCreate(savedInstanceState: Bundle?) {
        childFragmentManager.fragmentFactory = factory
        super.onCreate(savedInstanceState)
    }


    private fun openLocalUser(phoneNumber: String) {
        openFragment { factory.localUserFragment(phoneNumber = phoneNumber) }
    }


    private fun openRemoteUser(userId: String) {
        openFragment { factory.remoteUserFragment(userId = userId) }
    }


    private fun openFragment(factory: () -> UserFragment) {
        childFragmentManager.commit {
            replace(R.id.content, factory())
        }
    }
}


private class ParentFragmentFactory : FragmentFactory() {
    override fun instantiate(classLoader: ClassLoader, className: String): Fragment =
        when (loadFragmentClass(classLoader, className)) {
            // Is it a local or a remote user?
            // How we can get the phoneNumber or the userId?
            UserFragment::class.java -> TODO()
            else -> super.instantiate(classLoader, className)
        }


    fun localUserFragment(phoneNumber: String): UserFragment =
        UserFragment(LocalUserRepository(phoneNumber = phoneNumber))


    fun remoteUserFragment(userId: String): UserFragment =
        UserFragment(RemoteUserRepository(userId = userId))
}

Again, when the UserFragment is recreated there is no way to determine which implementation of the UserRepository we should use. Also there is no way to associate the userId and the phone number with each UserFragment.

同样,当重新创建UserFragment时,无法确定我们应使用UserRepository的哪种实现。 同样,也没有办法将userId和电话号码与每个UserFragment关联。

显示具有不同UserRepository的多个UserFragment (Show multiple UserFragments with different UserRepository)

For the same reason we can notify display more than one instance of the UserFragment with different implementations of the UserRepository, at the same time. For example, one UserFragment at the top and another at the bottom.

出于相同的原因,我们可以同时通过UserRepository的不同实现通知显示多个UserFragment实例。 例如,一个UserFragment在顶部,另一个在底部。

丑陋的解决方法 (An ugly workaround)

There is an ugly workaround though, that can partially solve the problem. We can make our UserFragment aware of the user type, and pass a provider of the UserRepository.

不过,有一个丑陋的解决方法可以部分解决问题。 我们可以使UserFragment知道用户类型,并传递UserRepository的提供程序。

Here is how it could look like:

它看起来像这样:

class UserFragment(
    private val userRepositoryProvider: (UserType) -> UserRepository
) : Fragment() {
    private lateinit var userId: String
    private lateinit var userType: UserType


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)


        val args = requireArguments()
        userId = args.getString(KEY_USER_ID)!!
        userType = args.getSerializable(KEY_USER_TYPE) as UserType
    }


    fun withArguments(userId: String, userType: UserType): UserFragment =
        apply {
            arguments = bundleOf(
                KEY_USER_ID to userId,
                KEY_USER_TYPE to userType
            )
        }


    private companion object {
        private const val KEY_USER_ID = "USER_ID"
        private const val KEY_USER_TYPE = "USER_TYPE"
    }
    
    enum class UserType {
        LOCAL, REMOTE
    }
}

This workaround helps. Unless we want to add another dimension. E.g. we may need to reuse the UserFragment, and load users from different databases. This is because the UserFragment must be aware of all possible kinds of user sources. And of course we still have to pass the userId via arguments. We can’t hide it behind the UserRepository.

此解决方法有帮助。 除非我们要添加另一个维度。 例如,我们可能需要重用UserFragment,并从不同的数据库加载用户。 这是因为UserFragment必须知道所有可能的用户来源。 当然,我们仍然必须通过参数传递userId 。 我们无法将其隐藏在UserRepository后面。

Another possible workaround is to extend the UserFragment with LocalUserFragment and RemoteUserFragment. In this case we can distinguish between the two when instantiating. But the issue with the userId (or phone number) is still there.

另一个可能的解决方法是使用LocalUserFragment和RemoteUserFragment扩展UserFragment。 在这种情况下,我们可以在实例化时区分两者。 但是userId (或电话号码)的问题仍然存在。

虚构的正确API解决方案 (An imaginary proper API solution)

So what API do I think would be perfect? The minimum change we need is to add an additional argument to FragmentFactory.instantiate(...) method:

那么我认为哪种API是完美的? 我们需要做的最小更改是在FragmentFactory.instantiate(...)方法中添加一个附加参数:

@NonNull
public Fragment instantiate(
    @NonNull ClassLoader classLoader, 
    @NonNull String className,
    @Nullable Bundle arguments // <-- add this argument
) {
    // Omitted code
}

Simply having arguments of a fragment being instantiated would solve all kinds of problems. This would allow us to put all required information into the UserFragment’s arguments bundle, same as before. But the key difference here, is that we would do it in the ParentFragment, not in the UserFragment!

只需实例化片段的参数即可解决各种问题。 与以前一样,这将使我们能够将所有必需的信息放入UserFragment的参数包中。 但是这里的主要区别在于,我们将在ParentFragment中而不是在UserFragment中进行操作!

Here is thenew code of the UserFragment:

这是UserFragment的新代码:

class UserFragment(
    private val userRepositoryrovider: UserRepository
) : Fragment() {
    // Some logic here
}

And here is the new imaginary code of the ParentFragmentFactory:

这是ParentFragmentFactory的新虚构代码:

internal class ParentFragmentFactory : FragmentFactory() {
    override fun instantiate(classLoader: ClassLoader, className: String, arguments: Bundle?): Fragment {
        val config = arguments?.getParcelable<Configuration<*>>(KEY_CONFIGURATION)
        check(config != null) { "Configuration is not provided" }


        return when (config) {
            is Configuration.LocalUser -> localUserFragment(phoneNumber = config.phoneNumber)
            is Configuration.RemoteUser -> remoteUserFragment(userId = config.userId)
            null -> throw IllegalStateException()
        }
    }


    fun localUserFragment(phoneNumber: String): UserFragment =
        fragment(Configuration.LocalUser(phoneNumber = phoneNumber)) {
            UserFragment(LocalUserRepository(phoneNumber = phoneNumber))
        }


    fun remoteUserFragment(userId: String): UserFragment =
        fragment(Configuration.RemoteUser(userId = userId)) {
            UserFragment(RemoteUserRepositoryuserId = userId))
        }


    private inline fun <T: Fragment> fragment(
        configuration: Configuration<T>,
        factory: () -> T
    ): T =
        factory().apply {
            arguments = bundleOf(KEY_CONFIGURATION to configuration)
        }


    private sealed class Configuration<out T: Fragment> : Parcelable {
        @Parcelize
        class LocalUser(val phoneNumber: String): Configuration<UserFragment>()
        @Parcelize
        class RemoteUser(val userId: String): Configuration<UserFragment>()
    }


    private companion object {
        private const val KEY_CONFIGURATION = "CONFIGURATION"
    }
}

Here are some key points:

以下是一些关键点:

  1. The UserFragment benefits from proper DI: no arguments are used, no lateinit var , no service locators, no UserRepositoryProvider.

    UserFragment得益于正确的DI:不使用任何参数,不使用lateinit var ,不使用服务定位符,不使用UserRepositoryProvider。

  2. The UserFragment benefits from proper IoC: the whole user loading logic is abstracted via UserRepository, the UserFragment doesn’t care about how users are loaded.

    UserFragment得益于适当的IoC:整个用户加载逻辑是通过UserRepository提取的,UserFragment并不关心如何加载用户。
  3. We have more type and compile time safety in the ParentFragment:

    ParentFragment具有更多的类型和编译时间安全性:

    - We put all work with arguments into the

    -我们将所有带有参数的工作放到

    fragment(...) function, once properly defined it is always safe.

    fragment(...)函数,一旦正确定义,它始终是安全的。

    - We associated a

    -我们关联了一个

    sealed class Configuration with every fragment, so all possible configurations must be handled by the factory.

    sealed class Configuration每个片段都有配置,因此所有可能的配置必须由工厂处理。

    - Every configuration is associated with a fragment via generic type, the

    -每个配置都通过通用类型与片段相关联,

    fragment(...) function guarantees the consistency between a fragment and a configuration being associated with the fragment.

    fragment(...)函数可确保片段和与该片段关联的配置之间的一致性。

API的进一步改进 (Further API improvements)

We can modify the API further, so the ParentFragment will not touch UserFragment’s arguments:

我们可以进一步修改API,以便ParentFragment不会碰到UserFragment的参数:

First of all we need to add an additional argument FragmentTransaction.add(...) andFragmentTransaction.replace(...)methods. For example the replace(...) method could look like this:

首先,我们需要添加一个附加参数FragmentTransaction.add(...)FragmentTransaction.replace(...)方法。 例如, replace(...)方法可能如下所示:

public FragmentTransaction replace(
    @IdRes int containerViewId, 
    @NonNull Fragment fragment,
    @Nullable String tag,
    @Nullable Bundle configuration // <-- Add this argument
)

So we could associate a configuration Bundle with every fragment. This Bundle will be received in the FragmentFactory.instantiate(...) method:

因此,我们可以将配置捆绑包与每个片段相关联。 该捆绑包将在FragmentFactory.instantiate(...)方法中接收:

@NonNull
public Fragment instantiate(
    @NonNull ClassLoader classLoader, 
    @NonNull String className,
    @Nullable Bundle configuration // <-- add this argument
) {
    // Omitted code
}

This variant requires more changes in the API, but there is an additional feature: the ParentFragment will not interact with the UserFragment’s arguments. This may be more preferable, since child fragments may modify their arguments.

此变体需要在API中进行更多更改,但还有一个附加功能:ParentFragment将不会与UserFragment的参数进行交互。 这可能是更可取的,因为子片段可以修改其参数。

另一种方式 (An alternative way)

There is a successful attempt to implement lifecycle-aware components: Badoo RIBs framework, which is inspired by the Uber RIBs framework. This brings proper DI and IoC in all cases.

有一个成功的尝试可以实现生命周期感知的组件: Badoo RIBs框架,它受Uber RIBs框架的启发。 在所有情况下,这都会带来适当的DI和IoC。

结论 (Conclusion)

The FragmentFactory allows fragment dependency injection in many cases. But there is no way to distinguish between particular instances of fragments during instantiation. This in some cases prevents proper inversion of control. There are workarounds that may help, but API changes are required if we want to eliminate the problem completely. At the moment we can only hope that this problem will be solved someday.

在许多情况下,FragmentFactory允许片段依赖项注入。 但是无法在实例化期间区分片段的特定实例。 在某些情况下,这会导致无法正确控制反转。 有一些变通办法可能会有所帮助,但是如果我们想彻底消除问题,则需要更改API。 目前,我们只能希望有一天能解决这个问题。

Thanks for reading, and don’t forget to follow me on Twitter!

感谢您的阅读,不要忘记在Twitter上关注我!

翻译自: https://proandroiddev.com/the-defective-androidx-fragmentfactory-599b63879f35

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值