The defective AndroidX FragmentFactory

Arkadii Ivanov
ProAndroidDev
Published in
5 min readSep 6, 2020

--

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.

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.

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.

Suppose we have the following sample 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.

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.

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

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

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.
  • We have to use lateinit var because we can’t access arguments and parentFragment in the init section.

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?

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:

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

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

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.

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.

Consider the following example:

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

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.

Our UserFragment could just accept the UserRepository:

And here is an example of the ParentFragment:

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.

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.

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.

Here is how it could look like:

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.

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.

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:

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!

Here is thenew code of the UserFragment:

And here is the new imaginary code of the ParentFragmentFactory:

Here are some key points:

  1. The UserFragment benefits from proper DI: no arguments are used, no lateinit var , no service locators, no 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.
  3. We have more type and compile time safety in the ParentFragment:
    - We put all work with arguments into the fragment(...) function, once properly defined it is always safe.
    - We associated a sealed class Configuration with every fragment, so all possible configurations must be handled by the factory.
    - 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.

Further API improvements

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

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

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

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.

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.

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.

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

--

--