Jetpack Compose é um dos tópicos mais comentados na série de vídeos do Android 11 que substituiu o Google IO. Muitos esperam que a biblioteca resolva os problemas da atual estrutura de IU do Android, que contém muitos códigos legados e decisões arquitetônicas ambíguas. Outra estrutura igualmente popular, que discutirei neste artigo, é o Kotlin Coroutines e, mais especificamente, a API de fluxo incluída nele, que pode ajudar a evitar o excesso de engenharia ao usar RxJava.
Vou mostrar como usar essas ferramentas usando um pequeno aplicativo de gerenciamento de café escrito usando Jetpack Compose para a IU e StateFlow como uma ferramenta de gerenciamento de estado. Ele também usa arquitetura MVI.

. , ( ), . , . : .
Jetpack Compose
Jetpack Compose XML , , - UI- Kotlin . Flutter . UI . UI-.
Flutter, Compose MainActivity . . , Flutter Compose . Compose API , , Flutter.
Compose- Android Studio. MainActivity.kt:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CoffeegramTheme {
Greeting("Android")
}
}
}
}
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
CoffeegramTheme {
Greeting("Android")
}
}
Compose,
Compose , @Composable. .
setContentView(), Activity.onCreate() , setContent(), Composable-.
@Preview Composable-, Android Studio ( 4.2 Canary) . . Hot Reload Flutter, - , . , UI , .
, , .idea Git . , - . , .
, .
Composable- - , , , , . , .
. , .

data class CoffeeType(
@DrawableRes
val image: Int,
val name: String,
val count: Int = 0
)
@Composable
fun CoffeeTypeItem(type: CoffeeType) {
Row(
modifier = Modifier.padding(16.dp)
) {
Image(
imageResource(type.image), modifier = Modifier
.preferredHeightIn(maxHeight = 48.dp)
.preferredWidthIn(maxWidth = 48.dp)
.fillMaxWidth()
.clip(shape = RoundedCornerShape(24.dp))
.gravity(Alignment.CenterVertically),
contentScale = ContentScale.Crop
)
Spacer(Modifier.preferredWidth(16.dp))
Text(
type.name, style = typography.body1,
modifier = Modifier.gravity(Alignment.CenterVertically).weight(1f)
)
Row(modifier = Modifier.gravity(Alignment.CenterVertically)) {
val count = state { type.count }
Spacer(Modifier.preferredWidth(16.dp))
val textButtonModifier = Modifier.gravity(Alignment.CenterVertically)
.preferredSizeIn(
maxWidth = 32.dp,
maxHeight = 32.dp,
minWidth = 0.dp,
minHeight = 0.dp
)
TextButton(
onClick = { count.value-- },
padding = InnerPadding(0.dp),
modifier = textButtonModifier
) {
Text("-")
}
Text(
"${count.value}", style = typography.body2,
modifier = Modifier.gravity(Alignment.CenterVertically)
)
TextButton(
onClick = { count.value++ },
padding = InnerPadding(0.dp),
modifier = textButtonModifier
) {
Text("+")
}
}
}
}
ListView — Row. ( Image png- drawable); Spacer; Text , - weight(1f) ( ListView); Row .
Android Studio . , . ( Android Studio ), . .
State
, , . - val count = state { type.count }, state type Composable- . count.value. , , , , ( state collectAsState ).
Flutter, Compose Stateful ( ) Stateless ( ) . , Stateful, Stateless.
. — Column , :
@Composable
fun CoffeeList(coffeeTypes: List<CoffeeType>) {
Column {
coffeeTypes.forEach { type ->
CoffeeTypeItem(type)
}
}
}
@Composable
fun ScrollableCoffeeList(coffeeTypes: List<CoffeeType>) {
VerticalScroller(modifier = Modifier.weight(1f)) {
CoffeeList(coffeeTypes: List<CoffeeType>)
}
}
Composable , if, for, when .. Column ListView , VerticalScroller — ScrollView.
. . Compose RecyclerView? — LazyColumnItems ( AdapterList). CoffeeList :
@Composable
fun CoffeeList( coffeeTypes: List<CoffeeType>, modifier: Modifier = Modifier) {
LazyColumnItems(data = coffeeTypes, modifier = modifier.fillMaxHeight()) { type ->
CoffeeTypeItem(type)
}
}
RecyclerView GridLayoutManager ( ). .

