TRY ANDROID DEV

Android アプリ開発のコーディングネタ。あとエンジニアとしての活動

GW:全力でTODOアプリを作ってみる(3日目)

背景

  • GWなのでアウトプットに専念してみる
  • 一旦基本のTODOアプリを作ったらどんな風になるのか試してみる
  • 子供が寝た隙に毎日1時間程度の稼働でどこまでいけるか
  • 子供が高熱を出して早速3日ストップした。。。

Repositoryを用意する

直接Daoを操作するのは忍びないというか、今後サーバ連携を考えるとRepositoryを経由させておきたい。

class TodoRepository(private val todoDao: TodoDao) {

    fun fetch() : LiveData<List<TodoEntity>>{
        return todoDao.getAllWords()
    }

    fun add(todoEntity: TodoEntity) {
        todoDao.insert(todoEntity)
    }
}

現状Entityをそのまま返却しているが、できればドメインモデルに変換して返したい。これはあとで対応する。

ViewModelにつなぐ

AACのViewModelを作成して、Repositoryを持たせる。

class TodoListViewModel(repository: TodoRepository) : ViewModel() {

    val todoList = repository.fetch()

    class Factory(private val todoRepository: TodoRepository) : ViewModelProvider.NewInstanceFactory() {
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            return TodoListViewModel(todoRepository) as T
        }
    }
}

一応RepositoryはFragmentからDIするようにした。

class TodoListFragment : Fragment() {

    lateinit var todoListViewModel: TodoListViewModel

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_todolist, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        todoListViewModel = ViewModelProviders.of(
            this,
            TodoListViewModel.Factory(TodoRepository(TodoDatabase.getDatabase(activity!!.application).todoDao()))
        )
            .get(TodoListViewModel::class.java)

        todoListViewModel.todoList.observe(this, Observer<List<TodoEntity>> {
            // TODO : ここでRecyclerViewに表示する?Bindしても良いかも。
        })
    }
}

ドメインモデルを作成する

やっぱりモデルを先に作っておこうと思った。

data class Task(val title: String,
                val detail: String,
                val expireTime: String)

ViewModelにクリック時の処理を追加

coroutineを利用して非同期処理実施。 viewmodel-ktx 2.1.0で追加されたviewModelScopeを利用すればjobを作らなくて済む。 (現時点でalpha04)

class AddTodoViewModel(private val todoRepository: TodoRepository) : ViewModel() {

    fun onClickSaveButton(title: String, detail: String, date: String) = runBlocking {
        viewModelScope.launch {
            val task = Task(title, detail, date)
            todoRepository.add(task)
        }
    }

    class Factory(private val todoRepository: TodoRepository) : ViewModelProvider.NewInstanceFactory() {
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            return AddTodoViewModel(todoRepository) as T
        }
    }
}

今日はここまで。

GW:全力でTODOアプリを作ってみる(2日目)

背景

  • GWなのでアウトプットに専念してみる
  • 一旦基本のTODOアプリを作ったらどんな風になるのか試してみる
  • 子供が寝た隙に毎日1時間程度の稼働でどこまでいけるか
  • 子供が高熱を出して早速3日ストップした。。。

Navigationを用意する

まずはres/navigation フォルダを作成してnavigation.xmlを作成する。

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"

    android:id="@+id/navigation"
    app:startDestination="@id/todolist">

    <fragment
        android:id="@+id/todolist"
        android:name="todo.view.TodoListFragment"
        tools:layout="@layout/fragment_todolist" />
</navigation>

あとはActivityのlayoutファイルにnavigationを設定する。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context=".MainActivity"
    tools:showIn="@layout/activity_main">

    <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/navigation" />

</androidx.constraintlayout.widget.ConstraintLayout>

fragmentのlayoutファイルにはRecyclerViewを設定しておく。 tools:listitemをRecyclerViewに設定すると、Designタブで見れるので良い。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">


    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        tools:spanCount="1"
        tools:itemCount="30"
        tools:listitem="@layout/item_todo"
        tools:orientation="vertical" />

