visible true

技術的なメモを書く

Clean Architectureを理解するための補助的なコンポーネント図のようなもの

Clean Architectureを雰囲気でしか理解していなかったんだけど、なんでだろうな〜って考えるとあの図とか説明文がややこしいからだな〜と思った。 抽象的なやつはええねん、具体をくれ具体を〜、と思ったので、Android-CleanArchitectureのサンプルコードをコンポーネント図のようなものにおこした。

Android-CleanArchitecture Sample Code

実際にサンプルのソースを眺めると、レイヤの接続のための実装やAndroid固有(NavigatorなどIntentを処理するようなやつ)の実装などが混ざっていて、概念図とソースだけでは結構分かりづらい。しかもFragmentでActivityをキャストして使ってたり、UserCaseのコールバックをPresenterのインナークラスのObserverでやってたり、DataSourceは呼び出し毎に実装をnewしてたりわりとトリッキーだったり雑だったりするのでノイズが多い。

という事で概念が伝えている要素だけを抽出して関係だけを書いたコンポーネント図のようなもの*1を描いた。

f:id:sys1yagi:20170624213749p:plain

登場人物が少ないのでちょっとレイヤー化っていうイメージはつかみにくいなぁとは思うがこれはこれで理解の助けにはなるんじゃないかなと。

もうちょっと機能を足してみる

層感がないんでちょっと適当に機能を足してみた。新たな要素としてViewModelが加わっている。これは単純にViewのデータ群を管理するための要素として書いている。

f:id:sys1yagi:20170624213755p:plain

こういう図がいいのは実装を考えずにこうして機能を追加できたりする点だな〜とか思った。UML様〜。

それで

こういうのは各レイヤの責務が相互に漏れ出さないってのが重要なので、何かを足す時に気をつけないといけない。 たいていの場合Data層は固まるのが早い。初期のAPIセットを実装したら一旦終わるし、追加変更は大体同じようなことを繰り返すだけだからだ。 で、次にPresentation層。固まるっていうか表示要素とデータ、発生するイベントが決まれば大体OKだからどっちかっていうとレイアウトXMLの実装のほうが大変なくらいだ。 一番むずかしいのがDomain層で、ここが一番変化すると思う。特定のPresentation層に特化しすぎてたり、汎用的にしたけど結局共有できないから分離したり、試行錯誤が発生する。

まぁとりあえず概念の理解の助けになれば〜〜。個人的にはVIPERが好きだ。

*1:これって実際は何図って言うんだろう?

もうAndroidの非同期処理はasync/awaitでいいんじゃないかなぁと思った

Rx Ja Night Vol.2 - connpassで「 Androidの非同期処理をKotlinコルーチンで行う」という話をしてきました。

スライドで使っているコードは次のリポジトリに置いています。

github.com

今回取り扱った非同期処理の範囲

スライドやリポジトリのREADME.mdに大体書いているのですがコチラにも載せときます。 詳細な説明はスライドやリポジトリを参照してください。 次の非同期処理をコルーチンで実現します。

単発の実行
直列の実行
並列の実行
+
エラーハンドリング
キャンセル

環境

すべてKotlinが提供する標準の機能を用います。

implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:1.1.2-4"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:0.16"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:0.16"

単発の実行

コルーチン(async/await)ではこういう風に書けます。

val job = launch(UI) {
    try {
        val shop = async(CommonPool) { shopApi.getShop(10) }.await()
        // success
    } catch (e: Exception) {
        // error
    }
}

launch(UI)async(CommonPool)はコルーチンのビルダー関数と呼び、続いて渡すブロックをコルーチンにしてくれます。 ここではlaunch(UI)はコルーチン内をUIスレッドで実行する、async(CommonPool)はコルーチン内をスレッドプールで実行する、という理解でいいと思います。 毎度書くと冗長なので、次のようなパッケージレベル関数を用意して簡潔に書けるようにしたりできます。

fun <T> async(context: CoroutineContext = CommonPool, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> T)
        = kotlinx.coroutines.experimental.async(context, start, block)

fun ui(start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit)
        = launch(UI, start, block)

スッキリ

val job = ui {
    try {
        val shop = async { shopApi.getShop(10) }.await()
        // success
    } catch (e: Exception) {
        // error
    }
}

エラーについてもtry-catch式なので、様々な例外をよしなに扱えます。 uiの中でawait()して待ち合わせていて大丈夫かって気になりますが、 Kotlinコンパイラがよしなにコルーチン内のコードを状態毎に分解していて、 await()の呼び出しの所で処理を中断しているので大丈夫です。 await()が完了すると結果がdispatchされ、Handler.post()で続きを実行するといった具合です。

直列の実行

await()を後続のasyncの前か、その中で呼び出せばOK。

val job = ui {
    try {
        val userJob = async { userApi.me() }
        val subscriptionShopsJob = async { subscriptionShopApi.getSubscriptionShops(userJob.await().id) }
        val subscriptionShops = subscriptionShopsJob.await()
        // success
    } catch (e: Exception) {
        // error
    }
}

並列の実行

並列で実行したいasyncを呼び出したあとにawaitすればよい。

val job = ui {
    try {
        val userJob = async { userApi.me() } // start immediately
        val shopJob = async { shopApi.getShop(10L) } // start immediately
        val user = userJob.await()
        val shop = shopJob.await()
        // success
    } catch (e: Exception) {
        // error
    }
}

キャンセル

launch(UI)から返るJobcancel()を呼び出せばよい。

job.cancel()

するとコルーチンの中でCancellationExceptionが飛ぶ。

job = ui {
    try {
        val shop = async { shopApi.getShop(10) }.await()
        // success
    } catch(e: CancellationException) {
        // cancel
    } catch (e: Exception) {
        // error
    }
}

感想

特にデメリットもなさそうだし圧倒的に簡潔だし、もう非同期処理はasync/awaitでいいやって思った。 なんてったって非同期処理のための実装だからね。 このほかproducerやactorなどを使うと更にUIイベントのストリームとかイベントバスみたいなこともできそうだけど、 この辺はそのために用意されているかでいうと微妙な気もするのでどちらでもいい気もしている。 一応experimentalなんだけど、裏側の実装が変わる事はあっても利用コード側はそこまで影響受けないだろうしいいんじゃないかなとか。 プロダクトでも入れ始めているしそうしようみたいな気持ち。

参考

これ読んだらもう使ってよしだと思う。

KotlinでViewDataBindingをシュッとinflateするやつ

RecyclerViewなどでViewDataBindingを使う時に次のように書くのめんどくさくて。

class ViewHolder(val binding:ListItemCommentBinding)
: RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, type: Int) = 
  ViewHolder(ListItemCommentBinding
    .inflate(LayoutInflater.from(parent.context), parent, false))

こういう感じにViewGroupに関数生やすと、

ViewExtensions.kt

inline fun <reified T : ViewDataBinding> 
  ViewGroup.inflateBinding(): T {
    return T::class.java
            .getDeclaredMethod(
              "inflate",
              LayoutInflater::class.java, 
              ViewGroup::class.java, 
              Boolean::class.javaPrimitiveType
            )
            .invoke(null, LayoutInflater.from(context), this, false) as T
}

良さそう。

override fun onCreateViewHolder(parent: ViewGroup, type: Int) =
  ViewHolder(parent.inflateBinding()) 

ViewHolderが複数種類のViewDataBindingを取り扱う場合は型引数が要る。

override fun onCreateViewHolder(parent: ViewGroup, type: Int) =
  ViewHolder(parent.inflateBinding<ListItemCommentBinding>()) 

追記 2017/05/23

proguardで死ぬので、使うときは次の設定が必要になります。ひょえ〜

-keep class * extends android.databinding.ViewDataBinding {
    public static ** inflate(android.view.LayoutInflater, android.view.ViewGroup, boolean);
}

DataBindingが原因のビルドエラー時にエラーを抽出するスクリプト

AndroidでDataBinding周りミスるとめっちゃエラー出て辛いですね。辛いのでDataBindingのエラーを抽出するスクリプトを書きました。スクリプトはかなり雑なので適宜いい感じにしてください。

extract_data_binding_error.rb

#! /bin/sh
exec ruby -S -x "$0" "$@"
#! ruby

state = 0
while str = STDIN.gets
  break if str.chomp == "exit"
  case state
  when 0
    state = 1 if str.match(/.*Found data binding errors.*/)
  when 1
    state = 2 if str.match(/.*e: .*/)
    next if state == 2
    print str
  end
end

gradleのコマンドに2>&1を足してextract_data_binding_error.rbに流せばok

$ ./gradlew assembleDebug 2>&1 | ./extract_data_binding_error.rb
Identifiers must have user defined types from the XML file. user is missing it
file:///hoge/foo/git/android-app/app/src/main/res/layout/list_item_comment.xml Line:60


    at android.databinding.tool.processing.Scope.assertNoError(Scope.java:110)
    at android.databinding.annotationprocessor.ProcessDataBinding.process(ProcessDataBinding.java:89)
    at com.sun.tools.javac.processing.JavacProcessingEnvironment.callProcessor(JavacProcessingEnvironment.java:794)
    at com.sun.tools.javac.processing.JavacProcessingEnvironment.discoverAndRunProcs(JavacProcessingEnvironment.java:705)
    at com.sun.tools.javac.processing.JavacProcessingEnvironment.access$1800(JavacProcessingEnvironment.java:91)
    at com.sun.tools.javac.processing.JavacProcessingEnvironment$Round.run(JavacProcessingEnvironment.java:1035)
    at com.sun.tools.javac.processing.JavacProcessingEnvironment.doProcessing(JavacProcessingEnvironment.java:1176)
    at com.sun.tools.javac.main.JavaCompiler.processAnnotations(JavaCompiler.java:1170)
    at com.sun.tools.javac.main.JavaCompiler.processAnnotations(JavaCompiler.java:1068)
    at org.jetbrains.kotlin.kapt3.AnnotationProcessingKt.doAnnotationProcessing(annotationProcessing.kt:73)
    ... 50 more

こんな感じ。やったね

mastodon4j v0.0.6 をリリースしました

mastodon4jをリリースしました - visible true では0.0.3でしたが、0.0.6まで出ました。

差分はReleases · sys1yagi/mastodon4j · GitHubに書いてますがここでも軽く書きます。

v0.0.4

Release v0.0.4 · sys1yagi/mastodon4j · GitHub

mastodon4j

  • 各メソッドにContractを追加。PublicとAuthRequiredに分かれている。
    • 認証が必要なものと不要なものを明示するのが目的だったが、interfaceは@JvmOverloadsが使えないため0.0.7でやめる予定
  • Breaking メソッドの各関数にMastodon4jRequestExceptionのチェック例外を付与
  • Breaking booleanのgetter名をisXXXに変更
  • いくつかの関数に@JvmOverloadsを付与

mastodon4j-rx

なし

v0.0.5

Release v0.0.5 · sys1yagi/mastodon4j · GitHub

mastodon4j

  • Timelinesのpublicとtagにlocalパラメータを追加。これをつけるか付けないかでローカルタイムライン、連合タイムラインを切り替えるらしい。それに伴い次の変更を入れた
    • Deprecated Timelines#getPublic()
    • Add Timelines#getLocalPublic()
    • Add Timelines#getFederatedPublic()
    • Deprecated Timelines#getTag()
    • Add Timelines#getLocalTag()
    • Add Timelines#getFederatedTag()

mastodon4j-rx

なし

v0.0.6

Release v0.0.6 · sys1yagi/mastodon4j · GitHub

mastodon4j

  • Timelinesのlocalパラメータは、falseの時はパラメータを付与してはいけないという事で修正
  • Accounts.getStatusesにonly_mediaフラグを追加。メディアを持つ投稿を抽出する時に使うっぽい
  • 次のDeprecated関数を削除
    • Delete Timelines#getTag()
    • Delete Timelines#getPublic()
  • Link Headerをサポート。そのためにList<T>の代わりにPageable<T>Linkを導入

mastodon4j-rx

mastodon4jの変更に追従

その後

  • 0.0.7 Milestone · GitHub
    • raw jsonを取り出す仕組みを入れる
    • Mastodon4jRequestExceptionはResponseを内包しているけど、Mastodon4jRequestException自身にcode()とかを持たせる
    • Contractを捨ててアクセストークンが不要なメソッドはPublicクラスに集約する
    • できればStreaming APIをサポートする

所感

PR投げてくれる人がいたり(fix only_media param by takke · Pull Request #39 · sys1yagi/mastodon4j · GitHub)、

設計周りでアドバイスを頂けたりしてありがたいです。

mastodon4jをリリースしました

mastodon4jをリリースしました。Kotlinで書かれていて、Javaからでも使えるように今後チューニングしていきます。現在の最新は0.0.3です。 最初はDroiDonの副産物としてmastodon4jを実装していて、まぁだれか出すだろうと思っていたけど1週間経っても出てこないので自分で出すことにしました。 公式ドキュメントのAvailable librariesに載せてもらえてやっぴー。

github.com

mastodon4j

github.com

0.0.1

  • mastodon4jでmastodonAPI documentにかかれているデータ、メソッドをすべて実装
  • mastodon4j-rxで一部のメソッドを実装

0.0.2

Release v0.0.2 · sys1yagi/mastodon4j · GitHub

  • ユーザ名/パスワードで認証する Apps#postUserNameAndPassword() を追加
  • Statues内でRangeを利用するようにした。(0.0.1でもRangeはあったが、Statuesではmax_id, since_id, limitを個別のパラメータにしていた)
  • mastodon4j-rxですべてのメソッドを実装

0.0.3

Release v0.0.3 · sys1yagi/mastodon4j · GitHub

  • Mastodon4jRequestExceptionでResponseオブジェクトを持つようにした(401などのハンドリングのため)。
  • Scopeのコンストラクタが可変長引数で、空のまま実行するとエラーになるのでデフォルト引数を追加した

その後

所感

mastodon4jを実装して改めて思ったのはAndroidアプリケーションの開発って大変だなーということ。DroiDonの進捗はせいぜい5%くらいでまだまだ先は長い。

github.com

React NativeでFirebase Storageに画像を上げるときにputStringが上手くいかないのでbase-64を入れてatob関数を補完する

React Nativeを、ペーパープロトタイピングprottなどを使ったモックアッププロトタイピングの次のフェーズとして動くプロトタイピングツールに使えないかなぁと思ってちょこちょこ触ってます。

うまくいくと両ユーザ向けに同時に動くプロトタイプを提供できてフィードバックが捗るのとAndroidiOSの両方のチームで同時に一つの仕様をいじれるので仮説や価値の理解や共有らなんやら色々捗るんじゃないかなぁとか思ってます。

バックエンド側もFirebaseを使うと結構カジュアルに色々やれそうだな〜とか思っていて色々試し始めたんですがFirebase Storageにデバイス上の画像をアップロードする処理ではまりました。

環境

  • React Native: 0.42.3
  • Firebase: 3.7.3

問題

react-native-image-pickerなんかを使って写真を撮ったりデバイス上の写真を選択すると、ファイル名やContentTypeやbase64に変換された実データを取得できます。次のコードはFirebase StorageにputString関数で画像をアップロードする例です。

firebase.initializeApp(config);
const storage = firebase.storage().ref();

const ref = storage.child(response.fileName);
const metadata = {contentType: response.type};

ref.putString(response.data, 'base64', metadata).then((snapshot) => {
  done();
});

putString関数の第一引数のresponse.dataには画像をbase64に変換した文字列が詰まっており、第二引数はデータフォーマットを示すためにbase64を渡しています。これをReact Nativeで実行すると次のようにInvalid character foundとなります。

f:id:sys1yagi:20170326215741p:plain:w300

どーもよくわからないのでBlobやUint8Arrayを使う方法を試みるも、そもそもReact NativeにはBlobはないらしいという事がわかり、react-native-fetch-blobというBlob周りのポリフィルを提供しているライブラリを導入して試してみるもうまくいかず途方にくれてました。

原因

万策尽きたのでしぶしぶFirebase Storageのクライアントコードを読むことにしました。エラー画面にstacktraceが吐かれてるのでそんなに追いかけるのは難しくなかったです。

f:id:sys1yagi:20170326220926p:plain

こんな感じでtry-catchがあって、atob関数しか呼び出してないのでこれがちゃんと動いてないんだな〜という事がわかりました。

対応

Blobを使ったりするところであれこれ試す中で、

javascript - How to convert base64 into Blob in React Native? - Stack Overflow

とかを試していたのでピンときて、

npm install --save base-64

して(base64)、

const atob = require('base-64').decode;
window.atob = atob;

コンポーネントの冒頭に書いたらうごいた。わいわい。

f:id:sys1yagi:20170326221919p:plain

雑感

React Nativeのポリフィル集ありそうだけどどーなんだろ。 React.parts – A catalog of React componentsとかを眺めていると結構色々あって面白い。一方で改廃も激しいので大変そう。