前人未踏の領域へ アプリ開発編

Android, iOSアプリ開発に関する調査メモ置き場。ほとんどAndroid。はてなダイアリーから移行したため古い記事にはアプリ以外も含まれます。

Gradle 4.10とFirebaseについて

Android Studioを3.3 Previewにしたところ、Gradleのバージョンを4.10.xに上げるように案内があった。

The Android Gradle plugin 3.3.0 requires Gradle 4.10 and higher, and includes the following updates.

で、上げたところアプリが起動しなくなった。

調べたところFirebase Performance Monitoringが原因でエラーになっていた。 このライブラリをコメントアウトすれば動作はするが、 StackOverFlowにはFirebaseの現状のライブラリでは4.10に対応していないような 記事も見かけたのでしばらくは4.9系のままでいくことにする。

動く設定

app/build.gradle

    implementation "com.google.firebase:firebase-core:16.0.4"
    implementation "com.google.firebase:firebase-perf:16.1.2"
gradle-wrapper.properties

    distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-all.zip

LayoutInflatorのattachToRootについてのメモ

infrate時の引数 attachToRoot は常にfalseにしとけばOKという おまじないでこれまで問題なくやってきたが trueにするケースに遭遇したのでメモ

遭遇ケース

  • LinearLayoutにViewを繰り返しaddしたい
  • 対象のViewはmergeタグを使用している
val tagsLayout: LinearLayout = <省略>
val tags = arrayOf ("漫画", "JOJO", "集英社")
tags.forEach{ tag -> 
     val tagView = inflator.inflate(R.layout.tag, tagsLayout, attachToRoot = true) // tagsLayoutが返却される
     tagView.findViewById(R.id.name).text = "hoge" //idが同じなので常に1件目が取得されてしまう
     
}

学んだ事

  1. merge タグで記述されたlayout.xmlを inflateする場合には attachToRoot がtrueでないとエラーになる
  2. attachToRoot がtrueになっていると明示的にaddViewを書かなくても親ViewにaddViewされる。
  3. inflateの戻りのViewがinflateしたlayoutのViewではなくその親Viewが返る

特に3でinflateしたはずのViewからfindViewById()しても常に1つ目のViewが取れてきてハマった。

Kotlin ラムダ周りの標準関数について(let, also, with, apply, run)

ラムダ周りで似たような関数が多くて使い分けがさっぱり...
というかletしか使わずにいたのでこれではいかんと一念発起して整理。

結論

こちらのスタイルガイドラインを参考 Coding Conventions - Kotlin Programming Language

  • ブロックの中身で判断
    • ブロック内で複数のオブジェクトに対するアクセスをしているなら this よりも it を使うのが良いため、 letalso を使う
    • ブロック内で一切レシーバを使わない場合も also を使う。
    • ブロック内でレシーバに対するアクセスのみの場合は with , apply , run のいずれかを使う
  • 戻り値で判断
    • レシーバ(コンテキストオブジェクト)が戻り値がよい場合は applyalso を使う
    • 何らかの値を返す必要がある場合は with, let , run のいずれかを使う
  • NULL許可で判断
    • レシーバがnull可かコールチェーンの結果で判断されるなら apply , let , run のいずれかを使う
    • nullが許可されない場合は withalso を使う

あんまりスッキリしてないけど。

用語の整理

書いてある内容や用語でつまずかないための整理

型変数

  • T: Type(クラスの型)
  • R: Return (実行結果。ラムダの最後の行の式。最後が代入式などの場合はUnitが返る)

レシーバ

  • メソッドを呼び出される側のオブジェクト。呼び出し側はセンダー。

レシーバ付きラムダ

  • ラムダ内において別のオブジェクトのメソッドを追加の修飾辞を使用せずに呼び出すことができる機能。
  • 同じオブジェクトに対してその名前を繰り返し記述することなく複数の操作をできる
  • レシーバをthisで参照することもできる
  • 通常はthisを省略してメソッドやプロパティを参照できる
  • ラムダを実行した返り値を返す(ラムダの最後の行の式)。

it

  • ラムダの引数が1つのときに省略して使用できるキーワード

