#はじめに
Kotlin/NativeがBetaに到達しましたね!
AndroidエンジニアとしてはまだまだレガシーコードとJavaで戦ったりもするのですが、Androidアプリ開発といえばKotlinとなりつつある昨今、スマホアプリ開発としてはiOSアプリの開発もKotlinで書けるのは嬉しい限りです。
※他のプラットフォームも書けますが、そこは個人的な今後の課題です。
そんなわけで今回はKotlin/Nativeを使ってAndroid/iOSのアプリを作ってみます。
Kotlin/NativeのリポジトリのREADMEとKotlinサイトにあるKotlin/Nativeのチュートリアル「Multiplatform Project: iOS and Android」を参考にしました。
#開発環境
試してみた環境は下記の通りです。
-
Mac Mojave 10.14
-
AndroidStudio 3.2.1
-
Xcode 10.1
-
Kotlin 1.3.10
-
Gradle 4.10.2
#Kotlin/Nativeをインストール
まずはKotlin/Nativeをインストールします。
落としてきたプロジェクトのルートで下記2つのコマンドを実行します。
./gradlew dependencies:update
./gradlew bundle
READMEによるとMacBook Proで約1時間かかることがあるとのことです。
プライベートで使用しているMacBook Air(Early 2015)では5時間近くかかりました...ビルドが完了するとdistディレクトリ
以下にバイナリが吐き出されるのでbin
にパスを通します。
#共通ライブラリの作成
早速Android/iOSで使う共通ライブラリを作っていきたいのですが、AndroidStudioのProjectの作成から直接ライブラリを生成することはできません。
簡単な手順は、先にAndroidアプリのプロジェクトを作成し、その中でライブラリモジュールを作成する方法ですが、それは皆さんやっているのであえて別のアプローチをしてみました。
##共通ライブラリのフォルダを作成する
まずはFinderで任意の場所に共通ライブラリのフォルダを作成します。
次に共通ライブラリのフォルダを下記の手順でAndroidStudioに読み込ませます。
- import project (Gradle, Eclipse ADT, etc.)を選択
##build.gradleを編集する
作成したライブラリフォルダを読み込むと空のbuild.gradleファイルが生成されるので、下記のように編集します。
Androidプロジェクトを作成した時に自動作成されるbuild.gradleと同じ内容です。
編集したらgradle syncを実行します。
buildscript {
ext.kotlin_version = '1.3.10'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
##gradle-wrapper.propertiesを編集する
gradle/wrapper/gradle-wrapper.propertiesを開き、distributionUrlを下記のように編集します。
編集したらgradle syncを実行します。
distributionUrl=http\://services.gradle.org/distributions/gradle-4.10.2-all.zip
##ライブラリモジュールを作成する
いよいよライブラリモジュールを作成します。
[File]->[Nwe]->[New Module]を選択し、Java Libraryを作成します。
モジュール名、パッケージ名には特に決まりはありません。
普段ライブラリモジュールを作成するときと同じように作成します。
##共通ライブラリのソースコードを追加する
共通ライブラリのソースコードは、共通コードと各プラットフォーム向けのコードがあります。
深堀りはできていないのですが、共通コードでexpect
を使って抽象メソッドを定義し、各プラットフォーム向けのコードでactual
を使って実際の処理を書くと考えています。
チュートリアルにあるコードから気持ち変更をした下記のファイルを用意しました。
###共通のソースコード(モジュールルート/src/commonMain/kotlin/common.kt)
共通コードなので、expect
を使ってplatformName()
というメソッドを定義しています。
各プラットフォームからはcreateApplicationScreenMessage()
を呼ぶことで適当な文字列が取得できるというコードです。
package {パッケージ名}
expect fun platformName(): String
fun createApplicationScreenMessage() = "Hello Kotlin/Native for ${platformName()}"
Android用のソースコード(モジュールルート/src/androidMain/kotlin/actual.kt)
Androidプラットフォーム用のコードです。
actual
を使ってplatformName()
で「Android」と返すように定義しています。
package {パッケージ名}
actual fun platformName(): String = "Android"
iOS用のソースコード(モジュールルート/src/common/iosMain/kotlin/actual.kt)
チュートリアルに倣ってUIDevice
をimportしています。
SwiftやObjective-Cでのみ使用できるので、iOS用のコード内でのみ使うように注意が必要ですが、マルチプラットフォーム開発をする上で非常に便利です。
こちらもiOSプラットフォーム用にactual
を使って実際の処理を定義しています。
package {パッケージ名}
import platform.UIKit.UIDevice
actual fun platformName(): String
= UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
##共通ライブラリモジュールに生成されたbuild.gradleを編集する
共通ライブラリモジュールに生成されたbuild.gradle下記のように編集し、gradle syncを実行します。
実行後はsrc/main以下は不要になるので削除してOKです。
apply plugin: 'kotlin-multiplatform'
kotlin {
targets {
final def iOSTarget = System.getenv('SDK_NAME')?.startsWith("iphoneos") \
? presets.iosArm64 : presets.iosX64
fromPreset(iOSTarget, 'iOS') {
compilations.main.outputKinds('FRAMEWORK')
}
fromPreset(presets.jvm, 'android')
}
sourceSets {
commonMain.dependencies {
api 'org.jetbrains.kotlin:kotlin-stdlib-common'
}
androidMain.dependencies {
api 'org.jetbrains.kotlin:kotlin-stdlib'
}
}
}
// workaround for https://youtrack.jetbrains.com/issue/KT-27170
configurations {
compileClasspath
}
##iOS用にFrameworkファイルを生成
Androidは上記までの設定でプロジェクトにライブラリを読み込ませることができるのですが、iOSはFrameworkファイルを生成する必要があります。
共通ライブラリモジュールのbuild.gradleに下記の設定を追加しgradle syncを実行します。
task packForXCode(type: Sync) {
final File frameworkDir = new File(buildDir, "xcode-frameworks")
final String mode = project.findProperty("XCODE_CONFIGURATION")?.toUpperCase() ?: 'DEBUG'
inputs.property "mode", mode
dependsOn kotlin.targets.iOS.compilations.main.linkTaskName("FRAMEWORK", mode)
from { kotlin.targets.iOS.compilations.main.getBinary("FRAMEWORK", mode).parentFile }
into frameworkDir
doLast {
new File(frameworkDir, 'gradlew').with {
text = "#!/bin/bash\nexport 'JAVA_HOME=${System.getProperty("java.home")}'\ncd '${rootProject.rootDir}'\n./gradlew \$@\n"
setExecutable(true)
}
}
}
tasks.build.dependsOn packForXCode
その後、AndroidStudio内のターミナル(通常のターミナルアプリでもプロジェクトルートに移動すれば大丈夫です)で./gradlew build
を実行すると私の環境ではPermission deniedと言われました...
その場合は、chmod +x gradlew
を実行してから./gradlew build
を実行すればbuildディレクトリが生成され、「build/xcode-framework」に{ライブラリ名}.frameworkファイルができます。
#AndroidProjectの作成
共通ライブラリを利用するAndroidプロジェクトを作成します。
やることは基本的にJarライブラリを読み込ませる時と同じです。
##Androidプロジェクトを作成する
共通ライブラリ作成から順に進めている程で説明します。
[File]->[New]->[New Project]を選択し、Androidプロジェクトを作成します。
ポイントは、「include Kotlin support」にチェックを入れることですが、Kotlin/Nativeの共通ライブラリとAndroidプロジェクトはここでは別プロジェクトとして作成しているのでチェックを入れずにJavaのプロジェクトでも大丈夫です。
Kotlinで書かれたAndroidプロジェクトでKotlin/Nativeの共通ライブラリを使う方法はチュートリアル等で実証されているので、ここではJavaのAndroidプロジェクトを作成しました。
##共通ライブラリを読み込ませる
チュートリアルではAndroidプロジェクト内にライブラリモジュールを作成していたので、モジュールの読み込みを行えばよかったのですが、別プロジェクトなのでJarファイルを読み込ませる必要があります。
[File]->[Project Structure]を選択し、appモジュール
のDependenciesタブ
を開きます。
ウィンドウ下部の+をクリックするとメニューが開くので、「Jar dependency」を選択します。
共通ライブラリ作成の最後にbuildしたので「ライブラリモジュールルート/build/libs/{ライブラリ名}-android.jar」が作成されています。
##共通ライブラリのコードを利用する
共通ライブラリには文字列を取得するメソッドcreateApplicationScreenMessage()
を定義していました。
このメソッドで取得した文字列をTextViewに表示するため、xmlにTextViewを追加し、idをtextViewとして、MainActivityクラスに下記のように実装します。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv = findViewById(R.id.textView);
tv.setText(CommonKt.createApplicationScreenMessage());
}
}
Kotlinのプロジェクトであればメソッド名から書き始めても良いのですが、JavaではCommonKtから書き始める必要があります。
入力中にサジェストされるのでimport文は自動で入力されます。
##Hello Kotlin/Native for Androidと表示される
※textSizeはデフォルトだと小さすぎたので30dpに指定しました。
#iOSProjectの作成
次に、共通ライブラリを利用するiOSプロジェクトを作成します。
.frameworkファイルを読み込ませるだけだと思っていたのですが、色々やることがあり大変でした。
##iOSプロジェクトを作成する
まずはXcodeを起動してプロジェクトを作成します。
「Create a new Xcode project」を選択肢、「Single View App」を作成します。
##プロジェクトの設定
プロジェクトの設定を開いて下記の設定を行います。
###Embedded Binaries
「General」の中にある「Embedded Binaries」の+をクリックし、開いたウィンドウの「Add Otherボタン」から「ライブラリモジュールルート/build/xcode-frameworks/{ライブラリ名}.framework」を指定します。
オブション設定は「Create folder references」のままで大丈夫です。
###Enable Bitcode
Kotlin/Nativeは、LLVMのBitcodeではなく、完全ネイティブのバイナリを生成するため、「Build Settings」->「All」->「Combined」の中にある「Build Options」の「Enable Bitcode」をNoに変更し、Bitcode機能を無効にします。
###Framework Search Paths
「Build Settings」->「All」->「Combined」の中に「Search Paths」の設定がありあます。
ここの「Frameword Search Paths」にフレームワークを探すディレクトリを指定します。
補完が効かないので「ライブラリモジュールルート/build/xcode-frameworks」のパスをpwdコマンドを使って確認します。
チュートリアルには相対パスと書いてありますが、絶対パスで大丈夫です。
###Run Script Phase
「Build Phases」の+をクリックし、「New Run Script Phase」を選択します。
作成するRunScriptは下記です。
cd ライブラリモジュールルート/build/xcode-frameworks/
./gradlew :{ライブラリ名}:packForXCode -PXCODE_CONFIGURATION=${CONFIGURATION}
ここもチュートリアルでは相対パスが書いてありましたが絶対パスで大丈夫でした。
####RunScriptの並べ替え
新たに作成されたRunScriptはもともとある設定の一番下にあります。
今回作ったRUnScriptは「Compile Sources」より上におく必要があるのでドラッグ&ドロップで移動します。
「Run Script」と書かれている名前の部分もダブルクリックで変更できるのでわかりやすい名前に変更しても良いです。
##共通ライブラリのコードを利用する
ViewControllerを下記のように実装します。
ライブラリのimport文を書くことで、ライブラリのコードが参照できるようになります。
import UIKit
import {ライブラリ名}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 300, height: 21))
label.center = CGPoint(x: 160, y: 285)
label.textAlignment = .center
label.font = label.font.withSize(20)
label.text = CommonKt.createApplicationScreenMessage()
view.addSubview(label)
}
}
##Hello Kotlin/Native for iOS {OSバージョン}と表示される
まとめ
iOSアプリをSwift(Objective-C)を使わずKotlinで書くこともできるそうですが、今回はそこまで行けませんでした。
クロスプラットフォーム開発の一手段としてKotlin/Nativeで共通ライブラリを作るのはAndroidエンジニア的には普段から親しんでいるKotlinで書けるという点でポイントが高いと思います。
個人的にはKotlin/Nativeのビルドに時間がかかりすぎたので手軽とは言い難いですが...
なお、AndroidProject内でKotlinを使わない理由は無い(特にKotlin/Nativeを使うのであれば)ので、皆さんは素直にKotlinで書いてみましょう。