丁度自分でもData Binding Libraryの細かい所を忘れていて自分の原稿読んでわーいって感じだったのでついでにGitBookに公開していつでも読めるようにしました。2015年夏に書いたものなので一部古い内容が含まれているかもしれません。適宜修正します。(セットアップ部分については最新になってます)
Fundamentals Of The Data Binding
PRとかも多分できるので適当にもらえるとうれしいです。
丁度自分でもData Binding Libraryの細かい所を忘れていて自分の原稿読んでわーいって感じだったのでついでにGitBookに公開していつでも読めるようにしました。2015年夏に書いたものなので一部古い内容が含まれているかもしれません。適宜修正します。(セットアップ部分については最新になってます)
Fundamentals Of The Data Binding
PRとかも多分できるので適当にもらえるとうれしいです。
すでにある気がするけど見当たらなかったのでメモ。
こちらにあります。
GitHub - sys1yagi/rxrecyclerview-load-more
rxbinding-recyclerview-v7をdependenciesに追加する。
dependencies {
compile 'com.jakewharton.rxbinding:rxbinding-recyclerview-v7:0.4.0'
}
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する感じです。
もっとなんかいい方法ある気もしつつ。
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を見つけたのでメモします。
NullPointerExceptionが起こっているStackTraceに従って該当箇所を見てみると以下の様な処理をしていました。
//ライブラリが提供するアノテーションを取り出す Simplify simplify = element.getAnnotation(Simplify.class); // ここでNullPointerException String actionName = simplify.value();
ここではコードに付与したSimplify
アノテーションのパラメータ取り出してコード生成に利用しようとしています。elementはRoundEnvironment.getElementsAnnotatedWith(Simplify.class)
で取り出しているので必ずSimplify
アノテーションを持っているはずなのですがnullが返ってきてエラーとなっているわけです。
Better Annotation Processing: Supporting Stubs in kapt | Kotlin BlogのSource-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を出すといいんじゃないでしょうか。
@Retention(RetentionPolicy.SOURCE)
と@Retention(RetentionPolicy.CLASS)
の違いは.classにアノテーションが残るかどうかなのでまぁ問題ないと思います。Runtimeには乗らないので精々数百byteくらいclassファイルが大きくなる程度じゃないかなと思います。。
Android端末でスクリーンショットが撮れるようになって久しいですが、スクロールを含む画面全体のスクリーンショットについてはサポートされていません。デザインの全体を俯瞰してレビューする際などにはスクロールを含む画面全体のスクリーンショットがあると助かります。
PGSSoft/scrollscreenshotを利用すると比較的カンタンにスクロールを含む画面全体のスクリーンショットを撮れます。
リポジトリをcloneすれば利用できます。
git clone https://github.com/PGSSoft/scrollscreenshot.git
scrollscreenshotはjarで提供されています。java
コマンドでスクリーンショットを撮るクラスを実行すれば良いのですがその前にいくつか準備が必要です。
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
こんな感じでスクリーンショットが撮れます(仕組み上ガタつく所が出ます)。
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
scrollscreenshotのソースを読むとjavaからAndroidDebugBridge
でadb接続してBufferedImageを使って結合などしていてとてもシンプルなので簡単に俺得スクリーンショットツールが作れるんじゃないかなと思います。タッチイベントも送出できるので特定動作を繰り返す仕組みとかできるんじゃないですかね。
*1:README.mdから抜粋し日本語化しました
前回の記事(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.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をサポートしました。setterを用意しておく必要があるのでご注意ください(リフレクションでsetしてもいいんですがそれだとコード生成している意味がないのでやってません)。
public MainFragment extends Fragment { @Args String keyword; @Args(require = false) private String userId; public String setUserId(String userId){ this.userId = userId; } }
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での生成周りのコード自身が結構単純作業でなかなか大変。
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に出せたらいいかなとか。
robolectric3のドキュメント通りにShadowクラスを書いてもうまく動かず、結局robolectric自身のソースを読んで理解してめんどくさかったのでメモしておきます。
Shadowクラスは以下の手順で宣言します。
@Implements
アノテーションを付与し、書き換え対象となるクラスを設定する@RealObject
アノテーションを付与した書き換え対象のクラスを宣言する。これは本当のオブジェクトの処理を呼び出したい場合に使います。Shadowクラスはだいたい${application_id}.testtool.shadow
パッケージに置いてます。本エントリではGsonBuilderの書き換えを行います。
import com.google.gson.GsonBuilder; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.annotation.RealObject; @Implements(GsonBuilder.class) public class ShadowGsonBuilder { @RealObject private GsonBuilder realObject; //色々 }
メソッドを書き換えるには@Implementation
アノテーションを使います。
@Implementation public GsonBuilder setDateFormat(String format) { return Shadow.directlyOn(realObject, GsonBuilder.class) .setDateFormat(format.replace("Z", "X")); }
ここではGsonBuilderのsetDateFormat(String)
を書き換えてます。Gson 2.4以下ではタイムゾーンのZ
が利用できずISO8601のパース時にコケます*1。代わりにX
を使わないといけないのですがX
はJava7以降でサポートされたものでAndroid上では動作しません*2。という事でsetDateFormat(String)
の引数をrobolectricでのテスト時だけ書き換えるようにしてます。
return realObject.setDateFormat(format.replace("Z", "X"))
と書くと無限ループしてStackOverflowErrorになります。そこでShadow.directlyOn()
を使って本物のクラスのメソッドを呼び出します。
コンストラクタを書き換えたいケースはほぼないと思いますがもし書き換えるなら以下のようになります*3。ポイントは__constructor__
というメソッド名とShadow.invokeConstructor()
でしょう。
@Implements(Paint.class) public class ShadowPaint { //省略 @RealObject Paint paint; public void __constructor__(int flags) { //省略 Shadow.invokeConstructor(Paint.class, paint, ReflectionHelpers.ClassParameter.from(int.class, flags)); } //省略 }
Shadowクラスを作ったら次にカスタムRobolectricTestRunnerを作ってShadowクラスの登録とマッピングを行います。Shadowクラスを追加する度に更新しないといけないので面倒です。
public class MyRobolectricTestRunner extends RobolectricTestRunner { public MyRobolectricTestRunner(Class<?> testClass) throws InitializationError { super(testClass); } @Override protected ShadowMap createShadowMap() { return super.createShadowMap().newBuilder() //Shadowクラスを登録する .addShadowClass(ShadowGsonBuilder.class) .build(); } public InstrumentationConfiguration createClassLoaderConfig() { return InstrumentationConfiguration.newBuilder() //クラスロード時に書き換え対象となるクラスのFQCNを登録する .addInstrumentedClass(GsonBuilder.class.getName()) .build(); } }
Shadowクラスをテストで利用するにはShadowクラスのマッピングを行うRunnerを@RunWith
で指定し、さらに@Config
アノテーションで利用するShadowクラスを宣言します。
@RunWith(MyRobolectricTestRunner.class) @Config(shadows = {ShadowGsonBuilder.class}) public class FooTest { //... }
これでこのテスト内ではGsonBuilderが書き換えられます。やったね。
https://github.com/robolectric/robolectric/blob/master/robolectric/src/main/java/org/robolectric/internal/bytecode/InstrumentationConfiguration.java#L161などに書き換えを除外するパッケージやクラスが定義されてます。書き換わらないぞ!?という時はチェックするとよさそうです。もしかしたらこの辺も設定次第でいけるかもしれません。
とりあえずShadowクラスは最後の手段だと考えておいた方がいいでしょう。ほとんどの場合はrobolectric自身が提供するShadowクラスの利用やpowermockでの書き換えで十分対応できると思います。