Apps foundation

Leve o estado do Compose além da interface com o AppState do Jetpack

Alto-falantes

  • Ben Sagmoe

    Engenheiro de relações com desenvolvedores

  • Elif Bilgin Morris

    Engenheiro de software

Sobre

Sobre a palestra

O ambiente de execução de snapshot, o rastreamento automático de dependências e as atualizações atômicas do Compose não precisam parar na camada de interface. O Jetpack AppState permite mover o estado do Compose para cima, simplificando o que chega às telas ao elevar e derivar o estado com o sistema de snapshot antes que ele chegue à interface.

Nesta sessão, você vai conhecer as duas bibliotecas fundamentais do AppState. Vamos falar sobre o StateStore para elevação de estado e o Transform para usar a composição para derivar o estado com o fluxo de controle simples do Kotlin, eliminando cadeias de combinação de fluxo complexas e mantendo a interface sincronizada sem esforço.

Resumo

  • O Compose Runtime funciona como um mecanismo de gerenciamento de estado de uso geral sem a interface do Compose. O sistema de snapshots rastreia as leituras de forma dinâmica e mantém as atualizações simultâneas consistentes em todas as propriedades.
  • A Jetpack AppState Transform calcula e expõe o estado do Compose de blocos combináveis headless. Ele substitui cadeias complexas de operadores de fluxo, como combine, e oferece suporte a retornos antecipados e derivações de estado aninhadas.
  • O Transform usa um relógio de frame headless adiantado para executar transformações fora da linha de execução de interface. O relógio é estacionado sempre que os estados monitorados permanecem inalterados, evitando desperdício de trabalho.
  • O StateStore do Jetpack AppState compartilha o estado do Compose entre diferentes componentes, como atividades e serviços em segundo plano. A integração com o DataStore permite que chaves individuais sobrevivam ao encerramento do processo sem código boilerplate personalizado.

Recursos

Gemini Notebook

Tópicos

  • jetpack-compose
  • jetpack-appstate
  • state-management
  • android-architecture
  • compose-runtime

Transcrição

Introdução

  • Olá! Este é o curso "Leve o estado do Compose além da interface com o AppState do Jetpack".

  • Meu nome é Ben, e sou engenheiro de relações com desenvolvedores que trabalha nas bibliotecas de arquitetura do Jetpack.

  • Eu sou Elif, uma das engenheiras que trabalha nas bibliotecas de arquitetura de apps.

  • Hoje, vamos falar sobre um futuro em que o Compose vai além da interface e entra nas camadas de dados e de domínio dos aplicativos.

  • Como vocês já devem saber, o Compose é o kit de ferramentas recomendado para o desenvolvimento de IU no Android.

  • De smartphones e tablets a widgets, relógios, TVs e dispositivos XR, o Compose é a base de tudo.

  • Mas, à medida que o crescimento em apps nessas plataformas de interface e componentes em segundo plano, o gerenciamento do estado por trás deles fica mais difícil, especialmente em relação a dois problemas comuns.

  • A primeira é derivar um estado complexo.

  • Combinar vários fluxos de dados, entradas de IU locais e estado de ambiente, como conectividade de rede, geralmente se transforma em um emaranhado de operadores, como combine, flatMapLatest e distinctUntilChanged.

  • A segunda é compartilhar o estado entre componentes: manter uma atividade, um serviço, um widget de app e outros elementos sincronizados e garantir que esse estado sobreviva ao encerramento do processo quando necessário.

  • Quando analisamos esses desafios, percebemos como o Compose é criado.

  • A maioria das pessoas pensa no Compose como um kit de ferramentas de interface, mas, na verdade, ele é composto por duas camadas distintas: a interface do Compose na parte de cima e o ambiente de execução do Compose na parte de baixo.

  • Enquanto a IU do Compose processa o layout, o desenho e a entrada, o ambiente de execução do Compose é um mecanismo de uso geral para gerenciamento de estado e recomposição.

  • Ele não tem dependência da interface.

  • Essa foi a conclusão que nos levou a criar o Jetpack AppState.

  • É uma família de bibliotecas que tem como foco aproveitar ao máximo o estado do Compose além da interface.

O que é o estado do Compose?

  • Mas antes de falarmos sobre o AppState, vamos garantir que estamos na mesma página quando falamos sobre o estado do Compose.

  • A resposta é androidx.compose.runtime.State.

    /**
     * A value holder where reads to the [value] property during the execution of a [Composable]
     * function, the current [RecomposeScope] will be subscribed to changes of that value.
     */
    @Stable
    public interface State<out T> {
        public val value: T
    }
  • E a interface que você provavelmente conhece melhor, MutableState.

    /**
     * A mutable value holder where reads to the [value] property during the execution of a [Composable]
     * function, the current [RecomposeScope] will be subscribed to changes of that value. When the
     * [value] property is written to and changed, a recomposition of any subscribed [RecomposeScope]s
     * will be scheduled. If [value] is written to with the same value, no recompositions will be
     * scheduled.
     */
    @Stable
    public interface MutableState<T> : State<T> {
        override var value: T
        public operator fun component1(): T
        public operator fun component2(): (T) -> Unit
    }
  • Basicamente, são apenas contêineres simples.

  • O que faz com que eles realmente funcionem é o sistema de snapshot do Compose.

  • Portanto, quando dizemos "estado do Compose", geralmente estamos nos referindo ao estado do snapshot.

  • Portanto, ao chamar mutableStateOf, você cria um contêiner que roteia leituras e gravações pelo sistema de snapshots.

  • Sempre que o código é executado dentro de um observador de snapshot, como uma composição, você recebe duas propriedades que um stream como um Flow não oferece.

    @Composable
    fun Counter() {
        val count = remember { mutableStateOf(0) }
    }
  • A primeira é o rastreamento de leitura automático e dinâmico.

    @Composable
    fun Counter() {
        val count = remember { mutableStateOf(0) }
        Column {
            Text("Count: ${count.value}")
            AddButton(onClick = { count.value++ })
        }
    }
  • Portanto, automático significa que não há inscrição ou coleta.

  • A assinatura é apenas para ler .value.

  • Portanto, se analisarmos o Counter aqui, a leitura de count.value diretamente dentro do elemento combinável informa essa leitura à composição e registra o Counter na lista de leitores.

  • Agora, observe AddButton.

    @Composable
    fun Counter() {
        val count = remember { mutableStateOf(0) }
        Column {
            Text("Count: ${count.value}")
            AddButton(onClick = { count.value++ })
        }
    }
    
    @Composable
    fun AddButton(onClick: () -> Unit) {
        Button(onClick = onClick) {
            Text("Add")
        }
    }
  • Ele só grava para contar no clique.

  • Portanto, não é um leitor.

  • Quando um usuário clica em AddButton e modifica a contagem, o sistema de snapshot rastreia essa gravação, o que permite que a composição saiba que precisa invalidar Counter, mas não AddButton.

  • E dinâmico significa que o rastreamento de leitura acontece no ambiente de execução com base nos caminhos de código que realmente são executados.

    @Composable
    fun Counter() {
        val isVisible = remember { mutableStateOf(true) }
        val count = remember { mutableStateOf(0) }
        Column {
            if (isVisible.value) {
                Text("Count: ${count.value}")
            } else {
                Text("Hidden")
            }
            AddButton(onClick = { count.value++ })
        }
    }
  • Aqui, estamos protegendo o texto do contador com uma flag isVisible.

  • Quando isVisible é verdadeiro, isVisible e count são lidos.

  • Portanto, Counter é registrado como leitor de ambos.

  • E quando isVisible é falso, se a ramificação "if" não é executada, "count" não é lido, então "Counter" é registrado apenas como um leitor de isVisible, não de "count".

    @Composable
    fun Counter() {
        val isVisible = remember { mutableStateOf(false) }
        val count = remember { mutableStateOf(0) }
        Column {
            if (isVisible.value) {
                Text("Count: ${count.value}")
            } else {
                Text("Hidden")
            }
            AddButton(onClick = { count.value++ })
        }
    }
  • Portanto, quando AddButton grava em count, não há mais nada para invalidar.

  • A segunda propriedade é não ter que escolher entre consistência e granularidade.

  • Consistência significa que, quando você atualiza vários valores em um snapshot, essas mudanças permanecem isoladas enquanto o código é executado, mesmo que outra linha de execução esteja gravando neles ao mesmo tempo.

  • Essas atualizações ficam visíveis de uma só vez quando o snapshot é concluído.

  • Granularidade significa que propriedades separadas podem ser declaradas e rastreadas individualmente, principalmente quando não estão relacionadas.

  • Portanto, atualizar uma propriedade só invalida os leitores que realmente dependem dela.

  • Vamos ver isso em um código real.

  • Por exemplo, vamos analisar um jogo que tem três partes de estado: um nome de nível, tempo de sobrevivência e pontuação.

    val levelName: String = "Level 1 - Stay Alive!"
    val timeSurvived: Int = 0
    val score: Double = 0.0
  • Para começar, vamos analisar uma implementação do Flow que escolhe a consistência em detrimento da granularidade, agrupando tudo em uma classe GameUiState.

    data class GameUiState(
        val levelName: String = "Level 1 - Stay Alive!",
        val timeSurvived: Int = 0,
        val score: Double = 0.0
    )
  • Isso significa que, sempre que updateScore é executado, você copia todo o GameUiState para que timeSurvived e a pontuação possam ser atualizados juntos em sincronia.

    data class GameUiState(
        val levelName: String = "Level 1 - Stay Alive!",
        val timeSurvived: Int = 0,
        val score: Double = 0.0
    )
    
    class GameViewModel : ViewModel() {
        val uiState = MutableStateFlow(GameUiState())
        fun updateScore() = uiState.update {
            it.copy(
                timeSurvived = it.timeSurvived + 1,
                score = it.score + 10 + .01 * it.timeSurvived
            )
        }
    }
  • Na interface, o GameScreen coleta o estado da interface e faz a leitura dele para transmitir as propriedades relevantes para Header e ScoreBadge.

    data class GameUiState(
        val levelName: String = "Level 1 - Stay Alive!",
        val timeSurvived: Int = 0,
        val score: Double = 0.0
    )
    
    class GameViewModel : ViewModel() {
        val uiState = MutableStateFlow(GameUiState())
        fun updateScore() = uiState.update {
            it.copy(
                timeSurvived = it.timeSurvived + 1,
                score = it.score + 10 + .01 * it.timeSurvived
            )
        }
    }
    
    @Composable
    fun GameScreen(viewModel: GameViewModel = viewModel()) {
        val state by viewModel.uiState.collectAsStateWithLifecycle()
        Column {
            Header(levelName = state.levelName)
            ScoreBadge(
                timeSurvived = state.timeSurvived,
                score = state.score
            )
        }
    }
  • Como GameScreen lê todos os estados, cada chamada para updateScore invalida GameScreen, mesmo que os dados do cabeçalho nunca tenham mudado.

  • Agora, você pode dividir cada propriedade em um Flow próprio para ter granularidade, mas perderia a consistência, porque outra linha de execução poderia atualizar um valor enquanto o outro bloco de atualização estivesse em execução.

  • Agora, vamos considerar uma implementação alternativa baseada em estado.

    @Stable
    class GameUiState {
        var levelName by mutableStateOf("Level 1 - Stay Alive!")
        var timeSurvived by mutableStateOf(0)
        var score by mutableStateOf(0.0)
    }
  • Neste, usamos um GameUiState novamente.

  • Mas, neste caso, é um pouco diferente, porque cada propriedade delega para mutableStateOf.

  • Aqui, o updateScore une as gravações em withMutableSnapshot, o que garante a consistência entre as gravações.

    @Stable
    class GameUiState {
        var levelName by mutableStateOf("Level 1 - Stay Alive!")
        var timeSurvived by mutableStateOf(0)
        var score by mutableStateOf(0.0)
    }
    
    class GameViewModel : ViewModel() {
        val uiState = GameUiState()
        fun updateScore() = Snapshot.withMutableSnapshot {
            uiState.timeSurvived += 1
            uiState.score += 10 + .01 * uiState.timeSurvived
        }
    }
  • Em seguida, na interface, GameScreen transmite o estado diretamente para o cabeçalho e ScoreBadge.

    @Stable
    class GameUiState {
        var levelName by mutableStateOf("Level 1 - Stay Alive!")
        var timeSurvived by mutableStateOf(0)
        var score by mutableStateOf(0.0)
    }
    
    class GameViewModel : ViewModel() {
        val uiState = GameUiState()
        fun updateScore() = Snapshot.withMutableSnapshot {
            uiState.timeSurvived += 1
            uiState.score += 10 + .01 * uiState.timeSurvived
        }
    }
    
    @Composable
    fun GameScreen(viewModel: GameViewModel = viewModel()) {
        val state = viewModel.uiState
        Column {
            Header(state = state)
            ScoreBadge(state = state)
        }
    }
  • Ele não lê nenhuma das propriedades. Portanto, quando updateScore é executado, GameScreen não é um leitor.

  • Somente o componente real, neste caso, ScoreBadge, é invalidado.

  • Por fim, observe que nada disso está vinculado à interface.

  • A IU é apenas um lugar em que essas propriedades realmente se destacam.

  • E com isso, agradecemos por assistir à nossa palestra.

  • Ok, brincadeiras à parte, vou passar a palavra para Elif, que vai apresentar a primeira biblioteca, Transform.

