Chapters

Hide chapters

Jetpack Compose by Tutorials

First Edition · Android 11 · Kotlin 1.4 · Android Studio Canary

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

11. Reacting to Compose Lifecycle
Written by Tino Balint

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

In previous chapters, you focused on building the JetReddit app by adding advanced layouts and complex UI.

In this chapter, you’ll learn how to react to the lifecycle of composable functions. This approach will allow you to execute your code at specific moments while your composable is active.

Jetpack Compose offers a list of events that can trigger at specific points in the the lifecycle, called effects. Throughout this chapter, you’ll learn about the different kinds of effects and how to use them to implement your logic.

Events in Compose

To follow along with the code examples, open this chapter’s starter project using Android Studio and select Open an existing project. Navigate to 11-reacting-to-compose-lifecycle/projects and select the starter folder as the project root. Once the project opens, let it build and sync and you’re ready to go!

You might already be familiar with the project hierarchy from the previous chapter, but in case you aren’t, look at the following image:

Project Hierarchy
Project Hierarchy

In this chapter, you’ll only work with two of these packages: screens, to implement a new screen, and routing, to add a new routing option. The rest of the packages are already prepared to handle navigation, fetching data from the database, dependency injection and theme switching for you.

Once you’re familiar with the file organization, build and run the app. You’ll see:

Home Screen
Home Screen

This is a fully implemented home screen. When you browse the app, you’ll notice that two screens are pre-built and implemented for you: My Profile, in the app drawer, and New Post, the third option in the bottom navigation.

In this chapter, you’ll implement the option to choose a community inside the New Post screen:

New Post Screen
New Post Screen

However, before you start building this new screen, you need to learn more about effects in Compose.

Basic effects in Compose

In Compose, an effect is an event that triggers at a specific time during the composable lifecycle. There are three basic events:

onActive { Log.d("HomeScreen", "onActive") }
onCommit { Log.d("HomeScreen", "onCommit") }
onDispose { Log.d("HomeScreen", "onDispose") }

Implementing the community chooser

Next, you’ll implement a community chooser like the one the original Reddit app uses. Look at the following image for reference:

Reddit Community Chooser
Nijmih Xurrewofq Pbuilom

Creating a list of communities

As you learned in the previous chapters, you’ll build the smaller components first, starting with SearchedCommunities(). Start by changing SearchedCommunities() code to the following:

@Composable
fun SearchedCommunities(
  communities: List<String>,
  viewModel: MainViewModel?,
  modifier: Modifier = Modifier
) {
  communities.forEach {
    Community(
      text = it,
      modifier = modifier,
      onCommunityClicked = {
        viewModel?.selectedCommunity?.postValue(it)
        JetRedditRouter.goBack()
      }
    )
  }
}
@Preview
@Composable
fun SearchedCommunitiesPreview() {
  Column {
    SearchedCommunities(defaultCommunities, null, Modifier)
  }
}
Searched Communities Preview
Puadxnap Xistuwugoet Hjaqueh

Making the community list searchable

The next step is to add a TextField() to search the communities according to user input. Replace ChooseCommunityScreen() with the code below:

@Composable
fun ChooseCommunityScreen(viewModel: MainViewModel, modifier: Modifier = Modifier) {
  val scope = rememberCoroutineScope()
  val communities: List<String> by viewModel.subreddits.observeAsState(emptyList())
  var searchedText by remember { mutableStateOf("") }
  var currentJob by remember { mutableStateOf<Job?>(null) }

  onActive {
    viewModel.searchCommunities(searchedText)
  }

  Column {
    ChooseCommunityTopBar()
    TextField(
      value = searchedText,
      onValueChange = {
        searchedText = it
        currentJob?.cancel()
        currentJob = scope.async {
          delay(SEARCH_DELAY_MILLIS)
          viewModel.searchCommunities(searchedText)
        }
      },
      leadingIcon = { Icon(Icons.Default.Search) },
      label = { Text(stringResource(R.string.search)) },
      modifier = modifier
        .fillMaxWidth()
        .padding(horizontal = 8.dp),
      backgroundColor = MaterialTheme.colors.surface,
      activeColor = MaterialTheme.colors.onSurface
    )
    SearchedCommunities(communities, viewModel, modifier)
  }
}
Community Chooser
Jewlifuhl Zmuivek

