Compose MultiplatformでOSSライセンス表示を作る
Compose MultiplatformでOSSライセンス表示を作る方法として現時点ではLicenseeとAbout Librariesを利用する方法があります。今回はLicenseeを利用してOSSライセンス表示画面を作る方法を記載していきます。
前提知識
Licenseeとは
LicenseeはCash Appによって開発されているGradle Pluginです。Licenseeはアプリの依存関係を解析して、意図しないOSSライセンスが指定されているアーティファクトを誤って追加していないか、チェックできるツールです。
https://github.com/cashapp/licensee
Licenseeではチェックした結果をレポートとして出力してくれるのですが、そのなかのartifact.json
にはアプリの依存関係に含まれる全アーティファクトのOSSライセンス情報を含めてくれます。
https://github.com/cashapp/licensee#usage
レポート | 内容 |
---|---|
artifacts.json | アプリの依存関係の全アーティファクトのOSSライセンス情報を含むJSONファイルをレポートとして出力する。 |
validation.txt | 各アーティファクトとそのOSSライセンスの利用が許可されているかチェックした結果をレポートとして出力する。 |
今回はLicenseeが出力してくれるこのartifacts.json
を流用して、OSSライセンス表示を作成するというのやっていきます。
セットアップ
今回はCompose Multiplatform Wizardで生成したCompose Multiplatformプロジェクトをベースに実装を進めていきます。ちなみにCompose Multiplatform WizardでのCompose Multiplatformプロジェクトの作成はこちらの方法で進めています。
https://kaleidot.net/compose-multiplatform%e3%83%97%e3%83%ad%e3%82%b8%e3%82%a7%e3%82%af%e3%83%88%e3%82%92%e4%bd%9c%e6%88%90%e3%81%99%e3%82%8b/
依存関係
OSSライセンス表示を作る際にはKotlin Serialization・Kotlin Coroutines・Licenseeのアーティファクトが必要になります。なのでVersionCatalogのtomlファイルに定義を追加した後に、build.gradle.ktsの依存関係にアーティファクトを追加してやります。
名称 | 役割 |
---|---|
Kotlin Serialization | artifacts.jsonのJSONデータをオブジェクトに変換するために必要 |
Kotlin Coroutines | artifacts.jsonのJSONデータをIOスレッドで読み出すために必要 |
Licensee | artifacts.jsonを生成するために必要 |
︙ 省略
licensee="1.7.0"
kotlinx-serialization = "1.5.1"
kotlinx-coroutines = "1.7.3"
︙ 省略
[libraries]
︙ 省略
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
︙省略
[plugins]
︙省略
licensee = { id = "app.cash.licensee", version.ref = "licensee"}
kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
︙省略
plugins {
︙省略
alias(libs.plugins.licensee)
alias(libs.plugins.kotlinx.serialization)
}
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
kotlin {
︙省略
sourceSets {
︙省略
val commonMain by getting {
dependencies {
︙省略
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.coroutines.core)
}
}
val androidMain by getting {
dependencies {
︙省略
implementation(libs.kotlinx.coroutines.android)
}
}
val desktopMain by getting {
dependencies {
︙省略
implementation(libs.kotlinx.coroutines.swing)
}
}
︙省略
}
}
Licensee
LicenseeではどのOSSライセンスの利用を許可するか、許可しないか設定することができる仕組みになっています。今回の用途では許可するかどうかはあまり関係ないのですが、適切に許可しないとartifacts.json
を生成する際にエラーが発生します。本プロジェクトでは主にApache-2.0とMITライセンスを利用しているのでbuild.gradle.kts
のlicense
ブロックでApache-2.0とMITライセンスを許可するように設定しておきます。
licensee {
allow("Apache-2.0")
allow("MIT")
}
今回はlicenseeのセットアップ方法については本題にあまり関係がないので詳細については省きますが、セットアップ方法は以下のページに記載がありますので、必要に応じて確認してみてください。
https://github.com/cashapp/licensee#configuration
実装
セットアップが完了したのでOSSライセンス表示の実装を進めていきます。OSSライセンス表示の実装ですが大まかに以下の手順で進めていきます。
- Licenseeで
artifacts.json
を生成する - 生成された
artifacts.json
のJSONデータを読み込む - 読み込んだ
artifacts.json
のJSONデータをオブジェクトにする - 生成したライセンス情報のオブジェクトを画面に表示する
Licenseeでartifacts.json
を生成する
Licenseeでは./gradlew check
を実行するとプロジェクトにしている各ターゲット用にartifacts.json
を生成してくれます。今回のプロジェクトではAndroid・JVM・iOSX64・iOSArm64・iOSSimulatorArm64をターゲットに設定しています。なのでLicenseeは以下のように各ターゲットごとのartifacts.json
を生成するようになっています。
OS | パス | 内容 |
---|---|---|
Android | composeApp/build/reports/licensee/androidRelease/artifacts.json | Androidアプリ向け |
JVM | composeApp/build/reports/licensee/desktop/artifacts.json | デスクトップアプリ向け |
iOS X64 | composeApp/build/reports/licensee/iosX64/artifacts.json | x86-64プラットフォームで動作するiOSシミュレータ向け |
iOS Arm64 | composeApp/build/reports/licensee/iosArm64/artifacts.json | ARM64プラットフォームで動作するiOS・iPadOS向け |
iOS SimultorArm64 | composeApp/build/reports/licensee/iosSimulatorArm64/artifacts.json | Apple Siliconプラットフォームで動作するiOSシミュレータ向け |
というようにLicenseeはartifacts.json
を生成してくれるので、このartifacts.jsonをコピーしてCompose Multiplatformのresourceとして取り扱えるようにするupdateLicenseというタスクを定義します。
- LicenseFileではartifacts.jsonのコピー元と先を宣言する
- tasks.register(“updateLicense”)でupdateLicenseのタスクを宣言する
- updateLicenseのタスクではLicenseFileの定義を元にartifacts.jsonのコピーを実行する
- updateLicenseのタスクを実行する前にcheckタスクを実行するようにdependsOnをつけておく
enum class LicenseFile(
val from: File, val to: File
) {
Android(
from = File("composeApp/build/reports/licensee/androidRelease/artifacts.json"),
to = File("composeApp/src/commonMain/resources/licensee/android/artifacts.json")
),
IOSX64(
from = File("composeApp/build/reports/licensee/iosX64/artifacts.json"),
to = File("composeApp/src/commonMain/resources/licensee/iosX64/artifacts.json"),
),
IOSArm64(
from = File("composeApp/build/reports/licensee/iosArm64/artifacts.json"),
to = File("composeApp/src/commonMain/resources/licensee/iosArm64/artifacts.json"),
),
IOSSimulatorArm64(
from = File("composeApp/build/reports/licensee/iosSimulatorArm64/artifacts.json"),
to = File("composeApp/src/commonMain/resources/licensee/iosSimulatorArm64/artifacts.json"),
),
JVM(
from = File("composeApp/build/reports/licensee/desktop/artifacts.json"),
to = File("composeApp/src/commonMain/resources/licensee/desktop/artifacts.json"),
)
}
tasks.register("updateLicense") {
doFirst {
LicenseFile.values().forEach { item ->
item.from.copyTo(item.to, true)
}
}
}.dependsOn(tasks.check)
あとは./gradlew updateLicense
でupdateLicenseタスクを実行するだけです。updateLicenseタスクを実行するとLicenseeがartifacts.jsonを生成して、最終的にCompose Multiplatfromのresourceに登録されます。Compose Multiplatformのresourceに登録されるとaritfacts.json
をアプリから読み取れるようになります。
生成されたartifacts.json
のJSONデータを読み込む
Compose Multiplatformではresourceディレクトリに登録したファイルをresource(path: String)
でリソースとして取得する仕組みが用意されています。通常はresource(path: String)
に単一のパスを指定してリソースを取得すればよいですが、今回は各ターゲット向けに用意されたartifacts.jsonのリソースを取得する必要があります。
fun resource(path: String): Resource
なのでresource(path: String)
を利用して、各ターゲット向けのartifacts.jsonのリソースを取得できるように、getLicenseResourceという関数をexpectとactualを利用して定義します。
- CommonMainにてgetLicenseResourceをexpectで定義して、ターゲットごとに処理を実装できるようにする
- Andriod・iosX64・iosArm64・iosSimulatorArm64・JVMでgetLicenseResourceをactualで定義して、ターゲットごとに処理を実装していく。
expect suspend fun getLicenseResource(): Resource
actual suspend fun getLicenseResource(): Resource = resource("licensee/android/artifacts.json")
actual suspend fun getLicenseResource(): Resource = resource("licensee/desktop/artifacts.json")
actual suspend fun getLicenseResource(): Resource = resource("licensee/iosX64/artifacts.json")
actual suspend fun getLicenseResource(): Resource = resource("licensee/iosArm64/artifacts.json")
actual suspend fun getLicenseResource(): Resource = resource("licensee/iosSimulatorArm64/artifacts.json")
このようにgetLicenseFileを利用してリソースを取得するとターゲットが変われば読み込むartifacts.jsonが変わるというのを実現できます。ちなみにgetLicenseFileから取得したリソースですが以下のようなコードを記述するとartifacts.jsonからJSONデータを簡単に読み出すことができます。
val text = getLicenseResource().readBytes().decodeToString()
println(text)
読み込んだartifacts.json
のJSONデータをオブジェクトにする
artifacts.json
からJSONデータを読み出すことができたら、あとはJSONデータをデシリアライズして、OSSライセンス情報を表示するだけです。artifacts.jsonには以下のようなJSONデータが含まれているので、このJSONデータをデシリアライズできるようにしていきます。
{
"groupId": "androidx.activity",
"artifactId": "activity",
"version": "1.7.2",
"name": "Activity",
"spdxLicenses": [
{
"identifier": "Apache-2.0",
"name": "Apache License 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0"
}
],
"scm": {
"url": "https://cs.android.com/androidx/platform/frameworks/support"
}
},
︙ 省略
}
上記のJSONデータはKotlin Serializationでデシリアライズするので、JSONデータの各要素を格納するLicense・Scm・SpdxLicenseというデータクラスを定義してやります。
@Serializable
data class License(
val artifactId: String,
val groupId: String,
val name: String,
val scm: Scm,
val spdxLicenses: List<SpdxLicense>,
val version: String,
)
@Serializable
data class Scm(
val url: String,
)
@Serializable
data class SpdxLicense(
val identifier: String,
val name: String,
val url: String,
)
あとはJSONデータからOSSライセンス情報を取得する処理をKotlin Serializationで実装してやれば良いです。ちなみにwithContext(Dispatchers.IO)を呼び出していますがこれはJSONデータを呼び出す際にUIをスレッドをブロッキングしないように切り替えてやっています。
suspend fun readLicenses(): List<License> {
return withContext(Dispatchers.IO) {
val text = getLicenseResource().readBytes().decodeToString()
Json.decodeFromString<List<License>>(text)
}
}
生成したライセンス情報のオブジェクトを画面に表示する
artifacts.jsonからOSSライセンス情報を生成できるようになったので、あとはOSSライセンス情報表示する画面を組み立てるだけです。今回は以下のようなリストとカードでOSSライセンス情報を表示する画面を作成してみました。
@Composable
internal fun App() = MaterialTheme {
var isLoading by remember { mutableStateOf(false) }
var licenses by remember { mutableStateOf(emptyList<License>()) }
LaunchedEffect(isLoading) {
if (isLoading.not()) {
licenses = readLicenses()
isLoading = false
}
}
Box(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.safeDrawing)
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
} else {
LicenseList(
licenses = licenses,
modifier = Modifier.fillMaxSize()
)
}
}
}
@Composable
fun LicenseList(
licenses: List<License>,
modifier : Modifier = Modifier,
) {
LazyColumn(modifier) {
licenses.forEach { license ->
item { LicenseCard(license) }
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun LicenseCard(
license: License,
) {
Card(
modifier = Modifier.fillMaxWidth().padding(8.dp),
) {
Column(
modifier = Modifier.padding(8.dp),
) {
Text(text = license.name, style = MaterialTheme.typography.titleMedium)
Text(text = license.artifactId, style = MaterialTheme.typography.titleSmall)
Text(text = license.version, style = MaterialTheme.typography.titleSmall)
FlowRow {
license.spdxLicenses.forEach { spdxLicense ->
ClickableText(
buildAnnotatedString {
append(spdxLicense.name)
addStyle(
style = SpanStyle(
color = MaterialTheme.colorScheme.primary,
textDecoration = TextDecoration.Underline,
),
start = 0,
end = spdxLicense.name.length,
)
addStringAnnotation(
tag = spdxLicense.name,
annotation = spdxLicense.url,
start = 0,
end = spdxLicense.name.length,
)
},
onClick = {
openUrl(spdxLicense.url)
},
)
}
}
}
}
}
アプリを起動してみると以下のようにOSSライセンス情報が表示されます。内容についても確認してみるとAndroid・iOS・JVMで異なるアーティファクトが表示されているのでそれぞれのターゲット向けのartifacts.jsonがうまく読み出せていそうです。
おわりに
Compose Multiplatformはまだまだ開発途中ということもありOSSライセンス表示を作るのも一苦労です。幸いにもLicenseeというCompose Multiplatformで利用できるツールがあったので良かったかなと思います。
Compose Multiplatformではこのようにつまづくポイントが沢山あるのですが、一つ一つ解決していこうと思うのでまたこのような形で解決方法を紹介することができればなーと思います。