</LinearLayout>

RecyclerViewのtoolsについては以下のQiita記事にまとめた。

qiita.com

感想

  • なかなか時間が取れずやきもきする。。

GW:全力でTODOアプリを作ってみる(1日目)

背景

  • GWなのでアウトプットに専念してみる
  • 一旦基本のTODOアプリを作ったらどんな風になるのか試してみる
  • 子供が寝た隙に毎日1時間程度の稼働でどこまでいけるか

Roomを使ってDB作成

@Dao
interface TodoDao {

    @Query("SELECT * from todo")
    fun getAllWords(): List<TodoEntity>

    @Insert
    fun insert(todoEntity: TodoEntity)
}
@Entity(tableName = "todo")
data class TodoEntity(
    @PrimaryKey val taskNo: Int,
    val title: String,
    val detail: String,
    val expireTime: String?
)
@Database(entities = [TodoEntity::class], version = 1)
abstract class TodoDatabase : RoomDatabase() {
    abstract fun todoDao(): TodoDao

    companion object {
        @Volatile
        private var INSTANCE: TodoDatabase? = null

        fun getDatabase(context: Context): TodoDatabase {
            val tempInstance = INSTANCE
            if (tempInstance != null) {
                return tempInstance
            }
            synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    TodoDatabase::class.java,
                    "todo" // テーブル名
                ).build()
                INSTANCE = instance
                return instance
            }
        }
    }
}

一旦ここまでは最近もやったので比較的スムーズ。

buildsrcを用いたdependencies管理

Navigation導入しようと思ったけど先にdependenciesを整理しておく。 前やったしスムーズでしょ、と思ったらどハマりして1時間かかった。。。

buildSrcフォルダにすべきところをbuildsrcにしていた。

ひどいハマり方をしてしまった。。でも最初のパッケージ名入力大文字指定できなかったよね。。?

build.gradle.ktsの中身にrepository指定が必要

バージョンが変わったからなのかリポジトリ指定が必要になっていた。

plugins {
    `kotlin-dsl`
}

repositories {
    jcenter()
    google()
}

Gradle sync failed: No such propertyが発生していた

Depファイル(dependenciesを書いているobjectクラス)にパッケージ名が記載されていなかった。。。 パッケージ名を設定して、appのbuild.gradleにimportしたら動作した。

import dependencies.Dep

今日はここまで。

明日やること。

Navigationを使って画面を作る

Android Architecture Components: Room の再復習

背景

  • 急遽RoomとPagingの再復習をする必要に駆られたので頑張る
  • まずはRoomから

Room

RoomはQLiteをラップして利用できるコンポーネント

以下のCodeLabsを行う。 codelabs.developers.google.com

Gradleの設定

以下の設定をbuild.gradleに記載する

// Room components
implementation "android.arch.persistence.room:runtime:$rootProject.roomVersion"
kapt "android.arch.persistence.room:compiler:$rootProject.roomVersion"
androidTestImplementation "android.arch.persistence.room:testing:$rootProject.roomVersion"

// Lifecycle components
implementation "android.arch.lifecycle:extensions:$rootProject.archLifecycleVersion"
kapt "android.arch.lifecycle:compiler:$rootProject.archLifecycleVersion"

// Coroutines
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$rootProject.coroutines"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines"

build.gradleに記載するimplementationについて復習しておく。

compile : 依存関係を追加する。ただしBプロジェクトをcompileで指定したAプロジェクトは、BがCライブラリに依存していた場合、AもCライブラリに依存することになる。deplicated。
implementation : 依存関係を追加する。ただしBプロジェクトをcompileで指定したAプロジェクトは、BがCライブラリに依存していた場合でもAはCライブラリに依存しない(AはCライブラリを利用できない)
api : compileと同じ動作をする。

