visible true

技術的なメモを書く

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する感じです。

最後に

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

kaptで発生するエラーを回避するworkaround (JSR 269ライブラリ作者向け)

KotlinでJSR 269のライブラリを使う場合以下の様にkaptを使って設定するわけですが不安定な動きをする場合があります。

kapt {
    generateStubs = true
}
dependencies {
  compile 'com.github.sys1yagi.fragment-creator:library:0.6.0'
  kapt 'com.github.sys1yagi.fragment-creator:processor:0.6.0'
}

次のようなエラーが出たり、ファイルが生成されたりされなかったり。

:app:compileDebugKotlin UP-TO-DATE
:app:compileDebugJavaWithJavac FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':app:compileDebugJavaWithJavac'.
> java.lang.NullPointerException

ちょうどJSR 269を使ったライブラリを2つ作っていたので原因を探りつつ回避する方法がないか調べていたらworkaroundを見つけたのでメモします。

annotation processor内で起こっている事

NullPointerExceptionが起こっているStackTraceに従って該当箇所を見てみると以下の様な処理をしていました。

//ライブラリが提供するアノテーションを取り出す
Simplify simplify = element.getAnnotation(Simplify.class); 
// ここでNullPointerException
String actionName = simplify.value(); 

ここではコードに付与したSimplifyアノテーションのパラメータ取り出してコード生成に利用しようとしています。elementはRoundEnvironment.getElementsAnnotatedWith(Simplify.class)で取り出しているので必ずSimplifyアノテーションを持っているはずなのですがnullが返ってきてエラーとなっているわけです。

なぜnullに?

Better Annotation Processing: Supporting Stubs in kapt | Kotlin BlogSource-retained annotationsの部分を読むとRetentionPolicy.SOURCEアノテーションはgenerateStubsでは直接.classを生成するので消失するよと書いています。このケースは生成したコードにRetentionPolicy.SOURCEアノテーションを付与していた場合消失するという事だと思うのでannotation processorで処理する時には関係ないように思うのですがどうもannotation processorに渡ってくるものもkt->classを経由してRetentionPolicy.SOURCEアノテーションが消失しているぽいです(にもかかわらずRoundEnvironment.getElementsAnnotatedWith()で取り出せるのが謎です)。

しかも初回ビルド時はアノテーションが保持されている状態で渡ってきて、二回目以降は消失するという挙動をするので「不安定だ」という印象を持ってしまうのだと思います(実際どういう風になっているかはkotlin-pluginの実装などを調べてみないとわかりませんが)。