Introdução ao Transform

  • Ok.

  • Obrigado, Ben.

  • Como o Ben acabou de observar, o estado do snapshot do Compose oferece rastreamento de leitura, consistência e granularidade automáticos e dinâmicos. Isso me leva à pergunta: e se pudéssemos ter nossos fluxos de dados também orientados por um snapshot de estado do Compose, um estado de snapshot?

  • E se o estado é nosso modelo de dados, como calculamos e transformamos um estado para todos os consumidores?

  • É aí que entra a nova biblioteca AppState Transform do Jetpack.

  • Antes de nos aprofundarmos na biblioteca, precisamos falar sobre como ela surgiu.

  • Portanto, a ideia de aproveitar o mecanismo do Compose, que é o ambiente de execução e o compilador, para produzir valores de estado em uma composição sem interface não é nova.

  • Muitos de vocês já devem usar ou conhecer a biblioteca Molecule.

  • E, por headless, queremos dizer que não há interface.

  • O Molecule foi pioneiro em demonstrar o poder de usar o Compose Runtime, mostrando na prática que uma função combinável não precisa emitir nós de interface e pode ser usada para escrever uma função combinável que retorna um modelo de dados puro.

  • No caso do Molecule, era um fluxo do Kotlin ou um StateFlow.

  • Isso significa que, em vez de encadear operadores reativos, como combine, você pode escrever em Kotlin comum.

  • Mas, em vez de lidar com fluxos, e se o modelo de dados fosse State, já que a IU já está no Compose?

  • Com o Transform, seu modelo de dados é o State.

  • Vamos dar uma olhada na API.

    public fun <R> transform(
        initialValue: R,
        scope: CoroutineScope,
        onUpdate: @Composable (R) -> R,
    ): State<R>
  • Essa é a API inteira.

  • É muito simples, uma função.

  • Ele retorna um State observável simples, não um Flow nem um StateFlow.

  • E quem o consome não sabe ou não se importa que uma Transform o tenha produzido.

  • O que é realmente interessante aqui é este bloco.

  • É combinável.

  • Isso significa que tudo o que você já sabe funciona dentro dele.

  • Lembre-se de que LaunchedEffect, collectAsState, retornos antecipados, ifs simples, tudo o que você pode ler de dentro de um elemento que pode ser composto, pode ser derivado de uma transformação.

  • Vale lembrar que todas as APIs que estamos vendo hoje nesta apresentação estão na versão Alfa e estão sujeitas a mudanças. Mesmo assim, vamos dar uma olhada em um exemplo.

  • Vamos usar um app de timer de foco muito simples e pequeno.

  • Você escolhe uma tarefa, define um timer de 25 minutos e o relógio começa a contar.

  • Sempre que você cria uma tela única, mesmo que seja simples como esta, provavelmente vai precisar combinar e gerenciar vários fluxos de dados independentes para montar o estado em segundo plano.

  • Para a tela de foco, digamos que temos duas: a sessão de foco em si, que vai ser a fase em que estamos, ou seja, pausada ou não, quantos segundos restam, e tudo isso vai vir de um mecanismo de timer como um StateFlow que vai marcar, digamos, a cada segundo.

  • O outro fluxo de dados será a tarefa atual em que o usuário está trabalhando, que virá de um banco de dados do Room como um Flow.

  • Portanto, dois fluxos de dados e um estado de tela no final.

  • O ViewModel é onde eles serão reunidos, e a saída, que chamaremos de resumo, será o trecho de código que vamos escrever em seguida.

  • Tradicionalmente, isso significa que, no ViewModel, você precisa combinar esses fluxos usando operadores encadeados.

    class FocusScreenViewModel(
        focusManager: FocusSessionManager,
        taskRepository: TaskRepository
    ) : ViewModel() {
        val summary: StateFlow<FocusSummary> =
            combine(focusManager.sessionFlow, taskRepository.tasksFlow) { session, tasks ->
                if (session.phase == FocusPhase.Idle) {
                    FocusSummary.Idle
                } else {
                    val task = tasks.find { it.id == session.activeTaskId }
                    FocusSummary.Active(...)
                }
            }.stateIn(
                    scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = FocusSummary.Idle,
    )
    }
  • Então, combine os dois fluxos em streams, a ramificação dentro da lambda, então se houver outra ali, e um estado no final.

  • Com o Transform, podemos descartar todo esse código boilerplate.

    class FocusScreenViewModel(
        focusManager: FocusSessionManager,
        taskRepository: TaskRepository
    ) : ViewModel() {
        val summary: State<FocusSummary> =
            transform<FocusSummary>(
    initialValue = FocusSummary.Idle,
    scope = viewModelScope,
    container = handle.getOrCreateContainer("transform")
    ) {
                val session by focusManager.sessionFlow.collectAsState()
                if (session.phase == FocusPhase.Idle) return@transform FocusSummary.Idle
                val tasks by taskRepository.tasksFlow.collectAsState(emptyList())
                val task = tasks.find { it.id == session.activeTaskId }
                FocusSummary.Active(...)
  • Essa chamada parece familiar porque temos um valor inicial em um escopo sendo transmitido, mas não há nada upstream para encadear. A derivação é o próprio bloco.

  • E, ao contrário de stateIn, em que você provavelmente precisaria configurar uma suspensão temporária de WhileSubscribed para pausar e retomar os fluxos upstream, uma transformação é executada continuamente durante o ciclo de vida desse escopo.

  • Nossas fontes são fluxos, e eles são bem-vindos.

    class FocusScreenViewModel(
        focusManager: FocusSessionManager,
        taskRepository: TaskRepository
    ) : ViewModel() {
        val summary: State<FocusSummary> =
            transform<FocusSummary>(
                initialValue = FocusSummary.Idle,
                scope = viewModelScope,
                       container = handle.getOrCreateContainer("transform")
            ) {
                val session by focusManager.sessionFlow.collectAsState()
                if (session.phase == FocusPhase.Idle)
                    return@transform FocusSummary.Idle
                val tasks by taskRepository.tasksFlow.collectAsState(emptyList())
                val task = tasks.find {
                    it.id == session.activeTaskId
                }
                         FocusSummary.Active(...)
            }
    }
  • Vamos conectar cada um com collectAsState, e a sessão é um StateFlow. Portanto, não precisamos de um valor inicial, mas o fluxo do Room é frio, então fornecemos um.

  • E essa é toda a história da assinatura.

  • A leitura é o que nos inscreve.

  • É o rastreamento automático de leitura que o Ben mencionou no exemplo que vimos, mas em um ViewModel.

  • Podemos até introduzir retornos antecipados como este aqui quando estivermos em um estado inativo.

  • Digamos que não temos novas tarefas, então não vamos nem processar o resto.

  • E, mais uma vez, o rastreamento dinâmico de leitura está valendo a pena.

  • Enquanto estamos inativos, o fluxo do Room não é coletado.

  • Por fim, emitimos o resumo.

  • Portanto, se o usuário pausar a tarefa ou o timer for acionado, tudo será atualizado automaticamente.

Transformações aninhadas

  • As coisas ficam um pouco mais complicadas quando saímos do exemplo de um único ViewModel.

  • Digamos que essa mesma sessão precise aparecer em mais duas plataformas: uma barra de timer em miniatura que fica na parte de baixo de todas as telas do app e um widget da tela inicial.

  • Ambos vão precisar das mesmas informações derivadas sobre o resumo da sessão, qual fase, se está pausado ou não, quanto tempo resta no timer.

  • Isso significa que, se cada superfície montasse a própria cópia, seria a mesma derivação três vezes em três lugares diferentes que precisam estar de acordo entre si.

  • Portanto, o que precisamos fazer é derivar o resumo uma vez no escopo do aplicativo e, em seguida, tudo o que estiver downstream vai consumi-lo.

  • Então, como podemos implementar algo assim?

  • Nossa implementação provavelmente vai precisar de três partes.

    class FocusSessionManager(taskRepository: TaskRepository, scope: CoroutineScope) {
        ...
    }
    
    class MiniTimerViewModel(focusManager: FocusSessionManager) : ViewModel() {
        ...
    }
    
    class FocusWidget : GlanceAppWidget() {
        override suspend fun provideGlance(context: Context, id: GlanceId) {
            provideContent {
                ...
            }
        }
    }
  • Portanto, um gerenciador de sessões no escopo do aplicativo, que é onde o resumo será derivado, um ViewModel do minicrono e um widget do Resumo, que vão consumir o resumo derivado nesse gerenciador de sessões de foco.

  • Vamos criar cada um da maneira tradicional e depois com o Transform.

  • Então, tradicionalmente, começaríamos derivando o resumo uma vez no escopo do aplicativo como um StateFlow.

    class FocusSessionManager(
        taskRepository: TaskRepository,
        scope: CoroutineScope
    ) {
        val sessionFlow: StateFlow<FocusSession> = ...
        val summary: StateFlow<FocusSummary> =
            combine(
                sessionFlow,
                taskRepository.tasksFlow
            ) { session, tasks ->
                if (session.phase == FocusPhase.Idle) {
                    FocusSummary.Idle
                } else {
                    val task = tasks.find {
                        it.id == session.activeTaskId
                    }
                    FocusSummary.Active(...)
                }
            }.stateIn(
    scope = appScope,
    started = SharingStarted.WhileSubscribed(5_000),
    initialValue = FocusSummary.Idle
    )
    }
  • São as mesmas chamadas de combinação e stateIn que vimos anteriormente no exemplo de ViewModel, mas movidas para um nível acima.

  • Com o Transform, também podemos reutilizar a mesma declaração do ViewModel que vimos antes.

    class FocusSessionManager(
        taskRepository: TaskRepository,
        scope: CoroutineScope
    ) {
        val sessionFlow: StateFlow<FocusSession> = ...
        val summary: State<FocusSummary> =
            transform<FocusSummary>(
               initialValue = FocusSummary.Idle,
    scope = scope,
    container = handle.getOrCreateContainer("transform")
            ) {
                val session by sessionFlow.collectAsState()
                if (session.phase == FocusPhase.Idle)
                    return@transform FocusSummary.Idle
                val tasks by taskRepository.tasksFlow
                    .collectAsState(emptyList())
                val task = tasks.find {
                    it.id == session.activeTaskId
                }
                FocusSummary.Active(...)
            }
    }
  • A única coisa que mudou é que, agora, ele está no escopo do aplicativo.

  • Em seguida, temos o ViewModel do minitimer.

    class MiniTimerViewModel(
        focusManager: FocusSessionManager
    ) : ViewModel() {
        val isExpanded = MutableStateFlow(false)
    
        val uiState: StateFlow<MiniTimerUiState> =
            combine(
                focusManager.summary,
                isExpanded
            ) { summary, expanded ->
                MiniTimerUiState(
        summary = summary,
        showControls = expanded && summary is FocusSummary.Active
    )
            }.stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = MiniTimerUiState()
    )
    
        fun toggleExpanded() { isExpanded.update { !it } }
    }
  • Digamos que queremos nos desafiar um pouco aqui também.

  • Digamos que, no minitimer, além dos dois fluxos de dados que já tínhamos, o StateFlow e o fluxo do Room, também queremos monitorar se a barra do timer está aberta ou não.

  • Para isso, usamos um MutableStateFlow.

  • Para isso, vamos usar outro combine para rastrear o fluxo de resumo e o StateFlow de isExpanded.

  • E temos um segundo estado.

  • Com o Transform, não precisamos usar nenhum desses operadores encadeados novamente para sincronizar os fluxos de dados.

    class MiniTimerViewModel(
        focusManager: FocusSessionManager
    ) : ViewModel() {
        val isExpanded = MutableStateFlow(false)
    
        val uiState: State<MiniTimerUiState> =
            transform(
                initialValue = MiniTimerUiState(),
                scope = viewModelScope,
                       container = handle.getOrCreateContainer("transform")
            ) {
                val summary by focusManager.summary
                       val expanded by isExpanded.collectAsState()
                MiniTimerUiState(
        summary = summary,
        showControls = expanded && summary is FocusSummary.Active
    )
            }
    
        fun toggleExpanded() { isExpanded.update { !it } }
    }
  • O que podemos fazer, e isso é muito legal, são transformações aninhadas.

  • Aqui, o ViewModel lê o resumo, que é a saída da transformação upstream, como qualquer outro estado, e coleta isExpanded com collectAsState.

  • Assim, podemos misturar um estado upstream e um fluxo downstream no StateFlow no mesmo bloco.

  • E lembre-se da granularidade.

  • Portanto, se você alternar isExpanded, ele só vai executar novamente esse bloco.

  • O bloqueio no nível do app e o widget nem precisam perceber.

  • Por fim, temos o widget.

    class FocusWidget : GlanceAppWidget() {
        override suspend fun provideGlance(
            context: Context,
            id: GlanceId
        ) {
            val focusManager = ...
            provideContent {
                val summary by focusManager.summary
                    .collectAsState()
                GlanceTheme { FocusWidgetContent(summary) }
            }
        }
    }
  • Como o Resumo é o Compose, podemos coletar o StateFlow com collectAsState dentro de um provideContent como qualquer outro elemento combinável.

  • Com o Transform, isso é ainda mais simples.

    class FocusWidget : GlanceAppWidget() {
        override suspend fun provideGlance(
            context: Context,
            id: GlanceId
        ) {
            val focusManager = ...
            provideContent {
                val summary by focusManager.summary
                GlanceTheme { FocusWidgetContent(summary) }
            }
        }
    }
  • O gerenciador de foco está fornecendo um estado do Compose, então não precisamos converter nada aqui.

  • Por fim, a barra do minitimer e o widget da tela inicial nunca podem discordar porque estão lendo o mesmo estado.

  • Além disso, qualquer mudança feita nesse estado será refletida em todos os consumidores dele.

  • O que isso significa?

  • Então, se você pausar o widget aqui, ele vai passar pelo gerenciador para o fluxo da sessão, voltar pelo resumo, e as três telas serão atualizadas em sincronia.

  • Agora que já vimos um pouco sobre como usar o Transform, vamos fazer uma investigação nos bastidores.

  • A API Transform que vimos anteriormente é, na verdade, uma função auxiliar usada para criar uma transformação gerenciada básica.

    public fun <R> transform(
        initialValue: R,
        scope: CoroutineScope,
        onUpdate: @Composable (R) -> R,
    ): State<R> {
            val transformScope = TransformScope()
        val transform = transformScope
    .launchTransform(initialValue, onUpdate)
        scope.launch(
    dispatcher,
    start = CoroutineStart.UNDISPATCHED
     ) {
            try { transformScope.run() } finally {
                transform.dispose()
            }
        }
        return transform
    }
  • Na verdade, ele usa uma função de extensão TransformScope e launchTransform, que inicia uma transformação combinável nesse TransformScope e retorna um estado descartável.

  • Ao ler de cima para baixo, você vai perceber que ele faz tudo por você.

  • Ele cria o TransformScope e inicia a transformação nele, que será o estado que você vai receber de volta.

  • Em seguida, ele também inicia a execução no TransformScope no dispatcher que você forneceu, para que o ciclo de vida do escopo e a linha de execução pertençam a você.

  • E, por fim, o finally descarta a transformação quando o escopo termina.

  • Essa é a história de todo o ciclo de vida da transformação.

  • Vamos dar uma olhada rápida na interface do TransformScope.

    public fun TransformScope(context: CoroutineContext = Dispatchers.Default): TransformScope
    
    public abstract class TransformScope {
            public fun <T> createTransform(initialState: T, onUpdate: @Composable (T) -> T): DisposableState<T>
    public abstract suspend fun run()
    }
    
    public fun <T> TransformScope.launchTransform(
    initialState: T,
    onUpdate: @Composable (T) -> T,
    ): DisposableState<T>
    
    public interface DisposableState<T> : State<T> {
    public fun dispose()
    }
  • Podemos ver que ela tem uma função createTransform, que vai criar uma transformação anexada a esse TransformScope.

  • Ele também tem uma função de execução de suspensão, que inicia o TransformScope e o processo de atualização das transformações de estado atuais.

  • O que isso significa?

  • Portanto, run configura o Recomposer e a composição.

  • E cada Transform no escopo reside dentro dessa composição e tem o próprio escopo de recomposição.

  • É por isso que queremos compartilhar o escopo e por que temos TransformScope.

  • Assim, sempre que um estado muda, apenas as transformações que o leem são executadas novamente.

  • Como já mencionamos, run é uma função de suspensão.

    override suspend fun run() {
            val context = currentCoroutineContext()
            val clock = context[MonotonicFrameClock] ?: HeadlessTransformClock()
            ...
            val recomposer = Recomposer(context)
            val composition = Composition(UnitApplier, recomposer)
            try {
                withContext(clock) {
                    if (clock is HeadlessTransformClock) {
                        launch(start = CoroutineStart.UNDISPATCHED) {
                            clock.runClock()
                        }
                    }
                    ...
                }
            } finally {
                // dispose of composition, cancel the clock, close the recomposer
            }
        }
  • Portanto, ele é executado em qualquer corrotina de onde é chamado até que ela seja cancelada.

  • Agora, você pode estar se perguntando: um Recomposer precisa de um relógio de frame para saber quando produzir um frame.

  • Na interface do Compose, essa é a tela, mas, como mencionamos antes, estamos em um ambiente headless, então não há interface.

  • Portanto, a menos que você forneça um relógio, vamos criar o nosso.

  • Ao separar a recomposição do relógio do frame de exibição, o Transform executa todas as transformações e derivações de estado fora da linha de execução de interface.

  • Para permitir isso, criamos um HeadlessTransformClock dedicado.

  • Transformar o relógio de frame.

  • Como um widget, um serviço ou outro downstream de Transform pode estar esperando um valor de um Transform, nosso relógio é adiantado.

  • Isso significa que, no momento em que qualquer estado que um Transform está consumindo muda, um novo frame é produzido imediatamente.

  • É uma pequena máquina de estado sem bloqueio, então solicitar um frame é uma operação simples de comparação e definição.

  • E quando nada muda no estado, o relógio para.

  • Portanto, uma transformação que consome um estado que não foi alterado não custa nada.

  • Antes de passar a palavra de volta para o Ben, vale lembrar que o Transform agora é compatível com rememberSaveable para garantir que seus dados sejam salvos em todas as mudanças de configuração.

  • Isso é feito pela nova API SavedStateContainer, que agora é o elemento básico em que o SaveableStateRegistry, o SavedStateHandle e o SavedStateRegistry do Compose são criados.

  • Você pode transformar qualquer um deles em um SavedStateContainer ou criar contêineres filhos e transmiti-los para o Transform para garantir que os dados sejam salvos.

  • Por fim, vale lembrar que todas as APIs que estamos discutindo hoje estão na versão Alfa, então estão sujeitas a mudanças.

  • Agora, vou passar a palavra de volta para o Ben.

Estado do Compose fora da interface

  • Certo.

  • Certo, obrigada, Elif.

  • Agora que vimos como o estado do Compose é poderoso e como o Transform permite que ele seja derivado fora da interface, talvez você tenha notado que, no exemplo até agora, as entradas eram fluxos que conectamos usando collectAsState.

  • Mas será que isso é sempre necessário?

  • Por que não usar o estado do Compose diretamente nos detentores de estado?

  • Para responder a essa pergunta, é útil pensar no estado em três categorias amplas.

  • O primeiro é o estado ativo, que é armazenado apenas na memória.

  • Por exemplo, o status da conexão de rede ou Bluetooth.

  • Não é recomendável usar um valor persistente.

  • O segundo é o estado temporário, que precisa sobreviver ao encerramento do processo, mas não precisa ser armazenado indefinidamente.

  • Pense em coisas como o estado da interface, por exemplo, se um card está expandido ou não, e o estado da sessão, como a backstack de navegação ou um rascunho de formulário.

  • E o terceiro é o estado permanente, que é armazenado indefinidamente.

  • Pense em preferências do usuário ou uma biblioteca de mídia em um banco de dados.

  • Vamos começar com o uso do estado do Compose para estados ativos e temporários.

  • Até agora, nossa orientação de arquitetura mantinha o estado do Compose na camada de interface ou, às vezes, nos ViewModels, e dissemos para você usar o Flow em todos os outros lugares.

  • Mas por quê?

  • Isso acontece porque, assim que você precisou combinar uma propriedade de estado com um fluxo, foi necessário convertê-la de volta usando snapshotFlow.

  • Mas com o Transform, essa conversão não é necessária.

  • Vamos ver o que isso significa, começando em um ViewModel.

    @HiltViewModel
    class TaskListViewModel @Inject constructor(
        private val tasksRepository: TasksRepository,
        private val networkMonitor: NetworkMonitor,
        savedStateHandle: SavedStateHandle,
    ) : ViewModel() {
        ...
    }
  • Para entradas de tela locais, como selectedTaskIds, é possível declarar a propriedade mutableStateOf simples em vez de MutableStateFlow.

    @HiltViewModel
    class TaskListViewModel @Inject constructor(...) : ViewModel() {
        var selectedTaskIds by mutableStateOf(emptySet<String>())
            private set
    }
  • Como isso não é algo que você necessariamente quer manter após o encerramento do processo, não há problema em usar apenas mutableStateOf.

  • Para dados que você quer que sobrevivam ao encerramento do processo, como a consulta de pesquisa, é possível fazer backup do estado mutável diretamente com savedStateHandle.saveable.

    @HiltViewModel
    class TaskListViewModel @Inject constructor(...) : ViewModel() {
        var selectedTaskIds by mutableStateOf(emptySet<String>())
            private set
    
        var searchQuery by savedStateHandle.saveable { mutableStateOf("") }
            private set
    }
  • E, assim como updateScore no exemplo de jogo anterior, quando um método atualiza vários valores de estado de uma só vez, envolver as gravações em withMutableSnapshot garante a consistência.

    @HiltViewModel
    class TaskListViewModel @Inject constructor(...) : ViewModel() {
        var selectedTaskIds by mutableStateOf(emptySet<String>())
            private set
    
        var searchQuery by savedStateHandle.saveable { mutableStateOf("") }
            private set
    
        fun updateSearchQuery(query: String) {
            Snapshot.withMutableSnapshot {
                searchQuery = query
                selectedTaskIds = emptySet()
            }
        }
    }
  • Em seguida, para combinar searchQuery com um fluxo do Room, basta ler diretamente dentro da transformação, enquanto coleta o fluxo.

    val tasksState: State<TaskListState> = transform(
        initialValue = TaskListState.Loading,
        scope = viewModelScope,
    ) {
        val tasks = remember(searchQuery) {
            tasksRepository.getTasks(searchQuery)
        }.collectAsState(initial = null).value
            ?: return@transform TaskListState.Loading
    
        TaskListState.Success(tasks = tasks)
    }
  • E quanto ao estado em tempo real, como a conectividade de rede?

    @Singleton
    class NetworkMonitor @Inject constructor(
        @ApplicationContext context: Context,
    ) {
        private val connectivityManager =
            context.getSystemService(ConnectivityManager::class.java)
        ...
    }
  • Isso geralmente fica em um Singleton, porque é lido em várias telas ou até mesmo por vários componentes do app, como serviços.

  • Mas isso não significa que você não pode usar o estado do Compose para armazená-lo.

  • Na verdade, geralmente é mais simples do que uma versão baseada em fluxo.

  • Não é necessário um fluxo de callback ou algo para fazer a adaptação. Basta atualizar o valor do ponto diretamente nos callbacks.

    @Singleton
    class NetworkMonitor @Inject constructor(...) {
        ...
        private val _isOnline = mutableStateOf(checkCurrentConnectivity())
        val isOnline: State<Boolean> = _isOnline
    }
  • E, se um consumidor ainda precisar de um fluxo, use snapshotFlow para fazer a ponte de volta.

    @Singleton
    class NetworkMonitor @Inject constructor(...) {
        ...
        private val _isOnline = mutableStateOf(checkCurrentConnectivity())
        val isOnline: State<Boolean> = _isOnline
    
        private val callback = object : ConnectivityManager.NetworkCallback() {
            override fun onAvailable(network: Network) { _isOnline.value = true }
            override fun onLost(network: Network) { _isOnline.value = false }
        }
    
        init {
            connectivityManager.registerDefaultNetworkCallback(callback)
        }
    }
  • Vamos ver como isso fica no ViewModel.

  • Você pode ler isOnline diretamente em uma transformação, e ela será recomposta quando o valor mudar.

    val tasksState: State<TaskListState> = transform(
        initialValue = TaskListState.Loading,
        scope = viewModelScope,
    ) {
        if (!networkMonitor.isOnline.value) {
            return@transform TaskListState.Offline
        }
    
        val tasks = remember(searchQuery) {
            tasksRepository.getTasks(searchQuery)
        }.collectAsState(initial = null).value
            ?: return@transform TaskListState.Loading
    
        TaskListState.Success(tasks = tasks)
    }
  • E tudo isso acontece diretamente, sem escopo de aplicativo ou de corrotina que você precise fornecer para atualizar o estado. É apenas uma atualização direta e síncrona.

  • Assim, com o Transform, seu ViewModel pode combinar o estado de gerenciadores Singleton, o estado em um ViewModel e os fluxos de repositórios, tudo em um único bloco de transformação.

  • Agora, vamos voltar ao estado temporário.

  • Anteriormente, mencionei dois tipos: estado da interface e estado da sessão.

  • Para o estado da interface e até mesmo o estado da sessão que reside em uma única atividade, ViewModel e SavedState funcionam muito bem.

  • Eles foram criados exatamente para isso.

  • No entanto, as coisas começam a dar errado quando esse estado de sessão precisa ser compartilhado entre várias interfaces e componentes de app diferentes.

  • Quando esse estado sai de um ViewModel, nossas APIs e orientações atuais deixam uma lacuna.

  • Primeiro, as APIs SavedState salvam em um Bundle de uma atividade, mas isso não está disponível em outros componentes do app, como um serviço.

  • Em segundo lugar, mutableStateOf em um Singleton, que só existe na memória, então não sobrevive ao encerramento do processo.

Introdução ao StateStore

  • E essa é a lacuna que o Jetpack AppState, StateStore, preenche.

  • É um mapa de segurança de tipos do estado do Compose que pode ser compartilhado entre componentes do app e apoiado por DataStore para sobreviver ao encerramento do processo.

  • Você pode conectar DataStore a cada Singleton, mas isso significa escrever um código boilerplate de salvamento e restauração para cada propriedade.

  • Com a integração do StateStore com o DataStore, você conecta o DataStore uma vez e, em seguida, ativa cada parte do estado que quer manter.

  • Vamos criar isso no nosso app de timer de foco.

  • Vamos começar na memória e, em seguida, adicionar a persistência do DataStore.

  • Para preparar o cenário, estamos modelando um FocusSession como uma interface selada com os estados inativo, em execução e pausado.

    sealed interface FocusSession {
        data object Idle : FocusSession
        data class Running(val taskId: String, val endTimeMillis: Long) : FocusSession
        data class Paused(val taskId: String, val remainingMillis: Long) : FocusSession
    }
  • Em seguida, para armazenar esse estado em StateStore, definimos um StateStoreKey com o tipo FocusSession e um valor padrão, idle.

    sealed interface FocusSession { ... }
    
    object FocusSessionKey :
        StateStoreKey<FocusSession>(defaultValue = FocusSession.Idle)
  • Em seguida, fornecemos um Singleton StateStore no Hilt que pode ser injetado em diferentes componentes, como o Focus Session Manager.

    @Module
    @InstallIn(SingletonComponent::class)
    object AppModule {
        @Provides
        @Singleton
        fun provideStateStore(): StateStore = StateStore()
    }
    
    @Singleton
    class FocusSessionManager @Inject constructor(
        private val stateStore: StateStore,
    ) {
        ...
    }
  • Isso é semelhante à forma como o ViewModel injeta SavedStateHandle.

  • Ok.

  • Agora temos um StateStore, mas como usá-lo?

  • Ainda bem que é uma API pequena.

  • Ela tem apenas três métodos, então vamos analisar um por um.

  • O primeiro é setState, que grava um novo valor para uma chave.

    fun startFocus(taskId: String, durationMinutes: Int = 25) {
        val endTime = System.currentTimeMillis() + durationMinutes * 60_000L
        stateStore.setState(
            FocusSessionKey,
            FocusSession.Running(taskId = taskId, endTimeMillis = endTime),
        )
    }
    
    fun endFocus() {
        stateStore.setState(FocusSessionKey, FocusSession.Idle)
    }
  • Quando uma sessão começa, ela é definida como "em execução" e, quando termina, como "ociosa".

  • É isso.

  • Em segundo lugar, ainda temos updateState, que permite calcular um novo valor com base no valor atual, como calcular o tempo restante ao alternar entre execução e pausa.

    fun pauseFocus(nowMillis: Long = System.currentTimeMillis()) {
        stateStore.updateState(FocusSessionKey) { session ->
            if (session !is FocusSession.Running) return@updateState session
            FocusSession.Paused(session.taskId, session.endTimeMillis - nowMillis)
        }
    }
    
    fun resumeFocus(nowMillis: Long = System.currentTimeMillis()) {
        stateStore.updateState(FocusSessionKey) { session ->
            if (session !is FocusSession.Paused) return@updateState session
            FocusSession.Running(session.taskId, nowMillis + session.remainingMillis)
        }
    }
  • Por fim, há o getState, que retorna um estado somente leitura para uma chave e tem a opção de substituir o valor padrão, caso você queira mudar isso em um site de chamada específico.

    @Singleton
    class FocusSessionManager @Inject constructor(
        private val stateStore: StateStore,
    ) {
        val session: State<FocusSession> = stateStore.getState(FocusSessionKey)
        ...
    }
  • Além disso, expor o estado FocusSession nesse gerenciador significa que os consumidores nunca precisam acessar StateStore ou as chaves diretamente.

  • É possível manter o encapsulamento de gravação e leitura.

  • Em vez de consumidores, eles injetam o Focus Session Manager, que provavelmente já têm porque precisam controlar a sessão, e podem ler o estado.

    @HiltViewModel
    class MiniTimerViewModel @Inject constructor(
        focusSessionManager: FocusSessionManager,
    ) : ViewModel() { ... }
    
    @EntryPoint
    @InstallIn(SingletonComponent::class)
    interface WidgetEntryPoint {
        fun focusSessionManager(): FocusSessionManager
    }
  • E esses leitores podem reexpor o State diretamente ou usar o Transform para criar um novo State atualizado, como vimos no exemplo do Mini-Timer ViewModel.

    @HiltViewModel
    class MiniTimerViewModel @Inject constructor(
        focusSessionManager: FocusSessionManager,
    ) : ViewModel() {
        val session: State<FocusSession> = focusSessionManager.session
    }
  • Em seguida, os elementos combináveis da interface podem ler esse estado como qualquer outro estado do Compose e fazer a recomposição quando necessário.

    @HiltViewModel
    class MiniTimerViewModel @Inject constructor(
        focusSessionManager: FocusSessionManager,
    ) : ViewModel() {
        val session: State<FocusSession> = focusSessionManager.session
    }
    
    @Composable
    fun MiniTimerBar(viewModel: MiniTimerViewModel = hiltViewModel()) {
        val session by viewModel.session
        if (session !is FocusSession.Idle) {
            TimerReadout(session = session)
        }
    }
  • Mas, até agora, o FocusSession só existe na memória.

  • Vamos ver o que é necessário para sobreviver ao encerramento do processo.

  • Primeiro, precisamos fazer uma configuração única, iniciando o syncToDataStore ao criar o repositório.

    @Module
    @InstallIn(SingletonComponent::class)
    object AppModule {
        @Provides
        @Singleton
        fun provideStateStore(
            @ApplicationContext context: Context,
            @ApplicationScope scope: CoroutineScope,
        ): StateStore = StateStore().apply {
            val path = context.dataStoreFile("app_state.json").absolutePath
            scope.launch { syncToDataStore(path = path, coroutineScope = scope) }
        }
    }
  • Depois, ativamos cada chave que queremos persistir.

    @Serializable
    sealed interface FocusSession {
        @Serializable data object Idle : FocusSession
        @Serializable data class Running(val taskId: String, val endTimeMillis: Long) : FocusSession
        @Serializable data class Paused(val taskId: String, val remainingMillis: Long) : FocusSession
    }
    
    object FocusSessionKey :
        StateStoreKey<FocusSession>(defaultValue = FocusSession.Idle),
        PersistToDataStore
  • Basta garantir que o tipo seja serializável e que a chave implemente a interface PersistToDataStore.

  • Você pode estar pensando: "Parece fácil".

  • Então, o que ele realmente faz para persistir a chave?

  • Por isso, nos bastidores, syncToDataStore usa a API de listener do Transform, que é basicamente a mesma coisa que o Transform, mas não retorna nada, sendo usada apenas para observação e efeitos colaterais.

    suspend fun StateStore.syncToDataStore(dataStore: DataStore<StateStorePreferences>) {
        listener { … }
    }
  • Em seguida, dentro desse listener, ele lê cada chave no armazenamento e verifica se uma delas é uma chave PersistToDataStore.

    suspend fun StateStore.syncToDataStore(dataStore: DataStore<StateStorePreferences>) {
        listener {
            for (key in keys) {
                if (key is PersistToDataStore) {
                    val value = getState(key).value
    
                    LaunchedEffect(key) {
                        // Restore saved value from dataStore on first access
                    }
    
                    LaunchedEffect(key, value) {
                        // Flush updated value to dataStore on change
                    }
                }
            }
        }
    }
  • Quando isso acontece, ele usa LaunchedEffect para sincronizar esses valores de volta com DataStore sempre que eles mudam.

  • E o que quero destacar aqui é que ter um contêiner padrão como o StateStore é o que nos permite fornecer essas integrações de substituição.

  • DataStore é apenas o primeiro que estamos analisando.

Resumo das categorias de estado do Compose

  • Voltando às categorias de estado, agora temos uma ideia bem clara de como usar o estado do Compose em todo o aplicativo.

  • De mutableStateOf para o estado ativo, às APIs SavedState para o estado de apresentação local da tela e StateStore para o estado de sessão entre componentes. Por fim, temos collectAsState para interoperabilidade com fluxos.

  • Mas, na verdade, a interoperabilidade não se limita a uma única categoria. Você pode usar esses auxiliares, como collectAsState, sempre que precisar conectar tipos observáveis ao estado do Compose.

  • Mas para o estado permanente, outra coisa que estamos analisando são maneiras de trabalhar com o Compose ou para que o DataStore e o Room exponham diretamente o estado do Compose, para que você não precise escrever sua própria sintaxe mais agradável para fazer isso.

  • E, com isso, tenho o prazer de anunciar que o Transform, o StateStore e a integração com o DataStore estão disponíveis em Alfa hoje. Também publicamos toda a documentação em developer.android.com e temos um repositório de receitas do AppState no GitHub que mostra exemplos de uso do StateStore e do Transform.

    [versions]
    appstate = "1.0.0-alpha01"
    ...
    
    [libraries]
    ...
    transform = { module = "androidx.appstate.transform:transform", version.ref = "appstate" }
    transform-testing = { module = "androidx.appstate.transform:transform-testing", version.ref = "appstate" }
    statestore = { module = "androidx.appstate.statestore:statestore", version.ref = "appstate" }
    statestore-datastore = { module = "androidx.appstate.statestore:statestore-datastore", version.ref = "appstate" }
  • E, sim, teste-os no seu app.

  • Envie seu feedback.

  • Como estamos na fase Alfa, é o momento de iterar e fazer mudanças para garantir que elas funcionem nos seus casos de uso.

Código

Snippets de código

Snippet do Kotlin em 01:56 em "O que é o estado do Compose?"
/**
 * A value holder where reads to the [value] property during the execution of a [Composable]
 * function, the current [RecomposeScope] will be subscribed to changes of that value.
 */
@Stable
public interface State<out T> {
    public val value: T
}
Snippet do Kotlin em 02:00 em "O que é o estado do Compose?"
/**
 * A mutable value holder where reads to the [value] property during the execution of a [Composable]
 * function, the current [RecomposeScope] will be subscribed to changes of that value. When the
 * [value] property is written to and changed, a recomposition of any subscribed [RecomposeScope]s
 * will be scheduled. If [value] is written to with the same value, no recompositions will be
 * scheduled.
 */
@Stable
public interface MutableState<T> : State<T> {
    override var value: T
    public operator fun component1(): T
    public operator fun component2(): (T) -> Unit
}
Snippet do Kotlin em 02:23 em "O que é o estado do Compose?"
@Composable
fun Counter() {
    val count = remember { mutableStateOf(0) }
}
Snippet do Kotlin em 02:32 em "O que é o estado do Compose?"
@Composable
fun Counter() {
    val count = remember { mutableStateOf(0) }
    Column {
        Text("Count: ${count.value}")
        AddButton(onClick = { count.value++ })
    }
}
Snippet do Kotlin em 02:52 em "O que é o estado do Compose?"
@Composable
fun Counter() {
    val count = remember { mutableStateOf(0) }
    Column {
        Text("Count: ${count.value}")
        AddButton(onClick = { count.value++ })
    }
}

@Composable
fun AddButton(onClick: () -> Unit) {
    Button(onClick = onClick) {
        Text("Add")
    }
}
Snippet do Kotlin em 03:08 em "O que é o estado do Compose?"
@Composable
fun Counter() {
    val isVisible = remember { mutableStateOf(true) }
    val count = remember { mutableStateOf(0) }
    Column {
        if (isVisible.value) {
            Text("Count: ${count.value}")
        } else {
            Text("Hidden")
        }
        AddButton(onClick = { count.value++ })
    }
}
Snippet do Kotlin em 03:26 em "O que é o estado do Compose?"
@Composable
fun Counter() {
    val isVisible = remember { mutableStateOf(false) }
    val count = remember { mutableStateOf(0) }
    Column {
        if (isVisible.value) {
            Text("Count: ${count.value}")
        } else {
            Text("Hidden")
        }
        AddButton(onClick = { count.value++ })
    }
}
Snippet do Kotlin em 04:15 em "O que é o estado do Compose?"
val levelName: String = "Level 1 - Stay Alive!"
val timeSurvived: Int = 0
val score: Double = 0.0
Snippet do Kotlin em 04:23 em "O que é o estado do Compose?"
data class GameUiState(
    val levelName: String = "Level 1 - Stay Alive!",
    val timeSurvived: Int = 0,
    val score: Double = 0.0
)
Snippet do Kotlin em 04:33 em "O que é o estado do Compose?"
data class GameUiState(
    val levelName: String = "Level 1 - Stay Alive!",
    val timeSurvived: Int = 0,
    val score: Double = 0.0
)

class GameViewModel : ViewModel() {
    val uiState = MutableStateFlow(GameUiState())
    fun updateScore() = uiState.update {
        it.copy(
            timeSurvived = it.timeSurvived + 1,
            score = it.score + 10 + .01 * it.timeSurvived
        )
    }
}
Snippet do Kotlin em 04:42 em "O que é o estado do Compose?"
data class GameUiState(
    val levelName: String = "Level 1 - Stay Alive!",
    val timeSurvived: Int = 0,
    val score: Double = 0.0
)

class GameViewModel : ViewModel() {
    val uiState = MutableStateFlow(GameUiState())
    fun updateScore() = uiState.update {
        it.copy(
            timeSurvived = it.timeSurvived + 1,
            score = it.score + 10 + .01 * it.timeSurvived
        )
    }
}

@Composable
fun GameScreen(viewModel: GameViewModel = viewModel()) {
    val state by viewModel.uiState.collectAsStateWithLifecycle()
    Column {
        Header(levelName = state.levelName)
        ScoreBadge(
            timeSurvived = state.timeSurvived,
            score = state.score
        )
    }
}
Snippet do Kotlin em 05:09 em "O que é o estado do Compose?"
@Stable
class GameUiState {
    var levelName by mutableStateOf("Level 1 - Stay Alive!")
    var timeSurvived by mutableStateOf(0)
    var score by mutableStateOf(0.0)
}
Snippet do Kotlin em 05:21 em "O que é o estado do Compose?"
@Stable
class GameUiState {
    var levelName by mutableStateOf("Level 1 - Stay Alive!")
    var timeSurvived by mutableStateOf(0)
    var score by mutableStateOf(0.0)
}

class GameViewModel : ViewModel() {
    val uiState = GameUiState()
    fun updateScore() = Snapshot.withMutableSnapshot {
        uiState.timeSurvived += 1
        uiState.score += 10 + .01 * uiState.timeSurvived
    }
}
Snippet do Kotlin em 05:30 em "O que é o estado do Compose?"
@Stable
class GameUiState {
    var levelName by mutableStateOf("Level 1 - Stay Alive!")
    var timeSurvived by mutableStateOf(0)
    var score by mutableStateOf(0.0)
}

class GameViewModel : ViewModel() {
    val uiState = GameUiState()
    fun updateScore() = Snapshot.withMutableSnapshot {
        uiState.timeSurvived += 1
        uiState.score += 10 + .01 * uiState.timeSurvived
    }
}

@Composable
fun GameScreen(viewModel: GameViewModel = viewModel()) {
    val state = viewModel.uiState
    Column {
        Header(state = state)
        ScoreBadge(state = state)
    }
}
Snippet do Kotlin em 07:35 em "Introducing Transform"
public fun <R> transform(
    initialValue: R,
    scope: CoroutineScope,
    onUpdate: @Composable (R) -> R,
): State<R>
Snippet do Kotlin em 09:20 em "Introducing Transform"
class FocusScreenViewModel(
    focusManager: FocusSessionManager,
    taskRepository: TaskRepository
) : ViewModel() {
    val summary: StateFlow<FocusSummary> =
        combine(focusManager.sessionFlow, taskRepository.tasksFlow) { session, tasks ->
            if (session.phase == FocusPhase.Idle) {
                FocusSummary.Idle
            } else {
                val task = tasks.find { it.id == session.activeTaskId }
                FocusSummary.Active(...)
            }
        }.stateIn(
                scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = FocusSummary.Idle,
)
}
Snippet do Kotlin em 09:37 em "Introducing Transform"
class FocusScreenViewModel(
    focusManager: FocusSessionManager,
    taskRepository: TaskRepository
) : ViewModel() {
    val summary: State<FocusSummary> =
        transform<FocusSummary>(
initialValue = FocusSummary.Idle,
scope = viewModelScope,
container = handle.getOrCreateContainer("transform")
) {
            val session by focusManager.sessionFlow.collectAsState()
            if (session.phase == FocusPhase.Idle) return@transform FocusSummary.Idle
            val tasks by taskRepository.tasksFlow.collectAsState(emptyList())
            val task = tasks.find { it.id == session.activeTaskId }
            FocusSummary.Active(...)
Snippet do Kotlin em 10:01 em "Introducing Transform"
class FocusScreenViewModel(
    focusManager: FocusSessionManager,
    taskRepository: TaskRepository
) : ViewModel() {
    val summary: State<FocusSummary> =
        transform<FocusSummary>(
            initialValue = FocusSummary.Idle,
            scope = viewModelScope,
                   container = handle.getOrCreateContainer("transform")
        ) {
            val session by focusManager.sessionFlow.collectAsState()
            if (session.phase == FocusPhase.Idle)
                return@transform FocusSummary.Idle
            val tasks by taskRepository.tasksFlow.collectAsState(emptyList())
            val task = tasks.find {
                it.id == session.activeTaskId
            }
                     FocusSummary.Active(...)
        }
}
Snippet do Kotlin às 11:39 em "Transformações aninhadas"
class FocusSessionManager(taskRepository: TaskRepository, scope: CoroutineScope) {
    ...
}

class MiniTimerViewModel(focusManager: FocusSessionManager) : ViewModel() {
    ...
}

class FocusWidget : GlanceAppWidget() {
    override suspend fun provideGlance(context: Context, id: GlanceId) {
        provideContent {
            ...
        }
    }
}
Snippet do Kotlin às 12:03 em "Transformações aninhadas"
class FocusSessionManager(
    taskRepository: TaskRepository,
    scope: CoroutineScope
) {
    val sessionFlow: StateFlow<FocusSession> = ...
    val summary: StateFlow<FocusSummary> =
        combine(
            sessionFlow,
            taskRepository.tasksFlow
        ) { session, tasks ->
            if (session.phase == FocusPhase.Idle) {
                FocusSummary.Idle
            } else {
                val task = tasks.find {
                    it.id == session.activeTaskId
                }
                FocusSummary.Active(...)
            }
        }.stateIn(
scope = appScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = FocusSummary.Idle
)
}
Snippet do Kotlin às 12:17 em "Transformações aninhadas"
class FocusSessionManager(
    taskRepository: TaskRepository,
    scope: CoroutineScope
) {
    val sessionFlow: StateFlow<FocusSession> = ...
    val summary: State<FocusSummary> =
        transform<FocusSummary>(
           initialValue = FocusSummary.Idle,
scope = scope,
container = handle.getOrCreateContainer("transform")
        ) {
            val session by sessionFlow.collectAsState()
            if (session.phase == FocusPhase.Idle)
                return@transform FocusSummary.Idle
            val tasks by taskRepository.tasksFlow
                .collectAsState(emptyList())
            val task = tasks.find {
                it.id == session.activeTaskId
            }
            FocusSummary.Active(...)
        }
}
Snippet do Kotlin às 12:28 em "Transformações aninhadas"
class MiniTimerViewModel(
    focusManager: FocusSessionManager
) : ViewModel() {
    val isExpanded = MutableStateFlow(false)

    val uiState: StateFlow<MiniTimerUiState> =
        combine(
            focusManager.summary,
            isExpanded
        ) { summary, expanded ->
            MiniTimerUiState(
    summary = summary,
    showControls = expanded && summary is FocusSummary.Active
)
        }.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = MiniTimerUiState()
)

    fun toggleExpanded() { isExpanded.update { !it } }
}
Snippet do Kotlin às 12:56 em "Nested transforms"
class MiniTimerViewModel(
    focusManager: FocusSessionManager
) : ViewModel() {
    val isExpanded = MutableStateFlow(false)

    val uiState: State<MiniTimerUiState> =
        transform(
            initialValue = MiniTimerUiState(),
            scope = viewModelScope,
                   container = handle.getOrCreateContainer("transform")
        ) {
            val summary by focusManager.summary
                   val expanded by isExpanded.collectAsState()
            MiniTimerUiState(
    summary = summary,
    showControls = expanded && summary is FocusSummary.Active
)
        }

    fun toggleExpanded() { isExpanded.update { !it } }
}
Snippet do Kotlin às 13:37 em "Transformações aninhadas"
class FocusWidget : GlanceAppWidget() {
    override suspend fun provideGlance(
        context: Context,
        id: GlanceId
    ) {
        val focusManager = ...
        provideContent {
            val summary by focusManager.summary
                .collectAsState()
            GlanceTheme { FocusWidgetContent(summary) }
        }
    }
}
Snippet do Kotlin às 13:47 em "Transformações aninhadas"
class FocusWidget : GlanceAppWidget() {
    override suspend fun provideGlance(
        context: Context,
        id: GlanceId
    ) {
        val focusManager = ...
        provideContent {
            val summary by focusManager.summary
            GlanceTheme { FocusWidgetContent(summary) }
        }
    }
}
Snippet do Kotlin às 14:33 em "Transformações aninhadas"
public fun <R> transform(
    initialValue: R,
    scope: CoroutineScope,
    onUpdate: @Composable (R) -> R,
): State<R> {
        val transformScope = TransformScope()
    val transform = transformScope
.launchTransform(initialValue, onUpdate)
    scope.launch(
dispatcher,
start = CoroutineStart.UNDISPATCHED
 ) {
        try { transformScope.run() } finally {
            transform.dispose()
        }
    }
    return transform
}
Snippet do Kotlin aos 15:18 em "Transformações aninhadas"
public fun TransformScope(context: CoroutineContext = Dispatchers.Default): TransformScope

public abstract class TransformScope {
        public fun <T> createTransform(initialState: T, onUpdate: @Composable (T) -> T): DisposableState<T>
public abstract suspend fun run()
}

public fun <T> TransformScope.launchTransform(
initialState: T,
onUpdate: @Composable (T) -> T,
): DisposableState<T>

public interface DisposableState<T> : State<T> {
public fun dispose()
}
Snippet do Kotlin em 16:02 em "Transformações aninhadas"
override suspend fun run() {
        val context = currentCoroutineContext()
        val clock = context[MonotonicFrameClock] ?: HeadlessTransformClock()
        ...
        val recomposer = Recomposer(context)
        val composition = Composition(UnitApplier, recomposer)
        try {
            withContext(clock) {
                if (clock is HeadlessTransformClock) {
                    launch(start = CoroutineStart.UNDISPATCHED) {
                        clock.runClock()
                    }
                }
                ...
            }
        } finally {
            // dispose of composition, cancel the clock, close the recomposer
        }
    }
Snippet do Kotlin em 19:12 em "Compose State outside of the UI"
@HiltViewModel
class TaskListViewModel @Inject constructor(
    private val tasksRepository: TasksRepository,
    private val networkMonitor: NetworkMonitor,
    savedStateHandle: SavedStateHandle,
) : ViewModel() {
    ...
}
Snippet do Kotlin aos 19:17 em "Estado do Compose fora da interface"
@HiltViewModel
class TaskListViewModel @Inject constructor(...) : ViewModel() {
    var selectedTaskIds by mutableStateOf(emptySet<String>())
        private set
}
Snippet do Kotlin aos 19:33 em "Estado do Compose fora da interface"
@HiltViewModel
class TaskListViewModel @Inject constructor(...) : ViewModel() {
    var selectedTaskIds by mutableStateOf(emptySet<String>())
        private set

    var searchQuery by savedStateHandle.saveable { mutableStateOf("") }
        private set
}
Snippet do Kotlin às 19:41 em "Estado do Compose fora da interface"
@HiltViewModel
class TaskListViewModel @Inject constructor(...) : ViewModel() {
    var selectedTaskIds by mutableStateOf(emptySet<String>())
        private set

    var searchQuery by savedStateHandle.saveable { mutableStateOf("") }
        private set

    fun updateSearchQuery(query: String) {
        Snapshot.withMutableSnapshot {
            searchQuery = query
            selectedTaskIds = emptySet()
        }
    }
}
Snippet do Kotlin aos 19:52 em "Estado do Compose fora da interface"
val tasksState: State<TaskListState> = transform(
    initialValue = TaskListState.Loading,
    scope = viewModelScope,
) {
    val tasks = remember(searchQuery) {
        tasksRepository.getTasks(searchQuery)
    }.collectAsState(initial = null).value
        ?: return@transform TaskListState.Loading

    TaskListState.Success(tasks = tasks)
}
Snippet do Kotlin aos 20:00 em "Estado do Compose fora da interface"
@Singleton
class NetworkMonitor @Inject constructor(
    @ApplicationContext context: Context,
) {
    private val connectivityManager =
        context.getSystemService(ConnectivityManager::class.java)
    ...
}
Snippet do Kotlin aos 20:17 em "Estado do Compose fora da interface"
@Singleton
class NetworkMonitor @Inject constructor(...) {
    ...
    private val _isOnline = mutableStateOf(checkCurrentConnectivity())
    val isOnline: State<Boolean> = _isOnline
}
Snippet do Kotlin em 20:23 em "Estado do Compose fora da interface"
@Singleton
class NetworkMonitor @Inject constructor(...) {
    ...
    private val _isOnline = mutableStateOf(checkCurrentConnectivity())
    val isOnline: State<Boolean> = _isOnline

    private val callback = object : ConnectivityManager.NetworkCallback() {
        override fun onAvailable(network: Network) { _isOnline.value = true }
        override fun onLost(network: Network) { _isOnline.value = false }
    }

    init {
        connectivityManager.registerDefaultNetworkCallback(callback)
    }
}
Snippet do Kotlin aos 20:32 em "Estado do Compose fora da interface"
val tasksState: State<TaskListState> = transform(
    initialValue = TaskListState.Loading,
    scope = viewModelScope,
) {
    if (!networkMonitor.isOnline.value) {
        return@transform TaskListState.Offline
    }

    val tasks = remember(searchQuery) {
        tasksRepository.getTasks(searchQuery)
    }.collectAsState(initial = null).value
        ?: return@transform TaskListState.Loading

    TaskListState.Success(tasks = tasks)
}
Snippet do Kotlin às 22:07 em "Introducing StateStore"
sealed interface FocusSession {
    data object Idle : FocusSession
    data class Running(val taskId: String, val endTimeMillis: Long) : FocusSession
    data class Paused(val taskId: String, val remainingMillis: Long) : FocusSession
}
Snippet do Kotlin aos 22:14 em "Introducing StateStore"
sealed interface FocusSession { ... }

object FocusSessionKey :
    StateStoreKey<FocusSession>(defaultValue = FocusSession.Idle)
Snippet do Kotlin aos 22:23 em "Introducing StateStore"
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
    @Provides
    @Singleton
    fun provideStateStore(): StateStore = StateStore()
}

@Singleton
class FocusSessionManager @Inject constructor(
    private val stateStore: StateStore,
) {
    ...
}
Snippet do Kotlin aos 22:42 em "Introducing StateStore"
fun startFocus(taskId: String, durationMinutes: Int = 25) {
    val endTime = System.currentTimeMillis() + durationMinutes * 60_000L
    stateStore.setState(
        FocusSessionKey,
        FocusSession.Running(taskId = taskId, endTimeMillis = endTime),
    )
}

fun endFocus() {
    stateStore.setState(FocusSessionKey, FocusSession.Idle)
}
Snippet do Kotlin aos 22:52 em "Introducing StateStore"
fun pauseFocus(nowMillis: Long = System.currentTimeMillis()) {
    stateStore.updateState(FocusSessionKey) { session ->
        if (session !is FocusSession.Running) return@updateState session
        FocusSession.Paused(session.taskId, session.endTimeMillis - nowMillis)
    }
}

fun resumeFocus(nowMillis: Long = System.currentTimeMillis()) {
    stateStore.updateState(FocusSessionKey) { session ->
        if (session !is FocusSession.Paused) return@updateState session
        FocusSession.Running(session.taskId, nowMillis + session.remainingMillis)
    }
}
Snippet do Kotlin aos 23:03 em "Introducing StateStore"
@Singleton
class FocusSessionManager @Inject constructor(
    private val stateStore: StateStore,
) {
    val session: State<FocusSession> = stateStore.getState(FocusSessionKey)
    ...
}
Snippet em Kotlin aos 23:22 em "Introdução ao StateStore"
@HiltViewModel
class MiniTimerViewModel @Inject constructor(
    focusSessionManager: FocusSessionManager,
) : ViewModel() { ... }

@EntryPoint
@InstallIn(SingletonComponent::class)
interface WidgetEntryPoint {
    fun focusSessionManager(): FocusSessionManager
}
Snippet do Kotlin aos 23:33 em "Introducing StateStore"
@HiltViewModel
class MiniTimerViewModel @Inject constructor(
    focusSessionManager: FocusSessionManager,
) : ViewModel() {
    val session: State<FocusSession> = focusSessionManager.session
}
Snippet do Kotlin aos 23:45 em "Introducing StateStore"
@HiltViewModel
class MiniTimerViewModel @Inject constructor(
    focusSessionManager: FocusSessionManager,
) : ViewModel() {
    val session: State<FocusSession> = focusSessionManager.session
}

@Composable
fun MiniTimerBar(viewModel: MiniTimerViewModel = hiltViewModel()) {
    val session by viewModel.session
    if (session !is FocusSession.Idle) {
        TimerReadout(session = session)
    }
}
Snippet do Kotlin aos 23:58 em "Introducing StateStore"
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
    @Provides
    @Singleton
    fun provideStateStore(
        @ApplicationContext context: Context,
        @ApplicationScope scope: CoroutineScope,
    ): StateStore = StateStore().apply {
        val path = context.dataStoreFile("app_state.json").absolutePath
        scope.launch { syncToDataStore(path = path, coroutineScope = scope) }
    }
}
Snippet do Kotlin em 24:04 em "Introducing StateStore"
@Serializable
sealed interface FocusSession {
    @Serializable data object Idle : FocusSession
    @Serializable data class Running(val taskId: String, val endTimeMillis: Long) : FocusSession
    @Serializable data class Paused(val taskId: String, val remainingMillis: Long) : FocusSession
}

