Я разрабатываю приложение для Android, используя Jetpack Compose и архитектуру MVVM. Когда я нажимаю кнопку «Добавить», в список добавляется новая заметка, но пользовательский интерфейс не перестраивается для отображения новой заметки.
Вот мой полный код:
data class Note(
val text: String = ""
)
data class UiState(
val notes: List<Note>
)
class NoteRepository {
private val notes = mutableListOf<Note>()
fun getNotes(): List<Note> = notes
fun addNote(note: Note) {
notes.add(note)
}
}
class NoteViewModel : ViewModel() {
private val repository = NoteRepository()
private val _uiState = MutableStateFlow(UiState(repository.getNotes()))
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun addNote(note: Note) {
repository.addNote(note)
// Logging to check the state update
Log.d("NoteViewModel", "Notes list: ${repository.getNotes()}")
//_uiState.value = uiState.value.copy(notes = repository.getNotes())
_uiState.value = UiState(repository.getNotes())
}
}
@Composable
fun Board(modifier: Modifier = Modifier, viewModel: NoteViewModel) {
val uiState by viewModel.uiState.collectAsState()
Box(modifier = Modifier
.fillMaxSize()
.then(modifier)
) {
Button(
onClick = { viewModel.addNote(Note()) },
modifier = Modifier.align(Alignment.BottomEnd)
) {
Text(text = "Add")
}
Column {
uiState.notes.forEach { _ ->
Box(
modifier = Modifier
.padding(6.dp)
.size(20.dp)
.background(Color.Blue)
)
}
}
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
NoteTheme {
Board(viewModel = viewModel())
}
}
}
}
Могу подтвердить, что заметка добавляется в список при нажатии на кнопку. Однако пользовательский интерфейс не обновляется и не отображает новую добавленную заметку.
Редактировать
Использование mutableStateListOf вместо mutableListOf в классе репозитория решает проблему:
private val notes = mutableStateListOf<Note>()
Однако такой подход делает репозиторий зависимым от Compose, чего я бы предпочел избежать.
Вы правы, не используя MutableState в репозитории, но его даже не следует использовать в модели представления. Вместо этого вам следует использовать Flows.
Вы уже используете потоки в модели представления, но их также следует использовать на нижних уровнях вашего приложения. Многие библиотеки источников данных (базы данных, сетевой ввод-вывод, ввод-вывод файловой системы) уже изначально поддерживают потоки, а другие (например, API на основе обратных вызовов) можно легко преобразовать в потоки. Общая идея состоит в том, чтобы как можно скорее создать поток и передать его через слои в пользовательский интерфейс, где он собирается. По мере прохождения слоев контент может трансформироваться по мере необходимости.
В вашем случае источником данных является репозиторий, поэтому данные должны быть представлены в виде потока:
class NoteRepository {
private val _notes = MutableStateFlow<List<Note>>(emptyList())
val notes: StateFlow<List<Note>> = _notes.asStateFlow()
fun addNote(note: Note) {
_notes.update { it + note }
}
}
Затем модель представления преобразует содержимое (a List<Note>) в UiState, применяя mapLatest. Результирующий поток преобразуется в StateFlow, чтобы его можно было легко собрать в пользовательском интерфейсе:
class NoteViewModel : ViewModel() {
private val repository = NoteRepository()
@OptIn(ExperimentalCoroutinesApi::class)
val uiState: StateFlow<UiState> = repository.notes
.mapLatest { UiState(it) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = UiState(emptyList()),
)
fun addNote(note: Note) {
repository.addNote(note)
}
}
Существуют и другие функции преобразования потоков. Вы можете, например, combine создать несколько потоков или заменить один поток другим, в зависимости от его содержания (flatMapLatest).
В вашей композиции вы должны использовать collectAsStateWithLifecycle вместо collectAsState, чтобы собрать поток. Это позаботится о правильном запуске и остановке потока, когда компонуемый объект входит в композицию и покидает ее, чтобы сэкономить ресурсы. Для этого вам понадобится зависимость Gradle androidx.lifecycle:lifecycle-runtime-compose. В вашем конкретном случае с базовым StateFlow в репозитории это не будет иметь значения, но однажды вы можете отключить репозиторий с помощью базы данных, и тогда это будет актуально. Лучше использовать collectAsStateWithLifecycle с самого начала.
it shouldn't even be used in the view model Можете объяснить почему? В официальной лаборатории кода ViewModel они используют mutableStateOf во ViewModel, и я также видел это в некоторых других проектах. Я считал, что использовать MutableState во ViewModel вполне нормально.
Изменения MutableState можно наблюдать только в том случае, если изменения вносятся из среды, поддерживающей моментальные снимки. Если модель представления обрабатывается только Compose, все будет работать нормально. Однако в других случаях обновления MutableState не будут распространяться должным образом. Наиболее ярким примером являются модульные тесты модели представления. Чтобы это исправить, вам нужно будет обернуть весь код, который изменяет MutableState модели представления, в withMutableSnapshot. Это чревато ошибками, и если их пропустить, это может привести к ошибкам, которые трудно отладить.
Официальная документация и лаборатории кода еще не полностью обновлены, но некоторые примеры приложений уже обновлены (например, Now in Android, конец 2022 г.). Эта проблема также рассматривалась здесь: stackoverflow.com/a/74601657/6216216
Спасибо за ссылки! На самом деле я не знал, что происходит этот сдвиг. Буду иметь это в виду.
StateFlow uiState не обновляется новым значением, новый UiState считается таким же, как и предыдущий, даже если список изменяется, свойство объединения потока состояний предотвращает выброс этого нового значения.
Самое простое решение — изменить класс данных следующим образом.
data class UiState(val notes: List<Note>, private var hashCode: Int = notes.hashCode())
Здесь целочисленное поле также меняется при изменении списка, это приведет к повторной эмиссии из stateFlow
Обычно я использую способ uiState.value.copy() при обновлении uiState. Вы это закомментируете, потому что это не работает?