どくぴーの備忘録

真面目なことを書こうとするクソメガネのブログ。いつ投げ捨てられるのかは不明

ガルパン劇場版のお陰でコンテンツ消費環境に興味が出てしまった話

まずはじめに

この記事は ガールズ&パンツァー Advent Calendar 2017 19日目の記事です。

最終章第一話が始まりましたね

いやー、ガールズ&パンツァー 最終話 第一話がついに公開されましたね!一週間以上前の話ですけれども! 皆さんはもう見られましたか!?僕はいろいろ忙しかった事もあってこの間の日曜日にシネマシティに行って観てきました! じゃあレビューでもするのかとでもなるのですが、そんなネタバレなどという無粋なことは言いません。ただ一言

ガルパンはいいぞ

と言い残すのみです。

じゃあ何の話をするのか

コンテンツ消費の環境についてのお話をしようかと思います。

ガルパンにハマるまで

ガルパンガルパン劇場版に出会うまでは、正直なところ「コンテンツは消費さえできればそれでいい」なんて思っていました。音質や画質よりもコンテンツそのものが良ければどうでもいいと。なので映画をわざわざ映画館で見る、なんてせずに後からストリーミングサービスでタブレットやパソコンに安いイヤホンを繋いでみればいいじゃない、みたいな消費をしていたわけです。

そんな感じでコンテンツを消費していた大学時代、同期が東京に行ってくるついでにあのシネマシティでガルパン劇場版を観て帰ってきました。どうにもドハマリしたようで「いいぞ」という言葉を残してきました。当時ガルパンを知らなかったので、そこまで言うなら劇場版を見に行こうと話を聞いていたら「絶対にシネマシティで極上爆音上映を観ろ」と言ってきます。極上爆音という言葉はひたすらヤバそうなのですが一体何が違うのかわからないなぁって思いながらその一週間後に東京に行く予定があったので、そこまでに予習としてAmazonプライムビデオでTV版とOVAアンツィオ戦を観て本編サントラを買うなどしていました。本編サントラを買うあたり若干もうコンテンツ自体にハマりかけていますね。

そんなこんなで迎えた劇場版を見に行く日。もうすでに観た人は「ガルパンはいいぞ」としか言い残さない文化が出来上がっているので「ネタバレをしないように最大限考えた上で出てくる言葉なんだろうなぁ」くらいにしか思っていませんでした。なのでこんなツイートもしてしまう。

そして観てきた後の僕がこれです。

風潮に乗っかったつもりは毛頭なかったのですが、色々と体験が素晴らしすぎたのか、ツイートどころかシネマシティの建物を出てため息のように出た言葉すら「よかった…」でした。素晴らしい環境での素晴らしい体験は脳を揺さぶり溶かすということを身をもって知った瞬間でした。

ハマってから

その後も一回では飽き足らず、他の映画館でもガルパンを観たい!などと言い出し何度もガルパンを鑑賞する一年になりました。すべてを数えてみると

  • シネマシティ:6回(内レオバルド3回)
  • 梅田ブルク7:2回
  • 塚口サンサン劇場:3回
  • BD:数えるのをやめた

という感じに。あまりにも観すぎて次に来るセリフがわかるあたりはガルパンおじさんなら皆経験があるのではないでしょうか。

この中でもやはり素晴らしかったなと思うのはレオパルド化したシネマシティと塚口サンサン劇場でした。当時関西圏の学生だった僕には塚口であの脳が揺さぶられる体験ができるって言う事実に打ち震えたものです。

コンテンツ消費に思うこと

まず映像作品、特に映画を見る際の意識が変わりました、やはり新鮮さという意味でも体験の品質という意味でも映画館のスクリーンで見るという体験は家のディスプレイで見るものとは違うものがあります。家のディスプレイで友達とワイワイ見るのも素晴らしいんですが、好きなコンテンツで映画館に行ける環境があるならばぜひとも映画館でみたい!って思えるようになりました。なので気になった映画は映画館に行く余裕があれば観に行っています。『君の名は。』とか『響け!ユーフォニアム』とか。事前情報がまったくなかったのですが『ダンケルク』も面白かったです。

そんなことをしていると映像作品ではないコンテンツ消費にも気を使いたくなってきました。真っ先に思いつくのは音楽ですね。これまで「イヤホン良くわからないんだよなー、いいやつとか何が違うの?」という考えで、2000円程度のイヤホンを使っていたのですが社会人になって自由に使えるお金が発生したこともあり、プラシーボでもなんでもいいからいい環境でコンテンツを消費したくなってイヤホンを突然10000円くらいするJVCのものに買い替えたりしだしたり、安物ですがポータブルアンプを買ったりしました。そして今ではポータブルアンプがSony PHA-1Aに変わっていたりしています。「何が変わった?」と聞かれると説明しにくいのですが僕としては幸せな気分なので大丈夫でしょう(一応ちゃんと感想を述べると中低音の粒がくっきり聞こえるようになったのでベースとかドラムの音が聞き分けやすくなったりしたと思っている)。まだオーディオ沼の入り口に片足も突っ込んでいないのですがいつ足を取られるかわかったもんじゃないので己を強く持ちたいところです。