なぜcoroutinesのdependenciesをapiにしているのかは謎だ。。 あとCodeLabsで利用しているバージョンは0.29.1と古いものだったので最新の1.0.1にしておく。

Entityを作る

ここでいうEntityはSQLの文脈。Entityはテーブルの行を表しており、以下のようなクラスにAnnotation Processingで表すことができる。

@Entity(tableName = "word_table")
data class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)

各要素について記載します。

  • @Entity

    • テーブル内の行を指定するAnnotation。クラスの名前と別にしたい場合はtableNameを指定する
  • @PrimaryKey

    • すべてのエンティティに主キーを設定する必要がある
  • @ColumnInfo

    • メンバー変数の名前と異なる名前にしたい場合はnameで指定する(なくても良い)

注意事項として、全てのフィールドはpublicもしくはgetterを設定する必要があるようです。

Daoを作る

Data Access Objectを作成する。要するにテーブルに対してアクセスするオブジェクト。

@Dao
interface WordDao {

    @Query("SELECT * from word_table ORDER BY word ASC")
    fun getAllWords(): List<Word>

    @Insert
    fun insert(word: Word)

    @Query("DELETE FROM word_table")
    fun deleteAll()
}

各要素について記載します。

  • @Dao
    • Data Access Objectであることを指定する。interfaceに対して指定すること。(このinterfaceから自動的にDataAccessObjectを作ってくれる)
  • @Query
    • このAnnotationを設定したメソッドを実行すると指定したSQLが実行される
  • @Insert
    • メソッドの引数に指定したEntityを挿入する。競合した場合の振る舞いをonConflictで指定できる。
    • 詳細は次のURLを参照: developer.android.com

LiveDataと連携する

データベース変更された際にUIに通知したいよね。RoomはLiveDataを戻り値に設定してUIに通知できるんです。

  @Query("SELECT * from word_table ORDER BY word ASC")
   fun getAllWords(): LiveData<List<Word>> // LiveDataを設定する

RoomDatabaseを作る

RoomはSQLiteデータベースをラッピングする仕組みです。SQLiteOpenHelperのようなSQLiteを利用する時に必要だったタスクを肩代わりしてくれるようです。 Daoを利用してSQLiteにクエリを発行します。またSQLiteステートメントコンパイル時チェックをしてくれるようです。素敵!

@Database(entities = [Word::class], version = 1)
abstract class WordRoomDatabase : RoomDatabase() {
    abstract fun wordDao(): WordDao

    companion object {
        @Volatile
        private var INSTANCE: WordRoomDatabase? = null

        fun getDatabase(context: Context): WordRoomDatabase {
            val tempInstance = INSTANCE
            if (tempInstance != null) {
                return tempInstance
            }
            synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    WordRoomDatabase::class.java,
                    "Word_database" // テーブル名
                ).build()
                INSTANCE = instance
                return instance
            }
        }
    }
}

各要素について記載します。

  • @Database(entities = [Word::class], version = 1)
    • 対象のクラスがDatabaseであることを指定する。
    • entitiesにこのDatabaseのテーブルを指定する.
    • versionも指定できます。マイグレーション時に必要。
  • Databaseに指定するクラスはabstractを指定すること。
  • また、このデータベースにアクセスするDaoもabstractメソッドで指定すること。
  • @Volatile
    • これはJVM上でvolatileとして解釈されるannotationです。
    • 簡単に説明すると値のキャッシュを行わず常にメモリから取ってくることでマルチスレッド時の動作でも値が異ならないようにするための指定です。
    • synchronizedとの違いはスレッドセーフなのかそうでないのからしいです。
  • Databaseは1アプリにつき1インスタンスなのでシングルトンとします。
  • インスタンスを生成する時はRoom.databaseBuilder()を利用します。

リポジトリを作成する

リポジトリパターンのためにリポジトリを作成します。 基本的にはこの層でAPIから取ってくるかDatabaseから取ってくるかを決める感じです。

class WordRepository(private val wordDao: WordDao) {

