| /* |
| * Copyright 2021 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| // Ignore lint warnings in documentation snippets |
| @file:Suppress("unused", "UNUSED_PARAMETER", "UNUSED_VARIABLE") |
| |
| package androidx.compose.integration.docs.lifecycle |
| |
| import androidx.activity.OnBackPressedCallback |
| import androidx.activity.OnBackPressedDispatcher |
| import androidx.compose.foundation.layout.Box |
| import androidx.compose.foundation.layout.Column |
| import androidx.compose.foundation.layout.fillMaxSize |
| import androidx.compose.foundation.lazy.LazyColumn |
| import androidx.compose.foundation.lazy.items |
| import androidx.compose.material.Button |
| import androidx.compose.material.ExperimentalMaterialApi |
| import androidx.compose.material.Scaffold |
| import androidx.compose.material.ScaffoldState |
| import androidx.compose.material.Text |
| import androidx.compose.material.rememberScaffoldState |
| import androidx.compose.runtime.Composable |
| import androidx.compose.runtime.DisposableEffect |
| import androidx.compose.runtime.getValue |
| import androidx.compose.runtime.LaunchedEffect |
| import androidx.compose.runtime.SideEffect |
| import androidx.compose.runtime.Stable |
| import androidx.compose.runtime.State |
| import androidx.compose.runtime.derivedStateOf |
| import androidx.compose.runtime.key |
| import androidx.compose.runtime.mutableStateListOf |
| import androidx.compose.runtime.produceState |
| import androidx.compose.runtime.remember |
| import androidx.compose.runtime.rememberCoroutineScope |
| import androidx.compose.runtime.rememberUpdatedState |
| import androidx.compose.ui.Modifier |
| import kotlinx.coroutines.delay |
| import kotlinx.coroutines.launch |
| import kotlin.random.Random |
| |
| /** |
| * This file lets DevRel track changes to snippets present in |
| * https://developer.android.com/jetpack/compose/xxxxxxxxxxxxxxx |
| * |
| * No action required if it's modified. |
| */ |
| |
| private object LifecycleSnippet1 { |
| @Composable |
| fun MyComposable() { |
| Column { |
| Text("Hello") |
| Text("World") |
| } |
| } |
| } |
| private object LifecycleSnippet2 { |
| @Composable |
| fun LoginScreen(showError: Boolean) { |
| if (showError) { |
| LoginError() |
| } |
| LoginInput() // This call site is used to generate the key |
| } |
| |
| @Composable |
| fun LoginInput() { /* ... */ } |
| } |
| |
| private object LifecycleSnippet3 { |
| @Composable |
| fun MoviesScreen(movies: List<Movie>) { |
| LazyColumn { |
| items(movies) { movie -> |
| // All MovieOverview composables in Composition will have the same key! |
| // Thus, all calls will always recompose and restart all side effects. |
| MovieOverview(movie) |
| } |
| } |
| } |
| } |
| |
| private object LifecycleSnippet4 { |
| @Composable |
| fun MoviesScreen(movies: List<Movie>) { |
| LazyColumn { |
| items(movies) { movie -> |
| key(movie.id) { // Unique ID for this movie |
| MovieOverview(movie) |
| } |
| } |
| } |
| } |
| } |
| |
| private object LifecycleSnippet5 { |
| // Marking the type as stable to favor skipping and smart recompositions. |
| @Stable |
| interface UiState<T : Result<T>> { |
| val value: T? |
| val exception: Throwable? |
| |
| val hasError: Boolean |
| get() = exception != null |
| } |
| } |
| |
| @ExperimentalMaterialApi |
| private object LifecycleSnippet6 { |
| @Composable |
| fun MyScreen( |
| state: UiState<List<Movie>>, |
| scaffoldState: ScaffoldState = rememberScaffoldState() |
| ) { |
| |
| // If the UI state contains an error, show snackbar |
| if (state.hasError) { |
| |
| // `LaunchedEffect` will cancel and re-launch if `scaffoldState` changes |
| LaunchedEffect(scaffoldState) { |
| // Show snackbar using a coroutine, when the coroutine is cancelled the |
| // snackbar will automatically dismiss. This coroutine will cancel whenever |
| // `state.hasError` is false, and only start when `state.hasError` |
| // is true (due to the above if-check), or if `scaffoldState` changes. |
| scaffoldState.snackbarHostState.showSnackbar( |
| message = "Error message", |
| actionLabel = "Retry message" |
| ) |
| } |
| } |
| |
| Scaffold(scaffoldState = scaffoldState) { |
| /* ... */ |
| } |
| } |
| } |
| |
| @ExperimentalMaterialApi |
| private object LifecycleSnippet7 { |
| @Composable |
| fun MoviesScreen(scaffoldState: ScaffoldState = rememberScaffoldState()) { |
| |
| // Creates a CoroutineScope bound to the MoviesScreen's lifecycle |
| val scope = rememberCoroutineScope() |
| |
| Scaffold(scaffoldState = scaffoldState) { |
| Column { |
| /* ... */ |
| Button( |
| onClick = { |
| // Create a new coroutine in the event handler to show a snackbar |
| scope.launch { |
| scaffoldState.snackbarHostState.showSnackbar("Something happened!") |
| } |
| } |
| ) { |
| Text("Press me") |
| } |
| } |
| } |
| } |
| } |
| |
| private object LifecycleSnippet8 { |
| @Composable |
| fun LandingScreen(onTimeout: () -> Unit) { |
| |
| // This will always refer to the latest onTimeout function that |
| // LandingScreen was recomposed with |
| val currentOnTimeout by rememberUpdatedState(onTimeout) |
| |
| // Create an effect that matches the lifecycle of LandingScreen. |
| // If LandingScreen recomposes, the delay shouldn't start again. |
| LaunchedEffect(Unit) { |
| delay(SplashWaitTimeMillis) |
| currentOnTimeout() |
| } |
| |
| /* Landing screen content */ |
| } |
| } |
| |
| private object LifecycleSnippet9 { |
| @Composable |
| fun BackHandler(backDispatcher: OnBackPressedDispatcher, onBack: () -> Unit) { |
| |
| // Safely update the current `onBack` lambda when a new one is provided |
| val currentOnBack by rememberUpdatedState(onBack) |
| |
| // Remember in Composition a back callback that calls the `onBack` lambda |
| val backCallback = remember { |
| // Always intercept back events. See the SideEffect for a more complete version |
| object : OnBackPressedCallback(true) { |
| override fun handleOnBackPressed() { |
| currentOnBack() |
| } |
| } |
| } |
| |
| // If `backDispatcher` changes, dispose and reset the effect |
| DisposableEffect(backDispatcher) { |
| // Add callback to the backDispatcher |
| backDispatcher.addCallback(backCallback) |
| |
| // When the effect leaves the Composition, remove the callback |
| onDispose { |
| backCallback.remove() |
| } |
| } |
| } |
| } |
| |
| private object LifecycleSnippet10 { |
| @Composable |
| fun BackHandler( |
| backDispatcher: OnBackPressedDispatcher, |
| enabled: Boolean = true, // Whether back events should be intercepted or not |
| onBack: () -> Unit |
| ) { |
| // START - DO NOT COPY IN CODE SNIPPET |
| val currentOnBack by rememberUpdatedState(onBack) |
| |
| val backCallback = remember { |
| // Always intercept back events. See the SideEffect for a more complete version |
| object : OnBackPressedCallback(true) { |
| override fun handleOnBackPressed() { |
| currentOnBack() |
| } |
| } |
| } |
| // END - DO NOT COPY IN CODE SNIPPET, just use /* ... */ |
| |
| // On every successful composition, update the callback with the `enabled` value |
| // to tell `backCallback` whether back events should be intercepted or not |
| SideEffect { |
| backCallback.isEnabled = enabled |
| } |
| |
| /* Rest of the code */ |
| } |
| } |
| |
| private object LifecycleSnippet11 { |
| @Composable |
| fun loadNetworkImage( |
| url: String, |
| imageRepository: ImageRepository |
| ): State<Result<Image>> { |
| |
| // Creates a State<T> with Result.Loading as initial value |
| // If either `url` or `imageRepository` changes, the running producer |
| // will cancel and will be re-launched with the new inputs. |
| return produceState(initialValue = Result.Loading, url, imageRepository) { |
| |
| // In a coroutine, can make suspend calls |
| val image = imageRepository.load(url) |
| |
| // Update State with either an Error or Success result. |
| // This will trigger a recomposition where this State is read |
| value = if (image == null) { |
| Result.Error |
| } else { |
| Result.Success(image) |
| } |
| } |
| } |
| } |
| |
| private object LifecycleSnippet12 { |
| @Composable |
| fun TodoList(highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")) { |
| |
| val todoTasks = remember { mutableStateListOf<String>() } |
| |
| // Calculate high priority tasks only when the todoTasks or highPriorityKeywords |
| // change, not on every recomposition |
| val highPriorityTasks by remember(todoTasks, highPriorityKeywords) { |
| derivedStateOf { todoTasks.filter { it.containsWord(highPriorityKeywords) } } |
| } |
| |
| Box(Modifier.fillMaxSize()) { |
| LazyColumn { |
| items(highPriorityTasks) { /* ... */ } |
| items(todoTasks) { /* ... */ } |
| } |
| /* Rest of the UI where users can add elements to the list */ |
| } |
| } |
| } |
| |
| private object LifecycleSnippet13 { |
| @Composable |
| fun BackHandler(backDispatcher: OnBackPressedDispatcher, onBack: () -> Unit) { |
| // START - DO NOT COPY IN CODE SNIPPET |
| val currentOnBack by rememberUpdatedState(onBack) |
| |
| val backCallback = remember { |
| // Always intercept back events. See the SideEffect for a more complete version |
| object : OnBackPressedCallback(true) { |
| override fun handleOnBackPressed() { |
| currentOnBack() |
| } |
| } |
| } |
| // END - DO NOT COPY IN CODE SNIPPET, just use /* ... */ |
| |
| DisposableEffect(backDispatcher) { |
| backDispatcher.addCallback(backCallback) |
| onDispose { |
| backCallback.remove() |
| } |
| } |
| } |
| } |
| |
| /* |
| Fakes needed for snippets to build: |
| */ |
| private const val SplashWaitTimeMillis = 1000L |
| |
| @Composable |
| private fun LoginError() { } |
| |
| @Composable |
| private fun MovieOverview(movie: Movie) { } |
| private data class Movie(val id: Long) |
| |
| private data class UiState<T>( |
| val loading: Boolean = false, |
| val exception: Exception? = null, |
| val data: T? = null |
| ) { |
| val hasError: Boolean |
| get() = exception != null |
| } |
| |
| private class Image |
| private class ImageRepository { |
| fun load(url: String): Image? = if (Random.nextInt() == 0) Image() else null // Avoid warnings |
| } |
| |
| private sealed class Result<out R> { |
| data class Success<out T>(val data: T) : Result<T>() |
| object Loading : Result<Nothing>() |
| object Error : Result<Nothing>() |
| } |
| |
| private class User |
| private class Weather |
| private class Greeting(val name: String) |
| private fun prepareGreeting(user: User, weather: Weather) = Greeting("haha") |
| |
| private fun String.containsWord(input: List<String>): Boolean = false |