ReactNativeプロジェクトのAndroid環境を整備する

投稿開発部の吉田です。React Native 新アプリシリーズ連載3日目はAndroid担当の私からReactNative(以下RN)のAndroidプロジェクトを出来る限り健康な状態にするために行ったことを紹介します。

手元の環境

RNは開発が活発なため執筆時点の環境を載せておきます。バージョンが乖離している場合は動かない可能性があります。

  • react-native-cli(2.0.1)
  • npm(5.8.0)
  • node(8.9.3)
  • react-native(0.55.1)
  • Android Studio(3.1)
  • macOS High Sierra(10.13.4)

Androidの開発環境はセットアップ済みとして話を進めます。

設定ファイルの掃除

react-native initで生成されたプロジェクトは丁寧な作りになっていますが、コメントが多すぎたり、設定が古かったりするので整えていきます。 RNはAndroidプラットフォームにそこまで依存性がないので、設定ファイルを大胆に書き換えても問題なく動作します。

AndroidStudioでプロジェクトを開く

AndroidStudioを起動してOpen an existing Android Studio projectを選択します。 ディレクトリは$project_root/androidを指定します。

起動すると"Android Gradle Plugin Update Recommneded" というタイトルのダイアログが出てきます。問題ないので更新しましょう。 内容はAndroidの標準のビルドシステムであるGradleの更新とGradleでAndroidProjectをビルドするためのプラグインの更新です。

Gradle pluginの更新が終わるとgradle syncやAndroidStudioのindexingが動き始めます。それらが終わると恐らく右下に下記の警告が現れます。

Configuration 'compile' is obsolete and has been replaced with 'implementation'.
It will be removed at the end of 2018


The specified Android SDK Build Tools version (23.0.1) is ignored, as it is below the minimum supported version (27.0.3) for Android Gradle Plugin 3.1.0.
Android SDK Build Tools 27.0.3 will be used.
To suppress this warning, remove "buildToolsVersion '23.0.1'" from your build.gradle file, as each version of the Android Gradle Plugin now has a default version of the build tools.

警告は2点あります。

  • ライブラリの依存関係を表す文法が変更され、古いcompileというシンタックスは2018年末を目処に削除されること
  • Android-Gradle-Plugin(3.1.0)はAndroid SDK Build Tools(23.0.1)をサポートしていないこと

上の警告は app/build.gradle以下のこの部分の事を指しているので素直に置き換えましょう

 dependencies {
-    compile fileTree(dir: "libs", include: ["*.jar"])
-    compile "com.android.support:appcompat-v7:23.0.1"
-    compile "com.facebook.react:react-native:+"  // From node_modules
+    implementation fileTree(dir: "libs", include: ["*.jar"])
+    implementation "com.android.support:appcompat-v7:23.0.1"
+    implementation "com.facebook.react:react-native:+"  // From node_modules
 }

