Dagger Hilt: Custom Entry Point for FragmentFactory Integration

In my previous article I explored Dagger Hilt, which is a new library that wraps aroud Dagger 2 dependency injection framework. As part of my research for that post, I migrated IDoCare app from “vanilla” Dagger to Dagger Hilt. It didn’t take much time and I was quite satisfied with the results.

Unfortunately, I introduced a serious bug during the migration. Fortunately, one of the readers immediately spotted that bug and alerted me (shoutout to Guilherme). Not surprisingly, the bug related to Android lifecycles and process death. However, I couldn’t figure out how to fix it right away due to the way Hilt operates.

In this post, I’ll describe that bug, explain why it’s tricky to fix when using Hilt, and then show you how to use Hilt’s custom Entry Points to work around its own limitations.

The Bug

Before I migrated IDoCare to Dagger Hilt, onCreate() method in its MainActivity looked like this:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        getControllerComponent().inject(this);
        mFragmentManager.setFragmentFactory(mFragmentFactory);
        super.onCreate(savedInstanceState);

        ...

        if (savedInstanceState == null) {
            mScreensNavigator.toAllRequests();
        }
    }

After migration to Dagger Hilt, it took on the following shape:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mFragmentManager.setFragmentFactory(mFragmentFactory);

        ...

        if (savedInstanceState == null) {
            mScreensNavigator.toAllRequests();
        }
    }

I had to delay setFragmentFactory(FragmentFactory) call because Hilt injects dependencies during a call to super.onCreate(), so I wouldn’t have a valid reference to FragmentFactory before this point. However, this seemingly innocent change introduced a critical bug into my code.

During “normal” operation, the app worked alright. However, when I started IDoCare, put it in the background, and then made the system kill its process, the app crashed on the next startup.

The root cause of this bug is realated to the mechanism of app’s state restoration after process death. When the system restores an application after its process had been killed, it re-initializes the latest attached Fragment during a call to super.onCreate() in MainActivity. And since I no longer bind my FragmentFactory to FragmentManager before this call, the sytem can’t use it to re-create the Fragment, which is a fatal error.

The Challenge

Once I identified the root cause of the bug, it should be very easy to fix it, right? Well, not exactly.

The additional challenge in this case is “lifecycle mismatch”: I need to set my custom FragmentFactory before the call to super.onCreate(), but, when I use Hilt, this object is injected into MainActivity only during that same call. I can’t really use the object before I inject it, right?

Looks like it’s impossible to resolve this bug using Hilt’s “default” approach. Fortunately, Hilt includes additional convention called Entry Point which can save the day in this case.

Bug Fix Using Custom Entry Point

Long story short, this code fixes the bug:

    @EntryPoint
    @InstallIn(ActivityComponent.class)
    public interface MainActivityEntryPoint {
        public FragmentManager getFragmentManager();
        public FragmentFactory getFragmentFactory();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        MainActivityEntryPoint entryPoint =
                EntryPointAccessors.fromActivity(this, MainActivityEntryPoint.class);
        entryPoint.getFragmentManager().setFragmentFactory(entryPoint.getFragmentFactory());

        super.onCreate(savedInstanceState);

        ...

        if (savedInstanceState == null) {
            mScreensNavigator.toAllRequests();
        }
    }

Let’s understand what’s going on here.

In onCreate() method, I bind a custom FragmentFactory to FragmentManager before the call to super.onCreate(). That’s the fix. To make this work, I get references to both FragmentManager and FragmentFactory from an object of type MainActivityEntryPoint.

MainActivityEntryPoint is an interface that I myself defined. This interface is annotated with @EntryPoint annotation to let Dagger Hilt know that it should generate the respective implementation. The additional @InstallIn annotation declares the “ownership” of this entry point. In this case, since MainActivityEntryPoint is installed in ActivityComponent, that entry point will give me access to ActivityComponent’s objects graph. The two methods inside my custom entry point indicate that I’ll use it to grab instances of FragmentManager and FragmentFactory.

A call to EntryPointAccessors.fromActivity(Activity, Class) is a classical service location. Since I declared that MainActivityEntryPoint will be installed in ActivityComponent, all my Activities will be able to retrieve that entry point using this approach. This means that the name of this entry point is only important for readability and maintainability. In addition, you can make your entry points protected if you’d like to restrict their usage (but not private).

Intuitively, you can think of entry points as “windows” into object graphs of specific Hilt’s Components.

Developers who took my Dagger 2 or Android Architecture courses can also think of entry points as interfaces that composition roots implement. In fact, that’s exactly what’s going on behind all Hilt’s magic. These interfaces allow you to interact with various composition roots directly (without “injection by annotation magic”), while limiting the amount of “visible” services. In essense, entry points leverage Interface Segregation Principle (I in SOLID) to provide access to a subset of composition roots’ methods.

In most cases, you wouldn’t grab dependencies from Components using entry points and rely on @Inject annotated fileds instead. However, in some cases, you might want to just get an instance of a specific object. That’s what entry points are used for.

Summary

In this short post you learned how to use custom Entry Points, which are advanced feature in Dagger Hilt, to work around lifecycle mismatch between Hilt and FragmentFactory.

On the one hand, it’s a bit disappointing that you need to jump through additional hoops to integrate Dagger Hilt with FragmentFactory. On the other hand, the fact that Hilt already supported a convention which allowed to implement relatively simple workaround is encouraging.

As usual, thanks for reading and leave your comments and questions below.

One comment on "Dagger Hilt: Custom Entry Point for FragmentFactory Integration"

  1. Thanks for this follow-up post. As you said, it is great that Hilt already has a way to get a direct dependency from the object graph, but I expect they provide an easier way of implementing this scenario in the future, as probably this will be a common source of bugs.

    Reply

Leave a Comment