Вид:
val viewModel = hiltViewModel<ActivityViewModel>()
Text("STATE: ${viewModel.state.activity?.invitation?.state?.title}")
Модель просмотра:
@HiltViewModel
class ActivityViewModel @Inject constructor(
private val repository: ActivityRepository,
@ApplicationContext private val context: Context,
) : ViewModel() {
var state by mutableStateOf(ActivityScreenState())
private set
suspend fun fetchActivity(id: String) {
val resource = repository.fetchActivity(id)
val activity = resource.data
resource.errorMessage?.let {
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
}
state = state.copy(
isLoading = false,
activity = activity,
)
}
suspend fun accept(invitation: Invitation) {
val tempActivity = state.activity
tempActivity?.invitation?.state = InvitationState.ACCEPTED
state = state.copy(
activity = tempActivity,
)
}
}
Активитскринстате:
data class ActivityScreenState(
val isLoading: Boolean = true,
val activity: Activity? = null,
)
data class Activity(
val id: String,
val invitation: Invitation?,
)
data class Invitation(
val id: String,
var state: InvitationState,
)
enum class InvitationState(val title: String) {
ACCEPTED("accepted"),
DECLINED("declined"),
}
У меня есть класс данных viewModel, ActivityScreenState, который содержит класс данных Activity. Когда я обновляю Activity внутри ActivityScreenState, оно не перекомпоновывает мое составное представление, но я знаю, что оно изменится, если я запишу его в журнал.
Я пробовал искать, но не нашел, что делаю не так. Также выяснилось, что он перекомпонуется только в том случае, если я аннулирую активность внутри ActivityScreenState.
Я делаю что-то не так или это баг?
@dev.tejasb спасибо, обновил вопрос. Да, я сначала получаю активность из API, а затем пытаюсь обновить ее после того, как пользователи нажимают кнопку. См. обновленный фрагмент кода модели представления.
можешь попробовать val state by remember { viewModel.state } Text("STATE: ${state.activity?.inspectionInvitation?.state?.title ?: "Unknown"}")
@dev.tejasb Я получаю выделенную ошибку Type 'TypeVariable(T)' has no method 'getValue(Nothing?, KProperty<*>)' and thus it cannot serve as a delegate
замените by
на =
@dev.tejasb спасибо, ошибку избавили. Но теперь он вообще не перекомпоновывает представление (даже при первой выборке данных).
можешь попробовать MutableStateFlow
вместо mutableStateOf
и использовать .collectAsState()
в своей композиции?
Та же проблема, не работает даже при первом получении данных. var state = MutableStateFlow(InspectionActivityScreenState()) val state = viewModel.state.collectAsState() Text("STATE: ${state.value.activity?.inspectionInvitation?.state?.title ?: "Unknown"}")
когда вы используете MutableStateFlow
, вам нужно заменить state = state.copy(isLoading = false, activity = activity)
на state.update {it.copy(isLoading = false,activity = activity)}
спасибо, да, теперь он работает при первой загрузке, но не перекомпоновывается после обновления state.activity
(исходная проблема).
Я заметил, что это не работает только при обновлении активности. Если я обновляю что-нибудь еще внутри ActivityScreenState, это работает, и представление перестраивается. Может быть, Jetpack Compose не знает, как перекомпоновать вложенные объекты?
Я взял на себя смелость обновить ваш код, чтобы он скомпилировался. Убедитесь, что это именно то, что вы хотите задать, и внесите необходимые изменения.
Причина в том, что класс данных использует equals
и hashCode
для структурного равенства ==
и с
@Suppress("UNCHECKED_CAST")
fun <T> structuralEqualityPolicy(): SnapshotMutationPolicy<T> =
StructuralEqualityPolicy as SnapshotMutationPolicy<T>
private object StructuralEqualityPolicy : SnapshotMutationPolicy<Any?> {
override fun equivalent(a: Any?, b: Any?) = a == b
override fun toString() = "StructuralEqualityPolicy"
}
@StateFactoryMarker
fun <T> mutableStateOf(
value: T,
policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()
): MutableState<T> = createSnapshotMutableState(value, policy)
Вы не обновляете параметры ActivityScreenState
suspend fun accept(invitation: Invitation) {
val tempActivity = state.activity
tempActivity?.invitation?.state = InvitationState.ACCEPTED
state = state.copy(
activity = tempActivity
)
}
вы не меняете никаких свойств конструктора
data class ActivityScreenState(
val isLoading: Boolean = true,
val activity: Activity? = null,
)
вы фактически меняете параметр Activity
, в то время как экземпляр остается прежним.
у вас должен быть новый activity
, пока вы устанавливаете тот же activity
после изменения state = InvitationState.ACCEPTED
, или вы можете использовать referentialEqualityPolicy()
, который запускает рекомпозицию при назначении нового объекта.
suspend fun accept(invitation: Invitation) {
val newActivity = state.activity.copy(activity = ...new instance here with copy or creating new Activity instance with new invitation)
state = state.copy(
activity = newActivity
)
}
В общем, если вы хотите принудительно обновить с тем же значением или другой ссылкой, вы можете изменить snapshotMutationPolicies.
Например
@Preview
@Composable
fun ForceRecpompositionSample() {
Column {
Composable1()
Composable2()
}
}
@Composable
fun Composable1() {
var myCounter by remember {
mutableStateOf(MyCounter(0))
}
Column(
modifier = Modifier.border(2.dp, getRandomColor()).fillMaxWidth().padding(8.dp)
) {
Button(
onClick = {
myCounter = myCounter.copy(value = 5)
}
) {
Text("Update MyCounter")
}
Text("Value: ${myCounter.value}")
}
}
@Composable
fun Composable2() {
var myCounter by remember {
mutableStateOf(
value = MyCounter(0),
policy = referentialEqualityPolicy()
)
}
Column(
modifier = Modifier.border(2.dp, getRandomColor()).fillMaxWidth().padding(8.dp)
) {
Button(
onClick = {
myCounter = myCounter.copy(value = 5)
}
) {
Text("Update MyCounter")
}
Text("Value: ${myCounter.value}")
}
}
data class MyCounter(val value: Int)
если вы отметите второй компонуемый объект, вы увидите, что вы запускаете рекомпозицию, даже установив то же значение MyCounter
, в то время как значение по умолчанию не используется в Composable1.
getRandom color — это функция, которая визуально возвращает новый цвет при рекомпозиции наблюдателя.
fun getRandomColor() = Color(
red = Random.nextInt(256),
green = Random.nextInt(256),
blue = Random.nextInt(256),
alpha = 255
)
Вы даже можете принудительно выполнить рекомпозицию со значениями Int или String, если измените политику никогдаEquals, например
@Preview
@Composable
fun ForceRecompositionSample2() {
var counter by remember {
mutableStateOf(
value = 0,
policy = neverEqualPolicy()
)
}
Column(
modifier = Modifier.border(2.dp, getRandomColor()).fillMaxWidth().padding(8.dp)
) {
Button(
onClick = {
counter = 5
}
) {
Text("Update MyCounter")
}
Text("Value: ${counter}")
}
}
Если вы хотите использовать маршрут класса данных, вы можете обновить предыдущий пример как
data class MyCounter(
val value: Int,
val innerCounter: InnerCounter = InnerCounter()
)
data class InnerCounter(var value: Int = 0)
И обновить InnerCounter и запустить проверку рекомпозиции Composable2
@Composable
fun Composable1() {
var myCounter by remember {
mutableStateOf(MyCounter(0))
}
Column(
modifier = Modifier.border(2.dp, getRandomColor()).fillMaxWidth().padding(8.dp)
) {
Button(
onClick = {
val innerCounter = myCounter.innerCounter
val newValue = innerCounter.value + 1
innerCounter.value = newValue
myCounter = myCounter.copy()
}
) {
Text("Update MyCounter")
}
Text("Value: ${myCounter.value}")
}
}
@Composable
fun Composable2() {
var myCounter by remember {
mutableStateOf(
value = MyCounter(0)
)
}
Column(
modifier = Modifier.border(2.dp, getRandomColor()).fillMaxWidth().padding(8.dp)
) {
Button(
onClick = {
val innerCounter = myCounter.innerCounter
val newValue = innerCounter.value + 1
myCounter =
myCounter.copy(innerCounter = myCounter.innerCounter.copy(value = newValue))
}
) {
Text("Update MyCounter")
}
Text("Value: ${myCounter.value}")
}
}
Если вы имеете дело с классами данных, убедитесь, что параметры основного конструктора изменились в соответствии с политикой по умолчанию. Если вы хотите запускать одноразовые события, вы можете перейти к классу, который не имеет равных и реализации хэш-кода, или использовать другие предопределенные или свои собственные SnapshotMutationPolicy
Спасибо! referentialEqualityPolicy()
сделал свое дело! Не знал, что это такое. Также спасибо за хорошее объяснение.
Пожалуйста. Другой тоже должен работать. Позвольте мне показать на другом примере. Если вы измените параметры конструктора класса данных, как во втором, вы сможете запустить рекомпозицию.
Другой тоже работает. Спасибо
Есть ли какие-либо недостатки в использовании referentialEqualityPolicy
? Я имею в виду, за исключением того, что он срабатывает даже без изменения значения (что не должно происходить «случайно»)
нет, недостатков нет. Я использую их для разовых мероприятий. override fun equivalent(a: Any?, b: Any?) = a === b
— проверка рекомендаций. Хотя это зависит от вашей реализации. Если вы не хотите запускать рекомпозицию каждый раз при копировании, используйте второй маршрут. Вы можете проверить это руководство для получения дополнительных ссылок. github.com/SmartToolFactory/Jetpack-Compose-Tutorials/blob/…
По сути, они говорят, когда и с изменением того, что вы хотите, инициировать рекомпозицию. Например, политика NeverEqual возвращает false
, и поэтому любое значение, независимо от того, что вызывает рекомпозицию. Вы также можете реализовать свою с некоторой логикой, если хотите
Отлично! Большое спасибо @Thracian
Прежде всего, под "перекомпилировать" вы подразумеваете "перекомпоновать", так что исправьте вопрос. И присваиваете ли вы
activity
ненулевое значение в любой точке кода?