From 3b93bb67156ce7387637259ea5215020441abbe1 Mon Sep 17 00:00:00 2001 From: htl-leonding Date: Tue, 28 May 2024 20:44:03 +0200 Subject: [PATCH] 2024-05-28 lecture notes android mvvm --- asciidocs/images/viewmodel.png | Bin 0 -> 39137 bytes asciidocs/index.adoc | 34 ++- labs/android-mvvm/.gitignore | 33 +++ labs/android-mvvm/LICENSE | 24 ++ labs/android-mvvm/app/.gitignore | 1 + labs/android-mvvm/app/build.gradle.kts | 111 ++++++++ labs/android-mvvm/app/proguard-rules.pro | 21 ++ .../app/src/main/AndroidManifest.xml | 30 +++ .../java/at/htl/leonding/MainActivity.java | 28 ++ .../java/at/htl/leonding/MainViewRenderer.kt | 53 ++++ .../java/at/htl/leonding/ToDoApplication.java | 16 ++ .../at/htl/leonding/feature/home/HomeView.kt | 144 +++++++++++ .../leonding/feature/home/HomeViewModel.java | 51 ++++ .../leonding/feature/settings/SettingsView.kt | 39 +++ .../htl/leonding/feature/tabscreen/TabView.kt | 112 ++++++++ .../feature/tabscreen/TabViewModel.java | 32 +++ .../htl/leonding/feature/todo/ToDoClient.java | 15 ++ .../leonding/feature/todo/ToDoService.java | 41 +++ .../at/htl/leonding/feature/todo/ToDoView.kt | 75 ++++++ .../leonding/feature/todo/ToDoViewModel.java | 25 ++ .../java/at/htl/leonding/model/Model.java | 11 + .../java/at/htl/leonding/model/Store.java | 15 ++ .../main/java/at/htl/leonding/model/ToDo.java | 10 + .../java/at/htl/leonding/model/UIState.java | 31 +++ .../java/at/htl/leonding/ui/theme/Color.kt | 11 + .../java/at/htl/leonding/ui/theme/Theme.kt | 70 +++++ .../java/at/htl/leonding/ui/theme/Type.kt | 34 +++ .../leonding/util/config/ConfigModule.java | 62 +++++ .../at/htl/leonding/util/config/readme.md | 11 + .../at/htl/leonding/util/immer/Immer.java | 53 ++++ .../at/htl/leonding/util/mapper/Mapper.java | 51 ++++ .../util/resteasy/JsonMediaTypeMatcher.java | 14 + .../util/resteasy/JsonMessageBodyReader.java | 24 ++ .../util/resteasy/JsonMessageBodyWriter.java | 26 ++ .../util/resteasy/RestApiClientBuilder.java | 49 ++++ .../at/htl/leonding/util/store/StoreBase.java | 40 +++ .../leonding/util/store/ViewModelBase.java | 38 +++ .../res/drawable/ic_launcher_background.xml | 170 ++++++++++++ .../res/drawable/ic_launcher_foreground.xml | 30 +++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 + .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes .../app/src/main/res/values/colors.xml | 10 + .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/themes.xml | 5 + .../app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 ++ .../META-INF/microprofile-config.properties | 0 .../src/main/resources/application.properties | 1 + ...croprofile.config.spi.ConfigSourceProvider | 1 + labs/android-mvvm/build.gradle.kts | 6 + labs/android-mvvm/gradle.properties | 23 ++ labs/android-mvvm/gradle/libs.versions.toml | 37 +++ labs/android-mvvm/gradle/wrapper/.gitignore | 1 + .../gradle/wrapper/gradle-wrapper.properties | 7 + labs/android-mvvm/gradlew | 244 ++++++++++++++++++ labs/android-mvvm/gradlew.bat | 92 +++++++ labs/android-mvvm/readme.md | 32 +++ labs/android-mvvm/run-javadoc.sh | 16 ++ labs/android-mvvm/settings.gradle.kts | 17 ++ 69 files changed, 2169 insertions(+), 5 deletions(-) create mode 100644 asciidocs/images/viewmodel.png create mode 100644 labs/android-mvvm/.gitignore create mode 100644 labs/android-mvvm/LICENSE create mode 100644 labs/android-mvvm/app/.gitignore create mode 100644 labs/android-mvvm/app/build.gradle.kts create mode 100644 labs/android-mvvm/app/proguard-rules.pro create mode 100644 labs/android-mvvm/app/src/main/AndroidManifest.xml create mode 100644 labs/android-mvvm/app/src/main/java/at/htl/leonding/MainActivity.java create mode 100644 labs/android-mvvm/app/src/main/java/at/htl/leonding/MainViewRenderer.kt create mode 100644 labs/android-mvvm/app/src/main/java/at/htl/leonding/ToDoApplication.java create mode 100644 labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/home/HomeView.kt create mode 100644 labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/home/HomeViewModel.java create mode 100644 labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/settings/SettingsView.kt create mode 100644 labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/tabscreen/TabView.kt create mode 100644 labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/tabscreen/TabViewModel.java create mode 100644 labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/todo/ToDoClient.java create mode 100644 labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/todo/ToDoService.java create mode 100644 labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/todo/ToDoView.kt create mode 100644 labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/todo/ToDoViewModel.java create mode 100644 labs/android-mvvm/app/src/main/java/at/htl/leonding/model/Model.java create mode 100644 labs/android-mvvm/app/src/main/java/at/htl/leonding/model/Store.java create mode 100644 labs/android-mvvm/app/src/main/java/at/htl/leonding/model/ToDo.java create mode 100644 labs/android-mvvm/app/src/main/java/at/htl/leonding/model/UIState.java create mode 100644 labs/android-mvvm/app/src/main/java/at/htl/leonding/ui/theme/Color.kt create mode 100644 labs/android-mvvm/app/src/main/java/at/htl/leonding/ui/theme/Theme.kt create mode 100644 labs/android-mvvm/app/src/main/java/at/htl/leonding/ui/theme/Type.kt create mode 100644 labs/android-mvvm/app/src/main/java/at/htl/leonding/util/config/ConfigModule.java create mode 100644 labs/android-mvvm/app/src/main/java/at/htl/leonding/util/config/readme.md create mode 100644 labs/android-mvvm/app/src/main/java/at/htl/leonding/util/immer/Immer.java create mode 100644 labs/android-mvvm/app/src/main/java/at/htl/leonding/util/mapper/Mapper.java create mode 100644 labs/android-mvvm/app/src/main/java/at/htl/leonding/util/resteasy/JsonMediaTypeMatcher.java create mode 100644 labs/android-mvvm/app/src/main/java/at/htl/leonding/util/resteasy/JsonMessageBodyReader.java create mode 100644 labs/android-mvvm/app/src/main/java/at/htl/leonding/util/resteasy/JsonMessageBodyWriter.java create mode 100644 labs/android-mvvm/app/src/main/java/at/htl/leonding/util/resteasy/RestApiClientBuilder.java create mode 100644 labs/android-mvvm/app/src/main/java/at/htl/leonding/util/store/StoreBase.java create mode 100644 labs/android-mvvm/app/src/main/java/at/htl/leonding/util/store/ViewModelBase.java create mode 100644 labs/android-mvvm/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 labs/android-mvvm/app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 labs/android-mvvm/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 labs/android-mvvm/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 labs/android-mvvm/app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 labs/android-mvvm/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 labs/android-mvvm/app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 labs/android-mvvm/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 labs/android-mvvm/app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 labs/android-mvvm/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 labs/android-mvvm/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 labs/android-mvvm/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 labs/android-mvvm/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 labs/android-mvvm/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 labs/android-mvvm/app/src/main/res/values/colors.xml create mode 100644 labs/android-mvvm/app/src/main/res/values/strings.xml create mode 100644 labs/android-mvvm/app/src/main/res/values/themes.xml create mode 100644 labs/android-mvvm/app/src/main/res/xml/backup_rules.xml create mode 100644 labs/android-mvvm/app/src/main/res/xml/data_extraction_rules.xml create mode 100644 labs/android-mvvm/app/src/main/resources/META-INF/microprofile-config.properties create mode 100644 labs/android-mvvm/app/src/main/resources/application.properties create mode 100644 labs/android-mvvm/app/src/main/resources/unused/org.eclipse.microprofile.config.spi.ConfigSourceProvider create mode 100644 labs/android-mvvm/build.gradle.kts create mode 100644 labs/android-mvvm/gradle.properties create mode 100644 labs/android-mvvm/gradle/libs.versions.toml create mode 100644 labs/android-mvvm/gradle/wrapper/.gitignore create mode 100644 labs/android-mvvm/gradle/wrapper/gradle-wrapper.properties create mode 100755 labs/android-mvvm/gradlew create mode 100644 labs/android-mvvm/gradlew.bat create mode 100644 labs/android-mvvm/readme.md create mode 100755 labs/android-mvvm/run-javadoc.sh create mode 100644 labs/android-mvvm/settings.gradle.kts diff --git a/asciidocs/images/viewmodel.png b/asciidocs/images/viewmodel.png new file mode 100644 index 0000000000000000000000000000000000000000..85f7f95aec6e8c8da0fa255db358cb1be58dc04d GIT binary patch literal 39137 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 F{{sclxY_^! literal 0 HcmV?d00001 diff --git a/asciidocs/index.adoc b/asciidocs/index.adoc index 74b74ca..7cf4008 100644 --- a/asciidocs/index.adoc +++ b/asciidocs/index.adoc @@ -1,11 +1,13 @@ = 23/24 4bhif wmc - Lecture Notes -ifndef::imagesdir[:imagesdir: images] +Thomas Stütz +1.0.0, {docdate}: Lecture Notes for Courses at HTL Leonding :icons: font :experimental: :sectnums: +ifndef::imagesdir[:imagesdir: images] :toc: -ifdef::backend-html5[] +ifdef::backend-html5[] // https://fontawesome.com/v4.7.0/icons/ icon:file-text-o[link=https://github.com/2324-4bhif-wmc/2324-4bhif-wmc-lecture-notes/main/asciidocs/{docname}.adoc] ‏ ‏ ‎ icon:github-square[link=https://github.com/2324-4bhif-wmc/2324-4bhif-wmc-lecture-notes] ‏ ‏ ‎ @@ -835,9 +837,9 @@ public class Fahrzeug { Möglichkeit 3: JOINED -> Attribute der Basisklasse in einer Tabelle und je eine Tabelle pro abgeleiteter Klasse (mit -Diskriminator* DTYPE) +Diskriminator DTYPE) -__*Diskriminator = Unterscheidungsmerkmal__ +* __Diskriminator: Unterscheidungsmerkmal__ [source, java] ---- @@ -1287,7 +1289,7 @@ ng g c components/todo == 2024-04-16 Angular HttpClient -* http-client hat fetch gegenüber den Vorteil, dass gute Infrastruktur wir JWT support und middleware zum debuggen +* http-client hat fetch gegenüber den Vorteil, dass gute Infrastruktur mit JWT support und middleware zum debuggen == 2024-05-21 Android @@ -1364,3 +1366,25 @@ PGADMIN_DEFAULT_EMAIL= PGADMIN_DEFAULT_PASSWORD= ---- +== 2024-05-28 ViewModel in Android + +image::viewmodel.png[] + + +* https://medium.com/sahibinden-technology/package-by-layer-vs-package-by-feature-7e89cde2ae3a[Package by Layer vs Package by Feature^] + + +* https://medium.com/androiddevelopers/under-the-hood-of-jetpack-compose-part-2-of-2-37b2c20c6cdd[Under the hood of Jetpack Compose — part 2 of 2^] + + +* Anmerkungen zur https://github.com/caberger/android[Prof. Abergers Android-App^] + + +. Einführung des MVVM - Design Patterns, ein ViewModel pro View. Damit das verständlich wird vorher unbedingt die ersten 17 Minuten von https://www.youtube.com/watch?v=W1ymVx6dmvc ansehen. Hierbei im Kopf "Swift" durch „Java" ersetzen und "SwiftUI" durch "Jetpack Compose". +. Read Only State: In den ViewModels (Todo, Home, TabScreen) sind die Models jetzt Java Records, also readonly, damit sind die 3 Design Prinzipien OK (https://redux.js.org/understanding/thinking-in-redux/three-principles) +. Es gibt jetzt eine saubere Preview - Architektur für alle Views und das obwohl alles in Java und nicht in Kolin geschrieben ist (mit Ausnahme der Jetpack @Composables). +. Verbesserungen bei der Android - Implementierung (util Package) für: +.. RestEasy Client f. Android, +.. Microprofile config (=application.properties f. Android), +.. Jakarta Dependency Injection (@Inject für Android) und +.. RxJava (https://rxmarbles.com für Android). \ No newline at end of file diff --git a/labs/android-mvvm/.gitignore b/labs/android-mvvm/.gitignore new file mode 100644 index 0000000..347e252 --- /dev/null +++ b/labs/android-mvvm/.gitignore @@ -0,0 +1,33 @@ +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Log/OS Files +*.log + +# Android Studio generated files and folders +captures/ +.externalNativeBuild/ +.cxx/ +*.apk +output.json + +# IntelliJ +*.iml +.idea/ +misc.xml +deploymentTargetDropDown.xml +render.experimental.xml + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Android Profiling +*.hprof diff --git a/labs/android-mvvm/LICENSE b/labs/android-mvvm/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/labs/android-mvvm/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/labs/android-mvvm/app/.gitignore b/labs/android-mvvm/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/labs/android-mvvm/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/labs/android-mvvm/app/build.gradle.kts b/labs/android-mvvm/app/build.gradle.kts new file mode 100644 index 0000000..08593a8 --- /dev/null +++ b/labs/android-mvvm/app/build.gradle.kts @@ -0,0 +1,111 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + + kotlin("kapt") // we have to use kapt, because android hilt is still in alpha although kapt is deprecated already ?! :( + id("com.google.dagger.hilt.android") +} + +android { + namespace = "at.htl.leonding" + compileSdk = 34 + + defaultConfig { + applicationId = "at.htl.leonding" + minSdk = 28 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.4.3" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + excludes += "META-INF/INDEX.LIST" + excludes += "META-INF/DEPENDENCIES" + excludes += "META-INF/LICENSE.md" + excludes += "META-INF/NOTICE.md" + excludes += "META-INF/DEPENDENCIES.txt" + } + } +} + +dependencies { + implementation(libs.androidx.ktx) + implementation(libs.androidx.lifecycle) + implementation(libs.androidx.lifecycle.runtime.compose) + implementation(libs.androidx.activity.compose) + + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.compose.ui.ui) + implementation(libs.compose.ui.graphics) + implementation(libs.compose.ui.ui.tooling.preview) + implementation(libs.compose.material) + + testImplementation(libs.test.junit) + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.androidx.test.espresso) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.compose.ui.test) + + debugImplementation(libs.debug.compose.ui.tooling) + debugImplementation(libs.debug.compose.manifest) + + implementation(libs.rxjava.rxjava) + implementation(libs.rxjava.android) + implementation(libs.compose.rxjava) + + implementation(libs.dagger.hilt) + kapt(libs.kapt.hilt) + implementation(libs.fasterxml.jackson) + implementation(libs.resteasy.client) + implementation(libs.smallrye.config) +} +kapt { + correctErrorTypes = true +} + +/** JavaDoc helper. + * This tasks writes the the class-path to a file that can be used with javadoc + * javadoc ... @javadoc.txt + */ +tasks.register("javadoc-params") { + doLast { + val variant = project.android.applicationVariants + val release = variant.filter{ it.buildType.name == "release" }.first() + val outputFile = project.layout.buildDirectory.file("javadoc.txt").get().asFile + outputFile.printWriter().use { out -> + val classpath = release.compileConfiguration.joinToString(separator=":") { it.toString() } + out.println("--class-path " + classpath) + out.println() + } + println("javadoc options written to " + outputFile.absolutePath) + } +} \ No newline at end of file diff --git a/labs/android-mvvm/app/proguard-rules.pro b/labs/android-mvvm/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/labs/android-mvvm/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/labs/android-mvvm/app/src/main/AndroidManifest.xml b/labs/android-mvvm/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0abfbf9 --- /dev/null +++ b/labs/android-mvvm/app/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/labs/android-mvvm/app/src/main/java/at/htl/leonding/MainActivity.java b/labs/android-mvvm/app/src/main/java/at/htl/leonding/MainActivity.java new file mode 100644 index 0000000..797d400 --- /dev/null +++ b/labs/android-mvvm/app/src/main/java/at/htl/leonding/MainActivity.java @@ -0,0 +1,28 @@ +package at.htl.leonding; + +import android.os.Bundle; + +import javax.inject.Inject; + +import androidx.activity.ComponentActivity; +import androidx.compose.ui.platform.ComposeView; +import dagger.hilt.android.AndroidEntryPoint; + +/** Our main activity implemented in java. + * We separate the application logic from the view. + * The View is implemented in a separate file (separation of concerns). + * We use Kotlin for the Jetpack Compose views. + */ +@AndroidEntryPoint +public class MainActivity extends ComponentActivity { + @Inject + MainViewRenderer mainViewRenderer; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + var view = new ComposeView(this); + mainViewRenderer.setContent(view); + setContentView(view); + } +} diff --git a/labs/android-mvvm/app/src/main/java/at/htl/leonding/MainViewRenderer.kt b/labs/android-mvvm/app/src/main/java/at/htl/leonding/MainViewRenderer.kt new file mode 100644 index 0000000..a6b9265 --- /dev/null +++ b/labs/android-mvvm/app/src/main/java/at/htl/leonding/MainViewRenderer.kt @@ -0,0 +1,53 @@ +package at.htl.leonding + +import android.content.res.Configuration +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Surface +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalConfiguration +import at.htl.leonding.feature.tabscreen.TabView +import at.htl.leonding.model.Store +import at.htl.leonding.model.UIState +import javax.inject.Inject +import javax.inject.Singleton + +/** Render the contents of the Main Compose View. + * We also watch orientation changes and forward those to the Model. + */ +@Singleton +class MainViewRenderer @Inject constructor() { + @Inject + lateinit var tabScreenView: TabView + @Inject + lateinit var store: Store + + fun setContent(view: ComposeView) { + view.setContent { + Surface(modifier = Modifier.fillMaxSize()) { + var orientation by remember { mutableIntStateOf(Configuration.ORIENTATION_PORTRAIT) } + val configuration = LocalConfiguration.current + LaunchedEffect(configuration) { + orientation = configuration.orientation + val currentOrientation = orientationFromConfiguration(configuration) + store.apply { it.uiState.orientation = currentOrientation } + } + tabScreenView.TabViewLayout() + } + } + } + private fun orientationFromConfiguration(configuration: Configuration): UIState.Orientation { + return when(configuration.orientation) { + Configuration.ORIENTATION_PORTRAIT -> UIState.Orientation.portrait + Configuration.ORIENTATION_LANDSCAPE -> UIState.Orientation.landscape + else -> UIState.Orientation.undefined + } + } +} + + diff --git a/labs/android-mvvm/app/src/main/java/at/htl/leonding/ToDoApplication.java b/labs/android-mvvm/app/src/main/java/at/htl/leonding/ToDoApplication.java new file mode 100644 index 0000000..9b57a43 --- /dev/null +++ b/labs/android-mvvm/app/src/main/java/at/htl/leonding/ToDoApplication.java @@ -0,0 +1,16 @@ +package at.htl.leonding; + +import android.app.Application; +import android.util.Log; + +import javax.inject.Singleton; + +import dagger.hilt.android.HiltAndroidApp; + +/** Our application entry point. + * Needed as the dependency injection container. + */ +@HiltAndroidApp +@Singleton +public class ToDoApplication extends Application { +} diff --git a/labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/home/HomeView.kt b/labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/home/HomeView.kt new file mode 100644 index 0000000..156ebdd --- /dev/null +++ b/labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/home/HomeView.kt @@ -0,0 +1,144 @@ +package at.htl.leonding.feature.home + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rxjava3.subscribeAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import at.htl.leonding.model.Store +import at.htl.leonding.model.UIState.Orientation +import at.htl.leonding.ui.theme.ToDoTheme +import javax.inject.Inject + +/** + * Example of an editing composable using remember. + */ +class HomeView @Inject constructor() { + @Inject + lateinit var homeScreenViewModel: HomeViewModel + @Composable + fun HomeScreen() { + val model = homeScreenViewModel.subject.subscribeAsState(homeScreenViewModel.value) + val text = remember { mutableStateOf(model.value.greetingText) } + val orientation = model.value.orientation + + /** we update the model whenever the text is changed by the user */ + SideEffect { + homeScreenViewModel.setGreetingText(text.value) + } + fun reset() { + homeScreenViewModel.cleanToDos() + text.value = "" + } + Column(modifier = Modifier.fillMaxSize()) { + Spacer(Modifier.weight(2.0f)) + Row( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(16.dp) + ) { + Text( + text = text.value, + textAlign = TextAlign.Center, + fontSize = 20.sp, + fontWeight = FontWeight.Bold + ) + } + Row( + Modifier + .align(Alignment.CenterHorizontally) + .padding(16.dp) + ) { + TextField(value = text.value, + onValueChange = {text.value = it}, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + placeholder = { + Text(text="Header to display", modifier = Modifier.alpha(0.2f)) + }) + } + Row(Modifier.align(Alignment.CenterHorizontally)) { + Text("${model.value.numberOfToDos} Todos have been loaded") + } + Spacer(Modifier.weight(1.0f)) + if (orientation == Orientation.landscape) { + Row(Modifier.align(Alignment.CenterHorizontally)) { + Buttons(Modifier.align(Alignment.CenterVertically)) { reset() } + } + } else { + Buttons(Modifier.align(Alignment.CenterHorizontally)) { reset() } + } + Spacer(Modifier.weight(2.0f)) + } + } + @Composable + fun LoadAllToDosButton(modifier: Modifier) { + Row(modifier) { + Button(modifier = Modifier.padding(16.dp), + onClick = { homeScreenViewModel.loadAllTodos() }) { + Text("load ToDos") + } + } + } + @Composable + fun ClearButton(modifier: Modifier, onClick: () -> Unit) { + Row(modifier) { + Button( + onClick = { + //homeScreenViewModel.cleanToDos(); + onClick() + }) { + Text("reset") + } + } + } + @Composable + fun Buttons(modifier: Modifier, reset: () -> Unit) { + LoadAllToDosButton(modifier) + ClearButton(modifier, reset) + } + + @Composable + fun Preview(orientation: Orientation) { + if (LocalInspectionMode.current) { + val store = Store() + store.pipe.value!!.uiState.orientation = orientation + homeScreenViewModel = HomeViewModel(store, null) + ToDoTheme { + HomeScreen() + } + } + } + @Preview(name = "85%", fontScale = 0.85f) + @Preview(name = "100%", fontScale = 1f) + @Preview(name = "200%", fontScale = 2f) + @Composable + fun HomeViewPreviewPortrait() { + Preview(Orientation.portrait) + } + + @Preview(name = "100%", fontScale = 1f, device = "spec:parent=pixel_5,orientation=landscape") + @Preview(name = "150%", fontScale = 1.5f, device = "spec:parent=pixel_5,orientation=landscape") + @Composable + fun HomeViewPreviewLandscape() { + Preview(Orientation.landscape) + } +} \ No newline at end of file diff --git a/labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/home/HomeViewModel.java b/labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/home/HomeViewModel.java new file mode 100644 index 0000000..acf21c0 --- /dev/null +++ b/labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/home/HomeViewModel.java @@ -0,0 +1,51 @@ +package at.htl.leonding.feature.home; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import at.htl.leonding.model.Model; +import at.htl.leonding.model.Store; +import at.htl.leonding.model.ToDo; +import at.htl.leonding.feature.todo.ToDoService; +import at.htl.leonding.model.UIState; +import at.htl.leonding.util.store.ViewModelBase; + +/** Map our global application state to vision that our HomeView has of the world. + */ +@Singleton +public class HomeViewModel extends ViewModelBase { + + /** the model for our HomeView, which only knows about a list of todos a text and the orientation */ + public record HomeModel( + int numberOfToDos, + String greetingText, + UIState.Orientation orientation + ) {} + + private final ToDoService toDoService; + + @Inject + HomeViewModel(Store store, ToDoService toDoService) { + super(store); + this.toDoService = toDoService; + } + @Override + protected HomeModel toViewModel(Model model) { + return new HomeModel( + model.toDos.length, + model.greetingModel.greetingText, + model.uiState.orientation); + } + + public void setGreetingText(String text) { + store.apply(model -> model.greetingModel.greetingText = text); + } + public void cleanToDos() { + store.apply(model -> { + model.toDos = new ToDo[0]; + }); + } + public void loadAllTodos() { + toDoService.getAll(); + } +} diff --git a/labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/settings/SettingsView.kt b/labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/settings/SettingsView.kt new file mode 100644 index 0000000..366ed95 --- /dev/null +++ b/labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/settings/SettingsView.kt @@ -0,0 +1,39 @@ +package at.htl.leonding.feature.settings + +import androidx.compose.material3.Text + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.sp +import at.htl.leonding.ui.theme.ToDoTheme + +@Composable +fun SettingsScreen() { + Column(modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center) { + Row(modifier = Modifier.align(Alignment.CenterHorizontally)) { + Text( + text = "Settings", + textAlign = TextAlign.Center, + fontSize = 20.sp, + fontWeight = FontWeight.Bold + ) + } + } +} +@Preview(showBackground = true) +@Composable +fun SettingsViewPreview() { + ToDoTheme { + SettingsScreen() + } +} \ No newline at end of file diff --git a/labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/tabscreen/TabView.kt b/labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/tabscreen/TabView.kt new file mode 100644 index 0000000..09b2d97 --- /dev/null +++ b/labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/tabscreen/TabView.kt @@ -0,0 +1,112 @@ +package at.htl.leonding.feature.tabscreen + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.Badge +import androidx.compose.material3.BadgedBox +import androidx.compose.material3.Icon +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rxjava3.subscribeAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import at.htl.leonding.feature.home.HomeView +import at.htl.leonding.feature.settings.SettingsScreen +import at.htl.leonding.feature.todo.ToDoView +import at.htl.leonding.model.Store +import at.htl.leonding.ui.theme.ToDoTheme +import javax.inject.Inject + +class TabView @Inject constructor() { + @Inject + lateinit var tabScreenViewModel: TabViewModel + @Inject + lateinit var homeScreenView: HomeView + @Inject + lateinit var toDoView: ToDoView + + @Composable + fun TabViewLayout() { + val model = tabScreenViewModel.subject.subscribeAsState(tabScreenViewModel.value) + val tab = model.value.selectedTab + val tabIndex = tab.index() + val selectedTab = remember { mutableIntStateOf(tabIndex) } + val numberOfTodos = model.value.numberOfToDos + val tabs = listOf("Home", "ToDos", "Settings") + Column(modifier = Modifier.fillMaxWidth()) { + TabRow(selectedTabIndex = selectedTab.intValue) { + tabs.forEachIndexed { index, title -> + Tab(text = { Text(title) }, + selected = selectedTab.intValue == index, + onClick = { + selectedTab.intValue = index + tabScreenViewModel.selectTabByIndex(index) + }, + icon = { + when (index) { + 0 -> Icon(imageVector = Icons.Default.Home, contentDescription = null) + 1 -> BadgedBox(badge = { Badge { Text("$numberOfTodos") }}) { + Icon(Icons.Filled.Favorite, contentDescription = "ToDos") + } + 2 -> Icon(imageVector = Icons.Default.Settings, contentDescription = null) + } + } + ) + } + } + ContentArea(selectedTab.intValue) + } + } + @Composable + fun ContentArea(selectedTab: Int) { + if (LocalInspectionMode.current) { + PreviewContentArea() + } else { + when (selectedTab) { + 0 -> homeScreenView.HomeScreen() + 1 -> toDoView.ToDos() + 2 -> SettingsScreen() + } + } + } + @Composable + fun PreviewContentArea() { + Column(modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center) { + Row(modifier = Modifier.align(Alignment.CenterHorizontally).padding(8.dp)) { + Text(text = "Content area of the selected tab", softWrap = true) + } + } + } + @Preview(showBackground = true) + @Preview(name="150%", fontScale = 1.5f) + @Composable + fun TabViewPreview() { + if (LocalInspectionMode.current) { + tabScreenViewModel = TabViewModel(Store()) + ToDoTheme { + TabViewLayout() + } + } + } +} + + + + diff --git a/labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/tabscreen/TabViewModel.java b/labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/tabscreen/TabViewModel.java new file mode 100644 index 0000000..77004eb --- /dev/null +++ b/labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/tabscreen/TabViewModel.java @@ -0,0 +1,32 @@ +package at.htl.leonding.feature.tabscreen; + +import java.util.Arrays; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import at.htl.leonding.model.Model; +import at.htl.leonding.model.Store; +import at.htl.leonding.model.UIState; +import at.htl.leonding.util.store.ViewModelBase; + +@Singleton +public class TabViewModel extends ViewModelBase { + + /** all the TabView needs to know is the number of todos and the selected tab */ + public record TabScreenModel(int getNumberOfToDos, UIState.Tab selectedTab) {} + + @Inject + TabViewModel(Store store) { + super(store); + } + @Override + protected TabScreenModel toViewModel(Model model) { + return new TabScreenModel(model.toDos.length, model.uiState.selectedTab); + } + public void selectTabByIndex(int index) { + var tabs = Arrays.stream(UIState.Tab.values()); + var tab = tabs.filter(t -> t.index() == index).findFirst().orElse(UIState.Tab.home); + store.apply(model -> model.uiState.selectedTab = tab); + } +} diff --git a/labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/todo/ToDoClient.java b/labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/todo/ToDoClient.java new file mode 100644 index 0000000..4afb176 --- /dev/null +++ b/labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/todo/ToDoClient.java @@ -0,0 +1,15 @@ +package at.htl.leonding.feature.todo; + +import at.htl.leonding.model.ToDo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; + +@Path("/todos") +public interface ToDoClient { + @GET + @Produces(MediaType.APPLICATION_JSON) + ToDo[] all(@QueryParam("_start") int start, @QueryParam("_limit") int maxRecords); +} diff --git a/labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/todo/ToDoService.java b/labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/todo/ToDoService.java new file mode 100644 index 0000000..1136883 --- /dev/null +++ b/labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/todo/ToDoService.java @@ -0,0 +1,41 @@ +package at.htl.leonding.feature.todo; + +import org.eclipse.microprofile.config.Config; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import at.htl.leonding.model.Store; +import at.htl.leonding.model.ToDo; +import at.htl.leonding.util.resteasy.RestApiClientBuilder; + +import static java.util.concurrent.CompletableFuture.supplyAsync; + +@Singleton +public class ToDoService { + public static final String JSON_PLACEHOLDER_BASE_URL_SETTING = "json.placeholder.baseurl"; + public final ToDoClient toDoClient; + public final Store store; + + @Inject + ToDoService(Config config, RestApiClientBuilder builder, Store store) { + var baseUrl = config.getValue(JSON_PLACEHOLDER_BASE_URL_SETTING, String.class); + toDoClient = builder.build(ToDoClient.class, baseUrl); + this.store = store; + } + + /** read the first 20 todos from the service. + * TODO: add currentPage und pageSize to UIState + */ + public void getAll() { + Consumer setToDos = todos -> { + store.apply(model -> model.toDos = todos); + }; + CompletableFuture + .supplyAsync(() -> toDoClient.all(0, 40)) + .thenAccept(setToDos); + } +} diff --git a/labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/todo/ToDoView.kt b/labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/todo/ToDoView.kt new file mode 100644 index 0000000..fbe5e9e --- /dev/null +++ b/labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/todo/ToDoView.kt @@ -0,0 +1,75 @@ +package at.htl.leonding.feature.todo + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rxjava3.subscribeAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import at.htl.leonding.model.Store +import at.htl.leonding.model.ToDo +import at.htl.leonding.ui.theme.ToDoTheme +import javax.inject.Inject + +class ToDoView @Inject constructor() { + @Inject + lateinit var toDoViewModel: ToDoViewModel + + @Composable + fun ToDos() { + val model = toDoViewModel.subject.subscribeAsState(toDoViewModel.getValue()).value + val todos = model.toDos + LazyColumn(modifier = Modifier + .fillMaxSize() + .padding(16.dp)) { + items(todos.size) { ToDoRow(todos[it]) } + } + } + @Composable + fun ToDoRow(toDo: ToDo) { + Row(modifier = Modifier.padding(4.dp)) { + Text(toDo.id.toString(), modifier = Modifier.padding(horizontal = 6.dp)) + Text(text = toDo.title, overflow = TextOverflow.Ellipsis, maxLines = 1) + } + } + @Composable + fun preview() { + if (LocalInspectionMode.current) { + fun toDo(id: Long, title: String): ToDo { + val toDo = ToDo() + toDo.id = id + toDo.title = title + return toDo + } + val todos = arrayOf( + toDo(1, "short title"), + toDo(2, "title generated by ChatGPT: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Viverra ipsum nunc aliquet bibendum enim facilisis gravida neque convallis. Vulputate enim nulla aliquet porttitor lacus luctus accumsan tortor. Neque egestas congue quisque egestas diam in arcu. Est lorem ipsum dolor sit amet consectetur adipiscing elit pellentesque. Ut venenatis tellus in metus. Consectetur adipiscing elit pellentesque habitant morbi tristique. Ut tellus elementum sagittis vitae et leo duis ut. Vulputate ut pharetra sit amet aliquam id diam. Arcu cursus vitae congue mauris rhoncus aenean vel. Consequat interdum varius sit amet mattis. Faucibus purus in massa tempor nec feugiat nisl pretium fusce. Facilisi nullam vehicula ipsum a arcu cursus. Enim ut tellus elementum sagittis vitae et leo. Neque sodales ut etiam sit amet nisl purus. Vitae tempus quam pellentesque nec. Diam quam nulla porttitor massa id neque aliquam vestibulum morbi. Sed sed risus pretium quam vulputate dignissim suspendisse in est. Nibh mauris cursus mattis molestie a."), + toDo(3, "another tile that is too long for portrait mode, but ok for landscape") + ) + + val store = Store() + store.pipe.value!!.toDos = todos + toDoViewModel = ToDoViewModel(store) + ToDoTheme { + ToDos() + } + } + } + @Preview(showBackground = true) + @Composable + fun ToDoViewPreviewPortrait() { + preview() + } + @Preview(device = "spec:parent=pixel_5,orientation=landscape") + @Preview(name = "150%", fontScale = 1.5f, showBackground = true, device = "spec:parent=Nexus 5,orientation=landscape") + @Composable + fun ToDoViewPreviewLandscape() { + preview() + } +} diff --git a/labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/todo/ToDoViewModel.java b/labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/todo/ToDoViewModel.java new file mode 100644 index 0000000..ba96aaa --- /dev/null +++ b/labs/android-mvvm/app/src/main/java/at/htl/leonding/feature/todo/ToDoViewModel.java @@ -0,0 +1,25 @@ +package at.htl.leonding.feature.todo; + +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import at.htl.leonding.model.Model; +import at.htl.leonding.model.Store; +import at.htl.leonding.model.ToDo; +import at.htl.leonding.util.store.ViewModelBase; + +@Singleton +public class ToDoViewModel extends ViewModelBase { + public record ToDoModel(List toDos) {} + + @Inject + public ToDoViewModel(Store store) { + super(store); + } + @Override + protected ToDoModel toViewModel(Model model) { + return new ToDoModel(List.of(model.toDos)); + } +} diff --git a/labs/android-mvvm/app/src/main/java/at/htl/leonding/model/Model.java b/labs/android-mvvm/app/src/main/java/at/htl/leonding/model/Model.java new file mode 100644 index 0000000..1e31d18 --- /dev/null +++ b/labs/android-mvvm/app/src/main/java/at/htl/leonding/model/Model.java @@ -0,0 +1,11 @@ +package at.htl.leonding.model; + +/** Our read only single source of truth model */ +public class Model { + public static class GreetingModel { + public String greetingText = "Hello, world!"; + } + public ToDo[] toDos = new ToDo[0]; + public UIState uiState = new UIState(); // sub-model für ui state + public GreetingModel greetingModel = new GreetingModel(); +} diff --git a/labs/android-mvvm/app/src/main/java/at/htl/leonding/model/Store.java b/labs/android-mvvm/app/src/main/java/at/htl/leonding/model/Store.java new file mode 100644 index 0000000..001df3e --- /dev/null +++ b/labs/android-mvvm/app/src/main/java/at/htl/leonding/model/Store.java @@ -0,0 +1,15 @@ +package at.htl.leonding.model; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import at.htl.leonding.util.store.StoreBase; + +/** This is our Storage area for our single source of truth {@link Model}. */ +@Singleton +public class Store extends StoreBase { + @Inject + public Store() { + super(Model.class, new Model()); + } +} diff --git a/labs/android-mvvm/app/src/main/java/at/htl/leonding/model/ToDo.java b/labs/android-mvvm/app/src/main/java/at/htl/leonding/model/ToDo.java new file mode 100644 index 0000000..4451d5f --- /dev/null +++ b/labs/android-mvvm/app/src/main/java/at/htl/leonding/model/ToDo.java @@ -0,0 +1,10 @@ +package at.htl.leonding.model; + +/** A ToDo as we get it from jsonplaceholder.typicode.com +*/ +public class ToDo { + public Long userId; + public Long id; + public String title; + public boolean completed; +} diff --git a/labs/android-mvvm/app/src/main/java/at/htl/leonding/model/UIState.java b/labs/android-mvvm/app/src/main/java/at/htl/leonding/model/UIState.java new file mode 100644 index 0000000..133714a --- /dev/null +++ b/labs/android-mvvm/app/src/main/java/at/htl/leonding/model/UIState.java @@ -0,0 +1,31 @@ +package at.htl.leonding.model; + + +/** The current state of the user interface */ +public class UIState { + /** the type of Tab - Bars in our main view */ + public enum Tab { + home(0), + todo(1), + settings(2); + + public int index() { + return index; + } + private int index; + Tab(int index) { + this.index = index; + } + } + /** we define our own enum to have the model independent of the view technology */ + public enum Orientation { + undefined, + portrait, + landscape + } + /** the currently selected tab */ + public Tab selectedTab = Tab.home; + + /** the current orientation of the device. */ + public Orientation orientation = Orientation.undefined; +} diff --git a/labs/android-mvvm/app/src/main/java/at/htl/leonding/ui/theme/Color.kt b/labs/android-mvvm/app/src/main/java/at/htl/leonding/ui/theme/Color.kt new file mode 100644 index 0000000..bc42e73 --- /dev/null +++ b/labs/android-mvvm/app/src/main/java/at/htl/leonding/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package at.htl.leonding.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/labs/android-mvvm/app/src/main/java/at/htl/leonding/ui/theme/Theme.kt b/labs/android-mvvm/app/src/main/java/at/htl/leonding/ui/theme/Theme.kt new file mode 100644 index 0000000..8f36b91 --- /dev/null +++ b/labs/android-mvvm/app/src/main/java/at/htl/leonding/ui/theme/Theme.kt @@ -0,0 +1,70 @@ +package at.htl.leonding.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun ToDoTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.primary.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/labs/android-mvvm/app/src/main/java/at/htl/leonding/ui/theme/Type.kt b/labs/android-mvvm/app/src/main/java/at/htl/leonding/ui/theme/Type.kt new file mode 100644 index 0000000..336e795 --- /dev/null +++ b/labs/android-mvvm/app/src/main/java/at/htl/leonding/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package at.htl.leonding.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/labs/android-mvvm/app/src/main/java/at/htl/leonding/util/config/ConfigModule.java b/labs/android-mvvm/app/src/main/java/at/htl/leonding/util/config/ConfigModule.java new file mode 100644 index 0000000..15c6f2a --- /dev/null +++ b/labs/android-mvvm/app/src/main/java/at/htl/leonding/util/config/ConfigModule.java @@ -0,0 +1,62 @@ +package at.htl.leonding.util.config; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.eclipse.microprofile.config.spi.ConfigSourceProvider; + +import java.io.IOException; +import java.net.URL; +import java.util.concurrent.CompletionException; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; +import dagger.hilt.InstallIn; +import dagger.hilt.components.SingletonComponent; +import io.smallrye.config.PropertiesConfigSource; +import io.smallrye.config.SmallRyeConfigBuilder; + +/* Provide ecplipse microprofile config settings + (see src/main/resources/application.properties) + */ +@Module +@InstallIn(SingletonComponent.class) +public class ConfigModule { + @Provides + @Singleton + public Config provideConfiguration() { + //return new SmallRyeConfigBuilder().forClassLoader(classLoader).build(); <=== does not work on android yet. + + var config = new SmallRyeConfigBuilder() + .forClassLoader(getClass().getClassLoader()) + .addDefaultInterceptors() + .setAddDefaultSources(false) + .withSources(new FixMissingJavaNioJarFileProviderConfigSourceProvider()) + .build(); + return config; + } +} +class FixMissingJavaNioJarFileProviderConfigSourceProvider implements ConfigSourceProvider { + Iterable configSources; + @Override + public Iterable getConfigSources(ClassLoader forClassLoader) { + if (configSources == null) { + Function createSource = url -> { + try { + return new PropertiesConfigSource(url); + } catch(IOException e) { + throw new CompletionException(e); + } + }; + configSources = Stream.of("application.properties", "META-INF/microprofile-config.properties") + .map(name -> getClass().getClassLoader().getResource(name)) + .map(createSource) + .collect(Collectors.toList()); + } + return configSources; + } +} diff --git a/labs/android-mvvm/app/src/main/java/at/htl/leonding/util/config/readme.md b/labs/android-mvvm/app/src/main/java/at/htl/leonding/util/config/readme.md new file mode 100644 index 0000000..6f19ea0 --- /dev/null +++ b/labs/android-mvvm/app/src/main/java/at/htl/leonding/util/config/readme.md @@ -0,0 +1,11 @@ +# SmallRye config Module + +Propably it is not a SmallRye config problem, I found the following Android issue: +https://issuetracker.google.com/issues/153773248 + +This is from 2020, so i am not sure it will be fixed soon. + +The propblematic call comes from line 139 in ClassPathUtils.java: +``` java +try (FileSystem jarFs = FileSystems.newFileSystem(jar, (ClassLoader) null)) {...} +``` diff --git a/labs/android-mvvm/app/src/main/java/at/htl/leonding/util/immer/Immer.java b/labs/android-mvvm/app/src/main/java/at/htl/leonding/util/immer/Immer.java new file mode 100644 index 0000000..1948abb --- /dev/null +++ b/labs/android-mvvm/app/src/main/java/at/htl/leonding/util/immer/Immer.java @@ -0,0 +1,53 @@ +package at.htl.leonding.util.immer; + +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +import at.htl.leonding.util.mapper.Mapper; + +/** Immer simplifies handling immutable data structures. + * @author Christian Aberger (http://www.aberger.at) + * @see https://immerjs.github.io/immer/ + * @see https://redux.js.org/understanding/thinking-in-redux/motivation + * + * @param The type of the baseState + */ + +public class Immer { + private static final String TAG = Immer.class.getSimpleName(); + final public Mapper mapper; + final Handler handler; + + public Immer(Class type) { + mapper = new Mapper(type); + handler = new Handler(Looper.getMainLooper()); + } + /** Create a deep clone of the existing model, apply a recipe to it and finally pass the new state to the consumer. + * To reduce the load on the main thread we clone the current state in a separate thread. + * To avoid multithreading issues we call back the recipe and resultConsumer running on the one and only Main thread of the app. + * We do not call the resultConsumer if the clone equals the currentState, + * @param currentState the previous readonly single source or truth + * @param recipe the callback function that modifies parts of the cloned state + * @param resultConsumer the callback function that uses the cloned & modified model + */ + public void produce(final T currentState, Consumer recipe, Consumer resultConsumer) { + Consumer runOnMainThread = t -> handler.post(() -> { + var currentAsJson = mapper.toResource(t); + recipe.accept(t); + var nextAsJson = mapper.toResource(t); + if (!nextAsJson.equals(currentAsJson)) { + Log.d(TAG, String.format("=== state changed ===\n%s\n=>\n%s---", currentAsJson, nextAsJson)); + resultConsumer.accept(t); + } else { + Log.w(TAG, "produce() without change"); + } + }); + CompletableFuture + .supplyAsync(() -> mapper.clone(currentState)) + .thenAccept(runOnMainThread); + } +} diff --git a/labs/android-mvvm/app/src/main/java/at/htl/leonding/util/mapper/Mapper.java b/labs/android-mvvm/app/src/main/java/at/htl/leonding/util/mapper/Mapper.java new file mode 100644 index 0000000..fd6da3e --- /dev/null +++ b/labs/android-mvvm/app/src/main/java/at/htl/leonding/util/mapper/Mapper.java @@ -0,0 +1,51 @@ +package at.htl.leonding.util.mapper; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +import java.io.IOException; + +/** A Mapper that maps types to their json representation and back. + * ... plus a convenient deep-clone function + * @param the Class that is mapped + */ +public class Mapper { + private Class clazz; + private ObjectMapper mapper; + + public Mapper(Class clazz) { + this.clazz = clazz; + mapper = new ObjectMapper() + .configure(SerializationFeature.INDENT_OUTPUT, true) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); // records + } + public String toResource(T model) { + try { + return mapper.writeValueAsString(model); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + public T fromResource(String json) { + T model = null; + try { + model = mapper.readValue(json.getBytes(), clazz); + } catch (IOException e) { + throw new RuntimeException(e); + } + return model; + } + /** deep clone an object by converting it to its json representation and back. + * + * @param thing the thing to clone, unchanged + * @return the deeply cloned thing + */ + public T clone(final T thing) { + return fromResource(toResource(thing)); + } +} \ No newline at end of file diff --git a/labs/android-mvvm/app/src/main/java/at/htl/leonding/util/resteasy/JsonMediaTypeMatcher.java b/labs/android-mvvm/app/src/main/java/at/htl/leonding/util/resteasy/JsonMediaTypeMatcher.java new file mode 100644 index 0000000..3d20be8 --- /dev/null +++ b/labs/android-mvvm/app/src/main/java/at/htl/leonding/util/resteasy/JsonMediaTypeMatcher.java @@ -0,0 +1,14 @@ +package at.htl.leonding.util.resteasy; + +import jakarta.ws.rs.core.MediaType; + +/** an interface that amends a class with a function to check for the application/json MediaType + */ +public interface JsonMediaTypeMatcher { + public static final String APPLICATION = "application"; + public static final String JSON = "json"; + + default boolean isJson(MediaType mediaType) { + return mediaType.getType().equalsIgnoreCase(APPLICATION) && mediaType.getSubtype().equalsIgnoreCase(JSON); + } +} diff --git a/labs/android-mvvm/app/src/main/java/at/htl/leonding/util/resteasy/JsonMessageBodyReader.java b/labs/android-mvvm/app/src/main/java/at/htl/leonding/util/resteasy/JsonMessageBodyReader.java new file mode 100644 index 0000000..35a3972 --- /dev/null +++ b/labs/android-mvvm/app/src/main/java/at/htl/leonding/util/resteasy/JsonMessageBodyReader.java @@ -0,0 +1,24 @@ +package at.htl.leonding.util.resteasy; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.ext.MessageBodyReader; + +public class JsonMessageBodyReader implements MessageBodyReader, JsonMediaTypeMatcher { + ObjectMapper objectMapper = new ObjectMapper(); + @Override + public boolean isReadable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return isJson(mediaType); + } + @Override + public T readFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) throws IOException, WebApplicationException { + return objectMapper.readValue(entityStream, type); + } +} diff --git a/labs/android-mvvm/app/src/main/java/at/htl/leonding/util/resteasy/JsonMessageBodyWriter.java b/labs/android-mvvm/app/src/main/java/at/htl/leonding/util/resteasy/JsonMessageBodyWriter.java new file mode 100644 index 0000000..2a5132b --- /dev/null +++ b/labs/android-mvvm/app/src/main/java/at/htl/leonding/util/resteasy/JsonMessageBodyWriter.java @@ -0,0 +1,26 @@ +package at.htl.leonding.util.resteasy; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.ext.MessageBodyWriter; + +public class JsonMessageBodyWriter implements MessageBodyWriter, JsonMediaTypeMatcher { + ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return isJson(mediaType); + } + @Override + public void writeTo(T t, Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { + objectMapper.writeValue(entityStream, t); + } +} diff --git a/labs/android-mvvm/app/src/main/java/at/htl/leonding/util/resteasy/RestApiClientBuilder.java b/labs/android-mvvm/app/src/main/java/at/htl/leonding/util/resteasy/RestApiClientBuilder.java new file mode 100644 index 0000000..842c8ad --- /dev/null +++ b/labs/android-mvvm/app/src/main/java/at/htl/leonding/util/resteasy/RestApiClientBuilder.java @@ -0,0 +1,49 @@ +package at.htl.leonding.util.resteasy; + +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; +import org.jboss.resteasy.client.jaxrs.engines.URLConnectionClientEngineBuilder; +import org.jboss.resteasy.client.jaxrs.internal.LocalResteasyProviderFactory; +import org.jboss.resteasy.spi.ResteasyProviderFactory; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import jakarta.ws.rs.client.ClientBuilder; + +/** Build a RestEasy Client with reduced dependencies on a lot of the standard Java Runtime classes so that it works even on the Android Java Runtime. + * @author Christian Aberger (http://www.aberger.at) + */ +@Singleton +public class RestApiClientBuilder { + final public ScheduledExecutorService scheduledExecutorService; + final public ExecutorService executorService; + + @Inject + public RestApiClientBuilder() { + scheduledExecutorService = Executors.newScheduledThreadPool(1); + executorService = Executors.newSingleThreadExecutor(); + } + /** Build a reasteasy client + * @see single source of truth + * @author Christian Aberger (http://www.aberger.at) + * @param the class of the ReadOnly Single Source of Truth. + */ +public class StoreBase { + public final BehaviorSubject pipe; + protected final Immer immer; + + protected StoreBase(Class type, T initialState) { + try { + pipe = BehaviorSubject.createDefault(initialState); + immer = new Immer(type); + } catch (Exception e) { + throw new CompletionException(e); + } + } + + public T get() { + return immer.mapper.clone(pipe.getValue()); + } + /** clone the current Model, apply the recipe to it and submit it to the pipe as the next Model. + * @param recipe + * The function that receives a clone of the current model and applies its changes to it. + */ + public void apply(Consumer recipe) { + Consumer onNext = nextState -> pipe.onNext(nextState); + immer.produce(pipe.getValue(), recipe, onNext); + } +} diff --git a/labs/android-mvvm/app/src/main/java/at/htl/leonding/util/store/ViewModelBase.java b/labs/android-mvvm/app/src/main/java/at/htl/leonding/util/store/ViewModelBase.java new file mode 100644 index 0000000..1ddb902 --- /dev/null +++ b/labs/android-mvvm/app/src/main/java/at/htl/leonding/util/store/ViewModelBase.java @@ -0,0 +1,38 @@ +package at.htl.leonding.util.store; + +import at.htl.leonding.model.Model; +import at.htl.leonding.model.Store; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.subjects.Subject; + +/** Map our global application state to the small vision that a view has of the world. + * A base class for our ViewModels in the sense of the MVVM Pattern. + * In a lot of texts the term "Model-View-ViewModel" is often explained incorrectly. + * + * For a detailed explanation of MVVM + * watch the first 17 minutes of Lecture 3 | Stanford CS193p 2023. + * In that text replace "SwiftUI" -> Jetpack Compose/"Swift" -> Java/"struct" -> record + * + * @param the type of the view model, the small world of the view that this special viemodel serves. + */ +public abstract class ViewModelBase { + public final Subject subject = PublishSubject.create(); + + protected final Store store; + private final Disposable subscription; + + protected ViewModelBase(Store store) { + this.store = store; + this.subscription = store.pipe + .map(this::toViewModel) + .distinctUntilChanged() + .subscribe(subject::onNext); + } + public T getValue() { + return toViewModel(store.pipe.getValue()); + } + + /** map the "big" model to our "small" viewmodel */ + protected abstract T toViewModel(Model model); +} diff --git a/labs/android-mvvm/app/src/main/res/drawable/ic_launcher_background.xml b/labs/android-mvvm/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..1e4408c --- /dev/null +++ b/labs/android-mvvm/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/labs/android-mvvm/app/src/main/res/drawable/ic_launcher_foreground.xml b/labs/android-mvvm/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..fde1368 --- /dev/null +++ b/labs/android-mvvm/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/labs/android-mvvm/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/labs/android-mvvm/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/labs/android-mvvm/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/labs/android-mvvm/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/labs/android-mvvm/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/labs/android-mvvm/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/labs/android-mvvm/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/labs/android-mvvm/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..c209e78ecd372343283f4157dcfd918ec5165bb3 GIT binary patch literal 1404 zcmV-?1%vuhNk&F=1pok7MM6+kP&il$0000G0000-002h-06|PpNX!5L00Dqw+t%{r zzW2vH!KF=w&cMnnN@{whkTw+#mAh0SV?YL=)3MimFYCWp#fpdtz~8$hD5VPuQgtcN zXl<@<#Cme5f5yr2h%@8TWh?)bSK`O z^Z@d={gn7J{iyxL_y_%J|L>ep{dUxUP8a{byupH&!UNR*OutO~0{*T4q5R6@ApLF! z5{w?Z150gC7#>(VHFJZ-^6O@PYp{t!jH(_Z*nzTK4 zkc{fLE4Q3|mA2`CWQ3{8;gxGizgM!zccbdQoOLZc8hThi-IhN90RFT|zlxh3Ty&VG z?Fe{#9RrRnxzsu|Lg2ddugg7k%>0JeD+{XZ7>Z~{=|M+sh1MF7~ zz>To~`~LVQe1nNoR-gEzkpe{Ak^7{{ZBk2i_<+`Bq<^GB!RYG+z)h;Y3+<{zlMUYd zrd*W4w&jZ0%kBuDZ1EW&KLpyR7r2=}fF2%0VwHM4pUs}ZI2egi#DRMYZPek*^H9YK zay4Iy3WXFG(F14xYsoDA|KXgGc5%2DhmQ1gFCkrgHBm!lXG8I5h*uf{rn48Z!_@ z4Bk6TJAB2CKYqPjiX&mWoW>OPFGd$wqroa($ne7EUK;#3VYkXaew%Kh^3OrMhtjYN?XEoY`tRPQsAkH-DSL^QqyN0>^ zmC>{#F14jz4GeW{pJoRpLFa_*GI{?T93^rX7SPQgT@LbLqpNA}<@2wH;q493)G=1Y z#-sCiRNX~qf3KgiFzB3I>4Z%AfS(3$`-aMIBU+6?gbgDb!)L~A)je+;fR0jWLL-Fu z4)P{c7{B4Hp91&%??2$v9iRSFnuckHUm}or9seH6 z>%NbT+5*@L5(I9j@06@(!{ZI?U0=pKn8uwIg&L{JV14+8s2hnvbRrU|hZCd}IJu7*;;ECgO%8_*W Kmw_-CKmY()leWbG literal 0 HcmV?d00001 diff --git a/labs/android-mvvm/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/labs/android-mvvm/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..b2dfe3d1ba5cf3ee31b3ecc1ced89044a1f3b7a9 GIT binary patch literal 2898 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 literal 0 HcmV?d00001 diff --git a/labs/android-mvvm/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/labs/android-mvvm/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..4f0f1d64e58ba64d180ce43ee13bf9a17835fbca GIT binary patch literal 982 zcmV;{11bDcNk&G_0{{S5MM6+kP&il$0000G0000l001ul06|PpNU8t;00Dqo+t#w^ z^1csucXz7-Qrhzl9HuHB%l>&>1tG2^vb*E&k^T3$FG1eQZ51g$uv4V+kI`0<^1Z@N zk?Jjh$olyC%l>)Xq;7!>{iBj&BjJ`P&$fsCfpve_epJOBkTF?nu-B7D!hO=2ZR}

C%4 zc_9eOXvPbC4kzU8YowIA8cW~Uv|eB&yYwAObSwL2vY~UYI7NXPvf3b+c^?wcs~_t{ ze_m66-0)^{JdOMKPwjpQ@Sna!*?$wTZ~su*tNv7o!gXT!GRgivP}ec?5>l1!7<(rT zds|8x(qGc673zrvYIz;J23FG{9nHMnAuP}NpAED^laz3mAN1sy+NXK)!6v1FxQ;lh zOBLA>$~P3r4b*NcqR;y6pwyhZ3_PiDb|%n1gGjl3ZU}ujInlP{eks-#oA6>rh&g+!f`hv#_%JrgYPu z(U^&XLW^QX7F9Z*SRPpQl{B%x)_AMp^}_v~?j7 zapvHMKxSf*Mtyx8I}-<*UGn3)oHd(nn=)BZ`d$lDBwq_GL($_TPaS{UeevT(AJ`p0 z9%+hQb6z)U9qjbuXjg|dExCLjpS8$VKQ55VsIC%@{N5t{NsW)=hNGI`J=x97_kbz@ E0Of=7!TQj4N+cqN`nQhxvX7dAV-`K|Ub$-q+H-5I?Tx0g9jWxd@A|?POE8`3b8fO$T))xP* z(X?&brZw({`)WU&rdAs1iTa0x6F@PIxJ&&L|dpySV!ID|iUhjCcKz(@mE z!x@~W#3H<)4Ae(4eQJRk`Iz3<1)6^m)0b_4_TRZ+cz#eD3f8V;2r-1fE!F}W zEi0MEkTTx}8i1{`l_6vo0(Vuh0HD$I4SjZ=?^?k82R51bC)2D_{y8mi_?X^=U?2|F{Vr7s!k(AZC$O#ZMyavHhlQ7 zUR~QXuH~#o#>(b$u4?s~HLF*3IcF7023AlwAYudn0FV~|odGH^05AYPEfR)8p`i{n zwg3zPVp{+wOsxKc>)(pMupKF!Y2HoUqQ3|Yu|8lwR=?5zZuhG6J?H`bSNk_wPoM{u zSL{c@pY7+c2kck>`^q1^^gR0QB7Y?KUD{vz-uVX~;V-rW)PDcI)$_UjgVV?S?=oLR zf4}zz{#*R_{LkiJ#0RdQLNC^2Vp%JPEUvG9ra2BVZ92(p9h7Ka@!yf9(lj#}>+|u* z;^_?KWdzkM`6gqPo9;;r6&JEa)}R3X{(CWv?NvgLeOTq$cZXqf7|sPImi-7cS8DCN zGf;DVt3Am`>hH3{4-WzH43Ftx)SofNe^-#|0HdCo<+8Qs!}TZP{HH8~z5n`ExcHuT zDL1m&|DVpIy=xsLO>8k92HcmfSKhflQ0H~9=^-{#!I1g(;+44xw~=* zxvNz35vfsQE)@)Zsp*6_GjYD};Squ83<_?^SbALb{a`j<0Gn%6JY!zhp=Fg}Ga2|8 z52e1WU%^L1}15Ex0fF$e@eCT(()_P zvV?CA%#Sy08_U6VPt4EtmVQraWJX` zh=N|WQ>LgrvF~R&qOfB$!%D3cGv?;Xh_z$z7k&s4N)$WYf*k=|*jCEkO19{h_(%W4 zPuOqbCw`SeAX*R}UUsbVsgtuG?xs(#Ikx9`JZoQFz0n*7ZG@Fv@kZk`gzO$HoA9kN z8U5{-yY zvV{`&WKU2$mZeoBmiJrEdzUZAv1sRxpePdg1)F*X^Y)zp^Y*R;;z~vOv-z&)&G)JQ{m!C9cmziu1^nHA z`#`0c>@PnQ9CJKgC5NjJD8HM3|KC(g5nnCq$n0Gsu_DXk36@ql%npEye|?%RmG)

FJ$wK}0tWNB{uH;AM~i literal 0 HcmV?d00001 diff --git a/labs/android-mvvm/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/labs/android-mvvm/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..948a3070fe34c611c42c0d3ad3013a0dce358be0 GIT binary patch literal 1900 zcmV-y2b1_xNk&Fw2LJ$9MM6+kP&il$0000G0001A003VA06|PpNH75a00DqwTbm-~ zullQTcXxO9ki!OCRx^i?oR|n!<8G0=kI^!JSjFi-LL*`V;ET0H2IXfU0*i>o6o6Gy zRq6Ap5(_{XLdXcL-MzlN`ugSdZY_`jXhcENAu)N_0?GhF))9R;E`!bo9p?g?SRgw_ zEXHhFG$0{qYOqhdX<(wE4N@es3VIo$%il%6xP9gjiBri+2pI6aY4 zJbgh-Ud|V%3O!IcHKQx1FQH(_*TK;1>FQWbt^$K1zNn^cczkBs=QHCYZ8b&l!UV{K z{L0$KCf_&KR^}&2Fe|L&?1I7~pBENnCtCuH3sjcx6$c zwqkNkru);ie``q+_QI;IYLD9OV0ZxkuyBz|5<$1BH|vtey$> z5oto4=l-R-Aaq`Dk0}o9N0VrkqW_#;!u{!bJLDq%0092{Ghe=F;(kn} z+sQ@1=UlX30+2nWjkL$B^b!H2^QYO@iFc0{(-~yXj2TWz?VG{v`Jg zg}WyYnwGgn>{HFaG7E~pt=)sOO}*yd(UU-D(E&x{xKEl6OcU?pl)K%#U$dn1mDF19 zSw@l8G!GNFB3c3VVK0?uyqN&utT-D5%NM4g-3@Sii9tSXKtwce~uF zS&Jn746EW^wV~8zdQ1XC28~kXu8+Yo9p!<8h&(Q({J*4DBglPdpe4M_mD8AguZFn~ ztiuO~{6Bx?SfO~_ZV(GIboeR9~hAym{{fV|VM=77MxDrbW6`ujX z<3HF(>Zr;#*uCvC*bpoSr~C$h?_%nXps@A)=l_;({Fo#6Y1+Zv`!T5HB+)#^-Ud_; zBwftPN=d8Vx)*O1Mj+0oO=mZ+NVH*ptNDC-&zZ7Hwho6UQ#l-yNvc0Cm+2$$6YUk2D2t#vdZX-u3>-Be1u9gtTBiMB^xwWQ_rgvGpZ6(C@e23c!^K=>ai-Rqu zhqT`ZQof;9Bu!AD(i^PCbYV%yha9zuoKMp`U^z;3!+&d@Hud&_iy!O-$b9ZLcSRh? z)R|826w}TU!J#X6P%@Zh=La$I6zXa#h!B;{qfug}O%z@K{EZECu6zl)7CiNi%xti0 zB{OKfAj83~iJvmpTU|&q1^?^cIMn2RQ?jeSB95l}{DrEPTW{_gmU_pqTc)h@4T>~& zluq3)GM=xa(#^VU5}@FNqpc$?#SbVsX!~RH*5p0p@w z;~v{QMX0^bFT1!cXGM8K9FP+=9~-d~#TK#ZE{4umGT=;dfvWi?rYj;^l_Zxywze`W z^Cr{55U@*BalS}K%Czii_80e0#0#Zkhlij4-~I@}`-JFJ7$5{>LnoJSs??J8kWVl6|8A}RCGAu9^rAsfCE=2}tHwl93t0C?#+jMpvr7O3`2=tr{Hg$=HlnjVG^ewm|Js0J*kfPa6*GhtB>`fN!m#9J(sU!?(OSfzY*zS(FJ<-Vb zfAIg+`U)YaXv#sY(c--|X zEB+TVyZ%Ie4L$gi#Fc++`h6%vzsS$pjz9aLt+ZL(g;n$Dzy5=m=_TV(3H8^C{r0xd zp#a%}ht55dOq?yhwYPrtp-m1xXp;4X;)NhxxUpgP%XTLmO zcjaFva^}dP3$&sfFTIR_jC=2pHh9kpI@2(6V*GQo7Ws)`j)hd+tr@P~gR*2gO@+1? zG<`_tB+LJuF|SZ9tIec;h%}}6WClT`L>HSW?E{Hp1h^+mlbf_$9zA>!ug>NALJsO{ mU%z=YwVD?}XMya)Bp;vlyE5&E_6!fzx9pwrdz474!~g(M6R?N? literal 0 HcmV?d00001 diff --git a/labs/android-mvvm/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/labs/android-mvvm/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..1b9a6956b3acdc11f40ce2bb3f6efbd845cc243f GIT binary patch literal 3918 zcmV-U53%r4Nk&FS4*&pHMM6+kP&il$0000G0001A003VA06|PpNSy@$00HoY|G(*G z+qV7x14$dSO^Re!iqt-AAIE9iwr$(CZQJL$blA4B`>;C3fBY6Q8_YSjb2%a=fc}4E zrSzssacq<^nmW|Rs93PJni30R<8w<(bK_$LO4L?!_OxLl$}K$MUEllnMK|rg=f3;y z*?;3j|Nh>)p0JQ3A~rf(MibH2r+)3cyV1qF&;8m{w-S*y+0mM){KTK^M5}ksc`qX3 zy>rf^b>~l>SSHds8(I@hz3&PD@LmEs4&prkT=BjsBCXTMhN$_)+kvnl0bLKW5rEsj z*d#KXGDB4P&>etx0X+`R19yC=LS)j!mgs5M0L~+o-T~Jl!p!AJxnGAhV%~rhYUL4hlWhgES3Kb5oA&X z{}?3OBSS-{!v$nCIGj->(-TAG)8LR{htr41^gxsT8yqt2@DEG6Yl`Uma3Nd4;YUoW zTbkYl3CMU5ypMF3EIkYmWL|*BknM`0+Kq6CpvO(y$#j94e+q{vI{Zp8cV_6RK!`&C zob$*5Q|$IZ09dW=L!V zw@#2wviu|<#3lgGE8GEhcx+zBt`} zOwP8j9X%^f7i_bth4PiJ$LYtFJSCN$3xwDN;8mr*B;CJwBP2G0TMq0uNt7S^DO_wE zepk!Wrn#Z#03j{`c*Rf~y3o7?J}w?tEELRUR2cgxB*Y{LzA#pxHgf}q?u5idu>077 zd^=p)`nA}6e`|@`p?u}YU66PP_MA}Zqqe!c{nK&z%Jwq1N4e_q<#4g^xaz=ao;u|6 zwpRcW2Lax=ZGbx=Q*HhlJ`Ns#Y*r0*%!T?P*TTiX;rb)$CGLz=rSUum$)3Qyv{BL2 zO*=OI2|%(Yz~`pNEOnLp>+?T@glq-DujlIp?hdJeZ7ctP4_OKx|5@EOps3rr(pWzg zK4d3&oN-X2qN(d_MkfwB4I)_)!I_6nj2iA9u^pQ{;GckGLxBGrJUM2Wdda!k)Y>lq zmjws>dVQ*vW9lvEMkiN3wE-__6OWD0txS&Qn0n22cyj4Q*8(nG4!G{6OOwNvsrPIL zCl-$W9UwkEUVuLwyD%|inbOF*xMODZ4VMEVAq_zUxZ+K#Gdqf!DW$5f)?7UNOFMz! zrB~tuu=6X2FE(p^iqgxr+?ZK;=yz`e;C$#_@D9Lj-+TDVOrva>(#*PVbaHO>A)mhl z07OJWCqYC60518$!&c`eNBcBW%GnfaQ*$eazV^2_AW?j)h;J1nUjN(I9=0+!RVx~% z3@Tf!P0TE+98jA?WceK-}A1% zW!K)lyKcGqy#M~})315-A#2NXQ`?6NR#Apo=S!oF=JfpX>iR*49ec{7AN$xxpK{D$ z2d%Fz&rdfSqourN$~Y^NFIMV1CZ?J*bMx~H3k&meGtH@q9ra2vZxmA$S(#jaaj-g4 ztJmxG+DLV<*q<|sDXPp$X>E)#S}Vm&sRaO5P&goh2><}FEdZSXDqsL$06sAkh(e+v zAsBhKSRexgwg6tIy~GFJzaTxXD(}|+0eOwFDA%rn`X;MVwDHT9=4=g%OaJ9s%3b9>9EUTnnp0t;2Zpa{*>mk~hZqItE_!dQ zOtC>8`$l|mV43Jbudf0N6&&X;{=z}Zi}d1`2qmJ}i|0*GsulD3>GgQXHN)pkR6sf1 z?5ZU%&xtL}oH;YiAA)d*^Ndw2T$+Mjuzyzz@-SM`9df7LqTxLuIwC~S0092~+=qYv z@*ja;?Wt!T!{U?c*Z0YtGe)XbI&y-?B&G2$`JDM)(dIV9G`Sc#6?sI60de6kv+)Qb zUW~2|WjvJq3TA8`0+sWA3zRhY9a~ow)O~&StBkG2{*{TGiY~S8ep{V&Vo2l<6LWsu z^#p0-v*t2?3&aA1)ozu|%efSR=XnpX$lvTeRdKlvM!@|pM5p2w3u-6 zU>}t2xiYLS+{|%C65AzX+23Mtlq?BS&YdYcYsVjoiE&rT>;Necn6l^K)T^lmE`5u{ zm1i+-a-gc;Z&v-{;8r)z6NYfBUv+=_L}ef}qa9FX01)+Aaf+;xj(mL6|JUzGJR1|fnanb%?BPPIp>SCjP|8qE5qJ{=n5ZGw?81z3(k;pzH%1CtlX50{E7h)$h{qGKfzC`e2o`*IqA#tjA z`Fz&^%$b9F*N`)U-#6>a)Z`55`$Dd0cfcs0$d13^ONrdCu9xcv_=n#WQo8stcz3jP9|2EvdI-RhJM3%Q%oM&!OlShM|0 z?gz?wHZSnm45njLtsz8PVT1S&jAlbKg5kVam$p16=EK@Sj4EP0OtH zmJDmdc^v)x>56Qg_wmYHz6h)>kl_h$>0@J!ypv%APmjZTAQVLy6Fu50RGY&JAVNhx zrF_qG6`x9MkT;1SFWo$)l{M$;3qUDn9JwE}z zRl#E_bDRJFii61kPgBybIgp8dNW!Cc1b*^YYk-#oWLJvtM_v^hQx~9?8LD4VFFxBF z3MlrsSC%f9Oupn*ctPL0U1fwfX?`tRhPD{PSLFPQOmIt$mDy0SgpNVvHS+f#Do>h1Gn?LZU9(KaN>Q_=Y*_T zvtD7%_u^^+{g`0VGzg(VZrpVQ6Ub5M=tI_p7T93R8@3Zulu3|#{iNcu!oiHxZ4Rf*( zfmiN$$ru(*_Zqn=`Gq#OuHRTSwp7uH_SokR&|)RuW5yo=Z|_4?qU-JU+tpt>!B&Is z@N(=SG;bpVc;AO@zbmMM zScqq1)b-ZQIrs={oD}|?6y{$HNB1U0^LsBh8JI&3!GBZxOXI<}&5-$lgkAaYqhOTb z?2vEnZ$-kk;*M_17(upJF3%+iH*s0-r{vttXVB2OUwI1s^+G(Ft(U8gYFXC}#P&E^ z>T@C^tS`Z7{6HT4_nF~n>JlZtk5&qDBl6r|^kzQYe`wq!C)n@$c>WOPA61NDFj<<6 zGW71NMMhwAl!U-yqrq2xrSFqRCI8acw7?}3j;ynxo*-b7Co;g5r%^j=H@9({PXXBf z@r>U>>N;E)81wx`B4f%{PB~MHka_);%kBCb(d|Jy5!MqJ%2p`t&@L)4$T2j&-WHvG zv3(uyA_gwqNu(k?jQTtv3dgPKRZoH8prxe7>pQBW5L&dpumS&5Ld2?(sCpJjvc4L5 zEnh&?91WVm)ZdTj=fjJ$pPDdgAttLXuke+?KdKxu*;kTC(r!tQk6;gxj4h%FdHAt(^M3YvYj(!tOeN)+Hvj6+< zzyJRG?^lZfWuR#t!tUKP&(?%3v&Zd$R2YN>lB(Lq`OInY48%4%yTv2 zYe1{G`3)(PDEio5Y@-I5tUf`c%%OCJMtSW56g3iEg%3`$7XSJJHyA z<|7&N)5Xrlgv~%BO24eFd;Hd;uiK%D`EdK|quUeRZDqbh9l)%j%J#0lfrZumvA<_w zu&=AVvdChf6}eqh(bUz`(`Ue*p01{fBAcTgKyDYLs_I+YyJEk+rM@avU~>fB$n)HS zM7pfJydu`i%gfS<{PF94kZDv$t>06sAkheDzu40NJ$5CMW%n^Lls?8^p^QGWURbKu3ZduZQZ((s2? zzE`}<{;Zt7<$C|9R8A~DJ~@%x>TfP zF>TX8)@v|t)q4GjRt<}5s6hLHwRel7>V@&r-O|Av(yh;Q1A{E>Ir>p+%dHD|=l+lT zpr(Dg&>#Nu=!)6bCLr-ZS%|;h)Ij$+e@r8_{qO19QvDe=&1tmpY*0lcA^Cc-#{9fQ z<~$*<&P$Q<_jy#<$40PMofM7aQ}C=jphI`4kLg}Z7CIN#26D{-4v-_CA-LiE@(%{y!BzsU%gG`Q?sjLUf%qFSl0y)2#ae*+EI>s|i`d^V$Dn)qmzqRq6VJRY|{4ujsIU%#bnqU6MR&-1I_43=|5(6Jr;Jvert) zE?S|Tmn}Tv<-??sxV5@9t}3D=>YZ0JrQe$CO~|EY=Lj9RM&4svQHPQL6%pV5fPFiH zfXDx;l@~et{*{U*#c#Dvzu)|znDO7$#CRx)Z&yp-}SrD{&|(MQtfUz~n35@RLfUy=aqrhCX0M}J_r5QsK~NmRCR|Nm&L z41UdsLjWxSUlL41r^0K&nCCK>fdR-!MYjFg(z9_mF^C|#ZQw?`)f6uVzF^`bRnVY& zo}@M06J&_+>w9@jpaO4snmU;0t-(zYW1qVBHtuD!d?%?AtN7Plp><-1Y8Rqb20ZaP zTCgn*-Sri4Q8Xn>=gNaWQ57%!D35UkA@ksOlPB*Dvw}t02ENAqw|kFhn%ZyyW%+t{ zNdM!uqEM^;2}f+tECHbwLmH*!nZVrb$-az%t50Y2pg(HqhvY-^-lb}>^6l{$jOI6} zo_kBzj%8aX|6H5M0Y<)7pzz_wLkIpRm!;PzY)9+24wk2&TT{w--phDGDCOz{cN_ca zpnm7`$oDy=HX%0i-`769*0M6(e5j-?(?24%)<)&46y0e&6@HCDZAm9W6Ib#Y#BF6- z=30crHGg+RRTe%VBC>T00OV6F+gQDAK38Ne3N9bm|62tPccBJi)5{B z4zc^Db72XiBd}v$CF|yU{Z=M|DZ%-(XarYNclODlb1Kz1_EKLy(NSLCN`eUl(rBCL zT*jx@wNvze0|TSqgE(QArOZU)_?qH(sj#TwzElLs9q)(0u!_P|R%Cy_0JFQxgGV>1 zz4?_uq<8_gM0`c*Hh|;UMz~vrg1gQXp{ufg`hM_qU;U>+zmvc5blCLSq@PrEBSGR# z&8=2Z4uXN`F3p73ueD1l{s{k$WipAvSh5W7ABe?4)t;r@V?y`bNB5FvBuE|0VRTb< zM1Hn^?DSsJY+sX@T5xW=#>T9VEV|?<(=6|ge$X6Sb05!LFdjDcoq*gM(Zq=t;_)Le&jyt(&9jzR73noru`a# zN*<`KwGa^gZU3-)MSLF0aFag#f0<>E(bYTeHmtdbns#|I)-$)mJ`q9ctQ8g0=ET?| zdO}eZ*b_p>ygRTtR^5Ggdam=Zb5wmd{}np+Jn1d_=M`~P=M67jj})fH4ztb5yQqQW z^C|C&^LHAK-u+ooIK)yM)QM?t;|<{P;;{`p=BclzAN#JzL4jCwXkQB1Dy{=^KR`=~ zTrr)y7eiYBzSNs_DvO=4A6#EgGS-zY%Vi)N*Yb`U;6o}KR}dq{r9pT5wqZ@3NOE8- z9-(}D|Nc5732CSYQbL)!gPQ#RbD8BhK3dl{sUuPvei0tkvnJBxDEAYTesU8H$)g(Plra{VH(v3u^CO1~(+ zU0O7#)jaS4{NcwA+LuSm&VBcX2#Im3xg)W}ySNw%->orn1taZ&+d)}8gJTqA!u|5P z{yv?zol_3|(1(%M(EVU=cp?L`{Pi|ixk{U)*guFML3P!OSlz;zGA#T+E@8@cgQ_mv1o7RSU=Zo_82F?&&2r;WE z@wk}JHYEZ9nYUc(Vv~iTCa3u8e4q(yq<29VoNbKk|`mq%I6u)My=gPIDuUb&lzf4`MEA9^g8u z)vp8|$$HE9m_BTV?lOosIGa4jud=jIbw)O2eCMfyw2*S8?hjWw^nqws$O*M$3I1)x zR0PWFb3$ySOcGTe1dz%N0l;RPc`x%05FtT^f^j{YCP}*Q=lvp4$ZXrTZQHhO+w%wJn3c8j%+5C3UAFD&%8dBl_qi9D5g8fry}6Ev z2_Q~)5^N$!IU`BPh1O|=BxQ#*C5*}`lluC515$lxc-vNC)IgW=K|=z7o%cWFpndn= zX}f{`!VK02_kU+Q5a3m37J;c} zTzbxteE{GNf?yLt5X=Bzc-mio^Up0nunMCgp*ZJ;%MJvPM3QK)BryP(_v@ei4UvHr z6+sbCifQaOkL6-;5fL8$W($zZ_;CZp305C;~$hhRquZr-r)jjd1z z31%ZK{-(`P#|Um_Sivn@p$-vz46uqT>QG0B1w9znfS9A8PB2LaHdzA|_)yjXVR*l{ zkcu3@vEf7bxH0nkh`q?8FmoO_Ucui*>_a~P?qQrlZ9@+D7%MTpSnztpylXrt5!-k8_QPB?YL8Kx_On8WD zgT+111d(Op$^$&KLAN5+@?>f7F4~wFi(8TL8+szgVmcMDTp5l&k6~=rA{Dt}!gb^r zSWY<)M7D|Z2P0cEodj6E42PV>&>DFmQpgt)E-|#sSUU@uKed+F680H@<;-x{p|nuH4!_mn85rx>wz;0mPi2ZkL#k6;sznu?cXh!T0S>{w6 zL^gvR05NY64l*<+_L>On$rjx9!US;l;LX6@z}yi#2XHh)F@Oo+l)h%fq$v}DNmF2> zfs^_t0)3N-W<9-N?uedVv{)-J0W5mh#29QM5R5h&KuiRM=0Zvnf#lF=K#WlCgc#9c zS;qvh(P$!_a8JwyhI^ZJV2k+B6Z^64?w|1?5gyo6y{}923CRZfYVe1#?F% z7h2SUiNO3;T#JUOyovSs@@C1GtwipycA=*x5{BpIZ_#GCMuV8XK=x;qCNy{d7?wA~ zC+=vjls;ci&zW=6$H~4^K%v{p}Ab?U%C6Z4p%eC<3ExqU$XR<}LLF67A$Sr20DR_pJ3yeBa~ z^sw{V0FI5;UpwXsScYuhbqGQ`YQ25;6p6W^+tgL&;Ml;>S3CGpSZ>VrTn0m1$y$HU z&65)I!c?oREz};c=nLCliriqQX->4uivHTgd${GqeAlf*!P^B|jkU|*IdNP(&6C>4 zqOW$)Nw9nvjy^&`?E|gotDV{JmJ9Q~vuhy<`^C4XIUDt|j4o6rK^e8_(=YqC zuaR6TRVf@tUFHB079o4MBIh{M~4>WwnGgesQH*3?w(RA%hCZ*7)b!aNV=yOQ%o_Y=Lt0Sl*(9^jfRnC210Om$=y>*o|3z} zAR&vAdrB#mWoaB0fJSw9xw|Am$fzK>rx-~R#7IFSAwdu_EI|SRfB*yl0w8oX09H^q zAjl2?0I)v*odGJ40FVGaF&2qJq9Gv`>V>2r0|c`GX8h>CX8eHcOy>S0@<;M3<_6UM z7yCEpug5NZL!H_0>Hg_HasQGxR`rY&Z{geOy?N92Z z{lER^um|$*?*G63*njwc(R?NT)Bei*3jVzR>FWUDb^gKhtL4A=kE_1p-%Fo2`!8M} z(0AjuCiS;G{?*^1tB-uY%=)SRx&D)pK4u@>f6@KPe3}2j_har$>HqzH;UCR^ssFD0 z7h+VLO4o@_Yt>>AeaZKUxqyvxWCAjKB>qjQ30UA)#w z&=RmdwlT`7a8J8Yae=7*c8XL|{@%wA8uvCqfsNX^?UZsS>wX}QD{K}ad4y~iO*p%4 z_cS{u7Ek%?WV6em2(U9#d8(&JDirb^u~7wK4+xP$iiI6IlD|a&S)6o=kG;59N|>K1 zn(0mUqbG3YIY7dQd+*4~)`!S9m7H6HP6YcKHhBc#b%1L}VIisp%;TckEkcu0>lo@u995$<*Em;XNodjTiCdC%R+TX|_ZR#|1`RR|`^@Teh zl#w@8fI1FTx2Dy+{blUT{`^kY*V-AZUd?ZZqCS4gW(kY5?retkLbF=>p=59Nl|=sf zo1Pc|{{N4>5nt#627ylGF`3n>X%`w%bw-Y~zWM_{Si$dc82|=YhISal{N7OY?O`C4 zD|qb}6nLWJ`hUyL+E>-;ricg9J@ZNYP(x(Sct&OI$Y!QWr*=^VN;G3#i>^1n4e#Je zOVhbFbLpXVu*16enDM+ic;97@R~u&kh__kgP#!R`*rQEnA+_dLkNP~L`0alC|J;c; zeiK=s8;BsLE)KbG3BD&Br@(Ha@SBT&$?xX`=$;eeel=|R_dIr6-Ro?=HEjnsJ_b`1 zK6Yg^-6;^2aW!xeTK)A~3Rm|L^FCHB_I>jIju7ZGo&N_1*QHkxH2!!%@o4iZ?vntS;&zJdPe1dH#04YD93A44o-MpfD zP{rn_aq>U%RDvC2+bp;xPlsOzauIi3*Lf42`jVKKZCRuKdYhi>FDuL2l=v{$BCN#Q6796s%r-AG$Q^t(3c@ zD?w0UhYr11@feiyl9kY_@H8~|xlmO<8PfQmj1!$@WieW@VxR@Psxfe-v9WCi1+f>F4VL?0O~K7T?m4-u|pSkBpUJZZe*16_wAp zSYZ@;k`3;W3UHKUWc8QeI}0jH5Ly=cGWQPw(Kr2fm=-5L(d`lcXofy8tJY3@Tuadz zYWXR{mW7XT!RF#RVCe%}=tM*O6!AD3^(!8un~opNI%Uko7$5t@<8+?; zTxDys(MyyGsUjtSu9$+|_-t!U3fVb1dkK?l`17<+jfl=hrBHnDSV>^R1=TnQeyqbW z>ov#l%!1|S!1>8UUxIdhQq`_klcHVx0{?#>K3#$4GlXncwldt!g17TcvKq-jo_996 z>oA=tH9CqRl6Yw?Uc`am!V?lHJbizOJaVaScf1UP5e7Dbgabq=b!B~T&_F6?ooU>w%x0A zH~&MHJ=q`fCH{U<7MDXE4SD32cDZA)WJeWkllJ`UspWaS#eDe^kg^oU_A14UE9zG-a^g{xaXf$})Wik>gT zl#dkzGr(;h0JZDuFn(+k8wNq?PZ5grQ<+sM?wBGt@JnH6v0#or-5wBQWKU~(S_> zkE!tc*ZJ1Y&*p(xX84POb3cClRMd!^qJ#CAZfIepEj-<`VURS_yCz0(?*Ixcj4 z-!zV1_QZhpm=0<;*(nm+F>T=)o?ep@CK5I%g^VAA+RB25ab?7)A~z~egru=I1S|@v zH7tXV!0wmGS^qj#e+MY;C5eUjEAp$Y?LDkS^QPZ}8WN85?r$u<-Epi;yZ1|J2J`se z$D6DpH~2F=eI0B&=UFAUnJvZAmClJlK)sutJ?M>xpZiWV&0=G4MZP+x+p>EX=HbCz zxls%Mw?*u^;LbHWIWCyq+yi)`GmFn9J112CZda_u@YIP%i;srFg_paU02Ifij*7}l z&CF-(3|>*a|+vbNR`^RP=9G?ymEJ0Z~)d&c*UE$UMepZ zcITr{0WqhxkjUnM15js_gW=e3Uh|y6ZReaXHIz-=p`x5VvB&rH9y>Amv@^WmXFEw) zQXYrk3feir=a{jMQ+wDIkkFnZ$k{sJakHn*?u za%4b!00ev8NVLM1TY=cl?KB&55BY_MU-sg?c>=Dbz_W{(Z~c?HJi*XpYL)C6Bd8WH zt+v-#0&o~@t4qESi*)+eW%@VD0|o^yF)n0hME$UtXF$*Lvh}7sso{`|pn*JDIy5^Fm3s$5*zEE=?u5<=l8FJc3r%+H} zdfoNl2J0^~!-*mOL5o-x32|e0Im*E!yY7F7E5N)W3>+v_LBydlEx?4$RL5f2oYRD# zaR0wv(-p~wO0eLDl3K=%`{5+0Gd$ktO=W)gWlGZJ0`K z$_RNA=ckrfa;H0KA~dR^p�(p-{x$&=IACIfoAR!za)F-^da-t3#0Dycnp zwO~NVXwXCl;jE<}>%@xz|=8fIJAB?>+E{7)|4l${4ngA3G|=r z2Dyv;VVWSgZx9Wj>qUjleGl3Ei9K4>h!(lPS%8VOG>Xu0%6VDz^O=bjJmuP7>DeUv zrbI}MlHB^^d?{zv6d=@_ZD2lg1&G7UjnVN{1}9WkaM3H~btX0GtSzB+tZ^qRgWo4m z!GmimlG$=wgXCnr6j@m<1gAL46#T~5Bnm=2{^@>|t&`9mkEPddj zAvG~@Tv~TAm2i%VW}R-g(Z0)z-Y|szHr@rk>4MAyG*Ma*7Yh#H7(!-5>DZ@8r;_dx z{prSe<>~099F8vsYd2xff7uAS%7{S)f(|@me3t2$iy&NEc7OUEchp@9A|X;;IA>8!oX+y(BKJ$EzV* znR$z;!L$s7uy@{OT~nG#B!NRraT8(X##Ho!0r_o@gg0CA-9H^;-uE&?$2$nHv_00o z%cbuUc-tCx$Uh&EZ4Nf4Zgqv)Y6>usG3>GeQnxx_Z6+PcbX-+ysbt1hQ`K1LDpOE? zrAhIZhSN9yVIAOa22gn577tbc&i3|3V8NWy&!tw##`}9*x}gtI^h1DzZRA>UuaJG) zaZ7j)dq!O}{?#8Y7~7i6fHh4{`pL?>-18|p!S75Y#^DM>-S3)vuZG+Q7l@ek zQP~#cBpWgg#mApc_sPYjpw8odQuRokmTkzcNl`^CcKB7e&;zViV;{Y{o^Y$%7i0m# z62%#1Lq!RC?}lK>%mp}T!3Xv;L*0v*>USLm``N%>w>@fwC+#T&Tx2bN4w(20JB}oU zuSa6v^kXi0xPs?pbaOHnyiqq6By1EZY9OZ^^QA>{q-Hsd&m`pbQ%8121aWG-F5xf zlZ%;B{;C>X19|`^_?dVyCq>n+41w7|!tUS!{9rHlbhX=SZO5CQ^;!Du_E7*`GiR^Q w)2!4MKjfSAeNo!9>IaV6aUZ*?W>} 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

UmY2RIYtNPVDpE$%vda+HD#3m z&VuXJ{BK&Qe+rBa7eq}Q(bq|tn(RrJAk|ztj2(i{d>nmQnM?;HF2k&9sA6up5tmjl z7lySlzMbifH17-m-Lwa_F&e7nOH?ESi3#ckR3tsM+jsck3`oG!uMS}|eAwVXv>}qxwq?QY%QJ0}r@^;fhuUA9W z*BVl>TGo&N004@xSiwDUXUvp51sVmqO3m)=B55aPwf@0=e}cN+$-BdKxY`YrT_4)0 z_d10#i44Q*rFr8MC>*)v$EJvz``(pb{e&*6k+b zsMz%($|1+8hn8c2?P(l@;Rb&CsZeYoCI3?2!LqjbwPXW3z4G$Qfj=cT5Yb%vY0(AX oeb?AaKtwrnc|$|zzw9vfvn^aJJ!zd)XFXqqy0000001=f@-~a#s literal 0 HcmV?d00001 diff --git a/labs/android-mvvm/app/src/main/res/values/colors.xml b/labs/android-mvvm/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/labs/android-mvvm/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/labs/android-mvvm/app/src/main/res/values/strings.xml b/labs/android-mvvm/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..58613ed --- /dev/null +++ b/labs/android-mvvm/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + ToDo + \ No newline at end of file diff --git a/labs/android-mvvm/app/src/main/res/values/themes.xml b/labs/android-mvvm/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..4ac2d6f --- /dev/null +++ b/labs/android-mvvm/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +