2019/01/30
Kotlinのアップデートによりいくつか変更点があります。
一応追記していますが、追いきれない部分もありますので各自確認お願いします。
個人的にKotlin/Nativeはマルチプラットフォームの銀の弾丸になり得るポテンシャルを持っていると思っています。 しかし、まだKotlin/Nativeはβ版のため実用段階には至っていませんが、ついに先日1.0のtagが切られました!
2018年10月12日にも1.0でタグが切られていたのですが、正式リリースではないので23日に入れ替わっていました。
運用次第なので、どうなるかわかりませんが1.0の正式リリースが近いのは間違いありません。
この記事では、普段AndroidかiOSのどちらかを開発している方がほとんどだと思いますので、なるべく詳しくKotlin/NativeでAndroidとiOSのマルチプラットフォームアプリを作る方法を解説したいと思います。 特にgradleの設定周りは普段JVM系の言語を触る機会が少ないと思いますので、丁寧に解説したいと思います。
なお、この記事はMultiplatform Project: iOS and Android
がベースになっています。(順番、内容等はわかりやすいように変わっています。)
今回のサンプルのソースコードはGitHub に上がっていますので参考にしてみて下さい。
Base Projectの準備
開発環境
Android
IntelliJ IDEA を使う事も出来ますし、IDEAがベースとなっているAndroid Studio を使っても開発することが出来ます。 普段私はIntelliJ IDEAを使って開発をしているのですが、おそらくAndroid Studioに慣れている人の方が多いと思うので今回は無料のAndroid Studioを使って解説をしたいと思います。 ただし、AndroidStudio3.2.1で試しているとgradleのsyncが時々上手く出来なかったりするので、IntelliJ IDEAが使える人はそちらを使うことをオススメします。Android StudioはCanary Releaseを含めると更新頻度は高いほうだと思います。そのため最新版のAndroid Studioが上手く動かない場合は前のバージョン を使ったりします。
iOS
iOS側も今まで通りXcodeを使って開発することが可能です。AppCodeが使える方は補完などの機能がAppCodeの方が優れているので、こちらの方が開発しやすいと思いますがStoryBoardが使えません。 Xcodeに慣れている方はそのままXcodeを使うと良いと思います。この記事ではXcodeで解説します。
作るアプリ
各プラットフォーム毎に表示が異なるHello Worldのようなアプリを作ります。
- Android
- iOS
プロジェクト作成
Create new project
プロジェクトはgradleで管理していくので、Androidアプリから作成していきます。
Start a new Android Studio project
を選択しましょう。
Application nameはプロジェクトの名前になります。
Company domainはpackageのnamespaceになります。
当然Kotlinを使うのでInclude Kotlin support
のチェックを入れましょう。
minSDKは5.0以上の方が幸せになれます。
一旦 Empty Activity
で作ります。
名前はそのままMainActivityが一般的に用いられます。
プロジェクトが作成されました。
左に表示されている(されていない場合は⌘1)パッケージTreeが見辛いので、AndroidではなくProjectを選択するとPackageの構成が見やすくなります。
Kotlin
10/30にKotlin1.3がリリースされたのでこちらのeapの設定は不要です。(2018/10/30追記)
執筆時点ではまだβ版なのでEAP(Early Access Preview)を選択出来るようにします。
2018年10月23日時点では 1.3.0-rc-190
を選択します。
なお、Kotlin1.3が正式リリースされた場合この設定は不要になります。正式リリース版を選択してください。
プロジェクトのrootにあるbuild.gradleはプロジェクト全体の設定を司るファイルになります。各ディレクトリ(module)毎にもbuild.gradleを配置して管理するのが一般的です。
まずは、rootにあるgradleファイルのbuild script/repositoriesとallprojects/repositoriesに
maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' }
を追加します。
buildscript {
ext.kotlin_version = '1.3.0-rc-190'
repositories {
google()
jcenter()
maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' }
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
jcenter()
maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' }
}
}
Gradle
次に gradle/wrapper/gradle-wrapper.properties
にあるdistributionUrl
をこのようにします。
distributionUrl=https\://services.gradle.org/distributions/gradle-4.7-all.zip
gradleはバージョンアップと共にbuildが早くなったり良いことがあるので、なるべく最新版を使うことをオススメしますが、
5.1等はsyncの度にKotlin/Nativeの依存をダウンロードするためビルドがとても遅く感じます。そのため現状は4.7を使う方が良いかと思います。
一応回避方法
はあるので、最新バージョンを使いたい場合はgradleにこちらの設定を書きましょう。
また、annotation processorを使っていたりすると動かなくなることがよくあるので、既存プロジェクトで上げる場合は注意が必要です。
最新バージョンはこちら
から確認出来ます。
Gradle Cache
開発を快適に行うためにAndroid Studioにある設定から “Build, Execution, Deployment > Build Tools > Gradle” にある Offline work
を有効にしましょう。この設定をするとGradle syncの度にリモートのリポジトリからライブラリをダウンロードしなくなります。なお取得したライブラリのcacheは~/.gradle/caches/
に保存されます。cacheに存在しない新しいバージョンもしくはライブラリを追加した場合は、offline workにチェックが無いと動作しないので気をつけて下さい。
Common Module
次にAndroid, iOS, Web(今回は無い), Server(今回は無い), で使う共通のコードを追加します。
左のプロジェクトツリーのrootディレクトリの部分にカーソルを合わせて右クリック> New > Moduleの順にクリックします。
Android Studioからでは、(IntelliJ IDEAも 今後追加される事を期待)まだメニューから直接Kotlin/Nativeのcommon moduleを作成することが出来ないので一旦、Java Libraryを選択します。(現状何を選んでも一緒ですが一番変更が少ないです)
Library nameはcommon
にしました。(本家の名前はShareCodeになっています)
Java class nameは消すので名前は何でも良いです。
commonにMyClass.javaというJavaファイルとbuild.gradleが出来たと思います。
また、プロジェクトrootの settings.gradle
にcommonのmoduleが追加されたと思います。
settings.gradleはmoduleをプロジェクトのgradleに認識させるためのもので、自分でmoduleをimportや追加してもsettings.gradleに追加しなければgradleプロジェクトからは読み込まれませんので注意してください。
現時点ではこのようになります。
include ':app', ':common'
次に、各プラットフォーム用のコードを追加していきます。 MyClass.javaは不要なので消しましょう。
common module source
src/commonMain/kotlin/[your package]
のディレクトリを追加します。
私の場合は
src/commonMain/kotlin/com/github/aakira/kotlinnativesample/common
になります。
そこに共通で使用する common.kt
を追加します。
package com.github.aakira.kotlinnativesample.common
expect fun platformName(): String
fun createApplicationScreenMessage() : String {
return "Kotlin Rocks on ${platformName()}"
}
android module source
次にcomomnと同様にandorid用のディレクトリとsourceを追加します。
私の場合は
src/androidMain/kotlin/com/github/aakira/kotlinnativesample/common/actual.kt
になります。
ちなみにIntelliJでは、shift+command+F6
でrenameと関連するソースのrefactoringを一気にやってくれるので覚えておくと良いです。
package com.github.aakira.kotlinnativesample.common
actual fun platformName(): String {
return "Android"
}
iOS module source
iosも同様です。
私の場合は
src/iosMain/kotlin/com/github/aakira/kotlinnativesample/common/actual.kt
になります。 次のgradleの設定をするまで動作はしないのですが、一旦コピペしてください。
package com.github.aakira.kotlinnativesample.common
import platform.UIKit.UIDevice
actual fun platformName(): String {
return UIDevice.currentDevice.systemName() +
" " +
UIDevice.currentDevice.systemVersion
}
gradle
最後に、commonに追加されたbuild.gradleを編集します。
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
}
ここまで設定したら、Gradle Syncしてみましょう。 Syncは、このボタンで出来ます。Gradleファイルを変更するとIDE側でsyncする?と聞かれますが、自分で行う場合はこのボタンを押しましょう。
Common moduleの解説
common moduleには共通で使うコードを追加していきます。しかし、各プラットフォームによって関数の結果を変えたい事があると思います。
Kotlin/Nativeでは共通のコードに対して、expect
と actual
を用いて対処しています。
わかりやすく例えるなら abstract
と override
の様な関係です。common moduleで定義したexpectに対して各プラットフォーム用のディレクトリにあるコードでactualを実装してあげます。こうすることで、疎結合を保ったまま各プラットフォームによって期待する動作を出し分ける事が出来ます。
実装
Androidアプリ
先程common moduleで作ったコードをAndroidアプリのコードから呼び出してみます。
各moduleから別のmoduleを呼び出すには、呼び出し元module配下のbuild.gradleのdependenciesに呼び出し先module名を追記します。
dependencies {
implementation project(':common')
...
}
このようにすることで、他のmoduleをライブラリという扱いで読み込むことが出来ます。下にもいくつか書かれていると思いますが、ライブラリをimportする場合はここに追加していきます。今回のようにローカルのmoduleを読み込む事もできるし、maven等のリモートリポジトリからインターネット経由で取得することも可能です。
新しいリモートの参照先から取得する場合はKotlinのeapを取得する時に追加したようにプロジェクトroot配下のbuild.gradleに追加します。
repositories {
google()
jcenter()
...
}
それでは、app moduleにあるMainActivity.ktを見てみましょう。 AndroidではAndroidManifest.xmlのActivityのintent filterにこのように定義されているActivityが一番最初に呼ばれます。
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
手順通りプロジェクト作成した場合はMainActivityがアプリ起動時に呼び出されるので、このファイルを操作していきます。
Androidではlayoutファイルはxmlに記述するのが一般的です。もちろんコードからviewを追加することも可能ですが、xmlを使う方が良いでしょう。
layoutファイルがxmlで記述されているということは、当然コードとxmlで書かれたviewのヒモ付が必要となります。
先ずは、res/layout/activity.xml
にあるファイルにコードから認識させるためにidを追加します。
Hello World!
はデフォルトテキストで不要なので消しました。深く解説をすると長くなるのでここでは省きますが、android:id="@+id/textView"
このように任意のidをviewに対してつけることが出来ます。このidがソースコードとの紐付けに必要となります。
また、デフォルトの文字サイズだと少し小さいので android:textSize="32sp"
の行を追加して、文字サイズを調整しました。
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="32sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</android.support.constraint.ConstraintLayout>
次に、Kotlinファイルを開きましょう。
onCreateの中にtextViewという変数を追加してviewとバインディングをします。実際のプロダクトではこの様な書き方はしないのですが、ここでは簡略化するためこのように書きます。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val textView = findViewById<TextView>(R.id.text_view) // viewのbinding
textView.text = createApplicationScreenMessage() // viewのtextに対して値をセット
}
}
ここまで設定が間違っていなければ createApp
ぐらいまで入力するとsuggestされて勝手にimport文が追加されると思います。Xcodeではimportを自動で挿入してくれないので、普段Xcodeを使っている方は便利と感じるのでは無いでしょうか。
準備が整ったので実行してみましょう。
Android Studio上で行う場合はToolBarにある緑色の三角ボタンを押すと実行が出来ます。端末が繋がっていない場合はエミュレータを使いましょう。
Android端末上でKotlin Rocks on Androidと表示されたと思います。
iOSアプリ
次にiOSアプリを作っていきます。Xcodeを起動して Create a new Xcode project
を選択します。
SingleView Appを選びます。
product nameを任意の名前にしてcrateします。ここからだとパッケージ名の統一が出来ないので後で変更すると良いです。
andoridはgradleから直接読み込みをしたのですが、iOSではbuildの生成ファイルから読み込む必要があります。
しかし、現状のままではiOS用のコードが生成されていません。
そこで、common moduleのbuild.gradleにiOS用のコード生成を行うための設定を追加します。
...
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
gradleにこの設定を追加して、プロジェクトrootのディレクトリで./gradlew build
コマンドを実行すると、common moduleで作成したファイルがbuildディレクトリに追加されます。
buildディレクトリを覗いてみると、common/build/xcode-frameworks/common.framework
配下にiOS用のlibraryファイルが追加されていると思います。このファイルをXcodeから読み込みます。
Kotlin 1.3.20からデフォルトの名前はcommon.frameworkではなくmain.frameworkに変更されました
もちろん好きな名前に変更することも出来ます (2019/1/30 追記)
- General > Embedded Binaries
先ずは、General > Embedded Binaries の+ボタンを押してcommon.framework
の場所common/build/xcode-frameworks/common.framework
を指定しましょう。
optionはそのままで大丈夫です。
- Build Settings/Enable Bitcode
次に、Build Settings > All > Combined > Enable BitcodeをYesからNoに変更します。
- Build Settings > Framework Search Paths
FrameworkのSearch Pathを追加します。
ここのpathは各々異なるためpwdコマンド等でプロジェクトのpathを確認してください。
ここは補完してくれないので自分で入力する必要があります。
私の場合はこうなりました。
$SRCROOT/../common/build/xcode-frameworks
画像では表示が異なりますが入力内容は上記のものです。
- Build Phases > Run Script Phase
最後にRun Script Phaseに実行するScriptを設定します。
おそらくデフォルトでは定義されていないので、Build Phasesの+ボタンからNew Run Script Phase
を選択して新たに作成します。
Scriptを以下のように定義します。なおpathの部分に関しては適宜置き換えてください。
cd $SRCROOT/../common/build/xcode-frameworks/
./gradlew :common:packForXCode -PXCODE_CONFIGURATION=${CONFIGURATION}
普段Xcodeを使っていないと絶対わからないのが、Build Phaseの並び替えです。順番をCompile Sourcesより前にしましょう。 追加したRun Scriptの部分はダブルクリックで名前を変えることも出来ます。ここではRunScript(kotlin native)としました。名前はそのままでも大丈夫です。
以上で設定は終了です。
- ViewController
コードの編集を行います。簡単なサンプルなのでコードから追加します。
import UIKit
import common
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(25)
label.text = CommonKt.createApplicationScreenMessage() // common module
view.addSubview(label)
}
}
Xcodeは自分でimport common
を書かないといけないのがとても辛いですが自分でimport文を書きましょう。
commonをimportすることでCommonKt.createApplicationScreenMessage()
が参照できるようになります。
全ての準備が整ったので、⌘+Rで実行してみましょう。
iPhone端末上でもKotlin Rocks on iOS[version]と表示されたと思います。
まとめ
Kotlin/NativeをAndroidとiOS共通のコードで実行してみました。今回のサンプルでは共通ロジックの部分が無いのであまり旨味がないですが、 普段AndroidかiOSのアプリを作っている方なら、いかにKotlin/Nativeが強力なのか がわかったのではないでしょうか? Androidは通常のkotlinで書かれたlibrary moduleとして、iOSからは.frameworkとして読み込むlibraryとしてお互いのロジックコードを共有することが出来ました。しかし、これだけでは今までもXamarin等が同じ手法でコードを共有することが可能でした。 他のマルチプラットフォームと比較してKotlin/Nativeの強みは
- 比較的新しい言語であるKotlinを使用できる
- 初期設定を終えれば、新しくフレームワークの記法を覚える必要がない
- 他のマルチプラットフォームではAndroidの方がバグが多いが、Kotlin/NativeではAndroid側が今までと変わらず開発出来る
- Server側にも問題無くKotlinが使用可能
- Andorid, iOSのコードだけでなくWeb(JS), Serverのコードまでも共有することが可能
な点だと思います。もちろんまだ不十分な面もありますが、今後どんどんアップデートが行われますので期待をしましょう。