ここまで行くと次はゲームやBDの映像をきれいに観たい!ってなるのでしょうけど今の家だと流石に狭くなっちゃうのでしばらくはおあずけなのかなーって気がしています。今だと4KでHDR対応なディスプレイとそれが扱えるプレイヤーが当たるのでしょうね。一体いくらお金がかかるのやら…

最後に

とりとめのない感じになってしまいましたが、僕はガルパン劇場版を観たことで「コンテンツの消費のための環境に対して金をかけることは全然損にならないし気持ちいい」と思うようになってしまいました。周りの友人たちがコンテンツ消費のためにお金を注ぎ込んだりしているのを見てたりしていたのもあってかなり背中を押されまくっている感じがあります。これからも懐具合と相談しながらガルパンにかぎらず、様々なコンテンツを満足できる環境で消費したいなと思います。

頑張って作ったアプリがクソの域を出なかった話 #MA_2017

まずはじめに

この記事は クソアプリ Advent Calendar 2017 11日目の記事です。

一日遅刻しています。土下座モン。

作ったものの話をしよう

今年もMashup Awards真っ只中の冬でございます。MAというとハッカソンって言う人もめちゃくちゃいますがその本質は50以上の作品がひたすらプレゼンを繰り返す2nd stage(日曜にありました)に詰まっている気がします。この楽しみを「参加者として」楽しむために目指している人もいるんだろうなーと思っています。

僕はスタッフをしていました。カメラマンです。

そんなMAに僕が今年何を放ったか、そしてどうしてクソのまま終わったのか、どうやってクソから抜け出していこうかという話をしようと思います。

今年作ったもの

hacklog.jp

そう、今年僕は「課題解決」をしようと思いました。

LTイベントやMAのようなプレゼン大会で欠かせないのは「運営としてのTwitter実況」。その中でも僕が目をつけたのは「発表一回目のツイート」でした。

このツイート、僕は本当に大事だと思っていてハッシュタグウォッチしながら参加している人には今誰がどんな内容で登壇しているのかを伝える役割があり。更にはイベント後にtogetterにまとめるようなときにも区切りとしての役割を果たします。

ではその最初のツイート、どうやって投稿しているか考えてみましょう。

  1. 事前の発表リストを用意する
  2. ツイート内容を構成する
  3. 発表者の手番が来たらそれをTwitterの投稿フォームにコピペして投稿

1はしょうがないとして、2、3はかなり面倒です。ツイート内容の構成では決まったテンプレに収まるように与えられたデータを配置する必要があり、3に至ってはコピペなんかしていては発表が始まってスライドが2枚流れてしまう、なんて言ったことだってあります。

僕はそれを解決したくてこのとうだんくんを作る決意をしました。MAの締め切り3日前に。

どんな感じのものなの

f:id:e10dokup:20171211235853p:plain

Hacklogからサルベージした画像がくっそでかい。

こんな感じで登録したイベントの登壇タイトルを選んで開始時にワンボタンでSNS通知できるっていうそれだけのアプリです。なんで一回選ぶの?っていう疑問も持ったのですが、こういうイベントあるあるとして「直前に何らかの事情で発表順が前後する」という話があるのであえて一旦自分の・今から始まる発表を選択するというアクションを要求するようにしました。 イベントや登壇内容の登録はCSVでバルクアップロードできるように後々改良しましたが、基本的にはイベントに紐つけたIFTTTのWebhookに対してリクエストを飛ばしてそこから自作のレシピに応じてアクションを起こすのでIFTTTが許す範囲とWebhookで扱える変数3つの中であればなんでもできるようになっています。

f:id:e10dokup:20171211235753p:plain

実際の構成はこんな感じで、スマホからはデータ登録が(個人的に)めんどくさいので一切その手のことは扱わず、スマホからサーバへの通信はすべてGETリクエストです。ということは登録はWebアプリ化しないといけなかったのですがCSS力とJS力とHTML力が生まれたての赤子未満なレベルのためMaterial Design Lightを使ってもまともに作れていないやっつけ具合で用意しました。サーバはちょっとだけ見に覚えのあったGolangをgin/gormの構成で雑にGAEにデプロイしています。この辺の話は後々の Mashup Award Advent Calendar で書こうと思います。

こんな小規模なのですが、Androidアプリは当時は拡張する気まんまんで作っていたので多少設計しました。と言っても大げさなものを用意してもアレなのでRxJava2/DataBindingでMVVMな構成をとってKotlinで実装しました。KotlinかわいいよKotlin

何がクソだったのか

ここから反省会フェイズです。端的に三点反省していきましょう。

iOS版がない

ないです。持ってないですしおすし…。 こういうものを作って人に見せると日本って残念ながらiPhoneが圧倒的マジョリティなのでiOS版つくらないんです?」とか言われてしまいます。ぐやじい

ニーズに応えきれない

IFTTT-Webhookで変数3つ使えるじゃん!とか考えてしまったのが運の尽きでした。実際運用してみると思っていたよりも初回ツイートテンプレって柔軟性が高くて、発表順が必要なケースがあったり、文字数オーバーに応じてちょちょいと内容を修正できるようにしてあげられるような仕様にして上げる必要があったりと、IFTTTに投げてドゥン!みたいな銀の弾丸的思考は通用しませんでした。

