AndroidとかiOSとかモバイル多め。その他技術的なことも書いていきます。

2019年振り返り

2019年ももう終わりなので1年を振り返ろうかなと思います。

リニューアルプロジェクト

今年の1月〜4月は昨年から続くリニューアルプロジェクトをやっていました。今は7人いるAndroidエンジニアもこの時はまだ私ともう1人しかAndroidエンジニアがいなくてヒーヒー言ってた記憶があります。リリースにはこぎつけたもののだいぶスコープを削った結果なので結局10月辺りまで削った分の残件をやっていました。大規模な新機能開発がある中でのアーキテクチャの見直しやリファクタリングは良くないと諦めたこと、リニューアルによる大規模テストに合わせて溜まってたライブラリアップデートをできたのは良かったかなと思います。当時はKotlin Coroutines使い始めで0.xから1.0にアップデートするのが大変でした。

スペイン&フランス旅行

ゴールデンウィークには念願だったスペイン旅行に行ってきました。

f:id:phicdy:20191227195825j:plain

f:id:phicdy:20191227200244j:plain
パエリア最高

カンプ・ノウで一番いい席を取って試合を見れたのが最高でした。メッシが後半からでてきて決勝ゴールとなりバルセロナの優勝が決定した試合でした。優勝パレードも見れて大満足でした。

f:id:phicdy:20191227195812j:plain

f:id:phicdy:20191227200042j:plain

フランスは凱旋門とかよかったんですが、とにかく治安が悪くてあまりいい体験はなかったです...

f:id:phicdy:20191227200402j:plain

リードエンジニア

帰国して5月からはリードエンジニアとして働き始めました。社内の開発プロセスが新しいものになり、新しい働き方に慣れてなかった感じがしてます。何個もプロジェクトマネジメントしていると自分の実装時間がなくなったり、コードレビューがめっちゃ溜まったり、どうしたもんかなと悩んでた気がします。今はチームが2つに分かれてだいぶ負担は減ったものの、どうやっていくのが一番いいんだろうなと悩む毎日です。来年は引き続きエンジニアとして働きつつもプロジェクトマネジメント方面にも力を入れる割合を増やしていこうかなと思います。

今年やった取り組みとは会社ブログにまとめました。

buildersbox.corp-sansan.com

Fluxに向き合う

ちょうどチームが2つに分かれた辺りでチーム内でアーキテクチャについて話し合い、Fluxに向き合うようになりました。まず今年は基盤を作ってこういう風にやっていきましょうというのを出せたのがよかったかなと思います。当時は進捗が2週間くらい遅れており、100コメント超えのPRを3つマージすることでようやく基盤の初期案が完成しました。Fluxを横展開していく中でまだまだ課題はあり、徐々に改善していける段階まで来ているので来年はより横展開していき、基盤をよりよいものにできていけたらなと思います。

Flux移行の詳細は会社ブログに書きました。

buildersbox.corp-sansan.com

Kotlin Fest & Droidkaigi落ちる

今年は初めてカンファレンスにCfPを出しました。しかし両方とも落選...来年はもうちょっとネタやCfPの書き方を工夫して通るようにできたらなと思います。

Macbook Pro壊れる

12月の中頃、突然MacBook Pro (15-inch, 2016)のディスプレイがつかなくなりました。色々対処してみたものの直らず、外部ディスプレイに繋げるしか使う方法がなくなってしまいました。Apple Storeに持っていったら修理代見積もりは88000円でした... Macbook Pro 16-inch 2019を買うか悩みましたが高すぎるのでThinkpad X1 CarbonにUbuntuを入れてみるのに挑戦してみることにしました。

f:id:phicdy:20191227202012p:plain

年末に届くか微妙ですが楽しみです。

おわりに

こう振り返ってみると色んなことをした1年だったなと思います。また来年もAndroidエンジニアとして頑張りたいと思います。

Kotlin Coroutinesで並列にListの各項目を処理する

coroutineScope {
    list.map { async { updateListItem(it) } }
        .map {
          val updated = it.await()
          ... 
        }
}

最初のmapで処理を走らせてしまうと処理待ちになって逐次実行になる。そのためまず最初のmapですべてのListの項目の処理を遅延実行し、次のmapで各処理の結果を待つ。

ちなみに最初Flowでできないか試したが、Flowはイベントストリームのように逐次実行されるのでListの各項目を並列に処理するのには向いていない。

list.asFlow()
        .map {
            Timber.i("start async ${it.title} start")
            coroutineScope { async { updateListItem(it) } }
        }
        .map {
            Timber.i("start await ${it} start")
            it.await()
        }

とすると

