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

如何实现Jetpack Compose输入数据异步暂存后存入MongoDB?

Jetpack Compose 实现异步暂存数据后延迟写入MongoDB需求

我需要在Jetpack Compose中实现以下功能:

  • 创建姓名和电话号码两个TextField
  • 用户输入信息并点击确认按钮后,先异步暂存数据
  • 导航到仅显示“hello”的主屏幕后,再将数据保存到MongoDB

以下是我当前的代码,请提供实现方案和优化反馈:


输入界面代码

Column(
    modifier = Modifier
        .fillMaxSize()
        .weight(1f),
    verticalArrangement = Arrangement.Top,
    horizontalAlignment = Alignment.CenterHorizontally
) {

    OutlinedTextField(
        modifier = Modifier
            .fillMaxWidth(0.80f),
        value = name,
        onValueChange = onNameChanged,
        colors = OutlinedTextFieldDefaults.colors(
            focusedBorderColor = LightBlue,
        ),
        label = {
            Text(
                text = "Name",
                color = LightBlue,
                fontWeight = FontWeight.Bold
            )
        },
        placeholder = {
            Text(
                text = "Enter your name",
                color = Color.Black
            )
        },
        singleLine = true,
        leadingIcon = {
            Icon(
                imageVector = Icons.Default.Person,
                contentDescription = null
            )
        },
    )

    Spacer(modifier = Modifier.height(10.dp))

    OutlinedTextField(
        modifier = Modifier.fillMaxWidth(0.8f),
        value = phoneNumber,
        onValueChange = { phoneNumber = it },
        keyboardOptions = KeyboardOptions.Default.copy(
            keyboardType = KeyboardType.Number,
            imeAction = ImeAction.Done
        ),
        keyboardActions = KeyboardActions(
            onDone = {
                keyboardController?.hide()
            }
        ),
        singleLine = true,
        textStyle = MaterialTheme.typography.bodyMedium.copy(fontSize = 16.sp),
        colors = OutlinedTextFieldDefaults.colors(
            focusedBorderColor = Color.Blue,
            unfocusedBorderColor = Color.Gray
        ),
        label = {
            Text(
                text = "Phone Number",
                color = LightBlue,
                fontWeight = FontWeight.Bold
            )
        },
        placeholder = { Text(text = "Enter your telephone number") },

        leadingIcon = {
            Icon(
                imageVector = Icons.Default.Phone,
                contentDescription = null
            )
        },
    )

    Spacer(modifier = Modifier.height(10.dp))

Button(
    modifier = Modifier
        .wrapContentSize()
        .fillMaxWidth(0.8f)
        .height(80.dp)
        .padding(start = 20.dp, end = 20.dp, bottom = 40.dp)
        .align(Alignment.CenterHorizontally),
    colors = ButtonDefaults.buttonColors(ConfirmButtonColor),
    shape = RoundedCornerShape(10.dp),
    onClick = {
        if (name.isNotEmpty() && phoneNumber.isNotEmpty()) {
            navController.navigate(AuthScreen.GenderProfile.route)

        } else {
            Toast.makeText(context, "Not all fields are filled-in!", Toast.LENGTH_SHORT).show()
        }
    },
) {
    Text(
        text = "Confirm",
        color = Color.White,
    )
}

Person类

class Person : RealmObject {
    @PrimaryKey
    var _id: ObjectId = ObjectId.invoke()
    var owner_id: String = ""

    // Profile
    var name: String = ""
    var phoneNumber: Int = 0
}

MongoRepository接口

interface MongoRepository {
    fun configureTheRealm()
    fun getData(): Flow<List<Person>>
    suspend fun insertPerson(person: Person)
    suspend fun updatePerson(person: Person)
}

MongoDB实现类

object MongoDB : MongoRepository {
    private val app = App.create(APP_ID)
    private val user = app.currentUser
    private lateinit var realm: Realm

    init {
        configureTheRealm()
    }

    override fun configureTheRealm() {
        if (user != null) {
            val config = SyncConfiguration.Builder(
                user,
                setOf(Person::class)
            )
                .initialSubscriptions { sub ->
                    add(query = sub.query<Person>(query = "owner_id == $0", user.id))
                }
                .log(LogLevel.ALL)
                .build()
            realm = Realm.open(config)
        }
    }

    override fun getData(): Flow<List<Person>> {
        return realm.query<Person>().asFlow().map { it.list }
    }

    override suspend fun insertPerson(person: Person) {
        if (user != null) {
            realm.write {
                try {
                    copyToRealm(person.apply { owner_id = user.id })
                } catch (e: Exception) {
                    Log.d("MongoRepository", e.message.toString())
                }
            }
        }
    }

    override suspend fun updatePerson(person: Person) {
        realm.write {
            val queriedPerson =
                query<Person>(query = "_id == $0", person._id)
                    .first()
                    .find()
            if (queriedPerson != null) {
                queriedPerson.name = person.name
                queriedPerson.phoneNumber = person.phoneNumber
            } else {
                Log.d("MongoRepository", "Queried Person does not exist.")
            }
        }
    }
}

HomeViewModel

class HomeViewModel : ViewModel() {
    var objectId = mutableStateOf("")
    var data = mutableStateOf(emptyList<Person>())

    // Profile + PickGender screen
    var name = mutableStateOf("")
    var phoneNumber = mutableStateOf(0)
    
    init {
        viewModelScope.launch {
            MongoDB.getData().collect {
                data.value = it
            }
        }
    }

    fun updateObjectId(id: String) {
        this.objectId.value = id
    }

    fun updateName(name: String) {
        this.name.value = name
    }

    fun updatePhoneNumber(phoneNumber: Int) {
        this.phoneNumber.value = phoneNumber
    }

    // The phoneNumber and Age will be set in string since I don't know how to set it in Int form
    fun insertPerson() {
        viewModelScope.launch(Dispatchers.IO) {
            if (
                name.value.isNotEmpty() &&
                phoneNumber.value.toString().isNotEmpty()
            ) {
                MongoDB.insertPerson(person = Person().apply {
                    name = this@HomeViewModel.name.value
                    phoneNumber = this@HomeViewModel.phoneNumber.value
                })
            }
        }
    }

    fun updatePerson() {
        viewModelScope.launch(Dispatchers.IO) {
            if (objectId.value.isNotEmpty()) {
                MongoDB.updatePerson(person = Person().apply {
                    _id = ObjectId(hexString = this@HomeViewModel.objectId.value)
                    name = this@HomeViewModel.name.value
                    phoneNumber = this@HomeViewModel.phoneNumber.value
                })
            }
        }
    }
}

实现方案

1. 导航前异步暂存数据

修改确认按钮的点击逻辑,先将输入数据同步到ViewModel暂存,再执行导航:

onClick = {
    if (name.isNotEmpty() && phoneNumber.isNotEmpty()) {
        // 暂存姓名
        homeViewModel.updateName(name)
        // 处理手机号类型转换(带格式校验)
        runCatching {
            homeViewModel.updatePhoneNumber(phoneNumber.toInt())
        }.onFailure {
            Toast.makeText(context, "请输入有效的数字电话号码", Toast.LENGTH_SHORT).show()
            return@onClick
        }
        // 隐藏键盘并导航
        keyboardController?.hide()
        navController.navigate(AuthScreen.GenderProfile.route)
    } else {
        Toast.makeText(context, "请填写所有字段!", Toast.LENGTH_SHORT).show()
    }
}

2. 主屏幕触发MongoDB写入

在主屏幕Composable中,用LaunchedEffect监听页面进入事件,触发数据写入操作:

@Composable
fun HelloScreen(homeViewModel: HomeViewModel = viewModel()) {
    // 页面首次进入时执行数据写入
    LaunchedEffect(Unit) {
        homeViewModel.insertPerson()
    }

    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "hello", fontSize = 24.sp)
    }
}

