当前位置: 首页 > news >正文

山东大学创新实训项目个人博客——第二篇

本周是项目开始开发的第三周,记录一下工作进度:

一、登陆界面重构

第一周初学时创建的登陆界面所有的代码都集成在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 : NetworkCallback<BaseResponseWithoutData> { 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): Call<BaseResponseWithoutData> } // NetworkUtil 封装网络请求 object NetworkUtil { fun <T> request(call: Call<T>, callback: NetworkCallback<T>) { call.enqueue(object : Callback<T> { override fun onResponse(call: Call<T>, response: Response<T>) { if (response.isSuccessful) { callback.onSuccess(response.body()!!) } else { callback.onFailure("Error: ${response.code()}") } } override fun onFailure(call: Call<T>, t: Throwable) { callback.onFailure(t.message ?: "网络错误") } }) } }

Retrofit 发起 HTTP GET 请求:/sms/send?phone=13800000001,服务器处理请求并返回响应,
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的适配。

http://www.cnnetsun.cn/news/1991725.html

相关文章:

  • 042、功耗管理与动态频率电压调整(DVFS)集成:从一次电池续航翻车说起
  • Neeshck-Z-lmage_LYX_v2应用案例:快速生成电商产品精修图与风格海报
  • 学术科研必备!最全EndNote期刊名缩写!直接导入就可使用
  • 终极指南:如何用RPFM工具将全面战争MOD开发效率提升500%
  • 如何永久保存微信聊天记录?WeChatMsg开源工具深度解析
  • JiYuTrainer:极域电子教室破解工具的完整使用指南与教程
  • 他写了十年 Linux,我白嫖了十年
  • 从2016年蓝桥杯单片机省赛真题出发,手把手教你用STC15F2K60S2实现一个简易温控计时器
  • 告别网盘限速困扰:LinkSwift直链下载助手终极指南
  • 除了certutil,Windows 11/10还有哪些查文件‘指纹’的招?PowerShell和第三方工具横评
  • 2025年网盘直链下载终极解决方案:八大网盘全速下载完全指南
  • ComfyUI-Impact-Pack深度解析:AI图像增强的三大核心技术实战
  • WarcraftHelper:魔兽争霸3终极兼容性修复工具完整指南
  • 告别隐藏入口!Android 11开发者模式里,一键开启状态栏显示秒数(附源码追踪)
  • AIGlasses_for_navigation效果惊艳:强逆光下盲道黄色条纹抗干扰分割效果
  • 别再只怪驱动了!树莓派Pico设备管理器不识别,可能是你的MicroPython程序‘卡死’了Flash
  • 如何快速获取网盘直链下载地址:8大平台完整解析指南
  • 告别手动输入!用高德地图JS API的AMap.CitySearch插件,5分钟搞定用户IP自动定位城市
  • AI写论文别担心!4款AI论文写作利器,轻松应对论文创作挑战
  • 从零到一:Stegsolve在CTF图像隐写中的核心功能实战解析
  • OpenClaw 微信通道搭建方法 三种部署模式详细讲解
  • VXLAN集中式网关实战:为什么你的eNSP模拟器跑不通跨子网?可能是这些原因
  • 别再傻傻分不清了!一文讲透onnx、onnxruntime、onnxruntime-gpu在Windows上的区别与安装选择
  • AGI访问权正在加速分层!SITS2026最新监测数据揭示:2026Q2起个人开发者API配额将缩减40%
  • csp信奥赛C++高频考点专项训练之贪心算法 --【排序贪心】:排队接水
  • G-Helper终极指南:如何让华硕笔记本性能翻倍还能省电
  • 告别图像畸变:手把手教你用HALCON的`gen_image_to_world_plane_map`做高精度视觉测量
  • 别再手动示教了!用RobotStudio的Offs函数搞定ABB机器人复杂码垛(附完整RAPID代码)
  • 从零到可视化监控:手把手教你用RocketMQ Console搭建消息队列管理后台(含版本兼容性处理)
  • 网络拓扑可视化进阶:给你的D3.js力导向图加上流量监控和告警功能