2019-12-17 21:29:55.426 11681-11681/com.phicdy.sample I/Hoge$updateAll$$inlined$map: start async list1 start
2019-12-17 21:29:55.434 11681-11808/com.phicdy.sample I/Hoge$hoge: DefaultDispatcher-worker-3 list1 updateListItem start
2019-12-17 21:29:56.856 11681-11808/com.phicdy.sample I/Hoge$hoge: DefaultDispatcher-worker-3 list1 updateListItem done
2019-12-17 21:29:56.858 11681-11681/com.phicdy.sample I/Hoge$updateAll$$inlined$map: start await DeferredCoroutine{Completed}@d1d861 start
2019-12-17 21:29:56.863 11681-11681/com.phicdy.sample I/Hoge$updateAll$$inlined$map: start async list2 start
2019-12-17 21:29:56.881 11681-11808/com.phicdy.sample I/Hoge$hoge: DefaultDispatcher-worker-3 list2 updateListItem start
2019-12-17 21:29:58.045 11681-11807/com.phicdy.sample I/Hoge$hoge: DefaultDispatcher-worker-2 list2 updateListItem done
2019-12-17 21:29:58.046 11681-11681/com.phicdy.sample I/Hoge$updateAll$$inlined$map: start await DeferredCoroutine{Completed}@ef2999d start

と逐次実行になってしまう。

マルチモジュールのプロジェクトでdanger-android_lintを使う

Dangerでandroid lint結果をPR上でコメントしようと思うとdanger-android_lintを使うことになるがマルチモジュールに対応しておらずレポートを1つしか指定できない。

github.com

マルチモジュールに対応する1つの方法はレポートをマージする。

qiita.com

blog.bitrise.io

もう1つの方法が今回採用したやり方で、あらかじめlintを実行しておき、その後にdanger-android_lintではlintの実行をスキップして各レポートからDangerを実行する。こうすることでマージの手間が省けた。

android_lint.skip_gradle_task = true
android_lint.filtering = true
Dir["*/build/reports/lint-results-debug.xml"].each do |file|
  android_lint.report_file = file
  android_lint.lint(inline_mode: true)
end

MarkdownエディターをEvernoteからBoostoneに乗り換えてDropbox Paperに乗り換えた

tl;dr