二つ目の警告も解決は簡単です。Android SDK Build Toolsについて詳しく触れませんが、Android-Gradle-Plugin3.0以降では指定しなければ常に最新のものが利用されるので該当箇所を削除します。

 android {
     compileSdkVersion 23
-    buildToolsVersion "23.0.1"

     defaultConfig {
         applicationId "com.sampleproject"

SDK Build Tools Release Notes | Android Studio

上記の作業後画面の右上からSync Nowすると全ての問題は解決したので警告は表示されなくなります。

compileSdkVersionとtargetSdkVersionを最新にする

Android開発にはcompileSdkVersionとtargetSdkVersionとminSDKVersionの3つのバージョン概念があります。それぞれの違いについては公式ブログに譲るとして、compileSdkVersionとtargetSdkVersionを最新にします。 (2018年4月地点では27が最新)

diff --git a/android/app/build.gradle b/android/app/build.gradle
index e37c508..e645022 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -94,12 +94,12 @@ def enableSeparateBuildPerCPUArchitecture = false
 def enableProguardInReleaseBuilds = false

 android {
-    compileSdkVersion 23
+    compileSdkVersion 27

     defaultConfig {
         applicationId "com.sampleproject"
         minSdkVersion 16
-        targetSdkVersion 22
+        targetSdkVersion 27
         versionCode 1
         versionName "1.0"
         ndk {
@@ -137,7 +137,7 @@ android {

 dependencies {
     implementation fileTree(dir: "libs", include: ["*.jar"])
-    implementation "com.android.support:appcompat-v7:23.0.1"
+    implementation "com.android.support:appcompat-v7:27.1.1" //majorVersionがcompileSDKVersionと一致する必要がある
     implementation "com.facebook.react:react-native:+"  // From node_modules
 }

diff --git a/android/build.gradle b/android/build.gradle
index 3931303..7456eb1 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -17,6 +17,7 @@ allprojects {
     repositories {
         mavenLocal()
         jcenter()
+        google() //最新のsupportライブラリを取得するためgoogle-repositoryを追加している
         maven {
             // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
             url "$rootDir/../node_modules/react-native/android"

特にtargetSdkVersionを26以上に上げる作業はリリース前に必ずやっておくことをお薦めします。 今年中にtargetSdkVersionの低いアプリのリリースや更新が制限される事が公式のアナウンスで発表されています。

targetSdkVersionを上げるとアプリの一部の振る舞いが変わります。一番わかりやすい例はRuntimePermissionの有効化です。他にも細かな振る舞いの変更があるのでこの辺りで一度アプリが正常に動くのかチェックしましょう。

見落としがちな変更にランチャー上のアプリアイコンの見た目の変化があります。(ランチャーアプリによっては想定より小さく表示される) この問題は、roundIconリソースの追加することで解決します。2018年現在ではついでにAdaptiveIcon対応もすることをお薦めします。AdaptiveIconとはランチャーアプリ側でアイコンの外形を決めることの出来る機能です。どちらにせよ対応は難しくないのでデザイナーと相談してandroid:roundIconを設定しましょう。

不要なパーミッションや宣言を取り除く

ReactNativeでAndroidアプリを作ると不要なパーミッションがデフォルトでついているので適切に削除します。

DevSettingsActivityの宣言をリリース版から削除する

DevSettingsActivityは名前の通りデバック用途のActivityです。リリースビルドには必要ないのでapp/src/debug/AndroidManifest.xmlにデバッグ用のマニフェストを作り移動させます。

--- /dev/null
+++ b/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,6 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <application>
+        <activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
+    </application>
+</manifest>
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index c765926..d4b02f4 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -28,7 +28,6 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
-        <activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
     </application>

 </manifest>

SYSTEM_ALERT_WINDOWをリリースビルドから削除する

SYSTEM_ALERT_WINDOWは他アプリの上への描画を許可する強めのパーミッションです。こちらもデバックビルドで利用されるものですがリリースビルドにも紛れ込むので取り除く設定を追加します。

--- /dev/null
+++ b/android/app/src/release/AndroidManifest.xml
@@ -0,0 +1,8 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:tools="http://schemas.android.com/tools"
+        >
+    <uses-permission
+            android:name="android.permission.SYSTEM_ALERT_WINDOW"
+            tools:node="remove"
+            />
+</manifest>

READ_PHONE_STATEを削除する

ReactNative for Androidはandroid-jscというライブラリに間接的に依存しています。 このライブラリのtargetSDKlevelが4と異常に低い影響でREAD_PHONE_STATEのパーミッションが勝手に追加されます。多くの場合不要なパーミッションだと思うので削除しておきます。

--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,8 +1,11 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.sampleproject">
+          xmlns:tools="http://schemas.android.com/tools"
+          package="com.sampleproject"
+        >

     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
+    <uses-permission android:name="android.permission.READ_PHONE_STATE" tools:node="remove"/>

     <application
       android:name=".MainApplication"

残りは好みですが不要な設定やコメントやファイルを削除します。

  • 不要なコメントの削除
  • keyStore以下の削除
  • libs以下のjarの参照削除
  • mavenLocalを参照レポジトリから削除
  • BUCKファイルの削除

Clean up project · kazy1991/techlife-rn-android-sample@8366400 · GitHub

apkサイズの最適化

必ずしもやる必要はないですが、ReactNative製のアプリはデフォルトの設定だと不必要にバイナリサイズが大きくなってしまうのでダイエットする方法を2つ紹介します。 私達の開発しているMYキッチンアプリでは下記の二つを行い10MB程度あったapkを5MBまで減らすことが出来ました。

Multiple APKs化

Androidは多種多様な環境の端末で動く可能性があるため、必要になりそうなものは全部詰め込んでいます。Multiple APKsとは環境毎にバイナリを分けることでサイズを削減する方法です。 ReactNativeの場合Native Developer Kit(NDK)の部分が大きいためCPUアーキテクチャ(ABI)毎にバイナリを分けてあげるとapkのサイズがかなり小さくすることが出来ます。 対応が難しそうですが、実はプロジェクトの雛形の中にdef enableSeparateBuildPerCPUArchitecture = false というフラグが用意されているためtrueに切り替えるだけでMultiple APKsは完了します。

ちょっとした補足ですが、apkを分割する場合PlayConsoleの都合上versionCodeはそれぞれ異なる必要があります。テンプレートではaapt(Android Asset Packaging Tool)コマンドを使って確認しやすいように、既存のバージョンコードに1MB(1048576)を足し合わせた値を採用しています。これは個人の込みのですが、私たちはversionCodeをログ分析などでも利用する都合上パッと見のわかりやすさを重視して既存のversionCode+末尾1桁をABIの識別という形に変更しています。

diff --git a/android/app/build.gradle b/android/app/build.gradle
index f491bca..d8dc9bc 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -69,11 +69,11 @@ android {

     applicationVariants.all { variant ->
         variant.outputs.each { output ->
-            def versionCodes = ["armeabi-v7a":1, "x86":2]
+            def versionCodes = ["armeabi-v7a": 1, "x86": 2]
             def abi = output.getFilter(OutputFile.ABI)
             if (abi != null) {
-                output.versionCodeOverride =
-                        versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
+                // 例) version1.1.1のx86のversionCode: 101012
+                output.versionCodeOverride = defaultConfig.versionCode + versionCodes.get(abi)
             }
         }
     }

https://developer.android.com/studio/build/configure-apk-splits.html

Proguardの有効化

Proguardとはプロジェクトを静的解析して参照のないコードを削除したり、難読化を行うツールです。 これも詳細は下記のリンクに譲りますが、こちらもdef enableProguardInReleaseBuilds = falseというフラグが用意されているためtrueに切り替えるだけで有効になります。 ネイティブモジュールを多用しない限り嵌まらないと思いますが、正しい設定がされていないままProguardを有効にするとビルドが通らなくなったり、実行時にアプリが意図せずクラッシュすることもあります。 有効に切り替える際には動作確認することをお薦めします。また可能ならProguardの振る舞いについて正しく理解できていると安心できます。

https://developer.android.com/studio/build/shrink-code.html

Stetho(デバッグツール)の導入

StethoはFacebook社が開発しているAndroid向けデバッグツールです。おおまかに言うとAndroid開発でもChromeのdevToolが使えるようになります。 特にNetwork Inspectionはとても便利なので導入しておくことをお薦めします。

f:id:kazy1991:20180409202337p:plain

一般的な導入方法は公式のドキュメントに譲りますが、一点だけ注意点があります。ReactNative:0.45~0.54を利用されている場合公式ドキュメント通りに導入しても動作しません。ReactNativeのバージョンを上げるか下記のリンクのように強引に差し込む必要があります。

Fix NetworkingModule losing Cookies when multiple CatalystInstances e… · facebook/react-native@0a71f48 · GitHub

バージョニングをsemverぽく変更する

ReactNativeを使った開発ではCodePushを利用することも多いと思います。CodePushと相性を良くするためにAndroid側のバージョニングもsemverっぽく管理すると便利です。 AndroidのバージョニングにはversionCodeversionNameがありますがMYキッチンアプリでは下記のように管理しています。

--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -7,6 +7,7 @@ project.ext.react = [
 ]

 apply from: "../../node_modules/react-native/react.gradle"
+apply from: "${project.rootDir}/gradle/appversion/appversion.gradle"

 def enableSeparateBuildPerCPUArchitecture = false
 def enableProguardInReleaseBuilds = false
@@ -18,8 +19,8 @@ android {
         applicationId "com.sampleproject"
         minSdkVersion 16
         targetSdkVersion 27
-        versionCode 1
-        versionName "1.0"
+        versionCode project.ext.getVersionCode()
+        versionName project.ext.getVersionName()
         ndk {
             abiFilters "armeabi-v7a", "x86"
         }
diff --git a/android/gradle/appversion/appversion.gradle b/android/gradle/appversion/appversion.gradle
new file mode 100644
index 0000000..e9bdb5c
--- /dev/null
+++ b/android/gradle/appversion/appversion.gradle
@@ -0,0 +1,49 @@
+class AppVersion {
+
+    private int major
+
+    private int minor
+
+    private int patch
+
+    AppVersion(int major, int minor, int patch) {
+        throwExceptionIfVersionIsNotValid(minor)
+        throwExceptionIfVersionIsNotValid(patch)
+
+        this.major = major
+        this.minor = minor
+        this.patch = patch
+    }
+
+    private static void throwExceptionIfVersionIsNotValid(int version) {
+        if (version >= 100) {
+            throw new IllegalArgumentException("Can't use version number more than three digit")
+        }
+    }
+
+    int getCode() {
+        return major * 100_00_0 + minor * 100_0 + patch * 10
+    }
+
+    String getName() {
+        return "${major}.${minor}.${patch}"
+    }
+}
+
+ext {
+    def vProperties = new Properties()
+    vProperties.load(rootProject.file('version.properties').newDataInputStream())
+    def versionMajor = vProperties.getProperty("version.major").toInteger()
+    def versionMinor = vProperties.getProperty("version.minor").toInteger()
+    def versionPatch = vProperties.getProperty("version.patch").toInteger()
+    def appVersion = new AppVersion(versionMajor, versionMinor, versionPatch)
+
+    getVersionCode = {
+        return appVersion.getCode()
+    }
+
+    getVersionName = {
+        return appVersion.getName()
+    }
+}
+
diff --git a/android/version.properties b/android/version.properties
new file mode 100644
index 0000000..9f61962
--- /dev/null
+++ b/android/version.properties
@@ -0,0 +1,3 @@
+version.major=1
+version.minor=0
+version.patch=0

ReactNativeに限らないTips

紹介した内容以外にもReactNativeプロジェクトに限らない変更をいくつか入れているので簡単に紹介します。

おわり

ReactNaiveを使ったアプリ開発は評判通り高速に開発が可能でとても気に入っています。AndroidエンジニアはRNプロジェクトであまり活躍できない印象がありましたが、より安全に(例えば秘匿値の管理)より快適な体験(例えばSmartLock for passwordを使って自動ログインを実現するなど)を提供するために活躍する場面はたくさんあります。

もし私達の取り組みに少しでも興味を持って頂けたらぜひお声がけください。私個人へのDMでも大丈夫です! 😀

明日は@sn_taigaさんから 「クックパッド MYキッチン」のアプリアイコンができるまでのお話です。お楽しみに!

今回作成したプロジェクトのレポジトリはこちらです。

GitHub - kazy1991/techlife-rn-android-sample

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/