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"] ... } }
リリース自体を自動化してもっとシュッとしたい(小並感)
エミュレーターでプリインアプリを再現する
今回は API 27 でお試し。
- 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以下への書き込み権限の取得方法が違ったのでちょっとだけハマった
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された。今後動かなくなるかもしれない。
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"