Evernote Boostnote Notion Inkdrop Dropbox Paper
Markdown対応 ○(Markdown here使用)
無料もしくは買い切りの有料(サブスクでない) △(3台目からは要サブスク) ○(無料) ☓(サブスク) ☓(サブスク) ○(無料)
メモ内容で検索できる
PC・モバイルで同期できる ☓(3台目から有料) ○(Dropbox同期)
PC・モバイルで編集できる △(モバイルはMarkdownに戻して編集しないとフォーマットが崩れる ☓(モバイル開発凍結)

Dropbox Paperになった

背景

Evernoteの端末制限が厳しくなるまではMarkdown hereをブラウザに入れてブラウザ版のEvernoteでメモを書いていた。それをモバイルで見ていた訳だが端末制限で乗り換えを考え始めた。Markdown hereでフォーマットするとブラウザ上でMarkdownに戻して編集しないとフォーマットが崩れてしまうのもある。

markdown-here.com

乗り換え条件としては

  • Markdown対応
  • 無料もしくは買い切りの有料(サブスクでない)
  • メモ内容で検索できる
  • PC・モバイルで同期できる
  • PC・モバイルで編集できる

まず最初にBoostnoneを選んだ。

boostnote.io

BoostnoteはOSSDropbox同期が可能でMarkdownがリアルタイムにプレビューされるので体験が良かった。

他の候補としては、Notion はサブスクかつタイトルでしか検索できない(もう改善された?)、Inkdrop はサブスクなのでNG。

Boostnoteをしばらく使っていた訳だが、やはりモバイルで閲覧・編集したくなる機会が増えてきた。 しかしBoostnoteのモバイルは開発が現状止まっており、再度乗り換えを考え始めた。

github.com

結果、今日Dropbox Paperに行き着いた。

www.dropbox.com

Dropbox PaperはWebのDropbox上もしくはモバイルアプリ上からMarkdownでメモができる。Dropboxにファイルを置くのでそのまま同期できる。しいてだめな点をあげるとAndroidアプリから新規ファイルを作るとDropbox直下に置かれてしまうのでそれだけなんとかされたい。WebのDropboxでフォルダを分けて作ったファイルはAndroidアプリからも編集可能ではある。

GitHub ActionでAndroidのCIを設定してみた

ベータを申し込んだら1日で有効になったので使ってみた

help.github.com

設定

ひとまずPR時にUTとlintを並列で動かしてみる。

name: Android CI

on:
  pull_request:
    branches:
    - develop

jobs:
  unit_test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v1
    - name: set up JDK 1.8
      uses: actions/setup-java@v1
      with:
        java-version: 1.8
    - name: Decode google-services.json
      env:
        GOOGLE_SERVICE: ${{ secrets.GOOGLE_SERVICE}}
      run: echo $GOOGLE_SERVICE | base64 --decode > ./app/google-services.json
    - name: Unit Test
      env:
        TZ: Asia/Tokyo
      run: ./gradlew testDebugUnitTest
      
  lint:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v1
    - name: set up JDK 1.8
      uses: actions/setup-java@v1
      with:
        java-version: 1.8
    - name: Decode google-services.json
      env:
        GOOGLE_SERVICE: ${{ secrets.GOOGLE_SERVICE}}
      run: echo $GOOGLE_SERVICE | base64 --decode > ./app/google-services.json
    - name: Lint
      run: ./gradlew lintDebug

を.github/workflows以下に置くだけ。 google-services.jsonは一旦base64のデコードでお茶を濁す。Secretsはリポジトリ -> Settings -> Secret -> Add a new secretで追加できる。 タイムゾーン環境変数TZを設定してあげればよい。

help.github.com

感想

まず無料でベータだから仕方ないかもしれないがCircleCIに比べて単純に遅い。無課金のCircleCIで ./gradlew testDebugUnitTest が1分10秒ほど、 ./gradlew lintDebug が1分30秒ほどだったのに対し、GitHub Actionでは ./gradlew testDebugUnitTest が3分20秒ほど、 ./gradlew lintDebug が3分40秒ほどかかった。個人プロジェクトなど時間にこだわらない用途であればいい選択肢だと思う。無料で1リポジトリ辺り20並列まで使えるのはCircleCIやBitriseより遥かに上を行っているし、この程度の設定なら30分くらいで設定できた。実際のプロダクトで使うかはCPUが強いプランが来てくれたらかなと思った。

また↓の記事では不安定だったというのもあるのでそのあたりも改善されたい。

diary.app.ssig33.com

あとGitHub Actionが出たことによってCircleCIやBitriseとの競争がより激しくなり、互いのサービスがよりよくなる流れがきてるのがいいなと思った。

Kotlin Fest 2019に参加した #kotlinfest

2019/08/24(土)に行われたKotlin Fest 2019に参加しました。

kotlin.connpass.com

去年

phicdy.hatenablog.com

会場は去年と同じ東京コンファレンスセンター品川でした。

f:id:phicdy:20190824105158j:plain f:id:phicdy:20190824105108j:plain

まい泉の軽食うまい。

f:id:phicdy:20190824105120j:plain

トートバッグは黒にしました。

f:id:phicdy:20190824105131j:plain

オープニング待機。

f:id:phicdy:20190824105142j:plain

今年は英語セッションがあり、同時通訳レシーバーや案内が英語で行われたりと海外から参加者への対応が強化された印象でした。

f:id:phicdy:20190824105902j:plain

お昼は去年に引き続き3Fの500円カレービュッフェ

f:id:phicdy:20190824182329j:plain

いろんなお菓子がデプロイされてました。

f:id:phicdy:20190824182305j:plain f:id:phicdy:20190824182315j:plain

以下セッションのメモ

基調講演

  • KEEP(Kotlin Evolution and Enhancement Process)

github.com

  • Inline classはIdクラスに使えそう
  • ImmutableListはListに比べてなにがうれしい?

  • 新しい本を執筆中とのこと(Kotlinを書いたことないエンジニア向け?)
  • Kotlin MPPはビジネスロジックの共有が目的でUI部分の共有はしない。本番環境での事例もちらほら出始めている

Kotlin コルーチンを 理解しよう 2019

speakerdeck.com

  • suspend関数の中でasyncしてエラーが起きたときにクラッシュしたときにハンドリングできない部分の説明が最高にわかりやすい(スライドの140ページ辺り)
  • Progress -> suspend関数で中断 -> Completedをテストするときは、そのsuspend関数をモックしてdelayし、advanceTimeByして仮想的に時間をすすめることでPregress -> suspend関数で中断 -> Completedの状態の変化をテストできる

改めて学ぶContracts

speakerdeck.com

  • 1.3.50で26メソッドでContractsに対応された
  • Kotlin ConfでKotlin 1.4の話があり、その中でContractsについても話がありそう
  • @Contract というJava用のアノテーションをJetBrainsが提供している。そこから契約とはなにかを探る
  • @Contarct はKotlinに対応しておらず、改良してKotlinに入れる話がKEEPにあったが対応がしんどい、IDEが怒るだけで値が設定できない訳ではないから見送られた
  • AT_MOST_ONCEは0回か最大1回呼ばれることをコンパイラに伝える
  • デフォルトはUNKNOWNで何回呼ばれるかわからない
  • Inline classのほうがIssueは多いけど、Contractsも負けてない
  • コンパイル時間などの問題がまだまだあるのですぐにstableにはならなそう

Kotlin Multiplatform Project入門

speakerdeck.com

  • Kotlin MPPはロジック部分の共通化のみでUIの共通化は行わない
  • Jetpack compose/Swift UIでUI部分の共通化の可能性がでてきた
  • Kotlin/JVM, Kotlin/Native, Kotin/JSなどを含めてKotlin MPP
  • Kotlin/Nativeはバイナリを出すのであってiOS向けだけでなく、Mac, Linuxなどにも対応する
  • 大きなメリットとしてはドメインオブジェクト・ロジックの共通化。Gradle pluginなので導入が簡単(build.gradleの設定は大変そう)
  • デメリットとしてはJavaの資産(RxJavaなど)を使えない、iOSでSwiftから直接suspend関数が呼べずCoroutinesがメインスレッドしか呼ばれないため別途対応が必要である等

Kotlin/Nativeはなぜ動くのか?

speakerdeck.com

  • JREで提供されているものをC++で実装している
  • GCを独自実装
  • LLVM経由で変換している

おわりに

今回セッション募集があったので出してみたんですが残念ながらCfPが採用されず次回は発表できたらなと思いました。 今年はAndroidっていうよりKotlin MPPやWeb側の話が多い印象で、AndroidだけじゃなくWeb方面でもKotlinが広がってきたなと感じました。気になったのはKotlin MPPで、Gradle pluginで既存プロジェクトに対してもKotlin MPPが始められそうなので荒谷さんの資料やブログを見つつちょっと触っていこうかなと思います。基調講演で話していたInline classが楽しみです。

DangerでTextViewのandroid:maxLinesがないことをチェックしようとした

APIからデータを受け取ってTextViewに表示するとき、大きいデータだと際限なく表示されてしまうので android:maxLines を設定されたい。結構忘れがちなのでDangerで自動チェックしようとした...けどやりたかったことを全部満たすのは大変そうだったので妥協した話。

やりたかったこと

  • レイアウトファイルのdiffのTextView内に android:maxLines がなかったら警告する
  • 警告にはファイルと行数を指定してPR上から飛べるようにする(e.g. warn("android:maxLinesがありません", file: "app/src/main/res/layout/content_main.xml", line: 10))
  • 既存のTextViewには警告を出さない

結論から言うと最後の 既存のTextViewには警告を出さない を諦めた

どうチェックするか?

Dangerではgit.diffでdiffが取れるのでパースできる。もしくは

active_files = (git.modified_files + git.added_files).uniq
layout_files = active_files.select { |file| file.include?("app/src/main/res/layout/") }

とすれば変更されたレイアウトファイル一覧が取れるので そのファイル全体を読み込んでパースすることもできる。

まずgit.diffをパースする場合、問題としては行数が取れない。diffと実際のファイルを比べて行数を取ってくるのはしんどい...妥協としてTextViewの始まりから終わりまでを表示させてみたが削除部分が誤判定されてしまい排除しなくてはいけなく、更にしんどい...

in_textview = false
maxline_exists  = false
textview_line = []
git.diff.patch.lines.each do |diff_line|
  if /^.*<TextView$/ === diff_line
    in_textview = true
    textview_line.clear
    message("In TextView, #{diff_line}")
  end
  if in_textview 
    textview_line.push(diff_line)
    if diff_line.include?("</TextView>") || diff_line.include?("/>")
      in_textview = false
      if !maxline_exists
        message("Please add android:maxLines\n```xml\n#{textview_line.join()}```")
      end
    elsif diff_line.include?("android:maxLines")
      maxline_exists  = true
    end
  end    
end

f:id:phicdy:20190817220449p:plain

レイアウトファイル全体をパースする場合、PRでの変更外も警告してしまうので鬱陶しい可能性がある。 android:maxLines が必要ないラベルなどがあると常に警告されてしまう。とはいえ行数が取れるので表示自体は理想通りになる。

active_files = (git.modified_files + git.added_files).uniq
layout_files = active_files.select { |file| file.include?("app/src/main/res/layout/") }

in_textview = false
maxline_exists  = false
textview_line_num = 0
layout_files.each do |filename|
  file = File.read(filename)
  lines = file.lines
  lines.each_with_index do |l, num|
    if l.include?("<TextView")
        in_textview = true
        textview_line_num = num + 1
    end
    if in_textview 
        if l.include?("</TextView>") || l.include?("/>")
            in_textview = false
            if !maxline_exists
                message("Please add android:maxLines", file: "#{filename}", line: textview_line_num)
            end
        elsif l.include?("android:maxLines")
            maxline_exists  = true
        end
    end    
  end
end

f:id:phicdy:20190817220626p:plain

結論

警告表示が理想的なレイアウトファイル全体をパースする方向で妥協した。不要な警告なら無視すればよいし、例えばstyleが適用されてたらスキップするとか改善のしようはありそう。