Blog Infos
Author
Published
Topics
, , , ,
Published

Kotlin Coroutines and Android Lifecycle are a match made in heaven! I will give you a quick intro on both, why they work so well together, and some of the cool stuff you can do with it.

In Android development, taking good care of your lifecycle is as important as taking good care of your goldfish: If you ignore it, that what shouldn’t die will definitely die. Coroutines and Android lifecycle have a great thing in common: They are both very vocal that they are a living things. Coroutines live in a Scope that gets created and cancelled, and Android components invoke their lifecycle methods, from onCreate all the way to onDestroy. Seeing we have two different things sharing their same desire to live and to die, we can start up coroutine scopes when an Android lifecycle gets created, and make sure those scopes are cancelled when the lifecycle gets destroyed. This concept is not new: some smart engineers at Google already figured this out and created lifecycle libraries that do all this stuff for you! You know of course about lifecycleOwner implementation for Activities, Fragments, LifecycleService and even Android Auto offers Screens that are lifecycle owners! The only place it is kind of missed, is within the Application class!

However, there is an amazing little library by Google that can help you here: lifecycle-process! Under the hood, the library hooks into the ActivityLifecycleCallbacks and basically counts how many activities went through onCreate/onStart/onResume and subtracts the amount of activities that went through onPause/onStop/onDestroy. Basically, we know the app is resumed when the counter for activies that called onResume is 1 or more, and we know the application is paused, if the activity lifecycle calls onPause and we minus that number to a zero. Google took this method of determining whether an application is started/stopped, resumed or paused, and added a little timeout in between to take into account orientation changes (because during an orientation change the activity is recreated, but the application doesn’t recreate so we should await the new create call on the recreated activity). Lifecycle-Process is basically a very fancy way of wrapping this horrendous shitshow of application lifecycle into a nice Coroutine friendly api!

So, let’s get to business! Assume that business wants the following rule: Please trigger this and that work when the application is started! You may think: This is easy, lets use the application’s onCreatemethod do this. After a while, somebody manages to put this into an activity onCreate and your new junior colleague figures yet another place to put in similar logic. What if I told you, you could abstract this all away? Let’s say we need to refresh caches every time the app starts anew:

class RefreshCacheOnLifecycleUseCase(
  private val lifecycleOwner: LifecycleOwner = ProcessLifecycleOwner.get(),
  private val cacheHelper: CacheHelper,
) {

  fun run() {
    lifecycleOwner.lifecycleScope.launch {
      cacheHelper.refreshData()
    }
  }

}

We now have a clean way to start up work at exactly the right time: At application creation! The only thing we need to do, is trigger the work in the right place. You can do this by bunching a bunch of use cases together, and in your application’s onCreate method, initialize those use cases:

class LifecycleUseCasesInitializer(
  private val refreshCacheOnLifecycleUseCase: RefreshCacheOnLifecycleUseCase,
  private val someOtherLifeCycleUseCase: SomeOtherLifeCycleUseCase,
) {

  fun initialize() {
    refreshCacheOnLifecycleUseCase.run()
    someOtherLifeCycleUseCase.run()
    // etc, you get the point
  }
  
}

class MyApplication: Application() {

  val lifecycleUseCasesInitializer by inject<LifecycleUseCasesInitializer>()

  override fun onCreate() {
    super.onCreate()
    lifecycleUseCasesInitializer.initialize()
  }
}

Now, this example is relatively easy, it simply uses the single entry point to your application, the Application class, to trigger the work. After this, we created a single entry point to fire up lifecycle work. Of course you can use the Androidx Start Up library as well and reuse the same concept.

Can you imagine, it can be even better than simply running work once on Application creation? Let’s say you have a banking application. Your security team demands that you clear sensitive data from memory every time the application goes to the background? Let’s have a look:

class InvalidateSensitiveDataOnLifecycleUseCase(
  private val lifecycleOwner: LifecycleOwner = ProcessLifecycleOwner.get(),
  private val tokenProvider: TokenProvider,
) {

  fun run() {
    lifeycycleOwner.lifecycleScope.launch {
      lifeycycleOwner.repeateOnLifecycle(state = Lifecycle.State.STARTED) {
        try {
          awaitCancellation()
        } finally {
          tokenProvider.clearSensitiveToken()
        }
      }
    }
  }

}

Here you can see, that we start up work every time the process’s lifecycle goes through the lifecycle event ON_START and after that we wait until a wild Cancellation will appear! A cancellation will only appear when the mirror of ON_START happens, which of course is the lifecycle event ON_STOP . Because we await the cancellation in a try/finally, we are sure that the code within the finally { } block is invoked, and only when the application is stopped. This usually means the app is moved to the background, or the screen is turned off, or it is actually being destroyed. We can also write a simple extension function for this if we use it more often:

suspend fun LifecycleOwner.onPause(block: () -> Unit) = repeateOnLifecycle(state = Lifecycle.State.STARTED) {
  try {
    awaitCancellation()
  } finally {
    block()
  }
}

// Example Usage in our use case:
fun run() {
  lifeycycleOwner.lifecycleScope,launch {
    lifeycycleOwner.onPause {
      tokenProvider.clearSensitiveToken()
    }
  }
}

In my current project, we extensively use the Process’s Lifecycle to observe work like invalidating caches upon user logout and many more! Every time the app starts (either freshly or coming from the background) we do checks on the user, like checking the validity of their driving licence:

