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 @@ + + + +