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 шт):

Автор решения: h8leet

Чтение состояния 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. В новом варианте в модификатор просто передаётся ранее созданная лямбда.

→ Ссылка