Jetpack Compose开发一个Android WiFi导航应用
在以前的一篇文章构建一个WIFI室内定位系统_wifi定位系统-CSDN博客中,我介绍了如何用Android来测量WiFi信号,上传到服务器进行分析后,生成室内不同地方的WiFi指纹,从而帮助进行室内导航。当时我是用的HTML5+的技术来快速开发一个Android的应用,可以看到HTML5+能很便利的用我们熟悉的Web技术来进行开发,而不需要了解原生Android应用繁琐的开发知识。但是Android原生应用也有其优势,尤其在性能上以及一些Android核心功能的调用上。尤其是Google推出了新的Jetpack Compose用于构建原生 Android 界面的新工具包,它使用更少的代码、强大的工具和直观的 Kotlin API,可以帮助简化并加快 Android 界面开发,打造生动而精彩的应用,让我们能更快速、更轻松地构建 Android 界面以及更加便利进行原生应用的开发。因此这次我也用Jetpack Compose来重构了我之前写的Wifi信号测量的应用。
WiFi测量主界面
界面UI设计
Jepack Compose的精髓在于用可组合函数来声明一个UI界面。界面是不可变的,在绘制后无法进行更新。您可以控制的是界面的状态。每当界面的状态发生变化时,Compose 都会重新创建界面数更新的部分。
在Android studio里面新建一个项目,选择Empty activity类型,在这种类型的项目,res资源文件夹没有layout这个子文件夹,因为这种类型已经是用新的Compose方式来进行布局了,不再采用以前的XML方式来定义布局。
新建一个名为WifiMeasure的class,在里面定义一个MeasureScreen的Composable函数,用来声明我们的主界面,代码如下:
@Composable
fun MeasureScreen() {Column(modifier = Modifier.padding(all = 8.dp)) {Text(text = stringResource(R.string.screen_title),style = MaterialTheme.typography.titleLarge,)Spacer(modifier = Modifier.height(8.dp))val imageModifier = Modifier.height(150.dp).fillMaxWidth().border(BorderStroke(1.dp, Color.Black))Image(painter = painterResource(id = R.drawable.indooratlas),contentDescription = null,contentScale = ContentScale.FillWidth,modifier = imageModifier)Spacer(modifier = Modifier.height(8.dp))OutlinedTextField(value = "",onValueChange = { },label = { Text(text = stringResource(R.string.label_position_name), style = MaterialTheme.typography.bodyMedium)},modifier = Modifier.fillMaxWidth())Spacer(modifier = Modifier.height(8.dp))TextField(value = "0.0",onValueChange = { },label = { Text(text = stringResource(R.string.label_current_angle), style = MaterialTheme.typography.bodyMedium)},readOnly = true,modifier = Modifier.fillMaxWidth())Spacer(modifier = Modifier.height(16.dp))TextButton(onClick = { },shape = RectangleShape,contentPadding = PaddingValues(16.dp),modifier = Modifier.fillMaxWidth().border(1.5.dp, MaterialTheme.colorScheme.secondary, CircleShape)) {Texttext = "Measure",style = MaterialTheme.typography.bodyMedium)}}
}
这里采用了Column来作为一个垂直布局,在里面放置了Text, Image等组件来显示界面。我们可以添加一个Preview的函数,这样在进行代码改动的时候,我们就可以马上在Android studio的Design里面看到UI的改动了,非常方便,代码如下:
@Preview(showBackground = true)
@Composable
fun PreviewMeasureScreen() {MeasureScreen()
}
这个界面的效果如下图:
在要进行测量的时候,我们需要首先输入当前位置的名字,同时手机会实时显示当前的朝向,因为不同的朝向对Wifi信号的测量也有影响。然后当我们点击Measure这个按钮的时候,就会把当前这个位置的Wifi信号信息测量出来。
在MainActivity的onCreate方法的setContent中直接调用刚才我们定义的函数MeasureScreen()即可在APP中显示我们的界面。
定义ViewModel保存UI状态
现在在输入框中输入位置的名字,可以看到输入无法显示,这是因为在OutlinedTextField里面我们没有定义value是一个可观测状态,因此Compose组件无法进行重组更新。为此我们需要定义一个ViewModel来保存UI的状态。新建一个WifiMeasureViewModel的class,代码如下:
class WifiMeasureViewModel : ViewModel() {var positionName by mutableStateOf("")private setfun updatePositionName(name: String) {positionName = name}
这个类里面定义了一个positionName的mutableStateOf的State容器,通过一个update方法来更新数值。
修改WifiMeasure这个函数,传入这个ViewModel进行绑定,这里的viewModel()是一个生命周期的组件,可以使得ViewModel与Compose UI生命周期同步存在。
@Composable
fun MeasureScreen(measureViewModel: WifiMeasureViewModel = viewModel()
)
修改OutlinedTextField,现在可以正常输入文字了,如果我们旋转手机,可以看到之前输入的文字能保留下来。
OutlinedTextField(value = measureViewModel.positionName,onValueChange = { measureViewModel.updatePositionName(it) },label = { Text(text = stringResource(R.string.label_position_name), style = MaterialTheme.typography.bodyMedium)},modifier = Modifier.fillMaxWidth())
获取手机朝向
因为手机的朝向对于Wifi测量会有影响,因此通常我们会在同一个地点测试不同朝向的WiFi信号并记录下来。我们需要在APP上实时显示当前的朝向,这就需要用到手机提供的传感器数据。
传统的Android应用的方法是,在Activity类里面继承SensorEventListener并重写相应的方法来实现。但是在Composable function里面如何实现,在官网上并没有介绍。我的做法是先定义一个新的类继承SensorEventListener,例如我们新建一个SensorDataManager的类,代码如下:
class SensorDataManager(context: Context): SensorEventListener {private val sensorManager by lazy {context.getSystemService(Context.SENSOR_SERVICE) as SensorManager}private var accelerometerReading = FloatArray(3)private var magnetometerReading = FloatArray(3)private var rotationMatrix = FloatArray(9)private var orientationAngles = FloatArray(3)val data: Channel<Float> = Channel(Channel.UNLIMITED)fun init() {Log.d("SensorDataManager", "init")val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)val magnetometer = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_UI)sensorManager.registerListener(this, magnetometer, SensorManager.SENSOR_DELAY_UI)}override fun onSensorChanged(event: SensorEvent) {if (event.sensor.type == Sensor.TYPE_ACCELEROMETER) {System.arraycopy(event.values, 0, accelerometerReading, 0, accelerometerReading.size)} else if (event.sensor.type == Sensor.TYPE_MAGNETIC_FIELD) {System.arraycopy(event.values, 0, magnetometerReading, 0, magnetometerReading.size)}SensorManager.getRotationMatrix(rotationMatrix,null,accelerometerReading,magnetometerReading)SensorManager.getOrientation(rotationMatrix, orientationAngles)data.trySend(orientationAngles[0])}override fun onAccuracyChanged(p0: Sensor?, p1: Int) {}fun cancel() {Log.d("SensorDataManager", "cancel")sensorManager.unregisterListener(this)}
}
解释一下代码,在init函数中获取accelerometer和maganetic这两个传感器并注册listener,根据官网的介绍,推荐用这两个传感器数据来获取准确的朝向。在重写的onSensorChanged方法中,根据这两个传感器的数据计算朝向,并通过Channel把协程的数据发送出去。最后在cancel中取消listener注册。
现在修改一下MeasureScreen这个函数,增加以下代码:
val context = LocalContext.currentval scope = rememberCoroutineScope()var angle by remember { mutableStateOf<Float>(0f) }DisposableEffect(Unit) {val dataManager = SensorDataManager(context)dataManager.init()val job = scope.launch {dataManager.data.receiveAsFlow().onEach { angle = it }.collect()}onDispose {dataManager.cancel()job.cancel()}}
这里用到了Compose里面的附带效应Side effects,按照官网的解释附带效应是指发生在可组合函数作用域之外的应用状态的变化。DisposableEffect可以在键发生变化或可组合项退出组合后进行清理。因此我采用DiposableEffect(Unit)来监控这个MeasureScreen函数,完成初始化和清除Sensor Listener的工作。
修改一下显示朝向角度的TextField,设置其Value
TextField(value = angle.toString(),onValueChange = { },label = { Text(text = stringResource(R.string.label_current_angle), style = MaterialTheme.typography.bodyMedium)},readOnly = true,modifier = Modifier.fillMaxWidth())
现在这个测量页面可以正常工作了。
WiFi测量报告页面
增加导航
当点击测量页面的Measure按钮的时候,应该能跳转到另一个页面,显示WiFi的测量结果。要实现导航的功能,我们需要用到Navigation组件。新增一个名为Navigation的class,代码如下:
object Destinations {const val MEASURE_ROUTE = "measure"const val REPORT_ROUTE = "report/{positionName}"
}@Composable
fun AppNavHost(modifier: Modifier = Modifier,navController: NavHostController = rememberNavController(),startDestination: String = MEASURE_ROUTE
) {NavHost(navController = navController,startDestination = startDestination) {composable(MEASURE_ROUTE) {MeasureScreen(navController = navController)}composable(REPORT_ROUTE,arguments = listOf(navArgument("positionName") {type = NavType.StringType},)) { backStackEntry ->val positionName = backStackEntry.arguments?.getString("positionName")WifiMeasureReport(positionName)}}
}
这里定义了两个route,分别对应APP的两个页面。在跳转到测量报告页面的时候,route会带上positionName这个参数。
修改MainActivity,把setContent的内容替换为调用AppNavHost(),如以下代码:
class MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {WifiPositionTheme {// A surface container using the 'background' color from the themeSurface(modifier = Modifier.fillMaxSize(),color = MaterialTheme.colorScheme.background) {AppNavHost()}}}}
}
WiFi扫描服务
增加一个Wifi扫描的服务,实现对Wifi信号的测量。新增一个WifiScanService的class,代码如下:
class WifiScanService(context: Context) {private val wifiManager by lazy {context.getSystemService(Context.WIFI_SERVICE) as WifiManager}private val context: Context = contextval data: Channel<List<WifiMeasureData>> = Channel(Channel.UNLIMITED)private val wifiScanReceiver = object : BroadcastReceiver() {override fun onReceive(context: Context, intent: Intent) {val success = intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false)if (success) {scanSuccess()} else {scanFailure()}}}fun init() {val intentFilter = IntentFilter()intentFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)context.registerReceiver(wifiScanReceiver, intentFilter)val success = wifiManager.startScan()if (!success) {scanFailure()}}private fun scanFailure() {Log.d("WIFI", "Scan failure")}@SuppressLint("MissingPermission")private fun scanSuccess() {val results = wifiManager.scanResultsif (!results.isNullOrEmpty()) {val wifiMeasureData = results.map {WifiMeasureData(it.BSSID,it.level)}data.trySend(wifiMeasureData)}}fun cancel() {context.unregisterReceiver(wifiScanReceiver)}
}
定义一个WifiMeasureData的数据class,保存测量数据
data class WifiMeasureData (val bssId: String,val signalStrength: Int
)
另外,要开启WiFi测量,还需要申请相应的权限,在AndroidManifest.xml里面,增加以下权限申请
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /><uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /><uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /><uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
报告页面设计
同样是采用Compose的方式来设计一个页面展示Wifi测量数据的结果,新建一个WifiMeasureReport的class,代码如下:
@Composable
fun WifiMeasureReport (positionName: String?) {val context = LocalContext.currentval scope = rememberCoroutineScope()var wifiScanResult by remember { mutableStateOf<List<WifiMeasureData>>(listOf(WifiMeasureData("", 0))) }DisposableEffect(Unit) {val wifiScanService = WifiScanService(context)wifiScanService.init()val job = scope.launch {wifiScanService.data.receiveAsFlow().onEach { wifiScanResult = it }.collect()}onDispose {wifiScanService.cancel()job.cancel()}}Column() {Text(text = stringResource(id = R.string.report_title),style = MaterialTheme.typography.titleLarge,textAlign = TextAlign.Center,modifier = Modifier.fillMaxWidth())Spacer(modifier = Modifier.height(8.dp))Text(text = stringResource(id = R.string.report_position_name)+": "+positionName,style = MaterialTheme.typography.bodyLarge,modifier = Modifier.padding(10.dp))LazyColumn(Modifier.fillMaxWidth(),contentPadding = PaddingValues(horizontal = 4.dp)){item {ItemHeader()}itemsIndexed(wifiScanResult) { index: Int, item: WifiMeasureData ->ItemRow(index, item)}}}
}@Composable
fun ItemHeader() {Row(Modifier.fillMaxWidth()//.border(BorderStroke(0.5.dp, Color.Black))) {Text(text = stringResource(R.string.report_header_bssid), fontWeight = FontWeight.Bold, modifier = Modifier.weight(5f).padding(10.dp))Text(text = stringResource(R.string.report_header_strength), fontWeight = FontWeight.Bold, modifier = Modifier.weight(5f).padding(10.dp))}Divider(color = Color.LightGray,modifier = Modifier.height(1.dp).fillMaxHeight().fillMaxWidth())
}@Composable
fun ItemRow(index: Int, item: WifiMeasureData) {val modifier: Modifier = Modifier.fillMaxWidth()Row(modifier = if (index%2 == 0) modifier.background(Color.LightGray) else modifier) {Text(text = item.bssId, modifier = Modifier.weight(5f).padding(10.dp))Text(text = item.signalStrength.toString(), modifier = Modifier.weight(5f).padding(10.dp))}Divider(color = Color.LightGray,modifier = Modifier.height(1.dp).fillMaxHeight().fillMaxWidth())
}@Preview(showBackground = true)
@Composable
fun PreviewMeasureReportScreen() {WifiMeasureReport("grid_1")
}
这里同样采用了Column垂直布局,其中用了一个LazyColumn来展示Wifi测量结果的列表。这个LazyColumn类似于以前的RecyclerView。
测试结果上报
WiFi测试的结果要上报到服务器来进行汇总分析,最后生成Wifi指纹,这部分的内容可以参考我之前提到的博客内容,这里不再重复。我们只需要修改一下WifiMeasureReport,把拿到的结果通过REST API上传即可。改动如下:
待补充。。。
运行效果
最后把项目打包为APK后上传到手机运行,实际效果如下:
待补充。。。
相关文章:

Jetpack Compose开发一个Android WiFi导航应用
在以前的一篇文章构建一个WIFI室内定位系统_wifi定位系统-CSDN博客中,我介绍了如何用Android来测量WiFi信号,上传到服务器进行分析后,生成室内不同地方的WiFi指纹,从而帮助进行室内导航。当时我是用的HTML5的技术来快速开发一个An…...

【Mode Management】ComM详细介绍
目录 1. Introduction and functional overview 2.Dependencies to other modules 3.Functional specification 3.1 Partial Network Cluster Management 3.2 ComM channel state machine 3.2.1 Behaviour in state COMM_NO_COMMUNICATION 3.2.1.1 COMM_NO_COM_NO_PENDI…...
【C++多线程编程】(二)之详解锁(lock)和解锁(unlock)
在C多线程编程中,锁(lock)和解锁(unlock)通常用于管理共享资源的访问,以防止多个线程同时对资源进行修改,从而避免竞态条件(Race Condition)和数据不一致性问题。C标准库…...
【Mypy】超级实用的python高级库!
今天,我很兴奋地向大家介绍一个神奇的Python库:Mypy。这个库是Python世界中的一颗璀璨明星,提供了静态类型检查的强大功能,极大地增强了Python这门动态类型语言的健壮性和可维护性。我们将深入探索Mypy的多个方面,并通…...
【Python基础】循环语句
文章目录 [toc]什么是循环Python中的循环方式while循环格式示例 什么是循环 程序中需要重复执行的代码,可以通过循环实现比如和女朋友道歉,或一万遍“宝宝,我错了”,在没有学习循环之前,我们只能通过如下方式实现 pr…...

【面试】广告优化
a1:点击率公式是什么?点击率低的原因是什么? 点击率点击/曝光,点击率低的原因主要有两点:一是创意不吸引人;二是目标受众不准确/定向过宽不精确,广告曝光给了对产品不感兴趣用户 a2:…...

RabbitMQ插件详解:rabbitmq_message_timestamp【Rabbitmq 五】
欢迎来到我的博客,代码的世界里,每一行都是一个故事 RabbitMQ时空之旅:rabbitmq_message_timestamp的奇妙世界 前言什么是rabbitmq_message_timestamprabbitmq_message_timestamp 的定义与作用:如何在 RabbitMQ 中启用消息时间戳&…...
AD9361 Evaluation Software配置脚本转换工具
最近在玩一个开源的AD9361项目,AD9361采用纯逻辑配置,不需要ARM或者MicroBlaze。其中,先是用AD9361 Evaluation Software生成配置脚本,再转换成ad9361_lut.v。 在网上查了一圈,有个转换工具叫bit_converter࿰…...

Centos7 配置Git
随笔记录 目录 1, 新建用户 2. 给用户设置密码相关操作 3. 为新用户添加sudo 权限 4. 配置Git 4.1 配置Git 4.2 查看id_ras.pub 5, 登录Git 配置SSH 秘钥 6. Centos7 登录Git 7. clone 指定branch到本地 8. 将新代码复制到指定路径 9. 上传指定代码 …...
python工具方法 44 数据仿真生成(粘贴目标切片到背景图像上,数据标签校验)
在深度学习训练中数据是一个很重要的因素,在数据不够时需要我们基于现有的数据进行增强生成新的数据。此外,在某特殊情况,如对某些目标切片数据(例如:石块分割切片)预测效果较差,需要增强其在训练数据中的频率。故此,我们可以将先有数据标注中的目标裁剪出来,作为样本…...

Llama 架构分析
从代码角度进行Llama 架构分析 Llama 架构分析前言Llama 架构分析分词网络主干DecoderLayerAttentionMLP 下游任务因果推理文本分类 Llama 架构分析 前言 Meta 开发并公开发布了 Llama系列大型语言模型 (LLM),这是一组经过预训练和微调的生成文本模型,参…...
vue3前端 md5工具类
工具类 /*** Namespace for hashing and other cryptographic functions* Copyright (c) Andrew Valums* Licensed under the MIT license, http://valums.com/mit-license/*/var V V || {}; V.Security V.Security || {};(function () {// for faster accessvar S V.Secur…...
Unity触摸 射线穿透UI解决
unity API 之EventSystem.current.IsPointerOverGameObject() 命名空间 :UnityEngine.EventSystems 官方描述: public bool IsPointerOverGameObject(); public bool IsPointerOverGameObject(int pointerId); //触摸屏时需要的参数ÿ…...

基于QTreeWidget实现带Checkbox的多级组织结构选择树
基于QTreeWidget实现带Checkbox的多级组织结构选择树 采用基于QWidgetMingw实现的原生的组织结构树 通过QTreeWidget控件实现的带Checkbox多级组织结构树。 Qt相关系列文章: 一、Qt实现的聊天画面消息气泡 二、基于QTreeWidget实现多级组织结构 三、基于QTreeWidget…...

探索 Vim:一个强大的文本编辑器
引言: Vim(Vi IMproved)是一款备受推崇的文本编辑器,拥有强大的功能和高度可定制性,提供丰富的编辑和编程体验。本文将探讨 Vim 的基本概念、使用技巧以及为用户带来的独特优势。 简介和发展 1. Vim 的简介和历史 V…...
K8S(十)—容器探针
这里写目录标题 容器探针(probe)检查机制探测结果探测类型何时该使用存活态探针?何时该使用就绪态探针?何时该使用启动探针? 使用exechttptcpgrpc使用命名端口 使用启动探针保护慢启动容器定义就绪探针配置探针HTTP 探测TCP 探测探针层面的…...
[C错题本]
1.int,short,long都是signed的 但是char可能是signed 也可能是unsigned的——《C Primer》 2.在16位的PC中 char类型占1个字节 int占2个字节 long int占4个字节 float占四个字节 double占八个字节 3.自增运算符和自减运算符即使是在判断条件中使用也会实际生效 int i 1; int…...

tomcat启动异常:子容器启动失败(a child container failed during start)
最近在使用eclipse启动Tomcat时,发现一个问题,启动以前的项目突然报子容器启动异常。 异常信息如下: 严重: 子容器启动失败 java.util.concurrent.ExecutionException: org.apache.catalina.LifecycleException: 无法启动组件[org.apache.…...

JAVA序列化(创建可复用的 Java 对象)
JAVA 序列化(创建可复用的 Java 对象) 保存(持久化)对象及其状态到内存或者磁盘 Java 平台允许我们在内存中创建可复用的 Java 对象,但一般情况下,只有当 JVM 处于运行时,这些对象才可能存在,即,这些对象的生命周期不…...

如何使用自动化工具编写测试用例?
以下为作者观点,仅供参考: 在快速变化的软件开发领域,保证应用程序的可靠性和质量至关重要。随着应用程序复杂性和规模的不断增加,仅手动测试无法满足行业需求。 这就是测试自动化发挥作用的地方,它使软件测试人员能…...

无法与IP建立连接,未能下载VSCode服务器
如题,在远程连接服务器的时候突然遇到了这个提示。 查阅了一圈,发现是VSCode版本自动更新惹的祸!!! 在VSCode的帮助->关于这里发现前几天VSCode自动更新了,我的版本号变成了1.100.3 才导致了远程连接出…...
java调用dll出现unsatisfiedLinkError以及JNA和JNI的区别
UnsatisfiedLinkError 在对接硬件设备中,我们会遇到使用 java 调用 dll文件 的情况,此时大概率出现UnsatisfiedLinkError链接错误,原因可能有如下几种 类名错误包名错误方法名参数错误使用 JNI 协议调用,结果 dll 未实现 JNI 协…...

【机器视觉】单目测距——运动结构恢复
ps:图是随便找的,为了凑个封面 前言 在前面对光流法进行进一步改进,希望将2D光流推广至3D场景流时,发现2D转3D过程中存在尺度歧义问题,需要补全摄像头拍摄图像中缺失的深度信息,否则解空间不收敛…...
django filter 统计数量 按属性去重
在Django中,如果你想要根据某个属性对查询集进行去重并统计数量,你可以使用values()方法配合annotate()方法来实现。这里有两种常见的方法来完成这个需求: 方法1:使用annotate()和Count 假设你有一个模型Item,并且你想…...

【Zephyr 系列 10】实战项目:打造一个蓝牙传感器终端 + 网关系统(完整架构与全栈实现)
🧠关键词:Zephyr、BLE、终端、网关、广播、连接、传感器、数据采集、低功耗、系统集成 📌目标读者:希望基于 Zephyr 构建 BLE 系统架构、实现终端与网关协作、具备产品交付能力的开发者 📊篇幅字数:约 5200 字 ✨ 项目总览 在物联网实际项目中,**“终端 + 网关”**是…...
鸿蒙DevEco Studio HarmonyOS 5跑酷小游戏实现指南
1. 项目概述 本跑酷小游戏基于鸿蒙HarmonyOS 5开发,使用DevEco Studio作为开发工具,采用Java语言实现,包含角色控制、障碍物生成和分数计算系统。 2. 项目结构 /src/main/java/com/example/runner/├── MainAbilitySlice.java // 主界…...

Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决
Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决 问题背景 在一个基于 Spring Cloud Gateway WebFlux 构建的微服务项目中,新增了一个本地验证码接口 /code,使用函数式路由(RouterFunction)和 Hutool 的 Circle…...

Chromium 136 编译指南 Windows篇:depot_tools 配置与源码获取(二)
引言 工欲善其事,必先利其器。在完成了 Visual Studio 2022 和 Windows SDK 的安装后,我们即将接触到 Chromium 开发生态中最核心的工具——depot_tools。这个由 Google 精心打造的工具集,就像是连接开发者与 Chromium 庞大代码库的智能桥梁…...

实战三:开发网页端界面完成黑白视频转为彩色视频
一、需求描述 设计一个简单的视频上色应用,用户可以通过网页界面上传黑白视频,系统会自动将其转换为彩色视频。整个过程对用户来说非常简单直观,不需要了解技术细节。 效果图 二、实现思路 总体思路: 用户通过Gradio界面上…...

软件工程 期末复习
瀑布模型:计划 螺旋模型:风险低 原型模型: 用户反馈 喷泉模型:代码复用 高内聚 低耦合:模块内部功能紧密 模块之间依赖程度小 高内聚:指的是一个模块内部的功能应该紧密相关。换句话说,一个模块应当只实现单一的功能…...