おでーぶでおでーぶ

いろいろ書く。いろいろ。

2018 年の振り返り

自分用のメモも兼ねて

f:id:jmatsu:20190103152041j:plain

人生

内容
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 までいったので、なるほど。という気持ち

趣味

今年一番気に入った日本酒はこれ、獅子の里。愛山特有のすっきりした酸味と甘味があって、口の中で転がすと本当にいちごのような香りがして半端なく美味しい。

でもちょっと体が心配になってきたので、量を減らそうと思います・・・

英語力

時々フリートークでDMM英会話をしているけど、前職 *2 からあんまり伸びてない気がする。ただ周りの英語話者(non-native含む)には今も恵まれていて、推敲含めて教えて貰えることが多い環境に身を置けていて大変ありがたい。

学部生の頃に TOEIC 315 点しかなくて留年の危機 *3 に陥ったし、入試や院試 *4で苦しんだけど、今はさすがに上がってるやろ〜〜ってことでTOEICを受けた。

f:id:jmatsu:20190103144915j:plain

英語を仕事で使う身としては圧倒的に低いけど、元が低いこともあって2.5倍になった。やはり「TOEICの点数をあげるには最初に低い点数をとればいいじゃない」説は正しい。

みんなもまず300点くらいを取ることをオススメします。その後900点を超えたらなんと3倍です。

2019 年での目標

  • 中華と和食が多いので、イタリアンのレパートリーを増やそうと思います。何か食いたいものがあったら言ってください。作れるようになります。
  • 今年はインフラやBackend周りを重点的にやろうと思います。
  • いっそ日本酒になりたい

*1:倉庫に本来戻すはずのバックパネルの骨が、ケースの破損を理由に今も我が家に鎮座している

*2:Quipper は外資(?)企業で英語を話す機会や読み書きの機会が大量にあった。ただし話す機会については逃げることが可能(要出展)で、少なくとも事前準備できるケースがほとんどだったので周りから助けられながら生きていた。

*3:当時の東工大は英語4という2年後期の英語科目の認定にTOEICの点数などを使う。今は水準が上がってるらしいが、自分の代はTOEICが500点あれば問題なく単位が取れるという緩い世界だった。つまり315点じゃ単位が取れなかった。もちろん別の講義で単位をとればOK。

*4:TOEICの点数は院試時に英語の点数として反映されていたはず

むしろ食われろ、餃子に

http://pbs.twimg.com/media/B7ZhPFgCMAAHw8W.jpg

http://www.anime-line.com/animes/156/stories/2982

今年もやってまいりました、SHIROBAKO Advent Calendar 2018 4日目です。

adventar.org

さて、SHIROBAKO 第12話、「えくそだす・クリスマス」の回を覚えているでしょうか?

作中で制作されているアニメ「えくそだす」の最終回を仕上げる重要な回であり、杉江さんによるプロフェッショナルな仕事を見ることができます。

杉江さんへの愛については 2016年のAdvent Calendar より、konifar さんによる SHIROBAKO12話の杉江さんが好きなのでただただまとめておきたい を御覧ください。「わかる」しか感想が出てこないので、この点について僕から言うことは何もありません。

konifar.hatenablog.com

しかしこの回には非常に特徴的なキーワードが出てくることをご存知でしょうか?開始5分頃の会話に出てきます。

興津さん「高梨さんは矢野さんを送っていったあと連絡が取れませんでしたが、先程、餃子を食べてから午後には戻る。とのメールが来ました」

本田さん「餃子?なんで?」

興津さん「せっかくだから、だそうです」

本田さん「むしろ食われろ、餃子に」

そうです、餃子です。餃子は美味しいですよね。14話にも出てくることを考えれば、そこらへんのモブよりも餃子の存在感は強いと言えるでしょう。

https://img.gifmagazine.net/gifmagazine/images/70409/original.gif

https://gifmagazine.net/post_images/70409

