Jetpack Compose est l'un des sujets les plus discutés de la série de vidéos Android 11 remplaçant Google IO. Beaucoup s'attendent à ce que la bibliothèque résolve les problèmes du cadre d'interface utilisateur Android actuel, qui contient beaucoup de code hérité et des décisions architecturales ambiguës. Un autre framework tout aussi populaire, dont je parlerai dans cet article, est Kotlin Coroutines, et plus spécifiquement, l'API Flow qui y est incluse, qui peut aider à éviter une sur-ingénierie lors de l'utilisation de RxJava.
Je vais vous montrer comment utiliser ces outils à l'aide d'une petite application de gestion du café écrite à l'aide de Jetpack Compose pour l'interface utilisateur et StateFlow en tant qu'outil de gestion d'état. Il utilise également l'architecture 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. , , .