そもそも自分以外セッティング不能

これが最悪。デザインセンスも思ったものを形にするWebフロント知識もないので、出来上がったWebフロントは使いにくく、作った自分くらいしかデータのセットアップが出来ない代物になってしまいました。時間をかければできたんじゃね?と言われれば勉強込みで出来た気もしますが、結果論では他人が気兼ねなく使えるような状態ではないので残念すぎました。

クソから抜け出すために

でもせっかくクソとはいえある程度形にして自分だけでも楽に使えるようにしたいってのが本音なので、これからも作っていこうと思います。というか作っていって自分で使いたいんです。とりあえず次のことを今は考えています。

Web APIの抜本的作り直し

上がったニーズに応えようとすると、IFTTTのWebhookを使うのはともかくとして、サービス側にテンプレを持っている必要があるという結論にしかなりませんでした。そうじゃないと文字数チェックも出来ないし、3つ以上のパラメータに対応できないし…。

ただIFTTT自体はクソ便利なので、最終的にIFTTTに投げるという格好はしばらく変わらないと思います。

ちゃんとWebフロントの勉強をする

少なくとも僕以外の人がまともに使えるものを作れるように勉強します。というか時間をかけます。従いましては誰かおすすめの本を教えてください。

iOS版を作る

モバイル端末に関してはどうもApple端末が気に食わなかったのですが、ついにこの間買いました。知り合いに譲ってもらったiPhone 6sです。 とりあえず早速使いだしていますが今は先月買ったポータブルアンプのPHA-1Aが接続できることに歓喜しておりほぼ開発に使っていないという有様です。さっさとこっちも勉強しろ。

多分Androidと似たような構成でMVVMにやっていく気がしています。AutoLayoutむずかちい。AutoLayoutを1時間くらいいじった後にAndroidのConstraintsLayoutのエディタを触るとなんかめっちゃ快適に思えてすごく楽しくなってくるのでおすすめです。

まとめ

とりあえず本当に思うことは「急ごしらえするんじゃなくて一旦自分でも身近な人にでも一度使ってもらって何かしらのフィードバックを得るべき」ということでした。ワイワイ作って勢いで作り上げるハッカソン的開発メソッドはテンションがハイのまま一段落つくまで持っていけるのですが、出来たものがどうなるのかというのはやってるうちはやっぱわからないので頭を冷やして出来たものを見る時間が必要だと思いました。ですがクソでも作り上げていかないと何も始まらないので一気にやってよかったなって思います。

多分来年のMAにはもっとまともな姿に生まれ変わっていると思います。みんなもクソアプリ、やっていこうな。

kotlinx.coroutinesでAndroid用画像ローダを実現する

まずこの記事について

この記事は #kosen10s Advent Calendar の6日目の記事です。

<- 前日の記事(@do_su_0805) 翌日の記事(@kogepan159)->

Kotlinについて何かを書きたい

ちょくちょくAndroidについて書いたりしている(最近イベントレポートしかしてない。まずい)んですが、最近業務でKotlinを書いてた(今はJavaに戻ったりしている)り、趣味コードや勉強会に出るときのサンプルを全部Kotlinにしたりしています。KotlinかわいいよKotlin。

というわけで今年も終わるということでちょっと趣味で書いているAndroid向けの画像ローダについて話してみます。ちゃんと欲しい機能ができればOSS化するつもりですが時間がなかなか取れなくてこいつにコミットする時間がなくてア

作りたい物

Androidで画像ローダといったら square/picassobumptech/glide が有名ですが、これらと似たような使用感の小さいライブラリを用意したいという感じです。usageとしてはこんな感じ

Katsushika.with(context)
        .load(url)
        .into(imageView)

トップクラスの命名は適当です。これを思いついたときはSplatoon2にドハマリしてたので思考回路がわかる人にはわかるはず。

何が必要か

ネットワークを介して画像をロードするので、Androidでは当然非同期処理を扱います(メインスレッドで通信はできない)。非同期で扱いたい処理を挙げると

  • 画像のネットワークからのダウンロード
  • 画像の縮小
  • etc.(キャッシュとか。今回は考えない)

そしてこれらの処理を終えた後にImageViewに対して画像を反映させるわけです。

まずは画像取得以外を作ってみる

とりあえずネットワーク周りはめんどくさいので今のところは square/okhttp を頼ります。

とりあえず最初のスニペットで動かせるように考えて実際にBitmapの取得以外をKotlinで実装するとこんなコードになるはず。 @JvmStatic をcompanion object内のwith関数につけるとJavaコードからも読めるようになるけど今回は省略。

class Katsushika private constructor(private val context: Context) {

    private var url: String? = null

    companion object {
        fun with(context: Context): Katsushika {
            return Katsushika(context)
        }
    }

    fun load(url: String): Katsushika {
        this.url = url
        return this
    }

    fun into(target: ImageView) {
        url ?: return

        // ここで画像ロード
    }

}

実際にpicassoやglideみたいに機能をバシバシつけたいときはこの部分を拡充していくことになるのだけど、キリがないので今回はここまでにします。