ということで、宇都宮餃子の町「宇都宮」で育った僕がオススメの餃子を紹介します。ここから SHIROBAKO の話は一切出てきません。

吉祥寺 - みんみん

みんみんはあの松亭がある吉祥寺のハモニカ横丁内に存在する中華料理屋です。

retty.me

なかなかに大ぶりな餃子です。皮は少し厚めでモチモチしつつも くどい弾力はなく、加えて肉と野菜のバランスの良い餡を味わうことができます。

また名物のあさりチャーハンも最高に美味しいです。焼き餃子と一緒に食べましょう。結構量があるので二人でいって餃子をシェアするといいかもしれません。

新橋 - 一味玲玲

2つ目は新橋の中華料理屋、一味玲玲です。

retty.me

15種類を超える餃子を焼き・水・蒸しの3種類の調理法から選べます。

オススメはなんといってもレモン餃子の焼き。レモン果肉の酸味やレモン皮にあるほのかな苦味が良いアクセントになり、ビールが無限に飲めます。

自分の好きな餃子のタネと調理法を見つけてみるのも楽しいかもしれませんね。

宇都宮 - 正嗣

「地元の人でも食べるお店へいきたい」と聞かれることがあります。ただよく驚かれるんですが、地元の友人に聞いても宇都宮民でもあまり餃子専門店に行きません。 *1

そんな中でも自分や地元の友人が行ったことのある餃子専門店が「正嗣」です。*2

www.ucatv.ne.jp

メニューは焼き餃子と水餃子のみ、また持ち帰りだと冷凍餃子が購入できます。ライスやビールはありません。なぜならここは餃子専門店だからです。しらんけど。

1枚210円という安さのため、たこ焼きを食べる感覚・セブンでコーヒーを買う感覚で立ち寄り、焼き1水1を頼んでシュッと食べましょう。

野菜多めの餡で皮は薄く、何個でも食べられるタイプの餃子です。リピーターが多いようで、友人の中には実家を出てからも通販で買ってる人もいるようです。

宇都宮 - 来らっせ

餃子を色々食べたい・・・というあなたには「来らっせ」というお店をオススメします。

retty.me

宇都宮には宇都宮餃子会と呼ばれる協会があり、来らっせはその協会の中でも有名な店舗の餃子を一箇所で楽しめる融合店舗になっています。また宇都宮餃子というワードとともにテレビ(首都圏しか知らない)で取り上げられるお店は大体この協会に所属しています。

これらの餃子屋は行列のできるお店もあり、全てを回ることはもちろんのこと、一部の店舗をはしごすることすら難しかったりします。それを1店舗で楽しめるというのはかなりの良さじゃないでしょうか?まあいったことないんですけど。

ちなみに上で紹介した正嗣はこの協会に所属していません。

*1:宇都宮は餃子の町やカクテルの町、日本一景観の悪い駅など様々な肩書を持ちますが、その中で一番「わかる〜〜〜」と思うのは日本一景観の悪い駅という肩書だけです(個人の感想です)

*2:高校のすぐそばにあったので。家族で餃子専門店にいったことは一回もないです

2018年に開拓したオススメのビアバー or 醸造所

完全に忘れてた Beer Advent Calendar 2018 2日目です。

adventar.org

もちろんこの日もビールを飲んだんですが、一枚も写真を撮ってなかったので、今年新しくいった国内外のビアバーや醸造*1 の中でもオススメをペタペタ貼っていきます。*2

ベルギー

BrewDog at Brussels

https://fastly.4sqi.net/img/general/590x442/81972631_GZTaxY8xazLJGvhC7O-6V67JXAA2Ze4UP04aDtEsxGw.jpg

半円型の建物で2階建て。中央は吹き抜けになっており、カウンター席も豊富。ふぉとじぇにっくなのでBrusselsにいったらオススメ。

なお熱があって一杯も飲んでない。泣いた。

foursquare.com

オランダ

Proeflokaal Arendsnest at Amsterdam

