9
3

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.

第2のドワンゴAdvent Calendar 2017

Day 4

明日使えないAndroidTips集

Last updated at Posted at 2017-12-03

これは第2のドワンゴ Advent Calendar 2017の4日目です

銀座松竹スクエアの12Fでお菓子神社やってた頃が懐かしい今日このごろ
皆様いかがお過ごしでしょうか?

私は「日頃から書くことまとめてないから、ギリギリになって慌てるんやで…」
みたいな気持ちで書いています。

相変わらずAndroidでとりあえず生活しているので、最近ハマった(忘れてた)こと特集とか…
GCPやGO周りでも生活しているけど、書くこと無いし
Scalaに至っては…
だれか(割の良い)お仕事ください。(おい)

環境

#Fri Nov 24 09:04:08 JST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip

buildscript {
    ext.kotlin_version = '1.1.60'
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.1'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"

    implementation "com.google.dagger:dagger:2.13"
    implementation "com.google.dagger:dagger-android:2.13"
    implementation "com.google.dagger:dagger-android-support:2.13"
    kapt "com.google.dagger:dagger-compiler:2.13"
    kapt "com.google.dagger:dagger-android-processor:2.13"
    
    implementation "com.github.bumptech.glide:glide:4.2.0"
    kapt "com.github.bumptech.glide:compiler:4.2.0"

    
    implementation 'android.arch.lifecycle:extensions:1.0.0'

    implementation 'org.funktionale:funktionale-all:1.1'

    implementation 'io.kategory:kategory:0.3.10'

    implementation 'com.squareup.moshi:moshi:1.5.0'
    implementation 'com.squareup.moshi:moshi-kotlin:1.5.0'

    implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.9.0'

    implementation 'com.infstory:proguard-annotations:1.0.2'
}

だいたい伝わるかと

KotlinとDagger2とNamed Injection

ActivityとかにはFieldInjectすると思います。

@Inject
lateinit var hoge:Hoge

で、たまに同一クラスなんだけどちょっと設定が違うものをInjectしたい時もあるでしょう。
そんな時、面倒なので@Named(...) 使って用意したりするかと思います。

んで

// NG
class HogeActivity ... {
  @Named("settingA")
  @Inject
  lateinit var hoge:Hoge
}

って書いて思ったとおり動かなくて数時間無駄にします。

// OK
class HogeActivity ... {
  @field:[Named("settingA") Inject]
  lateinit var hoge:Hoge
}

ここを読んだうえで
Namedを見る
となんでかがよくわかります。

上述BlogのTitleの通り、 JSR330のQualifierを付与したAnnotation classを作る場合はちゃんとTargetを指定した方がよい に従って、Annotation作って利用しましょう(Namedを使わない方向で)

Dagger Androidを使いならがGlideAppModuleにInjectする

Dagger2.10くらいからdagger.androidができています。
まあ位置づけ的には「使いたいなら使えば…?」ぐらいの位置づけのはずですが…
その使い方はDocumentを見てください

これを使うとActivity/FragmentではAndroidInjection.inject(this)で済むので楽ですね。
ただしそれ以外のClassでコンストラクタインジェクションができない場合…

GlideのDocumentを見て
GlideのModule作って…
あー、なんかInjectしたい〜

@GlideModule
class MyGlideModule : AppGlideModule() {

    @Inject
    lateinit var hoge: Hoge

    ...
}

と書きたいですが、
当然AndroidInjection.inject(this)なんてできません…
いやコンストラクタインジェクションできたらこんな問題にぶち当たらないんですけど…
やむを得ない…

なので、旧来同様の方法(?)でInjectしましょう?

@Singleton
@Component(modules = arrayOf(...))
interface AppComponent {

    @Component.Builder
    interface Builder {
        @BindsInstance
        fun application(application: Application): Builder

        fun build(): AppComponent
    }

    fun inject(application: MyApplication)

    /**
     * こんな感じで生やしとく
     */
    fun inject(glideModule: MyGlideModule)
}

class MyApplication : Application(), HasActivityInjector, HasServiceInjector {
    // Sampleなので直接公開している
    lateinit var appComponent: AppComponent

    override fun onCreate() {
        super.onCreate()

        appComponent = DaggerAppComponent
                .builder()
                .application(this)
                .build()

        appComponent
                .inject(this)
    }
}

@GlideModule
class MyGlideModule : AppGlideModule() {
    ...

    // 適当な場所でInjectする
    override fun registerComponents(context: Context?, glide: Glide?, registry: Registry?) {
        (context?.applicationContext as? MyApplication)?.appComponent.inject(this)
    }
}

