Практическое руководство по использованию Compose, MVI и MutableState для управления обращениями к БД внутри ВьюМодели

Введение

Compose - это новый инструмент от Google, который помогает облегчить процесс разработки пользовательского интерфейса. Он вынуждает разработчиков думать о приложении в терминах состояний, а не отдельных виджетов. Это позволяет сделать код более понятным и управляемым. Однако, при работе большинства приложений необходимо взаимодействовать с БД. Здесь возникает необходимость использовать архитектуру MVI вместе с Compose и MutableState.

MVI

MVI - это архитектурный подход, помогающий разработчикам писать приложения без побочных эффектов и зависимостей от жизненного цикла элементов. Он позволяет разделить UI, логику и состояние на три части. В этом случае мы разделяем View, Model и Intent на три компонента. View, при этом, становится пассивным компонентом, который генерирует события, определяя намерения и отображая интерфейс. Model - это активный компонент, который хранит состояние и содержит бизнес-логику. И, наконец, Intent набор событий.

Mutable State

MutableState - это аннотация компонента, позволяющая Compose обновлять компонент при изменении состояния внутри ViewModel.

ВьюМодель

ViewModel - это архитектурный компонент, который предоставляет данным жизненный цикл, соответствующий жизненному циклу компонента Activity. Он содержит состояние приложения и бизнес-логику.

Пример

Приведу пример приложения, которое использует Compose, MVI и MutableState для управления обращениями к БД внутри ViewModel.

@Composable
fun MyApp(viewModel: MyViewModel) {
    val state by viewModel.state.collectAsState()
    val scaffoldState = rememberScaffoldState()

    Scaffold(
        scaffoldState = scaffoldState,
        topBar = {
            TopAppBar(
                title = { Text("My App") },
            )
        },
        floatingActionButton = {
            FloatingActionButton(onClick = { viewModel.addItem() }) {
                Icon(Icons.Default.Add, contentDescription = "Add item")
            }
        },
    ) {
        when (state.screenState) {
            ScreenState.LOADING -> CircularProgressIndicator()
            ScreenState.ERROR -> Text(text = "Error")
            else -> ItemsList(state.items) { id -> viewModel.removeItem(id) }
        }
    }

}

@Composable
fun ItemsList(items: List<Item>, onRemove: (id: Long) -> Unit) {
    LazyColumn {
        items(items) { item ->
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(16.dp),
                verticalAlignment = Alignment.CenterVertically
            ) {
                Text(item.title, modifier = Modifier.weight(1f))
                IconButton(onClick = { onRemove(item.id) }) {
                    Icon(Icons.Default.Delete, contentDescription = "Remove item")
                }
            }
        }
    }
}

class MyViewModel : ViewModel() {
    private val _state = MutableStateFlow(MyState())
    val state: StateFlow<MyState> = _state.asStateFlow()

    private val _intents = Channel<MyIntent>(Channel.UNLIMITED)
    private val items = mutableListOf<Item>()

    init {
        viewModelScope.launch {
            _intents.consumeAsFlow().collect { intent ->
                when (intent) {
                    is MyIntent.LoadItems -> {
                        _state.value = MyState(screenState = ScreenState.LOADING, items = emptyList())

                        try {
                            items.addAll(loadFromDb())
                            _state.value = MyState(screenState = ScreenState.CONTENT, items = items)
                        } catch (e: Exception) {
                            _state.value = MyState(screenState = ScreenState.ERROR)
                        }
                    }
                    is MyIntent.AddItem -> {
                        val item = addItemToDb()
                        items.add(item)
                        _state.value = MyState(screenState = ScreenState.CONTENT, items = items)
                    }
                    is MyIntent.RemoveItem -> {
                        removeItemFromDb(intent.id)
                        items.removeIf { it.id == intent.id }
                        _state.value = MyState(screenState = ScreenState.CONTENT, items = items)
                    }
                }
            }
        }
    }

    fun loadItems() {
        viewModelScope.launch {
            _intents.send(MyIntent.LoadItems)
        }
    }

    fun addItem() {
        viewModelScope.launch {
            _intents.send(MyIntent.AddItem)
        }
    }

    fun removeItem(id: Long) {
        viewModelScope.launch {
            _intents.send(MyIntent.RemoveItem(id))
        }
    }

    private suspend fun loadFromDb(): List<Item> {
        delay(1000)
        return listOf(
            Item(1, "Item 1"),
            Item(2, "Item 2"),
            Item(3, "Item 3"),
        )
    }

    private suspend fun addItemToDb(): Item {
        delay(500)
        return Item(items.count() + 1, "Item ${items.count() + 1}")
    }

    private suspend fun removeItemFromDb(id: Long) {
        delay(500)
    }
}

data class MyState(val screenState: ScreenState = ScreenState.LOADING, val items: List<Item> = emptyList())

sealed class MyIntent {
    object LoadItems : MyIntent()
    object AddItem : MyIntent()
    data class RemoveItem(val id: Long) : MyIntent()
}

data class Item(val id: Long, val title: String)

enum class ScreenState {
    LOADING,
    CONTENT,
    ERROR
}

Здесь мы видим требуемые компоненты: View, Model и Intent. MyApp - это View, ItemsList - это подкомпонент View, MyViewModel - это Model, а MyIntent - Intent. Внутри ViewModel мы не используем непосредственно технологию Room, а используем заглушки для загрузки, добавления и удаления элементов. Задача ViewModel - управлять состоянием этих элементов и отображать их в View.

Мы используем MutableState для обновления View при изменении состояния элементов внутри ViewModel.

Заключение

Таким образом, использование Compose, MVI и MutableState совместно может значительно облегчить написание приложений с отображенением списка элементов из БД. MVI позволяет разделить код на три компонента, Compose облегчает работу с интерфейсом, а MutableState позволяет обновлять View при изменениях внутри ViewModel. Надеюсь, данное руководство будет полезным для разработчиков, желающих более эффективного использования данных технологий.

Смотри также: