visible true

技術的なメモを書く

AndroidでJava8環境 2016

RxAndroidとRetrolambdaで大体Java8をAndroidに持ち込む - visible trueから1年以上経過して界隈も色々更新されていってます。ということでイマドキのJava8環境構築をメモします。

バックポートライブラリとJava8の機能

バックポートライブラリとそのライブラリがカバーするJava8の機能を列挙します

実現不可の機能

次の機能はバックポートライブラリ等では実現不可能です*1

  • interfaceのデフォルト実装
  • interfaceのstaticメソッド

build.gradle

次の設定はRetrolambdaを使っています。そのまんまコピペでは使えないと思うので適切な場所にコピーしてください。

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
      classpath 'me.tatarka:gradle-retrolambda:3.2.5'
    }
}


apply plugin: 'me.tatarka.retrolambda'

dependencies {
  compile 'com.annimon:stream:1.0.9'
  compile 'com.jakewharton.threetenabp:threetenabp:1.0.3'
}

まとめ

結構イケるねJava8。

*1:一応minSdkVersionを24にすればいけます

Kotlin Tips: Reified Type Parametersで型チェックを汎用化する

問題

Kotlin Tips : Intentに必要な値が入っていない場合例外をスローしたい - visible trueではgetLongOrThrow()をIntentに追加しlazyを使う事で可読性を高めた。

Intent.getIntOrThrow(key: String)のおさらい

fun Intent.getIntOrThrow(key: String): Int =
  this.extras.get(key).let {
    if (it !is Int) {
      throw IllegalArgumentException("Extras don't have Int value specified by key $key")
    }
    return it
  }

さて、ではStringならどうか。当然次のようなコードを追加すれば実現できる。

fun Intent.getStringOrThrow(key: String): String =
  this.extras.get(key).let {
    if (it !is String) {
      throw IllegalArgumentException("Extras don't have String value specified by key $key")
    }
    return it
  }

getIntOrThrow(key: String)とほとんど同じ実装で戻り値と型チェック部分だけが異なる。今回は以下の点にフォーカスする。

  • IntentのgetExtraの型の種類は28個ある。28回実装するのはつらい
  • getStringOrThrowなど機能的な命名を出来るだけ意図を表す命名にしたい

改善策

Reified Type Parametersを使う。Reified Type Parametersは具象化された型パラメータを利用できる機能。これによりロジック中の型チェックをTを使って行える(つまりあたかも型消去が起こらなかったかのように扱える。T::class.javaClassとかもできるよ)。

inline fun <reified T> Intent.getRequired(key: String): T {
  extras?.get(key).let {
    if (it !is T) {
        throw IllegalArgumentException("Extras don't have a value specified by key $key")
    }
    return it
  }
}

さらに利用時には型推論によって以下のように書ける。

val value:String = intent.getRequired(key)

ただしlazyを使う時は型の明示が必要となる。といっても型情報は1度しか出てこないので冗長になることはない。

val value by lazy<String> { intent.getRequired(key) }
// or
val value by lazy { intent.getRequired<String>(key) }

やったね

  • Reified Type Parametersによって複数種類の型の取り出しを汎用化できた
  • 型推論のために型はどこかに必ず宣言されるのでメソッド名に型名を含めなくてよくなった
  • Intent.getRequired(key:String)という名前によって意図が明快になった

ついでに

Intent.getOptional()も実装しておく。

inline fun <reified T> Intent.getOptional(name: String, default: T): T {
  extras?.get(name).let {
    if (it == null) {
        return default
    } else {
        return it as T
    }
  }
}

さらなる議論

Reified Type Parametersはinlineだからこそ実現できる機能である。そのためインライン展開のデメリットについては考慮したほうがよさそうだ。といってもバイトコードが膨らむくらいしか思いつかない...。

Kotlin Tips: 可読性のために拡張関数で別名をつける

問題

次のコードはRxJavaのCompletableを利用している例である。

repository.deleteEntry(entry)
    .subscribe(
      { e ->
        view.hideProgress()
        view.showError(e)
      },
      {
        view.hideProgress()
      }
    )

このコードには次の問題がある。

  • repository.deleteEntry()がCompletableを返す事をコードからは読み取れない。
  • subscribe()の第一引数がonError:(Throwable)->UnitだがObservableはonNext:(T)->Unitを第一引数にとるので直感に反する

つまりロジック上問題ないがコードを読んだ時にわかりづらいという問題が発生する。

改善策

2つの問題を解決するためにCompletableにsubscribeCompletable()という名前の関数を追加する。

RxExtensions.kt

fun Completable.subscribeCompletable(onError: (Throwable) -> Unit, onComplete: () -> Unit) = subscribe(onError, onComplete)

Completable.subscribe()に別名をつけることで可読性が向上する。

repository.deleteEntry(entry)
    .subscribeCompletable(
      { e ->
        view.hideProgress()
        view.showError(e)
      },
      {
        view.hideProgress()
      }
    )

さらなる議論

別名をつけると言っても実際は拡張関数を実現するためにRxExtensionsKtクラスが作成され内部にはメソッドやネストしたクラスが作られる。可読性を取るかメソッド数、クラス数を取るかについては別途議論する必要がある。

Kotlin Tips : Intentに必要な値が入っていない場合例外をスローしたい

実際にKotlinを使っているなかで思いついたことなどをこまめにメモると良い気がしたのでなるべく書いていく

Before : デフォルト値を使ってチェックする

以下のようにActivityでintなどのプリミティブな値を受け取るとする。値はrequiredでセットされなかった場合は例外をスローすることで開発者に対して間違った利用方法であることを知らせたい。

class DetailActivity : AppCompatActivity() {
  companion object {
    fun createIntent(context: Context, id: Int): Intent =
           Intent(context, DetailActivity::class.java).apply {
             putExtra("id", id)
           }
  }

  var id: Int = -1

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    id = intent.getIntExtra("id", -1)
    if (id == -1) {
      throw IllegalArgumentException("Should set id. $id")
    }
    // do something
  }
}

このコードには以下の問題点がある

  • プロパティにvarを用い、デフォルト値を設定している
    • プリミティブな型はlateinitが使えない
  • Intent.getIntExtra(key, defaultValue)で必ずデフォルト値が必要なため仕方なく-1を使っている
  • -1の意図が明快ではない
    • プロパティの宣言、Intent.getIntExtra(key, defaultValue)-1を用いているがここからextraにidを設定せずに起動した場合に開発者に間違った利用方法であることを知らせるために例外をスローしたいという意図を読み取るのは難しい。仮にINVALID_ID=1という定数を作ったとしてもvalidationしたいのかどうかを判別するにはIllegalArgumentExceptionのスローの意図を考える必要がある。

After : Intent.getIntOrThrow()を生やしlazyを用いる

まずIntent.getIntExtra(key, defaultValue)を改善する。拡張関数を用いてkeyが存在しない場合に例外をスローする関数を追加する。

extensions.kt

fun Intent.getIntOrThrow(key: String): Int =
      this.extras.get(key).let {
          if (it !is Int) {
            throw IllegalArgumentException("Extras don't have Int value specified by key $key")
          }
          return it
      }

次にプロパティにlazyを用いてIntent.getIntOrThrow()を呼び出す。

class DetailActivity : AppCompatActivity() {
  companion object {
    fun createIntent(context: Context, id: Int): Intent =
           Intent(context, DetailActivity::class.java).apply {
             putExtra("id", id)
           }
  }

  val id: Int by lazy { intent.getIntOrThrow("id") }

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setup(id)
  }
}

これにより以下のことができた。

  • 不必要なデフォルト値を除去できた
  • Intent.getIntOrThrow()によりrequiredな値である事を示せた
  • lazyによりプロパティをvarからvalにできた

さらなる議論

val id: Int by lazy { intent.getIntOrThrow("id") }はbeforeに比べて明らかに意図が明快になったが十分かどうかについてはまだ議論ができそうだ。たとえばより明快にIntent.getRequiredInt(key:String)という名前にしても良いかもしれない。対となるIntent.getOptionalInt(key:String)といった名前の関数を用意するとより意味のあるコードになるかもしれない。

val id: Int by lazy { intent.getRequiredInt("id") }
val name: String? by lazy { intent.getOptionalString("name") }

Kotlin学習とライブラリ作成

第2回Kotlin勉強会 @ Sansan - connpassで「Kotlin学習とライブラリ作成」というタイトルで話してきました。

kmockito

jitpackで配信してるので使えます。

GitHub - sys1yagi/kmockito: Mockito for Kotlin.

allprojects {
  repositories {
    ...
    maven { url "https://jitpack.io" }
  }
}
compile 'com.github.sys1yagi:kmockito:0.1.0'

書き味

大体こんな感じです。assertionのisのバッククォート問題の解決には knit を使ってください。

before

var item = mock(Item::class.java)
`when`(item.length()).thenReturn(10)

