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 0000000..5da7da6
Binary files /dev/null and b/samples/androidApp/src/main/ic_launcher-playstore.png differ
diff --git a/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/MainActivity.kt b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/MainActivity.kt
new file mode 100644
index 0000000..ef1d81b
--- /dev/null
+++ b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/MainActivity.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
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.navigation.findNavController
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class MainActivity : AppCompatActivity() {
+
+ private val navController by lazy { findNavController(R.id.nav_host_fragment) }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.main_activity)
+ }
+
+ override fun onSupportNavigateUp() = navController.navigateUp()
+}
diff --git a/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/SamplesApplication.kt b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/SamplesApplication.kt
new file mode 100644
index 0000000..a93f6a0
--- /dev/null
+++ b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/SamplesApplication.kt
@@ -0,0 +1,23 @@
+/*
+ * 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
+
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+
+@HiltAndroidApp
+class SamplesApplication : Application()
diff --git a/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/SamplesFragment.kt b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/SamplesFragment.kt
new file mode 100644
index 0000000..ea419fa
--- /dev/null
+++ b/samples/androidApp/src/main/kotlin/com/appmattus/layercache/samples/SamplesFragment.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
+
+import android.os.Bundle
+import android.view.View
+import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.findNavController
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.appmattus.layercache.samples.databinding.RecyclerViewFragmentBinding
+import com.appmattus.layercache.samples.ui.component.SingleLineTextHeaderItem
+import com.appmattus.layercache.samples.ui.component.SingleLineTextItem
+import com.appmattus.layercache.samples.ui.viewBinding
+import com.xwray.groupie.GroupAdapter
+import com.xwray.groupie.GroupieViewHolder
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class SamplesFragment : Fragment(R.layout.recycler_view_fragment) {
+
+ private val binding by viewBinding()
+
+ 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 @@
+
+
+
+
+
+
diff --git a/samples/androidApp/src/main/res/layout/main_activity.xml b/samples/androidApp/src/main/res/layout/main_activity.xml
new file mode 100644
index 0000000..bc6eb61
--- /dev/null
+++ b/samples/androidApp/src/main/res/layout/main_activity.xml
@@ -0,0 +1,11 @@
+
+
diff --git a/samples/androidApp/src/main/res/layout/recycler_view_fragment.xml b/samples/androidApp/src/main/res/layout/recycler_view_fragment.xml
new file mode 100644
index 0000000..6a50718
--- /dev/null
+++ b/samples/androidApp/src/main/res/layout/recycler_view_fragment.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/samples/androidApp/src/main/res/layout/single_line_text_header_item.xml b/samples/androidApp/src/main/res/layout/single_line_text_header_item.xml
new file mode 100644
index 0000000..265f331
--- /dev/null
+++ b/samples/androidApp/src/main/res/layout/single_line_text_header_item.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
diff --git a/samples/androidApp/src/main/res/layout/single_line_text_item.xml b/samples/androidApp/src/main/res/layout/single_line_text_item.xml
new file mode 100644
index 0000000..48387ef
--- /dev/null
+++ b/samples/androidApp/src/main/res/layout/single_line_text_item.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
diff --git a/samples/androidApp/src/main/res/layout/two_line_text_item.xml b/samples/androidApp/src/main/res/layout/two_line_text_item.xml
new file mode 100644
index 0000000..c1b28ac
--- /dev/null
+++ b/samples/androidApp/src/main/res/layout/two_line_text_item.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/samples/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..7353dbd
--- /dev/null
+++ b/samples/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/samples/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..7353dbd
--- /dev/null
+++ b/samples/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png b/samples/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..b7cfb50
Binary files /dev/null and b/samples/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/samples/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png b/samples/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..44c438e
Binary files /dev/null and b/samples/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/samples/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png b/samples/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..e32d275
Binary files /dev/null and b/samples/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/samples/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png b/samples/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..a28ab2c
Binary files /dev/null and b/samples/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/samples/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png b/samples/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..78d5a26
Binary files /dev/null and b/samples/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/samples/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/samples/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..0421cc1
Binary files /dev/null and b/samples/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/samples/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png b/samples/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d235669
Binary files /dev/null and b/samples/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/samples/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/samples/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..66f6ab0
Binary files /dev/null and b/samples/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/samples/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/samples/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..ceea263
Binary files /dev/null and b/samples/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/samples/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/samples/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..18e22cf
Binary files /dev/null and b/samples/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/samples/androidApp/src/main/res/navigation/nav_graph.xml b/samples/androidApp/src/main/res/navigation/nav_graph.xml
new file mode 100644
index 0000000..db24a44
--- /dev/null
+++ b/samples/androidApp/src/main/res/navigation/nav_graph.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/androidApp/src/main/res/values/colors.xml b/samples/androidApp/src/main/res/values/colors.xml
new file mode 100644
index 0000000..6d4b656
--- /dev/null
+++ b/samples/androidApp/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #039be5
+ #006db3
+ #fdd835
+
diff --git a/samples/androidApp/src/main/res/values/ic_launcher_background.xml b/samples/androidApp/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000..39c3c6a
--- /dev/null
+++ b/samples/androidApp/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #C7E8AC
+
\ No newline at end of file
diff --git a/samples/androidApp/src/main/res/values/strings.xml b/samples/androidApp/src/main/res/values/strings.xml
new file mode 100644
index 0000000..33c3d92
--- /dev/null
+++ b/samples/androidApp/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ Layercache
+
diff --git a/samples/androidApp/src/main/res/values/styles.xml b/samples/androidApp/src/main/res/values/styles.xml
new file mode 100644
index 0000000..fafc322
--- /dev/null
+++ b/samples/androidApp/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/samples/androidApp/src/main/res/xml/network_security_config.xml b/samples/androidApp/src/main/res/xml/network_security_config.xml
new file mode 100644
index 0000000..683208f
--- /dev/null
+++ b/samples/androidApp/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/samples/androidApp/src/main/sqldelight/com/appmattus/layercache/samples/data/database/PersonalDetails.sq b/samples/androidApp/src/main/sqldelight/com/appmattus/layercache/samples/data/database/PersonalDetails.sq
new file mode 100644
index 0000000..7b232f6
--- /dev/null
+++ b/samples/androidApp/src/main/sqldelight/com/appmattus/layercache/samples/data/database/PersonalDetails.sq
@@ -0,0 +1,17 @@
+
+CREATE TABLE personalDetails (
+ name TEXT NOT NULL,
+ tagline TEXT NOT NULL,
+ location TEXT NOT NULL,
+ avatarUrl TEXT NOT NULL
+);
+
+personalDetails:
+SELECT * FROM personalDetails;
+
+insertPersonalDetails:
+INSERT INTO personalDetails
+VALUES (?, ?, ?, ?);
+
+deletePersonalDetails:
+DELETE FROM personalDetails;
diff --git a/settings.gradle.kts b/settings.gradle.kts
index b67bf4b..9b4c3b0 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 Appmattus Limited
+ * 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.
@@ -24,5 +24,7 @@ include(
"layercache-android-encryption",
"layercache-android-livedata",
- "testutils"
+ "testutils",
+
+ "samples:androidApp"
)