Edited at

Kotlinで、Androidとブラウザーで動くアプリを作ったのでハマったこととか共有


背景

GitHubの草原を是が非でも絶やしたくない!!!!

... ので、こんな感じのリポジトリーを作って毎日やったことを簡単に記録しています。

技術書をちょっと読んだだけでも更新して良いことにしているので、よほどのことがない限りは更新する条件が満たせるはずです。

少しズルっぽく見えるかも知れませんが1、結果的に私のモチベーションを維持するのに大変役立っています。

しかし、そんなリポジトリーを用意してもなお、体調を著しく崩した日などは、条件を満たしていても更新し忘れることがあるので、更新をチェックするアプリを作りました、というのが今回のお話です2

ハマった部分などを力の限り共有しますので、Kotlin/JSで作ったアプリなどとして参考にしていただければ。


作ったもの

👇こちらにそのブラウザー版がございます。

https://keep-me-contributing.igreque.info/

READMEに書いたとおり、フォームにGitHubのユーザー名やリポジトリー名、それからGitHubのPersonal Access Tokenを書いて「Start Checking」をクリックすれば、後は自動で数分おき(現在は5分で固定しています)に対象のリポジトリーの最新のコミット日時をチェックし、今日の0時0分よりも前の日時を指して入れば警告を出す、それだけのアプリです。

ブラウザー版は、Google Chromeの「ショートカットを作成」機能から「ウィンドウとして開く」を使ってインストールすることで、デスクトップの端っこに置いておくウィジェットとして使うのがおすすめです(Firefoxにも同じ機能が欲しいなぁ...)。

リポジトリーは👇こちらです。

https://github.com/igrep/keep-me-contributing-kt


作るのに使ったもの

タイトルでも触れたとおり、Kotlin(ver. 1.3です念為)でAndroidアプリを作り、Kotlin/JS(「KotlinJS」と表記するのもよく見かけるのでそれも併記しておきます)でブラウザー版のアプリを作りました。

Android版とブラウザー版とで共有する部分を独立したライブラリーとして切り出すことで、できるだけコードを再利用するよう務めました。

十分に単純なアプリケーションだったので、それ以外に特筆すべきライブラリーはほとんど使っていません。

DIフレームワークもVirtual DOMもRxなんちゃらも何も使ってません!

強いて言えば、GraphQLのクライアントとしてapollo-androidを使ったり、テスト用にJUnit 5などを使った、ぐらいでしょうか。

その辺の今どきなライブラリーの使用例として期待していた方はすみません🙇。

あ、ライブラリーではありませんが、kotlin-dce-jsというツールは使いました。

Kotlin/JSが生成したJavaScriptの、dead code elimination、つまり使用してないコードを削除するツールですね。

これのおかげで1MB以上あるKotlin/JSの標準ライブラリーも、250KBまで減らせました。

build.gradleにちょっと書くだけで使えるので楽ちんでした☺️。

ちなみに、今回はWebpackは使いませんでしたが、このkotlin-dce-js, 1モジュールごとに1つのJavaScriptファイルを出力する、という仕様のようなので、依存しているモジュールの数が多くなった場合や、NPMにあるJavaScript製のパッケージも使いたくなった場合は、やっぱりWebpackと組み合わせて1つのJSファイルにまとめるようにするべきでしょう。

その辺はまた機会があれば。


デプロイするのに使ったもの

最近話題のNetlifyです。

以前に私のブログを移行する際に行ったのと同様に、どうせ現状私しか編集しなさそうなアプリケーションなので、netlify-cliを使って手元のマシンからデプロイすることにしました。

Netlify上でビルドすることも設定すればできるのでチャレンジはしてみたのですが、どうもうまくいかなかったので諦めています。

ただこの運用、後で「ハマったところ」でも触れますが、どうもNetlifyの期待しているやり方ではないように見えるので、なんとかしたいところではありますが。


ハマったところ


タイムゾーン JSTが取得できない

直したコミット 1

直したコミット 2

Kotlinは、標準ライブラリーとしては日時を扱うクラスを提供しないため、JavaやJavaScriptなどの対象となるプラットフォームが提供するクラスを使う必要があります。

そして、Androidをターゲットにする場合はAndroid 7.0未満を切り捨てない限り、Java 8でできた便利な日時用のクラスを使えません。

悪名高いJava 7以前のDateCalendarを使いたくないという場合、Joda-Timeでも使うしかないわけです。

私は依存モジュールを増やしたくないあまり、古いCalendar等を使っていたのですが、それが失敗だったのかも知れません。

タイムゾーンを取得するコードとして、こちらを参考に、当初次のように書いていたのですが、なぜかUTCが返ってきてしまい、ひどく混乱しました。

TimeZone.getTimeZone("JST")

デバッガーでgetTimeZoneメソッドの中を追ってみたところ、どうやらこのメソッド、指定したタイムゾーンがなかった場合はUTCを返すという、非常に困った実装のようです😱。例外を投げてくれた方が遙かにありがたいです。

詳しい事情は知りませんが、AndroidではこのJSTという名前は無効だったみたいで、必ずUTCが返ってきてしまいます。

