From 2d4d5b3a86e989917eccf22e8f470c8433191d0c Mon Sep 17 00:00:00 2001 From: Matthew Dolan Date: Mon, 22 Feb 2021 16:33:27 +0000 Subject: [PATCH] Add simple sample app (#38) Fixes #15 --- build.gradle.kts | 3 + buildSrc/src/main/kotlin/Versions.kt | 14 ++- samples/androidApp/build.gradle.kts | 113 +++++++++++++++++ .../androidApp/src/debug/AndroidManifest.xml | 10 ++ .../samples/test/HiltTestActivity.kt | 23 ++++ .../androidApp/src/main/AndroidManifest.xml | 27 +++++ .../src/main/ic_launcher-playstore.png | Bin 0 -> 17562 bytes .../layercache/samples/MainActivity.kt | 35 ++++++ .../layercache/samples/SamplesApplication.kt | 23 ++++ .../layercache/samples/SamplesFragment.kt | 54 +++++++++ .../samples/data/LastRetrievedWrapper.kt | 31 +++++ .../database/PersonalDetailsExtensions.kt | 31 +++++ .../data/database/SqlDelightDataSource.kt | 48 ++++++++ .../samples/data/network/KtorDataSource.kt | 42 +++++++ .../network/PersonalDetailsNetworkEntity.kt | 35 ++++++ .../samples/domain/PersonalDetails.kt | 30 +++++ .../sharedprefs/SharedPrefsFragment.kt | 98 +++++++++++++++ .../samples/sharedprefs/SharedPrefsState.kt | 25 ++++ .../sharedprefs/SharedPrefsViewModel.kt | 114 ++++++++++++++++++ .../samples/sqldelight/SqlDelightFragment.kt | 88 ++++++++++++++ .../samples/sqldelight/SqlDelightState.kt | 24 ++++ .../samples/sqldelight/SqlDelightViewModel.kt | 78 ++++++++++++ .../samples/ui/FragmentViewBindingDelegate.kt | 76 ++++++++++++ .../samples/ui/component/ButtonItem.kt | 53 ++++++++ .../ui/component/SingleLineTextHeaderItem.kt | 54 +++++++++ .../ui/component/SingleLineTextItem.kt | 54 +++++++++ .../samples/ui/component/TwoLineTextItem.kt | 57 +++++++++ .../res/drawable/ic_launcher_foreground.xml | 15 +++ .../src/main/res/layout/button_item.xml | 20 +++ .../src/main/res/layout/main_activity.xml | 11 ++ .../res/layout/recycler_view_fragment.xml | 16 +++ .../layout/single_line_text_header_item.xml | 22 ++++ .../main/res/layout/single_line_text_item.xml | 22 ++++ .../main/res/layout/two_line_text_item.xml | 45 +++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 1877 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 3864 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1312 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2426 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 2596 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 5501 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 3980 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 8490 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 5507 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 12272 bytes .../src/main/res/navigation/nav_graph.xml | 26 ++++ .../androidApp/src/main/res/values/colors.xml | 6 + .../res/values/ic_launcher_background.xml | 4 + .../src/main/res/values/strings.xml | 4 + .../androidApp/src/main/res/values/styles.xml | 11 ++ .../main/res/xml/network_security_config.xml | 8 ++ .../samples/data/database/PersonalDetails.sq | 17 +++ settings.gradle.kts | 6 +- 54 files changed, 1480 insertions(+), 3 deletions(-) create mode 100644 samples/androidApp/build.gradle.kts create mode 100644 samples/androidApp/src/debug/AndroidManifest.xml create mode 100644 samples/androidApp/src/debug/kotlin/com/appmattus/layercache/samples/test/HiltTestActivity.kt create mode 100644 samples/androidApp/src/main/AndroidManifest.xml create mode 100644 samples/androidApp/src/main/ic_launcher-playstore.png create mode 100644 samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/MainActivity.kt create mode 100644 samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/SamplesApplication.kt create mode 100644 samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/SamplesFragment.kt create mode 100644 samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/data/LastRetrievedWrapper.kt create mode 100644 samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/data/database/PersonalDetailsExtensions.kt create mode 100644 samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/data/database/SqlDelightDataSource.kt create mode 100644 samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/data/network/KtorDataSource.kt create mode 100644 samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/data/network/PersonalDetailsNetworkEntity.kt create mode 100644 samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/domain/PersonalDetails.kt create mode 100644 samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/sharedprefs/SharedPrefsFragment.kt create mode 100644 samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/sharedprefs/SharedPrefsState.kt create mode 100644 samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/sharedprefs/SharedPrefsViewModel.kt create mode 100644 samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/sqldelight/SqlDelightFragment.kt create mode 100644 samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/sqldelight/SqlDelightState.kt create mode 100644 samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/sqldelight/SqlDelightViewModel.kt create mode 100644 samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/ui/FragmentViewBindingDelegate.kt create mode 100644 samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/ui/component/ButtonItem.kt create mode 100644 samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/ui/component/SingleLineTextHeaderItem.kt create mode 100644 samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/ui/component/SingleLineTextItem.kt create mode 100644 samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/ui/component/TwoLineTextItem.kt create mode 100644 samples/androidApp/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 samples/androidApp/src/main/res/layout/button_item.xml create mode 100644 samples/androidApp/src/main/res/layout/main_activity.xml create mode 100644 samples/androidApp/src/main/res/layout/recycler_view_fragment.xml create mode 100644 samples/androidApp/src/main/res/layout/single_line_text_header_item.xml create mode 100644 samples/androidApp/src/main/res/layout/single_line_text_item.xml create mode 100644 samples/androidApp/src/main/res/layout/two_line_text_item.xml create mode 100644 samples/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 samples/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 samples/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 samples/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 samples/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 samples/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 samples/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 samples/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 samples/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 samples/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 samples/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 samples/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 samples/androidApp/src/main/res/navigation/nav_graph.xml create mode 100644 samples/androidApp/src/main/res/values/colors.xml create mode 100644 samples/androidApp/src/main/res/values/ic_launcher_background.xml create mode 100644 samples/androidApp/src/main/res/values/strings.xml create mode 100644 samples/androidApp/src/main/res/values/styles.xml create mode 100644 samples/androidApp/src/main/res/xml/network_security_config.xml create mode 100644 samples/androidApp/src/main/sqldelight/com/appmattus/layercache/samples/data/database/PersonalDetails.sq diff --git a/build.gradle.kts b/build.gradle.kts index e189969..4d418ad 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -36,6 +36,9 @@ buildscript { } dependencies { classpath("com.android.tools.build:gradle:${Versions.androidGradlePlugin}") + classpath("com.google.dagger:hilt-android-gradle-plugin:${Versions.Google.dagger}") + classpath("androidx.navigation:navigation-safe-args-gradle-plugin:${Versions.AndroidX.navigation}") + classpath("com.squareup.sqldelight:gradle-plugin:${Versions.sqlDelight}") } } diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index fe857fc..c33ca6b 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -30,7 +30,11 @@ object Versions { const val coroutines = "1.4.2" const val diskLruCache = "2.0.2" const val ehcache = "3.9.1" + const val groupie = "2.9.0" + const val ktor = "1.5.1" + const val orbitMvi = "3.0.1" const val slf4j = "1.7.30" + const val sqlDelight = "1.4.4" const val systemRules = "1.19.0" const val junit4 = "4.13.2" @@ -40,10 +44,16 @@ object Versions { const val retrofit = "2.9.0" const val robolectric = "4.5.1" - object AndroidX { + const val desugar = "1.1.1" + object AndroidX { const val annotation = "1.1.0" + const val appCompat = "1.2.0" + const val constraintLayout = "2.1.0-alpha2" + const val fragment = "1.3.0" + const val hilt = "1.0.0-alpha02" const val lifecycle = "2.3.0" + const val navigation = "2.3.3" const val multidex = "2.0.1" const val securityCrypto = "1.1.0-alpha03" @@ -53,6 +63,8 @@ object Versions { } object Google { + const val dagger = "2.31.1-alpha" + const val material = "1.3.0-rc01" const val tink = "1.5.0" } diff --git a/samples/androidApp/build.gradle.kts b/samples/androidApp/build.gradle.kts new file mode 100644 index 0000000..1731df8 --- /dev/null +++ b/samples/androidApp/build.gradle.kts @@ -0,0 +1,113 @@ +/* + * Copyright 2021 Appmattus Limited + * + * 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. + */ + +plugins { + id("com.android.application") + kotlin("android") + id("kotlin-parcelize") + id("com.squareup.sqldelight") + kotlin("kapt") + id("androidx.navigation.safeargs.kotlin") + kotlin("plugin.serialization") +} + +apply(plugin = "dagger.hilt.android.plugin") + +dependencies { + + implementation(project(":layercache")) + implementation(project(":layercache-android")) + implementation(project(":layercache-android-encryption")) + + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutines}") + + // Architecture + implementation("androidx.fragment:fragment-ktx:${Versions.AndroidX.fragment}") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:${Versions.AndroidX.lifecycle}") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.AndroidX.lifecycle}") + implementation("androidx.lifecycle:lifecycle-common-java8:${Versions.AndroidX.lifecycle}") + implementation("androidx.navigation:navigation-fragment-ktx:${Versions.AndroidX.navigation}") + implementation("androidx.navigation:navigation-ui-ktx:${Versions.AndroidX.navigation}") + implementation("org.orbit-mvi:orbit-viewmodel:${Versions.orbitMvi}") + + // UI + implementation("com.google.android.material:material:${Versions.Google.material}") + implementation("androidx.appcompat:appcompat:${Versions.AndroidX.appCompat}") + implementation("androidx.constraintlayout:constraintlayout:${Versions.AndroidX.constraintLayout}") + implementation("com.xwray:groupie:${Versions.groupie}") + implementation("com.xwray:groupie-viewbinding:${Versions.groupie}") + + implementation("io.ktor:ktor-client-core:${Versions.ktor}") + implementation("io.ktor:ktor-client-serialization:${Versions.ktor}") + implementation("io.ktor:ktor-client-serialization-jvm:${Versions.ktor}") + implementation("io.ktor:ktor-client-android:${Versions.ktor}") + + // Database + implementation("com.squareup.sqldelight:runtime:${Versions.sqlDelight}") + implementation("com.squareup.sqldelight:android-driver:${Versions.sqlDelight}") + + // Dependency Injection + implementation("androidx.hilt:hilt-lifecycle-viewmodel:${Versions.AndroidX.hilt}") + kapt("androidx.hilt:hilt-compiler:${Versions.AndroidX.hilt}") + implementation("com.google.dagger:hilt-android:${Versions.Google.dagger}") + kapt("com.google.dagger:hilt-android-compiler:${Versions.Google.dagger}") + + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:${Versions.desugar}") +} + +android { + compileSdkVersion(30) + defaultConfig { + applicationId = "com.appmattus.layercache.samples" + minSdkVersion(21) + targetSdkVersion(30) + versionCode = 1 + versionName = "1.0" + vectorDrawables.useSupportLibrary = true + } + buildTypes { + getByName("release") { + isMinifyEnabled = false + } + } + + compileOptions { + isCoreLibraryDesugaringEnabled = true + + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + + buildFeatures { + buildConfig = true + viewBinding = true + } + + sourceSets.all { + java.srcDir("src/$name/kotlin") + } +} + +sqldelight { + database("AppDatabase") { + packageName = "com.appmattus.layercache.samples.data.database" + } +} diff --git a/samples/androidApp/src/debug/AndroidManifest.xml b/samples/androidApp/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..ea3306a --- /dev/null +++ b/samples/androidApp/src/debug/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/samples/androidApp/src/debug/kotlin/com/appmattus/layercache/samples/test/HiltTestActivity.kt b/samples/androidApp/src/debug/kotlin/com/appmattus/layercache/samples/test/HiltTestActivity.kt new file mode 100644 index 0000000..2b10bad --- /dev/null +++ b/samples/androidApp/src/debug/kotlin/com/appmattus/layercache/samples/test/HiltTestActivity.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2019 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.appmattus.layercache.samples.test + +import androidx.appcompat.app.AppCompatActivity +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class HiltTestActivity : AppCompatActivity() diff --git a/samples/androidApp/src/main/AndroidManifest.xml b/samples/androidApp/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0ab3a34 --- /dev/null +++ b/samples/androidApp/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + diff --git a/samples/androidApp/src/main/ic_launcher-playstore.png b/samples/androidApp/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..5da7da63b5feca9ea82a08b0d680ca51eaa9e662 GIT binary patch literal 17562 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 F{{R(+?N() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.recyclerView.apply { + layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false) + adapter = GroupAdapter().apply { + add(SingleLineTextHeaderItem("Samples")) + add(SingleLineTextItem("Encrypted shared preferences") { + findNavController().navigate(R.id.action_samplesFragment_to_sharedPrefsFragment) + }) + add(SingleLineTextItem("SqlDelight") { + findNavController().navigate(R.id.action_samplesFragment_to_sqlDelightFragment) + }) + } + } + } +} diff --git a/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/data/LastRetrievedWrapper.kt b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/data/LastRetrievedWrapper.kt new file mode 100644 index 0000000..325ac9b --- /dev/null +++ b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/data/LastRetrievedWrapper.kt @@ -0,0 +1,31 @@ +package com.appmattus.layercache.samples.data + +import com.appmattus.layercache.Cache + +/** + * Class used to wrap a cache just so we can figure out from composing two caches where data originated from + */ +class LastRetrievedWrapper { + // Stores the name of the cache data was returned from + var lastRetrieved: String? = null + + fun reset() { + lastRetrieved = null + } + + // A simple Cache wrapper to update lastRetrieved when the cache returns a value from its get function + fun Cache.wrap(name: String): Cache { + val delegate = this + return object : Cache { + override suspend fun get(key: K): V? = delegate.get(key)?.also { + if (lastRetrieved == null) { + lastRetrieved = name + } + } + + override suspend fun set(key: K, value: V) = delegate.set(key, value) + override suspend fun evict(key: K) = delegate.evict(key) + override suspend fun evictAll() = delegate.evictAll() + } + } +} diff --git a/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/data/database/PersonalDetailsExtensions.kt b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/data/database/PersonalDetailsExtensions.kt new file mode 100644 index 0000000..972de9d --- /dev/null +++ b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/data/database/PersonalDetailsExtensions.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2021 Appmattus Limited + * + * 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.appmattus.layercache.samples.data.database + +internal fun PersonalDetails.toDomainEntity() = com.appmattus.layercache.samples.domain.PersonalDetails( + name = name, + tagline = tagline, + location = location, + avatarUrl = avatarUrl +) + +internal fun com.appmattus.layercache.samples.domain.PersonalDetails.toDatabaseEntity() = PersonalDetails( + name = name, + tagline = tagline, + location = location, + avatarUrl = avatarUrl +) diff --git a/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/data/database/SqlDelightDataSource.kt b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/data/database/SqlDelightDataSource.kt new file mode 100644 index 0000000..795cff5 --- /dev/null +++ b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/data/database/SqlDelightDataSource.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2021 Appmattus Limited + * + * 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.appmattus.layercache.samples.data.database + +import android.content.Context +import com.appmattus.layercache.Cache +import com.appmattus.layercache.samples.domain.PersonalDetails +import com.squareup.sqldelight.android.AndroidSqliteDriver + +class SqlDelightDataSource(context: Context) : Cache { + + private val database = AppDatabase(AndroidSqliteDriver(AppDatabase.Schema, context, "cache.db")) + + override suspend fun get(key: Unit): PersonalDetails? = + database.personalDetailsQueries.personalDetails().executeAsOneOrNull()?.toDomainEntity() + + override suspend fun set(key: Unit, value: PersonalDetails) { + database.personalDetailsQueries.transaction { + with(value.toDatabaseEntity()) { + database.personalDetailsQueries.deletePersonalDetails() + database.personalDetailsQueries.insertPersonalDetails( + name = name, + tagline = tagline, + location = location, + avatarUrl = avatarUrl + ) + } + } + } + + override suspend fun evict(key: Unit) = database.personalDetailsQueries.deletePersonalDetails() + + override suspend fun evictAll() = database.personalDetailsQueries.deletePersonalDetails() +} diff --git a/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/data/network/KtorDataSource.kt b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/data/network/KtorDataSource.kt new file mode 100644 index 0000000..4275c9e --- /dev/null +++ b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/data/network/KtorDataSource.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2021 Appmattus Limited + * + * 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.appmattus.layercache.samples.data.network + +import com.appmattus.layercache.Fetcher +import com.appmattus.layercache.samples.domain.PersonalDetails +import io.ktor.client.HttpClient +import io.ktor.client.features.json.JsonFeature +import io.ktor.client.features.json.serializer.KotlinxSerializer +import io.ktor.client.request.get +import io.ktor.http.ContentType +import io.ktor.http.Url +import io.ktor.http.contentType + +class KtorDataSource( + private val client: HttpClient = HttpClient { + install(JsonFeature) { + serializer = KotlinxSerializer() + } + } +) : Fetcher { + + override suspend fun get(key: Unit): PersonalDetails { + return client.get(Url("https://mattdolan.com/cv/api/personal-details.json")) { + contentType(ContentType.Application.Json) + }.toDomainEntity() + } +} diff --git a/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/data/network/PersonalDetailsNetworkEntity.kt b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/data/network/PersonalDetailsNetworkEntity.kt new file mode 100644 index 0000000..0e4a9ad --- /dev/null +++ b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/data/network/PersonalDetailsNetworkEntity.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2021 Appmattus Limited + * + * 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.appmattus.layercache.samples.data.network + +import com.appmattus.layercache.samples.domain.PersonalDetails +import kotlinx.serialization.Serializable + +@Serializable +internal data class PersonalDetailsNetworkEntity( + val name: String, + val tagline: String, + val location: String, + val avatarUrl: String +) { + fun toDomainEntity(): PersonalDetails = PersonalDetails( + name = name, + tagline = tagline, + location = location, + avatarUrl = avatarUrl + ) +} diff --git a/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/domain/PersonalDetails.kt b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/domain/PersonalDetails.kt new file mode 100644 index 0000000..8b5ee39 --- /dev/null +++ b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/domain/PersonalDetails.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2021 Appmattus Limited + * + * 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.appmattus.layercache.samples.domain + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable + +@Parcelize +@Serializable +data class PersonalDetails( + val name: String, + val tagline: String, + val location: String, + val avatarUrl: String +) : Parcelable diff --git a/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/sharedprefs/SharedPrefsFragment.kt b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/sharedprefs/SharedPrefsFragment.kt new file mode 100644 index 0000000..bc3a846 --- /dev/null +++ b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/sharedprefs/SharedPrefsFragment.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2021 Appmattus Limited + * + * 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.appmattus.layercache.samples.sharedprefs + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.appmattus.layercache.samples.R +import com.appmattus.layercache.samples.databinding.RecyclerViewFragmentBinding +import com.appmattus.layercache.samples.ui.component.ButtonItem +import com.appmattus.layercache.samples.ui.component.SingleLineTextHeaderItem +import com.appmattus.layercache.samples.ui.component.TwoLineTextItem +import com.appmattus.layercache.samples.ui.viewBinding +import com.xwray.groupie.GroupAdapter +import com.xwray.groupie.GroupieViewHolder +import com.xwray.groupie.Section +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class SharedPrefsFragment : Fragment(R.layout.recycler_view_fragment) { + + private val binding by viewBinding() + + private val viewModel by viewModels() + + private val actionSection = Section().apply { + add(ButtonItem("Load data") { + viewModel.loadPersonalDetails() + }) + add(ButtonItem("Clear") { + viewModel.clear() + }) + } + private val personalDetailsSection = Section() + private val preferencesSection = Section() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.recyclerView.apply { + layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false) + adapter = GroupAdapter().apply { + add(SingleLineTextHeaderItem("Encrypted shared preferences")) + add(actionSection) + add(SingleLineTextHeaderItem("Response data")) + add(personalDetailsSection) + add(SingleLineTextHeaderItem("Preferences content")) + add(preferencesSection) + } + } + + lifecycleScope.launch { + viewModel.container.stateFlow.collect(::render) + } + } + + private fun render(state: SharedPrefsState) { + state.personalDetails?.let { + listOf( + TwoLineTextItem( + primaryText = it.name, + secondaryText = it.tagline + ), + TwoLineTextItem( + primaryText = "Retrieved from", + secondaryText = state.loadedFrom + ) + ).let(personalDetailsSection::update) + } ?: personalDetailsSection.clear() + + state.preferences.map { + TwoLineTextItem( + primaryText = it.key, + secondaryText = it.value.toString() + ) + }.let(preferencesSection::update) + } +} diff --git a/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/sharedprefs/SharedPrefsState.kt b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/sharedprefs/SharedPrefsState.kt new file mode 100644 index 0000000..0376711 --- /dev/null +++ b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/sharedprefs/SharedPrefsState.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2021 Appmattus Limited + * + * 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.appmattus.layercache.samples.sharedprefs + +import com.appmattus.layercache.samples.domain.PersonalDetails + +data class SharedPrefsState( + val personalDetails: PersonalDetails? = null, + val preferences: Map = emptyMap(), + val loadedFrom: String = "" +) diff --git a/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/sharedprefs/SharedPrefsViewModel.kt b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/sharedprefs/SharedPrefsViewModel.kt new file mode 100644 index 0000000..26319f2 --- /dev/null +++ b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/sharedprefs/SharedPrefsViewModel.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2021 Appmattus Limited + * + * 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.appmattus.layercache.samples.sharedprefs + +import android.content.Context +import androidx.lifecycle.ViewModel +import com.appmattus.layercache.Cache +import com.appmattus.layercache.asStringCache +import com.appmattus.layercache.encrypt +import com.appmattus.layercache.get +import com.appmattus.layercache.samples.data.LastRetrievedWrapper +import com.appmattus.layercache.samples.data.network.KtorDataSource +import com.appmattus.layercache.samples.domain.PersonalDetails +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.serialization.json.Json.Default.decodeFromString +import kotlinx.serialization.json.Json.Default.encodeToString +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.reduce +import org.orbitmvi.orbit.viewmodel.container +import javax.inject.Inject + +@HiltViewModel +class SharedPrefsViewModel @Inject constructor( + @ApplicationContext context: Context +) : ViewModel(), ContainerHost { + + // Stores the name of the cache data was returned from + private val lastRetrievedWrapper = LastRetrievedWrapper() + + // Network fetcher, wrapped so we can detect when get returns a value + private val ktorDataSource: Cache = with(lastRetrievedWrapper) { KtorDataSource().wrap("Ktor network call") } + + // Encrypted shared preferences cache + private val sharedPreferences = context.getSharedPreferences("encrypted", Context.MODE_PRIVATE) + private val encryptedSharedPreferencesDataSource: Cache = with(lastRetrievedWrapper) { + sharedPreferences + // Access as a Cache + .asStringCache() + // Wrap the cache so we can detect when get returns a value + .wrap("Shared preferences") + // Encrypt all keys and values stored in this cache + .encrypt(context) + // We are only storing one value so using a default key and mapping externally to Unit, i.e. cache becomes Cache + .keyTransform { + "personalDetails" + } + // Transform string values to PersonalDetails, i.e. cache becomes Cache + .valueTransform(transform = { + decodeFromString(PersonalDetails.serializer(), it) + }, inverseTransform = { + encodeToString(PersonalDetails.serializer(), it) + }) + } + + // Combine shared preferences and ktor caches, i.e. first retrieve value from shared preferences and if not available retrieve from network + private val repository = encryptedSharedPreferencesDataSource.compose(ktorDataSource) + + override val container: Container = container(SharedPrefsState()) { + loadPreferencesContent() + } + + // Update state with personal details retrieved from the repository + fun loadPersonalDetails() = intent { + lastRetrievedWrapper.reset() + + reduce { + state.copy(personalDetails = null, loadedFrom = "") + } + + val personalDetails = repository.get() + + reduce { + state.copy(personalDetails = personalDetails, loadedFrom = lastRetrievedWrapper.lastRetrieved ?: "") + } + + loadPreferencesContent() + } + + // Update the state with the current contents of shared preferences so we can demonstrate that the data is stored encrypted + private fun loadPreferencesContent() = intent { + val content = sharedPreferences.all + + reduce { + state.copy(preferences = content) + } + } + + // Clear all data from shared preferences and the state object + fun clear() = intent { + reduce { + state.copy(personalDetails = null, loadedFrom = "") + } + + sharedPreferences.edit().clear().apply() + loadPreferencesContent() + } +} diff --git a/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/sqldelight/SqlDelightFragment.kt b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/sqldelight/SqlDelightFragment.kt new file mode 100644 index 0000000..8df4400 --- /dev/null +++ b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/sqldelight/SqlDelightFragment.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2021 Appmattus Limited + * + * 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.appmattus.layercache.samples.sqldelight + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.appmattus.layercache.samples.R +import com.appmattus.layercache.samples.databinding.RecyclerViewFragmentBinding +import com.appmattus.layercache.samples.ui.component.ButtonItem +import com.appmattus.layercache.samples.ui.component.SingleLineTextHeaderItem +import com.appmattus.layercache.samples.ui.component.TwoLineTextItem +import com.appmattus.layercache.samples.ui.viewBinding +import com.xwray.groupie.GroupAdapter +import com.xwray.groupie.GroupieViewHolder +import com.xwray.groupie.Section +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class SqlDelightFragment : Fragment(R.layout.recycler_view_fragment) { + + private val binding by viewBinding() + + private val viewModel by viewModels() + + private val actionSection = Section().apply { + add(ButtonItem("Load data") { + viewModel.loadPersonalDetails() + }) + add(ButtonItem("Clear") { + viewModel.clear() + }) + } + private val personalDetailsSection = Section() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.recyclerView.apply { + layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false) + adapter = GroupAdapter().apply { + add(SingleLineTextHeaderItem("SqlDelight")) + add(actionSection) + add(SingleLineTextHeaderItem("Response data")) + add(personalDetailsSection) + } + } + + lifecycleScope.launch { + viewModel.container.stateFlow.collect(::render) + } + } + + private fun render(state: SqlDelightState) { + state.personalDetails?.let { + listOf( + TwoLineTextItem( + primaryText = it.name, + secondaryText = it.tagline + ), + TwoLineTextItem( + primaryText = "Retrieved from", + secondaryText = state.loadedFrom + ) + ).let(personalDetailsSection::update) + } ?: personalDetailsSection.clear() + } +} diff --git a/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/sqldelight/SqlDelightState.kt b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/sqldelight/SqlDelightState.kt new file mode 100644 index 0000000..aa00454 --- /dev/null +++ b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/sqldelight/SqlDelightState.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2021 Appmattus Limited + * + * 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.appmattus.layercache.samples.sqldelight + +import com.appmattus.layercache.samples.domain.PersonalDetails + +data class SqlDelightState( + val personalDetails: PersonalDetails? = null, + val loadedFrom: String = "" +) diff --git a/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/sqldelight/SqlDelightViewModel.kt b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/sqldelight/SqlDelightViewModel.kt new file mode 100644 index 0000000..53345b6 --- /dev/null +++ b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/sqldelight/SqlDelightViewModel.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2021 Appmattus Limited + * + * 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.appmattus.layercache.samples.sqldelight + +import android.content.Context +import androidx.lifecycle.ViewModel +import com.appmattus.layercache.Cache +import com.appmattus.layercache.get +import com.appmattus.layercache.samples.data.LastRetrievedWrapper +import com.appmattus.layercache.samples.data.database.SqlDelightDataSource +import com.appmattus.layercache.samples.data.network.KtorDataSource +import com.appmattus.layercache.samples.domain.PersonalDetails +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.reduce +import org.orbitmvi.orbit.viewmodel.container +import javax.inject.Inject + +@HiltViewModel +class SqlDelightViewModel @Inject constructor( + @ApplicationContext context: Context +) : ViewModel(), ContainerHost { + + // Stores the name of the cache data was returned from + private val lastRetrievedWrapper = LastRetrievedWrapper() + + // Network fetcher, wrapped so we can detect when get returns a value + private val ktorDataSource: Cache = with(lastRetrievedWrapper) { KtorDataSource().wrap("Ktor network call") } + + // Database cache, wrapped so we can detect when get returns a value + private val sqlDelightDataSource = with(lastRetrievedWrapper) { SqlDelightDataSource(context).wrap("SqlDelight database") } + + // Combine sql delight and ktor caches, i.e. first retrieve value from sql delight and if not available retrieve from network + private val repository = sqlDelightDataSource.compose(ktorDataSource) + + override val container: Container = container(SqlDelightState()) + + // Update state with personal details retrieved from the repository + fun loadPersonalDetails() = intent { + lastRetrievedWrapper.reset() + + reduce { + state.copy(personalDetails = null, loadedFrom = "") + } + + val personalDetails = repository.get() + + reduce { + state.copy(personalDetails = personalDetails, loadedFrom = lastRetrievedWrapper.lastRetrieved ?: "") + } + } + + // Clear all data from database and the state object + fun clear() = intent { + reduce { + state.copy(personalDetails = null, loadedFrom = "") + } + + sqlDelightDataSource.evictAll() + } +} diff --git a/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/ui/FragmentViewBindingDelegate.kt b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/ui/FragmentViewBindingDelegate.kt new file mode 100644 index 0000000..462b23d --- /dev/null +++ b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/ui/FragmentViewBindingDelegate.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2021 Matthew Dolan + * + * 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.appmattus.layercache.samples.ui + +import android.view.View +import androidx.fragment.app.Fragment +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.Observer +import androidx.viewbinding.ViewBinding +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +// See https://medium.com/@Zhuinden/simple-one-liner-viewbinding-in-fragments-and-activities-with-kotlin-961430c6c07c +class FragmentViewBindingDelegate( + val fragment: Fragment, + val viewBindingFactory: (View) -> T +) : ReadOnlyProperty { + private var binding: T? = null + + init { + fragment.lifecycle.addObserver(object : DefaultLifecycleObserver { + val viewLifecycleOwnerLiveDataObserver = + Observer { + val viewLifecycleOwner = it ?: return@Observer + + viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) { + binding = null + } + }) + } + + override fun onCreate(owner: LifecycleOwner) { + fragment.viewLifecycleOwnerLiveData.observeForever(viewLifecycleOwnerLiveDataObserver) + } + + override fun onDestroy(owner: LifecycleOwner) { + fragment.viewLifecycleOwnerLiveData.removeObserver(viewLifecycleOwnerLiveDataObserver) + } + }) + } + + override fun getValue(thisRef: Fragment, property: KProperty<*>): T { + val binding = binding + if (binding != null) { + return binding + } + + val lifecycle = fragment.viewLifecycleOwner.lifecycle + if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) { + throw IllegalStateException("Should not attempt to get bindings when Fragment views are destroyed.") + } + + return viewBindingFactory(thisRef.requireView()).also { this.binding = it } + } +} + +inline fun Fragment.viewBinding() = FragmentViewBindingDelegate(this) { view: View -> + T::class.java.getMethod("bind", View::class.java).invoke(null, view) as T +} diff --git a/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/ui/component/ButtonItem.kt b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/ui/component/ButtonItem.kt new file mode 100644 index 0000000..996c508 --- /dev/null +++ b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/ui/component/ButtonItem.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2021 Appmattus Limited + * + * 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.appmattus.layercache.samples.ui.component + +import android.view.View +import com.appmattus.layercache.samples.R +import com.appmattus.layercache.samples.databinding.ButtonItemBinding +import com.xwray.groupie.Item +import com.xwray.groupie.viewbinding.BindableItem + +data class ButtonItem( + val primaryText: CharSequence, + val clickListener: () -> Unit = emptyListener +) : BindableItem() { + + override fun isSameAs(other: Item<*>): Boolean = primaryText == (other as? ButtonItem)?.primaryText + + override fun hasSameContentAs(other: Item<*>): Boolean { + return primaryText == (other as? ButtonItem)?.primaryText + } + + override fun initializeViewBinding(view: View) = ButtonItemBinding.bind(view) + + override fun getLayout() = R.layout.button_item + + override fun bind(viewBinding: ButtonItemBinding, position: Int) { + viewBinding.primaryText.text = primaryText + + viewBinding.primaryText.setOnClickListener { + clickListener() + } + + viewBinding.primaryText.isEnabled = clickListener != emptyListener + } + + companion object { + private val emptyListener: () -> Unit = {} + } +} diff --git a/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/ui/component/SingleLineTextHeaderItem.kt b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/ui/component/SingleLineTextHeaderItem.kt new file mode 100644 index 0000000..01aaa12 --- /dev/null +++ b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/ui/component/SingleLineTextHeaderItem.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2021 Appmattus Limited + * + * 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.appmattus.layercache.samples.ui.component + +import android.view.View +import com.appmattus.layercache.samples.R +import com.appmattus.layercache.samples.databinding.SingleLineTextHeaderItemBinding +import com.xwray.groupie.Item +import com.xwray.groupie.viewbinding.BindableItem + +data class SingleLineTextHeaderItem( + val primaryText: CharSequence, + val clickListener: () -> Unit = emptyListener +) : BindableItem() { + + override fun isSameAs(other: Item<*>): Boolean = primaryText == (other as? SingleLineTextItem)?.primaryText + + override fun hasSameContentAs(other: Item<*>): Boolean { + return primaryText == (other as? SingleLineTextItem)?.primaryText + } + + override fun initializeViewBinding(view: View) = SingleLineTextHeaderItemBinding.bind(view) + + override fun getLayout() = R.layout.single_line_text_header_item + + override fun bind(viewBinding: SingleLineTextHeaderItemBinding, position: Int) { + viewBinding.primaryText.text = primaryText + + viewBinding.container.setOnClickListener { + clickListener() + } + if (clickListener == emptyListener) { + viewBinding.container.isClickable = false + } + } + + companion object { + private val emptyListener: () -> Unit = {} + } +} diff --git a/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/ui/component/SingleLineTextItem.kt b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/ui/component/SingleLineTextItem.kt new file mode 100644 index 0000000..449bcbd --- /dev/null +++ b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/ui/component/SingleLineTextItem.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2021 Appmattus Limited + * + * 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.appmattus.layercache.samples.ui.component + +import android.view.View +import com.appmattus.layercache.samples.R +import com.appmattus.layercache.samples.databinding.SingleLineTextItemBinding +import com.xwray.groupie.Item +import com.xwray.groupie.viewbinding.BindableItem + +data class SingleLineTextItem( + val primaryText: CharSequence, + val clickListener: () -> Unit = emptyListener +) : BindableItem() { + + override fun isSameAs(other: Item<*>): Boolean = primaryText == (other as? SingleLineTextItem)?.primaryText + + override fun hasSameContentAs(other: Item<*>): Boolean { + return primaryText == (other as? SingleLineTextItem)?.primaryText + } + + override fun initializeViewBinding(view: View) = SingleLineTextItemBinding.bind(view) + + override fun getLayout() = R.layout.single_line_text_item + + override fun bind(viewBinding: SingleLineTextItemBinding, position: Int) { + viewBinding.primaryText.text = primaryText + + viewBinding.container.setOnClickListener { + clickListener() + } + if (clickListener == emptyListener) { + viewBinding.container.isClickable = false + } + } + + companion object { + private val emptyListener: () -> Unit = {} + } +} diff --git a/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/ui/component/TwoLineTextItem.kt b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/ui/component/TwoLineTextItem.kt new file mode 100644 index 0000000..5b6d09f --- /dev/null +++ b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/ui/component/TwoLineTextItem.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2021 Appmattus Limited + * + * 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.appmattus.layercache.samples.ui.component + +import android.view.View +import com.appmattus.layercache.samples.R +import com.appmattus.layercache.samples.databinding.TwoLineTextItemBinding +import com.xwray.groupie.Item +import com.xwray.groupie.viewbinding.BindableItem + +data class TwoLineTextItem( + val primaryText: CharSequence, + val secondaryText: CharSequence, + val clickListener: () -> Unit = emptyListener +) : BindableItem() { + + override fun isSameAs(other: Item<*>): Boolean = primaryText == (other as? TwoLineTextItem)?.primaryText + + override fun hasSameContentAs(other: Item<*>): Boolean { + return primaryText == (other as? TwoLineTextItem)?.primaryText && + secondaryText == (other as? TwoLineTextItem)?.secondaryText + } + + override fun initializeViewBinding(view: View) = TwoLineTextItemBinding.bind(view) + + override fun getLayout() = R.layout.two_line_text_item + + override fun bind(viewBinding: TwoLineTextItemBinding, position: Int) { + viewBinding.primaryText.text = primaryText + viewBinding.secondaryText.text = secondaryText + + viewBinding.container.setOnClickListener { + clickListener() + } + if (clickListener == emptyListener) { + viewBinding.container.isClickable = false + } + } + + companion object { + private val emptyListener: () -> Unit = {} + } +} diff --git a/samples/androidApp/src/main/res/drawable/ic_launcher_foreground.xml b/samples/androidApp/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..37b6cfd --- /dev/null +++ b/samples/androidApp/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/samples/androidApp/src/main/res/layout/button_item.xml b/samples/androidApp/src/main/res/layout/button_item.xml new file mode 100644 index 0000000..bf219f3 --- /dev/null +++ b/samples/androidApp/src/main/res/layout/button_item.xml @@ -0,0 +1,20 @@ + + + +