Android KMP初探
Android KMP初探
前言:
最近线上听了Kotlin官网举行的KMP会议,感觉听神奇的,于是就把官方demo下载下来尝试了一下,下载插件和所需要的依赖都用了很久,但是发现里面的代码很少,于是尝试自己手写了一下,遇到了不少问题,这里记录一下.
1.定义:
Kotlin Multiplatform 技术可为多种平台创建应用程序并在平台之间高效重用代码,同时保留原生编程的优势。您的应用程序将在 iOS、Android、macOS、Windows、Linux 等平台上运行。
Compose Multiplatform 是 JetBrains 推出的声明式 UI 框架,可让您为 Android、iOS、桌面和 Web 开发共享 UI。将 Compose Multiplatform 集成到 Kotlin Multiplatform 项目中,更快交付应用和功能,而无需维护多个 UI 实现。

2.适合各类项目:

3.优点
使用 Compose Multiplatform 只需构建一次 UI
Compose Multiplatform 是一个基于 Kotlin 和 Jetpack Compose 的声明式框架,用于在 Android、iOS、Web 和桌面(通过 JVM)之间共享 UI。
加速 UI 开发
轻松同步多个 UI 实现,让应用更快交付到用户手中。
组件级重用
使用可在所有目标平台上使用的可自定义微件构建您的 UI。使用预设主题快速开始,或自行创建细节可精确至像素的视觉风格。
根据需要使用原生组件
轻松使用原生 UI 微件或将共享 UI 嵌入现有原生应用。
4.需要几个硬性条件:
在使用 KMP + Compose 进行开发时,需要以下条件,由于没有mac设备就暂时不跑ios项目,跑Android项目也是一样的,不用过于纠结.
- Mac电脑(苹果开发必须mac)
- Android Studio
- Xcode
- 配置 ios 开发环境(cocoapods、开发者账号等)
5.项目结构:
################## 目录结构说明 ##################
```
.
├── README.md
├── app - 主应用
│ ├── build.gradle.kts
│ ├── MainActivity
│ ├── libs
│ └── src
├── commonMain -公共组件
│ ├── app
│ ├── Greeting
│ ├── Platform
├── appleMain - ios平台
│ ├── getPlatform
├── iosMain - ios业务代码
│ ├── MainViewController
│ └── IOSPlatform
├── gradle
│ └── wrapper
├── gradle.properties
├── gradlew
├── gradlew.bat
├── build.gradle.kts
├── local.properties
└── settings.gradle.kts
################## 目录结构说明 ##################
6.App目录下的build.gradle.kts配置:
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.idea.tcs.extras.isCommonizedKey
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {alias(libs.plugins.kotlinMultiplatform)alias(libs.plugins.androidApplication)alias(libs.plugins.composeMultiplatform)alias(libs.plugins.composeCompiler)
}
kotlin {androidTarget {@OptIn(ExperimentalKotlinGradlePluginApi::class)compilerOptions {jvmTarget.set(JvmTarget.JVM_11)}}listOf(iosX64(),iosArm64(),iosSimulatorArm64()).forEach { iosTarget ->iosTarget.binaries.framework {baseName = "ComposeApp"isStatic = true}}sourceSets {androidMain.dependencies {implementation(compose.preview)implementation(libs.androidx.activity.compose)}commonMain.dependencies {implementation(compose.runtime)implementation(compose.foundation)implementation(compose.material)implementation(compose.ui)implementation(compose.components.resources)implementation(compose.components.uiToolingPreview)implementation(libs.androidx.lifecycle.viewmodel)implementation(libs.androidx.lifecycle.runtime.compose)implementation(libs.androidx.constraintlayout)}}
}android {namespace = "com.cloud.androidkmpdemo"compileSdk = libs.versions.android.compileSdk.get().toInt()defaultConfig {applicationId = "com.cloud.androidkmpdemo"minSdk = libs.versions.android.minSdk.get().toInt()targetSdk = libs.versions.android.targetSdk.get().toInt()versionCode = 1versionName = "1.0"}packaging {resources {excludes += "/META-INF/{AL2.0,LGPL2.1}"}}buildTypes {getByName("release") {isMinifyEnabled = false}}compileOptions {sourceCompatibility = JavaVersion.VERSION_11targetCompatibility = JavaVersion.VERSION_11}compose.desktop {application {mainClass = "com.example.composeApp.MainKt"nativeDistributions {targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)packageName = "com.cloud.kmpdemo"packageVersion = "1.0.0"// 描述应用程序description = "A simple demo for KMP"// 版权信息copyright = "© 2024 My Name. All rights reserved."// 厂商信息vendor = "Example vendor"// 设置许可证文件licenseFile.set(project.file("LICENSE.txt"))}}}
}
dependencies {debugImplementation(compose.uiTooling)
}
7.项目的build.gradle.kts配置:
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {alias(libs.plugins.androidApplication) apply falsealias(libs.plugins.androidLibrary) apply falsealias(libs.plugins.composeMultiplatform) apply falsealias(libs.plugins.composeCompiler) apply falsealias(libs.plugins.kotlinMultiplatform) apply falsealias(libs.plugins.jetbrainsKotlinAndroid) apply false
}
8.统一的依赖配置:
[versions]
agp = "8.5.2"
android-compileSdk = "34"
android-minSdk = "24"
android-targetSdk = "34"
androidx-activityCompose = "1.9.3"
androidx-appcompat = "1.7.0"
androidx-constraintlayout = "2.2.0"
androidx-core-ktx = "1.15.0"
androidx-espresso-core = "3.6.1"
androidx-lifecycle = "2.8.4"
androidx-material = "1.12.0"
androidx-test-junit = "1.2.1"
compose-multiplatform = "1.7.0"
junit = "4.13.2"
kotlin = "2.1.0"
kotlinVersion = "1.9.0"[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" }
androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-junit" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-espresso-core" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" }
androidx-material = { group = "com.google.android.material", name = "material", version.ref = "androidx-material" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
androidLibrary = { id = "com.android.library", version.ref = "agp" }
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlinVersion" }
9.公共组件:
9.1 Platform接口:
interface Platform {val name: String
}expect fun getPlatform(): Platform
9.2 Greeting类:
class Greeting {private val platform = getPlatform()fun greet(): String {return "Hello, ${platform.name}!"}
}
9.3 App类:
package com.cloud.kmpdemoimport androidkmpdemo.app.generated.resources.Res
import androidkmpdemo.app.generated.resources.compose_multiplatform
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.AlertDialog
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.ui.tooling.preview.Preview@Composable
@Preview
fun App() {MaterialTheme {var showContent by remember { mutableStateOf(false) }var showDialog by remember { mutableStateOf(false) }Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {Button(onClick = { showContent = !showContent }) {Text("Click me")}Button(onClick = { showDialog = !showDialog }) {Text("show Dialog")}AnimatedVisibility(showContent) {val greeting = remember { Greeting().greet() }Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {Image(painterResource(Res.drawable.compose_multiplatform), null)Text("Compose: $greeting")}}AnimatedVisibility(showDialog) {Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {AlertDialogSample()}}}}
}@Composable
@Preview
fun AlertDialogSample(){val dialog = remember { mutableStateOf(true) }if(dialog.value){AlertDialog(onDismissRequest = { dialog.value = false},title = { Text(text = "开启位置服务")},text = { Text(text = "这将意味着,我们会给您提供精准的位置服务,并且您将接受关于您订阅的位置信息。") },confirmButton = {TextButton(onClick = { dialog.value = false}){Text(text = "同意")}},dismissButton = {TextButton(onClick = {dialog.value = false}){Text(text = "取消")}})}
}
10.Android代码:
本文我在原来的基础上加入了一个弹框示例AlertDialogSample()
package com.cloud.kmpdemoimport android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Previewclass MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent(){App()}}
}@Preview
@Composable
fun AppAndroidPreview() {App()
}@Preview
@Composable
fun DialogAndroidPreview() {//显示dialogAlertDialogSample()
}
package com.cloud.kmpdemoimport android.os.Build/*** 获取平台版本信息*/
class AndroidPlatform : Platform {override val name: String = "Android ${Build.VERSION.SDK_INT}"
}actual fun getPlatform(): Platform = AndroidPlatform()
11.ios平台代码:
package org.example.kmpdemoimport androidx.compose.ui.window.ComposeUIViewController
import com.cloud.kmpdemo.Appfun MainViewController() = ComposeUIViewController { App() }
package org.example.kmpdemoimport com.cloud.kmpdemo.Platform
import platform.UIKit.UIDeviceclass IOSPlatform: Platform {override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}actual fun getPlatform(): Platform = IOSPlatform()
12.ios业务代码:
import UIKit
import SwiftUI
import ComposeAppstruct ComposeView: UIViewControllerRepresentable {func makeUIViewController(context: Context) -> UIViewController {MainViewControllerKt.MainViewController()}func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}struct ContentView: View {var body: some View {ComposeView().ignoresSafeArea(.keyboard) // Compose has own keyboard handler}
}
import SwiftUI@main
struct iOSApp: App {var body: some Scene {WindowGroup {ContentView()}}
}
info.plist:
和Android的build.gradle配置文件一样,都是管理依赖和第三方库的.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict><key>CFBundleDevelopmentRegion</key><string>$(DEVELOPMENT_LANGUAGE)</string><key>CFBundleExecutable</key><string>$(EXECUTABLE_NAME)</string><key>CFBundleIdentifier</key><string>$(PRODUCT_BUNDLE_IDENTIFIER)</string><key>CFBundleInfoDictionaryVersion</key><string>6.0</string><key>CFBundleName</key><string>$(PRODUCT_NAME)</string><key>CFBundlePackageType</key><string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string><key>CFBundleShortVersionString</key><string>1.0</string><key>CFBundleVersion</key><string>1</string><key>LSRequiresIPhoneOS</key><true/><key>CADisableMinimumFrameDurationOnPhone</key><true/><key>UIApplicationSceneManifest</key><dict><key>UIApplicationSupportsMultipleScenes</key><false/></dict><key>UILaunchScreen</key><dict/><key>UIRequiredDeviceCapabilities</key><array><string>armv7</string></array><key>UISupportedInterfaceOrientations</key><array><string>UIInterfaceOrientationPortrait</string><string>UIInterfaceOrientationLandscapeLeft</string><string>UIInterfaceOrientationLandscapeRight</string></array><key>UISupportedInterfaceOrientations~ipad</key><array><string>UIInterfaceOrientationPortrait</string><string>UIInterfaceOrientationPortraitUpsideDown</string><string>UIInterfaceOrientationLandscapeLeft</string><string>UIInterfaceOrientationLandscapeRight</string></array>
</dict>
</plist>
13.运行效果如下:




14.总结:
以上就是今天的内容,KMP初探,根据不同平台的设备显示不同类型和版本号,代码非常少,再也没有创建xml的烦恼,MainActivity也不需要加载activity_main布局,简直不要太爽,当然KMP如果不熟悉的话跑起来很费劲,我是从0到1自己全部重新撸了一遍,包含Android和IOS两个平台,build插件配置等等,这里我没有Mac环境就不演示iOS的效果了.如果只是单纯写代码看效果没有太大的意义,需要搞清楚整个运行过程和实现原理,在后面进行学习和开发过程中就会事半功倍。
15.项目源码地址:
https://gitee.com/jackning_admin/android-kmpdemo
相关文章:
Android KMP初探
Android KMP初探 前言: 最近线上听了Kotlin官网举行的KMP会议,感觉听神奇的,于是就把官方demo下载下来尝试了一下,下载插件和所需要的依赖都用了很久,但是发现里面的代码很少,于是尝试自己手写了一下&…...
网络安全之Web后端PHP
目录 一、PHP基础语法 1.PHP基础 (1)php的优点 (2)PhpStorm的优点 2.PHP基本语法 3.PHP变量 4.PHP运算符 二、PHP流控与数组 1.php流程控制语句以及循环 (1)if 语句 (2)if…...
Redis——用户签到BitMap,UV统计
目录 BitMap 使用场景 1. 用户签到系统 2. 用户行为标记 3. 布隆过滤器(Bloom Filter) BitMap介绍 Redis中的使用 Redis功能示例 添加: 获取: 批量获取: java中实现 统计本月连续签到次数 UV统计 UV 统计…...
pycharm技巧--鼠标滚轮放大或缩小 Pycharm 字体大小
1、鼠标滚轮调整字体 设置 Ctrl 鼠标滚轮调整字体大小 备注: 第一个是活动窗口,即缩放当前窗口 第二个是所有编辑器窗口,即缩放所有窗口的字体 2、插件 汉化包: Chinese Simplified 包...
数字信任的底层逻辑:密码学核心技术与现实应用
安全和密码学 --The Missing Semester of Your CS Education 目录 熵与密码强度密码散列函数密钥体系 3.1 对称加密 3.2 非对称加密信任模型对比典型应用案例安全实践建议扩展练习杂项 密码学是构建数字信任的基石。 本文浅析密码学在现实工具中的应用,涵盖 1&…...
全面理解-深拷贝与浅拷贝
在 C 中,深拷贝(Deep Copy) 和 浅拷贝(Shallow Copy) 是两种完全不同的对象拷贝策略,主要区别在于对指针和动态分配资源的处理方式。正确理解二者的区别是避免内存泄漏、悬空指针和程序崩溃的关键。 一、核…...
Redis分布式锁故障处理:当Redis不可用时的应对策略
Redis分布式锁故障处理:当Redis不可用时的应对策略 在分布式系统中,Redis因其高性能和丰富的特性常被用于实现分布式锁。但当加锁过程中Redis服务不可用时,系统将面临严重挑战。本文将深入探讨这一问题,并提供多维度解决方案。 目…...
WordPress平台如何接入Deepseek,有效提升网站流量
深夜改代码到崩溃?《2024全球CMS生态报告》揭露:78%的WordPress站长因API对接复杂,错失AI内容红利。本文实测「零代码接入Deepseek」的保姆级方案,配合147SEO的智能发布系统,让你用3个步骤实现日均50篇EEAT合规内容自动…...
ROS ur10机械臂添加140夹爪全流程记录
ROS ur10机械臂添加140夹爪 系统版本:Ubuntu20.04 Ros版本:noetic Moveit版本:moveit-noetic 参考博客: ur3robotiq ft sensorrobotiq 2f 140配置rviz仿真环境_有末端力传感器的仿真环境-CSDN博客 UR5机械臂仿真实例…...
16、Python面试题解析:python中的浅拷贝和深拷贝
在 Python 中,浅拷贝(Shallow Copy) 和 深拷贝(Deep Copy) 是处理对象复制的两种重要机制,它们的区别主要体现在对嵌套对象的处理方式上。以下是详细解析: 1. 浅拷贝(Shallow Copy&a…...
第十九天 HarmonyOS的文件操作和本地存储
一、前言:为什么需要掌握文件操作与本地存储? 在移动应用开发中,文件操作和本地存储是每个开发者都必须掌握的核心技能。无论是保存用户配置、缓存网络数据,还是处理图片/视频等多媒体文件,都需要通过文件系统进行操作…...
VLM(视觉语言模型)与DeepSeek R1(奖励机制)如何结合
VLM(视觉语言模型)与DeepSeek R1(奖励机制)如何结合 flyfish VLM的传统训练依赖于监督学习(直接拟合问答对),而规则奖励函数通常用于强化学习(通过试错和奖励反馈优化策略…...
FFMPEG编码容错处理解决办法之途径----升级库文件
在qt开发环境下接收网络数据,调用ffmpeg解码播放视频,出现闪屏现象,具体现象可以使用操作系统自带的ffplay播放器播放原始视频流可复现;而使用操作系统自带的mpv播放器播放视频则不会出现闪屏;闪屏时会报Could not fin…...
uniapp h5端和app端 使用 turn.js
前提:添加页后,添加页与当前页会重叠在一起,不知道为什么,没有找到解决办法 1.h5端 <template><view class"container"><view id"flipbook"><view class"page page1">Page 1</view><view class"page pag…...
【idea问题排查技巧】
以下是针对 IDEA 中 日志打标(动态标记) 和 全链路追踪 功能的分步详解,结合具体场景和操作截图说明,帮助快速掌握实战技巧。 一、动态日志打标:不修改代码输出关键信息 1. 断点日志打印(非侵入式打标) 场景:在调试时,需要临时查看某个变量的值,但不想修改代码添加…...
【入门音视频】音视频基础知识
🌈前言🌈 这个系列在我学习过程中,对音视频知识归纳总结的笔记。因为音视频相关讲解非常稀少,所以我希望通过这个音视频系列,跟大家一起学习音视频,希望减少初学者在学习上的压力。同时希望也欢迎指出文章的…...
JMeter性能问题
性能测试中TPS上不去的几种原因 性能测试中TPS上不去的几种原因_tps一直上不去-CSDN博客 网络带宽 连接池 垃圾回收机制 压测脚本 通信连接机制 数据库配置 硬件资源 压测机 业务逻辑 系统架构 CPU过高什么原因 性能问题分析-CPU偏高 - 西瓜汁拌面 - 博客园 US C…...
软考高级信息系统项目管理师笔记-第2章信息技术发展
第2章 信息技术发展 2.1 信息技术及其发展 1、按表现形态的不同,信息技术可分为硬技术(物化技术)与软技术(非物化技术)。前者指各种信息设备及其功 能,如传感器、服务器、智能手机、通信卫星、笔记本电脑。后者指有关信息获取与处理的各种知识、方法 与技能,如语言文字…...
大语言模型(LLM)提示词(Prompt)高阶撰写指南
——结构化思维与工程化实践 一、LLM提示词设计的核心逻辑 1. 本质认知 LLM是「超强模式识别器概率生成器」,提示词的本质是构建数据分布约束,通过语义信号引导模型激活特定知识路径。优秀提示词需实现: 精准性:消除歧义&#…...
捷 C++ 课程学习笔记:STL 应用与复杂度分析
一、STL 六大组件 STL(Standard Template Library)是 C 标准库的重要组成部分,提供了通用的模板类和函数,用于实现常用的数据结构和算法。STL 主要包括以下六大组件: 容器(Containers)…...
【python】提取word\pdf格式内容到txt文件
一、使用pdfminer提取 import os import re from pdfminer.high_level import extract_text import docx2txt import jiebadef read_pdf(file_path):"""读取 PDF 文件内容:param file_path: PDF 文件路径:return: 文件内容文本"""try:text ext…...
数据结构☞泛型
一.基础定义与应用方向 1.定义: 一般的类和方法,只能使用具体的类型 : 要么是基本类型,要么是自定义的类。如果要编写可以 应用于多种类型 的代码,这种刻板的限制对代码的束缚就会很大。----- 来源《 Java 编程思想》对泛型的介…...
MFC学习笔记-1
一、编辑框和按钮 //.h文件private:CString str;//给窗口类加了一个变量(定义一个成员变量),关联到IDC_EDIT1中(要在实现中关联,源文件文件夹中)CString str2;//接收button2,和IDC_EDIT2绑定 p…...
html中rel、href、src、url的区别
1.url url(统一资源定位符):是对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址。 2.href href:Hypertext Reference的缩写。 意思是超文本引用。 3.rel rel:relatio…...
hot100-二叉树
二叉树 二叉树递归 相当于这个的顺序来回调换 class Solution {private List<Integer> res new ArrayList<>();public List<Integer> inorderTraversal(TreeNode root) {if(root null)return res;inorderTraversal(root.left);res.add(root.val);inorde…...
嵌入式项目:STM32刷卡指纹智能门禁系统
本文详细介绍基于STM32的刷卡指纹智能门禁系统。 获取资料/指导答疑/技术交流/选题/帮助,请点链接: https://gitee.com/zengzhaorong/share_contact/blob/master/stm32.txt 1 系统功能 1.1 功能概述 本系统由STM32硬件端(下位机)…...
短剧小程序系统源码
短剧小程序系统源码 今天我要向大家介绍的是最新作品——短剧小程序系统源码。这不仅仅是一款简单的播放工具,它背后蕴含的强大功能能够帮助你的短剧业务实现质的飞跃! 为什么说这款源码很厉害? 首先,在当今竞争激烈的市场环境…...
鸿蒙5.0实战案例:基于measure实现的文本测量
往期推文全新看点(文中附带全新鸿蒙5.0全栈学习笔录) ✏️ 鸿蒙(HarmonyOS)北向开发知识点记录~ ✏️ 鸿蒙(OpenHarmony)南向开发保姆级知识点汇总~ ✏️ 鸿蒙应用开发与鸿蒙系统开发哪个更有前景&#…...
C#中级教程(2)——走进 C# 面向对象编程:从基础到进阶的深度探索
一、为什么选择面向对象编程 在软件开发的演进过程中,随着程序规模和复杂度的不断增加,传统的编程方式逐渐暴露出局限性。面向对象编程应运而生,它就像是一位智慧的组织者,将程序中的功能进行模块化划分。每个模块各司其职&#x…...
基于SpringBoot的“流浪动物救助系统”的设计与实现(源码+数据库+文档+PPT)
基于SpringBoot的“流浪动物救助系统”的设计与实现(源码数据库文档PPT) 开发语言:Java 数据库:MySQL 技术:SpringBoot 工具:IDEA/Ecilpse、Navicat、Maven 系统展示 系统功能结构图 局部E-R图 系统首页界面 系统…...