実はOS依存なのかも知れません。

他のサイトでは下記の修正後のように大抵Asia/Tokyoと書いていたのですが、JSTの方が短いし、個人的に親しみのある言い方だからそうしていたんですが...。

TimeZone.getTimeZone("Asia/Tokyo")


Android OS起動時に、コミット日時のチェックが実行されない

直したコミット

これは、私が仕様を勘違いしているのかも知れませんが、Androidにおいて、バックグラウンドで定期実行を行う際はandroid.app.job.JobInfo.Builderというクラスで定期実行に関わる諸々の設定を書く必要があります(参考)。

その際、setPersisted(true)と指定することで、端末が再起動した後も定期実行をできるようになる... はずだったんですが、手元のエミュレーターやスマホで試した限り、なぜかうまくいきませんでした。

必要なパーミッションもちゃんと設定していたはずですし、LogCatとにらめっこしても特にヒントになるようなメッセージが見当たらなかったので、しかたなく、自前で端末の起動時に定期実行の登録を行うよう設定しました。

詳細は直したコミットをご覧ください。

もちろんAndroidManifest.xmlに<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>と書くのもお忘れなく!


共通ライブラリーのbuild.gradleで、ビルド対象のプラットフォームごとにKotlinの標準ライブラリーを依存モジュールとして指定しなければならない

直したコミット

Kotlinで、JavaScriptやJVMなど、複数のプラットフォームをターゲットにしたアプリケーションを作る場合、プラットフォーム間で共通して使用するコードを、独立したモジュール(以下、「共通ライブラリー」と呼びます)にする必要があります。

その場合、「共通ライブラリー」は必然的に複数のプラットフォーム向けにコンパイルされることになります。

そのためか、「共通ライブラリー」が外部のまた別のモジュールに依存している場合、プラットフォームごとに依存モジュールを別々に記述しなければならないことがあります。

下記のstdlib-*などと書かれた名前のモジュール(すなわち各プラットフォームごとのKotlin標準のモジュール)などがそれに該当します。

抜粋して例示します。


build.gradle

kotlin {

...
sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib-common')
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-common:1.1.1"
}
}
jvm().compilations.main.defaultSourceSet {
dependencies {
implementation kotlin('stdlib-jdk7')
}
}
js().compilations.main.defaultSourceSet {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.1.1"
}
}
}
}

これ、単にこのように依存しているモジュールを書けば良いと言うだけのものなのですが、書き忘れていた場合、IDEは何も教えてくれず、Gradleで該当箇所をビルドするまでエラーが表示されないので、ちょっとわかりづらいです。

具体的には、例えば下記のようなものです。

e: KeepMeContributingKt\core\src\commonMain\kotlin\info\igreque\keepmecontributingkt\core\CheckTarget.kt: (10, 25): Unresolved reference: isNotBlank

e: KeepMeContributingKt\core\src\commonMain\kotlin\info\igreque\keepmecontributingkt\core\CheckTarget.kt: (10, 56): Unresolved reference: isNotBlank
e: KeepMeContributingKt\core\src\commonMain\kotlin\info\igreque\keepmecontributingkt\core\CheckTarget.kt: (10, 84): Unresolved reference: isNotBlank
e: KeepMeContributingKt\core\src\commonMain\kotlin\info\igreque\keepmecontributingkt\core\ContributionStatusChecker.kt: (32, 17): Unresolved reference: Pair
e: KeepMeContributingKt\core\src\commonMain\kotlin\info\igreque\keepmecontributingkt\core\ContributionStatusChecker.kt: (34, 17): Unresolved reference: Pair
e: KeepMeContributingKt\core\src\commonMain\kotlin\info\igreque\keepmecontributingkt\core\ContributionStatusChecker.kt: (37, 13): Unresolved reference: Pair

どうやら、isNotBlankという拡張メソッドやPairというクラスはKotlin標準のモジュールが提供しているため、それがないことになってしまうようです。


[未解決] Netlify上でgradle runDceKotlinJsしてもNO-SOURCEと表示されるだけで、ビルドできない

Netlifyでは、デプロイの際に実行するビルドコマンドを設定することで、リンクしているリポジトリーが更新された際、自動で配信するファイルのビルド・デプロイまでしてくれます。

よく使われる静的サイトジェネレーターはいい感じにサポートできているっぽいのですが、私の場合、先ほども触れたとおりkotlin-dce-jsを使ってKotlinからJavaScriptに変換しつつさらに軽量化したファイルをデプロイする必要があります。

他のCIサービスにあった、Android向け設定ファイルのテンプレートを参考に、👇こんな感じのシェルスクリプトを書いてみました。

ANDROID_COMPILE_SDK="28"

