You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Cloud Firestore数据变更后如何实现Jetpack Compose UI重组?

Cloud Firestore数据变更后Jetpack Compose UI未触发重组问题

当Cloud Firestore数据库中的数据发生变更(例如新增条目或修改条目的read值)时,即便已将列表设置为mutableState,Jetpack Compose UI仍不会触发重组。


ViewModel层:从Firebase获取数据

private val _booksFromFB = mutableStateOf(listOf<BookFB>())
val booksFromFB = _booksFromFB

init {
    getBooksFromFB()
}

fun getBooksFromFB() {
    viewModelScope.launch {
        _isLoading.value = true
        _booksFromFB.value = repository.getBooksFromFB().data!!

        if (_booksFromFB.value.isNotEmpty())
            _isLoading.value = false
    }
}

仓库层:从Firebase获取数据

override suspend fun getBooksFromFB(): Resource<List<BookFB>> {
    return try {
        Resource.Loading(true)

        val response = queryBook.get().await().documents.map { documentSnapshot ->
            documentSnapshot.toObject(BookFB::class.java)!!
        }
        if (response.isNotEmpty())
            Resource.Loading(false)
        Resource.Success(response)
        
    } catch (exception: FirebaseFirestoreException) {
        Resource.Error(exception.message.toString())
    }
}

UI层:获取并使用列表(LazyColumn内)

val userBooks = viewModel.booksFromFB.value.filter {
    it.userId == currentUser?.uid.toString()
}

userBooks的进一步使用

CurrentlyReadingSection(
    context = context,
    navController = navController,
    userBooks = userBooks.filter { it.read == false },
    commonViewModel = commonViewModel
)

CurrentlyReadingSection组件

LazyRow() {
    items(userBooks) { book ->
        BookRow(
            modifier = Modifier.padding(4.dp),
            onItemClicked = { }
        ) {
            val isExpanded = remember {
                mutableStateOf(false)
            }

            val isRead = rememberSaveable {
                mutableStateOf(false)
            }

            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .background(AppColors.mBackgroundSec),
                contentAlignment = BottomCenter
            ) {

                Row {

                    AnimatedVisibility(visible = isExpanded.value) {
                        Box(
                            modifier = Modifier
                                .fillMaxSize()
                                .clickable {
                                    isExpanded.value = !isExpanded.value
                                },
                            contentAlignment = Center
                        ) {
                            Column(
                                modifier = Modifier.fillMaxSize(),
                                verticalArrangement = Arrangement.Center,
                                horizontalAlignment = Alignment.CenterHorizontally
                            ) {
                                MyButton(
                                    modifier = Modifier
                                        .fillMaxWidth(0.7f)
                                        .fillMaxHeight(0.2f)
                                        .clip(RoundedCornerShape(12.dp)),
                                    text = "READ",
                                    fontSize = 12,
                                    contentPadding = 8
                                ) {
                                    isRead.value = !isRead.value
                                    isExpanded.value = !isExpanded.value
                                    viewModel.updateBook(context = context, book = book, isRead = isRead.value)
                                }
                                Spacer(modifier = Modifier.height(4.dp))

                                MyButton(
                                    modifier = Modifier
                                        .fillMaxWidth(0.7f)
                                        .fillMaxHeight(0.24f)
                                        .clip(RoundedCornerShape(12.dp)),
                                    text = "RATE",
                                    fontSize = 12,
                                    contentPadding = 8
                                ) {
                                    commonViewModel.currentBook.value = book
                                    navController.navigate(Screen.Rate.route)
                                }
                            }

                        }

                    }

                    AsyncImage(
                        modifier = Modifier
                            .fillMaxSize()
                            .clip(RoundedCornerShape(12.dp))
                            .clickable { isExpanded.value = !isExpanded.value },
                        model = ImageRequest.Builder(context)
                            .data(
                                if (isValid(book.image))
                                    book.image
                                else
                                    com.example.read.R.drawable.imagenotfound
                            )
                            .crossfade(true)
                            .build(),
                        contentScale = ContentScale.FillBounds,
                        contentDescription = "Book image"
                    )
                }

                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .fillMaxHeight(0.1f)
                        .background(if (book.read!!) AppColors.mGreen else AppColors.mRed)
                )
            }
        }
    }
}

