[go: nahoru, domu]

Datastore sample app that uses a compose UI.

WIP - Fix dependencies, maybe use more sophisticated view model code, add tests.  Looking for feedback on how much to comment, style, etc.

Test: Ran manual testing on device.  Automated testing WIP, pending feedback.

Change-Id: I9c5def2ff5bc6686ee3485e2c5d193ce48048dbd
diff --git a/datastore/datastore-compose-samples/build.gradle b/datastore/datastore-compose-samples/build.gradle
new file mode 100644
index 0000000..d7c2c16
--- /dev/null
+++ b/datastore/datastore-compose-samples/build.gradle
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.application")
+    id("org.jetbrains.kotlin.android")
+    id("com.google.protobuf")
+    alias(libs.plugins.kotlinSerialization)
+}
+
+dependencies {
+
+    implementation(libs.protobufLite)
+    implementation(libs.kotlinStdlib)
+
+    implementation('androidx.core:core-ktx:1.7.0')
+    implementation('androidx.lifecycle:lifecycle-runtime-ktx:2.3.1')
+    implementation('androidx.activity:activity-compose:1.3.1')
+    implementation("androidx.compose.ui:ui:1.0.0")
+    implementation("androidx.compose.ui:ui-tooling-preview:1.0.0")
+    implementation("androidx.compose.material:material:1.0.0")
+    testImplementation('junit:junit:4.13.2')
+    debugImplementation("androidx.compose.ui:ui-tooling:1.1.0-rc01")
+    debugImplementation("androidx.compose.ui:ui-test-manifest:1.1.0-rc01")
+
+    implementation("androidx.datastore:datastore-preferences:1.0.0")
+    implementation("com.google.protobuf:protobuf-javalite:3.19.4")
+
+    // For kotlin serialization
+    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1")
+}
+android {
+    namespace 'com.example.datastorecomposesamples'
+    defaultConfig {
+        minSdk 28
+    }
+    buildFeatures {
+        compose true
+    }
+    composeOptions {
+        kotlinCompilerExtensionVersion '1.1.0-rc02'
+    }
+}
+
+protobuf {
+    protoc {
+        artifact = "com.google.protobuf:protoc:3.19.4"
+    }
+
+    // Generates the java Protobuf-lite code for the Protobufs in this project. See
+    // https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
+    // for more informatio
+    generateProtoTasks {
+        all().each { task ->
+            task.builtins {
+                java {
+                    option 'lite'
+                }
+            }
+        }
+    }
+}
+// Allow usage of Kotlin's @OptIn.
+tasks.withType(KotlinCompile).configureEach {
+    kotlinOptions {
+        freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"]
+    }
+}
diff --git a/datastore/datastore-compose-samples/src/main/AndroidManifest.xml b/datastore/datastore-compose-samples/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..3d1b5aa
--- /dev/null
+++ b/datastore/datastore-compose-samples/src/main/AndroidManifest.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <application
+        android:allowBackup="true"
+        android:label="@string/app_name"
+        android:supportsRtl="true"
+        android:theme="@style/Theme.Androidx">
+        <activity
+            android:name=".CountActivity"
+            android:exported="true"
+            android:label="@string/app_name"
+            android:theme="@style/Theme.Androidx">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
\ No newline at end of file
diff --git a/datastore/datastore-compose-samples/src/main/java/com/example/datastorecomposesamples/CountActivity.kt b/datastore/datastore-compose-samples/src/main/java/com/example/datastorecomposesamples/CountActivity.kt
new file mode 100644
index 0000000..ec8bea7
--- /dev/null
+++ b/datastore/datastore-compose-samples/src/main/java/com/example/datastorecomposesamples/CountActivity.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.datastorecomposesamples
+
+import android.os.Bundle
+import android.os.StrictMode
+
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material.Button
+import androidx.compose.material.Divider
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import com.example.datastorecomposesamples.data.CountRepository
+import com.example.datastorecomposesamples.data.CountState
+
+/**
+ * Main activity for displaying the counts, and allowing them to be changed.
+ */
+class CountActivity : ComponentActivity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        val repo = CountRepository.getInstance(applicationContext)
+
+        // Strict mode allows us to check that no writes or reads are blocking the UI thread.
+        StrictMode.setThreadPolicy(
+            StrictMode.ThreadPolicy.Builder()
+                .detectDiskReads()
+                .detectDiskWrites()
+                .penaltyDeath()
+                .build()
+        )
+
+        setContent {
+            val coroutineScope = rememberCoroutineScope()
+            val countState: CountState by repo.countStateFlow.collectAsState(
+                CountState(0),
+                coroutineScope.coroutineContext
+            )
+            val countProtoState: CountState by repo.countProtoStateFlow.collectAsState(
+                CountState(0),
+                coroutineScope.coroutineContext
+            )
+            MaterialTheme {
+                // A surface container using the 'background' color from the theme
+                Surface(
+                    color = MaterialTheme.colors.background
+                ) {
+                    Column {
+                        Counters(
+                            title = getString(R.string.preference_counter),
+                            count = countState.count,
+                            >
+                            >
+                        )
+                        Divider()
+                        Counters(
+                            title = getString(R.string.proto_counter),
+                            count = countProtoState.count,
+                            >
+                            >
+                        )
+                    }
+                }
+            }
+        }
+    }
+}
+
+@Composable
+fun Counters(title: String, count: Int, onIncrement: () -> Unit, onDecrement: () -> Unit) {
+    Column(horizontalAlignment = Alignment.CenterHorizontally) {
+
+        Text(title, fontWeight = FontWeight.Bold)
+        Row(
+            modifier = Modifier.fillMaxWidth(),
+            horizontalArrangement = Arrangement.SpaceEvenly,
+        ) {
+            Button( {
+                Text(stringResource(id = R.string.count_minus))
+            }
+            Text(text = "${stringResource(R.string.count_colon)} $count")
+            Button( {
+                Text(stringResource(id = R.string.count_plus))
+            }
+        }
+    }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun DefaultPreview() {
+    MaterialTheme {
+        Counters("test", 1, {}, {})
+    }
+}
\ No newline at end of file
diff --git a/datastore/datastore-compose-samples/src/main/java/com/example/datastorecomposesamples/data/CountRepository.kt b/datastore/datastore-compose-samples/src/main/java/com/example/datastorecomposesamples/data/CountRepository.kt
new file mode 100644
index 0000000..52a3ec0
--- /dev/null
+++ b/datastore/datastore-compose-samples/src/main/java/com/example/datastorecomposesamples/data/CountRepository.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.datastorecomposesamples.data
+
+import android.content.Context
+import android.util.Log
+import androidx.datastore.core.DataStore
+import androidx.datastore.core.DataStoreFactory
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.emptyPreferences
+import androidx.datastore.preferences.core.intPreferencesKey
+import androidx.datastore.preferences.preferencesDataStore
+import com.example.datastorecomposesamples.CountPreferences
+import java.io.File
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.map
+import java.io.IOException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.launch
+
+data class CountState(val count: Int)
+
+private const val COUNT_STATE_NAME = "count_state"
+
+private val Context.preferencesDataStore by preferencesDataStore(
+    name = COUNT_STATE_NAME
+)
+
+/**
+ * Repository class for managing the DataStores.
+ */
+class CountRepository private constructor(
+    private val dataStore: DataStore<Preferences>,
+    private val protoDataStore: DataStore<CountPreferences>
+) {
+
+    companion object {
+        private val PROTO_STORE_FILE_NAME = "datastore_compose_test_app.pb"
+        private var instance: CountRepository? = null
+
+        fun getInstance(context: Context): CountRepository {
+            instance?.let { return it }
+            synchronized(this) {
+                instance?.let { return it }
+                val protoDataStore =
+                    DataStoreFactory.create(serializer = CountSerializer) {
+                        File(context.filesDir, PROTO_STORE_FILE_NAME)
+                    }
+                return CountRepository(context.preferencesDataStore, protoDataStore).also {
+                    instance = it
+                }
+            }
+        }
+    }
+
+    private val TAG: String = "CountStateRepo"
+    private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
+
+    private object PreferencesKeys {
+        val COUNT = intPreferencesKey("count")
+    }
+
+    val countStateFlow: Flow<CountState> = dataStore.data
+        .catch { exception ->
+            if (exception is IOException) {
+                Log.e(TAG, "Error reading preferences.", exception)
+                emit(emptyPreferences())
+            } else {
+                throw exception
+            }
+        }.map { preferences ->
+            CountState(preferences[PreferencesKeys.COUNT] ?: 0)
+        }
+
+    val countProtoStateFlow: Flow<CountState> = protoDataStore.data
+        .catch { exception ->
+            if (exception is IOException) {
+                Log.e(TAG, "Error reading proto.", exception)
+                emit(CountPreferences.getDefaultInstance())
+            } else {
+                throw exception
+            }
+        }.map { proto ->
+            CountState(proto.count)
+        }
+
+    fun incrementPreferenceCount() {
+        scope.launch {
+            dataStore.edit { preferences ->
+                val count = preferences[PreferencesKeys.COUNT] ?: 0
+                preferences[PreferencesKeys.COUNT] = count + 1
+            }
+        }
+    }
+
+    fun decrementPreferenceCount() {
+        scope.launch {
+            dataStore.edit { preferences ->
+                val count = preferences[PreferencesKeys.COUNT] ?: 0
+                preferences[PreferencesKeys.COUNT] = count - 1
+            }
+        }
+    }
+
+    fun incrementProtoCount() {
+        scope.launch {
+            protoDataStore.updateData { preferences ->
+                preferences.toBuilder().setCount(preferences.count + 1).build()
+            }
+        }
+    }
+
+    fun decrementProtoCount() {
+        scope.launch {
+            protoDataStore.updateData { preferences ->
+                preferences.toBuilder().setCount(preferences.count - 1).build()
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/datastore/datastore-compose-samples/src/main/java/com/example/datastorecomposesamples/data/CountSerializer.kt b/datastore/datastore-compose-samples/src/main/java/com/example/datastorecomposesamples/data/CountSerializer.kt
new file mode 100644
index 0000000..2e10667
--- /dev/null
+++ b/datastore/datastore-compose-samples/src/main/java/com/example/datastorecomposesamples/data/CountSerializer.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.datastorecomposesamples.data
+
+import androidx.datastore.core.CorruptionException
+import androidx.datastore.core.Serializer
+import androidx.datastore.preferences.protobuf.InvalidProtocolBufferException
+import com.example.datastorecomposesamples.CountPreferences
+import java.io.InputStream
+import java.io.OutputStream
+
+/**
+ * Handles converting the CountPreferences to and from an OutputStream for storing in protos.
+ */
+object CountSerializer : Serializer<CountPreferences> {
+
+    override val defaultValue: CountPreferences = CountPreferences.getDefaultInstance()
+
+    override suspend fun readFrom(input: InputStream): CountPreferences {
+        try {
+            return CountPreferences.parseFrom(input)
+        } catch (ipbe: InvalidProtocolBufferException) {
+            throw CorruptionException("Cannot read proto.", ipbe)
+        }
+    }
+
+    override suspend fun writeTo(t: CountPreferences, output: OutputStream) = t.writeTo(output)
+}
\ No newline at end of file
diff --git a/datastore/datastore-compose-samples/src/main/proto/count_prefs.proto b/datastore/datastore-compose-samples/src/main/proto/count_prefs.proto
new file mode 100644
index 0000000..6ba3596
--- /dev/null
+++ b/datastore/datastore-compose-samples/src/main/proto/count_prefs.proto
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+syntax = "proto3";
+
+option java_package = "com.example.datastorecomposesamples";
+option java_multiple_files = true;
+
+message CountPreferences {
+  int32 count = 1;
+}
\ No newline at end of file
diff --git a/datastore/datastore-compose-samples/src/main/res/values/colors.xml b/datastore/datastore-compose-samples/src/main/res/values/colors.xml
new file mode 100644
index 0000000..50a45cf
--- /dev/null
+++ b/datastore/datastore-compose-samples/src/main/res/values/colors.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources>
+    <color name="purple_200">#FFBB86FC</color>
+    <color name="purple_500">#FF6200EE</color>
+    <color name="purple_700">#FF3700B3</color>
+    <color name="teal_200">#FF03DAC5</color>
+    <color name="teal_700">#FF018786</color>
+    <color name="black">#FF000000</color>
+    <color name="white">#FFFFFFFF</color>
+</resources>
\ No newline at end of file
diff --git a/datastore/datastore-compose-samples/src/main/res/values/strings.xml b/datastore/datastore-compose-samples/src/main/res/values/strings.xml
new file mode 100644
index 0000000..354f573
--- /dev/null
+++ b/datastore/datastore-compose-samples/src/main/res/values/strings.xml
@@ -0,0 +1,24 @@
+<!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources>
+    <string name="app_name">datastore-compose-samples</string>
+    <string name="count_plus">Count++</string>
+    <string name="count_minus">Count--</string>
+    <string name="preference_counter">Preference Counter</string>
+    <string name="proto_counter">Proto Counter</string>
+    <string name="count_colon">Count:</string>
+</resources>
\ No newline at end of file
diff --git a/datastore/datastore-compose-samples/src/main/res/values/themes.xml b/datastore/datastore-compose-samples/src/main/res/values/themes.xml
new file mode 100644
index 0000000..2069d30
--- /dev/null
+++ b/datastore/datastore-compose-samples/src/main/res/values/themes.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources>
+
+    <style name="Theme.Androidx" parent="android:Theme.Material.Light.NoActionBar">
+        <item name="android:statusBarColor">@color/purple_700</item>
+    </style>
+</resources>
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index dd67423..811ac11 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -493,6 +493,7 @@
 includeProject(":customview:customview-poolingcontainer", [BuildType.MAIN, BuildType.COMPOSE])
 includeProject(":datastore:datastore", [BuildType.MAIN])
 includeProject(":datastore:datastore-core", [BuildType.MAIN])
+includeProject(":datastore:datastore-compose-samples", [BuildType.COMPOSE])
 includeProject(":datastore:datastore-preferences", [BuildType.MAIN])
 includeProject(":datastore:datastore-preferences-core", [BuildType.MAIN])
 includeProject(":datastore:datastore-preferences-proto", [BuildType.MAIN])