おでーぶでおでーぶ

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

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"]

        ...
    }
}

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

エミュレーターでプリインアプリを再現する

  1. Google APIの入っていないAVDを作成しておく

今回は API 27 でお試し。

f:id:jmatsu:20181112162241j:plain

  1. AVD name を確認しておく

ls ~/.android/avd/

cd $ANDROID_SDK_HOME/tools

export PATH=$PWD:$PATH

# tools 以下にいないと以下のコマンドは失敗する
# -writable-system で起動しないと/systemに書き込み権限がない
emulator -avd $avd_name -writable-system
# note: avd_name に拡張子はいらない

# Google APIが入ってるとproduction扱いなので、adbdがrootで立ち上がらない
adb root
adb remount

adb shell
# このディレクトリにプリインが入る。古いバージョンだとディレクトリが違う。
cd /system/priv-app
mkdir $package_name

exit

adb push $apk_file /system/priv-app/$package_name/$apk_file

# 再起動するまではインストールされない
adb reboot

昔とsystem以下への書き込み権限の取得方法が違ったのでちょっとだけハマった

qiita.com

CircleCI の特定ブランチの特定jobのアーティファクトをダウンロードする

はい

#!/usr/bin/env bash

set -o pipefail
set -eu

export PRODUCTION_BRANCH="release"
export JOB_NAME="build"

latest_artifacts() {
    local -r build_num=$(curl -u "$CIRCLECI_TOKEN:" "https://circleci.com/api/v1.1/project/github/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/tree/$PRODUCTION_BRANCH?filter=completed" | \
      ruby -rjson -e 'puts JSON.parse(STDIN.read).find { |j| j["build_parameters"]["CIRCLE_JOB"] == ENV.fetch("JOB_NAME") }["build_num"]')
    
    curl -u "$CIRCLECI_TOKEN:" "https://circleci.com/api/v1.1/project/github/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/$build_num/artifacts"
}

get_asset_url() {
    cat - | ruby -rjson -e 'puts JSON.parse(STDIN.read).select { |j| <a filter like `j["url"].include?(".apk")`> }.first["url"]'
}

download_asset() {
    local -r asset_url=$(latest_artifacts | get_asset_url)
    curl -o "$1" "$asset_url?circle-token=$CIRCLECI_TOKEN"
}

download_asset "$1"

GETでbranchを絞る方法はAPI Docになかったけれど、POSTの方法を参考にしたらちゃんとfilterされた。今後動かなくなるかもしれない。

https://circleci.com/docs/api/v1-reference/

GithubのLatest Releaseからassetをダウンロードするスクリプト

はい

#!/usr/bin/env bash

set -o pipefail
set -eu

GITHUB_USERNAME="..."
GITHUB_REPONAME="..."
GITHUB_API_TOKEN_USERNAME="...
: "${GITHUB_API_TOKEN:=$DANGER_GITHUB_API_TOKEN}"

latest_gh_release() {
    curl -#L -H "Authorization: token $GITHUB_API_TOKEN" "https://api.github.com/repos/$GITHUB_USERNAME/$GITHUB_REPONAME/releases/latest"
}

get_asset_url() {
    cat - | ruby -rjson -e 'puts JSON.parse(STDIN.read)["assets"].select { |j| <<the filter like `j["name"].end_with?(".apk")` %>> }.first["url"]'
}

download_asset() {
    local -r asset_url=$(latest_gh_release | get_asset_url)

    curl -#Lo "$1" -u "$GITHUB_API_TOKEN_USERNAME:$GITHUB_API_TOKEN" -H 'Accept: application/octet-stream' "$asset_url"
}

download_asset "$1"