如何在Kotlin多平台项目的共享模块中使用@Parcelize注解

4
我正在开发一个Kotlin Multiplatform应用程序,我想在我的模型类中使用@Parcelize注释。但是,在我使用的Kotlin Multiplatform插件中,@Parcelize注释位于android.extensions插件中,该插件适用于androidApp模块。
关于我的build.gradle.kts(androidApp):
plugins {
  id("com.android.application")
  kotlin("android")
  kotlin("android.extensions")
  kotlin("kapt")
  id("kotlinx-serialization")
  id("androidx.navigation.safeargs.kotlin")
}

android {
  compileSdkVersion(Versions.compileSdk)

  compileOptions{
    sourceCompatibility = org.gradle.api.JavaVersion.VERSION_1_8
    targetCompatibility = org.gradle.api.JavaVersion.VERSION_1_8
  }

  kotlinOptions{
    jvmTarget = JavaVersion.VERSION_1_8.toString()
  }

  kapt{
    generateStubs = true
    correctErrorTypes = true
  }

  androidExtensions{
    isExperimental = true
  }

  buildFeatures{
    dataBinding = true
    viewBinding = true
  }

  defaultConfig {
    applicationId = "com.jshvarts.kmp.android"
    minSdkVersion(Versions.minSdk)
    targetSdkVersion(Versions.targetSdk)
    versionCode = 1
    versionName = "1.0"

    testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
  }

  buildTypes {
    getByName("release") {
      isMinifyEnabled = false
      proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
    }
  }

  packagingOptions {
    exclude("META-INF/*.kotlin_module")
  }
}

dependencies {
  implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
  implementation(kotlin("stdlib-jdk8", Versions.kotlin))
  implementation(Coroutines.android)
  implementation(AndroidX.appCompat)
  implementation(AndroidX.constraintLayout)
  implementation(AndroidX.recyclerView)
  implementation(AndroidX.lifecycleExtensions)
  implementation(AndroidX.lifecycleViewModelKtx)
  implementation(material)
  implementation(AndroidX.swipeToRefreshLayout)
  implementation(timber)
  implementation(picasso)
  implementation(AndroidX.navigation)
  implementation(AndroidX.navigation_ui)
  implementation(Serialization.runtime)
  //implementation(Serialization.core)
  //Dependency for googlePay
  implementation("com.google.android.gms:play-services-wallet:16.0.1")
  kapt(databinding)
  implementation(glide){
    exclude( "com.android.support")
  }
  kapt(glide)
  implementation(project(":shared"))

}

build.gradle.kts(shared)

plugins {
  id("com.android.library")
  kotlin("multiplatform")
  kotlin("plugin.serialization")
  //id("kotlinx-serialization")
  id("org.jetbrains.kotlin.native.cocoapods")
  id("com.squareup.sqldelight")
}
// CocoaPods requires the podspec to have a version.
version = "1.0"

android {
  compileSdkVersion(Versions.compileSdk)
  buildToolsVersion(Versions.androidBuildTools)

  defaultConfig {
    minSdkVersion(Versions.minSdk)
    targetSdkVersion(Versions.targetSdk)
    versionCode = 1
    versionName = "1.0"
  }
}

version = "1.0"
dependencies {
  implementation("com.google.firebase:firebase-crashlytics-buildtools:2.8.1")
  implementation(project(mapOf("path" to ":androidApp")))
}

kotlin {
  targets {

    val sdkName: String? = System.getenv("SDK_NAME")

    val isiOSDevice = sdkName.orEmpty().startsWith("iphoneos")
    if (isiOSDevice) {
      iosArm64("iOS")
    } else {
      iosX64("iOS")
    }
    android()
  }

  cocoapods {
    // Configure fields required by CocoaPods.
    summary = "Description for a Kotlin/Native module"
    homepage = "Link to a Kotlin/Native module homepage"
  }

  sourceSets {
    all {
      languageSettings.apply {
        useExperimentalAnnotation("kotlinx.coroutines.ExperimentalCoroutinesApi")
      }
    }

    val commonMain by getting {
      dependencies {
        implementation(kotlin("stdlib-common"))
        implementation(Coroutines.Core.core)
        implementation(Ktor.Core.common)
        implementation(Ktor.Json.common)
        implementation(Ktor.Logging.common)
        implementation(Ktor.Serialization.common)
        implementation(SqlDelight.runtime)
        implementation(Serialization.runtime)
        //implementation(project(":androidApp"))
        //implementation("org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlin}")
        //implementation("org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlin}")
        //implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1")
      }
    }

    val commonTest by getting {
      dependencies {
        implementation(Ktor.Mock.jvm)
      }
    }

    val androidMain by getting {
      dependencies {
        implementation(kotlin("stdlib"))
        implementation(Coroutines.Core.core)
        implementation(Ktor.android)
        implementation(Ktor.Core.jvm)
        implementation(Ktor.Json.jvm)
        implementation(Ktor.Logging.jvm)
        implementation(Ktor.Logging.slf4j)
        implementation(Ktor.Mock.jvm)
        implementation(Ktor.Serialization.jvm)
        implementation(Serialization.runtime)
        //implementation(Serialization.core)
        implementation(SqlDelight.android)


      }
    }

    val androidTest by getting {
      dependencies {
        implementation(kotlin("test-junit"))
        implementation(Ktor.Mock.common)
      }
    }

    val iOSMain by getting {
      dependencies {
        implementation(Coroutines.Core.core)
        implementation(Ktor.ios)
        implementation(Ktor.Core.common)
        implementation(Ktor.Json.common)
        implementation(Ktor.Logging.common)
        implementation(Ktor.Serialization.jvm)
       // implementation(Serialization.runtimeNative)
        implementation(SqlDelight.runtime)
        implementation(Ktor.Mock.common)
      }
    }

    val iOSTest by getting {
      dependencies {
        implementation(Ktor.Mock.native)
      }
    }
  }
}