適当に書くとこんな感じでしょうか?
まあなんか良い方法があったら知りたいところです。

Kotlin data class と moshi-kotlin と JSON Annotation と Proguard

多分APIを叩くために大半の人がRetrofitを選ぶ気がします。
そしてJsonとのConverterで

  • Gson
  • Jackson
  • Moshi
    あたりから選択するかと思います

適当にググるとKotlinを使う上でGson→Moshi乗り換え組が多そうな印象…?

普通に使おうとして死ぬ(利用者が悪い)

で、まあそんなこんなでMoshiを選んだとして、Kotlin用のModuleも突っ込むでしょう

ココに書いてある一文を読まなくて死ぬ

Add the KotlinJsonAdapterFactory last to allow other installed Kotlin type factories to be used, since factories are called in order.

val moshi = Moshi.Builder()
    // Add any other JsonAdapter factories.
    .add(KotlinJsonAdapterFactory()) // このあとになんかaddしても困るで
    .build()

KotlinJsonAdapterFactoryは最後に突っ込めよと。
まあこの辺は普段適当でもテスト書いていれば即気がつくのでそこまで困らないかと思います。
(実際Document読まなかったけど、テストでコケて気がついたので)

めんどくて死ぬ(筋力が足りない利用者が悪い)

そしてこれを無事に乗り越えたところで、「現実問題」にぶつかります。

  • サーバサイドはsnake_caseでjson返す
  • こっちはcamelCaseにしたい
@KeepClassMembers
data class Hoge(
  @Json(name="hoge_fuga") val hogeFuga:String,
  @Json(name="piyo_piyo") val piyoPiyo:Int)

これ2つだから良いけど、こんなのがいっぱい発生しますね…
実際この辺なんだけど、
もう一括でどうにか…の場合、このClassJsonAdapterに変わるようなものを作れば…
みんなどうやってるんだろ…?

JacksonのJsonNamingやNamingStrategyが欲しい…

というのは置いといて、愚直に変換しているかと思います。

ちなみにJacksonでClass単位で付けるなら

@KeepClassMembers
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
data class Hoge(
  val hogeFuga:String,
  val piyoPiyo:Int)

です

Proguardで死ぬ(死ぬ)

val sample ="""
{
  "hoge_fuga" : "test",
  "piyo_piyo" : 1
}
"""

これをProgurdかけて実行しましょう
死にます。nullとか0とか得られます…

もちろんReadmeに書いてある通りの設定(2017/11/24現在)をしています

回避策1

@KeepClassMembers
data class Hoge(
  @Json(name="hoge_fuga") @field:Json(name="hoge_fuga") val hogeFuga:String,
  @Json(name="piyo_piyo") @field:Json(name="piyo_piyo") val piyoPiyo:Int)

proguard(optimize込)のときにどうやらfieldとして判定されるっぽい(Proguard後にDecompileするのがめんどかったので推測)のでfieldに対してだよ!と明示します。
ところかfieldに対してだけ明示すると、今度は通常開発時に困る(ProguardかけなければConstructorParameterなので、明示するなら@param:Json)ので上述のように2つ書くことで回避できます…

回避策2

proguardの設定を変える

これ偶然(Jacksonと比較したときに)気がついたのですが…

-keepclassmembers class kotlin.Metadata {
    public <methods>;
}

-keep class kotlin.Metadata { *; }

とすることで回避できます

ただしこの場合@field:Json...としているほうが効かなくなる(Proguard未設定と同じ挙動)点に注意

Genericsな型に突っ込むのが面倒で死ぬ(筋力)

どのAPIのResponseにも同じ形のMetadataが突っ込まれる仕様だったり、
Pagingだったりで、

// 名前は適当
data class WithPaging<T>(
  val pagingInfo : PagingInfo,
  val data : T
)

みたいな形で受け取りたいことは多々あるかと思います。

Generics的なmoshiのサンプルとしてはこのへんを参考にするっぽいんですが…
これあくまでUnwrapだけのサンプルです。

実際のjsonが

{
  pagingInfo : {...}
  data : {...}
}

だとして、Unwrap.javaのL.86みたいにObjectで返したいわけではないと思います。

なので、Unwrap以外にもCollectionJsonAdapterとかを参考にして、
Adapterを作ると良い感じ(めんどくさい)にAdapter作れるのでがんばってください。

CollectionJsonAdapter#fromJson当たりの通り、JsonReaderとかで素直にやるだけですけど…

いやほんと、頑張るしか無いんですよねこれ…?

Renameに失敗して死ぬ(未検証)

これなんかにあるんじゃねぇかと思って見てたら
JsonAnnotationでRenameしているのが効かないみたいなbug報告見つけて震えている…

