Androidを開発する上で、明日役に立たないTips集

Android Advent Calendar 2016 3日目の穴埋めです。

普段は社内でお菓子売って生計を立てようとしているdanekoです、こんにちは。
残念ながら全く成り立たないので、Androidアプリ開発したりしてます。

なぜか手元に未投稿のAdventCalendar用のやつがあったので放流しておきます。

内容としてはAndroidアプリで、ほぼ役に立たないTipsや、最近ハマった点を幾つか紹介したいと思います。

### やむを得ない仕様のJsonのParseをLoganSquareでやる

[LoganSquare](https://github.com/bluelinelabs/LoganSquare)というJacksonのstream APIを利用したライブラリです。
またDocumentでは触れられていませんが、[GenericsなModel](https://github.com/bluelinelabs/LoganSquare/blob/6c5ec5281fb58d85a99413b7b6f55e9ef18a6e06/core/src/main/java/com/bluelinelabs/logansquare/ParameterizedType.java)にも対応しています。
(使い方として[このtestコード](https://github.com/bluelinelabs/LoganSquare/blob/6c5ec5281fb58d85a99413b7b6f55e9ef18a6e06/processor/src/test/java/com/bluelinelabs/logansquare/processor/RoundTripTests.java)がわかりやすいかと)

日本語の概要としては、[Androidの高速なJSON パーサ/ジェネレータ、LoganSquareを使う](http://qiita.com/kazutoyo/items/b3e9a1f868ec63a7ba5a)がわかりやすいかと思います。

本題に戻ります。
下記のようなJsonが実際にありました。
そもそも設計が…というツッコミがあると思いますが、古いAPIとの繋ぎ込み等やむを得ない状況であった点はご了承ください。

なお[完全なSampleはこちら](https://github.com/daneko/android-advent-calendar-2016-12-03/tree/c036acb2c4dda9160521562136a877492f2110f8)

```js
{
  "response" : [
    {"key_a": "value1"},
    {"key_b": "value2"},
    {"key_c": "value3"},
    {"key_a": "value4"},
    {"key_b": "value5"},
    {"key_c": "value6"},
  ]
}
```

そして下記のようなclassにmappingしたいという要件です。
キー名はキー名で保存したいし、Mapにしたいわけではない(同一キーでも値は複数持ちたい)という条件ですね。

```java
class Sample {

  List<KeyValue> keyValues;

  static class KeyValue {
    String key;
    String value;
  }
}
```


さて、この場合素直にMapping出来ないため、自分で頑張る他ありません。

今回はTypeConverterを素で頑張りました…
[TypeConverterに関しては本家のDocumentをご確認ください](https://github.com/bluelinelabs/LoganSquare/blob/1.3.7/docs/TypeConverters.md#support-for-additional-types)



```java
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で保持しています。
なので、今回のようなケースの場合、「その[ルールに則った名前のクラス](https://github.com/bluelinelabs/LoganSquare/blob/1.3.7/core/src/main/java/com/bluelinelabs/logansquare/LoganSquare.java#L230)を自分で生成して、reflection使ってMapに追加する」
が考えられます。

TypeConverterは登録できるのですが、ObjectMapperは登録できないので力技で…

### LoganSquareを使ってParcelableを生成する

[LoganSquareのserializeが早いので、全部Jsonにして文字列で扱ってしまえ。](https://github.com/daneko/LogansquarePacelableSample)

というサンプルです。

もちろん[Parceler](https://github.com/johncarl81/parceler)などが王道だとは思います。

余談ですがPacelable関連では、Android Nougatから「Bundleのサイズ管理が厳格になった」ようで、
「TransactionTooLargeException」が多発するアプリもあるかと思います。

その辺のサイズ管理、皆様どうやっているのかちょっと知りたい…

### Robolectric+TimberでLog出力したいときにやりがちなミス

[Timber](https://github.com/JakeWharton/timber)を利用している方は多々いると思います。
また、[Robolectric](http://robolectric.org/)利用者も多いのではないでしょうか？

さて、Timberを利用するに辺り、[Sampleを参考にApplicationで登録をする](https://github.com/JakeWharton/timber/blob/4.3.1/timber-sample/src/main/java/com/example/timber/ExampleApp.java#L14)と思います。

また、(そもそもUnitTestをする上で、Logを出力しないと行けない状況になるのは設計が…というのはさておき）RobolectricでLogを出力するために、build.gradleに

```gradle
tasks.withType(Test) {
    systemProperty "robolectric.logging", "stdout"
}
```

と記載するでしょう。

この時、Test用のApplicationでTimberを登録すると思いますが、
登録時に、Treeに登録されている数をチェックする
ことを忘れると…

Logが重複して悲しい思いをします…

Test用のApplicationでは

```java
if (Timber.treeCount() == 0) {
    Timber.plant(new Timber.DebugTree());
}
```

のように登録することで、これを回避できます。

アプリと異なり、多重にApplicationを利用することになるので、当然といえばそうですが、
意外と忘れがちな上に、実害がないので放置するパターンも多いのではないでしょうか。

これを機に「UnitTestは並列に動かしても問題のない作りか？」を確認するのも良いと思います。

### RxBindingも使ってTapイベントも含んだCombineLatestを作りたい

RxJavaに依存していくと、なんでもObservableで包みたい衝動に狩られると思います。

また[Validation周りがスッキリ書ける(参考:「チェックAがONならば、項目Bは入力必須とする」という Validation を RxJava + RxAndroid でやる)](http://qiita.com/amay077/items/642b41c37f9c889376d8)ので、これにButton等のTapイベントも追加したくなるかと思います。
(上述のページは2年ほど前に書かれたものですが、CombineLatestの使い方がわかりやすいかと思います。)

```java

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](http://reactivex.io/documentation/operators/catch.html)や[Retry](http://reactivex.io/documentation/operators/retry.html)などを上手に利用することで、うまく動くものができると思いますので、
一度試してみてはいかがでしょう。


### 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報告しかけたし、[既に上がっていた](https://sourceforge.net/p/proguard/bugs/625/))

なお、[5.3.2は12/05にリリースされた模様…](https://sourceforge.net/projects/proguard/files/proguard/5.3/)
ピンポイントすぎるだろ…

従いまして、Versionを上げるのなら 5.3.2 を使いましょう。

なお、本日Android Studioの2.2.3がリリースされた模様ですが、そちらの[リリースノート](https://sites.google.com/a/android.com/tools/recent/androidstudio223isnowavailable)を見ると

* Android Studio 2.3 からはどうも Proguardが5.3.2になるっぽい

ですね。

つまりこの項目は、Title通り「明日役に立たないTips」どころか、本当に不要なTipsになりましたｗ

#### 以下元のやつ

[完全なSampleはこちら](https://github.com/daneko/android-advent-calendar-2016-12-03/tree/proguard-optimize-broken-case)
そもそもAndroidでProguardのVersionを上げたいという状況が稀だと思いますが…
実際にKotlin周りで必要が生じ、現在担当しているアプリではVersionを5.3に上げました。
上げ方は[こちら](http://qiita.com/daneko0123/items/56bff762ceec2b4850ce#proguard%E3%81%9D%E3%81%AE%E3%82%82%E3%81%AE%E3%81%AEversion%E3%82%92%E5%A4%89%E3%81%88%E3%81%9F%E3%81%84)など
[上げた理由と同様の現象は恐らくこちらです](https://youtrack.jetbrains.com/issue/KT-12866#comment=27-1492381)

さて本題に戻りますが…
これも設計が…と言われるとそうなのですが、具体的には下記のような状況です。

```java

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はこちら](https://github.com/daneko/android-advent-calendar-2016-12-03/tree/interface-visible)
Androidアプリ開発では割りと利用者が多いイメージのあるKotlinですが、次のコードはコンパイルが通ります。

```java
package a;

interface SampleInterface {
  void method();
}

public class SampleA {
  void method(SampleInterface sam) {
    ...
  }
}
```

```kotlin
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等でしたが如何でしたでしょうか？
なかなか必要になるケースが思いつかないのですが…何かのお役に立てればと思います。
もし「こんな解決方法があるぜ！」等御座いましたら、是非教えてください。