Adding life to Android components: Adding Lifecycle events to components

Adding life to Android components: Adding Lifecycle events to components

ยท

8 min read

The main guideline for any Android application following the MVVM architecture pattern is separation of concerns. This means the Activity is to be used solely for UI-related tasks and all other business logic needs to be implemented in the ViewModel class. But what should we do when our business logic is dependent on the lifecycle of the activity? Now there are simpler ways to solve this problem, for eg. creating public functions inside of ViewModel that get invoked in the appropriate lifecycle of the activity, but the Android Jetpack library comes with a solution to such lifecycle-related problems. In the below section, we will try to look at how we can make any class in Android lifecycle aware.

By the end of this article, you will understand clearly why lifecycle-aware components are more efficient than traditional callback-based approaches and how you can make sure that you never have to touch the activity to get the corresponding lifecycle in the ViewModel.

In this article, we will try to go over the following points:

  • What is a lifecycle observer and how it can be used to make lifecycle-aware components?
  • Problem with traditional callback-based approaches and how lifecycle-aware components are more efficient.
  • How to make any normal class lifecycle aware.

So let's get started.

The problem with callback-based approach

Let's try to look at the following situation where I want to start listening to the user's location updates and when the lat-lng is equal to a destination lat-lng then trigger a fragment transaction.

class MyLocationManager(
        context: Context,
        private val onDestinationReached: () -> Unit
) {
    //Some location object that contains current lat-lng.
    private val destination = Location() 
    private val request = LocationRequest.create()
    private var fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)

    private val locationCallback = object : LocationCallback() {
        override fun onLocationResult(locationResult: LocationResult?) {
            locationResult ?: return

            for (location in locationResult.locations) {
            //current location found
             if (location == destination)  {
                 onDestinationReached()
             }
            }
        }
    }

    fun start() {
        fusedLocationClient.requestLocationUpdates(
            request,
            locationCallback,
            Looper.getMainLooper()
            )
    }

    fun stop() {
       fusedLocationClient.removeLocationUpdates(locationCallback)
    }
}

The above class is a simple LocationManager class that has exposed start and stop functions to be used by the activity to start collecting the user's location updates and matching them to a destination location. After a match happens it triggers the onDestinationReached callback function.

This can be consumed in an Activity like this.


class MainActivity : AppCompatActivity() {

    private val locationManager = MyLocationManager(this) {
        supportFragmentManager.commit { 
            replace(R.id.container, SomeFragment())
        }
    }

    override fun onStart() {
        super.onStart()
        if (Utils.isRegularUser()) {
            locationManager.start()
        }

    }

    override fun onStop() {
        if (Utils.isRegularUser()) {
            locationManager.stop()
        }
        super.onStop()
    }

}

This approach looks okay to be fair but consider the following situations:

  1. Utils.isRegularUser() could be an API call or a complex computation that might take a bit of time to complete. Let's assume that it takes 1 min to complete and the user opens the MainActivity and closes it before this one minute completes in which case your app will now start listening to location updates.
  1. There could be a possibility that the onDestinationReached callback is invoked when the activity is in the paused state or in the stopped state in which case you will either see an abrupt UI experience or a crash for attempting to make a fragment transaction in the Stopped state.

As you can see that even though the implementation looked complete, we actually require lifecycle awareness in order to make this solution work properly.

Lifecycle, LifecycleOwner & LifecycleObserver

Fortunately, the good people of Google had already observed the issues/problems of a normal dev as he tries to combat the different lifecycles in Android and created a solution for us. Before applying the solution to the above problem we need first to understand the Lifecycle, LifecycleOwner, and LifecycleObserver classes.

1. Lifecycle

The Lifecycle as per Android documentation, is a class that holds the information about the lifecycle state of a component (like an activity or a fragment) and allows other objects to observe this state. The Lifecycle class contains two enums inside of it called States and Events.

The Event enum contains the following value:

 public enum Event {
        ON_CREATE,
        ON_START,
        ON_RESUME,
        ON_PAUSE,
        ON_STOP,
        ON_DESTROY,
        ON_ANY;
 }

The State enum contains the following value:

    public enum State {
        DESTROYED,
        INITIALIZED,
        CREATED,
        STARTED,
        RESUMED;
    }

The Lifecycle acts as a manager class where it maps the states of the lifecycle of a component to the events. The Lifecycle also contains a currentState value which represents the current state of the component it is attached to. When the lifecycle events that are dispatched from the Android framework are observed by the Lifecycle class, it updates its currentState value based on the Event that is triggered. This way the Lifecycle acts as a mediator between you and the lifecycle from the Android framework.

