October 24, 2018

Kotlin/Nativeチュートリアル Android, iOS編

個人的に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
Android App
  • iOS
iOS App

プロジェクト作成

Create new project

プロジェクトはgradleで管理していくので、Androidアプリから作成していきます。 Start a new Android Studio projectを選択しましょう。

Android Studio create project01

Application nameはプロジェクトの名前になります。 Company domainはpackageのnamespaceになります。 当然Kotlinを使うのでInclude Kotlin supportのチェックを入れましょう。

Android Studio create project02

minSDKは5.0以上の方が幸せになれます。

Android Studio create project03

一旦 Empty Activity で作ります。

Android Studio create project04

名前はそのままMainActivityが一般的に用いられます。

Android Studio create project05

プロジェクトが作成されました。
左に表示されている(されていない場合は⌘1)パッケージTreeが見辛いので、AndroidではなくProjectを選択するとPackageの構成が見やすくなります。

Android Studio create project06

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.10.2-all.zip

kotlin.orgでは4.7を使っていますがgradleはバージョンアップと共にbuildが早くなったり良いことがあるので、なるべく最新版を使うことをオススメします。 ただ、annotation processorを使っていたりすると動かなくなることがよくあるので、既存プロジェクトで上げる場合は注意が必要です。
最新バージョンはこちらから確認出来ます。

Gradle Cache

開発を快適に行うためにAndroid Studioにある設定から "Build, Execution, Deployment > Build Tools > Gradle" にある Offline work を有効にしましょう。この設定をするとGradle syncの度にリモートのリポジトリからライブラリをダウンロードしなくなります。なお取得したライブラリのcacheは~/.gradle/caches/に保存されます。cacheに存在しない新しいバージョンもしくはライブラリを追加した場合は、offline workにチェックが無いと動作しないので気をつけて下さい。

Gradle offline settings

Common Module

次にAndroid, iOS, Web(今回は無い), Server(今回は無い), で使う共通のコードを追加します。
左のプロジェクトツリーのrootディレクトリの部分にカーソルを合わせて右クリック> New > Moduleの順にクリックします。

Create module

Android Studioからでは、(IntelliJ IDEAも 今後追加される事を期待)まだメニューから直接Kotlin/Nativeのcommon moduleを作成することが出来ないので一旦、Java Libraryを選択します。(現状何を選んでも一緒ですが一番変更が少ないです)

Select java library

Library nameはcommonにしました。(本家の名前はShareCodeになっています)
Java class nameは消すので名前は何でも良いです。

Create java library

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する?と聞かれますが、自分で行う場合はこのボタンを押しましょう。

Sync

Common moduleの解説

common moduleには共通で使うコードを追加していきます。しかし、各プラットフォームによって関数の結果を変えたい事があると思います。 Kotlin/Nativeでは共通のコードに対して、expectactual を用いて対処しています。 わかりやすく例えるなら abstractoverride の様な関係です。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にある緑色の三角ボタンを押すと実行が出来ます。端末が繋がっていない場合はエミュレータを使いましょう。

Run

Android端末上でKotlin Rocks on Androidと表示されたと思います。

iOSアプリ

次にiOSアプリを作っていきます。Xcodeを起動して Create a new Xcode project を選択します。

create a new xcode project

SingleView Appを選びます。

select sinble view app

product nameを任意の名前にしてcrateします。ここからだとパッケージ名の統一が出来ないので後で変更すると良いです。

create new project

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から読み込みます。

  • General > Embedded Binaries

先ずは、General > Embedded Binaries の+ボタンを押してcommon.frameworkの場所
common/build/xcode-frameworks/common.frameworkを指定しましょう。

embedded binaries1
embedded binaries2

optionはそのままで大丈夫です。

embedded binaries3
  • Build Settings/Enable Bitcode

次に、Build Settings > All > Combined > Enable BitcodeをYesからNoに変更します。

Enable Bitcode
  • Build Settings > Framework Search Paths

FrameworkのSearch Pathを追加します。
ここのpathは各々異なるためpwdコマンド等でプロジェクトのpathを確認してください。 ここは補完してくれないので自分で入力する必要があります。
私の場合はこうなりました。 $SRCROOT/../common/build/xcode-frameworks
画像では表示が異なりますが入力内容は上記のものです。

Framework Search Paths
  • Build Phases > Run Script Phase

最後にRun Script Phaseに実行するScriptを設定します。
おそらくデフォルトでは定義されていないので、Build Phasesの+ボタンからNew Run Script Phaseを選択して新たに作成します。

Create Run Script Phase

Scriptを以下のように定義します。なおpathの部分に関しては適宜置き換えてください。

cd $SRCROOT/../common/build/xcode-frameworks/
./gradlew :common:packForXCode -PXCODE_CONFIGURATION=${CONFIGURATION}
Run Script Phase

普段Xcodeを使っていないと絶対わからないのが、Build Phaseの並び替えです。順番をCompile Sourcesより前にしましょう。 追加したRun Scriptの部分はダブルクリックで名前を変えることも出来ます。ここではRunScript(kotlin native)としました。名前はそのままでも大丈夫です。

Order Run Script Phase

以上で設定は終了です。

  • 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の強みは

  1. 比較的新しい言語であるKotlinを使用できる
  2. 初期設定を終えれば、新しくフレームワークの記法を覚える必要がない
  3. 他のマルチプラットフォームではAndroidの方がバグが多いが、Kotlin/NativeではAndroid側が今までと変わらず開発出来る
  4. Server側にも問題無くKotlinが使用可能
  5. Andorid, iOSのコードだけでなくWeb(JS), Serverのコードまでも共有することが可能

な点だと思います。もちろんまだ不十分な面もありますが、今後どんどんアップデートが行われますので期待をしましょう。

© AAkira 2018