Jetpack Compose: Using list-size as key for remember not working as expected

I have a Person class, with a mutable list of email addresses.
In the following Composable, the list is displayed and I want to allow the addition of new and the deletion of existing email addresses.

Reduced to the minimum, the Composable looks as follows:

@Composable
fun EmailList(p: Person) {
    var emails = remember(p.emails.size) { mutableStateListOf(*p.emails.toTypedArray()) }
    Row() {
        Button(onClick = { p.emails.add(Email()) }) { Text("add") }
        Button(onClick = { p.emails.removeAt(0) }) { Text("delete") }
    }
    emails.forEach { email ->
        Text(email.toString())
    }
}

By passing the p.emails.size key to the remember function, I expected the local email list to be recomputed when a new list entry is added to or removed from p.emails.
But pressing the add or delete button does not lead to a recomposition. The list will not be updated.

The only way it works is to also add/remove the new item in the local list:

@Composable
fun EmailList(p: Person) {
    var emails = remember(p.emails.size) { mutableStateListOf(*p.emails.toTypedArray()) }
    Row() {
        Button(onClick = { Email().also{p.emails.add(it);emails.add(it) }}) { Text("add") }
        Button(onClick = { p.emails.removeAt(0); emails.removeAt(0) }) { Text("delete") }
    }
    emails.forEach { email ->
        Text(email.toString())
    }
}

But I don’t understand why this should be necessary.

What’s more, when I page through different person objects, the email list is updated if the previous person has a different number of email addresses, and it remains unchanged (which is of course not wanted but only a problem in my minimised example) if the number of emails is the same for the current and the previous people object.
So it seems that the p.list.size key has an effect if the incoming person object is changed, but it has no effect if only the size of the list is changed.

I know I could define the list within my Person class as a MutableStateList. But that looks like I’m messing up the business logic with UI specialities.

The problem is with the Person class. You didn’t provide it in the question but from looking at the remaining code it probably is something along the lines of:

data class Person(
    val emails: MutableList<Email>,
    ...
)

The issue is that you have a MutableList here. Compose can only properly work with mutable objects when they are wrapped in a MutableState. In this case Compose never sees the updates you make to the list and therefore doesn’t properly update the UI.

One solution is to create the list of emails with mutableStateListOf and changing the data class accordingly:

data class Person(
    val emails: SnapshotStateList<Email>,
)

Althogh this is working now, the proper way is to make this list immutable in the first place:

data class Person(
    val emails: List<Email>,
)

The only way the emails can then be changed is by creating an entire new Person instance:

p = p.copy(emails = p.emails + Email()) // adds a new Email() to p
p = p.copy(emails = p.emails - p.emails[0]) // removes the first Email from p

Since p is now changed this shouldn’t even be part of the EmailList composable anymore. The caller should handle it. This is accomplished by passing additional callbacks to EmailList:

@Composable
fun EmailList(
    p: Person,
    addEmail: (Email) -> Unit,
    deleteEmail: (Email) -> Unit,
) {
    var emails = remember(p.emails.size) { mutableStateListOf(*p.emails.toTypedArray()) }
    Row {
        Button(onClick = { addEmail(Email()) }) { Text("add") }
        Button(onClick = { deleteEmail(p.emails[0]) }) { Text("delete") }
    }
    emails.forEach { email ->
        Text(email.toString())
    }
}

The caller could then look like this:

@Composable
fun DisplayPerson() {
    var p: Person by remember { mutableStateOf(Person(...) }

    EmailList(
        p = p,
        addEmail = { p = p.copy(emails = p.emails + it) },
        deleteEmail = { p = p.copy(emails = p.emails - it) },
    )
}

The important part is to keep the modification logic of Person as close to its source as possible. In general, while state is only passed down the Compose hierarchy (here the Person object), events are passed up (here by calling the addEmail and deleteEmail callbacks). This paradigm is known as Unidirectional Data Flow (UDF) and leads to clean and simple composables that are a lot less dependent on each other.

In this case you can also simplify EmailList by only passing the list of emails because the rest of the Person object is never used here. You also don’t need a local list of emails in the composable any more:

@Composable
fun EmailList(
    emails: List<Email>,
    addEmail: (Email) -> Unit,
    deleteEmail: (Email) -> Unit,
) {
    Row {
        Button(onClick = { addEmail(Email()) }) { Text("add") }
        Button(onClick = { deleteEmail(emails[0]) }) { Text("delete") }
    }
    emails.forEach { email ->
        Text(email.toString())
    }
}

This is much cleaner now than your initial version. It is self-contained and not dependent on the mutable state of some external data structure (Person). It receives a simple list of Emails and whatever is needed to update the list is also passed as callbacks. The details are not important for this composable, that is something the data source that actually produced the list of emails (and, by extension, the Person object) should handle.

This composable can now better be tested, previewed and reused.

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật