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:
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.
- 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.
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.
If you think this article helped you understand the lifecycle of Android better, consider sharing this with your friends.