In this post we will see how usage of Android Lifecycle Architecture Component can undermine the quality of Android projects.

Lifecycle Architecture Component:

Lifecycle class and the general feature associated with it are parts of Android Architecture Components – a set of experimental features for Android that were announced by Google at Google IO 2017 conference.

As stated on the official Handling Lifecycles page:

The android.arch.lifecycle package provides classes and interfaces that let you build lifecycle-aware components — which are components that can automatically adjust their behavior based on the current lifecycle of an activity or fragment.

It might not be immediately clear whether this is something we would like to have in our applications.

The rest of this post explores the suggested approach of using lifecycle-aware components, and shows several serious flaws in its design that can easily degrade the quality of our projects.

Comparison of Lifecycle Architecture Component to Existing Approach:

In order to see the impact of Lifecycle Architecture Component let’s take a code that uses the “old” approach and refactor it to use Lifecycle.

Consider this Activity:

public class OldApproachActivity extends Activity implements
        PostsManagerListener, UserStateManagerListener {

    @Inject PostsManager mPostsManager;
    @Inject UserStateManager mUserStateManager;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
    }

    @Override
    protected void onStart() {
        super.onStart();
        mPostsManager.registerListener(this);
        mUserStateManager.registerListener(this);
        mPostsManager.fetchAllPosts();
    }

    @Override
    protected void onStop() {
        super.onStop();
        mPostsManager.unregisterListener(this);
        mUserStateManager.unregisterListener(this);
    }

    @Override
    public void onNewPosts(List<Post> posts) {
        ...
    }

    @Override
    public void onUserStateChanged(UserState userState) {
        ...
    }
}

I omitted the code that is irrelevant to our discussion here, but it is not that difficult to imagine the actions that will be taken in response to new posts or user state changes.

Please note that this Activity is completely agnostic of the scope of both PostsManager and UsersStateManager. The fact that UsersStateManager is a global object that lives in Application scope while PostsManager is local to Activity scope is irrelevant here.

Now let’s refactor this code to make use of the new Lifecycle Architecture Component:

public class LifecycleApproachActivity extends LifecycleActivity implements
        PostsManagerListener, UserStateManagerListener {

    @Inject PostsManager mPostsManager;
    @Inject UserStateManager mUserStateManager;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        mPostsManager.registerLifecycleListener(getLifecycle(), this);
        mUserStateManager.registerLifecycleListener(getLifecycle(), this);
    }


    @Override
    public void onNewPosts(List<Post> posts) {
        ...
    }

    @Override
    public void onUserStateChanged(UserState userState) {
        ...
    }
}

Following the guidelines, we passed Lifecycle object alongside the callback target (which is the Activity itself) to the collaborator objects, and it is now their responsibility to perform actions in response to lifecycle events.

The code of Activity became more compact. This might be an advantage if all other factors are equal, but they are not.

Note that at least one piece of information about the functionality of this Activity is no longer present in its code: there is no way for a reader to know that the list of posts will be fetched when Activity started.

Unless, of course, the reader also reads the source code of PostsManager.

If we replace lifecycle management inside Activity with delegation to lifecycle-aware components, then the only way to understand the functionality of each individual Activity is to understand also functionality of each of its collaborator objects. In this example there are only two collaborator objects, but in real world implementations there might be ten and even more.

Furthermore, imagine that we decided that we don’t want to fetch posts when this specific Activity starts.

We need to edit PostsManager and remove a call to fetchAllPosts() from its “on start” method. However, by doing this, we are also removing this call for all Activities and Fragments that use PostsManager in the project. Will this change have undesired side effects? We can’t know for sure unless we read the source code of each Activity and Fragment that depend on PostsManager.

In projects of considerable size, we might be talking about tens of components that need to be reviewed and/or tested in order to safely make even trivial code modifications.

Lifecycle Architecture Component Considered Harmful:

In the previous section we saw how usage of lifecycle-aware components immediately creates hidden logical dependencies between system’s components. These dependencies can be roughly divided into two categories:

  1. Lifecycle owners “make assumptions” about functionality of their lifecycle-aware collaborators (e.g. posts will be fetched by collaborator when lifecycle owner starts). This type of coupling is called “semantic coupling” and it is the worst possible type of coupling between components of a software system.
  2. Functionality changes required by a single lifecycle owner can have side effects on other unrelated lifecycle owners.

The overall effect of these dependencies is loss of encapsulation – system components no longer have clear boundaries, therefore unrelated components need to be reviewed when making even the most trivial changes.

In this respect, I would like to quote Steve McConnell’s Code Complete (often referred to as “the most influential book on software engineering”):

Managing complexity is the most important technical topic in software development. In my view, it’s so important that Software’s Primary Technical Imperative has to be managing complexity.Steve McConnell, Code Complete (second edition)

In context of Minimal Complexity the book says:

If your design doesn’t let you safely ignore most other parts of the program when you’re immersed in one specific part, the design isn’t doing its job.Steve McConnell, Code Complete (second edition)

When switching to lifecycle-aware components from the usual approach, components that could be safely ignored beforehand can’t be safely ignored anymore. Therefore, usage of Lifecycle Architecture Component violates Software’s Primary Technical Imperative of managing complexity.

With all due respect to developers of this feature, if usage of lifecycle-aware components violates the primary principle stated in Code Complete, then this is a sufficient reason to stay away from this feature.

Lifecycle Architecture Component Subtle Bugs:

The previous section explained why you should avoid using Lifecycle Architecture Component at all costs. In this section we will review additional design limitations and subtle bugs that can be easily caused by usage of this feature.

First of all, in LifecycleApproachActivity example above, we pass a reference to Activity into a global UserStateManager object, but don’t clear that reference explicitly (as we would usually do in onStop()). Therefore, there is a risk of memory leak.

In order to prevent a memory leak, UserStateManager must intercept “on destroy” callback and clear that reference by itself. Alternatively, UserStateManager can keep all references as weak references, but the amount of work involved and potential complications make this approach non-viable.

However, memory leaks is not the worst bug associated with implementation of LifecycleApproachActivity. There is one subtle, but fatal design error in this class which I spotted only after 4 or 5 reviews of this article.

Since UserStateManager is global, there might be more than one lifecycle that it will be observing at any given instant. It means that this class, potentially, can receive lifecycle events from multiple independent lifecycle owners.

The sad part about this fatal error is that it might take long time before it ruins your application. For a single observed lifecycle UserStateManager will function as expected. It is only when multiple independent lifecycles will be observed simultaneously that the bug will begin to shine in its full glory.

When you think about this, the concept of a single object observing multiple lifecycles doesn’t make much sense. However, it does looks like Google devs thought that this should be supported. From official Lifecycle documentation:

Observer methods can receive zero or one argument. If used, the first argument must be of type LifecycleOwner. […] These additional parameters are provided to allow you to conveniently observe multiple providers and events without tracking them manually.

However, there is no official tutorial for how such a scheme should be implemented. I opened an issue asking Google devs to provide an example (you can follow it here).

Conclusion:

In this post we saw how usage of Lifecycle Architecture Component violates the Software’s Primary Technical Imperative as was defined by Steve McConnell in his classical Code Complete.

In addition, we saw how introduction of lifecycle-aware components can lead to subtle, but major bugs when global objects that live in Application scope are involved.

If you found this post useful or interesting, please share it with your colleagues in order to let them know about the risks associated with Lifecycle Architecture Component, and consider subscribing to our newsletter in order to receive notifications about new posts.

Leave a Comment

Your email address will not be published. Required fields are marked *