画像ロードをする

というわけでinto関数の中身を実装していきましょう。まずは愚直にCall.enqueueのCallbackを使って実装してみます。

fun into(target: ImageView) {

    OkHttpClient().newCall(request).enqueue(object: Callback {
        override fun onResponse(call: Call?, response: Response?) {
            val body = response?.body()
            body ?: return

            // 取得した画像バイト配列から縮小画像生成
            val byteArray = body.bytes()
            val options = byteArray.getBitmapOptions()
            val bitmap = byteArray.decodeByteArray(options, byteArray.size)

            // 対象のImageViewに画像を表示
            (context as Activity).runOnUiThread {
                target.setImageBitmap(bitmap)
            }
        }
    })

}

ちなみにここでのバイト配列からの縮小画像生成はByteArrayにKotlinの拡張関数を生やしたりして手抜きをしています

// Bitmapのサイズ情報を取得する
fun ByteArray.getBitmapOptions(): BitmapFactory.Options {
    val imageOptions = BitmapFactory.Options()
    imageOptions.inJustDecodeBounds = true
    BitmapFactory.decodeByteArray(this, 0, this.size, imageOptions)
    return imageOptions
}

// 取得したBitmapFactory.Optionsを元に2のべき乗のサンプルサイズで縮小する
fun ByteArray.getScaledBitmap(target: ImageView, options: BitmapFactory.Options): Bitmap {
    val widthScale = options.outWidth/target.width
    val bitmap : Bitmap
    if (widthScale > 2) {
        val imageOptions = BitmapFactory.Options()

        var i = 2
        while (i <= widthScale) {
            imageOptions.inSampleSize = i
            i *= 2
        }

        bitmap = BitmapFactory.decodeByteArray(this, 0, this.size, imageOptions)
    } else {
        bitmap = BitmapFactory.decodeByteArray(this, 0, this.size)
    }
    return bitmap
}

ここでinto関数を見ると、 Callbackの中で runOnUiThread ブロックを実行したりしている都合でネストが深めになってしまっています。しかもこんな感じでバシバシCallbackを使う処理を使っているとコールバック地獄になったりしてかなり怖い感じです。どうせならPromiseとか使って上から流れるように書いておきたい…

kotlinx.coroutinesを投入する

Coroutines - Kotlin Programming Language

kotlinx.coroutines はkotlin 1.1にてExperimentalとして実装された言語機能で、コルーチンを扱う低レベルAPIkotlin.coroutines.experimental を扱う高レベルAPIとなっています(開発者がコルーチンを扱うために触るAPIはこっち)。公式リファレンスのコルーチンに関するページを雑に読んでみると

  • コルーチンは軽量のスレッドのようなもの
  • 非同期処理の呼び出しでコルーチンを中断・再開することができる

みたいなことが書いています。kotlinx.coroutinesは外部ライブラリとして提供されているので、使用する際にはbuild.gradleのdependenciesに宣言してあげます。

dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:0.20"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:0.20"
}

詳しく日本語で読みたい場合は以下の記事とかがいいかも…

qiita.com

コルーチンの中断・再開というのがとても重要で、コルーチン内から呼ぶことができるsuspend関数と言うものを使うようになります