もうどうせSupportLibrary入れてGooglePlayServices入れて…とかやったりとMultiDex必要になるし、
今更新規アプリに4系サポートすることないだろうし
みんなJackson使えば良いんじゃないんですかね。
jackson-module-kotlinあるし…(未検証/ScalaGe版に不満はなかった)

とはいえ、APKばらしてみたときのDefinedMethodsの差を考えるとなかなか…
うーむ…

AndroidArchitectureComponentsのLiveDataとKotlin

AndroidArchitectureComponents(以下AAC)使ってますか?
気がついたらAppCompatActivity/FragmentがLifecycleOwnerを実装している形になっていますし、
これは使えというGoogleさんからのお告げに違いない…と思って使っています。

で、PagingLibraryはいつ1.0.0に…(2017/11/20 現在 alpha3)

// val livedata:LiveData<Sample> 的な
liveData.observe(this, Observer { response ->
    // なんか処理
})

これ成功だけを前提とするならば何の問題もないのですが、(場所は忘れたけどどこかに書いてあった通り)Errorを通知できる形にするべきかと思います…

が、「みんながみんなそんなのをApplicationの数だけ用意するのか…?」と思いませんか?
僕の考えた最強のErrorも伝搬できるInterfaceを溢れさせてどうする…
(ちなみにGoogleSamplesのAACのやつにもあります)

と思っている皆さんに紹介したいのがこちら。
FunktionaleのTry!!

// val livedata:LiveData<Try<Sample>> 的な
liveData.observe(this, Observer { response ->
    response?.fold({
      // 成功時のやつ
    }, {
      // 失敗時のやつ
    })
})

// 例えば呼び出し側がRetrofitとRxJava2を使っているとして
api() // Single<T>として
  ...
  .subscribeOn(AndroidSchedulers.mainThread())
  .subscribe(
    { livedata.value = Try.Success(it) },
    { livedata.value = Try.Failure(it)}
  )

もちろん単にThrowする何をラップするならTry{ /*なんかThrowするかもしれないやつ*/ }でOKです
成否がわかれば良いのならOption、Errorの型を自分で…ならEitherも。

ObserverがJavaとの兼ね合いでnullableなのがすっごい微妙だけど…まあ…
あとTry#foldの左側が成功時なのも微妙…
Eitherはもちろん右側が成功だとして、Optionも右のときに値がある前提なのになぜ…

ついでにご紹介したいのがこちら、Kategory

FunktionaleはKotlinが出てきた頃からありますし、
Moduleも分けられているしで現時点では利用する点では利用しやすいと思います。
(JavaだとFunctionalJavaから機能が減ったイメージくらいの位置づけなイメージ)

まあでもKEEPにtypeclass追加しようぜproposalが出る昨今ですから、
Kotlinerな皆さんでしたら、Funktionaleとか物足りないと思うんですよね。
上述KEEPのIssueにもリンクがありますが、KategoryっていうCatsにインスパイアされて作られた(なんで作ったし…)素敵…?な?Library??があります。

これv0.3.1の頃のReadmeを見ると「Kats」だったんですね。
そして全然関係ないけど今Masterみると

Functional Datatypes and abstractions for Kotlin inspired by Cats

って文言消しているんですけど、なんか都合悪かったんでしょうか…

もうサンプルコード書くのも疲れたんで、適当に流しますが、
KategoryのTryを使ってもだいたい同じ感じでかけます。

またAnnotationProcessingを使うことにより、@typeclass annotationをクラスに付与するだけで簡単に型クラスが!

みたいな感じでもっと詳細を見たい方のためのSampleが世の中に…
解説はこちらのBlogの記事この記事
DIにReaderMonad使っている説明とか、NestしたらMonadTransformerとかあってね〜
みたいな説明あります。
サンプルには「free」っていうDirectoryもあるので、同じ人がそのうちFreeMonadの解説してくれるんじゃないかな?(2017/11/20現在 freeのサンプルまで行くと@typeclass annotationを利用している)

なおGoogleSamplesのAndroidArchitectureをForkした版も絶賛作成中(2017/11/20現在)っぽいですね

Kategory自体(全部ではないけど)Documentも頑張っていこう感あるので、今後共監視(?)しておくと良いのではないでしょうか。httpsじゃないけど(2017/11/20現在)

って流石にKategoryが流行ったり、typeclassがKotlinに入るとは思えないんだけどどうなんでしょう…
(っていうかKategoryまだ1.0にもなってないし)

と去年以上に役に立たないものを放流したところで、明日はurakawaさんです!

9
3
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
9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?