こんにちは!Sansan事業部 プロダクト開発部のふるしんです。
私は大阪のオフィスでSansanプロダクトのAndroidアプリの開発に従事しています。
この記事はJetpack DataStore入門〜Preferences DataStore実装編〜 の連載として、Proto DataStoreの実装を見ていきます。
Preferences DataStoreに関しては以前の記事を御覧ください。
buildersbox.corp-sansan.com
Jetpack DataStoreとは?
SharedPreferencesに代わるものとして提案されている、新しいデータの永続化の方法です。
公式ドキュメントによると、SharedPreferencesから置き換えることを考えてね、と書かれていました。
DataStoreは、基本的にはSharedPreferencesの欠点を補うために提案されているようです。
ですので例えば動作はKotlin CotourinesのFlowに則って動作して非同期で走ってくれたりと、他にも素敵だなと思う点がいつくかありました。
今回はそんな点を紹介できたらなと思います。
DataStoreと一言で言っても実は2種類あります。
Preferences DataStoreと、Proto DataStoreです。
本記事ではProto DataStoreに関して見ていきます。
Proto DataStore
Preferences DataStoreとの違いは「Protocol Buffersを使う」部分です。
Preferences DataStoreではほぼプリミティブ型しか格納できなかったものが、Proto DataStoreではProto Buffersを活用することで自由な型を格納できるようになります。
この `自由な型を格納できるようになる` が嬉しい人は多いのではないでしょうか。
従来のSharedPrefernecesだとプリミティブ型しか格納できなかったためにわざわざDBのテーブルを用意していたものが、サラッと格納できるようになるのは大きな利点だと思います。
では実際にどうやって使うのか、コードを追って説明していきます。
build.gradleに追加
本記事の執筆時点では 1.0.0-beta01が最新でした。
また、後述しますがlifecycleScopeも必要なので追加します。
app/build.gradle
dependencies { // DataStore implementation "androidx.datastore:datastore:1.0.0-beta01" implementation "androidx.datastore:datastore-core:1.0.0-beta01" // Protocol Buffers implementation 'com.google.protobuf:protobuf-lite:3.0.1' // lifecycleScope implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1" }
Protocol Buffersを使えるようにする
次に、Protocol Buffersを使えるように設定をbuild.gradleに追加します。
Protocol Buffersってなんぞ?という方はGoogleのドキュメントを御覧ください。
developers.google.com
app/build.gradle
protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.0.0' } plugins { javalite { artifact = "com.google.protobuf:protoc-gen-javalite:3.0.0" } } generateProtoTasks { all().each { task -> task.plugins { javalite {} } } } }
Schemaを定義
実際に格納したい型となるSchemaを定義します。
ここで定義したものが実際にクラスとして生成されるのはビルドしたタイミングです。
編集したらリビルドすることを忘れないようにしましょう。
本記事ではサンプルとして、Userを定義します。
syntax = "proto3"; option java_package = "net.furusin.www.jetpackdatastoresample"; option java_multiple_files = true; message User { int32 age = 1; string name = 2; }
Serializerを準備
「Schemaを定義」で定義したものを使ってDataStoreへ書き込み/読み取りできるようにするための `Serializer` を定義します。
Serializerとは、定義した型をどのように格納するのか、取得するのかを定義するものとなります。
private const val DATA_STORE_FILE_NAME = "users.pb" object UserSerializer : Serializer<User> { override val defaultValue: User = User.getDefaultInstance() // データの読み取り override suspend fun readFrom(input: InputStream): User { try { return User.parseFrom(input) } catch (exception: InvalidProtocolBufferException) { throw CorruptionException("Cannot read proto.", exception) } } // データの書き込み override suspend fun writeTo(t: User, output: OutputStream) = t.writeTo(output) // UserのDataStoreのインスタンスを生成するための拡張関数 val Context.userDataStore: DataStore<User> by dataStore( fileName = DATA_STORE_FILE_NAME, serializer = UserSerializer ) }
readFromが読み取り, writeToメソッドが書き込みの方法をそれぞれ定義しています。*1
公式ドキュメントのサンプルではContextの拡張関数としてDataStoreのインスタンスを生成するようになっていましたのでここでもそうしてみました。
「Contextに拡張関数を生やすの…?」と少し疑問に思いましたが、 dataStoreメソッドの先で呼んでいるDataStoreSingletonDelegateでContextが必要なので、Contextに生やすのは適切なようです。
実際に使ってみる
それでは実際に呼び出すところを見ていきましょう。
SerializerのところでContextに拡張関数を生やしたので、Activityから触ってみます。
DataStoreインスタンスを用意する
まずはDataStoreのインスタンスを用意します。
class MainActivity : AppCompatActivity() { private lateinit var dataStore: DataStore<User> override fun onCreate(savedInstanceState: Bundle?) { // ... dataStore = userDataStore }
このdataStoreを使って、データの書き込み/読み取りを行います。
dataStore = userDataStore
だけで済んでいるのは、先ほどContextの拡張関数を作ったおかげです。
データを書き込む
まずは書き込む方法です。
private suspend fun setUserAge(age: Int) { dataStore.updateData { currentUser: User -> currentUser.toBuilder() .setAge(age) .build() } }
データを書き込む際は、DataStoreの `updateData` メソッドを使います。
最初に用意したSchemaでsetterが用意されるので活用します。
データを読み取る
次にデータを読み取る方法です。
GlobalScope.launch { dataStore.data.collect { Log.d("test", "age = ${it.age}, name = ${it.name}") } }
DataStore インターフェースが持つ変数のdataはFlowになっています。
collectで格納されているデータが取得可能です。
簡単なまとめ
サンプルのActivityの全体はこうなりました
class MainActivity : AppCompatActivity() { private lateinit var dataStore: DataStore<User> override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) dataStore = userDataStore findViewById<Button>(R.id.saveAgeButton).setOnClickListener { lifecycleScope.launch { setUserAge(20) } } findViewById<Button>(R.id.loadButton).setOnClickListener { lifecycleScope.launch { dataStore.data.collect { Log.d("test", "age = ${it.age}, name = ${it.name}") } } } } private suspend fun setUserAge(age: Int) { dataStore.updateData { currentUser: User -> currentUser.toBuilder() .setAge(age) .build() } } }
とても簡単にデータの書き込み/読み取りができました。
*1:以前の私の登壇資料ではSuspend関数ではなかったのですが、alphaから更新のタイミングでSuspend関数になったようです。素晴らしいですね