MVP - Presenters That Survive Configuration Changes (Part 2)

In part 1, I discussed the concept of what we are trying to achieve, and the underlying mechanism that this project uses for determining a presenters lifespan (or scope). This article is going to go over the same app given in part 1, but this time written using Dagger 2 with component scoping to inject the presenters instead.

The code discussed in this article can be found here.


A Brief Introduction to Dagger 2 Scoping

This is by no means a Dagger tutorial. I am just going to explain two Dagger 2 concepts that are pre-requisites for understanding the codebase.

Scoped Components

In Dagger 2, each component can be associated with a particular "scope" by annotating the component with an @Scope-annotated annotation. Let's look at AppComponent as an example:

@Component(
        modules = AppModule.class
)
@Singleton
public interface AppComponent {
    void inject(MainActivity activity);
    Bus getBus();
}

This component is scoped with @Singleton (provided by JSR330):

/**
 * Identifies a type that the injector only instantiates once. Not inherited.
 *
 * @see javax.inject.Scope @Scope
 */
@Scope
@Documented
@Retention(RUNTIME)
public @interface Singleton {} 

Now modules for that component can also annotate their @Provides methods with the same scope. For example, AppModule provides a singleton-scoped bus:

@Module
public class AppModule {
    @Provides @Singleton public Bus provideBus() {
        return new Bus();
    }
}

This means that within the singleton-scope, Dagger will only call provideBus() once and cache the result. Any subsequent requests from the same component will simply return the cached result.

In Dagger 2, unlike Dagger 1, your sub-components or graph dependencies need to be annotated with different scope annotations to represent that these components have a shorter (or equal) lifespan. If you try to annotate a sub-component with @Singleton as well, you will get a compile-time error. You should think of @Singleton as the "largest" scope available to you. All other scopes should be custom. In my example, I have created @Hello1Scope and @Hello2Scope which are used with Hello1Component and Hello2Component respectively.

Component dependencies and sub-components

If you've worked with Dagger 1, you will be familiar with the ObjectGraph#plus(Object... modules); method. In Dagger 2, you can achieve the same effect using component dependencies or sub-components.

Sub-components are the most comparable to dagger 1s plus method, as the sub-component will inherit all of its parents bindings. Component dependencies, on the other hand, only inherit what is exposed through the component interface.

You can read more about component relationships here.

I could have used either approach, but I opted for component dependencies in my example. You declare component dependencies in the component itself, like so:

@Component(
        modules = Hello1Module.class,
        dependencies = AppComponent.class
)
@Hello1Scope
public interface Hello1Component extends InjectsPresenter<Hello1Presenter> {
    Hello1LinearLayout inject(Hello1LinearLayout view);
}

In this example, my Hello1Component has a dependency on AppComponent. This means that anything exposed through the AppComponent interface can be used by Hello1Component. In my example, only the event bus is exposed through AppComponent interface via Bus getBus();.


Overview of the codebase

This section is going to go over the code in each package and explain its purpose, how it works, and how you might use it in your apps. I personally like knowing how the libraries I use work, so this section is going to be as transparent as possible and will contain a lot of code.

The Library Package

The idea is that everything in this package will be extracted out into a standalone library once the APIs are ready and documented. I'm currently building a "real-world" sample app on top of this to make sure that all situations are covered properly. This will be available on my GitHub page when it is ready.

Classes

Presenter.java:
An interface for all presenters

public interface Presenter<T> {
    void onCreate(@Nullable PresenterBundle bundle);
    void onSaveInstanceState(@NonNull PresenterBundle bundle);
    void onDestroy();
    void bindView(T view);
    void unbindView();
}

onCreate gets called when the presenter gets created. Note that because presenters survive configuration changes, onCreate will not get called every time your associated fragment gets created.

onSaveInstanceState gets called every time the view state is saved by the system. The reason why this is needed is explained in this article.

onDestroy gets called when the presenter instance is destroyed. Similar to onCreate this won't get called every time your associated fragment is destroyed for the same reason. This method is intended for cleanup, for example, cancelling a useless request.

bindView and unbindView should be fairly straightforward. Views have shorter lifecycles than presenters, so they need the ability to attach and detach.

