Chapters

Hide chapters

Jetpack Compose by Tutorials

First Edition · Android 11 · Kotlin 1.4 · Android Studio Canary - Arctic Fox Release

11. Reacting to Compose Lifecycle
Written by Tino Balint

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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

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
Wulfem Tidzujort Ppeiqiq

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
Keuphyiw Pexkezimaaf Bzofoac

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) }
  val activeColor = MaterialTheme.colors.onSurface

  LaunchedEffect(Unit) {
    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, contentDescription = stringResource(id = R.string.search))
      },
      label = { Text(stringResource(R.string.search)) },
      modifier = modifier
        .fillMaxWidth()
        .padding(horizontal = 8.dp),
      colors = TextFieldDefaults.outlinedTextFieldColors(
        focusedBorderColor = activeColor,
        focusedLabelColor = activeColor,
        cursorColor = activeColor,
        backgroundColor = MaterialTheme.colors.surface
      )
    )
    SearchedCommunities(communities, viewModel, modifier)
  }
}
Community Chooser
Goqpejozy Kwuagir

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 = localBackPressedDispatcher.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 CompositionLocal. Replace BackButtonAction() with the following:

@Composable
fun BackButtonAction(onBackPressed: () -> Unit) {
  CompositionLocalProvider(
    localBackPressedDispatcher provides (
        LocalLifecycleOwner.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()
}

Effects in Compose

To understand the topic of 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(emptyList<String>()) }
  LaunchedEffect(searchText) { 
    communities = viewModel.searchCommunities(searchText)
  }

  Communities(communities)
}

RememberUpdatedState

When using LaunchedEffect, it is initiated every time the passed parameter changes. If you want to use a constant parameter that never changes, your effect will never restart which is a problem if you have values that need to be updated.

@Composable
fun LandingScreen(onSplashFinished: () -> NetworkData) {

  val currentOnSplashFinished by rememberUpdatedState(onSplashFinished)

  LaunchedEffect(Unit) {
    delay(SplashWaitTimeMillis)
    currentOnSplashFinished()
  }
}

ProduceState

Sometimes you want to do some work in the background and pass it down to the presentation layer. Remember that composable functions have States and any data used in composables needs to be converted into compose State in order to be used and survive the recomposition.

@Composable
fun loadBooks(author: String, booksRepository: BooksRepository): State<Result<List<Book>>> {
  return produceState(initialValue = Result.Loading, author, booksRepository) {
    
    val books = booksRepository.load(author)

    value = if (books == null) {
      Result.Error
    } else {
      Result.Success(books)
    }
  }
}

Migrate effects

If you used older version of Jetpack Compose, you might have have a few different effects that were not mentioned in this chapter. Those effects are now removed, but you can still achieve the same implementation using LaunchedEffect, DisposableEffect and SideEffect.

// onActive without subject parameter
onActive {
  someFunction()
}
LaunchedEffect(Unit) {
  someFunction()
}
// onActive with subject parameter
onActive(parameter) {
  someFunction()
}
LaunchedEffect(parameter) {
  someFunction()
}
// onActive with onDispose
onActive {
  val disposable = getData()
  
  onDispose {
    disposable.dispose()
  }
}
DisposableEffect(Unit) {
  val disposable = getData()

  onDispose {
    disposable.dispose()
  }
}
// onCommit without subject parameter
onCommit {
  someFunction()
}
SideEffect {
  someFunction()
}

Key points

  • 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.
  • Use rememberUpdatedState() when you want to launch your effect only once but still be able to update the values.
  • Use produceState() to directly convert non-composable states into composable states.
  • Names of the composables with a return type should start with the lowercase letter.

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 reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now