object FocusSessionKey :
    StateStoreKey<FocusSession>(defaultValue = FocusSession.Idle),
    PersistToDataStore
Snippet do Kotlin aos 24:22 em "Introducing StateStore"
suspend fun StateStore.syncToDataStore(dataStore: DataStore<StateStorePreferences>) {
    listener { … }
}
Snippet do Kotlin aos 24:33 em "Introducing StateStore"
suspend fun StateStore.syncToDataStore(dataStore: DataStore<StateStorePreferences>) {
    listener {
        for (key in keys) {
            if (key is PersistToDataStore) {
                val value = getState(key).value

                LaunchedEffect(key) {
                    // Restore saved value from dataStore on first access
                }

                LaunchedEffect(key, value) {
                    // Flush updated value to dataStore on change
                }
            }
        }
    }
}
Snippet TOML aos 25:39 em "Resumo das categorias de estado do Compose"
[versions]
appstate = "1.0.0-alpha01"
...

[libraries]
...
transform = { module = "androidx.appstate.transform:transform", version.ref = "appstate" }
transform-testing = { module = "androidx.appstate.transform:transform-testing", version.ref = "appstate" }
statestore = { module = "androidx.appstate.statestore:statestore", version.ref = "appstate" }
statestore-datastore = { module = "androidx.appstate.statestore:statestore-datastore", version.ref = "appstate" }