2018 年の振り返り
自分用のメモも兼ねて
人生
月 | 内容 |
---|---|
1月末 | Quipper を退職 |
2月頭 | DroidKaigi |
2月~ | スノボいきまくる |
2月~ | 業務委託を2件持ってみて、忙殺される |
4月頭 | DeployGate に入社 |
5月 | 会社のお金で I/O |
8月 | KotlinFest のお手伝い |
8月 | 我が家納骨所になる*1 |
10月頭 | 会社のお金で Kotlin Conf (デンマーク経由・滞在) |
11月末 | JAL 修行終了 (会社のお金ではない) |
正直大体の月で忙殺されていた気がするけど、原因を具体的に覚えてない事項が多い。
2月から週2~3 の業務委託を2件契約して週5~6 で働いた時期がちょっとだけあったけれど、3日ごとくらいにコンテキストスイッチが必要になるし、日によっては1日でコンテキストスイッチを頻繁に考える必要があった。
当然効率は一定以上上がらないし、この状態で「自分から球を拾いに行くスタイル」で働くことは(少なくとも自分には)かなり厳しいことがわかったのでもう二度とやらない・・・という学びを得た
Backend (本業)
- GCM -> FCM 移行
- クレジットカード支払い周りの再設計
- Group のクレジットカード支払い対応
- サーバーサイドのローカル環境構築の自動化
- Enterprise における SAML 認証 (テスト運用中)
1個1個のタスクが大きくて、数にしたらあんまりなかった。
Android (本業)
- GCM -> FCM 移行
- CI チューニングとジョブの整備
- Crashlytics などの解析系導入と解析結果の GitHub Issues への連携
- WebTranslateIt による翻訳文書の管理
- svg -> vector drawable, optimized png の自動インポート
- Ribbonizer や git-pr-release などによる一人でもリリース間違えないもんリリースフローの整備
- Detekt, ktlint, Android Lint × Danger による一人でも寂しくないもん開発環境の構築
- Re-architect
- AndroidX と Material Components 対応 (未リリース)
弊社ァの Android リポジトリは独立時点で新しくしたことやそもそもそんなアップデートをしていなかったこともあって、入社時点で PR/Issue が合計で9個しかなかった。 今確認したら 330 までいったので、なるほど。という気持ち
趣味
- DeployGate TGIF で料理を作っている
- ビールの醸造所やバーを巡った http://jmatsu.hatenablog.com/entry/2018/12/03/154552
- 出社回数 < BrewDog Roppongi 来店数 の週が増えた
- 酒蔵めぐりと日本酒を浴びるように飲んだ
- Danger Plugin https://github.com/jmatsu/danger-apkstats , https://github.com/jmatsu/danger-checkstyle_reports
- 使ってる OSS の bug fixes
- DroidKaigi API 作成 (進行中)
- 松亭月例会
- https://www.swarmapp.com/user/year-in-review/2018
苺のような香りがほのかにして、美味い。気に入った。高いけど。 pic.twitter.com/PbwncM8qvn
— おなかすいた (@red_fat_daruma) December 26, 2018
今年一番気に入った日本酒はこれ、獅子の里。愛山特有のすっきりした酸味と甘味があって、口の中で転がすと本当にいちごのような香りがして半端なく美味しい。
でもちょっと体が心配になってきたので、量を減らそうと思います・・・
英語力
時々フリートークでDMM英会話をしているけど、前職 *2 からあんまり伸びてない気がする。ただ周りの英語話者(non-native含む)には今も恵まれていて、推敲含めて教えて貰えることが多い環境に身を置けていて大変ありがたい。
学部生の頃に TOEIC 315 点しかなくて留年の危機 *3 に陥ったし、入試や院試 *4で苦しんだけど、今はさすがに上がってるやろ〜〜ってことでTOEICを受けた。
英語を仕事で使う身としては圧倒的に低いけど、元が低いこともあって2.5倍になった。やはり「TOEICの点数をあげるには最初に低い点数をとればいいじゃない」説は正しい。
みんなもまず300点くらいを取ることをオススメします。その後900点を超えたらなんと3倍です。
2019 年での目標
- 中華と和食が多いので、イタリアンのレパートリーを増やそうと思います。何か食いたいものがあったら言ってください。作れるようになります。
- 今年はインフラやBackend周りを重点的にやろうと思います。
- いっそ日本酒になりたい
2019年を振り返って
セブンからの帰り道、しほちゃんちの鍵を自分の家のオートロックに差し込もうとして刺さらなくて困惑しながら2019年を迎えた。
上の階の人がハッピーニューイヤー!おやすみ!って叫んでた。
完
むしろ食われろ、餃子に
http://www.anime-line.com/animes/156/stories/2982
今年もやってまいりました、SHIROBAKO Advent Calendar 2018 4日目です。
さて、SHIROBAKO 第12話、「えくそだす・クリスマス」の回を覚えているでしょうか?
作中で制作されているアニメ「えくそだす」の最終回を仕上げる重要な回であり、杉江さんによるプロフェッショナルな仕事を見ることができます。
杉江さんへの愛については 2016年のAdvent Calendar より、konifar さんによる SHIROBAKO12話の杉江さんが好きなのでただただまとめておきたい を御覧ください。「わかる」しか感想が出てこないので、この点について僕から言うことは何もありません。
しかしこの回には非常に特徴的なキーワードが出てくることをご存知でしょうか?開始5分頃の会話に出てきます。
興津さん「高梨さんは矢野さんを送っていったあと連絡が取れませんでしたが、先程、餃子を食べてから午後には戻る。とのメールが来ました」
本田さん「餃子?なんで?」
興津さん「せっかくだから、だそうです」
本田さん「むしろ食われろ、餃子に」
そうです、餃子です。餃子は美味しいですよね。14話にも出てくることを考えれば、そこらへんのモブよりも餃子の存在感は強いと言えるでしょう。
https://gifmagazine.net/post_images/70409
ということで、宇都宮餃子の町「宇都宮」で育った僕がオススメの餃子を紹介します。ここから SHIROBAKO の話は一切出てきません。
吉祥寺 - みんみん
みんみんはあの松亭がある吉祥寺のハモニカ横丁内に存在する中華料理屋です。
なかなかに大ぶりな餃子です。皮は少し厚めでモチモチしつつも くどい弾力はなく、加えて肉と野菜のバランスの良い餡を味わうことができます。
また名物のあさりチャーハンも最高に美味しいです。焼き餃子と一緒に食べましょう。結構量があるので二人でいって餃子をシェアするといいかもしれません。
新橋 - 一味玲玲
2つ目は新橋の中華料理屋、一味玲玲です。
15種類を超える餃子を焼き・水・蒸しの3種類の調理法から選べます。
オススメはなんといってもレモン餃子の焼き。レモン果肉の酸味やレモン皮にあるほのかな苦味が良いアクセントになり、ビールが無限に飲めます。
自分の好きな餃子のタネと調理法を見つけてみるのも楽しいかもしれませんね。
宇都宮 - 正嗣
「地元の人でも食べるお店へいきたい」と聞かれることがあります。ただよく驚かれるんですが、地元の友人に聞いても宇都宮民でもあまり餃子専門店に行きません。 *1
そんな中でも自分や地元の友人が行ったことのある餃子専門店が「正嗣」です。*2
メニューは焼き餃子と水餃子のみ、また持ち帰りだと冷凍餃子が購入できます。ライスやビールはありません。なぜならここは餃子専門店だからです。しらんけど。
1枚210円という安さのため、たこ焼きを食べる感覚・セブンでコーヒーを買う感覚で立ち寄り、焼き1水1を頼んでシュッと食べましょう。
野菜多めの餡で皮は薄く、何個でも食べられるタイプの餃子です。リピーターが多いようで、友人の中には実家を出てからも通販で買ってる人もいるようです。
宇都宮 - 来らっせ
餃子を色々食べたい・・・というあなたには「来らっせ」というお店をオススメします。
宇都宮には宇都宮餃子会と呼ばれる協会があり、来らっせはその協会の中でも有名な店舗の餃子を一箇所で楽しめる融合店舗になっています。また宇都宮餃子というワードとともにテレビ(首都圏しか知らない)で取り上げられるお店は大体この協会に所属しています。
これらの餃子屋は行列のできるお店もあり、全てを回ることはもちろんのこと、一部の店舗をはしごすることすら難しかったりします。それを1店舗で楽しめるというのはかなりの良さじゃないでしょうか?まあいったことないんですけど。
ちなみに上で紹介した正嗣はこの協会に所属していません。
2018年に開拓したオススメのビアバー or 醸造所
完全に忘れてた Beer Advent Calendar 2018 2日目です。
もちろんこの日もビールを飲んだんですが、一枚も写真を撮ってなかったので、今年新しくいった国内外のビアバーや醸造所 *1 の中でもオススメをペタペタ貼っていきます。*2
ベルギー
BrewDog at Brussels
半円型の建物で2階建て。中央は吹き抜けになっており、カウンター席も豊富。ふぉとじぇにっくなのでBrusselsにいったらオススメ。
なお熱があって一杯も飲んでない。泣いた。
オランダ
Proeflokaal Arendsnest at Amsterdam
アムステルダムで有名なビアバー。タップの種類は50を超える。建物内はガヤガヤしており、ゆったり飲むなら外席をオススメする。
写真は外席。建物の中はカウンターが8席ほどとテーブルが数個、それと地下がある。あまり席数は多くないが、注文して外で飲める。地下ではちょうどビアーテイスティングスクールをやっていて入れなかった。
https://foursquare.com/v/proeflokaal-arendsnest/4a26ffc2f964a520e0801fe3
デンマーク
Mikkeller Bar at Copenhagen
ミッケラーバーの本家。半分地下のような感じで、テラス席がある。中はあまり静かではないので、ゆっくり飲むならテラス席へ。
https://foursquare.com/v/mikkeller-bar/4bdf0ac9be5120a162abfe70
TapHouse at Copenhagen
書いてある通り 61 のタップがある。開栓したタイミングなども表示されている。非常に落ち着いた雰囲気で、ゆっくり飲めるのでオススメ。
https://foursquare.com/v/taphouse/5311e1ab498e444e2f4ea063foursquare.com
らあめんとびいる at Copenhagen
あのミッケラーのオーナーがやっている日本式ラーメン屋。めちゃくちゃうまい日本のラーメンとミッケラービールを楽しめる。うまいラーメンとIPAは最高。
https://foursquare.com/v/ramen-to-b%C3%ADiru/5662c4f0498e91cb498a0a8d
日本
やっほーブルーイング
大人の醸造所見学ツアーで訪問。ツアーは評判通り最高に楽しかったです。来年もいきたいなあ。
台湾
Mikkeller Bar Taipei at 台北
台湾に2店舗あるうちの1つ。*3 台湾ミッケラーオリジナルビールはなく、ゲストビールで台湾のクラフトビールはある。
足下通のすぐ近くにあり、非常に台湾的な町並みの中で台湾的でない内装なので、休憩がてら行くのがオススメ。
https://foursquare.com/v/mikkeller-bar/578ed372498e00a07b726a42
静的解析(Lint等)に頼らず、ViewModelに載ってる LiveData を ViewModel 外で更新させないようにすることを考える
これはまだプロダクションで動かしてないので気をつけてください
AAC ViewModel に生えた LiveData を外に出すとき、フィールドの重複宣言は面倒だし、だからといって直接 expose はしたくないし、それに MutableLiveData や MediatorLiveData を返してしまうと最悪キャストすれば更新できてしまう。
弊社で Android を触ってるのは今の所自分ひとりなので、とりあえず Mutable で expose して速度を優先しているのだけど、将来的にメンバーが増えたときとかはそうも言ってられない。
もちろんキャストとか ViewModel 外での更新はコードレビューで弾けるようなものなのだけど、「してはいけない」タイプの原則を人間が指摘しなければならないのは無駄だなーと個人的には思っている。また今回の問題は Lint で簡単に検出できる(caller の先祖要素に ViewModel がいないことを見ればいい)ので静的解析でチェックしてもいいんだけど、そうじゃない方法を考えてみたい。
と思ってたときに以下の記事が出てきた。
MutableなLiveDataを特定のクラス外から更新できなくする · stsnブログ
わかる と思ったのだけど、複数のパッケージに跨る LiveData と ViewModel とかも存在する可能性があって、その場合にはこの方法が使えないのでそこらへんを解決してる別の方法を考えてみた。(正直AACを絡めた設計はまだ考えているという段階で、将来的にそういうケースが出てくるかも?程度の可能性でしかない)
- APT
- Compiler plugin
とかも考えたけど、これは結局メンテできるひとがいなくなると黒魔術を越えた失われし秘術になってしまうので却下。Kotlin versionに合わせて動かなくなったら面倒くさいし。
ということでViewModel とその派生クラスからのみ更新するにはやはり可視性を protected にするという点を利用する。また Observable#hide
のように、別の実体に変換して対応する。
import android.app.Application import androidx.annotation.CallSuper import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer open class ViewModel(application: Application) : AndroidViewModel(application) { protected fun <T> LiveData<T>.mutate(): MutableLiveData<T> { return when (this) { is LiveDataWrapper -> this.wrappedLiveData is MutableLiveData -> this else -> throw IllegalArgumentException("the given live data is not wrapped. Please use liveData delegation at declarations.") } } protected inline fun <reified T> ViewModel.lazyLiveData(crossinline initializer: () -> LiveData<T>): Lazy<LiveData<T>> { return lazy { val liveData = initializer() if (liveData is MutableLiveData) { LiveDataWrapper(liveData) } else { liveData } } } protected inline fun <reified T> ViewModel.liveData(liveData: LiveData<T>): Lazy<LiveData<T>> { return lazyOf( if (liveData is MutableLiveData) { LiveDataWrapper(liveData) } else { liveData } ) } protected class LiveDataWrapper<T>(val wrappedLiveData: MutableLiveData<T>) : LiveData<T>() { private val observer: Observer<T> = Observer { super.setValue(it) } @CallSuper override fun onActive() { wrappedLiveData.observeForever(observer) } @CallSuper override fun onInactive() { wrappedLiveData.removeObserver(observer) } override fun postValue(value: T) { wrappedLiveData.postValue(value) } override fun setValue(value: T) { wrappedLiveData.value = value } } }
この ViewModel を使って、任意の ViewModel を構築していく。
class TestViewModel(application: Application) : ViewModel(application) { val exposedLiveData: LiveData<Int> by liveData { MutableLiveData<Int>() } fun testUpdate() { // compilation error : cannot access the function // exposedLiveData.value = 0 exposedLiveData.reify().setValue2(2) } } fun test(application: Application) { val vm = TestViewModel(application) vm.apply { // compilation error : cannot access the function // exposedLiveData.reify() // compilation error : cannot access the function // exposedLiveData.value = 0 // will crash exposedLiveData as MutableLiveData<*> } }
画面更新有りのサンプル
http://jmatsu.hatenablog.com/entry/2018/12/01/171740 · GitHub
こんな感じでやればいけそうな気がする。Reflectionによる更新はさすがに考えない。
ところで Kotlin の List/MutableList は JDK との都合上で仕方ないとして、LiveData/MutableLiveData のネーミングはどうにかならなかったのだろうか・・・
最初に貼ってたやつは wrapper を更新していて、何の意味もなかった。
package xxxx import android.app.Application import androidx.annotation.AnyThread import androidx.annotation.CallSuper import androidx.annotation.MainThread import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.Observer open class ViewModel(application: Application) : AndroidViewModel(application) { // ViewModel クラス内でのみ Wrapper クラスを参照できるようにする protected fun <T> LiveData<T>.reify(): LiveDataWrapper<T> { return requireNotNull(this as? LiveDataWrapper) { "the given live data is not wrapped. Please use liveData delegation at declarations." } } // LiveData を wrap する delegation を用意する protected inline fun <reified T> ViewModel.lazyLiveData(crossinline initializer: () -> LiveData<T>): Lazy<LiveData<T>> { return lazy { LiveDataWrapper(initializer()) } } protected inline fun <reified T> ViewModel.liveData(liveData: LiveData<T>): Lazy<LiveData<T>> { return lazyOf(LiveDataWrapper(liveData)) } // LiveData を wrap して、MutableLiveData 及びその派生を直接 expose しない。 protected class LiveDataWrapper<T>(private val liveData: LiveData<T>) : LiveData<T>() { private val observer: Observer<T> = Observer { setValue2(it) } @CallSuper override fun onActive() { liveData.observeForever(observer) } @CallSuper override fun onInactive() { liveData.removeObserver(observer) } // Reflection での更新を防止しておく。 // KotlinのDeprecatedを使って、誤った利用を避ける。 @Deprecated( message = "use postValue2 instead. this method always cause a crash", replaceWith = ReplaceWith( "postValue2(value)", imports = ["xxxx.ViewModel.LiveDataWrapper.postValue2"] ), level = DeprecationLevel.ERROR ) override fun postValue(value: T) { throw IllegalAccessError("cannot be invoked even through reflection") } @Deprecated( message = "use setValue2 instead. this method always cause a crash", replaceWith = ReplaceWith( "setValue2(value)", imports = ["xxxx.ViewModel.LiveDataWrapper.setValue2"] ), level = DeprecationLevel.ERROR ) override fun setValue(value: T) { throw IllegalAccessError("cannot be invoked even through reflection") } // 更新用のメソッドを新たに生やしておく @AnyThread fun postValue2(value: T) { super.postValue(value) } @MainThread fun setValue2(value: T) { super.setValue(value) } } }
Kotlinの拡張関数を使って、Cursor#use すると ClassCastException で落ちる
現象
Fatal Exception: java.lang.ClassCastException: android.content.ContentResolver$CursorWrapperInner cannot be cast to java.io.Closeable
原因
Cursor が Closeable を継承するのは API 16 から。ContentResolver の返す Cursor が custom cursor にできない問題に気を取られすぎて、Closeable を継承してないのを忘れていた・・・
明示的に Cursor を Closeable にすると、ちゃんと Android Lint が怒ってくれる。use を使う場合は出てこない。悲しみが深い。
解決策
以下の拡張関数をcompatで書いて対応。関数名をuseにしていないのは書いてるときにimportする関数を間違える可能性が高いから。
import android.database.Cursor import android.os.Build import kotlin.io.use as ioUse inline fun <T : Cursor?, R> T.useCompat(block: (T) -> R): R { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { ioUse(block) } else { var exception: Throwable? = null try { return block(this) } catch (expected: Throwable) { exception = expected throw expected } finally { when { this == null -> { } exception == null -> close() else -> try { close() } catch (ignored: Throwable) { } } } } }
理想的な解決策
minimum support API を28にする
Android app の versioning と naming
弊社ァでは今までリリースごとに1つずつバージョンを上げていたけれど、以下の問題点が存在した。
- meaninglessなバージョンなので「それはアプリのx.y.zバージョンだとどれなのか」が分かりづらい。 e.g. SDK とのやり取り
- Internal Trackテスト版ではログが有効であるなど、そのアプリをそのままリリースはできない。したがって最終的なリリース版は内部テストの回数だけ番号が上がってしまう。どのapkがproduction apkが分かりづらく、↑の問題をさらに加速させる。
そこで versioning と naming をうまくやることで上記の問題点を解決することにした。
- CI でしかリリース署名apkは作らない
release
ブランチの成果物を production artifacts としているinternal/xxx
ブランチの成果物は Internal Track で使う artifacts
として、以下の記述にしている。
ext { isForInternalTrack = System.getenv("CI") == "true" && System.getenv("CIRCLE_BRANCH")?.startsWith("internal/") isReleaseCandidate = !isForInternalTrack && System.getenv("CI") == "true" && System.getenv("CIRCLE_BRANCH") == "release" } android { defaultConfig { applicationId ... def versions = [ "major" : 1, "minor" : 0, "patch" : 0, "internal": 0 ] assert 0 <= versions["major"] && versions["major"] <= 99 assert 0 <= versions["minor"] && versions["minor"] <= 99 assert 0 <= versions["patch"] && versions["patch"] <= 99 assert 0 <= versions["internal"] && versions["internal"] <= 99 versionName versions["major"] + "." + versions["minor"] + "." + versions["patch"] if (!isReleaseCandidate) { versionNameSuffix "-${hashOrCINum}-non-production" } if (isForInternalTrack) { versionNameSuffix = (versionNameSuffix + "-internal-track") } // max value which GooglePlay allows is 2,100,000,000. versionCode 1_000_000 * versions["major"] + 10_000 * versions["minor"] + 100 * versions["patch"] + versions["internal"] ... } }
リリース自体を自動化してもっとシュッとしたい(小並感)