Transformar Parte do Android Jetpack.
Transform é uma biblioteca criada no ambiente de execução do Compose que executa um bloco @Composable
sem uma interface para transformar entradas em State do Compose.
Sempre que qualquer leitura de State dentro do bloco mudar, o bloco vai ser recomposto
automaticamente e vai atualizar o State retornado, permitindo que você use o modelo de rastreamento de dependência
e recomposição do Compose em ViewModels, repositórios ou
serviços em segundo plano.
Casos de uso
- Transformação de dados reativa: converta fluxos de dados brutos (como respostas de rede ou consultas de banco de dados) em um estado pronto para a interface.
- Combinar várias entradas: mescle várias fontes de estado independentes (como
entrada do usuário, configurações e status da rede) em um único estado coeso sem
operadores
Flowcomplexos. - Debouncing e limitação: implemente o comportamento baseado em tempo (como
debouncing de consultas de pesquisa) usando efeitos colaterais padrão do Compose,
como
LaunchedEffect. - Operações condicionais: execute diferentes lógicas de coleta de estado com base nas
condições atuais usando expressões padrão
if-elseouwhen.
Visão geral da API
O pacote androidx.appstate.transform oferece as seguintes funções:
package androidx.appstate.transform
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
public fun <R> transform(
initialValue: R,
scope: CoroutineScope,
dispatcher: CoroutineDispatcher = Dispatchers.Default,
onUpdate: @Composable (R) -> R,
): State<R>
@Composable
public fun <R> transform(
initialValue: R,
dispatcher: CoroutineDispatcher = Dispatchers.Default,
onUpdate: @Composable (R) -> R,
): State<R>
public suspend fun listener(
dispatcher: CoroutineDispatcher = Dispatchers.Default,
onUpdate: @Composable () -> Unit,
)
Criar uma transformação
É possível criar transformações por conta própria (usando um
CoroutineScope fornecido) ou em uma composição atual. Os parâmetros principais de qualquer
transformação são initialValue e onUpdate. A lambda onUpdate recebe o
valor de saída anterior R como parâmetro (inicializado como initialValue na
primeira composição). É possível retornar esse parâmetro para manter o último estado emitido
em vez de redefinir para um estado de carregamento, como
{ previous -> flow.collectAsState(initial = null).value ?: previous }.
Por exemplo, é possível usar transform para observar mudanças em uma string de consulta de pesquisa
e reduzir cálculos caros, como chamadas de rede:
var query: String by mutableStateOf("") val searchResults: List<String> by transform<List<String>>( initialValue = emptyList(), scope = coroutineScope, ) { var results by remember { mutableStateOf(emptyList<String>()) } LaunchedEffect(query) { if (query.isBlank()) { results = emptyList() return@LaunchedEffect } delay(300.milliseconds) results = api.search(query) } results }
Neste snippet:
queryé apoiado por umMutableState, que pode ser lido e atualizado por um elemento combinável de interface da barra de pesquisa.transformusa uminitialValuede uma lista vazia, umCoroutineScopeeonUpdatecomo uma lambda final.- No bloco
onUpdate,remembermantém o estado deresultsnas recomposições enquantoLaunchedEffecté executado. LaunchedEffectusaquerycomo chave. Comoqueryé apoiado por umMutableState, o efeito é cancelado e reiniciado sempre quequerymuda.- Para evitar chamar a API a cada pressionamento de tecla, o corpo do efeito aguarda 300 ms. Se a consulta mudar antes que esse atraso seja concluído, o efeito será cancelado e nenhuma chamada de rede será feita.
Em uma função @Composable, também é possível usar a sobrecarga
@Composable transform, que lembra um CoroutineScope internamente e executa onUpdate em
Dispatchers.Default por padrão. Isso é útil quando você quer descarregar uma
derivação de estado cara (como filtrar ou pesquisar uma lista grande) da
linha de execução de interface principal:
@Composable fun LogViewerScreen( entriesState: State<List<LogEntry>>, filterQueryState: State<String>, ) { // Calls rememberCoroutineScope() internally and runs onUpdate on // Dispatchers.Default so main-thread UI recomposition isn't blocked val matchingEntries by transform<List<LogEntry>>( initialValue = emptyList(), dispatcher = Dispatchers.Default, ) { val entries by entriesState val filterQuery by filterQueryState if (filterQuery.isBlank()) { entries } else { entries.filter { entry -> entry.matchesQuery(filterQuery) } } } // Render matchingEntries... }
Observar o estado com listener
Quando você quiser reagir às mudanças de State do Compose em um componente que não seja de interface (como
um gerenciador de estado, repositório ou serviço em segundo plano) sem produzir um State<R>
derivado, use listener.
listener é uma função suspend que executa um bloco @Composable () -> Unit
até que a corrotina de chamada seja cancelada, fazendo a recomposição automática sempre que qualquer
estado lido dentro de onUpdate mude:
class FocusSessionManager( private val stateStore: StateStore, private val notificationManager: NotificationManager, scope: CoroutineScope, ) { val session: State<FocusSession> = stateStore.getState(FocusSessionKey) init { scope.launch { listener { when (val current = session.value) { is FocusSession.Running -> { LaunchedEffect(current.endTimeMillis) { val remainingMillis = current.endTimeMillis - System.currentTimeMillis() if (remainingMillis > 0) { delay(remainingMillis) } stateStore.setState(FocusSessionKey, FocusSession.Idle) notificationManager.notify( NOTIFICATION_ID, buildSessionCompleteNotification(current.taskId), ) } } FocusSession.Idle, is FocusSession.Paused -> Unit } } } } // startFocus, pauseFocus, resumeFocus, endFocus... }
Neste exemplo, listener observa session.value. Quando a sessão está
FocusSession.Running, LaunchedEffect aguarda até endTimeMillis antes de
redefinir a sessão para FocusSession.Idle e postar uma notificação
de conclusão. Se o usuário pausar ou encerrar a sessão antes do tempo, a ramificação Running
vai sair da composição e cancelar o timer automaticamente. Quando scope é
cancelado, listener para de observar.
Padrões comuns
Os padrões a seguir mostram como usar transform para lógica condicional,
combinar vários fluxos e produzir o estado da interface em um ViewModel.
Produção de estado condicional
Use expressões padrão if-else ou when do Kotlin dentro de transform para
alternar entre as fontes de estado upstream com base nas condições atuais. Como o rastreamento de
leitura é dinâmico, o transform observa apenas a ramificação que é executada na
recomposição atual:
var searchQuery by mutableStateOf("") val displayedTasks: State<List<Task>> = transform( initialValue = emptyList(), scope = coroutineScope, ) { if (searchQuery.isBlank()) { val recentTasks by tasksRepository.recentTasks.collectAsState( initial = emptyList(), ) recentTasks } else { val matchingTasks by remember(searchQuery) { tasksRepository.searchTasks(searchQuery) }.collectAsState(initial = emptyList()) matchingTasks } }
Combinar várias fontes de estado
Como o Compose rastreia automaticamente as leituras de estado, é possível combinar qualquer número de
propriedades State<T> e streams Flow em um único bloco transform. Ao contrário de
Flow.combine, que só fornece sobrecargas tipadas para até
cinco fluxos antes de voltar para um Array<Any?> não tipado, adicionar outra entrada
em transform é sempre uma única leitura com segurança de tipos:
var showCompleted by mutableStateOf(false) var selectedCategory by mutableStateOf<Category?>(null) val dashboardState: State<DashboardUiState> = transform( initialValue = DashboardUiState(), scope = coroutineScope, ) { val tasks by tasksRepository.tasksFlow.collectAsState(initial = emptyList()) val sortOrder by userPrefsRepository.sortOrderFlow.collectAsState( initial = SortOrder.BY_DEADLINE, ) val focusSession by focusSessionManager.session val isOnline by networkMonitor.isOnline val visibleTasks = tasks .filter { task -> (showCompleted || !task.isCompleted) && (selectedCategory == null || task.category == selectedCategory) } .sortedWith(sortOrder.comparator) DashboardUiState( tasks = visibleTasks, selectedCategory = selectedCategory, focusSession = focusSession, isOnline = isOnline, ) }
Usar transform em um ViewModel
A sobrecarga não combinável de transform executa a lógica reativa em camadas que não são de IU
(como ViewModels ou repositórios) ao usar o modelo de estado do Compose. Ele é executado
enquanto o CoroutineScope fornecido estiver ativo. Assim, é possível vinculá-lo ao
ciclo de vida de um componente transmitindo viewModelScope dentro de um
ViewModel:
class HomeViewModel( private val mediaRepository: MediaRepository, private val networkMonitor: NetworkMonitor, ) : ViewModel() { val uiState: State<HomeUiState> = transform<HomeUiState>( initialValue = HomeUiState.Loading, scope = viewModelScope, ) { val isOnline by networkMonitor.isOnline val tracks = if (isOnline) { mediaRepository.allTracksFlow.collectAsState(initial = null).value } else { mediaRepository.downloadedTracksFlow .collectAsState(initial = null).value } ?: return@transform HomeUiState.Loading HomeUiState.Success( tracks = tracks, isOnline = isOnline, ) } }
Nesse ViewModel, o uiState lê o networkMonitor.isOnline e alterna
dinamicamente entre a coleta de mediaRepository.downloadedTracksFlow quando off-line
e de mediaRepository.allTracksFlow quando on-line, expondo um único
State<HomeUiState> que os elementos combináveis da interface podem ler diretamente. Para mais detalhes sobre
como combinar transform com MutableState e SavedStateHandle.saveable, consulte
Usar o estado do Compose em um ViewModel.
Escolha entre transform, derivedStateOf e Flow
Use as diretrizes a seguir para escolher entre transform, derivedStateOf
e Flow do Kotlin:
derivedStateOf x transform
Tanto derivedStateOf quanto transform produzem um State<T> derivado que rastreia automaticamente os estados do Compose lidos no bloco.
Escolha entre eles com base em se você está calculando um valor diretamente de
objetos State<T> existentes ou se precisa de recursos do @Composable, como
collectAsState, produceState e LaunchedEffect:
| Capacidade | derivedStateOf |
transform |
|---|---|---|
| Tipo lambda | Lambda padrão do Kotlin (() -> T) |
@Composable lambda (@Composable (R) -> R) |
| Entradas compatíveis | Somente leituras síncronas de State<T> |
State<T>, Kotlin Flow (usando collectAsState), funções suspend e APIs de callback (usando produceState) |
| Execução e consistência | Preguiçoso e síncrono quando .value é lido; atualizações atômicas no mesmo snapshot sem um initialValue ou CoroutineScope |
Executa uma composição ativa em um CoroutineScope e faz a recomposição de forma assíncrona após a confirmação de um snapshot. Requer um initialValue. |
| Estado e efeitos colaterais | Não é possível chamar APIs remember ou de efeito colateral |
Oferece suporte a funções remember, LaunchedEffect, produceState e @Composable reutilizáveis |
Use derivedStateOf quando todas as entradas já forem objetos State<T> e você
não precisar de APIs @Composable:
- Derivações síncronas do mesmo snapshot: computar um valor ou uma lista filtrada
de propriedades
State<T>atuais sem gerenciar umCoroutineScopeou fornecer um marcador de posiçãoinitialValue. ComoderivedStateOfé avaliado de forma síncrona quando.valueé lido, os autores de chamada que leem.valueimediatamente depois de atualizar um estado de entrada sempre observam o resultado atualizado no mesmo snapshot. - Reduzir a frequência de atualização dos observadores: calcular um valor que muda com menos
frequência do que a origem
State<T>(por exemplo, verificar se um deslocamento de rolagem excede um limite). Para mais detalhes, consulte a documentação doderivedStateOf.
Use transform quando a derivação exigir o ambiente de execução do Compose:
- Combinar
State<T>com streams assíncronos: coletar umFlowdo Kotlin de um repositório ou banco de dados usandocollectAsStatecom entradasState<T>. - Reiniciar o trabalho upstream quando as entradas mudarem: use
remember(key)dentro do bloco para recriar umFlowparametrizado somente quando uma entradaState<T>específica for atualizada. - Executar efeitos colaterais ou lembrar o estado intermediário: usar
LaunchedEffect(por exemplo, para remover o debounce de entradas ou executar timers),produceState(para carregar dados de uma funçãosuspendou encapsular um callback comawaitDispose) ourememberpara manter o estado entre as recomposições.
O exemplo a seguir compara derivedStateOf com um bloco transform que
combina State<T> e um repositório Flow:
class TaskListViewModel( private val tasksRepository: TasksRepository, private val preferencesManager: TaskPreferencesManager, ) : ViewModel() { var searchQuery by mutableStateOf("") private set // Computing a value from existing State<T>: use derivedStateOf val hasSearchQuery: State<Boolean> = derivedStateOf { searchQuery.isNotBlank() } // Combining State<T> with a Flow and remember(key): use transform val uiState: State<TaskListUiState> = transform<TaskListUiState>( initialValue = TaskListUiState.Loading, scope = viewModelScope, ) { val showCompleted by preferencesManager.showCompleted val tasks by remember(showCompleted) { tasksRepository.observeTasks(showCompleted) }.collectAsState(initial = null) val currentTasks = tasks ?: return@transform TaskListUiState.Loading val filteredTasks = currentTasks.filter { task -> task.title.contains(searchQuery, ignoreCase = true) } TaskListUiState.Success(filteredTasks) } }
Neste exemplo, hasSearchQuery inspeciona apenas o estado searchQuery, então
derivedStateOf o calcula de forma lenta sem um CoroutineScope. Por outro lado,
uiState combina preferencesManager.showCompleted e searchQuery com um
Flow assíncrono de tasksRepository e usa remember(showCompleted) para
evitar a recriação do Flow quando apenas searchQuery muda.
Quando usar Flow em vez de transform
O transform foi projetado para representar o valor atual do estado, não um
fluxo de eventos. Se um estado mudar várias vezes antes da próxima
recomposição, o Compose vai usar apenas o valor mais recente e pular as atualizações quando o
novo valor for igual (==) ao anterior. Use o Flow
do Kotlin quando a lógica precisar processar todos os valores em ordem:
- Totais e médias acumulados: cálculos como médias móveis, somas cumulativas ou histórico de desfazer (
runningFoldouscan) precisam que cada valor seja contado uma vez. Em um blocotransform, as atualizações rápidas antes da recomposição são ignoradas, duas leituras idênticas seguidas (como um sensor emitindo10.0duas vezes) não acionam a recomposição, e as recomposições acionadas por outros estados no mesmo bloco podem contar o valor atual duas vezes. Primeiro, calcule o valor em execução em umFlowe, em seguida, colete o resultado emtransformse precisar combiná-lo com outro estado. - Log de eventos e filas: pipelines que registram interações do usuário, processam solicitações de rede em fila ou registram transações em que cada mudança intermediária (
A -> B -> CouA -> B -> A) precisa ser registrada. - Eventos únicos: ações como tocar um som ou enviar um comando de hardware, em que o envio do mesmo comando duas vezes seguidas precisa ser executado duas vezes.
transformrequer uminitialValuee ignora saídas iguais consecutivas. - Fluxos sensíveis à contrapressão: fluxos (como leituras de arquivos ou soquetes) em que um produtor rápido precisa ser suspenso quando um consumidor fica para trás. As gravações de estado do Compose não são suspensas e substituem os valores anteriores.
Testar transformações
Como as transformações dependem do ambiente de execução do Compose e das corrotinas do Kotlin, o teste
delas exige a coordenação da execução assíncrona e da recomposição de estado. Use
o artefato androidx.appstate.transform:transform-testing para controlar o
programador de corrotinas e acionar recomposições em testes de unidade.
O pacote androidx.appstate.transform.testing oferece as seguintes APIs:
package androidx.appstate.transform.testing
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.time.Duration
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.TestResult
public fun transformTest(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend TransformTestScope.() -> Unit,
): TestResult
public sealed interface TransformTestScope : CoroutineScope {
public val testCoroutineScheduler: TestCoroutineScheduler
public val testDispatcher: TestDispatcher
public val backgroundScope: CoroutineScope
public fun advance()
public fun advanceTimeBy(duration: Duration)
public fun advanceUntilIdle()
}
transformTest envolve runTest e executa o bloco de teste
com um receptor TransformTestScope:
backgroundScope: umCoroutineScopepara executar transformações que não devem bloquear a conclusão do teste. Ele é cancelado automaticamente quando o teste é concluído, o que interrompe a transformação.testDispatcher: umTestDispatchervinculado atestCoroutineScheduler. Transmita isso ao parâmetrodispatcherdetransformpara que a recomposição seja executada no programador de teste em vez de usarDispatchers.Defaultcomo padrão.advance: executa corrotinas pendentes no programador de teste, envia notificações de aplicação de snapshot do Compose (Snapshot.sendApplyNotifications) e recompõe otransformpara que a saídaStateseja atualizada.advanceTimeBy(duration)eadvanceUntilIdle: avança o relógio virtual emtestCoroutineSchedulerpara operações dependentes do tempo.
Testar atualizações de estado
Para testar um transform, transmita backgroundScope e testDispatcher de
TransformTestScope e chame advance depois de atualizar um estado de entrada ou
Flow:
@Test fun testStateUpdate() = transformTest { var sourceState by mutableStateOf(0) val state = transform( initialValue = 0, scope = backgroundScope, dispatcher = testDispatcher, ) { sourceState } assertThat(state.value).isEqualTo(0) // Modify the source state and advance to trigger recomposition. sourceState = 1 advance() assertThat(state.value).isEqualTo(1) }
Testar operações baseadas em tempo
Quando um transform usa efeitos colaterais baseados em tempo, como LaunchedEffect e
delay (por exemplo, consultas de pesquisa de debounce), combine advanceTimeBy(duration)
com advance:
- Depois de modificar um estado de entrada, chame
advancepara que o blocotransformseja recomposto e inicie oLaunchedEffect. - Chame
advanceTimeBy(duration)para avançar o relógio virtual e, em seguida, chameadvancepara executar tarefas programadas nesse carimbo de data/hora e recompor otransform.
@Test fun testDebouncedSearch() = transformTest { var query by mutableStateOf("") val searchResults = transform<List<String>>( initialValue = emptyList(), scope = backgroundScope, dispatcher = testDispatcher, ) { var results by remember { mutableStateOf(emptyList<String>()) } LaunchedEffect(query) { if (query.isBlank()) { results = emptyList() return@LaunchedEffect } delay(300.milliseconds) results = api.search(query) } results } query = "compose" // Recompose to start the LaunchedEffect for the new query. advance() // Results don't update before the 300ms debounce delay completes. advanceTimeBy(200.milliseconds) advance() assertThat(searchResults.value).isEmpty() // Advance past the remaining 100ms and trigger recomposition. advanceTimeBy(100.milliseconds) advance() assertThat(searchResults.value).containsExactly("Compose State", "Compose UI") }
Testar um ViewModel ou detentor de estado
Como transform executa a composição no scope fornecido e usa Dispatchers.Default como padrão
para o parâmetro dispatcher, as classes que criam um
transform precisam aceitar um CoroutineScope e um CoroutineDispatcher como
parâmetros do construtor. Em um ViewModel, transmita o CoroutineScope injetado para o construtor ViewModel(coroutineScope) e encaminhe o CoroutineDispatcher para transform:
class HomeViewModel( private val mediaRepository: MediaRepository, private val networkMonitor: NetworkMonitor, coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate), dispatcher: CoroutineDispatcher = Dispatchers.Default, ) : ViewModel(coroutineScope) { val uiState: State<HomeUiState> = transform<HomeUiState>( initialValue = HomeUiState.Loading, scope = viewModelScope, dispatcher = dispatcher, ) { val isOnline by networkMonitor.isOnline val tracks = if (isOnline) { mediaRepository.allTracksFlow.collectAsState(initial = null).value } else { mediaRepository.downloadedTracksFlow .collectAsState(initial = null).value } ?: return@transform HomeUiState.Loading HomeUiState.Success( tracks = tracks, isOnline = isOnline, ) } } @Test fun testHomeViewModelOfflineState() = transformTest { val mediaRepository = FakeMediaRepository() val networkMonitor = FakeNetworkMonitor() val viewModel = HomeViewModel( mediaRepository = mediaRepository, networkMonitor = networkMonitor, coroutineScope = backgroundScope, dispatcher = testDispatcher, ) val downloadedTrack = Track(id = "1", isDownloaded = true) val streamingTrack = Track(id = "2", isDownloaded = false) mediaRepository.emitAllTracks(listOf(downloadedTrack, streamingTrack)) mediaRepository.emitDownloadedTracks(listOf(downloadedTrack)) advance() assertThat(viewModel.uiState.value).isEqualTo( HomeUiState.Success( tracks = listOf(downloadedTrack, streamingTrack), isOnline = true, ) ) // Switch offline and verify it switches to downloadedTracksFlow. networkMonitor.setOnline(false) // First advance() recomposes into the offline branch (where collectAsState // initially returns null -> Loading) and starts collecting downloadedTracksFlow. advance() // Second advance() applies the collected downloadedTracksFlow value and // recomposes uiState with the offline Success state. advance() assertThat(viewModel.uiState.value).isEqualTo( HomeUiState.Success( tracks = listOf(downloadedTrack), isOnline = false, ) ) }
Nesse teste, transmitir backgroundScope e testDispatcher para
HomeViewModel vincula a composição viewModelScope e transform ao
programador de teste. Assim, cada chamada advance processa de forma determinista as emissões
de FakeMediaRepository e FakeNetworkMonitor. Para mais padrões sobre o uso de
SavedStateHandle.saveable e Snapshot.withMutableSnapshot em um ViewModel,
consulte Usar o estado do Compose em um ViewModel.