BasePresenter.java:
This is a default implementation for the Presenter interface which provides (mostly) empty methods so you can override only what is needed when writing your own presenters.

public abstract class BasePresenter<T> implements Presenter<T> {
    protected T view;
    @Override public void bindView(T view) { this.view = view; }
    @Override public void unbindView() { this.view = null; }
    @Override public void onCreate(
            @Nullable PresenterBundle bundle) {}
    @Override public void onDestroy() {}
    @Override public void onSaveInstanceState(
            @NonNull PresenterBundle bundle) {}
}

ComponentCache.java:
This is an interface for the Activity that will act as the cache for all of the non-configuration components.

ComponentCacheActivity.java:
This is a convenience class that extends from AppCompatActivity and implements ComponentCache. It delegates all of the hard work to ComponentCacheDelegate.

public class ComponentCacheActivity extends AppCompatActivity 
        implements ComponentCache {
    private ComponentCacheDelegate delegate = 
            new ComponentCacheDelegate();

    @Override public void onCreate(Bundle savedInstanceState) {
        delegate.onCreate(savedInstanceState, 
                getLastCustomNonConfigurationInstance());
        super.onCreate(savedInstanceState);
    }

    @Override public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        delegate.onSaveInstanceState(outState);
    }

    @Override 
    public Object onRetainCustomNonConfigurationInstance() {
        return delegate.onRetainCustomNonConfigurationInstance();
    }

    @Override public long generateId() {
        return delegate.generateId();
    }

    @Override public final <C> C getComponent(long index) {
        return delegate.getComponent(index);
    }

    @Override 
    public void setComponent(long index, Object component) {
        delegate.setComponent(index, component);
    }
}

ComponentCacheDelegate:
The delegate class that assists with correctly handling the ComponentCache interface, and the Activity's non-configuration instance. We've seen how this can be used above. Most of the time you won't need to use this class directly. The only time you would have to use this class would be if you wanted to write you own activity that doesn't extend from AppCompatActivity.

public class ComponentCacheDelegate {
    private static final String NEXT_ID_KEY = "next-presenter-id";

    private NonConfigurationInstance nonConfigurationInstance;

    public void onCreate(Bundle savedInstanceState, 
            Object nonConfigurationInstance) {
        if (nonConfigurationInstance == null) {
            long seed = 0;
            if (savedInstanceState != null) {
                seed = savedInstanceState.getLong(NEXT_ID_KEY);
            }
            this.nonConfigurationInstance = 
                    new NonConfigurationInstance(seed);
        } else {
            this.nonConfigurationInstance = 
                    (NonConfigurationInstance)
                    nonConfigurationInstance;
        }
    }

    public void onSaveInstanceState(Bundle outState) {
        outState.putLong(NEXT_ID_KEY, 
                nonConfigurationInstance.nextId.get());
    }

    public Object onRetainCustomNonConfigurationInstance() {
        return nonConfigurationInstance;
    }

    public long generateId() {
        return nonConfigurationInstance.nextId.getAndIncrement();
    }

    @SuppressWarnings("unchecked")
    public final <C> C getComponent(long index) {
        return (C) nonConfigurationInstance.components.get(index);
    }

    public void setComponent(long index, Object component) {
        nonConfigurationInstance.components.put(index, component);
    }

    private static class NonConfigurationInstance {
        private Map<Long, Object> components;
        private AtomicLong nextId;
        public NonConfigurationInstance(long seed) {
            components = new HashMap<>();
            nextId = new AtomicLong(seed);
        }
    }
}

This class is a little more interesting. It holds a class called NonConfigurationInstance which holds an AtomicLong instance for generating unique IDs, and a map of IDs->Components. This object instance is returned via the onRetainCustomNonConfigurationInstance() method, so as long as the delegate is wired up correctly, these components will survive configuration changes.

ComponentFactory.java:
An interface that defines a createComponent() method. This is used with ComponentControllerDelegate.

ComponentControllerFragment.java:
This is a convenience class that extends from the support library Fragment and uses ComponentControllerDelegate to control the lifespan of its non-configuration component.