assertThat(item.length(), `is`(10))
verify(item).length()

after

var item: Item = mock()
item.length().invoked.thenReturn(10)

assertThat(item.length(), `is`(10))
item.verify().length()

まとめ

大体スライドに書いてありますが、ライブラリ作成をすると実用的なKotlinの検討と学習がいい感じにできるんじゃないかと思います。 100行で大体実用的なものが書けるのであわよくばいい感じの何かをリリースしていい感じになれます。 しかし世界では同じ事考える人がたくさんいるのでデファクトを勝ちとる競争は激しいかもしれません。 とりあえずawesome-kotlinに載れば一人前と言ってよさそうなので頑張りましょう(ぼくはまだ載ってません。載りたい)。 PR出したら大体シュッとマージしてくれるっぽいので自信があるライブラリができたらPRを送るとよいと思います。

Fundamentals Of The Data BindingをGitBookで公開しました。

丁度自分でもData Binding Libraryの細かい所を忘れていて自分の原稿読んでわーいって感じだったのでついでにGitBookに公開していつでも読めるようにしました。2015年夏に書いたものなので一部古い内容が含まれているかもしれません。適宜修正します。(セットアップ部分については最新になってます)

Fundamentals Of The Data Binding

PRとかも多分できるので適当にもらえるとうれしいです。

rxbinding-recyclerview-v7とPublishSubjectを使ってRecyclerViewでスクロールが一番下まで行ったらロードする実装

すでにある気がするけど見当たらなかったのでメモ。

ソース

こちらにあります。

GitHub - sys1yagi/rxrecyclerview-load-more

準備

rxbinding-recyclerview-v7をdependenciesに追加する。

dependencies {
  compile 'com.jakewharton.rxbinding:rxbinding-recyclerview-v7:0.4.0'
}

RxRecyclerViewScrollSubjectを作る

RxRecyclerViewを使ってRecyclerViewScrollEventを受け取り、LinearLayoutManagerを使って最後尾かどうかの判定をします。メソッドは3つだけです。

public class RxRecyclerViewScrollSubject {
  Subject<RecyclerViewScrollEvent, RecyclerViewScrollEvent> subject = PublishSubject.create();
  Subscription subscription = Subscriptions.empty();

  public Observable<RecyclerViewScrollEvent> observable() {
    return subject;
  }

  public void start(RecyclerView recyclerView, LinearLayoutManager linearLayoutManager) {
    subscription.unsubscribe();
    subscription = RxRecyclerView.scrollEvents(recyclerView).subscribe(event -> {
      int totalItemCount = linearLayoutManager.getItemCount();
      int lastVisibleItemPosition = linearLayoutManager.findLastVisibleItemPosition();
      if (totalItemCount - 1 <= lastVisibleItemPosition) {
        subject.onNext(event);
      }
    });
  }

  public void stop() {
    subscription.unsubscribe();
  }
}

GridLayoutManagerはLinearLayoutManagerを継承しているのでそのまま使えます。StaggeredGridLayoutManagerは継承関係がないのでfindLastVisibleItemPosition()が存在せずそのまま使えません。似たようなメソッドがあるので別途StaggeredGridLayoutManagerを受け取る口を作っとくといいかもしれないです。

使う

ライフサイクルに合わせてRxRecyclerViewScrollSubjectをstart()したりstop()します。

public class MainActivity extends AppCompatActivity {
  RxRecyclerViewScrollSubject rxRecyclerViewScrollSubject = new RxRecyclerViewScrollSubject();
  RecyclerView recyclerView;
  LinearLayoutManager linearLayoutManager;
  Subscription subscription = Subscriptions.empty();

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    //省略
  }

  @Override
  protected void onResume() {
    super.onResume();
    rxRecyclerViewScrollSubject
                .start(recyclerView, linearLayoutManager);
  }

  @Override
  protected void onPause() {
    super.onPause();
    rxRecyclerViewScrollSubject.stop();
  }
}

初回の読み込みが終わったらRxRecyclerViewScrollSubject.observable()をsubscribeし、イベントが来たらunsubscribeして追加読み込みをし、追加読み込みが完了したら再度subscribeするといった使い方になります。

void loadWhenLastPosition(int nextPage) {
  subscription = rxRecyclerViewScrollSubject.observable().subscribe(event -> {
            subscription.unsubscribe();
            load(nextPage);
        });
}

追加読み込みを開始したら最後尾のスクロール通知を切り、また通知して欲しくなったらsubscribeする感じです。

最後に

もっとなんかいい方法ある気もしつつ。