    val allWords: LiveData<List<Word>> = wordDao.getAllWords()

    @WorkerThread
    suspend fun insert(word: Word) {
        wordDao.insert(word)
    }
}

@WorkerThreadは「このメソッドは非同期通信する必要があるよ」ということを明示的に示すためのAnnotaionです。 Roomは非同期(UIThread以外)で利用する必要があるので、記載します。

ViewModel を作る

class WordViewModel(application: Application) : AndroidViewModel(application) {

    private var parentJob = Job()
    private val coroutineContext: CoroutineContext
        get() = parentJob + Dispatchers.Main
    private val scope = CoroutineScope(coroutineContext)

    private val repository: WordRepository
    val allWords: LiveData<List<Word>>

    init {
        val wordsDao = WordRoomDatabase.getDatabase(application, scope).wordDao()
        repository = WordRepository(wordsDao)
        allWords = repository.allWords
    }

    fun insert(word: Word) = scope.launch(Dispatchers.IO) {
        repository.insert(word)
    }

    override fun onCleared() {
        super.onCleared()
        parentJob.cancel()
    }
}

ここはViewModelなので省きます。allWordsにLiveDataを設定し、initブロックでRoomと接続してますね。

Databaseにオープン時のCallbackを設定する

Databaseのイベントに対してCallbackを設定できるようです。 ここではオープン時に今までのデータを全て消して二つの単語を登録しています。

@Database(entities = [Word::class], version = 1)
abstract class WordRoomDatabase : RoomDatabase() {
    abstract fun wordDao(): WordDao

    companion object {
        @Volatile
        private var INSTANCE: WordRoomDatabase? = null

        fun getDatabase(
            context: Context,
            scope: CoroutineScope
        ): WordRoomDatabase {
            val tempInstance = INSTANCE
            if (tempInstance != null) {
                return tempInstance
            }
            synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    WordRoomDatabase::class.java,
                    "Word_database"
                ).addCallback(WordDatabaseCallback(scope)).build() // オープン時のコールバックを設定
                INSTANCE = instance
                return instance
            }
        }
        private class WordDatabaseCallback(
            private val scope: CoroutineScope
        ) : RoomDatabase.Callback() {

            override fun onOpen(db: SupportSQLiteDatabase) { // オープンイベント
                super.onOpen(db)
                INSTANCE?.let { database ->
                    scope.launch(Dispatchers.IO) {
                        populateDatabase(database.wordDao())
                    }
                }
            }

            // オープン時の処理
            fun populateDatabase(wordDao: WordDao) {
                wordDao.deleteAll()

                var word = Word("Hello")
                wordDao.insert(word)
                word = Word("World!")
                wordDao.insert(word)
            }
        }


    }


}

あとはinsertしたり、Observeしたり。

あとはViewModelのLiveDataをObserveして、変更が発生した場合の処理書いたり、

wordViewModel.allWords.observe(this, Observer { words ->
            // Update the cached copy of the words in the adapter.
            words?.let { adapter.setWords(it) }
})

Roomにデータを追加したい時はinsertしたりできます。

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)

    if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
        data?.let {
            val word = Word(it.getStringExtra(NewWordActivity.EXTRA_REPLY))
            wordViewModel.insert(word)
        }
    } else {
        Toast.makeText(
               applicationContext,
               R.string.empty_not_saved,
                Toast.LENGTH_LONG).show()
    }
}

まとめ

  • Room を利用して永続化を行うと、SQLiteの厄介なタスクやStatement等を書かなくて良い
  • しかもLiveDataと接続するとDatabaseの中身が変更された際にUIに通知できる。素敵。
  • SQLコンパイル時に検証できる!素敵!

Type parameter bound for T in XXX is not satisfied が発生した場合の対処方法

背景

  • 新規プロジェクトでViewModelの動きを試そうとしたら「Type parameter bound for T in XXX is not satisfied」が発生した

ソースコード

MainActivity.kt

import android.os.Bundle
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.ViewModelProviders