sqldelight {
  database("PetsDatabase") {
    packageName = "com.jshvarts.kmp.db"
    sourceFolders = listOf("sqldelight")
  }
}

我的项目 build.gradle.kts 文件

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
      google()
      mavenCentral()
      jcenter()
    }

    dependencies {
        classpath("com.android.tools.build:gradle:4.0.0")
        classpath(kotlin("gradle-plugin", version = Versions.kotlin))
        classpath(kotlin("serialization", version = Versions.kotlin))
        classpath("com.squareup.sqldelight:gradle-plugin:${Versions.sqldelight}")
        classpath("com.github.ben-manes:gradle-versions-plugin:0.28.0")
        classpath ("androidx.navigation:navigation-safe-args-gradle-plugin:${Versions.navigation}")
        classpath ("org.jetbrains.kotlin:kotlin-android-extensions-runtime:${Versions.kotlin}")
    }
}

allprojects {
    repositories {
        google()
        mavenCentral()
        jcenter()
    }
}

//TODO("Probar bajando a kotlin version 1.3.72, y habilitando el android-extensions")
plugins {
  //kotlin("jvm") version "${Versions.kotlin}"
  id("org.jlleitschuh.gradle.ktlint") version "9.2.1"
  id ("com.github.ben-manes.versions") version "0.28.0"
  //kotlin("android") version "${Versions.kotlin}" apply false
  //id("org.jetbrains.kotlin.plugin.parcelize") version "${Versions.kotlin}"
}
apply(from = "quality/lint.gradle") 

所以我在androidApp和shared模块中创建了一个期望值(expect)和实际值(actual)的Parcelable和Parcelize类:

androidApp

actual typealias Parcelable = android.os.Parcelable

actual typealias Parcelize = kotlinx.android.parcel.Parcelize

在共享模块中

// Common Code

expect interface Parcelable

@UseExperimental(ExperimentalMultiplatform::class)
@OptionalExpectation
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.BINARY)
expect annotation class Parcelize()

所以在这些课程中,我收到了以下错误:

In Parcelable(shared)

Expected interface 'Parcelable' has no actual declaration in module KmpMVVMGooglePay.shared for JVM
Expected interface 'Parcelable' has no actual declaration in module KmpMVVMGooglePay.shared.iOSMain for Native

并且在Android类中:

Actual typealias 'Parcelable' has no corresponding expected declaration

Actual typealias 'Parcelize' has no corresponding expected declaration

我对实际/期望关键字的行为有什么误解吗?

提前感谢您的帮助!


嗨@manuel-lucas,你找到解决方案了吗? - Mathieu de Brito
还没有。我还在努力中... - Manuel Lucas
谢谢你的回答!我想我会尝试一下 https://github.com/icerockdev/moko-parcelize - Mathieu de Brito
我在下面的回答中提供了如何使用 @Parcelize 的示例。希望这能有所帮助。 - RealityExpander
2个回答

3
这些片段向您展示如何在KMM项目中使用Android的Parcelable来处理包括基元类型在内的任何类型的类。它展示了注释、接口、泛型、对象、@TypeParcelerParcelerParcelableParcelize的正确使用方式,以及如何为公共代码、iOS和Android实现每个平台。
该代码是必要的,以防止当Android应用程序进入后台时发生崩溃,并自动运行Parceler以保存状态。
iOS不使用Parcel,因此我们需要在iOS端设置桩代码。
在此示例中,我正在使用LocalDateTime的非本机可包装类作为非本机可包装类的示例。您可以使用任何类,只需更改实现即可。
build.gradle.kts(:shared)中。
plugins {
    kotlin("multiplatform")
    id("com.android.library")
    id("kotlin-parcelize") // add this
    id("kotlin-kapt") // add this
    // ...rest of defintions...
}
kotlin {
    android()
    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach {
        it.binaries.framework {
            baseName = "shared"
        }
    }

    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")  // LocalDateTime library written in Kotlin (can't use java libraries)
            }
        }
        // ...rest of definitions...
     }
  // ...rest of definitions...
}