, .
Material design Flutter Compose. Scaffold, . TopAppBar ( ), BottomAppBar ( , — Floating action button) Drawer ( ). BottomNavigationView Material Scaffold Column BottomNavigation :
@Composable
fun DefaultPreview() {
CoffeegramTheme {
Scaffold() {
Column() {
var selectedItem by state { 0 }
when (selectedItem) {
0 -> {
Column(modifier = Modifier.weight(1f)){}
}
1 -> {
CoffeeList(listOf(...))
}
}
val items =
listOf(
"Calendar" to Icons.Filled.DateRange,
"Info" to Icons.Filled.Info
)
BottomNavigation {
items.forEachIndexed { index, item ->
BottomNavigationItem(
icon = { Icon(item.second) },
text = { Text(item.first) },
selected = selectedItem == index,
onSelected = { selectedItem = index }
)
}
}
}
}
}
}
selectedItem. when . BottomNavigation selectedItem. Compose.
. , , , , . ContextAmbient.current.context Composable-. :

png- . imageResource Image vectorResource. Icon ( ), .
StateFlow
. Flow . — ( ). BehaviorSubject RxJava. StateFlow. BehaviorSubject, .
, , selectedItem selectedItemFlow:
val selectedItemFlow = MutableStateFlow(0)
@Composable
fun DefaultPreview() {
...
val selectedItem by selectedItemFlow.collectAsState()
when (selectedItem) {
0 -> TablePage()
1 -> CoffeeListPage()
}
...
BottomNavigationItem(
selected = selectedItem == index,
onSelected = { selectedItemFlow.value = index }
)
}
StateFlow ( Flow) collectAsState(). , .
, selectedItemFlow.value.
, collectAsState() . . (val selectedItem by selectedItemFlow.collectAsState()) , MutableStateFlow (selectedItemFlow.value) — .
, , , StateFlow:
val yearMonthFlow = MutableStateFlow(YearMonth.now())
val dateFlow = MutableStateFlow(-1)
val daysCoffeesFlow: DaysCoffeesFlow = MutableStateFlow(mapOf())
yearMonthFlow .
dateFlow — : -1 — — TablePage. — CoffeeListPage .
daysCoffeesFlow — , . .
TablePage CoffeeListPage, ( ) , daysCoffeesFlow. CoffeeList . , , daysCoffeesFlow. , Flow .
, DayCoffee.kt. , .
UI-. MVI-. , MVICore, RxJava . Android MVI with Kotlin Coroutines & Flow article. MVI . Store:
abstract class Store<Intent : Any, State : Any>(private val initialState: State) {
protected val _intentChannel: Channel<Intent> = Channel(Channel.UNLIMITED)
protected val _state = MutableStateFlow(initialState)
val state: StateFlow<State>
get() = _state
fun newIntent(intent: Intent) {
_intentChannel.offer(intent)
}
init {
GlobalScope.launch {
handleIntents()
}
}
private suspend fun handleIntents() {
_intentChannel.consumeAsFlow().collect { _state.value = handleIntent(it) }
}
protected abstract fun handleIntent(intent: Intent): State
}
Store Intent- StateFlow<State> . , Reducer handleIntent() . Store state, StateFlow; newIntent().
NavigationStore, :
class NavigationStore : Store<NavigationIntent, NavigationState>(
initialState = NavigationState.TablePage(YearMonth.now())
) {
override fun handleIntent(intent: NavigationIntent): NavigationState {
return when (intent) {
NavigationIntent.NextMonth -> {
increaseMonth(_state.value.yearMonth)
}
NavigationIntent.PreviousMonth -> {
decreaseMonth(_state.value.yearMonth)
}
is NavigationIntent.OpenCoffeeListPage -> {
NavigationState.CoffeeListPage(
LocalDate.of(
_state.value.yearMonth.year,
_state.value.yearMonth.month,
intent.dayOfMonth
)
)
}
NavigationIntent.ReturnToTablePage -> {
NavigationState.TablePage(_state.value.yearMonth)
}
}
}
private fun increaseMonth(yearMonth: YearMonth): NavigationState {
return NavigationState.TablePage(yearMonth.plusMonths(1))
}
private fun decreaseMonth(yearMonth: YearMonth): NavigationState {
return NavigationState.TablePage(yearMonth.minusMonths(1))
}
}
sealed class NavigationIntent {
object NextMonth : NavigationIntent()
object PreviousMonth : NavigationIntent()
data class OpenCoffeeListPage(val dayOfMonth: Int) : NavigationIntent()
object ReturnToTablePage : NavigationIntent()
}
sealed class NavigationState(val yearMonth: YearMonth) {
class TablePage(yearMonth: YearMonth) : NavigationState(yearMonth)
data class CoffeeListPage(val date: LocalDate) : NavigationState(
YearMonth.of(date.year, date.month)
)
}
. sealed-, , Store. UI. — .
initialState NavigationStore -, , .
handleIntent() - .
DaysCoffeesStore, , , .
Jetpack Compose , Android-. , , (, , ) . , , , Compose ( ) .
UI-, Compose, Flutter SwiftUI, Web. , , .