Android Advent Calendar 2016 3日目の穴埋めです。
普段は社内でお菓子売って生計を立てようとしているdanekoです、こんにちは。
残念ながら全く成り立たないので、Androidアプリ開発したりしてます。
なぜか手元に未投稿のAdventCalendar用のやつがあったので放流しておきます。
内容としてはAndroidアプリで、ほぼ役に立たないTipsや、最近ハマった点を幾つか紹介したいと思います。
やむを得ない仕様のJsonのParseをLoganSquareでやる
LoganSquareというJacksonのstream APIを利用したライブラリです。
またDocumentでは触れられていませんが、GenericsなModelにも対応しています。
(使い方としてこのtestコードがわかりやすいかと)
日本語の概要としては、Androidの高速なJSON パーサ/ジェネレータ、LoganSquareを使うがわかりやすいかと思います。
本題に戻ります。
下記のようなJsonが実際にありました。
そもそも設計が…というツッコミがあると思いますが、古いAPIとの繋ぎ込み等やむを得ない状況であった点はご了承ください。
{
"response" : [
{"key_a": "value1"},
{"key_b": "value2"},
{"key_c": "value3"},
{"key_a": "value4"},
{"key_b": "value5"},
{"key_c": "value6"},
]
}
そして下記のようなclassにmappingしたいという要件です。
キー名はキー名で保存したいし、Mapにしたいわけではない(同一キーでも値は複数持ちたい)という条件ですね。
class Sample {
List<KeyValue> keyValues;
static class KeyValue {
String key;
String value;
}
}
さて、この場合素直にMapping出来ないため、自分で頑張る他ありません。
今回はTypeConverterを素で頑張りました…
TypeConverterに関しては本家のDocumentをご確認ください
public final class SampleKeyValueConverter implements TypeConverter<Sample.KeyValue> {
@Override
public Sample.KeyValue parse(JsonParser jsonParser) throws IOException {
if (jsonParser.getCurrentToken() == JsonToken.START_OBJECT) {
jsonParser.nextToken();
final Sample1.KeyValue instance = parseField(jsonParser);
jsonParser.skipChildren();
while (jsonParser.getCurrentToken() != JsonToken.END_OBJECT) {
jsonParser.nextToken();
}
return instance;
}
return null;
}
Sample.KeyValue parseField(JsonParser jsonParser) throws IOException {
Sample.KeyValue instance = new Sample.KeyValue();
final String fieldName = jsonParser.getCurrentName();
jsonParser.nextToken();
final String value = jsonParser.getValueAsString();
instance.key = fieldName;
instance.value = value;
return instance;
}
...
}
結局StreamApi素で触るのと変わらないじゃん…
という落ちでもありますが、こちらを定義することで、LoganSquareでparseできるメリットがあります。
もちろんregisterは必須です
なお、未確認ではありますがTypeConverterを使わない方法も考えられます。
LoganSquareはAnnotationProcessingで生成されたコードをMapで保持しています。
なので、今回のようなケースの場合、「そのルールに則った名前のクラスを自分で生成して、reflection使ってMapに追加する」
が考えられます。
TypeConverterは登録できるのですが、ObjectMapperは登録できないので力技で…
LoganSquareを使ってParcelableを生成する
LoganSquareのserializeが早いので、全部Jsonにして文字列で扱ってしまえ。
というサンプルです。
もちろんParcelerなどが王道だとは思います。
余談ですがPacelable関連では、Android Nougatから「Bundleのサイズ管理が厳格になった」ようで、
「TransactionTooLargeException」が多発するアプリもあるかと思います。
その辺のサイズ管理、皆様どうやっているのかちょっと知りたい…
Robolectric+TimberでLog出力したいときにやりがちなミス
Timberを利用している方は多々いると思います。
また、Robolectric利用者も多いのではないでしょうか?
さて、Timberを利用するに辺り、Sampleを参考にApplicationで登録をすると思います。
また、(そもそもUnitTestをする上で、Logを出力しないと行けない状況になるのは設計が…というのはさておき)RobolectricでLogを出力するために、build.gradleに
tasks.withType(Test) {
systemProperty "robolectric.logging", "stdout"
}
と記載するでしょう。
この時、Test用のApplicationでTimberを登録すると思いますが、
登録時に、Treeに登録されている数をチェックする
ことを忘れると…
Logが重複して悲しい思いをします…
Test用のApplicationでは
if (Timber.treeCount() == 0) {
Timber.plant(new Timber.DebugTree());
}
のように登録することで、これを回避できます。
アプリと異なり、多重にApplicationを利用することになるので、当然といえばそうですが、
意外と忘れがちな上に、実害がないので放置するパターンも多いのではないでしょうか。
これを機に「UnitTestは並列に動かしても問題のない作りか?」を確認するのも良いと思います。
RxBindingも使ってTapイベントも含んだCombineLatestを作りたい
RxJavaに依存していくと、なんでもObservableで包みたい衝動に狩られると思います。
またValidation周りがスッキリ書ける(参考:「チェックAがONならば、項目Bは入力必須とする」という Validation を RxJava + RxAndroid でやる)ので、これにButton等のTapイベントも追加したくなるかと思います。
(上述のページは2年ほど前に書かれたものですが、CombineLatestの使い方がわかりやすいかと思います。)
Observable<String> formA = RxTextView.textChanges(R.id...).map(c -> c.toString());
Observable<String> formB = ...
Observable<String> formC = ...
Observable<FormValues> values = Observable.combineLatest(
formA, formB, formC, (a,b,c) -> FormValues.create(a,b,c)).cache();
values.map(value -> value.isValid())
.subscribe(flag -> submitButton.setEnabled(flag));
// ここでのFilterはRxView.clickのEmitとして渡るEventを無視するためだけのもの
Observable<Integer> clickCount = RxView.click(...).scan(0, (a,b) -> a + 1)
.filter(count > 0);
Observable.combineLatest(values, clickCount, (v,c) -> Pair.create(v,c))
.distinctUntilChanged(pair -> pair.second)
.flatMap(pair -> {/* 何か処理 */})
.subscribe(result -> ...);
上述のようにクリック自体にカウントを載せて、それが変わったかどうかでFilterすることで、
- Tap時にFormのデータが正しければ通知する
ことが達成できます。
が、このままだと
- Errorが起きたときの復旧を考える(上述例のflatMap部でNetwork通信が走るなど)
- (上記のまま使うとして)flatMap部が重い処理の場合にBackPressureなども考慮する必要が出て来る
ことが課題になるかと思います。
もちろん「何度もTapされることが前提」のUIだと思うので、
そもそも上記課題がある状態なのが…という気持ちもありますが…
CatchやRetryなどを上手に利用することで、うまく動くものができると思いますので、
一度試してみてはいかがでしょう。
Proguard v5.3 Optimizeで B extends A のときに B.super.hoge() が A.hoge()でなく B.hoge()が呼ばれる場合がある
2016/12/07 追記
下記現象は Proguard 5.3.2 では発生しません。
5.3.1では起きます。
(危うくBug報告しかけたし、既に上がっていた)
なお、5.3.2は12/05にリリースされた模様…
ピンポイントすぎるだろ…
従いまして、Versionを上げるのなら 5.3.2 を使いましょう。
なお、本日Android Studioの2.2.3がリリースされた模様ですが、そちらのリリースノートを見ると
- Android Studio 2.3 からはどうも Proguardが5.3.2になるっぽい
ですね。
つまりこの項目は、Title通り「明日役に立たないTips」どころか、本当に不要なTipsになりましたw
以下元のやつ
完全なSampleはこちら
そもそもAndroidでProguardのVersionを上げたいという状況が稀だと思いますが…
実際にKotlin周りで必要が生じ、現在担当しているアプリではVersionを5.3に上げました。
上げ方はこちらなど
上げた理由と同様の現象は恐らくこちらです
さて本題に戻りますが…
これも設計が…と言われるとそうなのですが、具体的には下記のような状況です。
class Super {
A method(...){...}
}
class Sub extends Super {
@Override
A method(...){...}
void doSomething(){
new Handler().post(new Runnable() {
@Override
public void run() {
//Sub.super.method(args); // これはNG Sub#method()が呼ばれる
methodWrapper(args); // これは期待通りの動作をする
}
});
}
void methodWrapper(...){
super.method(...);
}
}
Optimize機能を利用(android-proguard-optimize.txtを利用)している場合且つ、上述のように
- Overrideしたメソッドのsuperメソッドを無名クラス内から呼ぶ
場合に、発生します。
仮にOverrideしたmethod内でやると無限ループに陥りますので気をつけましょう。
なおJackを有効にすると想定通りの動作をします。
(Jack側でProguardのOptimizeを取り込んでいるから…?)
Kotlinで無名クラス(lambda式)に可視性の誤ったInterfaceを渡してもコンパイルが通る
完全なSampleはこちら
Androidアプリ開発では割りと利用者が多いイメージのあるKotlinですが、次のコードはコンパイルが通ります。
package a;
interface SampleInterface {
void method();
}
public class SampleA {
void method(SampleInterface sam) {
...
}
}
package b
class SampleB {
fun method() {
SampleA().method({...})
}
}
実際に実行するとjava.lang.IllegalAccessError
で落ちます。
javaとkotlinが混ざった状態でコードを追加していくと、お互いの文法が混じって「publicだと思ったらpackage defaultだった」になりがち(?)かと思いますが、
そんなときに現れる意外と厄介な例だと思います。
なお、Jackを有効にしてjavaでlambdaで同様のことをした場合は、下記のようにコンパイルエラーで落ちます。
ERROR: /androidadventcalendar/app/src/main/java/com/github/daneko/androidadventcalendar/kotlin/SampleJava.java:13.35: The type SAMInterfaceSample from the descriptor computed for the target context is not visible here.
以上、最近私が遭遇したレアケースと思われるTips等でしたが如何でしたでしょうか?
なかなか必要になるケースが思いつかないのですが…何かのお役に立てればと思います。
もし「こんな解決方法があるぜ!」等御座いましたら、是非教えてください。