Implementing the back button handler

In previous sections, you used built-in back button handlers. This time, you’ll use effects to build your own.

@Composable
fun BackButtonHandler(
  enabled: Boolean = true,
  onBackPressed: () -> Unit
) {
  val dispatcher = BackPressedDispatcher.current ?: return
  val backCallback = remember {
    object : OnBackPressedCallback(enabled) {
      override fun handleOnBackPressed() {
        onBackPressed.invoke()
      }
    }
  }
  DisposableEffect(dispatcher) {
    dispatcher.addCallback(backCallback)
    onDispose {
      backCallback.remove()
    }
  }
}

Adding an action to the back button

The next step is to build BackButtonAction() and provide the previous Ambient. Replace BackButtonAction() with the following:

@Composable
fun BackButtonAction(onBackPressed: () -> Unit) {
  Providers(
    BackPressedDispatcher provides (
        AmbientLifecycleOwner.current as ComponentActivity
        ).onBackPressedDispatcher
  ) {
    BackButtonHandler {
      onBackPressed.invoke()
    }
  }
}

Calling the back button’s action

Now that you’ve implemented BackButtonAction(), the only thing left to do is to call it from inside ChooseCommunityScreen().

BackButtonAction {
  JetRedditRouter.goBack()
}

Complex effects in Compose

To understand the topic of complex effects more clearly, you first need to learn how side effects work in Compose.

SideEffect

SideEffect() ensures that your event only executes when a composition is successful. If the composition fails, the event is discarded. In addition, only use it when you don’t need to dispose the event, but want it to run with every recomposition.

@Composable
fun MainScreen(router: Router) {
  val drawerState = rememberDrawerState(DrawerValue.Closed)

  SideEffect {
    router.isRoutingEnabled = drawerState.Closed
  }
}

LaunchedEffect

LaunchedEffect launches a coroutine into the composition’s CoroutineScope. Just like rememberCoroutineScope(), its coroutine is canceled when LaunchedEffect leaves the composition and will relaunch on recomposition.

@Composable
fun SpeakerList(searchText: String) {
  var communities by remember { mutableStateOf<List<String>>(emptyList()) }
  LaunchedEffect(searchText) { 
    communities = viewModel.searchCommunities(searchText)
  }

  Communities(communities)
}

Invalidate

invalidate() is an Effect that manually invalidates the composition, which causes recomposition.

@Composable
fun MyComposable(viewModel: ViewModel) {
    val name = viewModel.getName { invalidate() }
    Text(text = "Hello: $name")
}

Key points

  • onActive() triggers the event only once, upon the first composition.
  • onCommit() triggers an event every time composition occurs.
  • onDispose() triggers an event when your composable leaves the composition.
  • Use rememberCoroutineScope() when you are using coroutines and need to cancel and relaunch the coroutine after an event.
  • Use LaunchedEffect() when you are using coroutines and need to cancel and relaunch the coroutine every time your parameter changes and it isn’t stored in a mutable state.
  • DisposableEffect() is useful when you aren’t using coroutines and need to dispose and relaunch the event every time your parameter changes.
  • SideEffect() triggers an event only when the composition is successful and you don’t need to dispose the subject.
  • invalidate() manually triggers recomposition.

Where to go from here?

Congratulations! Now, you know how to react to Compose lifecycle, which is one of the most complex parts of Jetpack Compose. At this point, you’ve seen an overview of how to solve some of the most complex and important problems you encounter while working with Compose.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now