public abstract class ComponentControllerFragment<C> 
        extends Fragment {
    private ComponentCache componentCache;
    private ComponentControllerDelegate<C> componentDelegate = 
            new ComponentControllerDelegate<>();

    @Override public void onAttach(Activity activity) {
        super.onAttach(activity);
        if (activity instanceof ComponentCache) {
            componentCache = (ComponentCache)activity;
        } else {
            throw new RuntimeException(getClass().getSimpleName()
                    + " must be attached to " +
                    "an Activity that implements " + 
                    ComponentCache.class.getSimpleName());
        }
    }

    @Override 
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        componentDelegate.onCreate(componentCache, 
                savedInstanceState, componentFactory);
    }

    @Override public void onResume() {
        super.onResume();
        componentDelegate.onResume();
    }

    @Override public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        componentDelegate.onSaveInstanceState(outState);
    }

    @Override public void onDestroy() {
        super.onDestroy();
        componentDelegate.onDestroy();
    }

    public C getComponent() {
        return componentDelegate.getComponent();
    }

    protected abstract C onCreateNonConfigurationComponent();

    private ComponentFactory<C> componentFactory = 
            new ComponentFactory<C>() {
        @NonNull @Override public C createComponent() {
            return onCreateNonConfigurationComponent();
        }
    };
}

Once again, it delegates all of the hard work to the delegate class.

ComponentControllerDelegate.java:
This is a delegate class for Fragments/Activities to use that manages the scoping of its non-configuration component instance. Like the other delegate classes, this typically won't be used by client code. The idea is that you could use this in other Fragment types (e.g. ListFragment, PreferenceFragment, etc) or Activities.

public class ComponentControllerDelegate<C> {
    private static final String PRESENTER_INDEX_KEY 
            = "presenter-index";

    private C component;
    private ComponentCache cache;
    private long componentId;
    private boolean isDestroyedBySystem;

    public void onCreate(ComponentCache cache, 
            Bundle savedInstanceState, 
            ComponentFactory<C> componentFactory) {
        this.cache = cache;
        if (savedInstanceState == null) {
            componentId = cache.generateId();
        } else {
            componentId = savedInstanceState
                    .getLong(PRESENTER_INDEX_KEY);
        }
        component = cache.getComponent(componentId);
        if (component == null) {
            component = componentFactory.createComponent();
            cache.setComponent(componentId, component);
        }
    }

    public void onResume() {
        isDestroyedBySystem = false;
    }

    public void onSaveInstanceState(Bundle outState) {
        isDestroyedBySystem = true;
        outState.putLong(PRESENTER_INDEX_KEY, componentId);
    }

    public void onDestroy() {
        if (!isDestroyedBySystem) {
            // User is exiting this view, remove component 
            // from the cache
            cache.setComponent(componentId, null);
        }
    }

    public C getComponent() {
        return component;
    }
}

As you can see, it uses the same underlying mechanism described in part 1 of this series.

HasPresenter.java:
All components used with PresenterControllerFragment must extends from this interface so that the library has a way of retrieving the presenter instance from the component and calling the presenter lifecycle methods automatically for you. This is explained more later.

PresenterBundle.java:
A bundle class with no Android-isms so that the presenter classes have no Android references. This is a questionable API - maybe I should just use Android's Bundle.

PresenterBundleUtils.java:
This is just a util class for adding the presenter bundle to an Android bundle, or extracting a presenter bundle from an Android bundle.

PresenterControllerFragment.java:
A convenience class that extends from ComponentControllerFragment and utilizes PresenterControllerDelegate to automatically call the lifecycle events of the associated presenter class.

public abstract class PresenterControllerFragment<C extends 
        HasPresenter<P>, P extends Presenter>
        extends ComponentControllerFragment<C> {
    private PresenterControllerDelegate<P> presenterDelegate = 
            new PresenterControllerDelegate<>();

    @Override 
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        presenterDelegate.onCreate(getComponent().getPresenter(), 
                savedInstanceState);
    }

    @Override 
    public void onViewCreated(View view, 
            @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        presenterDelegate.onViewCreated(view);
    }

    @Override public void onResume() {
        super.onResume();
        presenterDelegate.onResume();
    }

    @Override 
    public void onSaveInstanceState(@NonNull Bundle outState) {
        super.onSaveInstanceState(outState);
        presenterDelegate.onSaveInstanceState(outState);
    }

    @Override public void onDestroyView() {
        super.onDestroyView();
        presenterDelegate.onDestroyView();
    }

    @Override public void onDestroy() {
        super.onDestroy();
        presenterDelegate.onDestroy();
    }
}

