Compose Multiplatform初尝
2023-07-19 本文已影响0人
h2coder
前言
- Compose Multiplatform,让Compose不止用于Android开发,最近支持iOS也到了Alpha测试阶段
- Compose和Flutter比较类似,都是声明式UI,所以上手其中一个,另外一个也很快上手了
- 本篇用Compose写一个接口请求,然后使用列表渲染出来
效果展示
Compose.png依赖以及配置
- shared模块的build.gradle.kts
plugins {
kotlin("multiplatform")
id("com.android.library")
id("kotlinx-serialization")
}
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
kotlin {
targetHierarchy.default()
android {
compilations.all {
kotlinOptions {
jvmTarget = "1.8"
}
}
}
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach {
it.binaries.framework {
baseName = "shared"
}
}
sourceSets {
val commonMain by getting {
dependencies {
//put your multiplatform dependencies here
val ktorVersion = "2.0.1";
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
//core
api("io.ktor:ktor-client-core:$ktorVersion")
//CIO
api("io.ktor:ktor-client-cio:$ktorVersion")
//Logging
api("io.ktor:ktor-client-logging:$ktorVersion")
api("io.ktor:ktor-client-content-negotiation:$ktorVersion")
//Json格式化
api("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
//时间格式化
api("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
}
}
}
}
android {
namespace = "com.zh.kmmsample"
compileSdk = 33
defaultConfig {
minSdk = 21
}
}
- androidApp模块
plugins {
id("com.android.application")
kotlin("android")
id("kotlinx-serialization")
}
android {
namespace = "com.zh.kmmsample.android"
compileSdk = 33
defaultConfig {
applicationId = "com.zh.kmmsample.android"
minSdk = 21
targetSdk = 33
versionCode = 1
versionName = "1.0"
multiDexEnabled = true
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.7"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
signingConfigs {
getByName("debug") {
storeFile = file("../key")
storePassword = "123456"
keyAlias = "key"
keyPassword = "123456"
}
create("release") {
storeFile = file("../key")
storePassword = "123456"
keyAlias = "key"
keyPassword = "123456"
}
}
buildTypes {
getByName("debug") {
isMinifyEnabled = false
isDebuggable = true
}
getByName("release") {
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = false
isDebuggable = false
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
}
}
compileOptions {
//由于Ktor中,有用到Java高版本的语法,所以需要增加解糖处理
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
api(project(":shared"))
api("androidx.compose.ui:ui:1.4.3")
api("androidx.compose.ui:ui-tooling:1.4.3")
api("androidx.compose.ui:ui-tooling-preview:1.4.3")
api("androidx.compose.foundation:foundation:1.4.3")
api("androidx.compose.material:material:1.4.3")
api("androidx.activity:activity-compose:1.7.1")
//MultiDex
api("androidx.multidex:multidex:2.0.1")
val ktorVersion = "2.0.1";
api("io.ktor:ktor-client-android:$ktorVersion")
//由于Ktor中,有用到Java高版本的语法,所以需要增加解糖处理
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.2.2")
}
接口API地址配置(Api.kt)
object Api {
/**
* 问答
*/
const val wenDaApi = "https://wanandroid.com/wenda/list/1/json"
}
实体类(WenDaModel.kt)
/**
* 问答,每日一问
*/
@Serializable
data class WenDaModel(
val data: Data? = null,
val errorCode: Int = 0,
val errorMsg: String? = null
) {
@Serializable
data class Data(
var curPage: Int = 0,
var datas: List<Datas>? = null,
var offset: Int = 0,
var over: Boolean = false,
var pageCount: Int = 0,
var size: Int = 0,
var total: Int = 0
) {
@Serializable
data class Datas(
var adminAdd: Boolean = false,
var apkLink: String? = null,
var audit: Int = 0,
var author: String? = null,
var canEdit: Boolean = false,
var chapterId: Int = 0,
var chapterName: String? = null,
var collect: Boolean = false,
var courseId: Int = 0,
var desc: String? = null,
var descMd: String? = null,
var envelopePic: String? = null,
var fresh: Boolean = false,
var host: String? = null,
var id: Int = 0,
//var isAdminAdd: Boolean = false,
var link: String? = null,
var niceDate: String? = null,
var niceShareDate: String? = null,
var origin: String? = null,
var prefix: String? = null,
var projectLink: String? = null,
var publishTime: String = "0",
var realSuperChapterId: Int = 0,
var selfVisible: Int = 0,
var shareDate: String = "0",
var shareUser: String? = null,
var superChapterId: Int = 0,
var superChapterName: String? = null,
var tags: List<Tags>? = null,
var title: String? = null,
var type: Int = 0,
var userId: Int = 0,
var visible: Int = 0,
var zan: Int = 0,
) {
@Serializable
data class Tags(
val name: String? = null,
val url: String? = null
)
}
}
}
HTTP请求工具类(HttpUtil.kt)
/**
* Http工具类
*/
object HttpUtil {
/**
* 超时时间
*/
private const val mTimeOutSeconds: Long = 15000
/**
* Http客户端
*/
// val mClient: HttpClient = HttpClient(CIO) {
val mClient: HttpClient = HttpClient() {
expectSuccess = true
// engine {
// maxConnectionsCount = 1000
// requestTimeout = mTimeOutSeconds
//
// endpoint {
// maxConnectionsPerRoute = 100
// pipelineMaxSize = 20
// keepAliveTime = mTimeOutSeconds
// connectTimeout = mTimeOutSeconds
// }
// }
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.HEADERS
}
install(ContentNegotiation) {
json(Json {
isLenient = true
//如果Json有字段在实体类中找不到,那么忽略它,没有该配置的话,就会报错
ignoreUnknownKeys = true
prettyPrint = true
isLenient = true
})
}
}
/**
* 发起GET请求
*/
suspend inline fun <reified T> get(
url: String
): T {
return withContext(Dispatchers.IO) {
val response = mClient.get(url = Url(url))
response.call.body()
}
}
/**
* 发起POST请求
*/
suspend inline fun <reified T> post(
url: String
): T {
return withContext(Dispatchers.IO) {
val response = mClient.post(url = Url(url))
response.call.body()
}
}
}
Application(App.kt)
class App : Application() {
override fun attachBaseContext(context: Context) {
super.attachBaseContext(context)
MultiDex.install(this)
}
}
主题(MyApplicationTheme.kt)
@Composable
fun MyApplicationTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colors = if (darkTheme) {
darkColors(
primary = Color(0xFFBB86FC),
primaryVariant = Color(0xFF3700B3),
secondary = Color(0xFF03DAC5)
)
} else {
lightColors(
primary = Color(0xFF6200EE),
primaryVariant = Color(0xFF3700B3),
secondary = Color(0xFF03DAC5)
)
}
val typography = Typography(
body1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
)
)
val shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp)
)
MaterialTheme(
colors = colors,
typography = typography,
shapes = shapes,
content = content
)
}
列表条目(WenDaItem.kt)
/**
* 问答条目
*/
@Composable
fun WenDaItem(data: WenDaModel.Data.Datas?) {
Card(
modifier = Modifier
.background(Color.White)
.padding(10.dp)
.fillMaxWidth(),
elevation = 10.dp
) {
Column(modifier = Modifier.padding(10.dp)) {
Text(
text = "作者${data?.author}"
)
Text(text = "${data?.title}")
}
}
}
列表页面(MainActivity.kt)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
ContentView()
}
}
}
}
}
@Composable
fun ContentView() {
Column {
val scope = rememberCoroutineScope()
var wenDaModel by remember {
mutableStateOf(WenDaModel())
}
//按钮
Button(
modifier = Modifier
.padding(10.dp)
.fillMaxWidth(),
onClick = {
scope.launch {
wenDaModel = HttpUtil.get(
url = Api.wenDaApi
)
}
}) {
Text(text = "请求数据")
}
//列表
LazyColumn {
repeat(
wenDaModel.data?.datas?.size ?: 0
) {
item {
WenDaItem(data = wenDaModel.data?.datas?.get(it))
}
}
}
}
}
@Preview
@Composable
fun DefaultPreview() {
MyApplicationTheme {
ContentView()
}
}
清单配置(AndroidManifest.xml)
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".App"
android:allowBackup="false"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>