class CheckDrivingLicenceValidationOnLifecycleUseCase(
  private val fetchLoggedInUserUseCase: FetchLoggedInUserUsecase,
  private val checkDrivingLicenceValidationUseCase: CheckDrivingLicenceValidationUseCase,
  private val lifecycleOwner: LifecycleOwner = ProcessLifecycleOwner.get(),
) {

  fun run() {
    lifeycycleOwner.lifecycleScope.launch {
      lifeycycleOwner.repeatOnLifecycle(state = Lifecycle.State.STARTED) {
        val user = fetchLoggedInUserUseCase.run() ?: run {
          println("User not logged in, not checking driving licence validation")
          return@repeatOnLifecycle
        }
        checkDrivingLicenceValidationUseCase.run(user)
      }
    }
  }

}

Note how none of this code is in anyway connected to our activity or other components. It relies purely on the lifecycle owner, the only thing you need to figure out, is from where you invoke the run() method! You can use these methods to also observe state changes and invoke work in a friendly manner!

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

No results found.

Jobs

If our app offers a login for a user, most likely we wish to cache data for this user. Whenever the user logs out, we also wish to delete the cached data. We could use a form of logout use case which deletes cached data:

// Define our use case
class LogoutUserUseCase(
  private val userRepository: UserRepository,
  private val invalidateCachesUseCase: InvalidateCachesUseCase,
) {
  
  fun run() {
    userRepository.setLoggedInUser(null)
    invalidateCachesUseCase.run()
  }

}

// Pretend we have some view model that handles logout button presses
class LogoutViewModel(
  private val logoutUserUseCase: LogoutUserUseCase,
) : ViewModel() {
  
  fun onLogout() {
    logoutUserUseCase.run()
  } 

}

// Handle unauthorized network responses
class UnauthorizedResponseInterceptor(
  private val userRepository: UserRepository,
) : Interceptor {

  override fun intercept(chain: Interceptor.Chain): Response {
    val response = chain.proceed(chain.request())
    if (response.code == 401) {
      userRepository.setLoggedInUser(null)
    }
    return response
  }

}

OH NOES! The developer didn’t notice the use case and managed to forget to clear the cache when our user is signed off due to an unauthorized response from the backend. How could they forget to use the LogoutUserUseCase? How dare they!

Forgetting things is human. In my opinion, the best code does its best to prevent mistakes from happening, rather than punishing you for it. As nobody is actually forcing you to call this specific LogoutUserUseCase, only the knowledge of the codebase and possible coding conventions will help you here to avoid making mistakes. What if we don’t have to forget? What if we can detect the logout and invalidate the caches in way so we do not care what originally triggered the logout? For that, we can simply create a new use case, which will only run if the app is in foreground to save some system resources while the app is in background:

class InvalidateCacheOnUserLogoutUseCase(
  private val observeLoggedInUserUseCase: ObserveLoggedInUserUseCase,
  private val invalidateCachesUseCase: InvalidateCachesUseCase,
  private val lifecycleOwner: LifecycleOwner = ProcessLifecycleOwner.get(),
) {

  fun run() {
    lifecycleOwner.lifecycleScope.launch {
      lifecycleOwner.repeatOnLifecycle(STARTED) {
        observeLoggedInUserUseCase.run().collect { loggedInUser ->
          if (loggedInUser == null) {
            invalidateCachesUseCase.run()
          }
        }
      }
    }
  }

}

As you can see, we do not have to check multiple possible entry points of user logout, we can simply observe the logged-in user in a system-resource-friendly way. We check whether a user is logged in or not, and if not, we invalidate the caches! So if your logged-in user data got corrupted due to a faulty update, due to inactivity, due to user pressing a logout button, we do not care! All we know is that when you are logged out, your data is protected by not having it anymore.

If we bunch all of them together, we can see now we can start up all this work in a centralized place and we do not have to remember to invalidate/refresh/check anything anywhere else! We have made sure that whenever a specific event occurs we do the right thing, instead of relying on chaining actions in known places.

class LifecycleUseCasesInitializer(
  private val refreshCacheOnLifecycleUseCase: RefreshCacheOnLifecycleUseCase,
  private val invalidateSensitiveDataOnLifecycleUseCase: InvalidateSensitiveDataOnLifecycleUseCase,
  private val checkDrivingLicenceValidationOnLifecycleUseCase: CheckDrivingLicenceValidationOnLifecycleUseCase,
  private val invalidateCacheOnUserLogoutUseCase: InvalidateCacheOnUserLogoutUseCase,
) {

  fun initialize() {
    refreshCacheOnLifecycleUseCase.run()
    invalidateCacheOnUserLogoutUseCase.run()
    checkDrivingLicenceValidationOnLifecycleUseCase.run()
    invalidateCacheOnUserLogoutUseCase.run()
  }
  
}

class MyApplication: Application() {

  val lifecycleUseCasesInitializer by inject<LifecycleUseCasesInitializer>()

  override fun onCreate() {
    super.onCreate()
    lifecycleUseCasesInitializer.initialize()
  }
}

I can hear you think: What if I forget to start up the use case to begin with? Well, I am sure you can find a way. In my project, we have exactly one initializer and that is the single centralized place to fire up all the other use cases. It doesn’t require any extra tooling and it allows for simple debugging, as you can easily track down the calls and references. I can also imagine you want to use some other tooling like the aforementioned androidx app startup and register all your components there. You can also implement a simple interface on all the use cases so you can find them with your DI framework and invoke them like that. I am sure you can find a way that suits your needs.

I hope I could inspire you to make the functionality of your applications more reactive and independent, so please let me know what you think in the comments and don’t forget to put those digital hands together if you actually liked what you saw! Joost out.

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE
Menu