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

  • 19
    いいね
  • 0
    コメント

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との繋ぎ込み等やむを得ない状況であった点はご了承ください。

なお完全なSampleはこちら

{
  "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だと思うので、
そもそも上記課題がある状態なのが…という気持ちもありますが…

CatchRetryなどを上手に利用することで、うまく動くものができると思いますので、
一度試してみてはいかがでしょう。

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

この投稿は Android Advent Calendar 20163日目の記事です。
  • この記事は以下の記事からリンクされています
  • Proguard メモからリンク