https://fastly.4sqi.net/img/general/590x442/81972631_goXNnkVPc0K0nhga1EQipER71lbhrFP960bKxBdLC-0.jpg

アムステルダムで有名なビアバー。タップの種類は50を超える。建物内はガヤガヤしており、ゆったり飲むなら外席をオススメする。

写真は外席。建物の中はカウンターが8席ほどとテーブルが数個、それと地下がある。あまり席数は多くないが、注文して外で飲める。地下ではちょうどビアーテイスティングスクールをやっていて入れなかった。

https://foursquare.com/v/proeflokaal-arendsnest/4a26ffc2f964a520e0801fe3

デンマーク

Mikkeller Bar at Copenhagen

https://fastly.4sqi.net/img/general/590x442/81972631_knFp8C1Ka-ZfvU4tdOWXC6usKPsmw2qFuMDHcyHegCE.jpg

ミッケラーバーの本家。半分地下のような感じで、テラス席がある。中はあまり静かではないので、ゆっくり飲むならテラス席へ。

https://foursquare.com/v/mikkeller-bar/4bdf0ac9be5120a162abfe70

TapHouse at Copenhagen

https://fastly.4sqi.net/img/general/590x786/81972631_B_nI2GrdHNR7S9RfmqwLTVr-pHWOZjR428c0a2-KL2Q.jpg

書いてある通り 61 のタップがある。開栓したタイミングなども表示されている。非常に落ち着いた雰囲気で、ゆっくり飲めるのでオススメ。

https://foursquare.com/v/taphouse/5311e1ab498e444e2f4ea063foursquare.com

らあめんとびいる at Copenhagen

https://pbs.twimg.com/media/Doau0mYUcAESzNf.jpg

https://pbs.twimg.com/media/DoauHtAU4AExO1u.jpg

あのミッケラーのオーナーがやっている日本式ラーメン屋。めちゃくちゃうまい日本のラーメンとミッケラービールを楽しめる。うまいラーメンとIPAは最高。

https://foursquare.com/v/ramen-to-b%C3%ADiru/5662c4f0498e91cb498a0a8d

日本

やっほーブルーイング

https://fastly.4sqi.net/img/general/590x786/81972631_YFwIfqGPKAPa8TSJFD93f4H_gpcshImz5Bb62Ti7Zsc.jpg

大人の醸造所見学ツアーで訪問。ツアーは評判通り最高に楽しかったです。来年もいきたいなあ。

https://foursquare.com/v/%E3%82%88%E3%81%AA%E3%82%88%E3%81%AA%E3%82%A8%E3%83%BC%E3%83%AB-%E5%A4%A7%E4%BA%BA%E3%81%AE%E9%86%B8%E9%80%A0%E6%89%80%E8%A6%8B%E5%AD%A6%E3%83%84%E3%82%A2%E3%83%BC/5b4c19b01f7440003918cf04

台湾

Mikkeller Bar Taipei at 台北

https://fastly.4sqi.net/img/general/590x786/81972631_RRGK5mGk9Mc_nrSr5VhvtfZqhkiCQEie7AhuXBsrBA0.jpg

台湾に2店舗あるうちの1つ。*3 台湾ミッケラーオリジナルビールはなく、ゲストビールで台湾のクラフトビールはある。

足下通のすぐ近くにあり、非常に台湾的な町並みの中で台湾的でない内装なので、休憩がてら行くのがオススメ。

https://foursquare.com/v/mikkeller-bar/578ed372498e00a07b726a42

*1:新規ビアバー13店舗、醸造所は4件でした

*2:いったことあるビアバーを貼ると BrewDog Roppongi が28回出てくることに気づいてやめました。

*3:もう一店舗もいったけど写真が消えた

静的解析(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 を使う場合は出てこない。悲しみが深い。

f:id:jmatsu:20181115134434j:plain

解決策

以下の拡張関数を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"]

        ...
    }
}

リリース自体を自動化してもっとシュッとしたい(小並感)