├── .gitignore ├── .project ├── LICENSE ├── README.md ├── app ├── .classpath ├── .gitignore ├── .project ├── .settings │ └── org.eclipse.buildship.core.prefs ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── cchcc │ │ └── learn │ │ └── amu │ │ ├── e01 │ │ └── E01ActivityTest.kt │ │ ├── e01a │ │ └── E01aActivityTest.kt │ │ ├── e02 │ │ └── E02FragmentTest.kt │ │ ├── e02a │ │ ├── E02aFragmentTest.kt │ │ └── di │ │ │ └── E02aTestModules.kt │ │ ├── e03 │ │ └── E03ActivityTest.kt │ │ ├── e05 │ │ └── E05FragmentTest.kt │ │ └── util │ │ ├── ViewActionsEx.kt │ │ └── ViewAssertionsEx.kt │ ├── debug │ ├── AndroidManifest.xml │ └── java │ │ └── cchcc │ │ └── learn │ │ └── amu │ │ └── FragmentTestActivity.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── cchcc │ │ │ └── learn │ │ │ └── amu │ │ │ ├── MainActivity.kt │ │ │ ├── e01 │ │ │ ├── E01Activity.kt │ │ │ └── E01ViewModel.kt │ │ │ ├── e01a │ │ │ ├── E01aActivity.kt │ │ │ └── E01aViewModel.kt │ │ │ ├── e02 │ │ │ ├── E02Activity.kt │ │ │ ├── E02Fragment.kt │ │ │ ├── E02ViewModel.kt │ │ │ ├── E02ViewModelFactory.kt │ │ │ └── binding │ │ │ │ └── E02Bindings.kt │ │ │ ├── e02a │ │ │ ├── E02aActivity.kt │ │ │ ├── E02aFragment.kt │ │ │ ├── E02aViewModelFactory.kt │ │ │ └── di │ │ │ │ └── E02aModules.kt │ │ │ ├── e03 │ │ │ ├── E03Activity.kt │ │ │ ├── E03MemoAdapter.kt │ │ │ ├── E03ViewModel.kt │ │ │ └── data │ │ │ │ ├── E03DataRepository.kt │ │ │ │ ├── E03Memo.kt │ │ │ │ └── E03MemoRepository.kt │ │ │ ├── e04 │ │ │ ├── E04Activity.kt │ │ │ └── E04ViewModel.kt │ │ │ ├── e05 │ │ │ ├── E05Activity.kt │ │ │ ├── E05Fragment.kt │ │ │ ├── E05LogListAdapter.kt │ │ │ └── E05ViewModel.kt │ │ │ ├── e06 │ │ │ ├── E06Activity.kt │ │ │ ├── E06NumberAdapter.kt │ │ │ ├── E06ViewModel.kt │ │ │ ├── E06ViewModelFactory.kt │ │ │ ├── binding │ │ │ │ └── E06Bindings.kt │ │ │ └── data │ │ │ │ ├── E06DataRepository.kt │ │ │ │ ├── E06Number.kt │ │ │ │ ├── E06ThreeTimesTableDataSource.kt │ │ │ │ └── E06ThreeTimesTableDataSourceFactory.kt │ │ │ ├── e07 │ │ │ ├── E07ViewModelFactory.kt │ │ │ ├── E07WordActivity.kt │ │ │ ├── E07WordListActivity.kt │ │ │ ├── E07WordListAdapter.kt │ │ │ ├── E07WordListViewModel.kt │ │ │ ├── E07WordSharedElementCallback.kt │ │ │ ├── E07WordViewModel.kt │ │ │ └── coordinator │ │ │ │ ├── E07Navigator.kt │ │ │ │ ├── E07NavigatorImpl.kt │ │ │ │ ├── E07WordCoordinator.kt │ │ │ │ └── E07WordListCoordinator.kt │ │ │ └── util │ │ │ ├── ChangeTextSize.kt │ │ │ ├── SingleLiveEvent.kt │ │ │ ├── StartActivityResult.kt │ │ │ └── UserPermission.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_arrow_upward_24dp.xml │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_e01.xml │ │ ├── activity_e01a.xml │ │ ├── activity_e02.xml │ │ ├── activity_e03.xml │ │ ├── activity_e04.xml │ │ ├── activity_e05.xml │ │ ├── activity_e06.xml │ │ ├── activity_e07_word.xml │ │ ├── activity_e07_wordlist.xml │ │ ├── activity_main.xml │ │ ├── fragment_e02.xml │ │ ├── fragment_e05.xml │ │ ├── listitem_e03.xml │ │ ├── listitem_e05.xml │ │ ├── listitem_e06.xml │ │ └── listitem_e07.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── raw │ │ ├── e02_failed.json │ │ └── e02_succes.json │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── cchcc │ └── learn │ └── amu │ ├── e01 │ └── E01ViewModelTest.kt │ ├── e01a │ └── E01aViewModelTest.kt │ ├── e02 │ └── E02ViewModelTest.kt │ ├── e02a │ ├── E02aViewModelTest.kt │ └── di │ │ └── E02aTestModules.kt │ ├── e03 │ └── E03ViewModelTest.kt │ ├── e04 │ └── E04ViewModelTest.kt │ ├── e05 │ └── E05ViewModelTest.kt │ └── e06 │ └── E06ViewModelTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # IntelliJ 36 | *.iml 37 | .idea 38 | .settings 39 | 40 | # Keystore files 41 | # Uncomment the following line if you do not want to check your keystore files in. 42 | #*.jks 43 | 44 | # External native build folder generated in Android Studio 2.2 and later 45 | .externalNativeBuild 46 | 47 | # Google Services (e.g. APIs or Firebase) 48 | google-services.json 49 | 50 | # Freeline 51 | freeline.py 52 | freeline/ 53 | freeline_project_description.json 54 | 55 | # fastlane 56 | fastlane/report.xml 57 | fastlane/Preview.html 58 | fastlane/screenshots 59 | fastlane/test_output 60 | fastlane/readme.md 61 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | android-mvvm-unittest 4 | Project android-mvvm-unittest created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.buildship.core.gradleprojectbuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.buildship.core.gradleprojectnature 16 | 17 | 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 cchcc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # android-mvvm-unittest 2 | To learn android [MVVM](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel) based on [Android Architecture Components](https://developer.android.com/topic/libraries/architecture/) and unit test. 3 | 4 | - [Kotlin](https://kotlinlang.org/) :heart: 5 | - Start with basic and simple. 6 | - Each example should has a unit test at least or more. 7 | 8 | 9 | ## E01 - Simple Activity 10 | 11 | Simple [Data Binding](https://developer.android.com/topic/libraries/data-binding/), 12 | [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel), 13 | [LiveData](https://developer.android.com/topic/libraries/architecture/livedata) on an Activity. 14 | 15 | Simple Unit Test with [Espresso](https://developer.android.com/training/testing/espresso/). 16 | 17 | ## E01a - Simple Activity and Two-way binding 18 | 19 | Base on [E01](#e01---simple-activity) 20 | 21 | [MediatorLiveData](https://developer.android.com/reference/android/arch/lifecycle/MediatorLiveData) to merge events of two `LiveData` 22 | 23 | ## E02 - Simple Fragment with some animations 24 | 25 | Simple [Data Binding](https://developer.android.com/topic/libraries/data-binding/), 26 | [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel), 27 | [LiveData](https://developer.android.com/topic/libraries/architecture/livedata) on a Fragment. 28 | 29 | Some animations with [lottie](http://airbnb.io/lottie/)(resources by [Eddy Gann](https://www.lottiefiles.com/ed117)). 30 | 31 | Custom binding attribute `onAnimationEnd` using 32 | [BindingAdapter](https://developer.android.com/topic/libraries/data-binding/binding-adapters#custom-logic). 33 | 34 | For single Fragment android instrumented test, 35 | there is a dummy activity which only works on debug and test that implemented with 36 | debug [Source set](https://developer.android.com/studio/build/build-variants#sourcesets) and 37 | [@RestrictTo](https://developer.android.com/reference/android/support/annotation/RestrictTo) annotation. 38 | 39 | For unit test, it needs to decouple the logic that getting boolean result from ViewModel. 40 | so [ViewModelProvider.Factory](https://developer.android.com/reference/android/arch/lifecycle/ViewModelProvider.Factory) is here. 41 | 42 | 43 | ## E02a - Simple Fragment with some animations and DI library 44 | 45 | Based on [E02](#e02---simple-fragment-with-some-animations). 46 | 47 | [Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection) with [Kodein DI](http://kodein.org/Kodein-DI/) as a elegant way to decouple the logic. 48 | 49 | 50 | ## E03 - RecyclerView 51 | 52 | Simple [Data Binding](https://developer.android.com/topic/libraries/data-binding/), 53 | [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel), 54 | [LiveData](https://developer.android.com/topic/libraries/architecture/livedata) on a `RecyclerView`. 55 | 56 | Implementation of Add, Remove and Edit actions. 57 | 58 | ## E04 - Request Permission 59 | 60 | [Request Permission](https://developer.android.com/training/permissions/requesting) on MVVM. 61 | 62 | [Transformaions.map](https://developer.android.com/topic/libraries/architecture/livedata?hl=ko#transform_livedata) to convert strings to a simple format. 63 | 64 | [MockK](http://mockk.io/) for unit test. 65 | 66 | ## E05 - Interaction between 2 Fragments 67 | 68 | Reuse [E02](#e02---simple-fragment-with-some-animations). 69 | 70 | A Fragment knows two `ViewModel`, so two views observe one `ViewModel`. 71 | 72 | [ListAdapter](https://developer.android.com/reference/android/support/v7/recyclerview/extensions/ListAdapter) for `RecyclerView`. 73 | 74 | ## E06 - Infinite RecyclerView 75 | 76 | [Paging library](https://developer.android.com/topic/libraries/architecture/paging/) 77 | 78 | ## E07 - Change Screen 79 | 80 | Using Coordinator for `ViewModel`. By doing so, changing screen logic are in one place. 81 | 82 | [Transition Framework](https://developer.android.com/training/transitions/start-activity) -------------------------------------------------------------------------------- /app/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | app 4 | Project app created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.buildship.core.gradleprojectbuilder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.eclipse.buildship.core.gradleprojectnature 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | connection.project.dir=.. 2 | eclipse.preferences.version=1 3 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'kotlin-kapt' 5 | 6 | android { 7 | compileSdkVersion 29 8 | defaultConfig { 9 | applicationId "cchcc.learn.amu" 10 | minSdkVersion 16 11 | targetSdkVersion 29 12 | versionCode 1 13 | versionName "1.0" 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | } 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | dataBinding { 23 | enabled = true 24 | } 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_1_8 27 | targetCompatibility JavaVersion.VERSION_1_8 28 | } 29 | testOptions { 30 | unitTests.returnDefaultValues = true 31 | } 32 | } 33 | 34 | dependencies { 35 | implementation fileTree(dir: 'libs', include: ['*.jar']) 36 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 37 | 38 | implementation 'androidx.appcompat:appcompat:1.0.2' 39 | implementation 'com.google.android.material:material:1.0.0' 40 | implementation 'androidx.recyclerview:recyclerview:1.0.0' 41 | implementation 'androidx.cardview:cardview:1.0.0' 42 | 43 | implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2' 44 | implementation 'com.airbnb.android:lottie:2.5.6' 45 | 46 | testImplementation 'junit:junit:4.12' 47 | 48 | androidTestImplementation 'androidx.test:core:1.2.0' 49 | androidTestImplementation 'androidx.test:runner:1.2.0' 50 | androidTestImplementation 'androidx.test:rules:1.2.0' 51 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 52 | 53 | def mock_version = "1.9.2" 54 | testImplementation "io.mockk:mockk:$mock_version" 55 | androidTestImplementation "io.mockk:mockk-android:$mock_version" 56 | 57 | def espresso_version = "3.1.1" 58 | androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version" 59 | androidTestImplementation "androidx.test.espresso:espresso-contrib:$espresso_version" 60 | 61 | def support_test_version = "1.1.0" 62 | androidTestImplementation "androidx.test:runner:$support_test_version" 63 | androidTestImplementation "androidx.test:rules:$support_test_version" 64 | 65 | def lifecycle_version = "2.0.0" 66 | implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" 67 | implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" 68 | testImplementation "androidx.arch.core:core-testing:$lifecycle_version" 69 | 70 | def kodein_version = "6.1.0" 71 | implementation "org.kodein.di:kodein-di-generic-jvm:$kodein_version" 72 | implementation "org.kodein.di:kodein-di-framework-android-x:$kodein_version" 73 | 74 | def paging_version = "2.1.0" 75 | implementation "androidx.paging:paging-runtime:$paging_version" 76 | testImplementation "androidx.paging:paging-common:$paging_version" 77 | } 78 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/cchcc/learn/amu/e01/E01ActivityTest.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e01 2 | 3 | import androidx.test.espresso.Espresso 4 | import androidx.test.espresso.action.ViewActions 5 | import androidx.test.espresso.matcher.ViewMatchers.withId 6 | import androidx.test.ext.junit.runners.AndroidJUnit4 7 | import androidx.test.rule.ActivityTestRule 8 | import cchcc.learn.amu.R 9 | import cchcc.learn.amu.util.ViewAssertionsEx 10 | import org.junit.Rule 11 | import org.junit.Test 12 | import org.junit.runner.RunWith 13 | 14 | @RunWith(AndroidJUnit4::class) 15 | class E01ActivityTest { 16 | @get:Rule 17 | val rule = ActivityTestRule(E01Activity::class.java) 18 | 19 | private fun plus() { 20 | Espresso.onView(withId(R.id.et_left)).perform(ViewActions.typeText("1")) 21 | Espresso.onView(withId(R.id.et_right)).perform(ViewActions.typeText("1")) 22 | Espresso.onView(withId(R.id.bt_plus)).perform(ViewActions.click()) 23 | } 24 | 25 | @Test 26 | fun result_should_be_invisible_when_input() { 27 | // given 28 | plus() // result view is visible 29 | 30 | // when 31 | Espresso.onView(withId(R.id.et_left)).perform(ViewActions.typeText("11")) 32 | 33 | // then 34 | Espresso.onView(withId(R.id.tv_result)).check(ViewAssertionsEx.isInvisible()) 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/cchcc/learn/amu/e01a/E01aActivityTest.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e01a 2 | 3 | import androidx.test.espresso.Espresso 4 | import androidx.test.espresso.action.ViewActions 5 | import androidx.test.espresso.assertion.ViewAssertions.matches 6 | import androidx.test.espresso.matcher.ViewMatchers 7 | import androidx.test.ext.junit.runners.AndroidJUnit4 8 | import androidx.test.rule.ActivityTestRule 9 | import cchcc.learn.amu.R 10 | import org.junit.Rule 11 | import org.junit.Test 12 | import org.junit.runner.RunWith 13 | 14 | 15 | @RunWith(AndroidJUnit4::class) 16 | class E01aActivityTest { 17 | @get:Rule 18 | val rule = ActivityTestRule(E01aActivity::class.java) 19 | 20 | @Test 21 | fun one_plus_one_is_two() { 22 | // given 23 | val givenVal = "1" 24 | 25 | // when 26 | Espresso.onView(ViewMatchers.withId(R.id.et_left)).perform(ViewActions.typeText(givenVal)) 27 | Espresso.onView(ViewMatchers.withId(R.id.et_right)).perform(ViewActions.typeText(givenVal)) 28 | 29 | // then 30 | val expectedText = String.format(rule.activity.getString(R.string.e01_result_format),givenVal, givenVal, "2") 31 | Espresso.onView(ViewMatchers.withId(R.id.tv_result)).check(matches(ViewMatchers.withText(expectedText))) 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/cchcc/learn/amu/e02/E02FragmentTest.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e02 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import androidx.test.espresso.Espresso 6 | import androidx.test.espresso.action.ViewActions 7 | import androidx.test.espresso.assertion.ViewAssertions.matches 8 | import androidx.test.espresso.matcher.ViewMatchers.* 9 | import androidx.test.ext.junit.runners.AndroidJUnit4 10 | import androidx.test.rule.ActivityTestRule 11 | import cchcc.learn.amu.FragmentTestActivity 12 | import cchcc.learn.amu.R 13 | import cchcc.learn.amu.util.ViewActionsEx 14 | import org.junit.Before 15 | import org.junit.Rule 16 | import org.junit.Test 17 | import org.junit.runner.RunWith 18 | 19 | @RunWith(AndroidJUnit4::class) 20 | class E02FragmentTest { 21 | @get:Rule 22 | val rule = object : ActivityTestRule(FragmentTestActivity::class.java) {} 23 | 24 | private fun justTrue() = true 25 | 26 | @Before 27 | fun setFragment() { 28 | 29 | // given 30 | val createVMFactory = { 31 | object : ViewModelProvider.Factory { 32 | @Suppress("UNCHECKED_CAST") 33 | override fun create(modelClass: Class): T = E02ViewModel(::justTrue).apply { 34 | speedOfAnim.value = 10.0f 35 | } as T 36 | } 37 | } 38 | 39 | rule.activity.replaceFragment(E02Fragment.newInstance(createVMFactory)) 40 | } 41 | 42 | 43 | @Test 44 | fun score_should_increase_when_success() { 45 | // when 46 | Espresso.onView(withId(R.id.bt_try)).perform(ViewActions.click()) 47 | Espresso.onView(isRoot()).perform(ViewActionsEx.waiting(1000)) // waiting to end animation 48 | 49 | // then 50 | val expected = String.format(rule.activity.resources.getString(R.string.e02_score_format), 1) 51 | Espresso.onView(withId(R.id.tv_score)).check(matches(withText(expected))) 52 | } 53 | 54 | 55 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/cchcc/learn/amu/e02a/E02aFragmentTest.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e02a 2 | 3 | import androidx.test.espresso.Espresso 4 | import androidx.test.espresso.action.ViewActions 5 | import androidx.test.espresso.assertion.ViewAssertions.matches 6 | import androidx.test.espresso.matcher.ViewMatchers.* 7 | import androidx.test.ext.junit.runners.AndroidJUnit4 8 | import androidx.test.rule.ActivityTestRule 9 | import cchcc.learn.amu.FragmentTestActivity 10 | import cchcc.learn.amu.R 11 | import cchcc.learn.amu.e02a.di.E02aTestFragmentModule 12 | import cchcc.learn.amu.util.ViewActionsEx 13 | import org.junit.Before 14 | import org.junit.Rule 15 | import org.junit.Test 16 | import org.junit.runner.RunWith 17 | import org.kodein.di.Kodein 18 | 19 | @RunWith(AndroidJUnit4::class) 20 | class E02aFragmentTest { 21 | @get:Rule 22 | val rule = object : ActivityTestRule(FragmentTestActivity::class.java) {} 23 | 24 | @Before 25 | fun setFragment() { 26 | val createKodein:() -> Kodein = { 27 | Kodein.lazy { import(E02aTestFragmentModule) } 28 | } 29 | 30 | rule.activity.replaceFragment(E02aFragment.newInstance(createKodein)) 31 | } 32 | 33 | 34 | @Test 35 | fun score_should_increase_when_success() { 36 | // when 37 | Espresso.onView(withId(R.id.lav_result)).perform(ViewActionsEx.setSpeed(10.0f)) 38 | Espresso.onView(withId(R.id.bt_try)).perform(ViewActions.click()) 39 | Espresso.onView(isRoot()).perform(ViewActionsEx.waiting(1000)) // waiting to end animation 40 | 41 | // then 42 | val expected = String.format(rule.activity.resources.getString(R.string.e02_score_format), 1) 43 | Espresso.onView(withId(R.id.tv_score)).check(matches(withText(expected))) 44 | } 45 | 46 | 47 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/cchcc/learn/amu/e02a/di/E02aTestModules.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e02a.di 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import androidx.lifecycle.ViewModelProviders 6 | import androidx.fragment.app.Fragment 7 | import cchcc.learn.amu.e02.E02ViewModel 8 | import org.kodein.di.Kodein 9 | import org.kodein.di.generic.bind 10 | import org.kodein.di.generic.instance 11 | import org.kodein.di.generic.provider 12 | import org.kodein.di.generic.singleton 13 | 14 | 15 | val E02aTestFragmentModule = Kodein.Module("E02aTestFragmentModule") { 16 | bind() with singleton { 17 | object : ViewModelProvider.Factory { 18 | @Suppress("UNCHECKED_CAST") 19 | override fun create(modelClass: Class): T = E02ViewModel({ true }) as T 20 | } 21 | } 22 | bind() with provider { 23 | ViewModelProviders.of(context as Fragment, instance()).get(E02ViewModel::class.java) 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/cchcc/learn/amu/e03/E03ActivityTest.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e03 2 | 3 | import androidx.test.espresso.Espresso 4 | import androidx.test.espresso.UiController 5 | import androidx.test.espresso.ViewAction 6 | import androidx.test.espresso.action.ViewActions.click 7 | import androidx.test.espresso.action.ViewActions.replaceText 8 | import androidx.test.espresso.contrib.RecyclerViewActions 9 | import androidx.test.espresso.matcher.ViewMatchers.* 10 | import androidx.test.rule.ActivityTestRule 11 | import android.view.View 12 | import android.view.ViewConfiguration 13 | import android.widget.EditText 14 | import android.widget.TextView 15 | import androidx.test.ext.junit.runners.AndroidJUnit4 16 | import cchcc.learn.amu.R 17 | import cchcc.learn.amu.util.ViewActionsEx 18 | import org.hamcrest.Matcher 19 | import org.junit.Assert 20 | import org.junit.Rule 21 | import org.junit.Test 22 | import org.junit.runner.RunWith 23 | 24 | @RunWith(AndroidJUnit4::class) 25 | class E03ActivityTest { 26 | @get:Rule 27 | val rule = object : ActivityTestRule(E03Activity::class.java) {} 28 | 29 | @Test 30 | fun add() { 31 | // given 32 | val givenNewContent = "new content" 33 | 34 | // when 35 | Espresso.onView(withId(R.id.et_content_new)).perform(click(), replaceText(givenNewContent)) 36 | Espresso.onView(withId(R.id.bt_add)).perform(click()) 37 | 38 | // then 39 | Espresso.onView(withId(R.id.rcv_contents)) 40 | .perform(RecyclerViewActions.actionOnItemAtPosition(0 41 | , object : ViewAction { 42 | override fun getDescription(): String = "check added memo content is \"$givenNewContent\"" 43 | 44 | override fun getConstraints(): Matcher = hasDescendant(withText(givenNewContent)) 45 | 46 | override fun perform(uiController: UiController?, view: View?) { 47 | @Suppress("UNCHECKED_CAST") 48 | val matcher = constraints as Matcher 49 | Assert.assertThat(view, matcher) 50 | } 51 | 52 | })) 53 | } 54 | 55 | 56 | @Test 57 | fun remove() { 58 | // given 59 | val givenIdxWillBeRemoved = 3 60 | var beforeContent = "" 61 | 62 | Espresso.onView(withId(R.id.rcv_contents)) 63 | .perform(RecyclerViewActions.actionOnItemAtPosition(givenIdxWillBeRemoved 64 | , object : ViewAction { 65 | override fun getDescription(): String = "get before content" 66 | 67 | override fun getConstraints(): Matcher = isDisplayed() 68 | 69 | override fun perform(uiController: UiController, view: View) { 70 | beforeContent = view.findViewById(R.id.et_content).text.toString() 71 | } 72 | })) 73 | 74 | // when 75 | Espresso.onView(withId(R.id.rcv_contents)) 76 | .perform(RecyclerViewActions.actionOnItemAtPosition(givenIdxWillBeRemoved 77 | , object : ViewAction { 78 | override fun getDescription(): String = "click remove" 79 | 80 | override fun getConstraints(): Matcher = isDisplayed() 81 | 82 | override fun perform(uiController: UiController, view: View) { 83 | view.findViewById(R.id.bt_remove).performClick() 84 | uiController.loopMainThreadForAtLeast(ViewConfiguration.getTapTimeout().toLong()) 85 | } 86 | })) 87 | 88 | 89 | // then 90 | Espresso.onView(withId(R.id.rcv_contents)) 91 | .perform(RecyclerViewActions.actionOnItemAtPosition(givenIdxWillBeRemoved 92 | , object : ViewAction { 93 | override fun getDescription(): String = "check remove" 94 | 95 | override fun getConstraints(): Matcher = isDisplayed() 96 | 97 | override fun perform(uiController: UiController, view: View) { 98 | val content = view.findViewById(R.id.et_content).text.toString() 99 | Assert.assertFalse(content == beforeContent) 100 | } 101 | })) 102 | } 103 | 104 | 105 | @Test 106 | fun edit() { 107 | // given 108 | val givenIdxWillBeEdit = 3 109 | val givenReplaceText = "replaced!!" 110 | 111 | 112 | // when 113 | Espresso.onView(withId(R.id.rcv_contents)) 114 | .perform(RecyclerViewActions.actionOnItemAtPosition(givenIdxWillBeEdit 115 | , object : ViewAction { 116 | override fun getDescription(): String = "get before content" 117 | 118 | override fun getConstraints(): Matcher = isDisplayed() 119 | 120 | override fun perform(uiController: UiController, view: View) { 121 | view.findViewById(R.id.et_content).setText(givenReplaceText) 122 | } 123 | })) 124 | 125 | 126 | Espresso.onView(withId(R.id.rcv_contents)) // scrolling 127 | .perform(ViewActionsEx.waiting(500) 128 | , RecyclerViewActions.scrollToPosition(15) 129 | , ViewActionsEx.waiting(500) 130 | , RecyclerViewActions.scrollToPosition(0) 131 | , ViewActionsEx.waiting(500) 132 | ) 133 | 134 | 135 | // then 136 | Espresso.onView(withId(R.id.rcv_contents)) 137 | .perform(RecyclerViewActions.actionOnItemAtPosition(givenIdxWillBeEdit 138 | , object : ViewAction { 139 | override fun getDescription(): String = "check replaced text" 140 | 141 | override fun getConstraints(): Matcher = isDisplayed() 142 | 143 | override fun perform(uiController: UiController, view: View) { 144 | val content = view.findViewById(R.id.et_content).text.toString() 145 | Assert.assertEquals(givenReplaceText, content) 146 | } 147 | })) 148 | 149 | } 150 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/cchcc/learn/amu/e05/E05FragmentTest.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e05 2 | 3 | import androidx.lifecycle.Observer 4 | import androidx.test.espresso.Espresso.onView 5 | import androidx.test.espresso.matcher.ViewMatchers.withId 6 | import androidx.test.ext.junit.runners.AndroidJUnit4 7 | import androidx.test.internal.runner.junit4.statement.UiThreadStatement 8 | import androidx.test.rule.ActivityTestRule 9 | import cchcc.learn.amu.FragmentTestActivity 10 | import cchcc.learn.amu.R 11 | import cchcc.learn.amu.e02.E02ViewModel 12 | import cchcc.learn.amu.util.ViewActionsEx 13 | import cchcc.learn.amu.util.ViewAssertionsEx 14 | import org.junit.Assert 15 | import org.junit.Before 16 | import org.junit.Rule 17 | import org.junit.Test 18 | import org.junit.runner.RunWith 19 | 20 | @RunWith(AndroidJUnit4::class) 21 | class E05FragmentTest { 22 | @get:Rule 23 | val rule = ActivityTestRule(FragmentTestActivity::class.java) 24 | 25 | lateinit var fragment: E05Fragment 26 | lateinit var viewModel: E02ViewModel 27 | 28 | private fun justTrue() = true 29 | 30 | @Before 31 | fun setFragment() { 32 | UiThreadStatement.runOnUiThread { 33 | viewModel = E02ViewModel(::justTrue) 34 | fragment = E05Fragment.newInstance { viewModel } 35 | rule.activity.replaceFragment(fragment) 36 | } 37 | } 38 | 39 | @Test 40 | fun tryResult_should_add_list() { 41 | // when 42 | UiThreadStatement.runOnUiThread { 43 | viewModel.tryResult() 44 | viewModel.applyScore() 45 | } 46 | 47 | // then 48 | onView(withId(R.id.rcv_log)).check(ViewAssertionsEx.hasItemCountOfRecyclerView(4)) 49 | } 50 | 51 | @Test 52 | fun clear_should_clear_list() { 53 | // when 54 | UiThreadStatement.runOnUiThread { viewModel.tryResult() 55 | viewModel.applyScore() 56 | viewModel.clear() 57 | } 58 | 59 | // then 60 | onView(withId(R.id.rcv_log)).check(ViewAssertionsEx.hasItemCountOfRecyclerView(1)) 61 | } 62 | 63 | @Test 64 | fun observing_animSpeed_works() { 65 | UiThreadStatement.runOnUiThread { 66 | viewModel.speedOfAnim.observe(fragment, Observer {} ) 67 | } 68 | 69 | // given 70 | val givenProgress = 2 71 | 72 | // when 73 | onView(withId(R.id.sb_speed)).perform(ViewActionsEx.setProgress(givenProgress)) 74 | 75 | // then 76 | Assert.assertEquals(3.0f, viewModel.speedOfAnim.value) 77 | } 78 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/cchcc/learn/amu/util/ViewActionsEx.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.util 2 | 3 | import androidx.test.espresso.UiController 4 | import androidx.test.espresso.ViewAction 5 | import androidx.test.espresso.matcher.ViewMatchers 6 | import android.view.View 7 | import android.widget.ProgressBar 8 | import com.airbnb.lottie.LottieAnimationView 9 | import org.hamcrest.Matcher 10 | 11 | object ViewActionsEx { 12 | @JvmStatic 13 | fun setSpeed(speed: Float) = object : ViewAction { 14 | override fun getDescription(): String = "set animation speed to : $speed" 15 | 16 | override fun getConstraints(): Matcher = ViewMatchers.isAssignableFrom(LottieAnimationView::class.java) 17 | 18 | override fun perform(uiController: UiController, view: View) { 19 | (view as LottieAnimationView).speed = 10.0f 20 | } 21 | } 22 | 23 | @JvmStatic 24 | fun waiting(milliSec: Long) = object : ViewAction { 25 | override fun getDescription(): String = "waiting $milliSec milli seconds" 26 | 27 | override fun getConstraints(): Matcher = ViewMatchers.isDisplayed() 28 | 29 | override fun perform(uiController: UiController, view: View) = 30 | uiController.loopMainThreadForAtLeast(milliSec) 31 | } 32 | 33 | @JvmStatic 34 | fun setProgress(value: Int) = object : ViewAction { 35 | override fun getDescription(): String = "set progress to $value" 36 | 37 | override fun getConstraints(): Matcher = ViewMatchers.isAssignableFrom(ProgressBar::class.java) 38 | 39 | override fun perform(uiController: UiController, view: View) { 40 | (view as ProgressBar).progress = value 41 | } 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/cchcc/learn/amu/util/ViewAssertionsEx.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.util 2 | 3 | import androidx.test.espresso.ViewAssertion 4 | import androidx.recyclerview.widget.RecyclerView 5 | import android.view.View 6 | import org.junit.Assert 7 | 8 | object ViewAssertionsEx { 9 | @JvmStatic 10 | fun isInvisible() = ViewAssertion { view, noViewFoundException -> 11 | if (noViewFoundException != null ) throw noViewFoundException 12 | Assert.assertEquals(View.INVISIBLE, view.visibility) 13 | } 14 | 15 | @JvmStatic 16 | fun hasItemCountOfRecyclerView(count: Int) = ViewAssertion { view, noViewFoundException -> 17 | if (noViewFoundException != null ) throw noViewFoundException 18 | val v = view as androidx.recyclerview.widget.RecyclerView 19 | Assert.assertEquals(count, v.adapter!!.itemCount) 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/debug/java/cchcc/learn/amu/FragmentTestActivity.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | import android.os.Bundle 5 | import androidx.annotation.RestrictTo 6 | import androidx.fragment.app.Fragment 7 | import android.widget.FrameLayout 8 | 9 | @RestrictTo(RestrictTo.Scope.TESTS) 10 | class FragmentTestActivity : AppCompatActivity() { 11 | 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | 15 | val v = FrameLayout(this).apply { 16 | id = R.id.fl_main 17 | layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT 18 | , FrameLayout.LayoutParams.MATCH_PARENT) 19 | } 20 | 21 | setContentView(v) 22 | } 23 | 24 | inline fun replaceFragment(f: T, tag: String = T::class.java.name) { 25 | supportFragmentManager.beginTransaction() 26 | .replace(R.id.fl_main, f, tag) 27 | .commit() 28 | } 29 | 30 | inline fun addFragment(f: T, tag: String = T::class.java.name) { 31 | supportFragmentManager.beginTransaction() 32 | .add(R.id.fl_main, f, tag) 33 | .commit() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu 2 | 3 | import android.content.Intent 4 | import androidx.databinding.DataBindingUtil 5 | import android.os.Bundle 6 | import androidx.core.app.ActivityOptionsCompat 7 | import androidx.appcompat.app.AppCompatActivity 8 | import cchcc.learn.amu.databinding.ActivityMainBinding 9 | import cchcc.learn.amu.e01.E01Activity 10 | import cchcc.learn.amu.e01a.E01aActivity 11 | import cchcc.learn.amu.e02.E02Activity 12 | import cchcc.learn.amu.e02a.E02aActivity 13 | import cchcc.learn.amu.e03.E03Activity 14 | import cchcc.learn.amu.e04.E04Activity 15 | import cchcc.learn.amu.e05.E05Activity 16 | import cchcc.learn.amu.e06.E06Activity 17 | import cchcc.learn.amu.e07.E07WordListActivity 18 | 19 | class MainActivity : AppCompatActivity() { 20 | 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | DataBindingUtil.setContentView(this, R.layout.activity_main).also { 24 | it.host = this 25 | } 26 | } 27 | 28 | fun startE01() { 29 | startActivity(Intent(this, E01Activity::class.java)) 30 | } 31 | 32 | fun startE01a() { 33 | startActivity(Intent(this, E01aActivity::class.java)) 34 | } 35 | 36 | fun startE02() { 37 | startActivity(Intent(this, E02Activity::class.java)) 38 | } 39 | 40 | fun startE02a() { 41 | startActivity(Intent(this, E02aActivity::class.java)) 42 | } 43 | 44 | fun startE03() { 45 | startActivity(Intent(this, E03Activity::class.java)) 46 | } 47 | 48 | fun startE04() { 49 | startActivity(Intent(this, E04Activity::class.java)) 50 | } 51 | 52 | fun startE05() { 53 | startActivity(Intent(this, E05Activity::class.java)) 54 | } 55 | 56 | fun startE06() { 57 | startActivity(Intent(this, E06Activity::class.java)) 58 | } 59 | 60 | fun startE07() { 61 | startActivity(Intent(this, E07WordListActivity::class.java) 62 | ,ActivityOptionsCompat.makeSceneTransitionAnimation(this).toBundle()) 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e01/E01Activity.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e01 2 | 3 | import androidx.lifecycle.Observer 4 | import androidx.lifecycle.ViewModelProviders 5 | import androidx.databinding.DataBindingUtil 6 | import android.os.Bundle 7 | import androidx.appcompat.app.AppCompatActivity 8 | import cchcc.learn.amu.R 9 | import cchcc.learn.amu.databinding.ActivityE01Binding 10 | 11 | class E01Activity : AppCompatActivity() { 12 | 13 | override fun onCreate(savedInstanceState: Bundle?) { 14 | super.onCreate(savedInstanceState) 15 | 16 | val viewModel = ViewModelProviders.of(this).get(E01ViewModel::class.java) 17 | 18 | DataBindingUtil.setContentView(this, R.layout.activity_e01).let { 19 | it.setLifecycleOwner(this) 20 | it.viewModel = viewModel 21 | } 22 | 23 | val setInvisibleResult = Observer { viewModel.visibleResult.value = false } 24 | viewModel.left.observe(this, setInvisibleResult) 25 | viewModel.right.observe(this, setInvisibleResult) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e01/E01ViewModel.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e01 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.lifecycle.ViewModel 5 | 6 | class E01ViewModel : ViewModel() { 7 | val left = MutableLiveData() 8 | val right = MutableLiveData() 9 | val result = MutableLiveData() 10 | val visibleResult = MutableLiveData().apply { value = false } 11 | 12 | fun plus() { 13 | val leftNum = left.value.toString().toIntOrNull() ?: run { visibleResult.value = false; return } 14 | val rightNum = right.value.toString().toIntOrNull() ?: run { visibleResult.value = false; return } 15 | result.value = (leftNum + rightNum).toString() 16 | visibleResult.value = true 17 | } 18 | 19 | fun clear() { 20 | left.value = "" 21 | right.value = "" 22 | result.value = "" 23 | visibleResult.value = false 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e01a/E01aActivity.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e01a 2 | 3 | import androidx.lifecycle.ViewModelProviders 4 | import androidx.databinding.DataBindingUtil 5 | import androidx.appcompat.app.AppCompatActivity 6 | import android.os.Bundle 7 | import cchcc.learn.amu.R 8 | import cchcc.learn.amu.databinding.ActivityE01aBinding 9 | 10 | class E01aActivity : AppCompatActivity() { 11 | 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | 15 | val viewModel = ViewModelProviders.of(this).get(E01aViewModel::class.java) 16 | 17 | DataBindingUtil.setContentView(this, R.layout.activity_e01a).let { 18 | it.setLifecycleOwner(this) 19 | it.viewModel = viewModel 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e01a/E01aViewModel.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e01a 2 | 3 | import androidx.lifecycle.MediatorLiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.Observer 6 | import androidx.lifecycle.ViewModel 7 | 8 | class E01aViewModel : ViewModel() { 9 | val left = MutableLiveData() 10 | val right = MutableLiveData() 11 | val result = MediatorLiveData().apply { 12 | 13 | val plus: (String?) -> Unit = plus@{ 14 | val leftNum = left.value?.toIntOrNull() 15 | if (leftNum == null) { 16 | visibleResult.value = false 17 | return@plus 18 | } 19 | 20 | val rightNum = right.value?.toIntOrNull() 21 | if (rightNum == null) { 22 | visibleResult.value = false 23 | return@plus 24 | } 25 | 26 | value = (leftNum + rightNum).toString() 27 | visibleResult.value = true 28 | } 29 | 30 | addSource(left, plus) 31 | addSource(right, plus) 32 | } 33 | 34 | val visibleResult = MutableLiveData().apply { value = false } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e02/E02Activity.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e02 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | import android.os.Bundle 5 | import cchcc.learn.amu.R 6 | 7 | class E02Activity : AppCompatActivity() { 8 | 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | setContentView(R.layout.activity_e02) 12 | 13 | supportFragmentManager.beginTransaction() 14 | .replace(R.id.fl_main, E02Fragment.newInstance()) 15 | .commit() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e02/E02Fragment.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e02 2 | 3 | import androidx.lifecycle.Observer 4 | import androidx.lifecycle.ViewModelProvider 5 | import androidx.lifecycle.ViewModelProviders 6 | import androidx.databinding.DataBindingUtil 7 | import android.os.Bundle 8 | import androidx.fragment.app.Fragment 9 | import android.view.LayoutInflater 10 | import android.view.View 11 | import android.view.ViewGroup 12 | import cchcc.learn.amu.R 13 | import cchcc.learn.amu.databinding.FragmentE02Binding 14 | import kotlinx.android.synthetic.main.fragment_e02.* 15 | import java.io.Serializable 16 | 17 | class E02Fragment : androidx.fragment.app.Fragment() { 18 | 19 | private val createVMFactory: () -> ViewModelProvider.Factory by lazy { 20 | @Suppress("UNCHECKED_CAST") 21 | arguments?.getSerializable("createVMFactory") as? () -> ViewModelProvider.Factory 22 | ?: throw IllegalArgumentException("no createVMFactory for ${this::class.java.simpleName}") 23 | } 24 | val viewModel: E02ViewModel by lazy { ViewModelProviders.of(this, createVMFactory()).get(E02ViewModel::class.java) } 25 | 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | super.onCreate(savedInstanceState) 28 | 29 | viewModel.result.observe(this, Observer { 30 | lav_result.setAnimation(when (it) { 31 | E02ViewModel.TryResult.FAILED -> R.raw.e02_failed 32 | E02ViewModel.TryResult.SUCCESS -> R.raw.e02_succes 33 | else -> throw IllegalStateException() 34 | }) 35 | lav_result.playAnimation() 36 | }) 37 | 38 | viewModel.clearAction.observe(this, Observer { 39 | lav_result.cancelAnimation() 40 | lav_result.progress = 0.0f 41 | }) 42 | } 43 | 44 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, 45 | savedInstanceState: Bundle?): View? = 46 | DataBindingUtil.inflate(inflater, R.layout.fragment_e02, container, false).also { 47 | it.setLifecycleOwner(this) 48 | it.viewModel = viewModel 49 | }.root 50 | 51 | companion object { 52 | 53 | @JvmStatic 54 | fun newInstance(createVMFactory: () -> ViewModelProvider.Factory = ::E02ViewModelFactory) = 55 | E02Fragment().apply { 56 | arguments = Bundle().apply { 57 | putSerializable("createVMFactory", createVMFactory as Serializable) 58 | } 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e02/E02ViewModel.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e02 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.lifecycle.ViewModel 5 | 6 | class E02ViewModel(private val nextBoolean: () -> Boolean) : ViewModel() { 7 | 8 | enum class TryResult { FAILED, SUCCESS } 9 | 10 | val score = MutableLiveData().apply { value = 0 } 11 | val result = MutableLiveData() 12 | val speedOfAnim = MutableLiveData().apply { value = 1.0f } 13 | 14 | val clearAction = MutableLiveData() 15 | 16 | fun tryResult() { 17 | result.value = if (nextBoolean()) TryResult.SUCCESS else TryResult.FAILED 18 | } 19 | 20 | fun applyScore() { 21 | val amount = when (result.value) { 22 | E02ViewModel.TryResult.FAILED -> -1 23 | E02ViewModel.TryResult.SUCCESS -> 1 24 | else -> throw IllegalStateException("result.value must be one of TryResult. Call tryResult() first.") 25 | } 26 | 27 | score.value = score.value!! + amount 28 | } 29 | 30 | fun clear() { 31 | score.value = 0 32 | clearAction.value = Unit 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e02/E02ViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e02 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import java.util.* 6 | 7 | class E02ViewModelFactory : ViewModelProvider.Factory { 8 | 9 | private fun randomBooleanGenerator(): () -> Boolean { 10 | val r = Random() 11 | return { r.nextBoolean() } 12 | } 13 | 14 | @Suppress("UNCHECKED_CAST") 15 | override fun create(modelClass: Class): T = when (modelClass) { 16 | E02ViewModel::class.java -> E02ViewModel(randomBooleanGenerator()) 17 | else -> throw IllegalArgumentException("$modelClass is not registered ViewModel") 18 | } as T 19 | 20 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e02/binding/E02Bindings.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e02.binding 2 | 3 | import android.animation.Animator 4 | import androidx.databinding.BindingAdapter 5 | import android.util.Log 6 | import com.airbnb.lottie.LottieAnimationView 7 | 8 | interface LottieOnAnimationEnd { 9 | fun onAnimationEnd(v: LottieAnimationView) 10 | } 11 | 12 | @BindingAdapter("onAnimationEnd") 13 | fun bind_onAnimationEnd(v: LottieAnimationView, listener: LottieOnAnimationEnd) { 14 | Log.d("bind_onAnimationEnd", "binding") 15 | 16 | v.addAnimatorListener(object : Animator.AnimatorListener { 17 | override fun onAnimationRepeat(animation: Animator?) = Unit 18 | 19 | override fun onAnimationEnd(animation: Animator?) { 20 | listener.onAnimationEnd(v) 21 | } 22 | 23 | override fun onAnimationCancel(animation: Animator?) = Unit 24 | 25 | override fun onAnimationStart(animation: Animator?) = Unit 26 | 27 | }) 28 | 29 | } 30 | 31 | @BindingAdapter("animation_speed") 32 | fun bind_animSpeed(v: LottieAnimationView, speed: Float) { 33 | v.speed = speed 34 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e02a/E02aActivity.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e02a 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import cchcc.learn.amu.R 6 | 7 | class E02aActivity : AppCompatActivity() { 8 | 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | setContentView(R.layout.activity_e02) 12 | 13 | supportFragmentManager.beginTransaction() 14 | .replace(R.id.fl_main, E02aFragment.newInstance()) 15 | .commit() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e02a/E02aFragment.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e02a 2 | 3 | import androidx.lifecycle.Observer 4 | import androidx.databinding.DataBindingUtil 5 | import android.os.Bundle 6 | import androidx.fragment.app.Fragment 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import cchcc.learn.amu.R 11 | import cchcc.learn.amu.databinding.FragmentE02Binding 12 | import cchcc.learn.amu.e02.E02ViewModel 13 | import cchcc.learn.amu.e02a.di.E02aFragmentModule 14 | import kotlinx.android.synthetic.main.fragment_e02.* 15 | import org.kodein.di.Kodein 16 | import org.kodein.di.KodeinAware 17 | import org.kodein.di.generic.instance 18 | import java.io.Serializable 19 | 20 | class E02aFragment : androidx.fragment.app.Fragment(), KodeinAware { 21 | 22 | // to getting kodein is usually done by closestKodein() that is declared dependencies from parent(Activity, Application) layer. 23 | override val kodein: Kodein by lazy { 24 | @Suppress("UNCHECKED_CAST") 25 | val createKodein = arguments?.getSerializable("createKodein") as? () -> Kodein 26 | ?: throw IllegalArgumentException("no createKodein for ${this::class.java.simpleName}") 27 | 28 | createKodein() 29 | } 30 | 31 | private val viewModel: E02ViewModel by instance() 32 | 33 | override fun onCreate(savedInstanceState: Bundle?) { 34 | super.onCreate(savedInstanceState) 35 | 36 | viewModel.result.observe(this, Observer { 37 | lav_result.setAnimation(when (it) { 38 | E02ViewModel.TryResult.FAILED -> R.raw.e02_failed 39 | E02ViewModel.TryResult.SUCCESS -> R.raw.e02_succes 40 | else -> throw IllegalStateException() 41 | }) 42 | lav_result.playAnimation() 43 | }) 44 | 45 | viewModel.clearAction.observe(this, Observer { 46 | lav_result.cancelAnimation() 47 | lav_result.progress = 0.0f 48 | }) 49 | } 50 | 51 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, 52 | savedInstanceState: Bundle?): View? = 53 | DataBindingUtil.inflate(inflater, R.layout.fragment_e02, container, false).also { 54 | it.setLifecycleOwner(this) 55 | it.viewModel = viewModel 56 | }.root 57 | 58 | companion object { 59 | 60 | private val _createKodein: () -> Kodein by lazy { 61 | { 62 | Kodein.lazy { import(E02aFragmentModule) } 63 | } 64 | } 65 | 66 | @JvmStatic 67 | fun newInstance(createKodein: () -> Kodein = _createKodein) = 68 | E02aFragment().apply { 69 | arguments = Bundle().apply { 70 | putSerializable("createKodein", createKodein as Serializable) 71 | } 72 | } 73 | } 74 | } 75 | 76 | -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e02a/E02aViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e02a 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import cchcc.learn.amu.e02.E02ViewModel 6 | import cchcc.learn.amu.e02a.di.E02aViewModelModule 7 | import org.kodein.di.Kodein 8 | import org.kodein.di.KodeinAware 9 | import org.kodein.di.generic.instance 10 | import org.kodein.di.newInstance 11 | 12 | class E02aViewModelFactory : ViewModelProvider.Factory, KodeinAware { 13 | override val kodein: Kodein = Kodein.lazy { 14 | import(E02aViewModelModule) 15 | } 16 | 17 | @Suppress("UNCHECKED_CAST") 18 | override fun create(modelClass: Class): T { 19 | 20 | val vm by kodein.newInstance { E02ViewModel(instance()) } 21 | 22 | return when (modelClass) { 23 | E02ViewModel::class.java -> vm 24 | else -> throw IllegalArgumentException("$modelClass is not registered ViewModel") 25 | } as T 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e02a/di/E02aModules.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e02a.di 2 | 3 | import androidx.lifecycle.ViewModelProvider 4 | import androidx.lifecycle.ViewModelProviders 5 | import androidx.fragment.app.Fragment 6 | import cchcc.learn.amu.e02.E02ViewModel 7 | import cchcc.learn.amu.e02a.E02aViewModelFactory 8 | import org.kodein.di.Kodein 9 | import org.kodein.di.generic.bind 10 | import org.kodein.di.generic.instance 11 | import org.kodein.di.generic.provider 12 | import org.kodein.di.generic.singleton 13 | import java.util.* 14 | 15 | val E02aViewModelModule = Kodein.Module("E02ViewModel") { 16 | bind<() -> Boolean>() with singleton { 17 | val r = Random() 18 | val nextBoolean: () -> Boolean = { 19 | r.nextBoolean() 20 | } 21 | 22 | nextBoolean 23 | } 24 | } 25 | 26 | val E02aFragmentModule = Kodein.Module("E02Fragment") { 27 | bind() with singleton { 28 | E02aViewModelFactory() 29 | } 30 | bind() with provider { 31 | ViewModelProviders.of(context as Fragment, instance()).get(E02ViewModel::class.java) 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e03/E03Activity.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e03 2 | 3 | import androidx.lifecycle.Observer 4 | import androidx.lifecycle.ViewModelProviders 5 | import androidx.databinding.DataBindingUtil 6 | import android.os.Bundle 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.recyclerview.widget.LinearLayoutManager 9 | import androidx.recyclerview.widget.RecyclerView 10 | import android.widget.Toast 11 | import cchcc.learn.amu.R 12 | import cchcc.learn.amu.databinding.ActivityE03Binding 13 | import cchcc.learn.amu.e03.data.E03Memo 14 | import kotlinx.android.synthetic.main.activity_e03.* 15 | 16 | class E03Activity : AppCompatActivity() { 17 | 18 | private val viewModel: E03ViewModel by lazy { ViewModelProviders.of(this).get(E03ViewModel::class.java) } 19 | private val adapter: androidx.recyclerview.widget.RecyclerView.Adapter by lazy { E03MemoAdapter(viewModel.memos.value!!, viewModel::remove) } 20 | 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | DataBindingUtil.setContentView(this, R.layout.activity_e03).let { 24 | it.setLifecycleOwner(this) 25 | it.viewModel = viewModel 26 | } 27 | 28 | viewModel.listAction.observe(this, Observer { 29 | when(it) { 30 | is E03ViewModel.ListAction.Added -> onAdded() 31 | is E03ViewModel.ListAction.Removed -> onRemoved(it.memo, it.idx) 32 | } 33 | }) 34 | 35 | rcv_contents.let { 36 | it.adapter = adapter 37 | it.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this) 38 | } 39 | } 40 | 41 | private fun onAdded() { 42 | adapter.notifyItemInserted(0) 43 | adapter.notifyItemRangeChanged(1, viewModel.sizeOfMemos -1) 44 | rcv_contents.post { 45 | rcv_contents.scrollToPosition(0) 46 | } 47 | } 48 | 49 | private fun onRemoved(memo: E03Memo, idx: Int) { 50 | adapter.notifyItemRemoved(idx) 51 | adapter.notifyItemRangeChanged(idx, viewModel.sizeOfMemos - idx) 52 | Toast.makeText(this, "\"${memo.content}\" is removed", Toast.LENGTH_SHORT).show() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e03/E03MemoAdapter.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e03 2 | 3 | import androidx.lifecycle.LifecycleOwner 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.Observer 6 | import androidx.databinding.DataBindingUtil 7 | import androidx.recyclerview.widget.RecyclerView 8 | import android.view.LayoutInflater 9 | import android.view.View 10 | import android.view.ViewGroup 11 | import cchcc.learn.amu.R 12 | import cchcc.learn.amu.databinding.ListitemE03Binding 13 | import cchcc.learn.amu.e03.data.E03Memo 14 | 15 | typealias OnListItemEvent = (E03Memo, Int) -> Unit 16 | 17 | class E03MemoAdapter(private val items: List, val onClickRemove: OnListItemEvent) : androidx.recyclerview.widget.RecyclerView.Adapter() { 18 | 19 | inner class VH(view: View) : androidx.recyclerview.widget.RecyclerView.ViewHolder(view) { 20 | val content = MutableLiveData() 21 | 22 | fun onClickRemove(): Unit = this@E03MemoAdapter.onClickRemove(items[layoutPosition], layoutPosition) 23 | } 24 | 25 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH { 26 | val lifecycleOwner = parent.context as LifecycleOwner 27 | val binding = DataBindingUtil.inflate(LayoutInflater.from(parent.context), R.layout.listitem_e03, parent, false) 28 | val vh = VH(binding.root) 29 | 30 | binding.let { 31 | it.setLifecycleOwner(lifecycleOwner) 32 | it.vh = vh 33 | } 34 | 35 | vh.content.observe(lifecycleOwner, Observer { 36 | val memo = items[vh.layoutPosition] 37 | memo.content = it ?: "" 38 | }) 39 | return vh 40 | } 41 | 42 | override fun getItemCount(): Int = items.size 43 | 44 | override fun onBindViewHolder(holder: VH, position: Int): Unit = with(holder) { 45 | val memo = items[position] 46 | content.value = memo.content 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e03/E03ViewModel.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e03 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import cchcc.learn.amu.e03.data.E03DataRepository 7 | import cchcc.learn.amu.e03.data.E03Memo 8 | import cchcc.learn.amu.e03.data.E03MemoRepository 9 | 10 | class E03ViewModel(private val repository: E03DataRepository = E03MemoRepository()) : ViewModel() { 11 | sealed class ListAction { 12 | class Added : ListAction() 13 | class Removed(val memo: E03Memo, val idx: Int) : ListAction() 14 | } 15 | 16 | val memos: LiveData> = MutableLiveData>().apply { 17 | value = repository.list 18 | } 19 | 20 | val listAction: MutableLiveData = MutableLiveData() 21 | val newContent: MutableLiveData = MutableLiveData() 22 | val sizeOfMemos: Int = memos.value!!.size 23 | 24 | fun remove(memo: E03Memo, idx: Int) { 25 | repository.removeAt(idx) 26 | listAction.value = ListAction.Removed(memo, idx) 27 | } 28 | 29 | fun add() { 30 | repository.add(E03Memo(newContent.value ?: "")) 31 | newContent.value = "" 32 | listAction.value = ListAction.Added() 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e03/data/E03DataRepository.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e03.data 2 | 3 | interface E03DataRepository { 4 | val list: List 5 | 6 | fun add(item: T) 7 | fun removeAt(idx: Int) 8 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e03/data/E03Memo.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e03.data 2 | 3 | data class E03Memo(var content: String) -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e03/data/E03MemoRepository.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e03.data 2 | 3 | class E03MemoRepository : E03DataRepository { 4 | 5 | private val memos by lazy { 6 | "The Data Binding Library is a support library that allows you to bind UI components in your layouts to data sources in your app using a declarative format rather than programmatically." 7 | .split(' ') 8 | .map(::E03Memo) 9 | .toMutableList() 10 | } 11 | 12 | override val list: List by lazy { memos } 13 | 14 | override fun add(item: E03Memo) { 15 | memos.add(0, item) 16 | } 17 | 18 | override fun removeAt(idx: Int) { 19 | memos.removeAt(idx) 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e04/E04Activity.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e04 2 | 3 | import android.Manifest 4 | import android.app.Activity 5 | import androidx.lifecycle.Observer 6 | import androidx.lifecycle.ViewModelProviders 7 | import android.content.Intent 8 | import androidx.databinding.DataBindingUtil 9 | import android.os.Bundle 10 | import android.provider.ContactsContract 11 | import androidx.appcompat.app.AppCompatActivity 12 | import cchcc.learn.amu.R 13 | import cchcc.learn.amu.databinding.ActivityE04Binding 14 | import cchcc.learn.amu.util.activityResult 15 | import cchcc.learn.amu.util.permissionOf 16 | import cchcc.learn.amu.util.requestPermissionResult 17 | import cchcc.learn.amu.util.startActivityWithResult 18 | 19 | class E04Activity : AppCompatActivity() { 20 | 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | 24 | val viewModel = ViewModelProviders.of(this).get(E04ViewModel::class.java) 25 | 26 | DataBindingUtil.setContentView(this, R.layout.activity_e04).let { 27 | it.setLifecycleOwner(this) 28 | it.viewModel = viewModel 29 | } 30 | 31 | viewModel.pickContactAction.observe(this, Observer { 32 | permissionOf(Manifest.permission.READ_CONTACTS).checkOrRequest permissionIsGranted@{ 33 | 34 | val intent = Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Phone.CONTENT_URI) 35 | startActivityWithResult(intent) { resultCode, data -> 36 | if (resultCode == Activity.RESULT_OK && data?.data != null) { 37 | 38 | val cursor = contentResolver.query(data.data!!, null, null, null, null) 39 | val nameAndPhone = cursor?.use { 40 | it.moveToFirst() 41 | 42 | val name = it.getString(it.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME)) 43 | val phone = it.getString(it.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DATA)) 44 | 45 | name to phone 46 | } ?: "" to "" 47 | 48 | viewModel.nameAndPhone.value = nameAndPhone 49 | } 50 | } 51 | } 52 | }) 53 | } 54 | 55 | override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { 56 | super.onRequestPermissionsResult(requestCode, permissions, grantResults) 57 | requestPermissionResult(requestCode, grantResults) 58 | } 59 | 60 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 61 | super.onActivityResult(requestCode, resultCode, data) 62 | activityResult(requestCode, resultCode, data) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e04/E04ViewModel.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e04 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.Transformations 6 | import androidx.lifecycle.ViewModel 7 | 8 | 9 | class E04ViewModel : ViewModel() { 10 | 11 | val nameAndPhone = MutableLiveData>() 12 | val contact: LiveData = Transformations.map(nameAndPhone) { 13 | (name, phone) -> "Name : $name\nPhone : $phone" 14 | } 15 | val pickContactAction = MutableLiveData() 16 | 17 | fun pickContact() { 18 | pickContactAction.value = Unit 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e05/E05Activity.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e05 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | import android.os.Bundle 5 | import cchcc.learn.amu.R 6 | import cchcc.learn.amu.e02.E02Fragment 7 | 8 | class E05Activity : AppCompatActivity() { 9 | 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | setContentView(R.layout.activity_e05) 13 | 14 | val e02Fragment = E02Fragment.newInstance() 15 | val e05Fragment = E05Fragment.newInstance { e02Fragment.viewModel } 16 | 17 | supportFragmentManager.beginTransaction() 18 | .replace(R.id.fl_top, e02Fragment) 19 | .replace(R.id.fl_bottom, e05Fragment) 20 | .commit() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e05/E05Fragment.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e05 2 | 3 | import androidx.lifecycle.Observer 4 | import androidx.lifecycle.ViewModelProviders 5 | import androidx.databinding.DataBindingUtil 6 | import android.os.Bundle 7 | import androidx.fragment.app.Fragment 8 | import androidx.recyclerview.widget.LinearLayoutManager 9 | import android.view.LayoutInflater 10 | import android.view.View 11 | import android.view.ViewGroup 12 | import cchcc.learn.amu.R 13 | import cchcc.learn.amu.databinding.FragmentE05Binding 14 | import cchcc.learn.amu.e02.E02ViewModel 15 | import kotlinx.android.synthetic.main.fragment_e05.* 16 | import java.io.Serializable 17 | 18 | class E05Fragment : androidx.fragment.app.Fragment() { 19 | 20 | private val viewModel: E05ViewModel by lazy { ViewModelProviders.of(this).get(E05ViewModel::class.java) } 21 | private val adapter = E05LogListAdapter() 22 | 23 | override fun onCreate(savedInstanceState: Bundle?) { 24 | super.onCreate(savedInstanceState) 25 | 26 | viewModel.logList.observe(this, Observer { 27 | adapter.submitList(it) 28 | }) 29 | 30 | val e02ViewModel: E02ViewModel by lazy { 31 | @Suppress("UNCHECKED_CAST") 32 | val getE02ViewModel = arguments?.getSerializable("getE02ViewModel") as? () -> E02ViewModel 33 | ?: throw IllegalArgumentException("no getE02ViewModel for ${this::class.java.simpleName}\"") 34 | getE02ViewModel() 35 | } 36 | 37 | e02ViewModel.clearAction.observe(this, Observer { 38 | viewModel.clear() 39 | }) 40 | 41 | fun createObserverForAddingLine(prefix: String): Observer = Observer { 42 | viewModel.add("$prefix${it.toString()}") 43 | adapter.notifyItemInserted(adapter.itemCount - 1) 44 | rcv_log.scrollToPosition(adapter.itemCount - 1) 45 | } 46 | 47 | e02ViewModel.result.observe(this, createObserverForAddingLine("try result : ")) 48 | e02ViewModel.score.observe(this, createObserverForAddingLine("score : ")) 49 | 50 | val setSpeedObserver = createObserverForAddingLine("set speed ✕ ") 51 | viewModel.animSpeed.observe(this, Observer { 52 | e02ViewModel.speedOfAnim.value = it!!.toFloat() 53 | setSpeedObserver.onChanged(it) 54 | }) 55 | } 56 | 57 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, 58 | savedInstanceState: Bundle?): View? = DataBindingUtil.inflate(inflater, R.layout.fragment_e05, container, false).also { 59 | it.setLifecycleOwner(this) 60 | it.viewModel = viewModel 61 | }.root 62 | 63 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 64 | super.onViewCreated(view, savedInstanceState) 65 | rcv_log.adapter = adapter 66 | rcv_log.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context) 67 | } 68 | 69 | companion object { 70 | @JvmStatic 71 | fun newInstance(getE02ViewModel: () -> E02ViewModel) = 72 | E05Fragment().apply { 73 | arguments = Bundle().apply { 74 | putSerializable("getE02ViewModel", getE02ViewModel as Serializable) 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e05/E05LogListAdapter.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e05 2 | 3 | import androidx.recyclerview.widget.ListAdapter 4 | import androidx.recyclerview.widget.DiffUtil 5 | import androidx.recyclerview.widget.RecyclerView 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import cchcc.learn.amu.R 10 | import kotlinx.android.synthetic.main.listitem_e05.view.* 11 | 12 | class E05LogListAdapter : ListAdapter(getDiffCallback()) { 13 | class VH(v: View) : androidx.recyclerview.widget.RecyclerView.ViewHolder(v) 14 | 15 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH { 16 | val v = LayoutInflater.from(parent.context).inflate(R.layout.listitem_e05, parent, false) 17 | return VH(v) 18 | } 19 | 20 | override fun onBindViewHolder(holder: VH, position: Int) { 21 | holder.itemView.tv_line.text = getItem(position) 22 | } 23 | 24 | companion object { 25 | @JvmStatic 26 | fun getDiffCallback() = object : DiffUtil.ItemCallback() { 27 | override fun areItemsTheSame(oldItem: String, newItem: String): Boolean 28 | = oldItem === newItem 29 | 30 | override fun areContentsTheSame(oldItem: String, newItem: String): Boolean 31 | = oldItem == newItem 32 | } 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e05/E05ViewModel.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e05 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.Transformations 6 | import androidx.lifecycle.ViewModel 7 | import java.text.SimpleDateFormat 8 | import java.util.* 9 | 10 | class E05ViewModel : ViewModel() { 11 | 12 | val logList: MutableLiveData> = MutableLiveData>().apply { 13 | value = mutableListOf() 14 | } 15 | val progress = MutableLiveData().apply { value = 0 } 16 | val animSpeed: LiveData = Transformations.map(progress) { it + 1 } 17 | 18 | val sdf = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.getDefault()) 19 | 20 | fun add(line: String) { 21 | logList.value!!.add("${sdf.format(Date())} $line") 22 | } 23 | 24 | fun clear() { 25 | logList.value = mutableListOf("clearAction") 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e06/E06Activity.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e06 2 | 3 | import androidx.lifecycle.ViewModelProviders 4 | import androidx.databinding.DataBindingUtil 5 | import android.os.Bundle 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.recyclerview.widget.LinearLayoutManager 8 | import cchcc.learn.amu.R 9 | import cchcc.learn.amu.databinding.ActivityE06Binding 10 | import kotlinx.android.synthetic.main.activity_e06.* 11 | 12 | class E06Activity : AppCompatActivity() { 13 | 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | 17 | @Suppress("UNCHECKED_CAST") 18 | val createFactory = intent.getSerializableExtra("createVMFactory") as? () -> E06ViewModelFactory 19 | ?: createVMFactory 20 | val viewModelFactory = createFactory() 21 | 22 | val viewModel = ViewModelProviders.of(this, viewModelFactory).get(E06ViewModel::class.java) 23 | val adapter = E06NumberAdapter(viewModel::colorizeListItem) 24 | 25 | DataBindingUtil.setContentView(this, R.layout.activity_e06).also { 26 | it.setLifecycleOwner(this) 27 | it.viewModel = viewModel 28 | } 29 | 30 | rcv_numbers.also { 31 | it.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this) 32 | it.adapter = adapter 33 | } 34 | 35 | viewModel.numLists.observe(this::getLifecycle, adapter::submitList) 36 | viewModel.moveToTopAction.observe(this::getLifecycle) { 37 | rcv_numbers.scrollToPosition(0) 38 | } 39 | } 40 | 41 | companion object { 42 | val createVMFactory: () -> E06ViewModelFactory = { E06ViewModelFactory() } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e06/E06NumberAdapter.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e06 2 | 3 | import androidx.paging.PagedListAdapter 4 | import androidx.databinding.DataBindingUtil 5 | import androidx.recyclerview.widget.DiffUtil 6 | import androidx.recyclerview.widget.RecyclerView 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import cchcc.learn.amu.R 11 | import cchcc.learn.amu.databinding.ListitemE06Binding 12 | import cchcc.learn.amu.e06.data.E06Number 13 | 14 | class E06NumberAdapter(val colorize: (E06Number?, refreshItem: () -> Unit) -> Unit) : PagedListAdapter(getDiffCallback()) { 15 | inner class VH(val binding: ListitemE06Binding) : androidx.recyclerview.widget.RecyclerView.ViewHolder(binding.root) 16 | 17 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH { 18 | val binding = DataBindingUtil.inflate(LayoutInflater.from(parent.context), R.layout.listitem_e06, parent, false) 19 | val vh = VH(binding) 20 | binding.onClick = View.OnClickListener { 21 | val item = getItem(vh.layoutPosition) 22 | if (item != null) { 23 | val refreshItem = { 24 | notifyItemChanged(vh.layoutPosition) 25 | } 26 | 27 | this@E06NumberAdapter.colorize(item, refreshItem) 28 | } 29 | } 30 | return vh 31 | } 32 | 33 | override fun onBindViewHolder(holder: VH, position: Int) { 34 | holder.binding.num = getItem(position) 35 | } 36 | 37 | 38 | companion object { 39 | 40 | @JvmStatic 41 | fun getDiffCallback() = object : DiffUtil.ItemCallback() { 42 | override fun areItemsTheSame(oldItem: E06Number, newItem: E06Number): Boolean = oldItem === newItem 43 | override fun areContentsTheSame(oldItem: E06Number, newItem: E06Number): Boolean = oldItem.num == newItem.num 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e06/E06ViewModel.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e06 2 | 3 | import androidx.lifecycle.ViewModel 4 | import cchcc.learn.amu.e06.data.E06DataRepository 5 | import cchcc.learn.amu.e06.data.E06Number 6 | import cchcc.learn.amu.util.SingleLiveEvent 7 | 8 | class E06ViewModel(private val repo: E06DataRepository) : ViewModel() { 9 | val numLists by lazy { repo.listingThreeTimesTable() } 10 | 11 | val moveToTopAction = SingleLiveEvent() 12 | 13 | fun refreshList(doneRefresh: () -> Unit) { 14 | repo.refresh() 15 | doneRefresh() 16 | } 17 | 18 | fun moveToTopOfList() { 19 | moveToTopAction.value = Unit 20 | } 21 | 22 | fun colorizeListItem(num: E06Number?, refreshItem: () -> Unit) { 23 | if (num != null && num.alpha < 1.0f) { 24 | num.alpha += 0.1f 25 | refreshItem() 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e06/E06ViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e06 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import cchcc.learn.amu.e06.data.E06DataRepository 6 | 7 | class E06ViewModelFactory : ViewModelProvider.Factory { 8 | @Suppress("UNCHECKED_CAST") 9 | override fun create(modelClass: Class): T = when (modelClass) { 10 | E06ViewModel::class.java -> E06ViewModel(E06DataRepository()) 11 | else -> throw IllegalArgumentException("$modelClass is not registered ViewModel") 12 | } as T 13 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e06/binding/E06Bindings.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e06.binding 2 | 3 | import androidx.databinding.BindingAdapter 4 | import androidx.databinding.BindingConversion 5 | import androidx.swiperefreshlayout.widget.SwipeRefreshLayout 6 | 7 | interface SwipeOnRefresh { 8 | fun onRefresh(doneRefresh: () -> Unit) 9 | } 10 | 11 | @BindingAdapter("onSwipeRefresh") 12 | fun binding_onSwipeRefresh(v: androidx.swiperefreshlayout.widget.SwipeRefreshLayout, listener: SwipeOnRefresh) { 13 | 14 | val doneRefresh: () -> Unit = { 15 | v.post { v.isRefreshing = false } 16 | } 17 | 18 | v.setOnRefreshListener { listener.onRefresh(doneRefresh) } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e06/data/E06DataRepository.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e06.data 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.paging.LivePagedListBuilder 5 | import androidx.paging.PagedList 6 | 7 | class E06DataRepository { 8 | private val dataSourceLiveData = MutableLiveData() 9 | private val dataSourceFactory = E06ThreeTimesTableDataSourceFactory(dataSourceLiveData) 10 | 11 | fun listingThreeTimesTable() = LivePagedListBuilder(dataSourceFactory, 12 | PagedList.Config.Builder() 13 | .setEnablePlaceholders(true) 14 | .setPageSize(10) 15 | .build()) 16 | .build() 17 | 18 | fun refresh() { 19 | dataSourceLiveData.value?.invalidate() 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e06/data/E06Number.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e06.data 2 | 3 | data class E06Number(val num: Int, var alpha: Float) -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e06/data/E06ThreeTimesTableDataSource.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e06.data 2 | 3 | import androidx.paging.PageKeyedDataSource 4 | 5 | class E06ThreeTimesTableDataSource() : PageKeyedDataSource() { 6 | 7 | private fun generateThreeTimesTablePositive(pivot: Int, size: Int): List = List(size) { pivot + ((it + 1) * 3) } 8 | 9 | override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) { 10 | val list = generateThreeTimesTablePositive(0, params.requestedLoadSize).map { E06Number(it, 0.0f) } 11 | callback.onResult(list, list.first().num, list.last().num) 12 | } 13 | 14 | override fun loadAfter(params: LoadParams, callback: LoadCallback) { 15 | val list = generateThreeTimesTablePositive(params.key, params.requestedLoadSize).map { E06Number(it, 0.0f) } 16 | callback.onResult(list, list.last().num) 17 | } 18 | 19 | override fun loadBefore(params: LoadParams, callback: LoadCallback) { 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e06/data/E06ThreeTimesTableDataSourceFactory.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e06.data 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.paging.DataSource 5 | 6 | class E06ThreeTimesTableDataSourceFactory(private val dataSourceLiveData: MutableLiveData) : DataSource.Factory() { 7 | override fun create(): DataSource = E06ThreeTimesTableDataSource().apply { 8 | dataSourceLiveData.postValue(this) 9 | } 10 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e07/E07ViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e07 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import cchcc.learn.amu.e07.coordinator.E07WordCoordinator 6 | import cchcc.learn.amu.e07.coordinator.E07WordListCoordinator 7 | 8 | class E07ViewModelFactory : ViewModelProvider.Factory { 9 | 10 | @Suppress("UNCHECKED_CAST") 11 | override fun create(modelClass: Class): T = when (modelClass) { 12 | E07WordListViewModel::class.java -> E07WordListViewModel(E07WordListCoordinator()) 13 | E07WordViewModel::class.java -> E07WordViewModel(E07WordCoordinator()) 14 | else -> throw IllegalArgumentException("$modelClass is not registered ViewModel") 15 | } as T 16 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e07/E07WordActivity.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e07 2 | 3 | import androidx.lifecycle.ViewModelProviders 4 | import androidx.databinding.DataBindingUtil 5 | import android.os.Build 6 | import android.os.Bundle 7 | import androidx.core.view.ViewCompat 8 | import androidx.appcompat.app.AppCompatActivity 9 | import android.transition.ChangeBounds 10 | import android.transition.TransitionSet 11 | import android.view.MenuItem 12 | import cchcc.learn.amu.R 13 | import cchcc.learn.amu.databinding.ActivityE07WordBinding 14 | import cchcc.learn.amu.e07.coordinator.E07NavigatorImpl 15 | import cchcc.learn.amu.util.ChangeTextSize 16 | import kotlinx.android.synthetic.main.activity_e07_word.* 17 | 18 | class E07WordActivity : AppCompatActivity() { 19 | 20 | private val viewModel by lazy { 21 | ViewModelProviders 22 | .of(this, E07ViewModelFactory()) 23 | .get(E07WordViewModel::class.java).apply { 24 | coordinator.navigator = navigator 25 | } 26 | } 27 | 28 | private val navigator by lazy { E07NavigatorImpl(this) } 29 | 30 | override fun onCreate(savedInstanceState: Bundle?) { 31 | super.onCreate(savedInstanceState) 32 | 33 | DataBindingUtil.setContentView(this, R.layout.activity_e07_word).let { 34 | it.setLifecycleOwner(this) 35 | it.viewModel = viewModel 36 | } 37 | 38 | supportActionBar!!.setDisplayHomeAsUpEnabled(true) 39 | 40 | val transitionName = intent.getStringExtra("transitionName") 41 | ViewCompat.setTransitionName(tv_word, transitionName) 42 | 43 | val text = intent.getStringExtra("text") 44 | viewModel.word.value = text 45 | 46 | 47 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 48 | val startSize = intent.getFloatExtra("startSize", 0.0f) 49 | val endSize = tv_word.textSize 50 | 51 | setEnterSharedElementCallback(E07WordSharedElementCallback(startSize, endSize)) 52 | 53 | window.sharedElementEnterTransition = TransitionSet().apply { 54 | addTransition(ChangeBounds()) 55 | addTransition(ChangeTextSize()) 56 | } 57 | 58 | } 59 | } 60 | 61 | override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { 62 | android.R.id.home -> { 63 | viewModel.backToWordList() 64 | true 65 | } 66 | else -> super.onOptionsItemSelected(item) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e07/E07WordListActivity.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e07 2 | 3 | import androidx.lifecycle.ViewModelProviders 4 | import android.os.Build 5 | import android.os.Bundle 6 | import androidx.appcompat.app.AppCompatActivity 7 | import android.transition.Fade 8 | import android.transition.Slide 9 | import android.transition.TransitionSet 10 | import android.view.View 11 | import android.widget.TextView 12 | import androidx.core.app.ActivityOptionsCompat 13 | import androidx.core.view.ViewCompat 14 | import cchcc.learn.amu.R 15 | import cchcc.learn.amu.e07.coordinator.E07NavigatorImpl 16 | import kotlinx.android.synthetic.main.activity_e07_wordlist.* 17 | 18 | class E07WordListActivity : AppCompatActivity() { 19 | 20 | private val viewModel by lazy { 21 | ViewModelProviders 22 | .of(this, E07ViewModelFactory()) 23 | .get(E07WordListViewModel::class.java).apply { 24 | coordinator.navigator = navigator 25 | } 26 | } 27 | 28 | private val navigator by lazy { E07NavigatorImpl(this) } 29 | 30 | override fun onCreate(savedInstanceState: Bundle?) { 31 | super.onCreate(savedInstanceState) 32 | 33 | setContentView(R.layout.activity_e07_wordlist) 34 | 35 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 36 | window.enterTransition = TransitionSet().apply { 37 | addTransition(Fade()) 38 | addTransition(Slide()) 39 | } 40 | } 41 | 42 | val adapter = E07WordListAdapter(::goWordScreen) 43 | 44 | rcv_list.let { 45 | it.layoutManager = androidx.recyclerview.widget.GridLayoutManager(this, 2) 46 | it.adapter = adapter 47 | } 48 | 49 | viewModel.wordList.observe(this::getLifecycle, adapter::submitList) 50 | } 51 | 52 | private fun goWordScreen(text: String, sharedElement: View) { 53 | val startSize = (sharedElement as TextView).textSize 54 | val transitionName = ViewCompat.getTransitionName(sharedElement)!! 55 | 56 | val extras = Bundle().apply { 57 | putString("text", text) 58 | putString("transitionName", transitionName) 59 | putFloat("startSize", startSize) 60 | } 61 | 62 | val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(this, sharedElement, transitionName) 63 | .toBundle() 64 | 65 | val args = mapOf("extras" to extras, "bundle" to bundle) 66 | 67 | viewModel.goWordScreen { args } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e07/E07WordListAdapter.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e07 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.core.view.ViewCompat 5 | import androidx.recyclerview.widget.ListAdapter 6 | import androidx.recyclerview.widget.DiffUtil 7 | import androidx.recyclerview.widget.RecyclerView 8 | import android.view.LayoutInflater 9 | import android.view.View 10 | import android.view.ViewGroup 11 | import cchcc.learn.amu.R 12 | import kotlinx.android.synthetic.main.listitem_e07.view.* 13 | 14 | class E07WordListAdapter(val goWordScreen: (String, View) -> Unit) : ListAdapter(getDiffCallback()) { 15 | class VH(view: View) : androidx.recyclerview.widget.RecyclerView.ViewHolder(view) 16 | 17 | private val numberIter = generateSequence(0) { it + 1 }.iterator() 18 | 19 | @SuppressLint("ResourceType") 20 | override fun onCreateViewHolder(v: ViewGroup, viewType: Int): E07WordListAdapter.VH { 21 | 22 | val view = LayoutInflater.from(v.context).inflate(R.layout.listitem_e07, v, false) 23 | ViewCompat.setTransitionName(view.tv_word, "listItem#${numberIter.next()}") 24 | 25 | return VH(view).apply { 26 | itemView.setOnClickListener { 27 | goWordScreen(getItem(layoutPosition), itemView.tv_word) 28 | } 29 | } 30 | } 31 | 32 | override fun onBindViewHolder(vh: E07WordListAdapter.VH, position: Int) { 33 | val text = getItem(position) 34 | vh.itemView.tv_word.text = text 35 | } 36 | 37 | companion object { 38 | @JvmStatic 39 | fun getDiffCallback() = object : DiffUtil.ItemCallback() { 40 | override fun areItemsTheSame(p0: String, p1: String): Boolean = p0 === p1 41 | 42 | override fun areContentsTheSame(p0: String, p1: String): Boolean = p0 == p1 43 | 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e07/E07WordListViewModel.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e07 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.lifecycle.ViewModel 5 | import cchcc.learn.amu.e07.coordinator.E07WordListCoordinator 6 | 7 | class E07WordListViewModel(val coordinator: E07WordListCoordinator) : ViewModel() { 8 | val wordList = MutableLiveData>().apply { 9 | value = sampleText.split(" ") 10 | } 11 | 12 | fun goWordScreen(getArgs: () -> Map) { 13 | coordinator.goWordScreen(getArgs()) 14 | } 15 | 16 | override fun onCleared() { 17 | coordinator.navigator = null // avoid memory leak 18 | } 19 | 20 | companion object { 21 | const val sampleText = """Activity transitions in material design apps provide visual connections between different states through motion and transformations between common elements. You can specify custom animations for enter and exit transitions and for transitions of shared elements between activities.""" 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e07/E07WordSharedElementCallback.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e07 2 | 3 | import android.annotation.TargetApi 4 | import android.app.SharedElementCallback 5 | import android.os.Build 6 | import android.util.TypedValue 7 | import android.view.View 8 | import android.widget.TextView 9 | 10 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 11 | class E07WordSharedElementCallback(private val startSize: Float, private val endSize: Float) : SharedElementCallback() { 12 | override fun onSharedElementStart(sharedElementNames: MutableList?, sharedElements: MutableList?, sharedElementSnapshots: MutableList?) { 13 | val view = sharedElements?.get(0) as TextView 14 | view.setTextSize(TypedValue.COMPLEX_UNIT_PX, startSize) 15 | } 16 | 17 | override fun onSharedElementEnd(sharedElementNames: MutableList?, sharedElements: MutableList?, sharedElementSnapshots: MutableList?) { 18 | val view = sharedElements?.get(0) as TextView 19 | view.setTextSize(TypedValue.COMPLEX_UNIT_PX, endSize) 20 | 21 | val oldWidth = view.measuredWidth 22 | val oldHeight = view.measuredHeight 23 | 24 | val widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) 25 | val heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) 26 | view.measure(widthSpec, heightSpec) 27 | 28 | val newWidth = view.measuredWidth 29 | val newHeight = view.measuredHeight 30 | 31 | val widthDiff = newWidth - oldWidth 32 | val heightDiff = newHeight - oldHeight 33 | view.layout(view.left - widthDiff / 2, view.top - heightDiff / 2, 34 | view.right + widthDiff / 2, view.bottom + heightDiff / 2) 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e07/E07WordViewModel.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e07 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.lifecycle.ViewModel 5 | import cchcc.learn.amu.e07.coordinator.E07WordCoordinator 6 | 7 | class E07WordViewModel(val coordinator: E07WordCoordinator) : ViewModel() { 8 | val word = MutableLiveData() 9 | 10 | fun backToWordList() { 11 | coordinator.backToWordListScreen() 12 | } 13 | 14 | override fun onCleared() { 15 | coordinator.navigator = null // avoid memory leak 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e07/coordinator/E07Navigator.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e07.coordinator 2 | 3 | interface E07Navigator { 4 | // the reason why using generic T is to avoid clarifying a type `Bundle` 5 | // which is the platform dependency. maybe not the best way.. 6 | fun navigateWord(args: Map) 7 | fun navigateBackToWordList() 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e07/coordinator/E07NavigatorImpl.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e07.coordinator 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.appcompat.app.AppCompatActivity 6 | import cchcc.learn.amu.e07.E07WordActivity 7 | 8 | class E07NavigatorImpl(val activity: AppCompatActivity) : E07Navigator { 9 | 10 | override fun navigateWord(args: Map) { 11 | val extras = args["extras"] as Bundle 12 | val bundle = args["bundle"] as Bundle 13 | 14 | activity.startActivity(Intent(activity, E07WordActivity::class.java) 15 | .putExtras(extras) 16 | , bundle) 17 | } 18 | 19 | override fun navigateBackToWordList() { 20 | activity.onBackPressed() 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e07/coordinator/E07WordCoordinator.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e07.coordinator 2 | 3 | class E07WordCoordinator(var navigator: E07Navigator? = null) { 4 | fun backToWordListScreen() = navigator!!.navigateBackToWordList() 5 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/e07/coordinator/E07WordListCoordinator.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e07.coordinator 2 | 3 | class E07WordListCoordinator(var navigator: E07Navigator? = null) { 4 | fun goWordScreen(args: Map) = navigator!!.navigateWord(args) 5 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/util/ChangeTextSize.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.util 2 | 3 | import android.animation.ObjectAnimator 4 | import android.util.TypedValue 5 | import android.widget.TextView 6 | import android.view.ViewGroup 7 | import android.animation.Animator 8 | import android.annotation.TargetApi 9 | import android.content.Context 10 | import android.os.Build 11 | import android.transition.Transition 12 | import android.transition.TransitionValues 13 | import android.util.AttributeSet 14 | import android.util.Property 15 | 16 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 17 | class ChangeTextSize : Transition { 18 | constructor() : super() 19 | 20 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs) 21 | 22 | override fun captureStartValues(transitionValues: TransitionValues) = captureValues(transitionValues) 23 | 24 | override fun captureEndValues(transitionValues: TransitionValues) = captureValues(transitionValues) 25 | 26 | private fun captureValues(transitionValues: TransitionValues) { 27 | if (transitionValues.view is TextView) { 28 | val textView = transitionValues.view as TextView 29 | transitionValues.values.put(PROPNAME_TEXT_SIZE, textView.textSize) 30 | } 31 | } 32 | 33 | override fun createAnimator(sceneRoot: ViewGroup, startValues: TransitionValues?, 34 | endValues: TransitionValues?): Animator? { 35 | val startSize = startValues?.values?.get(PROPNAME_TEXT_SIZE) as? Float ?: return null 36 | val endSize = endValues?.values?.get(PROPNAME_TEXT_SIZE) as? Float ?: return null 37 | 38 | val view = endValues.view as TextView 39 | view.setTextSize(TypedValue.COMPLEX_UNIT_PX, startSize) 40 | return ObjectAnimator.ofFloat(view, TEXT_SIZE_PROPERTY, startSize, endSize) 41 | } 42 | 43 | companion object { 44 | const val PROPNAME_TEXT_SIZE = "cchcc.learn.amu:ChangeTextSize:textsize" 45 | 46 | private val TEXT_SIZE_PROPERTY = object : Property(Float::class.java, "textSize") { 47 | override fun get(textView: TextView): Float = textView.textSize 48 | 49 | override fun set(textView: TextView, textSizePixels: Float) { 50 | textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSizePixels) 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/util/SingleLiveEvent.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.util 2 | 3 | import androidx.lifecycle.LifecycleOwner 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.Observer 6 | import java.util.concurrent.atomic.AtomicBoolean 7 | 8 | // https://github.com/googlesamples/android-architecture/blob/dev-todo-mvvm-live/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/SingleLiveEvent.java 9 | class SingleLiveEvent : MutableLiveData() { 10 | 11 | private val pending = AtomicBoolean(false) 12 | 13 | override fun observe(owner: LifecycleOwner, observer: Observer) { 14 | super.observe(owner, Observer { t -> 15 | if (pending.compareAndSet(true, false)) { 16 | observer.onChanged(t) 17 | } 18 | }) 19 | } 20 | 21 | override fun setValue(t: T?) { 22 | pending.set(true) 23 | super.setValue(t) 24 | } 25 | 26 | override fun postValue(value: T?) { 27 | pending.set(true) 28 | super.postValue(value) 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/util/StartActivityResult.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.util 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.util.SparseArray 6 | 7 | private val Activity.activityResults by lazy { SparseArray<(Int, Intent?) -> Unit>() } 8 | 9 | fun Activity.startActivityWithResult(intent: Intent, resultBlock: (Int, Intent?) -> Unit) { 10 | val requestCode = Math.abs((hashCode() + resultBlock.hashCode()).toShort().toInt()) 11 | activityResults.put(requestCode, resultBlock) 12 | startActivityForResult(intent, requestCode) 13 | } 14 | 15 | fun Activity.activityResult(requestCode: Int, resultCode: Int, data: Intent?) = activityResults.get(requestCode)?.let { 16 | activityResults.remove(requestCode) 17 | it(resultCode, data) 18 | } -------------------------------------------------------------------------------- /app/src/main/java/cchcc/learn/amu/util/UserPermission.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.util 2 | 3 | import android.app.Activity 4 | import android.content.pm.PackageManager 5 | import androidx.core.app.ActivityCompat 6 | import androidx.core.content.ContextCompat 7 | import android.util.SparseArray 8 | import java.util.* 9 | 10 | class UserPermission(private val activity: Activity, vararg permissions: String) { 11 | val perms: Array by lazy { permissions } 12 | 13 | lateinit var isGrantedCallback: () -> Unit 14 | lateinit var notGrantedCallback: () -> Unit 15 | 16 | fun isGranted(): Boolean = perms.all { ContextCompat.checkSelfPermission(activity, it) == PackageManager.PERMISSION_GRANTED } 17 | 18 | fun checkOrRequest(isGranted: () -> Unit) = checkOrRequest(isGranted) {} 19 | 20 | /** 21 | * 1. check if permission is granted 22 | * 2. if permission is granted, callback `isGranted`, or request permission 23 | * 3. callback `isGranted` or `notGranted` as a result of requesting 24 | */ 25 | fun checkOrRequest(isGranted: () -> Unit, notGranted: () -> Unit) { 26 | val notGrantedPermissions = perms.filter { ContextCompat.checkSelfPermission(activity, it) != PackageManager.PERMISSION_GRANTED } 27 | .toTypedArray() 28 | 29 | val permissionIsAllGranted = notGrantedPermissions.isEmpty() 30 | if (permissionIsAllGranted) { 31 | isGranted() 32 | } else { 33 | isGrantedCallback = isGranted 34 | notGrantedCallback = notGranted 35 | val requestCode = Math.abs((activity.hashCode() + isGranted.hashCode()).toShort().toInt()) 36 | activity.permissions.put(requestCode, this) 37 | ActivityCompat.requestPermissions(activity, notGrantedPermissions, requestCode) 38 | } 39 | } 40 | 41 | fun shouldShowRequestRational(): Boolean = perms.all { ActivityCompat.shouldShowRequestPermissionRationale(activity, it) } 42 | 43 | override fun toString(): String = "${javaClass.simpleName} ${Arrays.toString(perms)}" 44 | } 45 | 46 | private val Activity.permissions by lazy { SparseArray() } 47 | 48 | fun Activity.permissionOf(vararg permissions: String): UserPermission = UserPermission(this, *permissions) 49 | 50 | 51 | /** 52 | * this has to be called in Activity.onRequestPermissionsResult 53 | */ 54 | fun Activity.requestPermissionResult(requestCode: Int, grantResults: IntArray) = permissions[requestCode]?.let { 55 | permissions.remove(requestCode) 56 | val isGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED } 57 | if (isGranted) it.isGrantedCallback() else it.notGrantedCallback() 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_upward_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_e01.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 30 | 31 | 41 | 42 | 43 | 52 | 53 | 63 | 64 | 65 | 82 | 83 | 84 | 99 | 100 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_e01a.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 30 | 31 | 41 | 42 | 43 | 52 | 53 | 63 | 64 | 65 | 66 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_e02.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_e03.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 18 | 19 | 31 | 32 | 46 | 47 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_e04.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | 34 | 35 | 45 | 46 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_e05.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | 20 | 21 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_e06.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 17 | 18 | 26 | 27 | 32 | 33 | 34 | 35 | 36 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_e07_word.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 19 | 20 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_e07_wordlist.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 15 | 16 | 19 | 20 | 24 | 25 | 30 | 31 | 36 | 37 | 42 | 43 | 48 | 49 | 54 | 55 | 60 | 61 | 66 | 67 | 72 | 73 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_e02.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 17 | 18 | 32 | 33 | 44 | 45 | 60 | 61 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_e05.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 17 | 18 | 28 | 29 | 41 | 42 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /app/src/main/res/layout/listitem_e03.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 10 | 11 | 12 | 15 | 16 | 28 | 29 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/res/layout/listitem_e05.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/listitem_e06.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 15 | 16 | 17 | 23 | 24 | 36 | 37 | 52 | 53 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /app/src/main/res/layout/listitem_e07.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cchcc/android-mvvm-unittest/9de200ea3018138077d513ebf349ea1b293804ec/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cchcc/android-mvvm-unittest/9de200ea3018138077d513ebf349ea1b293804ec/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cchcc/android-mvvm-unittest/9de200ea3018138077d513ebf349ea1b293804ec/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cchcc/android-mvvm-unittest/9de200ea3018138077d513ebf349ea1b293804ec/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cchcc/android-mvvm-unittest/9de200ea3018138077d513ebf349ea1b293804ec/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cchcc/android-mvvm-unittest/9de200ea3018138077d513ebf349ea1b293804ec/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cchcc/android-mvvm-unittest/9de200ea3018138077d513ebf349ea1b293804ec/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cchcc/android-mvvm-unittest/9de200ea3018138077d513ebf349ea1b293804ec/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cchcc/android-mvvm-unittest/9de200ea3018138077d513ebf349ea1b293804ec/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cchcc/android-mvvm-unittest/9de200ea3018138077d513ebf349ea1b293804ec/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/raw/e02_succes.json: -------------------------------------------------------------------------------- 1 | {"v":"4.6.9","fr":29.9700012207031,"ip":0,"op":172.000007005704,"w":800,"h":600,"nm":"Emoji Reaction","ddd":0,"assets":[{"id":"comp_2","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 8","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[45,45,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"n":"0_1_0p167_0p167","t":60,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-0.031,25.023],[-2.885,24.992],[-22.008,24.992],[-22,14.992],[-10.925,14.992],[0.075,14.98],[11.083,14.977],[22.008,14.992],[22,24.992],[2.835,25.007]],"c":true}],"e":[{"i":[[0,0],[0,0],[5.155,6.157],[0,8.347],[-15.706,0],[-4.707,-3.218],[-8.576,0],[0,-15.706],[5.861,-7.444],[2.854,-2.198]],"o":[[0,0],[-2.83,-2.203],[-5.859,-6.998],[0,-15.706],[9.291,0],[4.706,-3.219],[15.706,0],[0,7.944],[-5.085,6.459],[0,0]],"v":[[-0.031,40.685],[-2.886,38.462],[-35.836,10.603],[-44.543,-12.201],[-18.059,-40.685],[0,-32.728],[18.059,-40.685],[44.543,-12.201],[35.952,10.354],[2.835,38.477]],"c":true}]},{"t":67.0000027289659}]},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[0.908]},"n":["0_1_0p167_0p908"],"t":60,"s":[0.7764706,0.7764706,0.7764706,1],"e":[0.9058824,0.2862745,0.2862745,1]},{"t":67.0000027289659}]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":450.000018328876,"st":0,"bm":0,"sr":1}]},{"id":"comp_3","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 4","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"n":"0_1_0p167_0p167","t":60,"s":[55,107,0],"e":[55,50,0],"to":[0,-9.5,0],"ti":[0,9.5,0]},{"t":65.0000026475043}]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-52.98,-29],[53,-29]],"c":false}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[-0.289]},"n":["0_1_0p167_-0p289"],"t":60,"s":[0.7764706,0.7764706,0.7764706,1],"e":[0.1058824,0.1058824,0.1058824,1]},{"t":69.0000028104276}]},"o":{"a":0,"k":100},"w":{"a":0,"k":10},"lc":1,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"tr","p":{"a":0,"k":[0,1],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.692],"y":[0.893]},"o":{"x":[0.369],"y":[0]},"n":["0p692_0p893_0p369_0"],"t":60,"s":[34.9],"e":[10.657]},{"i":{"x":[0.495],"y":[1]},"o":{"x":[0.271],"y":[0.942]},"n":["0p495_1_0p271_0p942"],"t":61,"s":[10.657],"e":[1]},{"t":65.0000026475043}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.692],"y":[0.893]},"o":{"x":[0.369],"y":[0]},"n":["0p692_0p893_0p369_0"],"t":60,"s":[65],"e":[89.256]},{"i":{"x":[0.495],"y":[1]},"o":{"x":[0.271],"y":[0.936]},"n":["0p495_1_0p271_0p936"],"t":61,"s":[89.256],"e":[99]},{"t":65.0000026475043}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim"}],"ip":0,"op":450.000018328876,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 7","td":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[55,50,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[33.471,0],[0,37.361],[0,0],[0,0],[0,0]],"o":[[-32.978,0],[0,0],[0,0],[0,0],[0,37.919]],"v":[[0,33.974],[-52.667,-27.333],[-52.667,-32.988],[52.667,-32.988],[52.667,-27.333]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.1058824,0.1058824,0.1058824,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":450.000018328876,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 3","tt":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"n":"0_1_0p167_0p167","t":62,"s":[55,92.5,0],"e":[55,50,0],"to":[0,-7.08333349227905,0],"ti":[0,7.08333349227905,0]},{"t":71.0000028918893}]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"s","pt":{"a":0,"k":{"i":[[5.698,-7.341],[-10.166,0],[-7.286,5.746],[11.114,0]],"o":[[7.063,5.564],[10.112,0],[-5.58,-7.168],[-10.826,0]],"v":[[-25.551,15.244],[0.382,23.625],[26.57,14.97],[0.382,3.625]],"c":true}},"o":{"a":0,"k":100},"x":{"a":0,"k":0},"nm":"Mask 1"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[14.261,0],[9.486,9.234],[0,0],[0,0],[-16.039,0],[-6.816,-12.427],[0,0],[0,0]],"o":[[-14.394,0],[0,0],[0,0],[7.043,-12.558],[16.435,0],[0,0],[0,0],[-9.77,9.386]],"v":[[-0.118,33.125],[-36.106,19.208],[-38.857,16.528],[-36.979,13.18],[-0.118,-6.875],[37.016,12.971],[38.857,16.328],[36.096,18.98]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[-0.289]},"n":["0_1_0p167_-0p289"],"t":60,"s":[0.7764706,0.7764706,0.7764706,1],"e":[0.1058824,0.1058824,0.1058824,1]},{"t":69.0000028104276}]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":450.000018328876,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 6","td":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"n":"0_1_0p167_0p167","t":60,"s":[55,125,0],"e":[55,55,0],"to":[0,-11.6666669845581,0],"ti":[0,11.6666669845581,0]},{"t":65.0000026475043}]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[109.443,77.017]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect"},{"ty":"fl","c":{"a":0,"k":[1,0,0,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0.722,-2.742],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":450.000018328876,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":5,"ty":4,"nm":"Shape Layer 2","tt":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[55,50,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"s","pt":{"a":0,"k":{"i":[[0,0],[-6.436,-9.023],[-14.934,0],[-1.821,36.757]],"o":[[0.531,9.149],[7.221,10.124],[18.647,0],[0,0]],"v":[[-42.023,-22.014],[-32.888,7.576],[0.5,22.833],[43.046,-22.014]],"c":true}},"o":{"a":0,"k":100},"x":{"a":0,"k":0},"nm":"Mask 1"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[33.471,0],[0,37.361],[0,0],[0,0],[0,0]],"o":[[-32.978,0],[0,0],[0,0],[0,0],[0,37.919]],"v":[[0,33.974],[-52.667,-27.333],[-52.667,-32.988],[52.667,-32.988],[52.667,-27.333]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[-0.289]},"n":["0_1_0p167_-0p289"],"t":60,"s":[0.7764706,0.7764706,0.7764706,1],"e":[0.1058824,0.1058824,0.1058824,1]},{"t":69.0000028104276}]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":450.000018328876,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":6,"ty":4,"nm":"Shape Layer 8","parent":3,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[0,10,0],"e":[0,10,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":59,"s":[0,10,0],"e":[0,0,0],"to":[0,-1.66666662693024,0],"ti":[0,1.66666662693024,0]},{"t":60.0000024438501}]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[65.318,23.995]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[-0.591,12.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":450.000018328876,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":7,"ty":4,"nm":"Shape Layer 10","td":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"n":"0_1_0p167_0p167","t":60,"s":[55,125,0],"e":[55,55,0],"to":[0,-11.6666669845581,0],"ti":[0,11.6666669845581,0]},{"t":65.0000026475043}]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[109.443,77.017]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect"},{"ty":"fl","c":{"a":0,"k":[1,0,0,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0.722,-2.742],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":450.000018328876,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":8,"ty":4,"nm":"Shape Layer 9","tt":1,"ks":{"o":{"a":0,"k":25},"r":{"a":0,"k":0},"p":{"a":0,"k":[55,50,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[33.471,0],[0,37.361],[0,0],[0,0],[0,0]],"o":[[-32.978,0],[0,0],[0,0],[0,0],[0,37.919]],"v":[[0,33.974],[-52.667,-27.333],[-52.667,-32.988],[52.667,-32.988],[52.667,-27.333]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[-0.289]},"n":["0_1_0p167_-0p289"],"t":60,"s":[0.7764706,0.7764706,0.7764706,1],"e":[0.1058824,0.1058824,0.1058824,1]},{"t":69.0000028104276}]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":450.000018328876,"st":0,"bm":0,"sr":1}]}],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 7","td":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[400,300,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[47.744,0],[0,47.743],[-47.744,0],[0,-47.743]],"o":[[-47.744,0],[0,-47.743],[47.744,0],[0,47.743]],"v":[[0,86.586],[-86.586,0],[0,-86.586],[86.586,0]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.1058824,0.1058824,0.1058824,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":450.000018328876,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":2,"ty":0,"nm":"Eyes","tt":1,"refId":"comp_2","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"n":"0_1_0p167_0p167","t":157,"s":[292,293,0],"e":[437,293,0],"to":[24.1666660308838,0,0],"ti":[-24.1666660308838,0,0]},{"t":172.000007005704}]},"a":{"a":0,"k":[45,45,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":90,"h":90,"ip":157.000006394741,"op":607.000024723617,"st":157.000006394741,"bm":0,"sr":1},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 6","td":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[400,300,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[47.744,0],[0,47.743],[-47.744,0],[0,-47.743]],"o":[[-47.744,0],[0,-47.743],[47.744,0],[0,47.743]],"v":[[0,86.586],[-86.586,0],[0,-86.586],[86.586,0]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.1058824,0.1058824,0.1058824,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":450.000018328876,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":4,"ty":0,"nm":"Eyes","tt":1,"refId":"comp_2","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"n":"0_1_0p167_0p167","t":157,"s":[228,293,0],"e":[363.125,293,0],"to":[22.5208339691162,0,0],"ti":[-22.5208339691162,0,0]},{"t":172.000007005704}]},"a":{"a":0,"k":[45,45,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":90,"h":90,"ip":157.000006394741,"op":607.000024723617,"st":157.000006394741,"bm":0,"sr":1},{"ddd":0,"ind":5,"ty":4,"nm":"Shape Layer 4","td":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[400,300,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":65,"s":[131,131,100],"e":[117,117,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":150,"s":[117,117,100],"e":[100,100,100]},{"t":155.000006313279}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[47.744,0],[0,47.743],[-47.744,0],[0,-47.743]],"o":[[-47.744,0],[0,-47.743],[47.744,0],[0,47.743]],"v":[[0,86.586],[-86.586,0],[0,-86.586],[86.586,0]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.1058824,0.1058824,0.1058824,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":450.000018328876,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":6,"ty":0,"nm":"Eyes","tt":1,"refId":"comp_2","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"n":"0_1_0p167_0p167","t":59,"s":[437,293,0],"e":[448,279.5,0],"to":[1.83333337306976,-2.25,0],"ti":[-1.83333337306976,2.25,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":67,"s":[448,279.5,0],"e":[448,279.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"n":"0p833_0p833_1_0","t":150,"s":[448,279.5,0],"e":[638,279.5,0],"to":[31.6666660308838,0,0],"ti":[-31.6666660308838,0,0]},{"t":160.000006516934}]},"a":{"a":0,"k":[45,45,0]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p667_1_0p167_0p167","0p667_1_0p167_0p167","0p667_0p667_0p167_0p167"],"t":59,"s":[100,100,100],"e":[125,125,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p833_0p833_0p333_0","0p833_0p833_0p333_0","0p833_0p833_0p333_0p333"],"t":65,"s":[125,125,100],"e":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":71,"s":[100,100,100],"e":[100,100,100]},{"t":147.000005987433}]}},"ao":0,"w":90,"h":90,"ip":0,"op":450.000018328876,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":7,"ty":4,"nm":"Shape Layer 3","td":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[400,300,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":65,"s":[131,131,100],"e":[117,117,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":152,"s":[117,117,100],"e":[100,100,100]},{"t":157.000006394741}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[47.744,0],[0,47.743],[-47.744,0],[0,-47.743]],"o":[[-47.744,0],[0,-47.743],[47.744,0],[0,47.743]],"v":[[0,86.586],[-86.586,0],[0,-86.586],[86.586,0]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.1058824,0.1058824,0.1058824,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":450.000018328876,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":8,"ty":0,"nm":"Eyes","tt":1,"refId":"comp_2","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"n":"0_1_0p167_0p167","t":59,"s":[363,293,0],"e":[353,279.5,0],"to":[-1.66666662693024,-2.25,0],"ti":[1.66666662693024,2.25,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":67,"s":[353,279.5,0],"e":[353,279.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"n":"0p833_0p833_1_0","t":150,"s":[353,279.5,0],"e":[553,279.5,0],"to":[33.3333320617676,0,0],"ti":[-33.3333320617676,0,0]},{"t":160.000006516934}]},"a":{"a":0,"k":[45,45,0]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p667_1_0p167_0p167","0p667_1_0p167_0p167","0p667_0p667_0p167_0p167"],"t":59,"s":[100,100,100],"e":[125,125,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p833_0p833_0p333_0","0p833_0p833_0p333_0","0p833_0p833_0p333_0p333"],"t":65,"s":[125,125,100],"e":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":71,"s":[100,100,100],"e":[100,100,100]},{"t":147.000005987433}]}},"ao":0,"w":90,"h":90,"ip":0,"op":450.000018328876,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":9,"ty":4,"nm":"Shape Layer 1","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[400,300,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"s","pt":{"a":0,"k":{"i":[[41.127,0],[0,-41.127],[-41.127,0],[0,41.127]],"o":[[-41.127,0],[0,41.127],[41.127,0],[0,-41.127]],"v":[[0.5,-74.086],[-74.086,0.5],[0.5,75.086],[75.086,0.5]],"c":true}},"o":{"a":0,"k":100},"x":{"a":0,"k":0},"nm":"Mask 1"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[47.744,0],[0,47.743],[-47.744,0],[0,-47.743]],"o":[[-47.744,0],[0,-47.743],[47.744,0],[0,47.743]],"v":[[0,86.586],[-86.586,0],[0,-86.586],[86.586,0]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[-0.289]},"n":["0_1_0p167_-0p289"],"t":60,"s":[0.7764706,0.7764706,0.7764706,1],"e":[0.1058824,0.1058824,0.1058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":65,"s":[0.1058824,0.1058824,0.1058824,1],"e":[0.1058824,0.1058824,0.1058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":157,"s":[0.1058824,0.1058824,0.1058824,1],"e":[0.7764706,0.7764706,0.7764706,1]},{"t":160.000006516934}]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":450.000018328876,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":10,"ty":4,"nm":"Shape Layer 5","td":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[400,300,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[47.744,0],[0,47.743],[-47.744,0],[0,-47.743]],"o":[[-47.744,0],[0,-47.743],[47.744,0],[0,47.743]],"v":[[0,86.586],[-86.586,0],[0,-86.586],[86.586,0]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.1058824,0.1058824,0.1058824,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":450.000018328876,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":11,"ty":0,"nm":"Mouth","tt":1,"refId":"comp_3","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"n":"0_1_0p167_0p167","t":157,"s":[265,322,0],"e":[400,322,0],"to":[22.5,0,0],"ti":[-22.5,0,0]},{"t":172.000007005704}]},"a":{"a":0,"k":[55,50,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":110,"h":100,"ip":157.000006394741,"op":607.000024723617,"st":157.000006394741,"bm":0,"sr":1},{"ddd":0,"ind":12,"ty":4,"nm":"Shape Layer 2","td":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[400,300,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[47.744,0],[0,47.743],[-47.744,0],[0,-47.743]],"o":[[-47.744,0],[0,-47.743],[47.744,0],[0,47.743]],"v":[[0,86.586],[-86.586,0],[0,-86.586],[86.586,0]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.1058824,0.1058824,0.1058824,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":450.000018328876,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":13,"ty":0,"nm":"Mouth","tt":1,"refId":"comp_3","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"n":"0_1_0p167_0p167","t":59,"s":[400,322,0],"e":[400,333,0],"to":[0,1.83333337306976,0],"ti":[0,-1.83333337306976,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":67,"s":[400,333,0],"e":[400,333,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"n":"0p833_0p833_1_0","t":150,"s":[400,333,0],"e":[560,333,0],"to":[26.6666660308838,0,0],"ti":[-26.6666660308838,0,0]},{"t":160.000006516934}]},"a":{"a":0,"k":[55,50,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":110,"h":100,"ip":0,"op":450.000018328876,"st":0,"bm":0,"sr":1}]} -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | android-mvvm-unittest 3 | 4 | %s + %s = %s 5 | score : %d 6 | animation speed : ✕ %d 7 | loading 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/cchcc/learn/amu/e01/E01ViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e01 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import org.junit.Assert 5 | import org.junit.Rule 6 | import org.junit.Test 7 | 8 | class E01ViewModelTest { 9 | 10 | @Rule 11 | @JvmField 12 | val rule = InstantTaskExecutorRule() 13 | 14 | private val viewModel by lazy { E01ViewModel() } 15 | 16 | private fun setValues(value: String) { 17 | viewModel.left.value = value 18 | viewModel.right.value = value 19 | } 20 | 21 | @Test 22 | fun plus() { 23 | // given 24 | val givenVal = "1" 25 | setValues(givenVal) 26 | 27 | // when 28 | viewModel.plus() 29 | 30 | // then 31 | Assert.assertEquals("2", viewModel.result.value) 32 | } 33 | 34 | 35 | @Test 36 | fun clear() { 37 | // given 38 | val givenVal = "1" 39 | setValues(givenVal) 40 | 41 | // when 42 | viewModel.clear() 43 | 44 | // then 45 | Assert.assertEquals("", viewModel.left.value) 46 | Assert.assertEquals("", viewModel.right.value) 47 | Assert.assertEquals("", viewModel.result.value) 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/test/java/cchcc/learn/amu/e01a/E01aViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e01a 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import androidx.lifecycle.Lifecycle 5 | import androidx.lifecycle.LifecycleRegistry 6 | import io.mockk.every 7 | import io.mockk.mockk 8 | import io.mockk.verify 9 | import org.junit.Assert 10 | import org.junit.Rule 11 | import org.junit.Test 12 | 13 | class E01aViewModelTest { 14 | @Rule 15 | @JvmField 16 | val rule = InstantTaskExecutorRule() 17 | 18 | @Test fun result_only_when_two_values_changed() { 19 | val viewModel = E01aViewModel() 20 | val lifecycle = LifecycleRegistry(mockk()).apply { 21 | handleLifecycleEvent(Lifecycle.Event.ON_RESUME) 22 | } 23 | 24 | // given 25 | val givenLeft = "1" 26 | val givenRight = "1" 27 | 28 | val visibleResultObserver = mockk<(Boolean?) -> Unit>().also { 29 | every { it.invoke(false) } returns Unit // for initial value 30 | } 31 | viewModel.result.observe({ lifecycle }) {} 32 | viewModel.visibleResult.observe({ lifecycle }, visibleResultObserver) 33 | 34 | // when 35 | viewModel.left.value = givenLeft 36 | 37 | // then 38 | Assert.assertNull(viewModel.result.value) 39 | 40 | // when 41 | every { visibleResultObserver.invoke(true) } returns Unit 42 | viewModel.right.value = givenRight 43 | 44 | // then 45 | verify { visibleResultObserver.invoke(true) } 46 | Assert.assertEquals("2", viewModel.result.value) 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/test/java/cchcc/learn/amu/e02/E02ViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e02 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import org.junit.Assert 5 | import org.junit.Rule 6 | import org.junit.Test 7 | 8 | class E02ViewModelTest { 9 | @Rule 10 | @JvmField 11 | val rule = InstantTaskExecutorRule() 12 | 13 | private fun justTrue() = true 14 | 15 | @Test 16 | fun tryResult_and_applyScore() { 17 | val viewModel = E02ViewModel(::justTrue) 18 | 19 | // given 20 | // none 21 | 22 | // when 23 | viewModel.tryResult() 24 | 25 | // then 26 | Assert.assertEquals(E02ViewModel.TryResult.SUCCESS, viewModel.result.value) 27 | 28 | // given 29 | // none 30 | 31 | // when 32 | viewModel.applyScore() 33 | 34 | // then 35 | Assert.assertEquals(1, viewModel.score.value) 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /app/src/test/java/cchcc/learn/amu/e02a/E02aViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e02a 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import cchcc.learn.amu.e02.E02ViewModel 5 | import cchcc.learn.amu.e02a.di.E02aTestViewModelModule 6 | import org.junit.Assert 7 | import org.junit.Rule 8 | import org.junit.Test 9 | import org.kodein.di.Kodein 10 | import org.kodein.di.KodeinAware 11 | import org.kodein.di.generic.instance 12 | import org.kodein.di.newInstance 13 | 14 | class E02aViewModelTest : KodeinAware { 15 | 16 | @Rule 17 | @JvmField 18 | val rule = InstantTaskExecutorRule() 19 | 20 | override val kodein: Kodein = Kodein.lazy { 21 | import(E02aTestViewModelModule) 22 | } 23 | 24 | @Test 25 | fun tryResult_and_applyScore() { 26 | val viewModel by kodein.newInstance { E02ViewModel(instance()) } 27 | // given 28 | // none 29 | 30 | // when 31 | viewModel.tryResult() 32 | // then 33 | Assert.assertEquals(E02ViewModel.TryResult.SUCCESS, viewModel.result.value) 34 | 35 | // when 36 | viewModel.applyScore() 37 | // then 38 | Assert.assertEquals(1, viewModel.score.value) 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /app/src/test/java/cchcc/learn/amu/e02a/di/E02aTestModules.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e02a.di 2 | 3 | import org.kodein.di.Kodein 4 | import org.kodein.di.generic.bind 5 | import org.kodein.di.generic.singleton 6 | 7 | 8 | val E02aTestViewModelModule = Kodein.Module("E02ViewModel") { 9 | bind<() -> Boolean>() with singleton { { true } } 10 | } -------------------------------------------------------------------------------- /app/src/test/java/cchcc/learn/amu/e03/E03ViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e03 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import org.junit.Assert 5 | import org.junit.Rule 6 | import org.junit.Test 7 | 8 | class E03ViewModelTest { 9 | @Rule 10 | @JvmField 11 | val rule = InstantTaskExecutorRule() 12 | 13 | @Test 14 | fun add() { 15 | val viewModel = E03ViewModel() 16 | 17 | // given 18 | val beforeSize = viewModel.memos.value!!.size 19 | val givenNewContent = "new content" 20 | 21 | viewModel.newContent.value = givenNewContent 22 | 23 | // when 24 | viewModel.add() 25 | 26 | // then 27 | Assert.assertEquals(givenNewContent, viewModel.memos.value!![0].content) 28 | Assert.assertEquals(beforeSize + 1, viewModel.memos.value!!.size) 29 | } 30 | 31 | @Test 32 | fun remove() { 33 | val viewModel = E03ViewModel() 34 | 35 | // given 36 | val beforeSize = viewModel.memos.value!!.size 37 | val givenIdxWillBeRemove = 3 38 | val beforeMemo = viewModel.memos.value!![givenIdxWillBeRemove] 39 | 40 | // when 41 | viewModel.remove(beforeMemo, givenIdxWillBeRemove) 42 | 43 | // then 44 | Assert.assertFalse(beforeMemo == viewModel.memos.value!![givenIdxWillBeRemove]) 45 | Assert.assertEquals(beforeSize - 1, viewModel.memos.value!!.size) 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /app/src/test/java/cchcc/learn/amu/e04/E04ViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e04 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import androidx.lifecycle.Lifecycle 5 | import androidx.lifecycle.LifecycleRegistry 6 | import io.mockk.mockk 7 | import org.junit.* 8 | 9 | class E04ViewModelTest { 10 | @Rule 11 | @JvmField 12 | val rule = InstantTaskExecutorRule() 13 | 14 | 15 | @Test 16 | fun pickContact() { 17 | val viewModel = E04ViewModel() 18 | val lifecycle = LifecycleRegistry(mockk()).apply { 19 | handleLifecycleEvent(Lifecycle.Event.ON_RESUME) 20 | } 21 | 22 | // given 23 | val givenName = "abc" 24 | val givenPhone = "111" 25 | 26 | viewModel.contact.observe({ lifecycle }) {} 27 | viewModel.pickContactAction.observe({ lifecycle }) { 28 | viewModel.nameAndPhone.value = givenName to givenPhone 29 | } 30 | 31 | // when 32 | viewModel.pickContact() 33 | 34 | // then 35 | Assert.assertEquals("Name : $givenName\nPhone : $givenPhone", viewModel.contact.value) 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/test/java/cchcc/learn/amu/e05/E05ViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e05 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import androidx.lifecycle.Lifecycle 5 | import androidx.lifecycle.LifecycleRegistry 6 | import io.mockk.mockk 7 | import org.junit.Assert 8 | import org.junit.Rule 9 | import org.junit.Test 10 | 11 | class E05ViewModelTest { 12 | @Rule 13 | @JvmField 14 | val rule = InstantTaskExecutorRule() 15 | 16 | @Test 17 | fun addAndClear() { 18 | val viewModel = E05ViewModel() 19 | 20 | // given 21 | val givenLine = "first" 22 | 23 | // when 24 | viewModel.add(givenLine) 25 | 26 | // then 27 | Assert.assertEquals(1, viewModel.logList.value!!.size) 28 | Assert.assertTrue(viewModel.logList.value!![0].contains(givenLine)) 29 | 30 | // when 31 | viewModel.add(givenLine) 32 | viewModel.add(givenLine) 33 | viewModel.clear() 34 | 35 | // then 36 | Assert.assertEquals(1, viewModel.logList.value!!.size) 37 | } 38 | 39 | 40 | 41 | @Test 42 | fun animSpeed() { 43 | val viewModel = E05ViewModel() 44 | val lifecycle = LifecycleRegistry(mockk()).apply { 45 | handleLifecycleEvent(Lifecycle.Event.ON_RESUME) 46 | } 47 | viewModel.animSpeed.observe({ lifecycle }) {} 48 | 49 | // given 50 | val givenProgress = 0 51 | 52 | // when 53 | viewModel.progress.value = givenProgress 54 | 55 | // then 56 | Assert.assertEquals(1, viewModel.animSpeed.value) 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /app/src/test/java/cchcc/learn/amu/e06/E06ViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package cchcc.learn.amu.e06 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import androidx.lifecycle.Lifecycle 5 | import androidx.lifecycle.LifecycleRegistry 6 | import cchcc.learn.amu.e06.data.E06DataRepository 7 | import cchcc.learn.amu.e06.data.E06Number 8 | import io.mockk.every 9 | import io.mockk.mockk 10 | import io.mockk.verify 11 | import org.junit.Assert 12 | import org.junit.Rule 13 | import org.junit.Test 14 | 15 | class E06ViewModelTest { 16 | @Rule 17 | @JvmField 18 | val rule = InstantTaskExecutorRule() 19 | 20 | @Test 21 | fun refreshList() { 22 | val repo = mockk() 23 | val viewModel = E06ViewModel(repo) 24 | 25 | // given 26 | every { repo.refresh() } returns Unit 27 | 28 | // when 29 | viewModel.refreshList { } 30 | 31 | // then 32 | verify { repo.refresh() } 33 | } 34 | 35 | 36 | @Test 37 | fun moveToTopOfList() { 38 | val viewModel = E06ViewModel(E06DataRepository()) 39 | val lifecycle = LifecycleRegistry(mockk()).apply { 40 | handleLifecycleEvent(Lifecycle.Event.ON_RESUME) 41 | } 42 | 43 | // given 44 | val observe = mockk<(Unit?) -> Unit>() 45 | every { observe.invoke(Unit) } returns Unit 46 | viewModel.moveToTopAction.observe({ lifecycle }, observe) 47 | 48 | // when 49 | viewModel.moveToTopOfList() 50 | 51 | // then 52 | verify { observe.invoke(Unit) } 53 | } 54 | 55 | @Test 56 | fun colorizeListItem() { 57 | val viewModel = E06ViewModel(E06DataRepository()) 58 | 59 | // given 60 | val numWithAlphaIsLess = E06Number(1, 0.1f) 61 | 62 | // when 63 | viewModel.colorizeListItem(numWithAlphaIsLess) {} 64 | 65 | // then 66 | Assert.assertEquals(0.2f, numWithAlphaIsLess.alpha) 67 | 68 | 69 | // given 70 | val numWithAlphaIs1 = E06Number(1, 1.0f) 71 | 72 | // when 73 | viewModel.colorizeListItem(numWithAlphaIs1) {} 74 | 75 | // then 76 | Assert.assertEquals(1.0f, numWithAlphaIs1.alpha) 77 | } 78 | 79 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.3.50' 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.5.0' 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | jcenter() 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } 28 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | 15 | # New data binding compiler for binding classes 16 | # https://developer.android.com/topic/libraries/data-binding/start#preview-compiler 17 | #android.databinding.enableV2=true 18 | android.useAndroidX=true 19 | android.enableJetifier=true 20 | kapt.incremental.apt=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cchcc/android-mvvm-unittest/9de200ea3018138077d513ebf349ea1b293804ec/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Aug 27 17:03:39 KST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------