class MainActivity : FragmentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val viewModel = ViewModelProviders.of(this@MainActivity).get(MainViewModel::class.java) // <-- Type parameter bound for T in XXX is not satisfied
    }
}

MainViewModel.kt

import android.arch.lifecycle.ViewModel
import com.jakewharton.rxrelay2.BehaviorRelay

class MainViewModel : ViewModel() {

    public val relay = BehaviorRelay.create<String>()

    fun getStringData() {
        relay.accept("test")
    }
}

内容

Type parameter bound for T in XXX is not satisfiedは「Tにバインドされているクラスの型が満たされていないよ」という意味。
つまりViewModelProviders.of().get()に渡している型が満たされていないらしい。いやクラス設定してるじゃん。。と思ったら、Activity側はandroidxを利用していて、ViewModelはandroid.archを利用していた。それは一致しない。。

以下のように修正したら直った。

MainViewModel.kt

import androidx.lifecycle.ViewModel // <-- fix.
import com.jakewharton.rxrelay2.BehaviorRelay

class MainViewModel : ViewModel() {

    public val relay = BehaviorRelay.create<String>()

    fun getStringData() {
        relay.accept("test")
    }
}

まとめ

androidのライブラリこういうの多いのでつらい

'lateinit' modifier is not allowed on properties of primitive type はなぜ起こるのか。

背景

  • なんとなくnullableにしたくなくて次のように記載したら'lateinit' modifier is not allowed on properties of primitive type と警告された。
    /**
     * Constructs an [ClientException] with no detail message.
     */
    constructor() : super()

    /**
     * Constructs an [ClientException] with the specified detail message.

     * @param   message   the detail message.
     */
    constructor(message: kotlin.String, statusCode: Int) : super(message) {
        status = statusCode
    }

    private lateinit var status: Int

実際この場合ではどうしてもnullが発生しうるパターンだったので、lateinit使わずに以下のように表現すべきだった。

private var status: Int?

けどもなんでプリミティブ型はlateInitできないんだっけ?と思ったので調べてみた。

リファレンスを読んでみる

公式リファレンスを参照

lateInitが用意された理由は「そもそもlateInitないと、例えばテストコードのようなコンストラクタでないところで初期化せざるを得ないとき、毎回nullチェックするのだるい」ってことらしい

public class MyTest {
    lateinit var subject: TestSubject

    @SetUp fun setup() {
        subject = TestSubject()
    }

    @Test fun test() {
        subject.method()  // dereference directly
    }
}

しかし、「プリミティブ型にはlateInit使えないよ」とは書いてあったものの、なぜ使えないのかについては記載がなかった。

おんなじ疑問を持った人がいた。

stackoverflow.com

ここの回答に以下のような回答があった。

kotlinはnullを利用してlateinitプロパティが初期化されていないことをマークし、そのプロパティにアクセスした時に適切な例外をスローする。
プリミティブ型の場合そのような値がないためlateInitが判断できない。

え、Int型ってオブジェクトだからnull持てるのになんでって思った。

ソースコードを見てみる

そもそもInt型のようなプリミティブ型ラッパークラスについてあんまりよく理解していなかったのでInt型クラスを開いてみる

/**
 * Represents a 32-bit signed integer.
 * On the JVM, non-nullable values of this type are represented as values of the primitive type `int`.
 */
public class Int private constructor() : Number(), Comparable<Int> {

このクラスはJVM上ではintとして表現されるよって書いてある。そうだよね。JVM上で動作するんだからkotlinでいくらオブジェクト型だろうとintになったらnull持てないよね。。 なのでStringにはlateInit使えるけどIntやLongでは使えないのか。

まとめ

  • kotlinのInt型はJVM上でintとして扱われる
  • lateinitはプロパティが初期化されていないことをnullを用いて判断している
  • なのでJVM上nullを持てない型にはlateinitが利用できない

調べている場合ではない時ほどこういうの気になってしまうのはよくないと思いました。

Firebaseでチャットアプリを作る(Cloud Firestore)

背景