3. 修复现有代码核心问题

(1)手机号类型适配

手机号用Int存储存在长度溢出风险(11位手机号超过Int最大值),建议将Person类的phoneNumber改为String类型:

// 修改Person类
var phoneNumber: String = ""

// 同步修改ViewModel和输入逻辑
class HomeViewModel : ViewModel() {
    var phoneNumberStr = mutableStateOf("")
    
    fun updatePhoneNumberStr(str: String) {
        this.phoneNumberStr.value = str
    }

    fun insertPerson() {
        viewModelScope.launch(Dispatchers.IO) {
            if (name.value.isNotEmpty() && phoneNumberStr.value.isNotEmpty()) {
                MongoDB.insertPerson(person = Person().apply {
                    name = this@HomeViewModel.name.value
                    phoneNumber = this@HomeViewModel.phoneNumberStr.value
                    owner_id = MongoDB.user?.id ?: ""
                })
            }
        }
    }
}

(2)Realm初始化优化

避免在init块中依赖可能为空的currentUser,改为延迟初始化:

object MongoDB : MongoRepository {
    private val app = App.create(APP_ID)
    private var user: User? = app.currentUser
    private var realm: Realm? = null

    override fun configureTheRealm() {
        user = app.currentUser
        if (user != null && realm == null) {
            val config = SyncConfiguration.Builder(
                user!!,
                setOf(Person::class)
            )
                .initialSubscriptions { sub ->
                    add(query = sub.query<Person>("owner_id == $0", user!!.id))
                }
                .log(LogLevel.ALL)
                .build()
            realm = Realm.open(config)
        }
    }

    override suspend fun insertPerson(person: Person) {
        val currentRealm = realm ?: return
        val currentUser = user ?: return
        currentRealm.write {
            try {
                copyToRealm(person.apply { owner_id = currentUser.id })
            } catch (e: Exception) {
                Log.d("MongoRepository", e.message.toString())
            }
        }
    }
}

(3)ViewModel数据流收集优化

用repeatOnLifecycle避免后台持续收集流导致的内存泄漏:

init {
    viewModelScope.launch {
        repeatOnLifecycle(Lifecycle.State.STARTED) {
            MongoDB.getData().collect {
                data.value = it
            }
        }
    }
}

优化反馈

  • 手机号建议用String存储,避免长度溢出和格式限制问题
  • 输入框可添加手机号格式校验(如长度、开头数字规则)
  • 所有ViewModel的MutableState建议添加private set,限制外部直接修改,保证数据一致性
  • Realm操作需增加网络异常处理(如同步失败时的重试逻辑)
  • 可添加加载状态提示(如写入DB时显示加载动画)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 09:14:58