Compose: без собственной composable-функции код работает странно
Есть такой код:
@Composable
fun FragmentA(navController: NavController) {
val vm = viewModel(VM::class.java)
val a by vm.a.observeAsState("")
val b by vm.b.observeAsState("")
Column {
MyRandomBox()
MyBox(a)
MyBox2(b)
}
}
@Composable
fun MyRandomBox() {
Text(text = UUID.randomUUID().toString())
}
@Composable
fun MyBox(text: String) {
Box {
Log.e("!!!", "box-1")
Text(text = text)
}
}
@Composable
fun MyBox2(text: String) {
Box {
Log.e("!!!", "box-2")
Text(text = text)
}
}
class VM : ViewModel() {
val a = MutableLiveData<String>()
val b = MutableLiveData<String>()
init {
viewModelScope.launch {
(0..100).forEach {
a.value = "a-$it"
delay(1000)
b.value = "b-$it"
delay(1000)
}
}
}
}
Он работает отлично: каждую секунду меняется 1 из ЛайвДат и в логи выводится только тот Box, который поменялся.
А значение в самом первом Text как создалось рандомное при старте, так и висит (заново не рандомится).
Но если чуть-чуть изменить код, а именно вот так:
Column {
// MyRandomBox()
Text(text = UUID.randomUUID().toString())
MyBox(a)
MyBox2(b)
}
то теперь при изменении любой из ЛайвДат, в первый Text будет заново рандомиться текст
почему если обернуть этот код в собственную Composable-функцию все работает как надо, а без нее нет? (словно представление заново пересоздается)
UPD
так же непонятно следующее поведение:
прокинул действие onClick
@Composable
fun FragmentA(navController: NavController) {
val vm = viewModel(VM::class.java)
val a by vm.a.observeAsState("")
val b by vm.b.observeAsState("")
Column {
MyRandomBox()
MyBox(a) {
vm.rnd()
}
MyBox2(b)
}
}
@Composable
fun MyBox(text: String, onClick: () -> Unit) {
Box {
Log.e("!!!", "box-1")
Text(text = text,
modifier = Modifier
.clickable {
onClick()
}
)
}
}
LOG
10:04:12.770 E box-1
10:04:13.771 E box-1
10:04:13.772 E box-2
10:04:14.786 E box-1
10:04:15.787 E box-1
10:04:15.790 E box-2
10:04:16.786 E box-1
10:04:17.786 E box-1
10:04:17.787 E box-2
когда изменяется a логируется box-1
при изменении b логируется box-1 + box-2
но если заменить vm.rnd() на Log.e() то все будет работать как задумно.
почему так происходит?
Ответы (1 шт):
Чтение состояния a и b у вас происходит внутри Column в момент вызова функций MyBox и MyBox2. Т.к. Column - inline функция, то перестраивается весь FragmentA и в этом же фрагменте вызывается функция получения UUID.
Если выделяется отдельная функция MyRandomBox, то рекомпозиция FragmentA её не затрагивает, т.к. у этой функции (со своим scope в котором идёт получение UUID) нет состояния, которое меняется при рекомпозиции FragmentA.
Если переписать чтение состояния в функцию MyBox, то рекомпозиции фрагмента уже не будет, будет рекомпозиция только MyBox.
/* ... */
Column {
Text(text = UUID.randomUUID().toString())
val textProvider = { a }
MyBox(textProvider)
}
/* ... */
@Composable
fun MyBox(textProvider: () -> String) {
Box {
Log.e("!!!", "box-1")
Text(text = textProvider())
}
}
Подробнее (developer.android.com)
Просто для примера, чтобы понять где у функций scope, также сработает, если MyBox вообще не выделять в отдельную функцию и вызывать чтение внутри scope какой-нибудь не inline функции, например Surface:
Column {
Text(text = UUID.randomUUID().toString())
Surface {
Log.e("!!!", "box-1")
Text(text = a)
}
}
P.S.
Из-за таких неоднозначностей, я не люблю популярную функцию by. Предпочитаю объявлять непосредственно State и сразу видеть, когда читаю значение состояния.
val a = vm.a.observeAsState("")
val textProvider = { a.value }
MyBox(textProvider)
По поводу onClick:
ViewModel не считается stable классом и лямбда с вызовом нестабильного класса, не обёрнутая в remember { }, может создаваться заново при рекомпозиции scope в котором она создаётся (FragmentA у вас до сих рекомпозируется при изменениях a и b).
Если объявить отдельно лямбду вот так: val onClick = remember { { vm.rnd() } }
Затем использовать её вот так: MyBox(a, onClick), то изменение b уже не будет затрагивать MyBox.
Также, нужно поменять Modifier.clickable { onClick() } на Modifier.clickable(onClick = onClick).
Исходный вариант обозначает, что вы создаёте новую лямбду, которая вызывает ранее созданную лямбду onClick. В новом варианте в модификатор просто передаётся ранее созданная лямбда.