  • なんとなくFirebase使ったことないのまずいかなぁと思ってFirebaseに手を出してみる。
  • 今まではPush通知くらいしか使ってなかった。
  • 今回はCloud Firestoreを利用してチャットアプリを作ってみる

Cloud Firestore

NoSQLドキュメント指向データベースらしい。

firebase.google.com

ちなみに NoSQLっていうのはNot Only SQLの略で、SQL言語を利用しないDBのこと。Realmとかそうですね。 ドキュメント指向というのはSQLにおけるテーブルとかカラムみたいな管理ではなく、自由なデータ構造を持つドキュメントを一つの単位として持ちます。 そのためSQLと違い事前にデータ構造を決めておく必要がないみたいですね。すごい。

サンプルを組んでみる

codeLabあった!やった!

codelabs.developers.google.com

build.gradleに依存関係を追加

    // Firestore
    implementation 'com.google.firebase:firebase-firestore:17.1.5'

とりあえずcollectionに追加してみる。

FireBaseFireStoreのインスタンスを取得して、対象のcollectionにaddするだけ。簡単。

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // FireBaseFireStore: すべてのFireBaseデータベースのエントリポイント。
        val firebase = FirebaseFirestore.getInstance()

        // コレクションに追加するオブジェクト(ドキュメント)
        val user = User("testiD")

        // collection:users にuserオブジェクト(ドキュメント)を追加
        firebase.collection("users").add(user)

    }
}

f:id:off2white:20190122103602p:plain

なんて簡単なんや。。

FireStoreのデータを監視する。

これも簡単。

  1. 対象のCollectionのQueryを取得する。
  2. EventListenerをimplement、onEventを実装する。
  3. QueryにaddSnapshotListenerを設定する。

とりあえず適当にActivityに書いてみる。

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)


        // FireBaseFireStore: すべてのFireBaseデータベースのエントリポイント。
        val firebase = FirebaseFirestore.getInstance()
        findViewById<Button>(R.id.button).setOnClickListener {
            // コレクションに追加するオブジェクト(ドキュメント)
            val user = User("testiDBbcb")

            // collection:users にuserオブジェクト(ドキュメント)を追加
            firebase.collection("testUsers").add(user)
        }

        // Queryを作成する。collectionはcollectionReferenceを返しているんだけど、
        // collectionReferenceはQueryを継承しているクラスなので、Queryのメソッドが利用できる。
        val query = firebase.collection("testUsers")

        // Snapshotのリスナーを設定する。
        query.addSnapshotListener(this)

    }

    override fun onEvent(querySnapshot: QuerySnapshot?, firebaseFirestoreExeption: FirebaseFirestoreException?) {

        // nullableなので念の為。
        if (querySnapshot == null) {
            return
        }

        querySnapshot.documentChanges
                .forEach {
                    // typeは3つ。追加か変更か削除
                    when (it.type) {
                        DocumentChange.Type.ADDED -> {
                            val data = it.document.data
                            findViewById<TextView>(R.id.textView).text = data["userId"].toString().plus(" ADDED")
                        }
                        DocumentChange.Type.MODIFIED -> {
                            val data = it.document.data
                            findViewById<TextView>(R.id.textView).text = data["userId"].toString().plus(" MODIFERD")
                        }
                        DocumentChange.Type.REMOVED -> {
                            findViewById<TextView>(R.id.textView).text = "Remove"
                        }
                        else -> {
                            findViewById<TextView>(R.id.textView).text = "else"
                        }
                    }
                }
    }
}


ちょっとFluxベースで整理してみる。

3日くらいかかった。。。
サンプルコードあげるつもりだったけど、Google API Keyあるし載せられなかった。辛い。

感想

  • そこらへんのBaasよりすごい使いやすかった。
  • ただなんかFirebase側をみても更新されていない時があって、何でだろうってなった。タイミングかな。。