BookRow组件

@Composable
fun BookRow(
    modifier: Modifier = Modifier,
    book: BookFB = BookFB(),
    category: MyCategory = MyCategory("", ""),
    shapeDp: Int = 12,
    heightSize: Int = 170,
    widthSize: Int = 120,
    isForYou: Boolean = false,
    isYourCollection: Boolean = false,
    onItemClicked: () -> Unit = {},
    content: @Composable() () -> Unit,
) {

Column(
    horizontalAlignment = Alignment.CenterHorizontally,
    modifier = if (isYourCollection) Modifier.widthIn(max = widthSize.dp) else Modifier.fillMaxWidth()
) {
    Surface(
        shape = RoundedCornerShape(shapeDp.dp),
        elevation = 4.dp,
        modifier = modifier.border(2.dp, color = Color.Black, shape = RoundedCornerShape(shapeDp.dp))
    ) {
        Box(
            modifier = Modifier
                .clickable {
                    onItemClicked()
                }
                .height(heightSize.dp)
                .width(widthSize.dp)
                .background(AppColors.mBackgroundSec))
        {
            content()
        }
    }

    if (isYourCollection) {

            Text(
                modifier = Modifier.padding(top = 4.dp),
                text = book.title.toString(),
                fontWeight = FontWeight.Bold,
                fontStyle = FontStyle.Italic,
                fontSize = 14.sp,
                color = AppColors.mTextWhite,
                textAlign = TextAlign.Center,
                maxLines = 3,
                overflow = TextOverflow.Clip
            )

    }

    if (isForYou) {
        Text(
            modifier = Modifier.padding(top = 4.dp),
            text = category.name,
            fontWeight = FontWeight.Bold,
            fontStyle = FontStyle.Italic,
            fontSize = 18.sp,
            color = AppColors.mTextWhite,
            textAlign = TextAlign.Center,
            maxLines = 3,
            overflow = TextOverflow.Clip
        )
    }
}

首页进度条

if (viewModel.isLoading.value) {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        CircularProgressIndicator(color = AppColors.mMain)
    }
} else {
    content..
}

问题原因与解决方案

  1. 未监听Firestore实时数据变更
    当前代码仅通过get().await()一次性拉取数据,不会感知后续数据库变更。需改用Firestore的addSnapshotListener实时监听功能,数据变化时自动更新ViewModel中的mutableState。

  2. updateBook后未同步本地数据
    调用viewModel.updateBook后,未更新本地_booksFromFB的值,UI无法感知数据变化。若使用一次性拉取,需在更新完成后重新调用getBooksFromFB();若用实时监听,更新后监听器会自动同步最新数据。

  3. 优化State暴露方式
    将val booksFromFB = _booksFromFB改为val booksFromFB: State<List<BookFB>> = _booksFromFB,确保UI能正确观察State变化。

示例修改:仓库层添加实时监听

// 仓库层新增实时监听方法
fun observeBooksFromFB(onResult: (Resource<List<BookFB>>) -> Unit) {
    queryBook.addSnapshotListener { snapshot, error ->
        if (error != null) {
            onResult(Resource.Error(error.message.toString()))
            return@addSnapshotListener
        }
        snapshot?.let {
            val response = it.documents.map { doc -> doc.toObject(BookFB::class.java)!! }
            onResult(Resource.Success(response))
        }
    }
}

// ViewModel层初始化时启动监听
init {
    repository.observeBooksFromFB { resource ->
        when(resource) {
            is Resource.Success -> {
                _booksFromFB.value = resource.data!!
                _isLoading.value = false
            }
            is Resource.Error -> {
                // 处理错误逻辑
                _isLoading.value = false
            }
            is Resource.Loading -> {
                _isLoading.value = resource.loading
            }
        }
    }
}

修改updateBook逻辑

fun updateBook(context: Context, book: BookFB, isRead: Boolean) {
    viewModelScope.launch {
        repository.updateBookReadStatus(book.id, isRead)
        // 若未使用实时监听,需重新拉取数据刷新UI
        getBooksFromFB()
    }
}

内容的提问来源于stack exchange,提问作者deja

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 02:10:16