19
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

AndroidAdvent Calendar 2016

Day 3

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

Last updated at Posted at 2016-12-06

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

19
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?