The only interesting thing about this class is the way that the presenter is obtained through the component. As you can see by the class definition, the component that is passed in must implement the HasPresenter interface. This class uses that method to get the presenter instance. This was one of the more tricky problems to solve using Dagger 2.

PresenterControllerDelegate.java:
This is the delegate class for Fragments/Activities to manage the lifecycle events of it's presenter instance. Like the other delegate classes, this should only be used in client code when using the support library fragment isn't quite what you want.

public class PresenterControllerDelegate<P extends Presenter> {
    private boolean isDestroyedBySystem;
    private P presenter;

    public void onCreate(P presenter, Bundle savedInstanceState) {
        this.presenter = presenter;
        PresenterBundle bundle = 
                getPresenterBundle(savedInstanceState);
        presenter.onCreate(bundle);
    }

    @SuppressWarnings("unchecked")
    public void onViewCreated(View view) {
        try {
            presenter.bindView(view);
        } catch (ClassCastException e) {
            throw new RuntimeException("The view provided does not" 
                    + " implement the view interface expected by " 
                    + presenter.getClass().getSimpleName(), e);
        }
    }

    public void onResume() {
        isDestroyedBySystem = false;
    }

    public void onSaveInstanceState(Bundle outState) {
        isDestroyedBySystem = true;
        PresenterBundle bundle = new PresenterBundle();
        presenter.onSaveInstanceState(bundle);
        setPresenterBundle(outState, bundle);
    }

    public void onDestroyView() {
        presenter.unbindView();
    }

    public void onDestroy() {
        if (!isDestroyedBySystem) {
            presenter.onDestroy();
        }
    }
}

As you can see, this delegate class will attach/detach the view to/from the presenter for you. It also manages calling the presenters onCreate, onDestroy, and onSaveInstanceState methods at the correct time for you too.

The App Package

This is the client code. Most of the hard work is in the library, so this code should be easy to write and understand.

Your Activities can now extend ComponentCacheActivity to automatically manage components for you:

public class MainActivity extends ComponentCacheActivity {
    ....
}

Your Fragments can now simply extend PresenterControllerFragment to get an associated presenter instance and have all of the lifecycle methods handled for you:

public class Hello1Fragment extends PresenterControllerFragment
        <Hello1Component, Hello1Presenter> {
    @Override 
    protected Hello1Component onCreateNonConfigurationComponent() {
        return DaggerHello1Component.builder()
            .appComponent(getAppComponent(getActivity()))
            .build();
    }

    @Nullable @Override
    public View onCreateView(@NonNull LayoutInflater inflater,
            ViewGroup container, Bundle savedInstanceState) {
        return getComponent()
            .inject((Hello1LinearLayout)
                    inflater.inflate(R.layout.fragment_hello1, 
                            container, false));
    }
}

Hopefully this Fragment looks quite simple to you. It's responsibility is only creating and injecting the view.

The view itself is actually defined in a View class, Hello1LinearLayout:

public class Hello1LinearLayout extends LinearLayout 
        implements Hello1View {
    @Inject Hello1Presenter presenter;
    @Inject Bus bus;

    @InjectView(R.id.text) TextView textView;

    public Hello1LinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override protected void onFinishInflate() {
        super.onFinishInflate();
        ButterKnife.inject(this);
    }

    @Override public void show(CharSequence stuff) {
        textView.setText(stuff);
    } 

    @Override public void goToNextScreen() {
        bus.post(new NavigateToHello2Event());
    }

    @OnClick(R.id.button) public void buttonPressed() {
        presenter.buttonPressed();
    }
}

Hopefully this looks really simple too! It doesn't have to bother with attaching/detaching from the presenter, that's handled automatically by the library. It simply requests dependencies and uses them.

And that's everything! If you have any questions/suggestions, I would love to hear them.

Brad Campbell

Android application developer, currently working as the lead on the ANZ goMoney NZ applications. All of the opinions and code on this blog are my own, and not that of ANZ.