suspend fun foo(): Bar { // 何かしらの処理... }

kolinx.coroutinesではES7やC# 5.0以降で提供されるようなasync/awaitが一機能として提供されており、非同期処理を同期処理っぽく書くことが出来ます。

fun requestItems() {
    itemApi.getItems(object: Callback {
        override fun onSuccess(item: Item) {
            textView.text = item.name
        }
    })
}

という風に書いていたものがkotlinx.coroutinesのasync/awaitを使うことで

fun requestItems() {
    val job = launch(UI) {
        // 一旦ここで関数の実行がitemApi.getItems()が完了するまで止まる
        val item = async { itemApi.getItems() }.await()
        textView.text = item.name
    }
}

こんな風にかけるようになります。 launch(UI) のブロックはコルーチンビルダーといい、引数のCoroutineContextにUIを与えることでUIスレッドで動くコルーチンを作成することになります。async のブロックでは新しく別のスレッドで動くコルーチンを作成し、await()を呼ぶことでブロック内の処理が完了するまで停止することができ、ブロック内の返り値を処理の完了後に返すことが出来ます。asyncの引数としてこちらもCoroutineContextを渡したりでき、これによって実行するスレッドを制御したりすることが可能です。

すると最初に書いたinto関数もいい感じに書けそうだ、という感じがしてきます。とはいうもののまずはCall.enqueueをsuspend関数として呼べるようにCallに拡張関数を用意してあげる必要がありそう。

Call.enqueueをsuspendCancellableCoroutineで包んであげて、その結果に応じてcontinuationの発火をさせるメソッドを変更するようにしてあげます(理解不足で説明が謎なことになっている…)

suspend fun Call.do(): ResponseBody {
    return suspendCancellableCoroutine { continuation ->
        enqueue(object : Callback {
            override fun onResponse(call: Call, response: Response) {
                val responseBody = response.body()
                if (responseBody == null) {
                    // responseBodyが空なら失敗として扱う
                    continuation.resumeWithException(Exception("ResponseBody is null")) 
                } else {
                    // このresumeがawait()した結果の返り値となる
                    continuation.resume(responseBody) 
                }
            }

            override fun onFailure(call: Call, e: IOException) {
                if (continuation.isCancelled) return
                continuation.resumeWithException(e)
            }
        })
    }
}

最後に、これを扱うようにinto関数を書き換えて完了です。

fun into(target: ImageView) {
    url ?: return

    val job = launch(UI) {
        val byteArray = async(CommonPool) { // Responseを待ってからバイト配列を取得
            OkHttpClient().newCall(request).do().bytes()
        }.await()
        
        val options = async(CommonPool) { // 結果からBitmapFactory.Optionsを取得
            byteArray.getBitmapOptions()
        }.await()

        val bitmap = async(CommonPool) { // 上2つが終わったらBitmapを縮小
            byteArray.decodeByteArray(options, byteArray.size)
        }.await()

        target.setImageBitmap(bitmap.await()) // bitmapが出力されたらtargetに表示
    }
}

まとめ

こんな感じでkotlinx.coroutinesを利用することで動作スレッド・処理の完了待機を明示的に宣言でき、非同期処理を同期処理っぽい羅列で書くことが出来ます。やはりこういう機能やPromiseなりで非同期処理を完結にかけると嬉しいですね。

実際はpicasso/glideが強すぎるのでこういう完全な車輪の再発明はうーんうーんって感じになりがちですが、新しい機能なりで試すと結構勉強になったりでいいなぁと思いました。

追記

今回はコルーチン内のasyncを途中でキャンセルさせることをあまり考えていませんでしたが、実際にコルーチンをキャンセルするとその中のasyncもキャンセル出来ないケースがあるそうで、そういうケースにはasyncのCoroutineContextにContext(こっちはAndroidの方)を与えることで親のジョブの子のような扱いになり、ジョブをキャンセルしたときに子もキャンセルされるようになるらしいです。そして async(context + CommomPool) のように和で指定することでUIスレッドでない状態でかつジョブのキャンセルもされるようになるらしいです。

↓参考↓

medium.com

Bonfire Android #2に参加してきた

お詫び

色々あって投稿が本当に遅くなりました。申し訳ありません…

はじめに

yj-meetup.connpass.com

11/6にYahoo! JapanのLODGEで開催されたBonfire Android #2に参加してきました。テーマとしては「Kotlin × サービス」ということで、Yahoo! Japan内外問わず様々なプロダクトでのKotlin採用に関するトークがありました。Toggeterはこちらになります。

togetter.com

まとめ

スライドのまとめ等はconnpassのページから見ることができるので、トークを聞いていて個人的に思ったトピックを幾つか…

導入・JavaからKotlinへのコンバート

やはりサービスでKotlinを導入するとなると新規開発はともかく、既存で動いているアプリケーションのJavaコードをKotlinコードへと置き換えるというケースが多く、その点に関してのお話をたくさん伺うことが出来ました。

  • とりあえずAndroid Studioのconvert機能で1ファイルずつKotlin化する
    • コンバートした後にCompanion Object内のvalconst valにしたりNonNull/Nullableの考慮をしたり
  • 既存クラス置き換えは良い勉強になる。とりあえず「習うより慣れろ」でKotlinの学習を進めるのもあり
    • もちろんKotlin Koansもとっても有効
    • よく言われているように自由度が高いのでチームとしてのベストプラクティスを持つのが大事だと思いました
  • 成長のためのKotlin導入
    • 「使う技術・言語は自分で決める。いい道具で仕事をしよう」という言葉が印象的でした
    • 成長したら/勉強したらやるから… -> いつ成長するの?いつ勉強するの?というジレンマ

エンジニア外から見たKotlin採用

特に興味深かったのが、スクラムマスター、ビジネスサイドと言ったメインでコードを書くようなエンジニアではない視点からの言及がいくつかあったことでした

  • Kotlin導入にビジネスインパクトはあるのか
    • 一応品質向上や広報効果といったインパクトはある
      • Javaと比較してNull安全であること等による品質向上
      • 最新技術に挑戦しているという技術広報的なメリット
  • そもそもリプレースをやるべきであるか?という問に明確に答えが出せないなら踏みとどまるという策もある
    • 置換え中のサービスのグロースは?
    • 新機能開発は?
    • ビジはどう説得するの?
  • 業務の一部を使ってインパクトのないところで検証して新規モジュールからじわじわKotlinにというケース
    • 既存モジュールへの影響を廃していつでもやめられる状態を作り出す
  • 置き換えを頑張るくらいならユーザさんに新しい価値を届けることに時間を使いましょう
  • スクラムマスター的には…?
    • タイミングがあればチームのモチベーション・成長観念的にKotlin導入は全然アリ
    • Kotlinの時点でコストがかかる、けどポジティブな結果を呼べるので取り組みたい…!
      • 導入に対するネガティブチェック・ロードマップの作成で取り組みへの説得力をもたせたい
    • ここまで動けるスクラムマスター、素晴らしいし大変だしで尊敬する…

まとめ

KotlinがAndroid開発の1st languageになったことでみんな導入したいという思いがある中でこういうモデルケースをたくさん伺うことが出来たのはとても良かったなと思いました。ただ、その中でもAndroid FrameworkはJavaで書かれているという事実もあって、Kotlinを導入してもJavaからは逃げることが出来ないということや、プロタクトのインパクト的な観点から導入に際してどう付き合っていくのか、チームとしてどうKotlinを導入して実装を進めていくのかなど、課題と言うか考えるべき案件もまだまだあるなと…。でもそれを乗り越えて得られるものもたくさんあると思いました。

主催のYahoo! Japanさん、発表者の皆様、ありがとうございました :bow:

第50回 情報科学若手の会に参加してきた #wakate2017

通算3回目の参加だそうです。社会人になってからは初めての参加っぽいです。

というわけで、今年も伊東市は山喜旅館にて開催された情報科学若手の会に参加してきました。なんと50回目。アニバーサリーって感じです。おまけに伊東市はその日は秋祭りで市政70周年だったそうで、こちらもアニバーサリーって感じでした。

前回の参加ブログはこちらです。

e10dokup.hateblo.jp

雑なまとめ

  • 相も変わらず話題が豊富
  • 50周年記念なので記念セッションがあった
    • 国立国会図書館に眠る記録が掘り出されていた
    • 案外昔想像されていたことが今実現されてたりして心温まる感じ
    • 手書きの報告書の書き手が村井純先生だったり
  • 交流イベント
    • 今年は何をやらされるかと思ったらQRコード陣取りゲーム
      • QRコードが読めなくなるまでお互いに付箋を貼って読めなくなった時点で一番多い枚数を貼った人が勝ちとか
    • 第1問の回答が各位絞り出したエモさで強かった
      • 「相互投票」になると思っていなかったので幹事の方のサンプルと100%一致する答えをしてしまって選外になりました :bow:
    • 強いQRコードリーダーは強い
      • スマホのリーダーは全滅してもなお読めたりする
  • ナイトセッション
    • 相変わらずエモい話で盛り上がる
    • ピングー
    • 今年のLT用プロジェクタの配置が神がかっていて一杯の人が聴ける感じでとても良かった
    • 結局毎日3-4時就寝
    • 突発的スプラトゥーンバイト若手の会
      • オフラインよりオンラインプラベのほうが落ちないっぽい
      • 評価が155%まで上がりました :bow:
  • お祭り
    • 二日目にお祭りがあったのでみんな外に出てみた
    • ついに旅館から出ることの許されなかった若手の会の参加者が外へと足を踏み出す奇跡の瞬間
      • こう書くとシャバの空気を吸うみたいな話になる
  • ネットワーク
    • 今年は明らかにオーバーキルだった
    • 幹事/野生の幹事の方々ありがとうございます
    • 快適にも程がある

ところで来年は…

幹事を勤めさせていただくことになりました

噂によると「初参加の人が突然肩を叩かれて気付いたら幹事になっている」とのことでしたが3年目にして突然肩を叩かれました。 今年幹事を引退される方々に負けないように幹事業をやっていきたいと思います。とにかくやっていくぞという気持ちです。

ちなみに幹事の杯として50年物のワインをみんなで頂きました。ワイン苦手なのですがなんかドチャクソ美味しくてすごかったです。

来年の情報科学若手の会もよろしくお願いいたします :bow:

ところで何かあなた発表したの?

ちゃんとした学術・技術的な発表はしませんでした。ただ幹事の方からご希望を頂いたので新宿御苑をひたすら勧めるLTをしていました。新宿御苑はリビングであると同時にコワーキングスペースなのでみんな行きましょう。寝ましょう。コーディングもしましょう。

ちなみに新宿御苑というと映画「言の葉の庭」で雪野先生が東屋で金麦片手にチョコレートを食べているのが印象にある方も多いと思いますが新宿御苑は禁酒ですのでお気をつけください。一体何を気をつけるのか…

potatotips #41 に参加してきました

potatotips.connpass.com

Yahoo! JapanさんのLODGEで開催されたpotatotips #41にAndroid ブログまとめ枠として参加してきました。 (2日ほど遅れてしまっていますが…)

当日の様子はToggeterからどうぞ

togetter.com

Android Tips まとめ

Physics-based Animations (rkowase さん)

qiita.com

Google I/O 2017中のAndroid Animations Spring to Life で発表された Physics-based Animationsに関するLTでした。

  • 物理法則や関数に従ったAnimationが簡単に実現できるようになった
    • 自然な見た目になり、視覚的違和感が軽減できる
    • Interpolator等を使っても関数のチューニングが必要だったりしたのでありがたい…
  • 途中で移動先の座標が変わっても自然に軌跡を修正してくれる
    • ちゃんと連続関数的になるように座標移動を修正している感じがすごい
  • 係数調整も自由度が高くて良さそう
    • SplingAnimationならバネ係数や剛性、FingAnimationなら摩擦係数など、ちゃんと係数レベルから調整できるのは細かい人には嬉しそう

Icon Fonts in XML (anikaido さん)

SupportLibrary 26で追加されたFonts in XMLに関するLTでした。

  • Fonts in XMLでレイアウトXMLでFont指定できるようになった
      • 従来ならCalligraphyでやっていたことがSupportLibraryでできるようになった、という感じ
    • これを利用してMaterial Icon Fontを導入したい
  • res/font/material_icon.ttfに配置して
  • android:fontFamilyで指定するとandroid:textで対応するASCIIコードを指定することでアイコンフォントが表示されるように
    • ASCIIコードを直打ちするの大変そう…、strings.xmlとかで対応付けしたい…

AsyncLayoutInflater vs Litho (KeithYokoma さん)

speakerdeck.com

非同期でLayoutのInflateを行うAsyncLayoutInflatorと宣言型のUIフレームワークである facebook/Litho の非同期UI読み込みを比較したLTでした

  • まだLithoはベータだけど非同期UI読み込みではAsyncLayoutInflatorのほうが速い
    • 相対的な大きさのせいか、AsyncLayoutInflatorの時間のブレが大きく感じる…
  • 1万のTextViewが並んだLayout XMLを見た後にLithoのforループで宣言できるのを見ると場合によってはとても良さそう

Android Font Updates (uecchi さん)

speakerdeck.com

本日二件目のフォントに関するLT。内容がかぶっていないDownloadable FontsとEmojiCompatに関するお話がメインでした

  • Downloadble Fonts
    • Google Fontsであれば端末内に入れることなく使えるようになった
      • FontとかにこだわってもAPKサイズを下げることができるので良さそう
      • CJK対応はまだなので日本で使われるようになるのは…
      • M以下で使おうとするとNPE… (´・ω・`)
    • 同じフォントはアプリ間で共有してリソース削減を狙う仕様は細かい配慮が効いていていいなぁと思った
  • EmojiCompat
    • Downloadable Fonts/APK Bundleで最新Emojiを古い端末にも適用させる機能
      • 豆腐削減の1手
    • Emoji/EmojiAppCompatHogeViewがある
      • TextView/EditText/Buttonがあるみたい
  • Android OからEmojiが丸くなるらしいです

swagger-codegenpojo生成 (kgmyshin さん)

swagger.jsonからPOJOを生成するswagger-codegenに関するLTでした。

  • リクエストやレスポンスがネストが深いときや複雑な構造になっているときにミスの可能性を減らせそう
  • 新規や後発で開発が始まったときにこういうことができると幸せが強い
    • swagger.jsonを使ってきっちりサーバサイドの仕様を決めている前提はあるが… ()

      Instant Apps (bina1204 さん)

www.slideshare.net

正式に使えるようになったInstant Appsの紹介LTでした。

  • Instant Apps 正式リリース :tada:
    • インストールいらずになる
    • どこからでもアクセスできて
    • 5.0移行でも動くようになるはずの
    • 既存のアプリにちょちょいと手を加えて動く様になる機能
  • Module細分化してAndroidManifestにエントリーポイントを貼るとok
  • デバッグ仕様だとUpdate Error -27なのでInstant Apps版のアプリをアンインストールしてから入れよう
  • 4MB未満が推奨されてるけど10MBでも動くよ!今はとりあえず!
  • Nearbyと組み合わせてイベントとかでNearby範囲内に入ったときにイベント専用アプリをインストールさせる、とか楽しそう

自動テストが無ければDeviceFarmを使おう (としさん)

openSTF/AWS Device Farm/Firebase Test Lab といったAndroidクラウドテスト環境に関するLTでした。

  • いろんな端末で自動テストが走る
  • 最近は自動テストがなくても走る
    • OpenSTF/AWS Device FarmはMonkeyTest
    • Firebase Test LabはRoboTest
    • Firebaseのほうがある程度こちらで制御できる部分があったりして賢くできそう
  • AWS DeviceFarm/Firebase Test Lab
    • AWS、Firebaseも無料枠が出来た
  • まだ発展途上だけど、やれることレベル的にFirebase Test Labが一番強そう

感想

前回(#21だったはず)に出たときもブログまとめ枠でした。今回も濃い内容で充実した時間でした。毎回こういう勉強会に出ると「次こそは発表したい…!」ってなるのでモチベーションを維持して行きたいです。

会場・お寿司・お酒を提供してくださったヤフーさん、どうもありがとうございました!

次回参加するときがあればよろしくお願いいたします!

DataBinding + RxJavaでMVVMパターンな設計を考える

今更感がすごいが、DataBindingを使うことによってAndroidアプリケーションの実装でMVVMパターンな設計を考えやすくなったし、DroidKaigi 2017のアプリがMVVMで実装されていたりするので、自分なりに設計をまとめてみる。

全体図

他で実装されている記事を見るとDDDなりと混ぜ合わせた感じの設計がちらほら見えて、一番シンプル(かつ集合知的な知見が溜まっている)と感じたDroidKaigi/conference-app-2017のアーキテクチャを丸パクリする形になった。

f:id:e10dokup:20170507160545g:plain

github.com

何をしているかざっと書くと

  • View
    • Activity/Fragment/Adapter ItemといったViewは1対1で対応するViewModelを持つ
    • 各Layout XMLには対応するViewModelをDataBindingでbindする
  • ViewModel
    • Viewの要素をクリックしたときの処理の定義やAPI/DBとのModelのやり取り、Viewへの反映を行う
    • ViewModelが取り扱うRepositoryはDagger2のDIを用いてインジェクトして利用する
  • Repository
    • ModelのCRUD操作を提供する
    • 各Modelのクラスに対応するRepositoryが存在する
    • RepositoryはDataSourceを持ち、DataSourceを利用してDBやAPIからデータを取得するが、ViewModel以下には隠蔽する。
  • Local/RemoteDataSource
    • 実際にModelのCRUD操作を行う
  • DataSourceからViewModelまでの処理はRxJavaでストリーミングに扱う

という感じ。

View

Viewは対応するViewModelをInjectして、Layout XMLにbindする。

public class MainActivity extends BaseActivity {

    @Inject
    MainActivityViewModel viewModel;

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        getComponent().inject(this);
        bindViewModel(viewModel);

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        binding.setViewModel(viewModel);
    }

    // 以下省略
}

ちなみにBaseActivityの中は次のような感じ。ViewModelにもライフサイクル系メソッドを用意して、ActivityやFragmentのライフサイクルと同期して呼び出すようにしている。

public abstract class BaseActivity extends AppCompatActivity {

    private ActivityComponent component;
    private ActivityViewModel viewModel;

    @NonNull
    public ActivityComponent getComponent() {
        if (component == null) {
            MyApplication application = (MyApplication) getApplication();
            component = application.getComponent().plus(new ActivityModule(this));
        }
        return component;
    }

    protected void bindViewModel(ActivityViewModel viewModel) {
        this.viewModel = viewModel;
    }

    @Override
    protected void onStart() {
        super.onStart();
        checkViewModel();
        viewModel.onStart(this);
    }
    
    // 以下省略
}

ViewModel

ViewModelの実装は次のようにする。画面遷移は、Navigatorという画面遷移を取り扱うクラスを用意して、それをViewModelにinjectして行うようにしてみた。今回は直面していないがContextが必要な処理があるときはEventBusを使ってActivityにイベントとして流したほうがいい気がする…。

public class MainActivityViewModel extends ActivityViewModel {

    private final Navigator navigator;
    private final TaskRepository taskRepository;

    private ObservableList<TaskViewModel> taskViewModels;


    @Inject
    public MainActivityViewModel(Navigator navigator, TaskRepository taskRepository) {
        this.navigator = navigator;
        this.taskRepository = taskRepository;

        this.taskViewModels = new ObservableArrayList<>();
    }

    @Override
    public void onStart() {

    }

    @Override
    public void onResume() {
        taskRepository.findAll()
                .map(tasks -> Stream.of(tasks)
                        .sorted((o1, o2) -> (int)(o1.deadlineEpoch - o2.deadlineEpoch))
                        .toList())
                .map(tasks -> convertToViewModel(tasks))
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(taskViewModels1 -> {
                    this.taskViewModels.clear();
                    this.taskViewModels.addAll(taskViewModels1);
                });
    }

    // 以下省略
}

Navigatorの実装はこんな感じ。

@ActivityScope
public class Navigator {

    private final Activity activity;

    @Inject
    public Navigator(AppCompatActivity activity) {
        this.activity = activity;
    }

    public void navigateToCreateTask() {
        activity.startActivity(CreateTaskActivity.createIntent(activity));
    }

    public void navigateToTaskDetail(int taskId) {
        activity.startActivity(TaskDetailActivity.createIntent(activity, taskId));
    }

    public void closeActivity() {
        activity.finish();
    }
}

Repository

RepositoryにはLocal/RemoteDataSourceを持たせて、それらDataSourceにてCRUD操作を行う。例はローカルDBしかおいてないのでほぼ効果はないが、「ローカルDBにデータが有るときはローカルDBから、そうでないときはAPIから」という風に処理を分ける際にはここで分岐させる。

@Singleton
public class TaskRepository {

    private final TaskLocalDataSource taskLocalDataSource;

    @Inject
    public TaskRepository(TaskLocalDataSource taskLocalDataSource) {
        this.taskLocalDataSource = taskLocalDataSource;
    }

    public Single<List<Task>> findAll() {
        return taskLocalDataSource.findAll();
    }

    // 以下、省略
}

DataSource

DataSourceからの返り値はRxJavaのSingleで包んでストリーミングに流すようにする。例はLocalDataSourceだけだが、Retrofit等を用いてRemoteDataSourceを作るときはRetrofitのClientをinjectしてClientのアクセス結果をSingleで包む形になるはす。

public class TaskLocalDataSource {

    private final OrmaDatabase ormaDatabase;

    @Inject
    public TaskLocalDataSource(OrmaDatabase ormaDatabase) {
        this.ormaDatabase = ormaDatabase;
    }

    public Single<List<Task>> findAll() {
        return ormaDatabase.relationOfTask()
                .selector()
                .executeAsObservable()
                .toList()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }
   
   // 以下省略
}

終わりに

とりあえずこれを踏襲してサンプルでToDoアプリを作ってみた。

github.com

先人の知識に頼りっぱなしで実装したが、いざ組んで見てわかること、記事にして思い違いだったと気づくことがたくさんあるのでとても重要だと思った(感想)