Jetpack Compose es uno de los temas de los que más se habla en la serie de videos de Android 11 que reemplazó a Google IO. Muchos esperan que la biblioteca resuelva los problemas del marco de interfaz de usuario de Android actual, que contiene una gran cantidad de código heredado y decisiones arquitectónicas ambiguas. Otro marco igualmente popular, que discutiré en este artículo, es Kotlin Coroutines, y más específicamente, la API de flujo incluida en él, que puede ayudar a evitar la ingeniería excesiva al usar RxJava.
Le mostraré cómo usar estas herramientas usando una pequeña aplicación de administración de café escrita con Jetpack Compose para la interfaz de usuario y StateFlow como una herramienta de administración de estado. También utiliza la arquitectura 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. , , .