本周是项目开始开发的第三周记录一下工作进度一、登陆界面重构第一周初学时创建的登陆界面所有的代码都集成在LoginActivity中包括页面组件、viewModel、Activity等模块重构后的login界面采用MVVM架构由LoginScreen、LoginActivity、LoginViewModel和LoginUiState四个文件组成。其中Activity负责导航、窗口管理、系统交互ViewModel持有并处理 UI 状态承载业务逻辑UiState定义状态的不可变数据模型Screen纯 UI 渲染Compose绑定修改某一块时不影响其他块。功能示例发送验证码的完整流程场景描述用户在登录界面输入手机号 13800000001然后点击发送验证码按钮。第 1 步UI 层接收用户输入//LoginScreen.kt用户在 PhoneInput 组件中输入手机号 PhoneInput( value uiState.phoneNumber, // 从状态读取显示 onValueChange viewModel::updatePhone, // 输入变化时调用 ViewModel error uiState.phoneError // 显示错误信息 )用户每输入一个字符onValueChange都会触发调用viewModel.updatePhone(13800000001)第 2 步ViewModel 更新状态//LoginViewModel.kt fun updatePhone(phone: String) { _uiState.update { it.copy(phoneNumber phone) } }ViewModel 接收到新的手机号 13800000001使用 MutableStateFlow.update() 更新 _uiState创建新的 LoginUiState 对象不可变数据类状态从 phoneNumber 变为 phoneNumber 13800000001。第 3 步状态自动通知 UI 刷新//LoginScreen.kt val uiState by viewModel.uiState.collectAsState()collectAsState() 自动观察到 _uiState 的变化Compose 框架检测到状态改变自动重组Recompose相关的 UI 组件使手机输入框中显示最新的手机号。第 4 步用户点击发送验证码按钮//LoginScreen.kt SmsLoginSection( verifyCode uiState.verifyCode, countdown uiState.countdown, isLoading uiState.isLoading, onVerifyCodeChange viewModel::updateVerifyCode, onSendCode { viewModel.sendVerifyCode(context) }, // ← 点击触发 onToggleMode viewModel::toggleLoginMode, error uiState.codeError, codeMessage uiState.codeMessage )用户点击发送按钮调用 viewModel.sendVerifyCode(context)传入 context 用于后续可能的 Toast 提示第 5 步ViewModel 执行业务逻辑//LoginViewModel.kt fun sendVerifyCode(context: Context) { val phone _uiState.value.phoneNumber // ① 获取当前手机号 // ② 清空之前的错误和消息 _uiState.update { it.copy( phoneError , codeMessage ) } // ③ 校验手机号是否为空 if (phone.isBlank()) { _uiState.update { it.copy(phoneError 请输入手机号) } return } // ④ 校验手机号格式 if (!NetworkUtil.validatePhone(phone)) { _uiState.update { it.copy(phoneError 请输入正确的手机号) } return } // ⑤ 发起网络请求 NetworkUtil.request( ApiService.api.sendCode(phone), object : NetworkCallbackBaseResponseWithoutData { override fun onSuccess(data: BaseResponseWithoutData) { if (data.code 0) { startCountdown() // 启动倒计时 _uiState.update { it.copy(codeMessage 验证码已发送) } } else { _uiState.update { it.copy(phoneError data.message?.ifEmpty { 发送失败 } ?: 发送失败) } } } override fun onFailure(error: String) { _uiState.update { it.copy(phoneError 网络不佳请稍后重试) } } } ) }依次进行了以下事务① 读取状态从 _uiState.value 获取当前手机号 13800000001② 重置错误清空之前的错误提示避免干扰③ 空值校验如果手机号为空更新 phoneError 并提前返回④ 格式校验调用工具类验证手机号格式正则表达式匹配⑤ 网络请求调用 ApiService.api.sendCode(phone) 创建网络请求通过 NetworkUtil.request() 封装的请求方法发送传入回调接口处理成功/失败结果。第 6 步网络层执行请求//ApiService.kt NetworkUtil.kt //ApiService 定义接口 interface ApiInterface { GET(sms/send) fun sendCode(Query(phone) phone: String): CallBaseResponseWithoutData } // NetworkUtil 封装网络请求 object NetworkUtil { fun T request(call: CallT, callback: NetworkCallbackT) { call.enqueue(object : CallbackT { override fun onResponse(call: CallT, response: ResponseT) { if (response.isSuccessful) { callback.onSuccess(response.body()!!) } else { callback.onFailure(Error: ${response.code()}) } } override fun onFailure(call: CallT, t: Throwable) { callback.onFailure(t.message ?: 网络错误) } }) } }Retrofit 发起 HTTP GET 请求/sms/send?phone13800000001服务器处理请求并返回响应NetworkUtil 根据响应状态调用相应的回调方法。第 7 步A请求成功的处理流程ViewModel 处理成功override fun onSuccess(data: BaseResponseWithoutData) { if (data.code 0) { startCountdown() // ← 启动倒计时 _uiState.update { it.copy(codeMessage 验证码已发送) } } }启动倒计时private fun startCountdown() { countdownJob?.cancel() // 取消之前的倒计时任务 countdownJob viewModelScope.launch { for (i in 60 downTo 0) { // 从 60 秒开始倒数 _uiState.update { it.copy(countdown i) } delay(1000) // 每秒更新一次 } } }UI层反应// LoginScreen.kt 中的发送按钮 IconButton( onClick onSendCode, enabled countdown 0 !isLoading // 倒计时期间禁用 ) { if (countdown 0) { Text(text ${countdown}秒) // 显示倒计时 } else { Image(painter painterResource(id R.drawable.varifycode_send)) } }用户将看到蓝色提示文字验证码已发送而且发送按钮变成灰色显示 60秒、59秒...倒计时结束后按钮恢复可点击状态。第 7 步B请求失败的处理流程假设服务器返回失败或网络错误override fun onFailure(error: String) { val errorMessage when { error.contains(Response body is null) - 服务器响应异常 error.startsWith(Error:) - 服务器错误请稍后重试 else - 网络不佳请稍后重试 } _uiState.update { it.copy(phoneError errorMessage) } }UI 层的反应// LoginScreen.kt 中的错误提示区域 Box(modifier Modifier.fillMaxWidth().height(20.dp)) { if (error.isNotEmpty()) { Text( text error, color colorResource(id R.color.attention), // 红色 fontSize 12.sp ) } }手机号输入框下方将显示红色错误提示网络不佳请稍后重试用户可以重新点击发送按钮。整个过程中UI 层只负责渲染和收集用户输入ViewModel 处理所有验证、网络请求、状态转换State 作为唯一的数据源保证 UI 和数据的一致性。MVVM架构清晰便于测试和维护将应用于之后所有页面的搭建工作。二、搭建主界面使用前文提到的MVVM架构构建主页面创建的文件如下主界面的结构为底部一个导航栏用于切换对话界面和“我的”界面。对话界面则分为三个阶段如上图所示问诊、导航、康复。由于三个阶段的对话界面各有侧重所以需要分别创建自己的对话界面文件但是后端的接口尚未完成暂时使用一样的三个文件和写死的模拟数据模拟三个阶段的对话界面如下图所示。目前的按钮逻辑还有问题顶部的三个按钮应该在ConversationScreen中重新布置中间的聊天信息和输入框在各自的文件中描述。也就是说这些文件在项目目录中的相对路径结构应与其所描述的界面组件在视图层次结构中的包含关系即父组件包裹子组件的嵌套顺序保持一致。个人信息和健康画像部分的页面后端的Api已经实现这里先说明页面的搭建大致的结构与效果如下Column (垂直滚动) ├── 标题 个人中心 ├── 错误提示可选 ├── 加载指示器 / 内容区域 │ ├── 用户信息卡片 (Card) │ │ ├── 头像 (AsyncImage/Image) │ │ ├── 用户名 手机号 │ │ ├── 编辑/保存按钮 │ │ └── 账号状态 │ └── 健康画像卡片 (Card) │ ├── 年龄、性别、体重横向排列 │ ├── 过敏史 │ ├── 既往病史 │ ├── 家族病史 │ └── 保存按钮编辑模式显示该界面以双卡片布局展示用户信息卡片 健康画像卡片支持查看/编辑双模式切换利用 LaunchedEffect 在初始化时加载数据并同步到本地编辑状态使用 StateFlow collectAsState 实现响应式数据绑定结合 Coil 库加载网络头像并通过条件渲染处理加载状态、错误提示和编辑模式的动态切换最终形成一个具备完整 CRUD 功能的用户资料管理页面。三、适配真正的后端api本周后端的小伙伴已经完成了登录注册请求接口以及个人信息和健康的查看和修改接口我将后端在本地跑起来后进行了适配。首先修改baseurl为本地后端地址private const val BASE_URL http://10.0.2.2:8080在 WenKangApplication.kt 中完成全局初始化确保整个应用生命周期内该服务处于就绪状态//WenKangApplication.kt class WenKangApplication : Application() { override fun onCreate() { super.onCreate() ApiService.init(this) // 传入 Context } }OkHttp和Retrofit部分已经完成再根据后端返回的信息适配Response模板比如LoginResponse后端小伙伴给的返回模板为{ code: 0, message: login success, data: { userId: 1, username: android_user_01, phone: 13800000001, accessToken: 1f0c2d65e0d347ec8e7f72ea4e4f1fcb, tokenType: Bearer, expiresAt: 2026-04-20T17:00:00 }, requestId: null }那么LoginResponse适配为package com.example.wenkang.data.model.response data class LoginResponse( val code: String, val message: String, val data: LoginData, val requestId: String? null ) data class LoginData( val userId: Int, val username: String, val phone: String, val accessToken: String, val tokenType: String, val expiresAt: String )同理调整相关请求需要的参数以及路径后安卓前端就可以与后端交互了。测试登录功能和个人信息界面信息正常显示表示适配成功接下来的工作1.完善conversation界面以及美化整个主界面使其色调与登录界面统一、看上去更有质感2.完成注册界面完成对后端注册api的适配。