//一回目 ちゃんと処理できる
@Simplify("polling")
public final class PollingAlarmProcessor implements AlarmProcessor {

//二回目以降 書き換わっている
@KotlinClass(version={1, 0, 1}, abiVersion=32, data={"\035\025\tA\"A\003\002\031\005)\021\001B\001\006\003!\tQ\001A\003\002\031\005)\001!B\001\r\003\021\035A\002A\r\0021\003\t+!U\002\002\021\005)C\002B\006\t\0045\t\001DA\r\004\021\013i\021\001G\002\032\007!\035Q\"\001\r\005"}, strings={"Lcom/sys1yagi/longeststreakandroid/alarm/PollingAlarmProcessor;", "Lcom/sys1yagi/android/alarmmanagersimplify/AlarmProcessor;", "()V", "process", "", "context", "Landroid/content/Context;", "intent", "Landroid/content/Intent;"})
public final class PollingAlarmProcessor implements AlarmProcessor {

ライブラリ側の対処方法

realm-javaがこの問題に既に対応していました。 Support KotlinLang · Issue #509 · realm/realm-java · GitHub

以下のようにライブラリ側でアノテーションに定義している@Retention(RetentionPolicy.SOURCE)@Retention(RetentionPolicy.CLASS)にすれば問題がでなくなります。

// workaround for kapt
// @Retention(RetentionPolicy.SOURCE)
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface FragmentCreator {
}

やったね。

利用側の対処方法

別の対処法としてはアノテーションを書くファイルを.javaにする事です。これでつねに.javaがannotation processorで処理されるので安全です。とは言えこれだとKotlinにした意味がなくなるのでできるだけ避けたい所です。利用しているJSR 269のライブラリにPull Requestを出すといいんじゃないでしょうか。

enjoy

@Retention(RetentionPolicy.SOURCE)@Retention(RetentionPolicy.CLASS)の違いは.classにアノテーションが残るかどうかなのでまぁ問題ないと思います。Runtimeには乗らないので精々数百byteくらいclassファイルが大きくなる程度じゃないかなと思います。。

Androidでスクロールを含む画面全体のスクリーンショットを撮る

Android端末でスクリーンショットが撮れるようになって久しいですが、スクロールを含む画面全体のスクリーンショットについてはサポートされていません。デザインの全体を俯瞰してレビューする際などにはスクロールを含む画面全体のスクリーンショットがあると助かります。

PGSSoft/scrollscreenshotを利用すると比較的カンタンにスクロールを含む画面全体のスクリーンショットを撮れます。

セットアップ

リポジトリをcloneすれば利用できます。

git clone https://github.com/PGSSoft/scrollscreenshot.git

スクロールしながらスクリーンショットを撮る

scrollscreenshotはjarで提供されています。javaコマンドでスクリーンショットを撮るクラスを実行すれば良いのですがその前にいくつか準備が必要です。

環境変数ANDROID_SDK_HOMEを設定する

ANDROID_SDK_HOMEが設定されてない場合は.bash_profileなどに追加してください。実行時引数として渡すこともできますが面倒だと思うので環境変数を設定しておくと良いでしょう。

export ANDROID_SDK_HOME=/usr/local/opt/android-sdk

デバイスのデジタイザの入力番号を確認する

scrollscreenshotはadbでデバイスに接続し、デジタイザに対して直接タッチイベントを送信してスクロールをします。以下のコマンドを使って対象となるデバイスのデジタイザの入力番号を調べて下さい。

adb shell getevent -l

色々なイベントが流れてきます。最初は複数の入力番号が出てきますが端末を操作すると同じデジタイザで沢山イベントが発生するので判別ができます。以下のログだと/dev/input/event5の末尾の5が入力番号となります。

/dev/input/event5: EV_SYN       SYN_REPORT           00000000            
/dev/input/event5: EV_ABS       ABS_MT_POSITION_X    0000021f            
/dev/input/event5: EV_ABS       ABS_MT_POSITION_Y    0000051e            
/dev/input/event5: EV_SYN       SYN_MT_REPORT        00000000            
/dev/input/event5: EV_SYN       SYN_REPORT           00000000            
/dev/input/event5: EV_ABS       ABS_MT_POSITION_X    0000022c            

スクリーンショットを撮る

scrollscreenshotのバイナリにある場所に移動し、javaコマンドを使って実行します。-iオプションにデジタイザの入力番号を設定すれば正しくスクロールされ縦長のスクリーンショットが撮れるはずです。画像は実行したディレクトリの直下にout.pngという名前で保存されます。

cd scrollscreenshot/binaries
java -cp scrollscreenshot-latest.jar com.pgssoft.scrollscreenshot.ScrollScreenShot -i 5

こんな感じでスクリーンショットが撮れます(仕組み上ガタつく所が出ます)。

f:id:sys1yagi:20160127232235p:plain

注意点とオプション

scrollscreenshotはadb接続をしてスクロールをしながらスクリーンショットを撮って最後に結合するという仕組みで動いており画面上のスクロールの状態については関与できません。このためスクリーンショットを撮る画面毎にスクロール回数を調整する必要があります。スクロール回数(スクリーンショットの回数)は-cで指定できます。この他にもいくつかオプションがあります。

scrollscreenshotのオプションを次に示します*1

Usage: com.pgssoft.scrollscreenshot.ScrollScreenShot [options]
  Options:
    -c, --count
       スクロールの回数
       デフォルト: 5
    -v, --device
       複数の端末が接続されている場合Device IDを指定できます。特に指定がなければ最初のデバイスに接続します
    -d, --direction
       スワイプの方向を指定できます。topdown (default), leftright 
       デフォルト: topdown
    -h, --help
       ヘルプ
       デフォルト: false
    -e, --inertia
       コンテンツの慣性。スクロールに必要なピクセル数を指定できます
       デフォルト: 0
  * -i, --inputdevice
       デジタイザの入力番号。 /dev/input/eventNのN
       デフォルト: 1
    -n, --nameprefix
       出力ファイルのプレフィクス
       デフォルト: out
    -p, --pathsdk
       Android SDKのパス
    -s, --stitch
       画像の結合方法: full (つなぎ目がスムーズ), none (差分を気にせずそのまま結合する),
       separate (別々のファイルに分ける)
       デフォルト: full

enjoy

scrollscreenshotのソースを読むとjavaからAndroidDebugBridgeでadb接続してBufferedImageを使って結合などしていてとてもシンプルなので簡単に俺得スクリーンショットツールが作れるんじゃないかなと思います。タッチイベントも送出できるので特定動作を繰り返す仕組みとかできるんじゃないですかね。

*1:README.mdから抜粋し日本語化しました

fragment-creator 0.6.0 released

前回の記事(JSR 269の勉強がてらFragment生成と引数周りを楽にするfragment-creatorというライブラリを作った - visible true)は0.4.0を基にしており、その後いくつか書き方が変わったので差分を書いておきます。

本ライブラリはJitPackで配布しています。JitPackの設定をbuild.gradleに追加した上で以下をdependenciesに追加してください。

apt 'com.github.sys1yagi.fragment-creator:processor:0.6.0'
compile 'com.github.sys1yagi.fragment-creator:library:0.6.0'

Builderの作り方が変わった

Builder.newInstance()という命名はFragment#newInstance()を想起させてややこしいという意見を貰ったので改善しました。具体的にはnewBuilder()をCreatorに生やす形にしました。

before

MainFragment fragment = MainFragmentCreator.Builder
  .newInstance("keyword") // requiredな値はここで渡す
  .setUserId("userId")    // optionalな値はsetterで渡す
  .build();

after

MainFragment fragment = MainFragmentCreator
  .newBuilder("keyword") // requiredな値はここで渡す
  .setUserId("userId")    // optionalな値はsetterで渡す
  .build();

private fieldの利用

private fieldをサポートしました。setterを用意しておく必要があるのでご注意ください(リフレクションでsetしてもいいんですがそれだとコード生成している意味がないのでやってません)。

public MainFragment extends Fragment {
    @Args
    String keyword;
    @Args(require = false)
    private String userId;
    public String setUserId(String userId){
        this.userId = userId;
    }
}

Default値の設定

primitiveについてはDefault値の指定をサポートしました。引数がoptionalの場合利用できます。defaultStringとかdefaultIntとかダサいんですがアノテーションには定数しか指定できないので仕方がなさそうです。Class<? extends DefaultValueProvider>的なものを受け取ってゴニョゴニョしてもいいんですがprimitive typeの為にわざわざそういうクラス定義するのも大袈裟なので一旦defaultXXXを列挙する形にしました。今後は複雑な値のデフォルト値や引数を変換する仕組みなど入れようかなとか考えてます。

public MainFragment extends Fragment {
    @Args(require = false, defaultString = "unknown")
    String keyword;
    @Args(require = false, defaultInt = -1)
    int userId;
}

おわりに

JSR 269での生成周りのコード自身が結構単純作業でなかなか大変。

JSR 269の勉強がてらFragment生成と引数周りを楽にするfragment-creatorというライブラリを作った

aptしてますか。 正確にはPluggable Annotation Processing API(JSR 269)なので以降はJSR 269と書きます。

Dagger2やRealmなどJSR 269でコード生成を行っているライブラリ群の理解を深め、あわよくばcontributionするためJSR 269を勉強しつつチョットしたライブラリを作りることにしました。

完成したものが以下。

sys1yagi/fragment-creator - Java - GitHub

FragmentのnewInstanceメソッドとArgumentsの取扱いにまつわる部分を生成するライブラリです。

使い方

使い方はREADME.mdを見てもらえばわかるとは思いますが一応簡単に書いておきます。

以下のようにFragmentにアノテーションを付与するとMainFragmentCreatorというクラスが生成されます。

@FragmentCreator
public class MainFragment extends Fragment {
    @Args
    String keyword;
    @Args(require = false)
    String userId;
}

あとは使うだけ。

※0.6.0以降ではBuilder周りの使い方が変わっています。fragment-creator 0.6.0 releasedを参照してください。

MainFragment fragment = MainFragmentCreator.Builder
  .newInstance("keyword") // requiredな値はここで渡す
  .setUserId("userId")    // optionalな値はsetterで渡す
  .build();

FragmentのonCreate()でArgumentsの読み込みを行います。

@Args
String keyword;
@Args(require = false)
String userId;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  MainFragmentCreator.read(this);
  //keyword, userIdが初期化される
}

らくちんだ。

参考にしたもの

今後

とりあえず使えるようにはなったもののまだ機能が足りないのでvalidatorとかconverterとか設定できるようにしようかなと思います。あとKotlin化してkaptでハマった上で修正をKotlinに出せたらいいかなとか。