this

  • 関数を実行中のオブジェクト。レシーバ。

let

  • レシーバを第1引数にとる
  • 実行結果を返す
  • alsoのお仲間
/**
 * Calls the specified function [block] with `this` value as its argument and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

also

  • レシーバを第1引数にとる
  • レシーバを返す
  • letのお仲間
/**
 * Calls the specified function [block] with `this` value as its argument and returns `this` value.
 */
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}

with

  • レシーバ付きラムダ
  • レシーバとラムダの2つの引数を取る
  • 第1引数をラムダのレシーバに変換する

  • alsoのお仲間

/**
 * Calls the specified function [block] with the given [receiver] as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}

apply

  • レシーバ付きラムダ
  • withのお仲間
/**
 * Calls the specified function [block] with `this` value as its receiver and returns `this` value.
 */
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

run

runにはレシーバ付きラムダと通常のラムダの2パターンがある

  • レシーバを持たないブロック関数
  • 実行結果を返す
/**
 * Calls the specified function [block] and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}
  • レシーバ付きラムダ
  • 実行結果を返す
  • also, withのお仲間
/**
 * Calls the specified function [block] with `this` value as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

サンプル

使い分けが知りたいのに同じことしてどうする、という駄目なサンプル。 it を使用しているか、と値を返却するためにラムダの最終行がどうなっているかに注目。

class Book {
    lateinit var title: String
    lateinit var author: String
    override fun toString() = "$title : $author"
}

fun main(args: Array<String>) {

    //Block内の実行結果を返す
    val a: Book = Book().let {
        it.title = "四畳半神話大系"
        it.author = "森見登美彦"
        it
    }

    //ブロックを実行し、レシーバを返す、
    val b: Book = Book().also {
        it.title = "四畳半神話大系"
        it.author = "森見登美彦"
    }

    //レシーバ付きラムダ。ラムダを実行し、レシーバを返す
    val c: Book = Book().apply {
        title = "四畳半神話大系"
        author = "森見登美彦"
    }

    //レシーバ付きラムダ。第1引数をラムダのレシーバーとして実行し、実行結果を返す
    val d: Book = with(Book()) {
        title = "四畳半神話大系"
        author = "森見登美彦"
        this
    }

    //ブロックを実行し、実行結果を返す
    val e: Book = Book().run {
        title = "四畳半神話大系"
        author = "森見登美彦"
        this
    }

    //ブロックを実行し、ラムダ最後の行を返す
    val book = Book()
    val f: Book = run {
        book.title = "四畳半神話大系"
        book
    }
}

適切なサンプルがすぐに思いつかなかった
正直なところ無理して全部使い分ける必要はない気もするが、用途を意識できるようになれれば。
使いながら覚えていくしかないな。

参考

kotlin/Standard.kt at 1.2.70 · JetBrains/kotlin · GitHub

Coding Conventions - Kotlin Programming Language

先頭にスクロールさせる

お題

選択中のタブをもう一度タップした場合などに先頭にスクロールさせたい。
RecyclerViewにはアニメーション無しのscrollToPositionとアニメーション付きのsmoothScrollToPositionが
あるのでスクロール状態に応じて使い分けたい。

対応

対象の画面が縦スクロールするとして、RecyclerViewがどの程度スクロールしたかを見るのにscrollYだと常に
0が返ってきてしまうので、computeVerticalScrollOffsetを使う。

recyclerView?.let {
    if (it.computeVerticalScrollOffset() > 5000) {
        it.scrollToPosition(0)
    } else {
        it.smoothScrollToPosition(0)
    }
}

offsetをどのくらいにするかは実際に触ってみつつお好みで。

ライブラリバージョンの最新化

アプリをリリースしたら次のリリースの前にライブラリのアップデートを行いたい。
ある程度はAndroidStudioが教えてくれるが自分で更新したいときに。

gradle-versions-pluginを使う

build.gradleを参照して更新してくれるPluginがあるのでそれを使う
https://github.com/ben-manes/gradle-versions-plugin

下準備

プロジェクトのbuild.gradleを編集。$versionには最新のバージョンを

apply plugin: "com.github.ben-manes.versions"

buildscript {
  repositories {
    jcenter()
  }

  dependencies {
    classpath "com.github.ben-manes:gradle-versions-plugin:0.20.0"
  }
}
コマンド

AndroidStudioしか使っていなかったせいか色々とダウンロードが発生して結構時間がかかった。

./gradlew dependencyUpdates -Drevision=release
実行結果

長いので出力の一部を省略しているが、以下のような感じでバージョンをチェックしてくれる。
適用するかは自分で決めるのが良さそうだな。

The following dependencies are using the latest release version:
 - android.arch.lifecycle:extensions:1.1.1
 - com.android.installreferrer:installreferrer:1.0
 - com.android.support:multidex:1.0.3
 - com.android.support.test:rules:1.0.2
 - com.android.support.test:runner:1.0.2
 - com.android.support.test.espresso:espresso-core:3.0.2
 - com.deploygate:sdk:4.1.0

The following dependencies have later release versions:
 - androidx.core:core-ktx [0.3 -> 1.0.0-beta01]
     http://developer.android.com/tools/extras/support-library.html
 - com.android.support:appcompat-v7 [27.1.1 -> 28.0.0-beta01]
     http://developer.android.com/tools/extras/support-library.html
 - com.android.support:cardview-v7 [27.1.1 -> 28.0.0-beta01]
     http://developer.android.com/tools/extras/support-library.html
 - com.android.support:customtabs [27.1.1 -> 28.0.0-beta01]
     http://developer.android.com/tools/extras/support-library.html
 - com.android.support:design [27.1.1 -> 28.0.0-beta01]
     http://developer.android.com/tools/extras/support-library.html
 - com.android.support:gridlayout-v7 [27.1.1 -> 28.0.0-beta01]
     http://developer.android.com/tools/extras/support-library.html
 - com.android.support:multidex-instrumentation [1.0.2 -> 1.0.3]
 - com.android.support:recyclerview-v7 [27.1.1 -> 28.0.0-beta01]
     http://developer.android.com/tools/extras/support-library.html
 - com.android.support:support-annotations [27.1.1 -> 28.0.0-beta01]

Failed to determine the latest version for the following dependencies (use --info for details):
 - com.jakewharton:kotterknife

Gradle updates:
 - Gradle: [4.9-rc-1 -> 4.9]

Generated report file build/dependencyUpdates/report.txt

BUILD SUCCESSFUL in 13m 6s
1 actionable task: 1 executed

Firebase Cloud Messagingのバックグラウンド状態でのデータメッセージの受信方法について

Firebaseの通知タイプには通知メッセージとデータメッセージの2種類があり、
どちらか一方だけを送信することも、両方送信することもできる。

それぞれのメッセージの処理のされ方はアプリがフォアグラウンドにいるかバックグラウンドにいるかによって異なる

フォアグラウンドにいる場合は必ず FirebaseMessagingServiceのonMessageReceivedが呼ばれるのでそこの引数から取得する。
一方でバックグラウンドにいる場合は通知メッセージはOS側で処理され、システムトレイに表示される。

Notifications Composerなどで送信する場合、通知メッセージとデータメッセージ両方を含むことができるが、
バックグラウンドで通知を受信した際に、データメッセージをどこで受け取れば良いのか。

Intentである。アプリ起動時に最初に呼ばれるIntentのExtraの中に他の起動パラメータなどと一緒に含まれてくる。
onMessageReceivedとは違い、通知がタップされてからじゃないと発動しないので注意。

トピックで配信された場合は以下のようにfromキーの値として取得できるのでそれで判別できるだろう。

from=/topics/トピック名

AndroidX Refactor to AndroidX...

Android StudioのAndroidX対応の置換機能を使ったら
ConstraintLayoutのところでClassNotFoundExceptionが発生した。

Caused by: java.lang.ClassNotFoundException: Didn't find class "androidx.constraintlayout.widget.ConstraintLayout"

正しくは

androidx.constraintlayout.ConstraintLayout

どうやらバグらしい。