ANDROID_BUILD_TOOLS="28.0.2"
ANDROID_SDK_TOOLS="4333796"
wget --quiet --output-document=android-sdk.zip https://dl.google.com/android/repository/sdk-tools-linux-${ANDROID_SDK_TOOLS}.zip
unzip -d android-sdk-linux android-sdk.zip
echo y | android-sdk-linux/tools/bin/sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" >/dev/null
echo y | android-sdk-linux/tools/bin/sdkmanager "platform-tools" >/dev/null
echo y | android-sdk-linux/tools/bin/sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}" >/dev/null
export ANDROID_HOME=$PWD/android-sdk-linux
export PATH=$PATH:$PWD/android-sdk-linux/platform-tools/
chmod +x ./gradlew
# temporarily disable checking for EPIPE error and use yes to accept all licenses
set +o pipefail
yes | android-sdk-linux/tools/bin/sdkmanager --licenses
set -o pipefail

bash ./gradlew assemble -x lintVitalRelease
bash ./gradlew browser:runDceKotlinJs
mkdir -p public/dist/
cp browser/dist/*.graphql browser/dist/*.js public/dist/
cp -r browser/{css,img,index.html} public

しかし、なぜか肝心のbash ./gradlew browser:runDceKotlinJsを実行したところで、下記のようにrunDceKotlinJsの結果がNO-SOURCEとなってしまい、変換したJavaScriptが作成されません😞。

+ bash ./gradlew browser:runDceKotlinJs

> Configure project :core
Kotlin Multiplatform Projects are an experimental feature.
> Task :core:compileKotlinJs UP-TO-DATE
> Task :core:jsProcessResources NO-SOURCE
> Task :core:jsMainClasses UP-TO-DATE
> Task :core:jsJar UP-TO-DATE
> Task :browser:compileKotlin2Js UP-TO-DATE
> Task :browser:runDceKotlinJs NO-SOURCE

いろいろ試してみたんですが、どうもこの問題だけは解決できませんでした...。

ひょっとしたら今私のPCでしかビルドできなくなってたりするんでしょうか...?


Netlifyが意図せずデプロイしてしまう

このプロジェクトのREADMEを書いているときに気づいたんですが、Netlifyはどうやら、リンクしているリポジトリーのmasterブランチ(厳密には、「Production branch」として設定されているブランチ)にpushすると、ビルド設定をどうしていようと、必ずデプロイを開始する、という仕様のようです。

結果、デプロイするディレクトリーに何もない状態だと、空っぽのディレクトリーがデプロイされてしまう、というデプロイ事故が起きてしまいます😲。

私の場合、特に何も考えずにREADMEを更新していたところ、リロードしたときに突然404が返ってくる、という事態に陥ったので、非常に焦りました...。

この問題を回避するために、絶対にpushしないダミーのブランチを作り、それをNetlifyの「Deploy contexts」の設定で「Production branch」として設定する、という方法を試してみてます。

その場合は「Deploy previews」として「Don’t build deploy previews for pull requests」を、「Branch deploys」として「None」を設定するのを忘れずに(本当にpreview用のデプロイをしたい場合は別ですが)。

Netlifyのドキュメントを読む限り、これ以上に確実な方法は現状なさそうです。

ビルドはNetlify以外のCIサーバーからやりたい、という人には結構嫌な仕様ですね...😞。

Netlifyにメールでも伺ってみたところ、サポートにお願いすれば、リポジトリーとのリンクを解除してくれるとのことで、そうすればpushした際のデプロイを完全になくすことができるとか。


敢えて解決していない問題

現状、このアプリケーションは私が使うことしか想定していません。

需要があるかわからないので...😅

そのため、下記のような問題を抱えていますが、ひとまず放置することにします。

Pull requestは歓迎するので、もし使いたいという方がいらしたらぜひ。


  1. Androidアプリは作りましたが、Google Playに公開はしてません(これから挙げるような問題があるので)。

  2. タイムゾーンはJSTに限定しています。そのため、日本国外にお住まいの方が使うと予期せぬ時間が「0時0分」として扱われてしまいます。

  3. エラーハンドリングをあまりしっかりやってません。例えばGitHubのパーソナルアクセストークンを間違えた場合、エラーを表示するようにはできてますが、詳細な原因は一切教えてくれません。

  4. コミットの日時をチェックする際、「誰がそのコミットをしたのか」はチェックしてません。そのため、複数人でコミットするリポジトリーの場合は、間違った結果を表示する恐れがあります。

  5. GitHubしか現状サポートしてません。GitLabやBitbucketがお好きな方はすみません...🙇。

  6. そしてやっぱりiOS版はありません。私はiOS端末はおろかMacも持っていないので、やるとしたら、共通して使っているライブラリーをどこかのMavenリポジトリーに上げて、Kotlin/Nativeで誰かに作ってもらうしかありません。





  1. ⚠️なので当然っちゃ当然ですが、GitHubアカウントを見て採用の参考にする皆様はくれぐれもそこだけを見て判断することはないようお気を付けください! 



  2. 実は数年前にも同じようなものを作りました。こちらはContribution Mapが書かれたSVGを直接パースしてコミットをしたかどうかチェックしていたのですが、いかんせん公開されているAPIではないため、細かい仕様変更に振り回されたり、CORSポリシーが設定されておらずブラウザーだけで完結させることができないといった問題があったりしたため、今回作り直すことにしました。あと、Androidのネイティブアプリにすることで、より私の目に付きやすい位置にチェック結果を出したかった、というのもあります。