【KMP】既存のAndroid, iOSアプリにKotlin Multiplatformのsharedモジュールを作成する
こちらの記事の続きになります
新規開発だったらこの手順でいいと思いますが、世の中そんなに新規開発することってないですよね
開発当初にAndroid、iOSアプリを別々に作成してそれぞれメンテしているようなシチュエーションもよくあると思います
今回はそんな感じの場合を想定して、Kotlin Multiplatformでsharedモジュールを作成して両OSから呼び出せるAPIを作成していきたいと思います
サンプルプロジェクト
こちらにサンプルプロジェクトを作成しました
トップ階層がandroidのプロジェクトとなっており、ここに含まれるSharedExample以下がiOSのプロジェクトという構成になっています
それぞれのアプリを実行すると画像のようになります
+ボタンを押すと数字がインクリメントされるだけのシンプルなアプリになっています
このサンプルアプリにsharedモジュールを追加してボタンを押したら数値が2倍になるという動きに修正していきましょう
早速やっていきたいと思います
Android Studio設定の変更
Android Studioからsharedモジュールを作成していくことになるのですが、デフォルトの設定だとKotlin Multiplatformのモジュールテンプレートが表示されません
設定画面を開き、Advanced Settingsから「Enable experimental Multiplatform IDE features」を探してチェックを入れ、Android Studioを再起動しましょう(なんでこれがデフォルトオフなんだ・・・)
sharedモジュールを作成する
右クリックメニューを開いてNew>Moduleを選択してモジュールを追加していきます
Androidの設定を変更したおかげでKotlin Multiplatform Shared Moduleのテンプレートが表示されているので、選択して必要な情報を入力します
- Module Name:shared
- iOS framework distribution:Regular framework
- そのほかは適当に
これでプロジェクトにsharedモジュールが追加されました。この段階ではGradle syncがエラーになると思います
ビルドエラーの解決
今出ているエラーを解消してモジュールを利用できるようにします
build.gradle.ktsのエラー解消
追加したsharedモジュールにはbuild.gradle.ktsが含まれていますが、pluginsブロックに記載された「kotlinMultiplatform」「androidLibrary」の参照が解決できていません
これを解消するためにgradle/libs.versions.tomlを修正します。以下の2行を追加します
androidLibrary = { id = "com.android.library", version.ref = "agp" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
続いてこちらの記事にあるとおり、root階層のbuild.gradle.ktsを修正します
以下の2行をpluginsブロックの中に追加します
alias(libs.plugins.kotlinMultiplatform) apply false alias(libs.plugins.androidLibrary) apply false
最後にapp/build.gradle.ktsにsharedモジュールへの依存を追加します
以下の1行をdependenciesブロックの中に追加します
implementation(project(":shared"))
ここまでできたらGradleのsyncを実行します。そしたらなんかエラーが出ました
Unresolved reference. None of the following candidates is applicable because of receiver type mismatch:
想定外だったので原因を色々調べていたらlibs.versions.tomlのkotlinバージョンが1.9.0指定となっていたのが問題だったようです
なのでlibs.versions.tomlのversionsブロックに記載されているkotlinの値を以下に修正します
kotlin = "2.0.0"
これで再度syncを実行。すると今度はsourceSetsのcommonTest.dependenciesで同じエラー
これはlibs.kotlin.testが存在しないことが原因のため今回は削除で対応します
修正して再度sync
警告は残ってますがなんとかビルド成功までできました
APIの作成
sharedモジュールに数値を2倍にするAPIを作成していきます
両方のプラットフォームから呼び出せるAPIにするため、commonMain内にCalculatorクラスを追加します
実装はこんな感じ。追加してビルドしましょう
package com.jagapoko.shared class Calculator { fun double(number: Int): Int { return number * 2 } }
ここで再びエラーが出ました。Compose Compiler Gradle pluginが不足している場合に出るエラーのようです
Starting in Kotlin 2.0, the Compose Compiler Gradle plugin is required
なのでgradle/libs.versions.tomlとshared/build.gradle.ktsを修正してエラーを解消します
- libs.versions.toml
[versions] compose-runtime = "1.6.11" [plugins] compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } compose = { id = "org.jetbrains.compose", version.ref = "compose-runtime" }
- build.gradle.kts
plugins {
alias(libs.plugins.compose)
alias(libs.plugins.compose.compiler)
}
sourceSets {
commonMain.dependencies {
//put your multiplatform dependencies here
implementation(compose.runtime)
}
}
これでAPIの利用準備ができました
APIの利用
Android側からの利用
MainActivityのCounterScreenを以下のように修正します
fun CounterScreen(modifier: Modifier = Modifier) { // カウントの初期値を1に修正 var count by remember { mutableIntStateOf(1) } Column( modifier = modifier.run { fillMaxSize() .padding(16.dp) }, verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { Text(text = "$count", style = MaterialTheme.typography.headlineMedium) Spacer(modifier = Modifier.height(16.dp)) // 以下のボタンクリック時にCalculator.doubleを呼び出すように修正 Button(onClick = { count = Calculator().double(count) }) { Text(text = "+") } } }
これでビルドしたらまたまたエラー
Starting in Kotlin 2.0, the Compose Compiler Gradle plugin is required
なので、app側にもCompose Compilerの追加が必要のようです。app/build.gradle.ktsを先ほどと同じように修正しましょう
- app/build.gradle.kts
plugins { alias(libs.plugins.compose.compiler) }
これでビルドが通ればsharedモジュールの呼び出し実装も完了です
こんな感じで数値が2倍にされていくようになります
iOS側からの利用
iOS側からも呼び出してみましょう。XCodeでSharedExampleプロジェクトを開きます
プロジェクトを選択して「Build Phases」を選択し、+ボタンを押して「New Run Script Phase」を選択します
作成したRun Scriptを選択し、以下のスクリプトを追加します
cd "$SRCROOT/.." ./gradlew :shared:embedAndSignAppleFrameworkForXcode
作成したRun ScriptをCompile Sourcesの上に配置します
続いて「Build Settings」を開き、「User Script Sandboxing」をNoに設定します
これでsharedを利用するためのプロジェクト設定は完了です。ビルドしてみましょう
ビルドログを確認して追加したRun Scriptが動作しており、エラーが出てなければOKです
ContentViewを以下のように修正します
import SwiftUI import shared // sharedのimportを追加 struct ContentView: View { @State private var count: Int = 1 // カウントの初期値を1に修正 var body: some View { VStack { // 現在のカウントを表示 Text("\(count)") .font(.title) .padding() // カウントをインクリメントするボタン Button(action: { // Calculatorの呼び出しを追加(Int32, Intのキャストが必要) count = Int(Calculator().double(number: Int32(count))) }) { Text("+") .font(.headline) .padding() .background(Color.blue) .foregroundColor(.white) .cornerRadius(8) } } .padding() .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(.systemBackground)) } }
これでビルドエラーがなければ実行してみましょう
こんな感じで同じように数値が2倍になっていれば成功です
最後に
こんな感じですでに存在するアプリ向けにKotlin Multiplatformを導入するための手順を紹介しました
追加直後はエラーと格闘することになりますが、エラーが解消してしまえば気軽に共通処理をKotlinで実装できるのではないかと思います
追加前後での差分はこんな感じでした。何かの参考になればと思います
【KMP】Kotlin Multiplatformでクロスプラットフォームアプリを作る(プロジェクト新規作成)
以前こちらの記事で書いた通り、プロジェクト作成方法をまとめておこうと思います
開発環境
記事作成時点での開発環境は以下になります
- Android Studio Koala Feature Drop | 2024.1.2
- XCode Version 15.2
使用しているOSはMacです
KMPプロジェクトの作成
公式サイトのこちらの手順に沿って進めていきます
Android Studioでの開発環境はすでに構築済みの前提になります
Kotlin Multiplatform pluginの導入
クロスプラットフォーム用のプロジェクトを作成するために、Kotlin Multiplatform pluginを導入します
Android Studioを立ち上げた時に出るウィザードでPluginsを選択し、検索窓に「Kotlin Multiplatform」と入力
画像の通りKotlin Multiplatformのプラグインが出てくるので「Install」を押します
インストールが完了すると「Restart IDE」のボタンが出てくるので、ボタンを押してAndroid Studioを再起動します
プロジェクトの作成
再起動が完了したらウィザード画面で「New Project」を選択
メニューから「Kotlin Multiplatform App」を選択してNextを押します
次の画面ではプロジェクト情報を入力
Build configuration languageは特に理由がなければKotlin DSLのままにしておきましょう
さらに次の画面では、Android・iOS・共有モジュールの名前を設定します
当然ですがそれぞれの名前は被らないようにつけましょう。あとでバグります
iOS framework distributionは特に理由がなければRegular frameworkを選びましょう
新規作成のクロスプラットフォームアプリであればわざわざCocoaPodsを導入しなくてもよいのではと思います
すべて入力できたら「Finish」を押してプロジェクト作成完了です
Androidアプリの実行
プロジェクトを作成すると必要なパッケージのダウンロードなどが始まります(最初は結構時間がかかると思います)
全て完了して、シミュレータor実機の実行環境が整っていれば「Run」ボタンを押しましょう
画像のように「Hello, Android XXX!」と出ていれば成功です
iOSアプリの実行
Android StudioからiOSアプリの実行もできますが、ここではXCodeを使ってビルド&実行していきます
プロジェクトフォルダを開き、iOSモジュールのフォルダ(ここではsampleapp-ios)のsampleapp-ios.xcodeprojを開きます
XCodeが開いたら早速実行ボタンを押しましょう
画像のように「Hello, Android XXX!」と出ていれば成功です
おわりに
ここまでKotlin Multiplatformのプロジェクトを作成して、クロスプラットフォームアプリを新規作成する手順をまとめました
今回は新規作成についてまとめましたが、既存のAndroid・iOSプロジェクトにKotlin Multiplatformを導入する方法もそのうちまとめたいと思います
【KMP】Kotlin MultiplatformでJSONファイルをリソースに追加して読み込む
最近はKotlin Multiplatformをよく触るようになりました
つまづくことが多い割に日本語の記事も少ないので、調べたことをまとめておこうと思います(みんなどこで調べてるんだろう)
ソースコード
作成したサンプルは以下のGitHubリポジトリに上げています
開発環境
作成時点での開発環境は以下になります
- Android Studio Koala Feature Drop | 2024.1.2
- Kotlin Multiplatformプラグイン 0.8.3(241)-9
プロジェクトの設定
こちらの公式サイトを参考にプロジェクト作成後からリソースファイルの読み込みまでの手順を記載していきます
プロジェクト作成方法は別途まとめておきたいと思います
必要な依存関係の追加
モジュールでリソースを扱えるようにするためにcompose.components.resourcesを利用するのですが、そのために必要な依存関係を追加していきます
ここには修正内容のみ記載するので全体を見たい方はリポジトリの方をご覧ください
gradle/libs.versions.toml
依存関係のバージョン管理にはlibs.versions.tomlを利用しているのでこちらに以下の情報を追加します
[versions] compose-runtime = "1.6.11" [plugins] compose = { id = "org.jetbrains.compose", version.ref = "compose-runtime" }
shared/src/build.gradle.kts
自分はsharedモジュールの方でリソースを使いたかったのでこちらのbuild.gradle.ktsに依存関係を追加しています
composeとcompose-compilerをpluginsブロックに追加します
plugins { alias(libs.plugins.compose) alias(libs.plugins.compose.compiler) }
commonMain.dependenciesに以下を追加します
sourceSets { commonMain.dependencies { implementation(compose.runtime) implementation(compose.components.resources) } }
これで必要な依存関係の追加は完了です
composeResourcesディレクトリの作成
こちらに記載がある通り、リソースを利用するためにはcomposeResourcesディレクトリを利用する必要があります
commonMain以下に作成することでプラットフォーム共通のリソースとして使用することができます
以下の画像のようにディレクトリを作成しましょう
ここで各ディレクトリには以下の規則で利用する必要があるようです
- drawable: 画像
- font: フォント
- values: 文字列
- files: その他のファイル
今回はJSONファイルをリソースとして利用したいのでfilesディレクトリに配置します
ファイルを配置したらビルドしてみましょう
build/generated/compose/resourceGenerator/kotlin/commonResClass以下に自動生成されたResがあれば多分準備完了です
sharedモジュールにJsonを読み込むAPIを追加する
今回は元々存在するGreetingクラスのgreetをリソースを読み込むためのAPIに変更していきます
@OptIn(ExperimentalResourceApi::class) suspend fun greet(): String { val bytes = Res.readBytes("files/fruits_data.json") return bytes.decodeToString() }
ここでのポイントは以下の通りです。まだExperimentalなのは注意が必要ですね
- ResのreadBytesでファイルを読み込める
- readBytesがExperimentalResourceApiなので@OptIn(ExperimentalApi::class)をつける
- readBytesを扱うAPIはsuspendにする
- リソースはfilesディレクトリからのパスを指定する
- byte arrayが返るのでStringにデコードする
動かしてみる
androidApp, iosApp側のgreetを呼ぶソースをsuspend対応のものに修正して動かしてみます
どちらもちゃんと読み込めていますね
![](https://cdn-ak.f.st-hatena.com/images/fotolife/w/whitedog0215/20241017/20241017235943.png)
![](https://cdn-ak.f.st-hatena.com/images/fotolife/w/whitedog0215/20241018/20241018001435.png)
【C#】一番手っ取り早いSystem.Text.Jsonのデシリアライズ
年に1回ぐらいJsonを扱うんだけど、毎回忘れるので手順を残します
Jsonのデシリアライズでつまずきたくない人向け
※サンプル作成にあたり、以下のWebサイト様&WebAPIを使用させていただきました
ありがとうございました
デシリアライズするJson
{ "publicTime": "2021-01-09T17:00:00+09:00", "publicTime_format": "2021/01/09 17:00:00", "title": "東京都 東京 の天気", "link": "https://www.jma.go.jp/jp/yoho/319.html", "description": { "text": "東京都では、強風や高波、急な強い雨、落雷、空気の乾燥した状態が続くため、火の取り扱い、霜に対する農作物の管理に注意してください。\n\n日本付近は、強い冬型の気圧配置となっています。\n\n東京地方は、晴れや曇りとなっています。\n\n9日は、強い冬型の気圧配置が続きますが、気圧の谷や湿った空気の影響を受けるため、晴れ時々曇りとなるでしょう。伊豆諸島では、雷を伴い雪や雨の降る所がある見込みです。\n\n10日は、冬型の気圧配置は次第に緩み、高気圧に覆われますが、気圧の谷や湿った空気の影響を受けるため、晴れで朝晩は曇りとなる見込みです。伊豆諸島では、雷を伴い雪や雨の降る所がある見込みです。\n\n<天気変化等の留意点>\n伊豆諸島南部では、9日は、曇りで雷を伴い雪や雨の降る所があるでしょう。\n10日は、曇りで雷を伴い雪や雨の降る所がある見込みです。\n(雨の予想)\n9日18時から10日18時までに予想される24時間降水量は、多い所で、伊豆諸島南部20ミリの見込みです。\n\n【関東甲信地方】\n関東甲信地方は、晴れや曇りで、雪の降っている所があります。\n\n9日は、強い冬型の気圧配置が続くため、晴れや曇りで、長野県や関東地方北部では雪の降る所があるでしょう。伊豆諸島では、雷を伴って雪や雨の降る所がある見込みです。\n\n10日は、冬型の気圧配置は次第に緩み、高気圧に覆われますが、気圧の谷や湿った空気の影響を受ける見込みです。このため、晴れや曇りで、長野県や関東地方北部では雪の降る所があるでしょう。伊豆諸島では、雷を伴って雪や雨の降る所がある見込みです。\n\n関東地方と伊豆諸島の海上では、9日から10日にかけて、うねりを伴いしけるでしょう。船舶は高波に注意してください。", "publicTime": "2021-01-09T16:35:00+09:00", "publicTime_format": "2021/01/09 16:35:00" }, "forecasts": [ { "date": "2021-01-09", "dateLabel": "今日", "telop": "晴時々曇", "temperature": { "min": null, "max": null }, "chanceOfRain": { "00-06": "--%", "06-12": "--%", "12-18": "--%", "18-24": "10%", "T00_06": "--%", "T06_12": "--%", "T12_18": "--%", "T18_24": "10%" }, "image": { "title": "晴時々曇", "url": "https://weather.tsukumijima.net/icon/2.gif", "width": 50, "height": 31 } }, { "date": "2021-01-10", "dateLabel": "明日", "telop": "晴時々曇", "temperature": { "min": { "celsius": "-2", "fahrenheit": "28.4" }, "max": { "celsius": "6", "fahrenheit": "42.8" } }, "chanceOfRain": { "00-06": "10%", "06-12": "0%", "12-18": "0%", "18-24": "10%", "T00_06": "10%", "T06_12": "0%", "T12_18": "0%", "T18_24": "10%" }, "image": { "title": "晴時々曇", "url": "https://weather.tsukumijima.net/icon/2.gif", "width": 50, "height": 31 } }, { "date": "2021-01-11", "dateLabel": "明後日", "telop": "曇時々晴", "temperature": { "min": null, "max": null }, "chanceOfRain": { "00-06": "--%", "06-12": "--%", "12-18": "--%", "18-24": "--%", "T00_06": "--%", "T06_12": "--%", "T12_18": "--%", "T18_24": "--%" }, "image": { "title": "曇時々晴", "url": "https://weather.tsukumijima.net/icon/9.gif", "width": 50, "height": 31 } } ], "location": { "city": "東京", "area": "関東", "prefecture": "東京都" }, "copyright": { "link": "https://weather.tsukumijima.net/", "title": "(C) 天気予報 API(livedoor 天気互換)", "image": { "width": 120, "height": 120, "link": "https://weather.tsukumijima.net/", "url": "https://weather.tsukumijima.net/logo.png", "title": "天気予報 API(livedoor 天気互換)" }, "provider": [ { "link": "https://www.jma.go.jp/jma/", "name": "気象庁 Japan Meteorological Agency", "note": "気象庁 HP にて配信されている天気予報を json データへ編集しています。" } ] } }
NugetパッケージにSystem.Text.Jsonを追加
「text.json」とかで検索をかけてインストールを押す
Jsonをデシリアライズするためのクラスを作る
とりあえず、Tenkiとか名前を付けてファイルを作成
Jsonデータをコピーして、Visual Studioの「編集」-> 「形式を選択して貼り付け」-> 「Jsonをクラスとして貼り付ける」を選択
こんな感じでクラスが自動生成される。超便利。
クラス名がRootObjectになっているので修正しておく
「形式を選択して貼り付け」が無い場合
Visual Studio Installerから「ASP.NETとWeb開発」のコンポーネントを追加する
デシリアライズする
以下のコードでデシリアライズを実行する。
var tenki = JsonSerializer.Deserialize<Tenki>(jsonStr);
ちゃんとできてますね。
【C#】異なるn個のものからr個選ぶ組み合わせを列挙する(Combination)
作ったもの
入力のリストと選ぶ個数を渡すと組み合わせを列挙してくれるCombinationクラスを作りました
ほんとはyield returnで作って拡張メソッドにしたかったんですが、生成速度が遅くなる(自分の実力不足)のと動きが追っかけにくいのでこの形にしました
再帰とyield return組み合わさると難しすぎんか・・・
ソースコード
static class Combination { private static List<List<int>> _comb; public static List<List<int>> Generate(int n, int r, bool dupulication) { _comb = new List<List<int>>(); CalcCombination(new List<int>(), n, r, dupulication); return _comb; } private static void CalcCombination(List<int> list, int n, int r, bool dupulication) { if (list.Count == r) { _comb.Add(new List<int>(list)); return; } var index = 0; if (dupulication) { index = list.Any() ? list.Last() : 0; } else { index = list.Any() ? list.Last() + 1 : 0; } for (int i = index; i < n; i++) { list.Add(i); CalcCombination(list, n, r, dupulication); list.Remove(i); } } }
使い方
Generateメソッドは異なるn個のものからr個取ってくる組み合わせを列挙した結果を返します
dupulicationには重複あり、無しを指定します
異なる5個のものから3個取ってくる組み合わせを列挙すると
重複ありの場合
Combination.Generate(5, 3, true)
重複無しの場合
Combination.Generate(5, 3, false)
となります
これを配列やリストの添え字に使ってやればなんでも組み合わせ列挙できますね
解説
組み合わせの列挙
CalcCombinationを再帰呼び出ししてlistに要素を追加していきます
listの要素数がr個になったら_combにリストのコピーを追加します
コピーした後は最後の要素を取り除いてから次の要素をlistに追加します
重複ありの場合
列挙の処理の中で{0, 1, 2}, {2, 0, 1}, {1, 2, 0}のような重複を避けたいので、必ず前の要素<= 後ろの要素になるようにします
listに最後に追加した要素からforループが始まるようにして前の要素<= 後ろの要素を実現しています
重複無しの場合
必ず前の要素 < 後ろの要素であればよいので、listに最後に追加した要素+1からforループが始まるようにする
木での表現
上記の内容を木にするとこんな感じ
重複ありの木
重複無しの木
速度
n=55, r=5, 重複ありの組み合わせ(約5000000通り)を列挙するのに大体1500msでした
C# doubleをintにキャストするときはちゃんとMath.Roundする
タイトルそのままの記事です
問題のコード
1.01から10.0まで、それぞれに100を掛けた値を整数で出力するプログラム
double value = 1.01; while (value < 10.0) { var result = value * 100; Console.WriteLine($"{result} --- Cast --->{(int)result}"); value += 0.01; }
出力結果は101~1000になってほしいが、これを実行するとこんな感じになる
999.9999999999832はキャストしたら1000になってほしいが小数点以下が切り捨てられて999にされている
対策
Math.Roundを使いましょう
double value = 1.01; while (value < 10.0) { var result = Math.Round(value * 100); Console.WriteLine($"{result} --- Cast --->{(int)result}"); value += 0.01; }
これで良し
初歩的なことだけどたまに忘れるとハマるやつ
C# 2次元リストのコピー
C# でリストAの中身をリストBにコピーしてリストBで値の変更などをしたい場合、以下のようにすれば値渡しでコピーができる
var listA = new List<int> { 1, 2, 3 }; var listB = new List<int>(listA); // 値渡し // listB = listAは参照渡し listB[0] = 3; listB[1] = 3; listB[2] = 3; Console.WriteLine(string.Join(" ", listA)); // 1 2 3 Console.WriteLine(string.Join(" ", listB)); // 3 3 3
けど2次元リストで同じ方法を使うと入れ子になったリストが参照渡しになってしまう
var listA = new List<List<int>> { new List<int>{1, 2, 3}, new List<int>{1, 2, 3}, }; // 外のリストは値渡し、入れ子のリストは参照渡し var listB = new List<List<int>>(listA); listB[1][0] = 3; listB[1][1] = 3; listB[1][2] = 3; foreach (var item in listA) Console.WriteLine(string.Join(" ", item)); foreach (var item in listB) Console.WriteLine(string.Join(" ", item)); // 出力結果 // 1 2 3 3 3 3 // 1 2 3 3 3 3
listAは1 2 3 1 2 3のままでいてほしいのに1 2 3 3 3 3になってしまっている
しょうがないので2次元リストをDeepCopyする拡張メソッドを作る
namespace Extension { public static class Extensions { public static List<List<T>> DeepCopy<T>(this List<List<T>> source) { var copyList = new List<List<T>>(); foreach (var item in source) { copyList.Add(new List<T>(item)); } return copyList; } } }
これでOK
var listA = new List<List<int>> { new List<int>{1, 2, 3}, new List<int>{1, 2, 3}, }; var listB = listA.DeepCopy(); listB[1][0] = 3; listB[1][1] = 3; listB[1][2] = 3; foreach (var item in listA) Console.WriteLine(string.Join(" ", item)); foreach (var item in listB) Console.WriteLine(string.Join(" ", item)); // 出力結果 // 1 2 3 1 2 3 // 1 2 3 3 3 3