img

2. LifecycleOwner

LifecycleOwner is a single-method interface that is used to represent any class that contains the Lifecycle object associated with it. It has one method getLifeCycle() which must be implemented by the class to provide an instance of the Lifecycle.

public interface LifecycleOwner {
    /**
     * Returns the Lifecycle of the provider.
     *
     * @return The lifecycle of the provider.
     */
    @NonNull
    Lifecycle getLifecycle();
}

With the release of the Android Jetpack came the new version of AppCompatActivity class. Developers now needed to extend from androidx.appcompat.app.AppCompatActivity instead of the regular support package variant. Similarly, for the fragment, we now had androidx.fragment.app.Fragment to be used in place of the support variant. This was done specifically to introduce LifecycleOwner implementation into these classes. The Fragment directly implements the LifecycleOwner interface whereas the AppCompatActivity now has Component Activity as one of its superclasses which implements the LifecycleOwner interface.

So by default, all activities and fragments in Android are now implementing the LifecycleOwner interface and contain an associated Lifecycle object.

3. LifecycleObserver

LifecycleObserver on the other hand is a marker interface that allows a class to observe the various events inside the Lifecycle class. Since it's a marker interface it doesn't really have any methods and instead relies on OnLifecycleEvent annotations to listen to events.

The LifecycleObserver class allows any custom class to become lifecycle aware as long as it gets a Lifecycle object to observe upon. Below is a basic example of this implementation.

class MyObserver : LifecycleObserver {

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    fun onResumeMethod() { 
        //Name of this method can be anything.
        ...
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    fun onPauseMethod() {
        //Name of this method can be anything.
        ...
    }
}

/**
 * The `myLifecycleOwner` object here represents any class that
 * implements the ` LifecycleOwner ` interface. 
 */
myLifecycleOwner.getLifecycle().addObserver(MyObserver())

As we can see, it is this easy to create an observer class and hook it to the Lifecycle object of any LifecycleOwner. You can implement as many lifecycle event methods inside your observer class and it will always be triggered when the lifecycle of the associated LifecycleOwner changes.

Creating custom lifecycle-aware components

So now that we know how Lifecycle, LifecycleOwner, and LifecycleObserver work together, let's try to make our custom class MyLocationManager lifecycle aware.


class MyLocationManager(
    context: Context,
    private val lifecycle: Lifecycle,
    private val onDestinationReached: () -> Unit
): LifecycleObserver {
    //Some location object that contains current lat-lng.
    private val destination = Location()

   private var isEnabled = false

    private val locationCallback = object : LocationCallback() {
        override fun onLocationResult(locationResult: LocationResult?) {
            locationResult ?: return

            for (location in locationResult.locations) {
                //current location found
                if (location == destination)  {
                    if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED))
                        onDestinationReached()
                }
            }
        }
    }

    fun enableLocationObserving() {
        isEnabled = true
        if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
            // connect if not connected to location updates
        }
    }


    @SuppressLint("MissingPermission")
    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    fun start() {
        if (isEnabled) {
            // connect
        }
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    fun stop() {
        // disconnect if connected
    }
}

For brevity's sake, I have removed all other irrelevant code and focussed on only the main functionality.

The MyLocationManager class now accepts the Lifecycle object of the class it is associated with in the constructor itself. This allows us to query the current state of the Lifecycle before triggering the onDestinationReached() callback to avoid any crashes.

The MyLocationManager now also implements the LifecycleObserver interface allowing it to hook into the lifecycle.

Now let's look at how we can use this in the activity class. Since all Activities in Android implement the LifecycleOwner interface we can directly access the Lifecycle object inside it.

class MainActivity: AppCompatActivity() {
    private lateinit var manager: MyLocationManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        manager = MyLocationManager(this, lifecycle) {
            supportFragmentManager.commit {
                replace(R.id.container, SomeFragment())
            }
        }

        lifecycle.addObserver(manager)

        if (Utils.isRegularUser()) {
            manager.enableLocationObserving()
        }
    }

}

As you can see we have now completely removed all the lifecycle-related tasks from the activity and the activity is now completely bloat-free from the lifecycle methods and other initialization codes dependent on them.

Conclusions


The Android Jetpack library has brought a well-thought-of and easy-to-use solution for accessing the lifecycle associated with any Android Component. This gives us the ability to abstract away all lifecycle-related code to a separate class clearing up the Activity/Fragment classes to deal with only UI-related tasks. Any custom class can be made lifecycle-aware using the techniques mentioned in this article.

Lifecycle relationship

If you think this article helped you understand the lifecycle of Android better, consider sharing this with your friends.

ย