commonMain/.../Platform.kt

import kotlinx.datetime.LocalDateTime

// For Android @Parcelize
@OptIn(ExperimentalMultiplatform::class)
@OptionalExpectation
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.BINARY)
expect annotation class CommonParcelize()

// For Android Parcelable
expect interface CommonParcelable

// For Android @TypeParceler
@OptIn(ExperimentalMultiplatform::class)
@OptionalExpectation
@Retention(AnnotationRetention.SOURCE)
@Repeatable
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
expect annotation class CommonTypeParceler<T, P : CommonParceler<in T>>()

// For Android Parceler
expect interface CommonParceler<T>

// For Android @TypeParceler to convert LocalDateTime to Parcel
expect object LocalDateTimeParceler: CommonParceler<LocalDateTime>

androidMain/.../Platform.kt

重要提示: 必须导入 kotlinx.parcelize.*,而不是 kotlinx.android.parcel.*

import android.os.Parcel
import android.os.Parcelable
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.toLocalDateTime
import kotlinx.parcelize.Parceler
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.TypeParceler

actual typealias CommonParcelize = Parcelize
actual typealias CommonParcelable = Parcelable

actual typealias CommonParceler<T> = Parceler<T>
actual typealias CommonTypeParceler<T,P> = TypeParceler<T, P>
actual object LocalDateTimeParceler : Parceler<LocalDateTime> {
    override fun create(parcel: Parcel): LocalDateTime {
        val date = parcel.readString()
        return date?.toLocalDateTime()
            ?: LocalDateTime(0, 0, 0, 0, 0)
    }

    override fun LocalDateTime.write(parcel: Parcel, flags: Int) {
        parcel.writeString(this.toString())
    }
}

iosMain/.../Platform.kt

import kotlinx.datetime.LocalDateTime

// Note: no need to define CommonParcelize here (bc its @OptionalExpectation)
actual interface CommonParcelable  // not used on iOS

// Note: no need to define CommonTypeParceler<T,P : CommonParceler<in T>> here (bc its @OptionalExpectation)
actual interface CommonParceler<T> // not used on iOS
actual object LocalDateTimeParceler : CommonParceler<LocalDateTime> // not used on iOS

../shared/commonMain/.../domain/note/Note.kt

这是您的领域类,将由 iOS 和 Android 共享使用

import kotlinx.datetime.LocalDateTime

@CommonParcelize
data class Note(
    val id: Long?,
    val title: String,
    val content: String,
    val colorHex: Long,

    @CommonTypeParceler<LocalDateTime, LocalDateTimeParceler>()
    val created: LocalDateTime,
): CommonParcelable {

    companion object {
        private val colors = listOf(RedOrangeHex, RedPinkHex, LightGreenHex, BabyBlueHex, VioletHex)
        fun generateRandomColor() = colors.random()
    }
}

我尝试实现@RawValue,但它没有文档记录(据我所知),而使用@TypeParcelers的上述方法对于任何特定类都非常有效。我将把这留给其他人作为练习!

示例项目:https://github.com/realityexpander/NoteAppKMM


1
只是一个简短的说明,不幸的是,这个方法不再适用于K2。请参考https://youtrack.jetbrains.com/issue/KT-58892。 - undefined
谢谢你提醒我。我查了一下票,似乎没有任何解决方法或计划。我想知道最终版本中这个包裹化功能将如何得到支持? - undefined
1
我的理解是它不会得到支持。我唯一能想到的方法是更新kotlin-parcelize插件,并将所有代码生成从前端移到后端。你可以尝试在这里提出一个bug:https://issuetracker.google.com/issues/new?component=971607&template=1516698。就我个人而言,我正在迁移到kotlinx-serialization,它也提供了真正的多平台支持。 - undefined
那么使用 kotlinx-serialization 就可以完全避免使用 Parcelize 了吗? - undefined
1
可以的。你可以将常见的类标记为Serializable,在Android上将它们保存为JSON字符串或CBOR字节数组放入Bundle中。 - undefined

2
你需要在 iOS 中为 Parcelable 使用一个空的“actual interface”。我不知道为什么它会给你 JVM 的错误,因为它不在你的目标中。我走过如何在通用代码中使用“Parcelable”和“@Parcelize”的步骤,请点击here

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接