diff --git a/.github/workflows/runTests.yml b/.github/workflows/runTests.yml index f2995a8b..26e16eba 100644 --- a/.github/workflows/runTests.yml +++ b/.github/workflows/runTests.yml @@ -15,8 +15,8 @@ jobs: uses: actions/setup-java@v3 with: distribution: 'adopt' - java-version: 11 + java-version: 17 - name: Build with Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/actions/setup-gradle@v3 with: arguments: testDebugUnitTest lintRelease checkstyle lintKotlin diff --git a/CHANGELOG.md b/CHANGELOG.md index d95864fc..16a26c3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## 2.2.8-beta +- Support removal from `LazySetNavigator` + +## 2.2.7-beta +- Fixes crash during synchronous navigation in `LazySetNavigator` +- Support backstack read operations from a background thread + +## 2.2.6-beta +- Fixes `CircularRevealTransition` attempting to target non-existent views + +## 2.2.5-beta +- Fixes `CircularRevealTransition` leaking memory and crashing on back press. + +## 2.2.4-beta +- Fixes `DialogComponent` not displaying dialog when `showDialog` called before `resume` + +## 2.2.3-beta +- Fixes `onBackPressed` called twice for navigables at the top of a navigator's backstack +- Support non-Activity contexts (ie. ContextWrapper) + +## 2.2.2-beta +- Adds extension functions to `LinearNavigator` for common navigation patterns. + ## 2.2.1-beta - Support interrupting transitions to prevent overlapping animations in `LazySetNavigator` diff --git a/README.md b/README.md index 0d020159..714b6675 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Add the dependencies you need in your `build.gradle`: ### Core library ```groovy -def magellanVersion = '2.2.1-beta' +def magellanVersion = '2.2.8-beta' implementation "com.wealthfront:magellan-library:${magellanVersion}" ``` diff --git a/RELEASING.md b/RELEASING.md index fbffa369..ba05bae4 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -5,7 +5,7 @@ - Update CHANGELOG.md for the impending release. - Update README.md with the new version. 2. `git commit -am "Prepare for release X.Y.Z"` (where X.Y.Z is the new version) -3. Open a Pull Request with the above changes. Get it merged +3. Open a Pull Request with the above changes. Get it merged. 4. Create a tag for this version - `git tag -a X.Y.Z -m "Version X.Y.Z"` (where X.Y.Z is the version) - Push this tag to GitHub: `git push && git push --tags` @@ -20,7 +20,7 @@ Visit [Maven Central Repository Search](https://search.maven.org/search?q=magell 7. Change the repo's metadata to reflect the next development cycle - Change gradle.properties to the next SNAPSHOT version. - `git commit -am "Prepare next development version"` -8. Open a Pull Request with the above changes. Get it merged +8. Open a Pull Request with the above changes. Get it merged. ## Publish to local maven repo diff --git a/build.gradle b/build.gradle index 5ade326e..8a266af7 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { repositories { jcenter() mavenCentral() - maven { url "https://plugins.gradle.org/m2/" } + maven {url "https://plugins.gradle.org/m2/"} google() } dependencies { diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 714cb117..12851859 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'org.jetbrains.kotlin.jvm' version '1.6.20' + id 'org.jetbrains.kotlin.jvm' version '1.8.22' } repositories { @@ -9,17 +9,18 @@ repositories { } dependencies { - implementation 'com.android.tools.build:gradle:7.2.2' + implementation 'com.android.tools.build:gradle:8.1.4' + implementation 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.22' } compileKotlin { kotlinOptions { - jvmTarget = "1.8" + jvmTarget = JavaVersion.VERSION_17 } } compileTestKotlin { kotlinOptions { - jvmTarget = "1.8" + jvmTarget = JavaVersion.VERSION_17 } } \ No newline at end of file diff --git a/buildSrc/src/main/java/Plugins.kt b/buildSrc/src/main/java/Plugins.kt new file mode 100644 index 00000000..89498479 --- /dev/null +++ b/buildSrc/src/main/java/Plugins.kt @@ -0,0 +1,10 @@ +import Versions.detektVersion +import Versions.kotlinVersion +import Versions.kotlinterVersion + +object Plugins { + const val kotlinGradle = "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" + const val kotlinterGradle = "org.jmailen.gradle:kotlinter-gradle:$kotlinterVersion" + const val kotlinAllOpen = "org.jetbrains.kotlin:kotlin-allopen:$kotlinVersion" + const val detekt = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:$detektVersion" +} \ No newline at end of file diff --git a/buildSrc/src/main/java/Versions.kt b/buildSrc/src/main/java/Versions.kt new file mode 100644 index 00000000..f37be961 --- /dev/null +++ b/buildSrc/src/main/java/Versions.kt @@ -0,0 +1,9 @@ +object Versions { + const val kotlinVersion = "1.8.22" + const val kotlinterVersion = "3.9.0" + const val detektVersion = "1.19.0" + + const val compileSdkVersion = 34 + const val minSdkVersion = 21 + const val targetSdkVersion = 30 +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt deleted file mode 100644 index 372f6e55..00000000 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ /dev/null @@ -1,95 +0,0 @@ -import Versions.androidXCoreVersion -import Versions.archVersion -import Versions.butterKnifeVersion -import Versions.constraintLayoutVersion -import Versions.coroutinesVersion -import Versions.daggerVersion -import Versions.detektVersion -import Versions.espressoVersion -import Versions.glideVersion -import Versions.jacksonVersion -import Versions.javaInjectVersion -import Versions.jodaTimeVersion -import Versions.junitTestExtVersion -import Versions.junitVersion -import Versions.kotlinVersion -import Versions.kotlinterVersion -import Versions.lifecycleVersion -import Versions.lintVersion -import Versions.materialVersion -import Versions.mockKVersion -import Versions.mockitoVersion -import Versions.okhttpVersion -import Versions.retrofitVersion -import Versions.robolectricVersion -import Versions.rxAndroid2Version -import Versions.rxandroidVersion -import Versions.rxjava2Version -import Versions.rxjavaAdapterVersion -import Versions.rxjavaVersion -import Versions.supportLibVersion -import Versions.testCoreVersion -import Versions.testRunnerVersion -import Versions.truthVersion -import Versions.uiAutomatorVersion - -object Plugins { - const val kotlinGradle = "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" - const val kotlinterGradle = "org.jmailen.gradle:kotlinter-gradle:$kotlinterVersion" - const val kotlinAllOpen = "org.jetbrains.kotlin:kotlin-allopen:$kotlinVersion" - const val detekt = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:$detektVersion" -} - -object Dependencies { - - const val appCompat = "androidx.appcompat:appcompat:$supportLibVersion" - const val constraintLayout = "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion" - const val lifecycle = "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion" - const val androidXCore = "androidx.core:core-ktx:$androidXCoreVersion" - - const val material = "com.google.android.material:material:$materialVersion" - const val junit = "junit:junit:$junitVersion" - const val truth = "com.google.truth:truth:$truthVersion" - const val mockito = "org.mockito:mockito-core:$mockitoVersion" - const val mockK = "io.mockk:mockk:$mockKVersion" - const val archTesting = "androidx.arch.core:core-testing:$archVersion" - const val robolectric = "org.robolectric:robolectric:$robolectricVersion" - const val butterknife = "com.jakewharton:butterknife:$butterKnifeVersion" - const val butterknifeCompiler = "com.jakewharton:butterknife-compiler:$butterKnifeVersion" - const val dagger = "com.google.dagger:dagger:$daggerVersion" - const val daggerCompiler = "com.google.dagger:dagger-compiler:$daggerVersion" - const val inject = "javax.inject:javax.inject:$javaInjectVersion" - const val coroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" - const val coroutinesAndroid = "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" - const val coroutinesTest = "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion" - - const val glide = "com.github.bumptech.glide:glide:$glideVersion" - const val retrofit = "com.squareup.retrofit2:retrofit:$retrofitVersion" - const val retrofitMock = "com.squareup.retrofit2:retrofit-mock:$retrofitVersion" - const val rxjavaAdapter = "com.squareup.retrofit2:adapter-rxjava:$rxjavaAdapterVersion" - const val rxJava2Adapter = "com.squareup.retrofit2:adapter-rxjava2:$rxjavaAdapterVersion" - const val rxjava = "io.reactivex:rxjava:$rxjavaVersion" - const val rxjava2 = "io.reactivex.rxjava2:rxjava:$rxjava2Version" - const val rxandroid = "io.reactivex:rxandroid:$rxandroidVersion" - const val rxAndroid2 = "io.reactivex.rxjava2:rxandroid:$rxAndroid2Version" - const val jackson = "com.squareup.retrofit2:converter-jackson:$jacksonVersion" - const val okhttp = "com.squareup.okhttp3:logging-interceptor:$okhttpVersion" - const val jodaTime = "net.danlew:android.joda:$jodaTimeVersion" - - const val testCore = "androidx.test:core:$testCoreVersion" - const val testCoreKtx = "androidx.test:core-ktx:$testCoreVersion" - const val testRunner = "androidx.test:runner:$testRunnerVersion" - const val testRules = "com.android.support.test:rules:$testRunnerVersion" - const val uiAutomator ="androidx.test.uiautomator:uiautomator:$uiAutomatorVersion" - const val extJunit = "androidx.test.ext:junit:$junitTestExtVersion" - const val extJunitKtx = "androidx.test.ext:junit-ktx:$junitTestExtVersion" - const val espressoCore = "androidx.test.espresso:espresso-core:$espressoVersion" - const val rx2idler = "com.squareup.rx.idler:rx2-idler:0.11.0" - - const val lintApi = "com.android.tools.lint:lint-api:$lintVersion" - const val lintChecks = "com.android.tools.lint:lint-checks:$lintVersion" - const val lint = "com.android.tools.lint:lint:$lintVersion" - const val lintTests = "com.android.tools.lint:lint-tests:$lintVersion" - const val testUtils = "com.android.tools:testutils:$lintVersion" -} - diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt deleted file mode 100644 index 8271528e..00000000 --- a/buildSrc/src/main/kotlin/Versions.kt +++ /dev/null @@ -1,43 +0,0 @@ -object Versions { - const val compileSdkVersion = 30 - const val minSdkVersion = 18 - const val targetSdkVersion = 30 - - const val kotlinVersion = "1.6.20" - const val kotlinterVersion = "3.9.0" - const val detektVersion = "1.19.0" - const val supportLibVersion = "1.3.1" - const val constraintLayoutVersion = "2.1.0" - const val robolectricVersion = "4.8" - const val archVersion = "2.1.0" - const val lifecycleVersion = "2.2.0" - const val androidXCoreVersion = "1.6.0" - const val butterKnifeVersion = "10.0.0" - const val daggerVersion = "2.24" - const val javaInject = "1" - const val retrofitVersion = "2.9.0" - const val glideVersion = "4.11.0" - const val rxjavaVersion = "1.3.8" - const val rxjava2Version = "2.2.19" - const val rxjavaAdapterVersion = "2.3.0" - const val rxandroidVersion = "1.2.1" - const val rxAndroid2Version = "2.1.1" - const val jacksonVersion = "2.7.2" - const val okhttpVersion = "4.4.0" - const val jodaTimeVersion = "2.10.9.1" - const val javaInjectVersion = "1" - const val materialVersion = "1.4.0" - const val coroutinesVersion = "1.6.4" - - const val testCoreVersion = "1.4.0" - const val junitVersion = "4.13.2" - const val junitTestExtVersion = "1.1.3" - const val truthVersion = "1.0" - const val mockitoVersion = "2.23.4" - const val mockKVersion = "1.12.5" - const val testRunnerVersion = "1.4.0" - const val uiAutomatorVersion = "2.2.0" - const val espressoVersion = "3.4.0" - - const val lintVersion = "30.0.2" -} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 5332a2b0..3da0879f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ GROUP=com.wealthfront -VERSION_NAME=2.2.1-beta +VERSION_NAME=2.2.8-beta POM_DESCRIPTION=The simplest navigation library for Android @@ -21,4 +21,4 @@ SNAPSHOT_REPOSITORY_URL=https://oss.sonatype.org/content/repositories/snapshots/ android.useAndroidX=true android.enableJetifier=true -org.gradle.jvmargs=-Xmx8g -XX:MaxPermSize=1g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +org.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 diff --git a/gradle/gradle-mvn-push.gradle b/gradle/gradle-mvn-push.gradle index 72a69534..91d1d6d9 100644 --- a/gradle/gradle-mvn-push.gradle +++ b/gradle/gradle-mvn-push.gradle @@ -81,28 +81,10 @@ afterEvaluate { project -> } } - if (project.getPlugins().hasPlugin('com.android.application') || - project.getPlugins().hasPlugin('com.android.library')) { - - task androidSourcesJar(type: Jar) { - classifier = 'sources' - from android.sourceSets.main.java.source - } - } - - artifacts { - if (project.getPlugins().hasPlugin('com.android.application') || - project.getPlugins().hasPlugin('com.android.library')) { - archives androidSourcesJar - } - } - publishing.publications.all { publication -> publication.groupId = GROUP publication.version = VERSION_NAME - publication.artifact androidSourcesJar - configurePom(publication.pom) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..bc4906ad --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,105 @@ +[versions] +kotlin = "1.8.22" +kotlinter = "3.9.0" +detekt = "1.19.0" +supportLib = "1.3.1" +constraintLayout = "2.1.0" +robolectric = "4.8" +arch = "2.1.0" +lifecycle = "2.2.0" +androidXCore = "1.6.0" +butterKnife = "10.0.0" +dagger = "2.48" +javaInject = "1" +retrofit = "2.9.0" +glide = "4.11.0" +rxjava = "1.3.8" +rxjava2 = "2.2.19" +rxjavaAdapter = "2.3.0" +rxandroid = "1.2.1" +rxAndroid2 = "2.1.1" +jackson = "2.7.2" +okhttp = "4.4.0" +jodaTime = "2.10.9.1" +javainject = "1" +material = "1.4.0" +recyclerView = "1.2.1" +coroutines = "1.6.4" +compose-bom = "2024.04.01" + +testCore = "1.4.0" +junit = "4.13.2" +junitTestExt = "1.1.3" +truth = "1.0" +mockito = "2.23.4" +mockK = "1.12.5" +testRunner = "1.4.0" +uiAutomator = "2.2.0" +espresso = "3.4.0" + +lint = "30.0.2" + +[libraries] +appCompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "supportLib" } +constraintLayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintLayout" } +lifecycle = { group = "androidx.lifecycle", name = "lifecycle-common-java8", version.ref = "lifecycle" } +androidXCore = { group = "androidx.core", name = "core-ktx", version.ref = "androidXCore" } + +material = { group = "com.google.android.material", name = "material", version.ref = "material" } +recyclerView = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerView" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +truth = { group = "com.google.truth", name = "truth", version.ref = "truth" } +mockito = { group = "org.mockito", name = "mockito-core", version.ref = "mockito" } +mockitoAndroid = { group = "org.mockito", name = "mockito-android", version.ref = "mockito" } +mockK = { group = "io.mockk", name = "mockk", version.ref = "mockK" } +archTesting = { group = "androidx.arch.core", name = "core-testing", version.ref = "arch" } +robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" } +butterknife = { group = "com.jakewharton", name = "butterknife", version.ref = "butterKnife" } +butterknifeCompiler = { group = "com.jakewharton", name = "butterknife-compiler", version.ref = "butterKnife" } +dagger = { group = "com.google.dagger", name = "dagger", version.ref = "dagger" } +daggerCompiler = { group = "com.google.dagger", name = "dagger-compiler", version.ref = "dagger" } +inject = { group = "javax.inject", name = "javax.inject", version.ref = "javainject" } +coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" } +coroutinesAndroid = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" } +coroutinesTest = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" } + +compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } +compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } +compose-ui = { group = "androidx.compose.ui", name = "ui" } +compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +compose-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +compose-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +compose-material = { group = "androidx.compose.material", name = "material" } +compose-material3 = { group = "androidx.compose.material3", name = "material3" } +compose-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } +compose-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } + +glide = { group = "com.github.bumptech.glide", name = "glide", version.ref = "glide" } +retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } +retrofitMock = { group = "com.squareup.retrofit2", name = "retrofit-mock", version.ref = "retrofit" } +rxjavaAdapter = { group = "com.squareup.retrofit2", name = "adapter-rxjava", version.ref = "rxjavaAdapter" } +rxJava2Adapter = { group = "com.squareup.retrofit2", name = "adapter-rxjava2", version.ref = "rxjavaAdapter" } +rxjava = { group = "io.reactivex", name = "rxjava", version.ref = "rxjava" } +rxjava2 = { group = "io.reactivex.rxjava2", name = "rxjava", version.ref = "rxjava2" } +rxandroid = { group = "io.reactivex", name = "rxandroid", version.ref = "rxandroid" } +rxAndroid2 = { group = "io.reactivex.rxjava2", name = "rxandroid", version.ref = "rxAndroid2" } +jackson = { group = "com.squareup.retrofit2", name = "converter-jackson", version.ref = "jackson" } +okhttp = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } +jodaTime = { group = "net.danlew", name = "android.joda", version.ref = "jodaTime" } + +testCore = { group = "androidx.test", name = "core", version.ref = "testCore" } +testCoreKtx = { group = "androidx.test", name = "core-ktx", version.ref = "testCore" } +testRunner = { group = "androidx.test", name = "runner", version.ref = "testRunner" } +testRules = { group = "com.android.support.test", name = "rules", version.ref = "testRunner" } +uiAutomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiAutomator" } +extJunit = { group = "androidx.test.ext", name = "junit", version.ref = "junitTestExt" } +extJunitKtx = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "junitTestExt" } +espressoCore = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso" } +espressoContrib = { group = "androidx.test.espresso", name = "espresso-contrib", version.ref = "espresso" } +rx2idler = { group = "com.squareup.rx.idler", name = "rx2-idler", version = "0.11.0" } + +lintApi = { group = "com.android.tools.lint", name = "lint-api", version.ref = "lint" } +lintChecks = { group = "com.android.tools.lint", name = "lint-checks", version.ref = "lint" } +lint = { group = "com.android.tools.lint", name = "lint", version.ref = "lint" } +lintTests = { group = "com.android.tools.lint", name = "lint-tests", version.ref = "lint" } +testUtils = { group = "com.android.tools", name = "testutils", version.ref = "lint" } \ No newline at end of file diff --git a/gradle/static-analysis.gradle b/gradle/static-analysis.gradle index 3a651ddc..c6338566 100644 --- a/gradle/static-analysis.gradle +++ b/gradle/static-analysis.gradle @@ -23,7 +23,7 @@ kotlinter { } detekt { - toolVersion = Versions.detektVersion + toolVersion = "1.19.0" config = files("$rootDir/config/detekt/detekt-config.yml") parallel = true autoCorrect = true @@ -36,8 +36,6 @@ detekt { tasks.findByName("check")?.dependsOn("detekt") } -tasks.withType(Test) { - reports { - html.setEnabled(true) - } +tasks.withType(TestReport).configureEach { + enabled = true } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c062a07f..9742607e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Mon Aug 08 12:17:13 PDT 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/internal-test-support/build.gradle b/internal-test-support/build.gradle index d106b4ea..6fd039e7 100644 --- a/internal-test-support/build.gradle +++ b/internal-test-support/build.gradle @@ -7,15 +7,13 @@ android { defaultConfig { minSdkVersion Versions.minSdkVersion targetSdkVersion Versions.targetSdkVersion - versionCode 1 - versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } compileOptions { - setSourceCompatibility(JavaVersion.VERSION_1_8) - setTargetCompatibility(JavaVersion.VERSION_1_8) + setSourceCompatibility(JavaVersion.VERSION_17) + setTargetCompatibility(JavaVersion.VERSION_17) } buildFeatures { @@ -27,17 +25,18 @@ android { minifyEnabled false } } + namespace 'com.wealthfront.magellan.internal.test' } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { kotlinOptions.allWarningsAsErrors = true - kotlinOptions.jvmTarget = "1.8" + kotlinOptions.jvmTarget = JavaVersion.VERSION_17 } dependencies { implementation project(':magellan-library') implementation project(':magellan-legacy') - implementation Dependencies.inject - implementation Dependencies.appCompat + implementation libs.inject + implementation libs.appCompat } \ No newline at end of file diff --git a/internal-test-support/src/main/AndroidManifest.xml b/internal-test-support/src/main/AndroidManifest.xml index d69b3c2b..60a5ef46 100644 --- a/internal-test-support/src/main/AndroidManifest.xml +++ b/internal-test-support/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - diff --git a/magellan-legacy/build.gradle b/magellan-legacy/build.gradle index 663fb1ae..9bcb03d7 100644 --- a/magellan-legacy/build.gradle +++ b/magellan-legacy/build.gradle @@ -12,15 +12,13 @@ android { defaultConfig { minSdkVersion Versions.minSdkVersion targetSdkVersion Versions.targetSdkVersion - versionCode 1 - versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } compileOptions { - setSourceCompatibility(JavaVersion.VERSION_1_8) - setTargetCompatibility(JavaVersion.VERSION_1_8) + setSourceCompatibility(JavaVersion.VERSION_17) + setTargetCompatibility(JavaVersion.VERSION_17) } buildTypes { @@ -28,6 +26,7 @@ android { minifyEnabled false } } + namespace 'com.wealthfront.magellan.legacy' } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { @@ -35,23 +34,23 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { kotlinOptions.freeCompilerArgs = ['-Xexplicit-api=strict', '-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi'] } kotlinOptions.allWarningsAsErrors = true - kotlinOptions.jvmTarget = "1.8" + kotlinOptions.jvmTarget = JavaVersion.VERSION_17 } dependencies { implementation project(':magellan-library') - implementation Dependencies.appCompat - implementation Dependencies.inject - implementation Dependencies.coroutines - implementation Dependencies.coroutinesAndroid + implementation libs.appCompat + implementation libs.inject + implementation libs.coroutines + implementation libs.coroutinesAndroid testImplementation project(':internal-test-support') - testImplementation Dependencies.testCore - testImplementation Dependencies.junit - testImplementation Dependencies.truth - testImplementation Dependencies.mockito - testImplementation Dependencies.robolectric + testImplementation libs.testCore + testImplementation libs.junit + testImplementation libs.truth + testImplementation libs.mockito + testImplementation libs.robolectric } apply from: rootProject.file('gradle/gradle-mvn-push.gradle') diff --git a/magellan-legacy/src/main/AndroidManifest.xml b/magellan-legacy/src/main/AndroidManifest.xml index 6dee8de1..419acfa5 100644 --- a/magellan-legacy/src/main/AndroidManifest.xml +++ b/magellan-legacy/src/main/AndroidManifest.xml @@ -1,6 +1,4 @@ - + diff --git a/magellan-legacy/src/main/java/com/wealthfront/magellan/LegacyViewComponent.java b/magellan-legacy/src/main/java/com/wealthfront/magellan/LegacyViewComponent.java index ca630885..4327d3d2 100644 --- a/magellan-legacy/src/main/java/com/wealthfront/magellan/LegacyViewComponent.java +++ b/magellan-legacy/src/main/java/com/wealthfront/magellan/LegacyViewComponent.java @@ -1,6 +1,5 @@ package com.wealthfront.magellan; -import android.app.Activity; import android.content.Context; import android.os.Parcelable; import android.util.SparseArray; @@ -43,6 +42,6 @@ public void hide(@NotNull Context context) { } private void setActivity(@NotNull Context context) { - screen.setActivity((Activity) context); + screen.setActivity(ContextUtil.INSTANCE.findActivity(context)); } } diff --git a/magellan-legacy/src/main/java/com/wealthfront/magellan/Screen.java b/magellan-legacy/src/main/java/com/wealthfront/magellan/Screen.java index 0675d03d..641be3ae 100644 --- a/magellan-legacy/src/main/java/com/wealthfront/magellan/Screen.java +++ b/magellan-legacy/src/main/java/com/wealthfront/magellan/Screen.java @@ -227,7 +227,6 @@ public final void setView(@Nullable V view) { public final void setActivity(@Nullable Activity activity) { this.activity = activity; - this.dialogComponent.setContext(activity); } public final void setNavigator(@NotNull Navigator navigator) { diff --git a/magellan-legacy/src/test/java/com/wealthfront/magellan/LegacyViewComponentTest.java b/magellan-legacy/src/test/java/com/wealthfront/magellan/LegacyViewComponentTest.java index 9c10d48b..04090c39 100644 --- a/magellan-legacy/src/test/java/com/wealthfront/magellan/LegacyViewComponentTest.java +++ b/magellan-legacy/src/test/java/com/wealthfront/magellan/LegacyViewComponentTest.java @@ -2,13 +2,16 @@ import android.app.Activity; import android.content.Context; +import android.content.ContextWrapper; import android.util.SparseArray; import com.wealthfront.magellan.internal.test.DummyScreen; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.Mock; +import org.robolectric.RobolectricTestRunner; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.isA; @@ -16,19 +19,20 @@ import static org.mockito.Mockito.verify; import static org.mockito.MockitoAnnotations.initMocks; +@RunWith(RobolectricTestRunner.class) public class LegacyViewComponentTest { @Mock BaseScreenView view; - private LegacyViewComponent legacyViewComponent; + private LegacyViewComponent> legacyViewComponent; private DummyScreen screen; - private Context context = new Activity(); + private final Context context = new Activity(); @Before public void setUp() { initMocks(this); screen = new DummyScreen(view); - legacyViewComponent = new LegacyViewComponent(screen); + legacyViewComponent = new LegacyViewComponent<>(screen); } @Test @@ -86,6 +90,40 @@ public void viewComponentLifecycleConfigChange() { assertThat(screen.getActivity()).isEqualTo(context); } + @Test + public void viewComponentLifecycleWithContextWrapper() { + final Context context = new ContextWrapper(this.context); + assertThat(screen.getView()).isEqualTo(null); + assertThat(screen.getActivity()).isEqualTo(null); + + legacyViewComponent.create(context); + + assertThat(screen.getView()).isEqualTo(null); + assertThat(screen.getActivity()).isEqualTo(null); + + legacyViewComponent.show(context); + + assertThat(screen.getView()).isEqualTo(view); + assertThat(screen.getActivity()).isEqualTo(this.context); + + legacyViewComponent.resume(context); + + assertThat(screen.getView()).isEqualTo(view); + verify(view, never()).restoreHierarchyState(isA(SparseArray.class)); + assertThat(screen.getActivity()).isEqualTo(this.context); + + legacyViewComponent.pause(context); + + assertThat(screen.getView()).isEqualTo(view); + assertThat(screen.getActivity()).isEqualTo(this.context); + + legacyViewComponent.hide(context); + + assertThat(screen.getView()).isEqualTo(null); + verify(view).saveHierarchyState(isA(SparseArray.class)); + assertThat(screen.getActivity()).isEqualTo(null); + } + @Test public void viewRecreation() { viewComponentLifecycleUntilDestroy(); diff --git a/magellan-library/build.gradle b/magellan-library/build.gradle index 4b3bde8a..05d37338 100644 --- a/magellan-library/build.gradle +++ b/magellan-library/build.gradle @@ -12,15 +12,13 @@ android { defaultConfig { minSdkVersion Versions.minSdkVersion targetSdkVersion Versions.targetSdkVersion - versionCode 1 - versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } compileOptions { - setSourceCompatibility(JavaVersion.VERSION_1_8) - setTargetCompatibility(JavaVersion.VERSION_1_8) + setSourceCompatibility(JavaVersion.VERSION_17) + setTargetCompatibility(JavaVersion.VERSION_17) } testOptions { @@ -38,6 +36,14 @@ android { minifyEnabled false } } + + namespace 'com.wealthfront.magellan' + + publishing { + singleVariant("release") { + withSourcesJar() + } + } } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { @@ -47,27 +53,27 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { kotlinOptions.freeCompilerArgs = ['-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi,kotlinx.coroutines.InternalCoroutinesApi'] } kotlinOptions.allWarningsAsErrors = true - kotlinOptions.jvmTarget = "1.8" + kotlinOptions.jvmTarget = JavaVersion.VERSION_17 } dependencies { - implementation Dependencies.appCompat - implementation Dependencies.lifecycle - implementation Dependencies.inject - implementation Dependencies.coroutines - implementation Dependencies.coroutinesAndroid + implementation libs.appCompat + implementation libs.lifecycle + implementation libs.inject + implementation libs.coroutines + implementation libs.coroutinesAndroid testImplementation project(':internal-test-support') - testImplementation Dependencies.testCore - testImplementation Dependencies.testCoreKtx - testImplementation Dependencies.junit - testImplementation Dependencies.extJunitKtx - testImplementation Dependencies.truth - testImplementation Dependencies.mockito - testImplementation Dependencies.mockK - testImplementation Dependencies.archTesting - testImplementation Dependencies.robolectric - testImplementation Dependencies.coroutinesTest + testImplementation libs.testCore + testImplementation libs.testCoreKtx + testImplementation libs.junit + testImplementation libs.extJunitKtx + testImplementation libs.truth + testImplementation libs.mockito + testImplementation libs.mockK + testImplementation libs.archTesting + testImplementation libs.robolectric + testImplementation libs.coroutinesTest // Bug in AGP: Follow this issue - https://issuetracker.google.com/issues/141840950 // lintPublish project(':magellan-lint') diff --git a/magellan-library/src/main/AndroidManifest.xml b/magellan-library/src/main/AndroidManifest.xml index 6dd0ca9e..419acfa5 100644 --- a/magellan-library/src/main/AndroidManifest.xml +++ b/magellan-library/src/main/AndroidManifest.xml @@ -1,6 +1,4 @@ - + diff --git a/magellan-library/src/main/java/com/wealthfront/magellan/ContextUtil.kt b/magellan-library/src/main/java/com/wealthfront/magellan/ContextUtil.kt new file mode 100644 index 00000000..c2a56c33 --- /dev/null +++ b/magellan-library/src/main/java/com/wealthfront/magellan/ContextUtil.kt @@ -0,0 +1,21 @@ +package com.wealthfront.magellan + +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper + +public object ContextUtil { + public fun findActivity(context: Context): Activity { + return when (context) { + is Activity -> { + context + } + is ContextWrapper -> { + findActivity(context.baseContext) + } + else -> { + throw IllegalStateException("Context must be Activity or wrap Activity") + } + } + } +} diff --git a/magellan-library/src/main/java/com/wealthfront/magellan/navigation/LazySetNavigator.kt b/magellan-library/src/main/java/com/wealthfront/magellan/navigation/LazySetNavigator.kt index 70553c29..940aaeb6 100644 --- a/magellan-library/src/main/java/com/wealthfront/magellan/navigation/LazySetNavigator.kt +++ b/magellan-library/src/main/java/com/wealthfront/magellan/navigation/LazySetNavigator.kt @@ -1,7 +1,9 @@ package com.wealthfront.magellan.navigation import android.content.Context +import android.os.Build import android.view.View +import androidx.annotation.RequiresApi import androidx.annotation.VisibleForTesting import com.wealthfront.magellan.Direction import com.wealthfront.magellan.ScreenContainer @@ -22,6 +24,9 @@ public open class LazySetNavigator( private val navigationPropagator: NavigationPropagator = NavigationPropagator private var ongoingTransition: MagellanTransition? = null + @VisibleForTesting + internal var existingNavigables: MutableSet = mutableSetOf() + @VisibleForTesting internal var containerView: ScreenContainer? = null private var currentNavigable: NavigableCompat? = null @@ -36,9 +41,44 @@ public open class LazySetNavigator( } public fun addNavigable(navigable: NavigableCompat) { + existingNavigables.add(navigable) lifecycleRegistry.attachToLifecycleWithMaxState(navigable, LifecycleLimit.CREATED) } + @RequiresApi(Build.VERSION_CODES.N) + public fun removeNavigables(navigables: Set) { + for (navigable in navigables) { + removeNavigable(navigable) + } + } + + @RequiresApi(Build.VERSION_CODES.N) + public fun removeNavigable(navigable: NavigableCompat) { + existingNavigables.removeIf { it == navigable } + if (lifecycleRegistry.children.contains(navigable)) { + lifecycleRegistry.removeFromLifecycle(navigable) + } + } + + public fun safeAddNavigable(navigable: NavigableCompat) { + if (!existingNavigables.contains(navigable)) { + addNavigable(navigable) + } + } + + @RequiresApi(Build.VERSION_CODES.N) + public fun updateNavigables(navigables: Set, handleCurrentTabRemoval: () -> Unit) { + val navigablesToRemove = existingNavigables subtract navigables + val navigablesToAdd = navigables subtract existingNavigables + + if (navigablesToRemove.contains(currentNavigable)) { + handleCurrentTabRemoval() + } + + removeNavigables(navigablesToRemove) + addNavigables(navigablesToAdd) + } + override fun onShow(context: Context) { containerView = container() currentNavigable?.let { currentNavigable -> @@ -54,6 +94,7 @@ public open class LazySetNavigator( override fun onDestroy(context: Context) { lifecycleRegistry.children.forEach { lifecycleRegistry.removeFromLifecycle(it) } + existingNavigables.clear() } public fun replace( @@ -112,7 +153,15 @@ public open class LazySetNavigator( navigationPropagator.onNavigatedTo(currentNavigable) when (currentState) { is LifecycleState.Shown, is LifecycleState.Resumed -> { - containerView!!.addView(currentNavigable.view!!, 0) + val currentView = currentNavigable.view!! + val currentViewParent = currentView.parent + if (currentViewParent == null) { + containerView!!.addView(currentView, 0) + } else if (currentViewParent != containerView) { + throw IllegalStateException( + "currentNavigable ${currentNavigable.javaClass.simpleName} has view already attached to a parent" + ) + } } is LifecycleState.Destroyed, is LifecycleState.Created -> { } diff --git a/magellan-library/src/main/java/com/wealthfront/magellan/navigation/LinearNavigatorExtensions.kt b/magellan-library/src/main/java/com/wealthfront/magellan/navigation/LinearNavigatorExtensions.kt new file mode 100644 index 00000000..f9b1d69b --- /dev/null +++ b/magellan-library/src/main/java/com/wealthfront/magellan/navigation/LinearNavigatorExtensions.kt @@ -0,0 +1,87 @@ +package com.wealthfront.magellan.navigation + +import com.wealthfront.magellan.Direction +import com.wealthfront.magellan.core.Navigable +import com.wealthfront.magellan.init.getDefaultTransition +import com.wealthfront.magellan.transitions.MagellanTransition +import com.wealthfront.magellan.transitions.NoAnimationTransition +import java.util.Deque + +public fun LinearNavigator.goBackTo(navigable: Navigable) { + navigate(Direction.BACKWARD) { backStack -> + checkContains(backStack, navigable) + while (backStack.size > 1) { + if (backStack.peek()!!.navigable == navigable) { + break + } + backStack.pop() + } + backStack.peek()!!.magellanTransition + } +} + +public fun LinearNavigator.goBackBefore(navigable: Navigable) { + navigate(Direction.BACKWARD) { backStack -> + checkContains(backStack, navigable) + while (backStack.size > 0) { + if (backStack.pop()!!.navigable == navigable) { + break + } + } + val peek = backStack.peek() + ?: throw IllegalStateException("No Navigable before ${navigable::class.java.simpleName}") + peek.magellanTransition + } +} + +public fun LinearNavigator.goBackToRoot() { + goToRoot(Direction.BACKWARD) +} + +public fun LinearNavigator.goForwardToRoot() { + goToRoot(Direction.FORWARD) +} + +public fun LinearNavigator.resetWithRoot(root: Navigable) { + navigate(Direction.FORWARD) { backStack -> + backStack.clear() + val transition = NoAnimationTransition() + backStack.push(NavigationEvent(root, transition)) + transition + } +} + +public fun LinearNavigator.clearUntilRootAndGoTo( + navigable: Navigable, + overrideTransition: MagellanTransition? = null +) { + navigate(Direction.FORWARD) { backStack -> + if (backStack.isEmpty()) { + throw IllegalStateException("Root not found in backStack") + } + while (backStack.size > 1) { + backStack.pop() + } + val transition = overrideTransition ?: getDefaultTransition() + backStack.push(NavigationEvent(navigable, transition)) + transition + } +} + +private fun LinearNavigator.goToRoot(direction: Direction) { + navigate(direction) { backStack -> + if (backStack.isEmpty()) { + throw IllegalStateException("Root not found in backStack") + } + while (backStack.size > 1) { + backStack.pop() + } + backStack.peek()!!.magellanTransition + } +} + +private fun checkContains(backStack: Deque, navigable: Navigable) { + if (!backStack.any { it.navigable == navigable }) { + throw IllegalStateException("Navigable ${navigable::class.java.simpleName} not found in backStack") + } +} diff --git a/magellan-library/src/main/java/com/wealthfront/magellan/navigation/NavigationDelegate.kt b/magellan-library/src/main/java/com/wealthfront/magellan/navigation/NavigationDelegate.kt index 9de5fc34..35e63f1e 100644 --- a/magellan-library/src/main/java/com/wealthfront/magellan/navigation/NavigationDelegate.kt +++ b/magellan-library/src/main/java/com/wealthfront/magellan/navigation/NavigationDelegate.kt @@ -19,6 +19,7 @@ import com.wealthfront.magellan.transitions.NoAnimationTransition import com.wealthfront.magellan.view.whenMeasured import java.util.ArrayDeque import java.util.Deque +import java.util.concurrent.ConcurrentLinkedDeque public open class NavigationDelegate( protected val container: () -> ScreenContainer, @@ -32,15 +33,11 @@ public open class NavigationDelegate( protected var containerView: ScreenContainer? = null protected val navigationPropagator: NavigationPropagator = NavigationPropagator - public val backStack: Deque = ArrayDeque() + public val backStack: Deque = ConcurrentLinkedDeque() protected val Deque.currentNavigable: NavigableCompat? get() { - return if (isNotEmpty()) { - peek()?.navigable - } else { - null - } + return peek()?.navigable } protected val currentNavigable: NavigableCompat? @@ -181,7 +178,7 @@ public open class NavigationDelegate( } } - override fun onBackPressed(): Boolean = currentNavigable?.backPressed() ?: false || goBack() + override fun onBackPressed(): Boolean = goBack() public fun goBack(): Boolean { return if (!atRoot()) { diff --git a/magellan-library/src/main/java/com/wealthfront/magellan/navigation/NavigationTraverser.kt b/magellan-library/src/main/java/com/wealthfront/magellan/navigation/NavigationTraverser.kt index 09bac315..511c8d05 100644 --- a/magellan-library/src/main/java/com/wealthfront/magellan/navigation/NavigationTraverser.kt +++ b/magellan-library/src/main/java/com/wealthfront/magellan/navigation/NavigationTraverser.kt @@ -41,7 +41,7 @@ public class NavigationTraverser(private val root: NavigableCompat) { "$VERTICAL_LINE$INDENT_SPACE" } node.getNavigationSnapshotRecursive( - indent + childIndent, + childIndent, index == children.lastIndex ) }.forEach { stringBuilder.append(it) } diff --git a/magellan-library/src/main/java/com/wealthfront/magellan/transitions/CircularRevealTransition.kt b/magellan-library/src/main/java/com/wealthfront/magellan/transitions/CircularRevealTransition.kt index eb9fcfc1..0d36f674 100644 --- a/magellan-library/src/main/java/com/wealthfront/magellan/transitions/CircularRevealTransition.kt +++ b/magellan-library/src/main/java/com/wealthfront/magellan/transitions/CircularRevealTransition.kt @@ -7,17 +7,23 @@ import android.os.Build import android.view.View import android.view.ViewAnimationUtils import android.view.ViewGroup +import androidx.annotation.IdRes import androidx.interpolator.view.animation.FastOutSlowInInterpolator import com.wealthfront.magellan.Direction import kotlin.math.hypot +@Suppress("FunctionName") +public fun CircularRevealTransition(clickedView: View): MagellanTransition { + return CircularRevealTransition(clickedView.id) +} + /** * A [MagellanTransition] that reveals the next screen circularly outward from the middle of the - * [clickedView]. + * [clickedViewId]. * - * @property clickedView the view on which this circular reveal is centered + * @property clickedViewId the id of the view on which this circular reveal is centered */ -public class CircularRevealTransition(private val clickedView: View) : MagellanTransition { +private class CircularRevealTransition(@IdRes private val clickedViewId: Int) : MagellanTransition { private var animator: Animator? = null @@ -31,36 +37,45 @@ public class CircularRevealTransition(private val clickedView: View) : MagellanT direction: Direction, onAnimationEndCallback: () -> Unit ) { - val clickedViewCenter = getCenterClickedView(from as ViewGroup) - val circularRevealCenterX = clickedViewCenter[0] - val circularRevealCenterY = clickedViewCenter[1] - val finalRadius = hypot(to.width.toDouble(), to.height.toDouble()).toFloat() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - animator = ViewAnimationUtils.createCircularReveal( - to, circularRevealCenterX, - circularRevealCenterY, 0f, finalRadius - ).apply { - addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - animator = null - onAnimationEndCallback() - } - }) - interpolator = FastOutSlowInInterpolator() - } - animator!!.start() - } else { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { onAnimationEndCallback() + return } + + val clickedViewContainer = if (direction == Direction.FORWARD) from else to + val viewToReveal = if (direction == Direction.FORWARD) to else from!! + val clickedViewCenter = getCenterClickedView(clickedViewContainer as ViewGroup) + val circularRevealCenterX = clickedViewCenter[0] + val circularRevealCenterY = clickedViewCenter[1] + val revealedRadius = hypot(viewToReveal.width.toDouble(), viewToReveal.height.toDouble()).toFloat() + val finalRadius = if (direction == Direction.FORWARD) revealedRadius else 0f + val startRadius = if (direction == Direction.FORWARD) 0f else revealedRadius + animator = ViewAnimationUtils.createCircularReveal( + viewToReveal, circularRevealCenterX, + circularRevealCenterY, startRadius, finalRadius + ).apply { + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + animator = null + onAnimationEndCallback() + } + }) + interpolator = FastOutSlowInInterpolator() + }.also { it.start() } } - private fun getCenterClickedView(from: ViewGroup): IntArray { + private fun getCenterClickedView(clickedViewContainer: ViewGroup): IntArray { + val clickedView = clickedViewContainer.findViewById(clickedViewId) val clickedViewRect = Rect() - clickedView.getDrawingRect(clickedViewRect) - from.offsetDescendantRectToMyCoords(clickedView, clickedViewRect) - return intArrayOf( - clickedViewRect.exactCenterX().toInt(), - clickedViewRect.exactCenterY().toInt() - ) + return if (clickedView != null) { + clickedView.getDrawingRect(clickedViewRect) + clickedViewContainer.offsetDescendantRectToMyCoords(clickedView, clickedViewRect) + intArrayOf( + clickedViewRect.exactCenterX().toInt(), + clickedViewRect.exactCenterY().toInt() + ) + } else { + intArrayOf(clickedViewContainer.width / 2, clickedViewContainer.height / 2) + } } } diff --git a/magellan-library/src/main/java/com/wealthfront/magellan/view/DialogComponent.kt b/magellan-library/src/main/java/com/wealthfront/magellan/view/DialogComponent.kt index 3d53a3f2..26a218f1 100644 --- a/magellan-library/src/main/java/com/wealthfront/magellan/view/DialogComponent.kt +++ b/magellan-library/src/main/java/com/wealthfront/magellan/view/DialogComponent.kt @@ -4,6 +4,7 @@ import android.app.Activity import android.app.Dialog import android.content.Context import androidx.annotation.VisibleForTesting +import com.wealthfront.magellan.ContextUtil import com.wealthfront.magellan.DialogCreator import com.wealthfront.magellan.lifecycle.LifecycleAware import javax.inject.Inject @@ -11,6 +12,7 @@ import javax.inject.Inject public class DialogComponent @Inject constructor() : LifecycleAware { private var dialogCreator: DialogCreator? = null + private var resumed = false public var dialog: Dialog? = null private set public var context: Context? = null @@ -20,16 +22,20 @@ public class DialogComponent @Inject constructor() : LifecycleAware { public fun showDialog(dialogCreator: DialogCreator) { this.dialogCreator = dialogCreator - createDialog() + createDialogIfResumed() } public fun showDialog(dialogCreator: (Activity) -> Dialog) { this.dialogCreator = DialogCreator { dialogCreator.invoke(it) } - createDialog() + createDialogIfResumed() + } + + override fun create(context: Context) { } override fun resume(context: Context) { this.context = context + resumed = true if (shouldRestoreDialog) { createDialog() shouldRestoreDialog = false @@ -37,14 +43,30 @@ public class DialogComponent @Inject constructor() : LifecycleAware { } override fun pause(context: Context) { + this.context = null + resumed = false destroyDialog() + } + + override fun destroy(context: Context) { this.context = null } + private fun createDialogIfResumed() { + if (resumed) { + createDialog() + } else { + shouldRestoreDialog = true + } + } + private fun createDialog() { + val dialogCreator = dialogCreator + val context = context if (dialogCreator != null && context != null) { - dialog = dialogCreator!!.createDialog(context as Activity) + val dialog = dialogCreator.createDialog(ContextUtil.findActivity(context)) dialog!!.show() + this.dialog = dialog } } diff --git a/magellan-library/src/test/java/com/wealthfront/magellan/navigation/DefaultLinearNavigatorTest.kt b/magellan-library/src/test/java/com/wealthfront/magellan/navigation/DefaultLinearNavigatorTest.kt index f818edde..39d0ed6f 100644 --- a/magellan-library/src/test/java/com/wealthfront/magellan/navigation/DefaultLinearNavigatorTest.kt +++ b/magellan-library/src/test/java/com/wealthfront/magellan/navigation/DefaultLinearNavigatorTest.kt @@ -32,7 +32,7 @@ import org.robolectric.Shadows.shadowOf import org.robolectric.android.controller.ActivityController @RunWith(RobolectricTestRunner::class) -internal class DefaultLinearNavigatorTest { +class DefaultLinearNavigatorTest { private lateinit var activityController: ActivityController private lateinit var screenContainer: ScreenContainer @@ -260,6 +260,22 @@ internal class DefaultLinearNavigatorTest { assertThat(linearNavigator.backStack.first().navigable).isEqualTo(journey1) } + @Test + fun onBackPressed_oneJourney() { + linearNavigator.transitionToState(Resumed(context)) + linearNavigator.navigate(FORWARD) { + it.push(NavigationEvent(journey1, ShowTransition())) + it.first()!!.magellanTransition + } + + val didNavigate = linearNavigator.backPressed() + + assertThat(didNavigate).isFalse() + assertThat(linearNavigator.backStack.size).isEqualTo(1) + assertThat(linearNavigator.backStack.first().navigable).isEqualTo(journey1) + assertThat(journey1.onBackPressedCount).isEqualTo(1) + } + @Test fun goBack_withoutScreen() { val didNavigate = linearNavigator.goBack() @@ -299,8 +315,16 @@ internal class DefaultLinearNavigatorTest { private open class FakeActivity : AppCompatActivity() -private open class DummyJourney : - Journey( - MagellanDummyLayoutBinding::inflate, - MagellanDummyLayoutBinding::container - ) +private open class DummyJourney : Journey( + MagellanDummyLayoutBinding::inflate, + MagellanDummyLayoutBinding::container +) { + + var onBackPressedCount: Int = 0 + private set + + override fun onBackPressed(): Boolean { + onBackPressedCount += 1 + return super.onBackPressed() + } +} diff --git a/magellan-library/src/test/java/com/wealthfront/magellan/navigation/LazySetNavigatorTest.kt b/magellan-library/src/test/java/com/wealthfront/magellan/navigation/LazySetNavigatorTest.kt index f026a1d2..4fa723b9 100644 --- a/magellan-library/src/test/java/com/wealthfront/magellan/navigation/LazySetNavigatorTest.kt +++ b/magellan-library/src/test/java/com/wealthfront/magellan/navigation/LazySetNavigatorTest.kt @@ -9,6 +9,7 @@ import com.wealthfront.magellan.lifecycle.LifecycleState import com.wealthfront.magellan.lifecycle.transitionToState import com.wealthfront.magellan.transitions.CrossfadeTransition import io.mockk.MockKAnnotations.init +import io.mockk.clearAllMocks import io.mockk.clearMocks import io.mockk.every import io.mockk.impl.annotations.MockK @@ -28,6 +29,8 @@ class LazySetNavigatorTest { private lateinit var navigator: LazySetNavigator private lateinit var step1: DummyStep private lateinit var step2: DummyStep + private lateinit var step3: DummyStep + private lateinit var step4: DummyStep @MockK private lateinit var navigableListener: NavigationListener @Before @@ -38,6 +41,8 @@ class LazySetNavigatorTest { navigator = LazySetNavigator { ScreenContainer(activityController.get()) } step1 = DummyStep() step2 = DummyStep() + step3 = DummyStep() + step4 = DummyStep() NavigationPropagator.addNavigableListener(navigableListener) } @@ -45,6 +50,7 @@ class LazySetNavigatorTest { @After fun tearDown() { NavigationPropagator.removeNavigableListener(navigableListener) + clearAllMocks() } private fun initMocks() { @@ -100,10 +106,190 @@ class LazySetNavigatorTest { assertThat(navigator.containerView!!.getChildAt(0)).isEqualTo(step2.view) assertThat(step1.currentState).isInstanceOf(LifecycleState.Shown::class.java) assertThat(step2.currentState).isInstanceOf(LifecycleState.Resumed::class.java) + assertThat(navigator.existingNavigables).containsExactly(step1, step2) verify { navigableListener.beforeNavigation() } verify { navigableListener.onNavigatedFrom(step1) } verify { navigableListener.onNavigatedTo(step2) } verify { navigableListener.afterNavigation() } } + + @Test + fun replaceTwice() { + navigator.addNavigables(setOf(step1, step2)) + navigator.transitionToState(LifecycleState.Resumed(activityController.get())) + + navigator.replace(step1, CrossfadeTransition()) + step1.view!!.viewTreeObserver.dispatchOnPreDraw() + shadowOf(Looper.getMainLooper()).idle() + assertThat(navigator.containerView!!.childCount).isEqualTo(1) + assertThat(navigator.containerView!!.getChildAt(0)).isEqualTo(step1.view) + assertThat(step1.currentState).isInstanceOf(LifecycleState.Resumed::class.java) + assertThat(step2.currentState).isInstanceOf(LifecycleState.Created::class.java) + + verify { navigableListener.beforeNavigation() } + verify(exactly = 0) { navigableListener.onNavigatedFrom(any()) } + verify { navigableListener.onNavigatedTo(step1) } + verify { navigableListener.afterNavigation() } + clearMocks(navigableListener) + initMocks() + + navigator.replace(step2, CrossfadeTransition()) + navigator.replace(step1, CrossfadeTransition()) + + step1.view!!.viewTreeObserver.dispatchOnPreDraw() + shadowOf(Looper.getMainLooper()).idle() + assertThat(navigator.containerView!!.childCount).isEqualTo(1) + assertThat(navigator.containerView!!.getChildAt(0)).isEqualTo(step1.view) + assertThat(step1.currentState).isInstanceOf(LifecycleState.Resumed::class.java) + assertThat(step2.currentState).isInstanceOf(LifecycleState.Shown::class.java) + assertThat(navigator.existingNavigables).containsExactly(step1, step2) + } + + @Test + fun removeNavigables() { + navigator.addNavigables(setOf(step1, step2, step3, step4)) + navigator.transitionToState(LifecycleState.Resumed(activityController.get())) + + navigator.replace(step1, CrossfadeTransition()) + step1.view!!.viewTreeObserver.dispatchOnPreDraw() + shadowOf(Looper.getMainLooper()).idle() + assertThat(navigator.containerView!!.childCount).isEqualTo(1) + assertThat(navigator.containerView!!.getChildAt(0)).isEqualTo(step1.view) + assertThat(step1.currentState).isInstanceOf(LifecycleState.Resumed::class.java) + assertThat(step2.currentState).isInstanceOf(LifecycleState.Created::class.java) + assertThat(step3.currentState).isInstanceOf(LifecycleState.Created::class.java) + assertThat(step4.currentState).isInstanceOf(LifecycleState.Created::class.java) + assertThat(navigator.existingNavigables).containsExactly(step1, step2, step3, step4) + + verify { navigableListener.beforeNavigation() } + verify(exactly = 0) { navigableListener.onNavigatedFrom(any()) } + verify { navigableListener.onNavigatedTo(step1) } + verify { navigableListener.afterNavigation() } + clearMocks(navigableListener) + initMocks() + + navigator.removeNavigables(setOf(step2, step4)) + + step1.view!!.viewTreeObserver.dispatchOnPreDraw() + shadowOf(Looper.getMainLooper()).idle() + assertThat(navigator.containerView!!.childCount).isEqualTo(1) + assertThat(navigator.containerView!!.getChildAt(0)).isEqualTo(step1.view) + assertThat(step1.currentState).isInstanceOf(LifecycleState.Resumed::class.java) + assertThat(step3.currentState).isInstanceOf(LifecycleState.Created::class.java) + assertThat(navigator.existingNavigables).containsExactly(step1, step3) + + verify(exactly = 0) { navigableListener.onNavigatedTo(any()) } + } + + @Test + fun attemptRemoveNavigables_afterOnDestroy() { + navigator.addNavigables(setOf(step1, step2, step3, step4)) + navigator.transitionToState(LifecycleState.Resumed(activityController.get())) + + navigator.replace(step1, CrossfadeTransition()) + step1.view!!.viewTreeObserver.dispatchOnPreDraw() + shadowOf(Looper.getMainLooper()).idle() + assertThat(navigator.containerView!!.childCount).isEqualTo(1) + assertThat(navigator.containerView!!.getChildAt(0)).isEqualTo(step1.view) + assertThat(step1.currentState).isInstanceOf(LifecycleState.Resumed::class.java) + assertThat(step2.currentState).isInstanceOf(LifecycleState.Created::class.java) + assertThat(step3.currentState).isInstanceOf(LifecycleState.Created::class.java) + assertThat(step4.currentState).isInstanceOf(LifecycleState.Created::class.java) + assertThat(navigator.existingNavigables).containsExactly(step1, step2, step3, step4) + + verify { navigableListener.beforeNavigation() } + verify(exactly = 0) { navigableListener.onNavigatedFrom(any()) } + verify { navigableListener.onNavigatedTo(step1) } + verify { navigableListener.afterNavigation() } + clearMocks(navigableListener) + initMocks() + + navigator.transitionToState(LifecycleState.Destroyed) + navigator.removeNavigables(setOf(step2, step4)) + + assertThat(navigator.existingNavigables).isEmpty() + verify(exactly = 0) { navigableListener.onNavigatedTo(any()) } + } + + @Test + fun updateMultipleNavigables() { + navigator.updateNavigables(setOf(step1, step2)) {} + navigator.transitionToState(LifecycleState.Resumed(activityController.get())) + + navigator.replace(step1, CrossfadeTransition()) + step1.view!!.viewTreeObserver.dispatchOnPreDraw() + shadowOf(Looper.getMainLooper()).idle() + assertThat(navigator.containerView!!.childCount).isEqualTo(1) + assertThat(navigator.containerView!!.getChildAt(0)).isEqualTo(step1.view) + assertThat(step1.currentState).isInstanceOf(LifecycleState.Resumed::class.java) + assertThat(step2.currentState).isInstanceOf(LifecycleState.Created::class.java) + assertThat(navigator.existingNavigables).containsExactly(step1, step2) + + verify { navigableListener.beforeNavigation() } + verify(exactly = 0) { navigableListener.onNavigatedFrom(any()) } + verify { navigableListener.onNavigatedTo(step1) } + verify { navigableListener.afterNavigation() } + clearMocks(navigableListener) + initMocks() + + navigator.updateNavigables(setOf(step1, step3, step4)) { + navigator.replace(step1, CrossfadeTransition()) + } + navigator.replace(step3, CrossfadeTransition()) + + step3.view!!.viewTreeObserver.dispatchOnPreDraw() + shadowOf(Looper.getMainLooper()).idle() + assertThat(navigator.containerView!!.childCount).isEqualTo(1) + assertThat(navigator.containerView!!.getChildAt(0)).isEqualTo(step3.view) + assertThat(step1.currentState).isInstanceOf(LifecycleState.Shown::class.java) + assertThat(step3.currentState).isInstanceOf(LifecycleState.Resumed::class.java) + assertThat(step4.currentState).isInstanceOf(LifecycleState.Created::class.java) + assertThat(navigator.existingNavigables).containsExactly(step1, step3, step4) + + verify { navigableListener.beforeNavigation() } + verify { navigableListener.onNavigatedFrom(step1) } + verify { navigableListener.onNavigatedTo(step3) } + verify { navigableListener.afterNavigation() } + } + + @Test + fun updateMultipleNavigables_andRemoveCurrentNavigable() { + navigator.updateNavigables(setOf(step1, step2)) {} + navigator.transitionToState(LifecycleState.Resumed(activityController.get())) + + navigator.replace(step2, CrossfadeTransition()) + step2.view!!.viewTreeObserver.dispatchOnPreDraw() + shadowOf(Looper.getMainLooper()).idle() + assertThat(navigator.containerView!!.childCount).isEqualTo(1) + assertThat(navigator.containerView!!.getChildAt(0)).isEqualTo(step2.view) + assertThat(step1.currentState).isInstanceOf(LifecycleState.Created::class.java) + assertThat(step2.currentState).isInstanceOf(LifecycleState.Resumed::class.java) + assertThat(navigator.existingNavigables).containsExactly(step1, step2) + + verify { navigableListener.beforeNavigation() } + verify(exactly = 0) { navigableListener.onNavigatedFrom(any()) } + verify { navigableListener.onNavigatedTo(step2) } + verify { navigableListener.afterNavigation() } + clearMocks(navigableListener) + initMocks() + + navigator.updateNavigables(setOf(step1, step3, step4)) { + navigator.replace(step1, CrossfadeTransition()) + } + + step1.view!!.viewTreeObserver.dispatchOnPreDraw() + shadowOf(Looper.getMainLooper()).idle() + assertThat(navigator.containerView!!.childCount).isEqualTo(1) + assertThat(navigator.containerView!!.getChildAt(0)).isEqualTo(step1.view) + assertThat(step1.currentState).isInstanceOf(LifecycleState.Resumed::class.java) + assertThat(step3.currentState).isInstanceOf(LifecycleState.Created::class.java) + assertThat(step4.currentState).isInstanceOf(LifecycleState.Created::class.java) + assertThat(navigator.existingNavigables).containsExactly(step1, step3, step4) + + verify { navigableListener.beforeNavigation() } + verify { navigableListener.onNavigatedFrom(step2) } + verify { navigableListener.onNavigatedTo(step1) } + verify { navigableListener.afterNavigation() } + } } diff --git a/magellan-library/src/test/java/com/wealthfront/magellan/navigation/LinearNavigatorExtensionsTest.kt b/magellan-library/src/test/java/com/wealthfront/magellan/navigation/LinearNavigatorExtensionsTest.kt new file mode 100644 index 00000000..438bed42 --- /dev/null +++ b/magellan-library/src/test/java/com/wealthfront/magellan/navigation/LinearNavigatorExtensionsTest.kt @@ -0,0 +1,210 @@ +package com.wealthfront.magellan.navigation + +import androidx.appcompat.app.AppCompatActivity +import com.google.common.truth.Truth.assertThat +import com.wealthfront.magellan.ScreenContainer +import com.wealthfront.magellan.internal.test.DummyStep +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class LinearNavigatorExtensionsTest { + + val navigable1 = DummyStep() + val navigable2 = DummyStep() + val navigable3 = DummyStep() + + lateinit var screenContainer: ScreenContainer + lateinit var navigator: LinearNavigator + + @Before + fun setUp() { + val context = Robolectric.buildActivity(AppCompatActivity::class.java).get() + screenContainer = ScreenContainer(context) + navigator = DefaultLinearNavigator({ screenContainer }) + } + + @Test + fun goBackTo_navigableInBackStack() { + navigator.goTo(navigable1) + navigator.goTo(navigable2) + navigator.goTo(navigable3) + navigator.goBackTo(navigable2) + assertThat(navigator.backStack.first().navigable).isEqualTo(navigable2) + } + + @Test + fun goBackTo_navigableNotFound() { + var throwable: Throwable? = null + try { + navigator.goTo(navigable1) + navigator.goTo(navigable3) + navigator.goBackTo(navigable2) + } catch (t: Throwable) { + throwable = t + } + + assertThat(throwable).isNotNull() + assertThat(throwable).isInstanceOf(IllegalStateException::class.java) + assertThat((throwable as IllegalStateException).message) + .isEqualTo("Navigable DummyStep not found in backStack") + } + + @Test + fun goBackTo_empty() { + var throwable: Throwable? = null + try { + navigator.goBackTo(navigable2) + } catch (t: Throwable) { + throwable = t + } + + assertThat(throwable).isNotNull() + assertThat(throwable).isInstanceOf(IllegalStateException::class.java) + assertThat((throwable as IllegalStateException).message) + .isEqualTo("Navigable DummyStep not found in backStack") + } + + @Test + fun goBackBefore_navigableInBackStack() { + navigator.goTo(navigable1) + navigator.goTo(navigable2) + navigator.goTo(navigable3) + navigator.goBackBefore(navigable2) + assertThat(navigator.backStack.first().navigable).isEqualTo(navigable1) + } + + @Test + fun goBackBefore_nothingBeforeNavigable() { + var throwable: Throwable? = null + try { + navigator.goTo(navigable1) + navigator.goTo(navigable2) + navigator.goTo(navigable3) + navigator.goBackBefore(navigable1) + } catch (t: Throwable) { + throwable = t + } + assertThat(navigator.backStack).isEmpty() + assertThat((throwable as IllegalStateException).message) + .isEqualTo("No Navigable before DummyStep") + } + + @Test + fun goBackBefore_navigableNotFound() { + var throwable: Throwable? = null + try { + navigator.goTo(navigable1) + navigator.goTo(navigable3) + navigator.goBackBefore(navigable2) + } catch (t: Throwable) { + throwable = t + } + assertThat(navigator.backStack).hasSize(2) + assertThat(throwable).isNotNull() + assertThat((throwable as IllegalStateException).message) + .isEqualTo("Navigable DummyStep not found in backStack") + } + + @Test + fun goBackBefore_empty() { + var throwable: Throwable? = null + try { + navigator.goBackBefore(navigable2) + } catch (t: Throwable) { + throwable = t + } + assertThat(navigator.backStack).isEmpty() + assertThat(throwable).isNotNull() + assertThat((throwable as IllegalStateException).message) + .isEqualTo("Navigable DummyStep not found in backStack") + } + + @Test + fun resetWithRoot() { + navigator.goTo(navigable1) + navigator.goTo(navigable3) + navigator.resetWithRoot(navigable2) + assertThat(navigator.backStack).hasSize(1) + assertThat(navigator.backStack.first().navigable).isEqualTo(navigable2) + } + + @Test + fun goBackToRoot() { + navigator.goTo(navigable1) + navigator.goTo(navigable2) + navigator.goTo(navigable3) + navigator.goBackToRoot() + assertThat(navigator.backStack).hasSize(1) + assertThat(navigator.backStack.first().navigable).isEqualTo(navigable1) + } + + @Test + fun goBackToRoot_empty() { + var throwable: Throwable? = null + try { + navigator.goBackToRoot() + } catch (t: Throwable) { + throwable = t + } + assertThat(navigator.backStack).isEmpty() + assertThat(throwable).isNotNull() + assertThat((throwable as IllegalStateException).message) + .isEqualTo("Root not found in backStack") + } + + @Test + fun goForwardToRoot() { + navigator.goTo(navigable1) + navigator.goTo(navigable2) + navigator.goTo(navigable3) + navigator.goForwardToRoot() + assertThat(navigator.backStack).hasSize(1) + assertThat(navigator.backStack.first().navigable).isEqualTo(navigable1) + } + + @Test + fun goForwardToRoot_empty() { + var throwable: Throwable? = null + try { + navigator.goForwardToRoot() + } catch (t: Throwable) { + throwable = t + } + assertThat(navigator.backStack).isEmpty() + assertThat(throwable).isNotNull() + assertThat((throwable as IllegalStateException).message) + .isEqualTo("Root not found in backStack") + } + + @Test + fun clearUntilRootAndGoTo() { + val navigable4 = DummyStep() + navigator.goTo(navigable1) + navigator.goTo(navigable2) + navigator.goTo(navigable3) + navigator.clearUntilRootAndGoTo(navigable4) + assertThat(navigator.backStack).hasSize(2) + assertThat(navigator.backStack[0].navigable).isEqualTo(navigable4) + assertThat(navigator.backStack[1].navigable).isEqualTo(navigable1) + } + + @Test + fun clearUntilRootAndGoTo_empty() { + val navigable4 = DummyStep() + var throwable: Throwable? = null + try { + navigator.clearUntilRootAndGoTo(navigable4) + } catch (t: Throwable) { + throwable = t + } + + assertThat(navigator.backStack).isEmpty() + assertThat(throwable).isNotNull() + assertThat((throwable as IllegalStateException).message) + .isEqualTo("Root not found in backStack") + } +} diff --git a/magellan-library/src/test/java/com/wealthfront/magellan/navigation/NavigationTraverserTest.kt b/magellan-library/src/test/java/com/wealthfront/magellan/navigation/NavigationTraverserTest.kt index 7bcfafdc..9b255980 100644 --- a/magellan-library/src/test/java/com/wealthfront/magellan/navigation/NavigationTraverserTest.kt +++ b/magellan-library/src/test/java/com/wealthfront/magellan/navigation/NavigationTraverserTest.kt @@ -84,20 +84,34 @@ internal class NavigationTraverserTest { @Test fun globalBackStackWithSiblingJourney() { - traverser = NavigationTraverser(siblingRoot) - - siblingRoot.transitionToState(Created(context)) + val rootJourney = + DummyJourney( + DummyJourney( + DummyJourney( + DummyJourney( + siblingRoot + ) + ) + ) + ) + traverser = NavigationTraverser(rootJourney) + + rootJourney.transitionToState(Created(context)) journey3.goToAnotherJourney() assertThat(traverser.getGlobalBackstackDescription()).isEqualTo( """ - SiblingJourney - ├ DummyJourney3 - | ├ DummyStep3 - | └ DummyStep4 - └ DummyJourney2 - ├ DummyStep1 - └ DummyStep2 + DummyJourney + └ DummyJourney + └ DummyJourney + └ DummyJourney + └ SiblingJourney + ├ DummyJourney3 + | ├ DummyStep3 + | └ DummyStep4 + └ DummyJourney2 + ├ DummyStep1 + └ DummyStep2 """.trimIndent() ) @@ -129,6 +143,16 @@ internal class NavigationTraverserTest { } } + private inner class DummyJourney(val otherJourney: Journey<*>) : Journey( + MagellanDummyLayoutBinding::inflate, + MagellanDummyLayoutBinding::container + ) { + + override fun onCreate(context: Context) { + navigator.goTo(otherJourney) + } + } + private inner class MultiStepJourney : Journey( MagellanDummyLayoutBinding::inflate, MagellanDummyLayoutBinding::container diff --git a/magellan-lint/build.gradle b/magellan-lint/build.gradle index 3d9da822..323924b8 100644 --- a/magellan-lint/build.gradle +++ b/magellan-lint/build.gradle @@ -3,8 +3,8 @@ apply plugin: "kotlin" apply plugin: 'com.android.lint' java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } lintOptions { @@ -16,15 +16,15 @@ lintOptions { } dependencies { - compileOnly Dependencies.lintApi - compileOnly Dependencies.lintChecks + compileOnly libs.lintApi + compileOnly libs.lintChecks - testImplementation Dependencies.junit - testImplementation Dependencies.truth - testImplementation Dependencies.lint - testImplementation Dependencies.lintTests - testImplementation Dependencies.testUtils - testImplementation(Dependencies.truth) { + testImplementation libs.junit + testImplementation libs.truth + testImplementation libs.lint + testImplementation libs.lintTests + testImplementation libs.testUtils + testImplementation(libs.truth) { exclude group: 'com.google.guava', module: 'guava' } } diff --git a/magellan-rx/build.gradle b/magellan-rx/build.gradle index 1a69421c..97d0aac0 100644 --- a/magellan-rx/build.gradle +++ b/magellan-rx/build.gradle @@ -10,15 +10,13 @@ android { defaultConfig { minSdkVersion Versions.minSdkVersion targetSdkVersion Versions.targetSdkVersion - versionCode 1 - versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } compileOptions { - setSourceCompatibility(JavaVersion.VERSION_1_8) - setTargetCompatibility(JavaVersion.VERSION_1_8) + setSourceCompatibility(JavaVersion.VERSION_17) + setTargetCompatibility(JavaVersion.VERSION_17) } buildTypes { @@ -26,6 +24,7 @@ android { minifyEnabled false } } + namespace 'com.wealthfront.magellan.rx' } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { @@ -33,19 +32,19 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { kotlinOptions.freeCompilerArgs = ['-Xexplicit-api=strict'] } kotlinOptions.allWarningsAsErrors = true - kotlinOptions.jvmTarget = "1.8" + kotlinOptions.jvmTarget = JavaVersion.VERSION_17 } dependencies { implementation project(':magellan-legacy') implementation project(':magellan-library') - implementation Dependencies.rxjava - implementation Dependencies.inject + implementation libs.rxjava + implementation libs.inject - testImplementation Dependencies.junit - testImplementation Dependencies.truth - testImplementation Dependencies.mockito + testImplementation libs.junit + testImplementation libs.truth + testImplementation libs.mockito } apply from: rootProject.file('gradle/gradle-mvn-push.gradle') diff --git a/magellan-rx/src/main/AndroidManifest.xml b/magellan-rx/src/main/AndroidManifest.xml index 05f5f910..419acfa5 100644 --- a/magellan-rx/src/main/AndroidManifest.xml +++ b/magellan-rx/src/main/AndroidManifest.xml @@ -1,6 +1,4 @@ - + diff --git a/magellan-rx2/build.gradle b/magellan-rx2/build.gradle index 7beaa403..fde20df8 100644 --- a/magellan-rx2/build.gradle +++ b/magellan-rx2/build.gradle @@ -10,15 +10,13 @@ android { defaultConfig { minSdkVersion Versions.minSdkVersion targetSdkVersion Versions.targetSdkVersion - versionCode 1 - versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } compileOptions { - setSourceCompatibility(JavaVersion.VERSION_1_8) - setTargetCompatibility(JavaVersion.VERSION_1_8) + setSourceCompatibility(JavaVersion.VERSION_17) + setTargetCompatibility(JavaVersion.VERSION_17) } buildTypes { @@ -26,6 +24,14 @@ android { minifyEnabled false } } + + namespace 'com.wealthfront.magellan.rx2' + + publishing { + singleVariant("release") { + withSourcesJar() + } + } } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { @@ -33,19 +39,19 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { kotlinOptions.freeCompilerArgs = ['-Xexplicit-api=strict'] } kotlinOptions.allWarningsAsErrors = true - kotlinOptions.jvmTarget = "1.8" + kotlinOptions.jvmTarget = JavaVersion.VERSION_17 } dependencies { implementation project(':magellan-legacy') implementation project(':magellan-library') - implementation Dependencies.rxjava2 - implementation Dependencies.inject + implementation libs.rxjava2 + implementation libs.inject - testImplementation Dependencies.junit - testImplementation Dependencies.truth - testImplementation Dependencies.mockito + testImplementation libs.junit + testImplementation libs.truth + testImplementation libs.mockito } apply from: rootProject.file('gradle/gradle-mvn-push.gradle') \ No newline at end of file diff --git a/magellan-rx2/src/main/AndroidManifest.xml b/magellan-rx2/src/main/AndroidManifest.xml index 23990c37..419acfa5 100644 --- a/magellan-rx2/src/main/AndroidManifest.xml +++ b/magellan-rx2/src/main/AndroidManifest.xml @@ -1,6 +1,4 @@ - + diff --git a/magellan-sample-advanced/build.gradle b/magellan-sample-advanced/build.gradle index a2ea4db4..6ea5f751 100644 --- a/magellan-sample-advanced/build.gradle +++ b/magellan-sample-advanced/build.gradle @@ -39,13 +39,14 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = JavaVersion.VERSION_17 } + namespace 'com.wealthfront.magellan.sample.advanced' } dependencies { @@ -53,36 +54,36 @@ dependencies { implementation project(':magellan-library') implementation project(':magellan-rx2') - implementation Dependencies.androidXCore - implementation Dependencies.appCompat - implementation Dependencies.material - implementation Dependencies.constraintLayout - - implementation Dependencies.dagger - kapt Dependencies.daggerCompiler - - implementation Dependencies.retrofit - implementation Dependencies.rxjava - implementation Dependencies.rxAndroid2 - implementation Dependencies.retrofitMock - implementation Dependencies.rxJava2Adapter - implementation Dependencies.jackson - implementation Dependencies.okhttp - implementation Dependencies.coroutines - implementation Dependencies.coroutinesAndroid - - testImplementation Dependencies.junit - testImplementation Dependencies.testCore - testImplementation Dependencies.truth - testImplementation Dependencies.mockito - testImplementation Dependencies.robolectric - - androidTestImplementation Dependencies.rx2idler - androidTestImplementation Dependencies.testCore - androidTestImplementation Dependencies.testCoreKtx - androidTestImplementation Dependencies.espressoCore - androidTestImplementation Dependencies.testRunner - androidTestImplementation Dependencies.testRules + implementation libs.androidXCore + implementation libs.appCompat + implementation libs.material + implementation libs.constraintLayout + + implementation libs.dagger + kapt libs.daggerCompiler + + implementation libs.retrofit + implementation libs.rxjava + implementation libs.rxAndroid2 + implementation libs.retrofitMock + implementation libs.rxJava2Adapter + implementation libs.jackson + implementation libs.okhttp + implementation libs.coroutines + implementation libs.coroutinesAndroid + + testImplementation libs.junit + testImplementation libs.testCore + testImplementation libs.truth + testImplementation libs.mockito + testImplementation libs.robolectric + + androidTestImplementation libs.rx2idler + androidTestImplementation libs.testCore + androidTestImplementation libs.testCoreKtx + androidTestImplementation libs.espressoCore + androidTestImplementation libs.testRunner + androidTestImplementation libs.testRules androidTestImplementation 'androidx.test:runner:1.4.0' androidTestUtil 'androidx.test:orchestrator:1.4.1' diff --git a/magellan-sample-advanced/src/main/AndroidManifest.xml b/magellan-sample-advanced/src/main/AndroidManifest.xml index 764b3ad4..64c6ae2d 100644 --- a/magellan-sample-advanced/src/main/AndroidManifest.xml +++ b/magellan-sample-advanced/src/main/AndroidManifest.xml @@ -1,8 +1,6 @@ + xmlns:android="http://schemas.android.com/apk/res/android"> LenientStubber.coWhen(block: suspend CoroutineScope.() -> T): OngoingStubbing = + runBlocking { + this@coWhen.`when`(block()) + } + +fun coWhen(block: suspend CoroutineScope.() -> T): OngoingStubbing = + runBlocking { + `when`(block()) + } + +fun coVerify(mock: T, block: suspend CoroutineScope.(T) -> Unit) { + runBlocking { + block(verify(mock)) + } +} + +fun coVerify(mock: T, mode: VerificationMode, block: suspend CoroutineScope.(T) -> Unit) { + runBlocking { + block(verify(mock, mode)) + } +} diff --git a/magellan-sample-migration/src/androidTest/java/com/wealthfront/magellan/sample/migration/TestSampleApplication.kt b/magellan-sample-migration/src/androidTest/java/com/wealthfront/magellan/sample/migration/TestSampleApplication.kt new file mode 100644 index 00000000..b7cb41c8 --- /dev/null +++ b/magellan-sample-migration/src/androidTest/java/com/wealthfront/magellan/sample/migration/TestSampleApplication.kt @@ -0,0 +1,21 @@ +package com.wealthfront.magellan.sample.migration + +import android.app.Application + +class TestSampleApplication : Application(), AppComponentContainer { + + private lateinit var appComponent: TestAppComponent + + override fun onCreate() { + super.onCreate() + appComponent = DaggerTestAppComponent.builder() + .appModule(AppModule) + .testDogApiModule(TestDogApiModule) + .toolbarHelperModule(ToolbarHelperModule) + .build() + } + + override fun injector(): AppComponent { + return appComponent + } +} diff --git a/magellan-sample-migration/src/androidTestAndroidViews/java/com/wealthfront/magellan/sample/migration/uitest/NavigationTest.kt b/magellan-sample-migration/src/androidTestAndroidViews/java/com/wealthfront/magellan/sample/migration/uitest/NavigationTest.kt new file mode 100644 index 00000000..86b47aac --- /dev/null +++ b/magellan-sample-migration/src/androidTestAndroidViews/java/com/wealthfront/magellan/sample/migration/uitest/NavigationTest.kt @@ -0,0 +1,58 @@ +package com.wealthfront.magellan.sample.migration.uitest + +import android.app.Application +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import com.wealthfront.magellan.sample.migration.AppComponentContainer +import com.wealthfront.magellan.sample.migration.CoroutineIdlingRule +import com.wealthfront.magellan.sample.migration.DisableAnimationsAndKeyboardRule +import com.wealthfront.magellan.sample.migration.MainActivity +import com.wealthfront.magellan.sample.migration.R +import com.wealthfront.magellan.sample.migration.TestAppComponent +import com.wealthfront.magellan.sample.migration.api.DogApi +import com.wealthfront.magellan.sample.migration.api.DogBreedsResponse +import com.wealthfront.magellan.sample.migration.api.DogImageResponse +import com.wealthfront.magellan.sample.migration.coWhen +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +class NavigationTest { + + @Rule @JvmField + val disableAnimationsAndKeyboardRule = DisableAnimationsAndKeyboardRule() + + @Rule @JvmField + val coroutineIdlingRule = CoroutineIdlingRule() + + @Inject lateinit var api: DogApi + + private lateinit var activityScenario: ActivityScenario + + @Before + fun setup() { + val context = ApplicationProvider.getApplicationContext() + ((context as AppComponentContainer).injector() as TestAppComponent).inject(this) + + coWhen { api.getAllBreeds() } + .thenReturn(DogBreedsResponse(message = mapOf("robotic" to emptyList()), status = "success")) + coWhen { api.getRandomImageForBreed("robotic") }.thenReturn( + DogImageResponse(message = "image-url", status = "success") + ) + } + + @Test + fun visitRetriever() { + activityScenario = launchActivity() + onView(withText("robotic")).perform(click()) + onView(withId(R.id.dogDetailsView)).check(matches(isDisplayed())) + } +} diff --git a/magellan-sample-migration/src/androidTestCompose/java/com/wealthfront/magellan/sample/migration/uitest/NavigationTest.kt b/magellan-sample-migration/src/androidTestCompose/java/com/wealthfront/magellan/sample/migration/uitest/NavigationTest.kt new file mode 100644 index 00000000..4536af18 --- /dev/null +++ b/magellan-sample-migration/src/androidTestCompose/java/com/wealthfront/magellan/sample/migration/uitest/NavigationTest.kt @@ -0,0 +1,58 @@ +package com.wealthfront.magellan.sample.migration.uitest + +import android.app.Application +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.junit4.createEmptyComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider +import androidx.test.core.app.launchActivity +import com.wealthfront.magellan.sample.migration.AppComponentContainer +import com.wealthfront.magellan.sample.migration.CoroutineIdlingRule +import com.wealthfront.magellan.sample.migration.DisableAnimationsAndKeyboardRule +import com.wealthfront.magellan.sample.migration.MainActivity +import com.wealthfront.magellan.sample.migration.TestAppComponent +import com.wealthfront.magellan.sample.migration.api.DogApi +import com.wealthfront.magellan.sample.migration.api.DogBreedsResponse +import com.wealthfront.magellan.sample.migration.api.DogImageResponse +import com.wealthfront.magellan.sample.migration.coWhen +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +class NavigationTest { + + @Rule @JvmField + val disableAnimationsAndKeyboardRule = DisableAnimationsAndKeyboardRule() + + @Rule @JvmField + val coroutineIdlingRule = CoroutineIdlingRule() + + @Rule @JvmField + val composeRule: ComposeTestRule = createEmptyComposeRule() + + @Inject lateinit var api: DogApi + + private lateinit var activityScenario: ActivityScenario + + @Before + fun setup() { + val context = ApplicationProvider.getApplicationContext() + ((context as AppComponentContainer).injector() as TestAppComponent).inject(this) + + coWhen { api.getAllBreeds() } + .thenReturn(DogBreedsResponse(message = mapOf("robotic" to emptyList()), status = "success")) + coWhen { api.getRandomImageForBreed("robotic") }.thenReturn( + DogImageResponse(message = "image-url", status = "success") + ) + } + + @Test + fun visitRetriever() { + activityScenario = launchActivity() + + composeRule.onNodeWithText("robotic").performClick() + } +} diff --git a/magellan-sample-migration/src/androidViews/java/com/wealthfront/magellan/sample/migration/tide/DogListAdapter.kt b/magellan-sample-migration/src/androidViews/java/com/wealthfront/magellan/sample/migration/tide/DogListAdapter.kt new file mode 100644 index 00000000..358c891e --- /dev/null +++ b/magellan-sample-migration/src/androidViews/java/com/wealthfront/magellan/sample/migration/tide/DogListAdapter.kt @@ -0,0 +1,35 @@ +package com.wealthfront.magellan.sample.migration.tide + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.wealthfront.magellan.sample.migration.R + +class DogListAdapter( + var dataSet: List = emptyList(), + private val onDogSelected: (String) -> Unit +) : RecyclerView.Adapter() { + + class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + + val textView: TextView = view.findViewById(R.id.dogName) + } + + override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(viewGroup.context) + .inflate(R.layout.dog_item, viewGroup, false) + + return ViewHolder(view) + } + + override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { + viewHolder.textView.text = dataSet[position] + viewHolder.itemView.setOnClickListener { + onDogSelected(dataSet[position]) + } + } + + override fun getItemCount() = dataSet.size +} diff --git a/magellan-sample-migration/src/androidViews/java/com/wealthfront/magellan/sample/migration/tide/DogListStep.kt b/magellan-sample-migration/src/androidViews/java/com/wealthfront/magellan/sample/migration/tide/DogListStep.kt new file mode 100644 index 00000000..b0f83810 --- /dev/null +++ b/magellan-sample-migration/src/androidViews/java/com/wealthfront/magellan/sample/migration/tide/DogListStep.kt @@ -0,0 +1,50 @@ +package com.wealthfront.magellan.sample.migration.tide + +import android.content.Context +import android.view.View +import android.widget.Toast +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager.VERTICAL +import com.wealthfront.magellan.core.Step +import com.wealthfront.magellan.sample.migration.R +import com.wealthfront.magellan.sample.migration.api.DogApi +import com.wealthfront.magellan.sample.migration.databinding.DashboardBinding +import com.wealthfront.magellan.sample.migration.toolbar.ToolbarHelper +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.launch + +@AssistedFactory +fun interface DogListStepFactory { + fun create(goToDogDetails: (name: String) -> Unit): DogListStep +} + +class DogListStep @AssistedInject constructor( + private val toolbarHelper: ToolbarHelper, + private val api: DogApi, + @Assisted private val goToDogDetails: (name: String) -> Unit +) : Step(DashboardBinding::inflate) { + + override fun onShow(context: Context, binding: DashboardBinding) { + toolbarHelper.setTitle(context.getText(R.string.app_name)) + binding.dogItems.layoutManager = LinearLayoutManager(context, VERTICAL, false) + binding.dogItems.adapter = DogListAdapter(emptyList(), goToDogDetails) + val decoration = DividerItemDecoration(context, VERTICAL) + binding.dogItems.addItemDecoration(decoration) + + binding.dogItemsLoading.visibility = View.VISIBLE + shownScope.launch { + val dogBreedsResponse = runCatching { api.getAllBreeds() } + dogBreedsResponse.onSuccess { dogBreeds -> + (binding.dogItems.adapter as DogListAdapter).dataSet = dogBreeds.message.keys.toList() + (binding.dogItems.adapter as DogListAdapter).notifyDataSetChanged() + } + dogBreedsResponse.onFailure { throwable -> + Toast.makeText(context, throwable.message, Toast.LENGTH_SHORT).show() + } + binding.dogItemsLoading.visibility = View.GONE + } + } +} diff --git a/magellan-sample-migration/src/main/res/layout/dashboard.xml b/magellan-sample-migration/src/androidViews/res/layout/dashboard.xml similarity index 50% rename from magellan-sample-migration/src/main/res/layout/dashboard.xml rename to magellan-sample-migration/src/androidViews/res/layout/dashboard.xml index 43f8bb67..c267719c 100644 --- a/magellan-sample-migration/src/main/res/layout/dashboard.xml +++ b/magellan-sample-migration/src/androidViews/res/layout/dashboard.xml @@ -1,16 +1,24 @@ - - + + - \ No newline at end of file + \ No newline at end of file diff --git a/magellan-sample-migration/src/main/res/layout/dog_item.xml b/magellan-sample-migration/src/androidViews/res/layout/dog_item.xml similarity index 100% rename from magellan-sample-migration/src/main/res/layout/dog_item.xml rename to magellan-sample-migration/src/androidViews/res/layout/dog_item.xml diff --git a/magellan-sample-migration/src/compose/java/com/wealthfront/magellan/sample/migration/tide/ComposeStep.kt b/magellan-sample-migration/src/compose/java/com/wealthfront/magellan/sample/migration/tide/ComposeStep.kt new file mode 100644 index 00000000..3b1f1cfc --- /dev/null +++ b/magellan-sample-migration/src/compose/java/com/wealthfront/magellan/sample/migration/tide/ComposeStep.kt @@ -0,0 +1,34 @@ +package com.wealthfront.magellan.sample.migration.tide + +import androidx.annotation.VisibleForTesting +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.runtime.saveable.SaveableStateHolder +import androidx.compose.runtime.saveable.rememberSaveableStateHolder +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import com.wealthfront.magellan.core.Navigable +import com.wealthfront.magellan.lifecycle.LifecycleAwareComponent +import com.wealthfront.magellan.lifecycle.createAndAttachFieldToLifecycleWhenShown +import java.util.UUID + +abstract class ComposeStep : Navigable, LifecycleAwareComponent() { + + private var state: SaveableStateHolder? = null + + final override var view: ComposeView? by createAndAttachFieldToLifecycleWhenShown { ComposeView(it) } + @VisibleForTesting set + + fun setContent(content: @Composable () -> Unit) { + view?.setContent { + if (state == null) { + state = rememberSaveableStateHolder() + } + Box(modifier = Modifier) { + state!!.SaveableStateProvider(UUID.randomUUID()) { + content() + } + } + } + } +} diff --git a/magellan-sample-migration/src/compose/java/com/wealthfront/magellan/sample/migration/tide/DogBreedListItem.kt b/magellan-sample-migration/src/compose/java/com/wealthfront/magellan/sample/migration/tide/DogBreedListItem.kt new file mode 100644 index 00000000..bb7dd1cc --- /dev/null +++ b/magellan-sample-migration/src/compose/java/com/wealthfront/magellan/sample/migration/tide/DogBreedListItem.kt @@ -0,0 +1,38 @@ +package com.wealthfront.magellan.sample.migration.tide + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.Typography +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun DogBreedListItem(breedName: String, goToDogDetails: (name: String) -> Unit) { + Text( + text = breedName, + textAlign = TextAlign.Center, + style = Typography().bodyMedium.copy( + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + color = MaterialTheme.colorScheme.primary + ), + modifier = Modifier + .clickable { + goToDogDetails(breedName) + } + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surface) + .padding( + horizontal = 16.dp, + vertical = 16.dp + ) + ) +} diff --git a/magellan-sample-migration/src/compose/java/com/wealthfront/magellan/sample/migration/tide/DogBreeds.kt b/magellan-sample-migration/src/compose/java/com/wealthfront/magellan/sample/migration/tide/DogBreeds.kt new file mode 100644 index 00000000..6f323a55 --- /dev/null +++ b/magellan-sample-migration/src/compose/java/com/wealthfront/magellan/sample/migration/tide/DogBreeds.kt @@ -0,0 +1,20 @@ +package com.wealthfront.magellan.sample.migration.tide + +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.HorizontalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag + +@Composable +fun DogBreeds(dogBreeds: List, onBreedClick: (name: String) -> Unit) { + LazyColumn(modifier = Modifier.testTag("DogBreeds")) { + itemsIndexed(dogBreeds) { index, item -> + DogBreedListItem(item, onBreedClick) + if (index != (dogBreeds.size - 1)) { + HorizontalDivider() + } + } + } +} diff --git a/magellan-sample-migration/src/compose/java/com/wealthfront/magellan/sample/migration/tide/DogListStep.kt b/magellan-sample-migration/src/compose/java/com/wealthfront/magellan/sample/migration/tide/DogListStep.kt new file mode 100644 index 00000000..7ded5c3c --- /dev/null +++ b/magellan-sample-migration/src/compose/java/com/wealthfront/magellan/sample/migration/tide/DogListStep.kt @@ -0,0 +1,45 @@ +package com.wealthfront.magellan.sample.migration.tide + +import android.content.Context +import android.widget.Toast +import android.widget.Toast.LENGTH_SHORT +import androidx.compose.runtime.mutableStateListOf +import com.wealthfront.magellan.coroutines.ShownLifecycleScope +import com.wealthfront.magellan.lifecycle.attachFieldToLifecycle +import com.wealthfront.magellan.sample.migration.api.DogApi +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.launch + +@AssistedFactory +fun interface DogListStepFactory { + fun create(goToDogDetails: (name: String) -> Unit): DogListStep +} + +class DogListStep @AssistedInject constructor( + private val api: DogApi, + @Assisted private val goToDogDetails: (name: String) -> Unit +) : ComposeStep() { + + private val scope by attachFieldToLifecycle(ShownLifecycleScope()) + private val dogBreedsData = mutableStateListOf() + + override fun onShow(context: Context) { + setContent { + DogBreeds(dogBreeds = dogBreedsData, onBreedClick = goToDogDetails) + } + + scope.launch { + // show loading + val breeds = runCatching { api.getAllBreeds() } + breeds.onSuccess { response -> + dogBreedsData.clear() + dogBreedsData.addAll(response.message.keys) + }.onFailure { + Toast.makeText(context, it.message, LENGTH_SHORT).show() + } + // hide loading + } + } +} diff --git a/magellan-sample-migration/src/main/AndroidManifest.xml b/magellan-sample-migration/src/main/AndroidManifest.xml index feb755b6..0dba00ee 100644 --- a/magellan-sample-migration/src/main/AndroidManifest.xml +++ b/magellan-sample-migration/src/main/AndroidManifest.xml @@ -1,7 +1,6 @@ diff --git a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/AppComponent.java b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/AppComponent.java deleted file mode 100644 index 6c9ca312..00000000 --- a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/AppComponent.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.wealthfront.magellan.sample.migration; - -import com.wealthfront.magellan.sample.migration.tide.DogBreedsStep; -import com.wealthfront.magellan.sample.migration.tide.DogDetailsScreen; -import com.wealthfront.magellan.sample.migration.tide.HelpScreen; - -import javax.inject.Singleton; - -import dagger.Component; - -@Component(modules = AppModule.class) -@Singleton -public interface AppComponent { - - void inject(MainActivity activity); - - void inject(DogDetailsScreen screen); - - void inject(DogBreedsStep step); - - void inject(HelpScreen screen); - - void inject(Expedition expedition); -} diff --git a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/AppComponent.kt b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/AppComponent.kt new file mode 100644 index 00000000..09a91461 --- /dev/null +++ b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/AppComponent.kt @@ -0,0 +1,12 @@ +package com.wealthfront.magellan.sample.migration + +import dagger.Component +import javax.inject.Singleton + +@Component(modules = [AppModule::class, DogApiModule::class, ToolbarHelperModule::class]) +@Singleton +interface AppComponent { + + fun inject(activity: MainActivity) + fun inject(expedition: Expedition) +} diff --git a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/AppModule.java b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/AppModule.java deleted file mode 100644 index 3d83b508..00000000 --- a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/AppModule.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.wealthfront.magellan.sample.migration; - -import com.wealthfront.magellan.navigation.NavigationTraverser; -import com.wealthfront.magellan.sample.migration.api.DogApi; - -import javax.inject.Singleton; - -import dagger.Module; -import dagger.Provides; -import okhttp3.OkHttpClient; -import okhttp3.logging.HttpLoggingInterceptor; -import retrofit2.Retrofit; -import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory; -import retrofit2.converter.jackson.JacksonConverterFactory; -import rx.schedulers.Schedulers; - -@Module -final class AppModule { - - private static final String DOG_BASE_URL = "https://dog.ceo/api/"; - - @Provides - @Singleton - Expedition provideExpedition() { - return new Expedition(); - } - - @Provides - @Singleton - NavigationTraverser provideNavigationTraverser(Expedition root) { - return new NavigationTraverser(root); - } - - @Provides - @Singleton - DogApi provideDogApi(Retrofit retrofit) { - return retrofit.create(DogApi.class); - } - - @Provides - @Singleton - Retrofit provideRetrofit(OkHttpClient httpClient) { - return new Retrofit.Builder() - .baseUrl(DOG_BASE_URL) - .addCallAdapterFactory(RxJavaCallAdapterFactory.createWithScheduler(Schedulers.io())) - .addConverterFactory(JacksonConverterFactory.create()) - .client(httpClient) - .build(); - } - - @Provides - @Singleton - OkHttpClient provideHttpClient() { - HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(); - interceptor.setLevel(HttpLoggingInterceptor.Level.BODY); - return new OkHttpClient.Builder().addInterceptor(interceptor).build(); - } - -} diff --git a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/AppModule.kt b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/AppModule.kt new file mode 100644 index 00000000..28dd4fff --- /dev/null +++ b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/AppModule.kt @@ -0,0 +1,50 @@ +package com.wealthfront.magellan.sample.migration + +import com.wealthfront.magellan.navigation.NavigationTraverser +import com.wealthfront.magellan.sample.migration.api.RequestCountingInterceptor +import dagger.Module +import dagger.Provides +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.jackson.JacksonConverterFactory +import javax.inject.Singleton + +private const val DOG_BASE_URL = "https://dog.ceo/api/" + +@Module +object AppModule { + + @Provides + @Singleton + fun provideExpedition(): Expedition { + return Expedition() + } + + @Provides + @Singleton + fun provideNavigationTraverser(root: Expedition): NavigationTraverser { + return NavigationTraverser(root) + } + + @Provides + @Singleton + fun provideRetrofit(httpClient: OkHttpClient): Retrofit { + return Retrofit.Builder() + .baseUrl(DOG_BASE_URL) + .addConverterFactory(JacksonConverterFactory.create()) + .client(httpClient) + .build() + } + + @Provides + @Singleton + fun provideHttpClient(): OkHttpClient { + val interceptor = HttpLoggingInterceptor() + interceptor.setLevel(HttpLoggingInterceptor.Level.BODY) + return OkHttpClient.Builder() + .addInterceptor(interceptor) + .addInterceptor(RequestCountingInterceptor()) + .build() + } +} diff --git a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/DogApiModule.kt b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/DogApiModule.kt new file mode 100644 index 00000000..b45a534d --- /dev/null +++ b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/DogApiModule.kt @@ -0,0 +1,17 @@ +package com.wealthfront.magellan.sample.migration + +import com.wealthfront.magellan.sample.migration.api.DogApi +import dagger.Module +import dagger.Provides +import retrofit2.Retrofit +import javax.inject.Singleton + +@Module +object DogApiModule { + + @Provides + @Singleton + fun provideDogApi(retrofit: Retrofit): DogApi { + return retrofit.create(DogApi::class.java) + } +} diff --git a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/Expedition.kt b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/Expedition.kt index 5111f812..3aacc162 100644 --- a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/Expedition.kt +++ b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/Expedition.kt @@ -7,8 +7,8 @@ import com.wealthfront.magellan.lifecycle.attachLateinitFieldToLifecycle import com.wealthfront.magellan.navigation.LoggingNavigableListener import com.wealthfront.magellan.sample.migration.SampleApplication.Companion.app import com.wealthfront.magellan.sample.migration.databinding.ExpeditionBinding -import com.wealthfront.magellan.sample.migration.tide.DogDetailsScreen -import com.wealthfront.magellan.sample.migration.tide.DogListStep +import com.wealthfront.magellan.sample.migration.tide.DogDetailsScreenFactory +import com.wealthfront.magellan.sample.migration.tide.DogListStepFactory import com.wealthfront.magellan.sample.migration.toolbar.ToolbarHelper import javax.inject.Inject import javax.inject.Singleton @@ -20,23 +20,26 @@ class Expedition @Inject constructor() : LegacyJourney( ) { @set:Inject var navListener: LoggingNavigableListener by attachLateinitFieldToLifecycle() + @Inject lateinit var toolbarHelper: ToolbarHelper + @Inject lateinit var dogDetailsScreenFactory: DogDetailsScreenFactory + @Inject lateinit var dogListStepFactory: DogListStepFactory private val lifecycleMetricsListener by attachFieldToLifecycle(LifecycleMetricsListener()) override fun onCreate(context: Context) { app(context).injector().inject(this) - attachToLifecycle(ToolbarHelper) + attachToLifecycle(toolbarHelper) } override fun onShow(context: Context, binding: ExpeditionBinding) { - ToolbarHelper.init(viewBinding!!.menu, navigator) - navigator.showNow(DogListStep(::goToDetailsScreen)) + toolbarHelper.init(viewBinding!!.menu, navigator) + navigator.showNow(dogListStepFactory.create(::goToDetailsScreen)) } override fun onDestroy(context: Context) { - removeFromLifecycle(ToolbarHelper) + removeFromLifecycle(toolbarHelper) } private fun goToDetailsScreen(name: String) { - navigator.show(DogDetailsScreen(name)) + navigator.show(dogDetailsScreenFactory.create(name)) } } diff --git a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/SampleApplication.kt b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/SampleApplication.kt index 5a7329e9..ea532f00 100644 --- a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/SampleApplication.kt +++ b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/SampleApplication.kt @@ -10,7 +10,9 @@ class SampleApplication : Application(), AppComponentContainer { override fun onCreate() { super.onCreate() appComponent = DaggerAppComponent.builder() - .appModule(AppModule()) + .appModule(AppModule) + .dogApiModule(DogApiModule) + .toolbarHelperModule(ToolbarHelperModule) .build() } @@ -20,8 +22,8 @@ class SampleApplication : Application(), AppComponentContainer { companion object { @JvmStatic - fun app(context: Context): SampleApplication { - return context.applicationContext as SampleApplication + fun app(context: Context): AppComponentContainer { + return context.applicationContext as AppComponentContainer } } } diff --git a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/ToolbarHelperModule.kt b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/ToolbarHelperModule.kt new file mode 100644 index 00000000..c47530db --- /dev/null +++ b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/ToolbarHelperModule.kt @@ -0,0 +1,16 @@ +package com.wealthfront.magellan.sample.migration + +import com.wealthfront.magellan.sample.migration.toolbar.ToolbarHelper +import dagger.Module +import dagger.Provides +import javax.inject.Singleton + +@Module +object ToolbarHelperModule { + + @Provides + @Singleton + fun provideToolbarHelper(): ToolbarHelper { + return ToolbarHelper() + } +} diff --git a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/api/DogApi.kt b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/api/DogApi.kt index db21b3b0..d2992eaf 100644 --- a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/api/DogApi.kt +++ b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/api/DogApi.kt @@ -3,18 +3,29 @@ package com.wealthfront.magellan.sample.migration.api import com.fasterxml.jackson.annotation.JsonProperty import retrofit2.http.GET import retrofit2.http.Path -import rx.Observable interface DogApi { - @GET("breed/retriever/list") - suspend fun getListOfAllBreedsOfRetriever(): DogBreeds + @GET("breed/{id}/list") + suspend fun getAllSubBreeds(@Path("id") breed: String): DogSubBreedsResponse + + @GET("breeds/list/all") + suspend fun getAllBreeds(): DogBreedsResponse + + @GET("breed/{id}}/images") + suspend fun getAllImagesForBreed(@Path("id") breed: String): DogImagesResposne @GET("breed/{id}/images/random") - fun getRandomImageForBreed(@Path("id") breed: String): Observable + suspend fun getRandomImageForBreed(@Path("id") breed: String): DogImageResponse + + @GET("breed/{breed}/{subBreed}/images/random") + suspend fun getRandomImageForSubBreed( + @Path("breed") breed: String, + @Path("subBreed") subBreed: String + ): DogImageResponse } -data class DogMessage( +data class DogImageResponse( @JsonProperty("message") val message: String, @@ -22,10 +33,26 @@ data class DogMessage( val status: String ) -data class DogBreeds( +data class DogSubBreedsResponse( + @JsonProperty("message") + val message: List, + + @JsonProperty("status") + val status: String +) + +data class DogImagesResposne( @JsonProperty("message") val message: List, @JsonProperty("status") val status: String ) + +data class DogBreedsResponse( + @JsonProperty("message") + val message: Map>, + + @JsonProperty("status") + val status: String +) diff --git a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/api/RequestCountingInterceptor.kt b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/api/RequestCountingInterceptor.kt new file mode 100644 index 00000000..3f369c3b --- /dev/null +++ b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/api/RequestCountingInterceptor.kt @@ -0,0 +1,25 @@ +package com.wealthfront.magellan.sample.migration.api + +import okhttp3.Interceptor +import okhttp3.Response + +class RequestCountingInterceptor : Interceptor { + + companion object { + + var listener: CallEventListener? = null + } + + override fun intercept(chain: Interceptor.Chain): Response { + listener?.onRequestStart() + val response = chain.proceed(chain.request()) + listener?.onRequestEnd() + return response + } +} + +interface CallEventListener { + + fun onRequestStart() + fun onRequestEnd() +} diff --git a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/tide/DogBreedsStep.kt b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/tide/DogBreedsStep.kt deleted file mode 100644 index 17f3e179..00000000 --- a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/tide/DogBreedsStep.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.wealthfront.magellan.sample.migration.tide - -import android.content.Context -import android.view.View -import android.view.ViewGroup -import android.widget.ArrayAdapter -import android.widget.TextView -import android.widget.Toast -import android.widget.Toast.LENGTH_SHORT -import com.wealthfront.magellan.core.Step -import com.wealthfront.magellan.coroutines.ShownLifecycleScope -import com.wealthfront.magellan.lifecycle.attachFieldToLifecycle -import com.wealthfront.magellan.sample.migration.R -import com.wealthfront.magellan.sample.migration.SampleApplication.Companion.app -import com.wealthfront.magellan.sample.migration.api.DogApi -import com.wealthfront.magellan.sample.migration.databinding.DogBreedBinding -import kotlinx.coroutines.launch -import javax.inject.Inject - -class DogBreedsStep : Step(DogBreedBinding::inflate) { - - @Inject lateinit var api: DogApi - - private val scope by attachFieldToLifecycle(ShownLifecycleScope()) - - override fun onCreate(context: Context) { - app(context).injector().inject(this) - } - - override fun onShow(context: Context, binding: DogBreedBinding) { - scope.launch { - // show loading - val breeds = runCatching { api.getListOfAllBreedsOfRetriever() } - breeds.onSuccess { - binding.dogBreeds.adapter = DogBreedListAdapter(context, it.message) - }.onFailure { - Toast.makeText(context, it.message, LENGTH_SHORT).show() - } - // hide loading - } - } - - private inner class DogBreedListAdapter(context: Context, breeds: List) : - ArrayAdapter(context, R.layout.dog_item, R.id.dogName, breeds) { - - override fun getView( - position: Int, - convertView: View?, - parent: ViewGroup - ): View { - var view = convertView - if (convertView == null) { - view = View.inflate(context, R.layout.dog_item, null) - } - val dogDetail = getItem(position)!! - val dogDetailTextView = view!!.findViewById(R.id.dogName) - dogDetailTextView.text = dogDetail - return view - } - } -} diff --git a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/tide/DogDetailsScreen.kt b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/tide/DogDetailsScreen.kt index a1dfe3f4..ea3bb8d2 100644 --- a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/tide/DogDetailsScreen.kt +++ b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/tide/DogDetailsScreen.kt @@ -1,45 +1,48 @@ package com.wealthfront.magellan.sample.migration.tide import android.content.Context -import android.view.View import android.widget.Toast +import com.wealthfront.magellan.OpenForMocking import com.wealthfront.magellan.Screen -import com.wealthfront.magellan.lifecycle.attachFieldToLifecycle -import com.wealthfront.magellan.rx.RxUnsubscriber import com.wealthfront.magellan.sample.migration.R -import com.wealthfront.magellan.sample.migration.SampleApplication.Companion.app import com.wealthfront.magellan.sample.migration.api.DogApi import com.wealthfront.magellan.sample.migration.toolbar.ToolbarHelper -import com.wealthfront.magellan.transitions.CircularRevealTransition -import rx.android.schedulers.AndroidSchedulers.mainThread -import javax.inject.Inject +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.launch -class DogDetailsScreen(private val breed: String) : Screen() { +@AssistedFactory +fun interface DogDetailsScreenFactory { + fun create(breed: String): DogDetailsScreen +} - @Inject lateinit var api: DogApi - private val rxUnsubscriber by attachFieldToLifecycle(RxUnsubscriber()) +@OpenForMocking +class DogDetailsScreen @AssistedInject constructor( + private val api: DogApi, + private val toolbarHelper: ToolbarHelper, + @Assisted private val breed: String +) : Screen() { override fun createView(context: Context): DogDetailsView { - app(context).injector().inject(this) return DogDetailsView(context) } override fun onShow(context: Context) { - ToolbarHelper.setTitle("Dog Breed Info") - ToolbarHelper.setMenuIcon(R.drawable.clock_white) { + toolbarHelper.setTitle("Dog Breed Info") + toolbarHelper.setMenuIcon(R.drawable.clock_white) { Toast.makeText(activity, "Menu - Notifications clicked", Toast.LENGTH_SHORT).show() } - ToolbarHelper.setMenuColor(R.color.water) - rxUnsubscriber.autoUnsubscribe( - api.getRandomImageForBreed(breed) - .observeOn(mainThread()) - .subscribe { - view!!.setDogPic(it.message) - } - ) - } + toolbarHelper.setMenuColor(R.color.water) - fun goToHelpScreen(originView: View) { - navigator.goTo(HelpJourney(), CircularRevealTransition(originView)) + shownScope.launch { + val imageResponse = runCatching { api.getRandomImageForBreed(breed) } + imageResponse.onSuccess { image -> + view!!.setDogPic(image.message) + } + imageResponse.onFailure { throwable -> + Toast.makeText(context, throwable.message, Toast.LENGTH_SHORT).show() + } + } } } diff --git a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/tide/DogDetailsView.kt b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/tide/DogDetailsView.kt index 696bf4dc..4901243c 100644 --- a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/tide/DogDetailsView.kt +++ b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/tide/DogDetailsView.kt @@ -1,29 +1,25 @@ package com.wealthfront.magellan.sample.migration.tide import android.content.Context -import android.widget.ImageView -import butterknife.BindView -import butterknife.ButterKnife.bind +import android.view.LayoutInflater +import androidx.annotation.VisibleForTesting import com.bumptech.glide.Glide import com.wealthfront.magellan.BaseScreenView -import com.wealthfront.magellan.sample.migration.R +import com.wealthfront.magellan.OpenForMocking +import com.wealthfront.magellan.sample.migration.databinding.DogDetailsBinding +@OpenForMocking class DogDetailsView(context: Context) : BaseScreenView(context) { - @BindView(R.id.dogImage) lateinit var dogImage: ImageView + @VisibleForTesting + var glideBuilder = Glide.with(this) - init { - inflate(R.layout.dog_details) - bind(this) - dogImage.setOnLongClickListener { image -> - screen.goToHelpScreen(image) - return@setOnLongClickListener true - } - } + @VisibleForTesting + val viewBinding = DogDetailsBinding.inflate(LayoutInflater.from(context), this, true) fun setDogPic(dogUrl: String) { - Glide.with(this) + glideBuilder .load(dogUrl) - .into(dogImage) + .into(viewBinding.dogImage) } } diff --git a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/tide/DogListStep.kt b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/tide/DogListStep.kt deleted file mode 100644 index 212ec338..00000000 --- a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/tide/DogListStep.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.wealthfront.magellan.sample.migration.tide - -import android.content.Context -import android.view.View -import android.view.ViewGroup -import android.widget.ArrayAdapter -import android.widget.TextView -import com.wealthfront.magellan.core.Step -import com.wealthfront.magellan.sample.migration.R -import com.wealthfront.magellan.sample.migration.databinding.DashboardBinding -import com.wealthfront.magellan.sample.migration.toolbar.ToolbarHelper -import java.util.Locale - -class DogListStep(private val goToDogDetails: (name: String) -> Unit) : Step(DashboardBinding::inflate) { - - override fun onShow(context: Context, binding: DashboardBinding) { - ToolbarHelper.setTitle(context.getText(R.string.app_name)) - binding.dogItems.adapter = DogListAdapter(context) - } - - fun onDogSelected(name: String) { - goToDogDetails(name) - } - - enum class DogBreed { - AKITA, - BEAGLE, - CHOW, - MIX, - LABRADOR, - SHIBA, - HUSKY, - SHIHTZU; - - fun getName(): String { - return name.replace("_", " ").toLowerCase(Locale.getDefault()).capitalize() - } - - fun getBreedName(): String { - return name.replace("_", " ").toLowerCase(Locale.getDefault()) - } - } - - private inner class DogListAdapter(context: Context) : ArrayAdapter(context, R.layout.dog_item, R.id.dogName, DogBreed.values()) { - - override fun getView( - position: Int, - convertView: View?, - parent: ViewGroup - ): View { - var view = convertView - if (convertView == null) { - view = View.inflate(context, R.layout.dog_item, null) - } - val dogDetail = getItem(position)!! - val dogDetailTextView = view!!.findViewById(R.id.dogName) - dogDetailTextView.text = dogDetail.getName() - view.setOnClickListener { - onDogSelected(dogDetail.getBreedName()) - } - return view - } - } -} diff --git a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/tide/HelpJourney.kt b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/tide/HelpJourney.kt deleted file mode 100644 index 6152b499..00000000 --- a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/tide/HelpJourney.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.wealthfront.magellan.sample.migration.tide - -import android.content.Context -import com.wealthfront.magellan.core.SimpleJourney - -class HelpJourney : SimpleJourney() { - - override fun onCreate(context: Context) { - navigator.goTo(HelpScreen { navigator.goTo(DogBreedsStep()) }) - } -} diff --git a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/tide/HelpScreen.kt b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/tide/HelpScreen.kt deleted file mode 100644 index 9cb8ba4e..00000000 --- a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/tide/HelpScreen.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.wealthfront.magellan.sample.migration.tide - -import android.app.AlertDialog -import android.content.Context -import android.content.DialogInterface -import com.wealthfront.magellan.LegacyStep -import com.wealthfront.magellan.OpenForMocking -import com.wealthfront.magellan.lifecycle.attachFieldToLifecycle -import com.wealthfront.magellan.rx.RxUnsubscriber -import com.wealthfront.magellan.sample.migration.AppComponentContainer -import com.wealthfront.magellan.sample.migration.api.DogApi -import rx.android.schedulers.AndroidSchedulers -import javax.inject.Inject - -@OpenForMocking -class HelpScreen(private val goToBreedsStep: () -> Unit) : LegacyStep() { - - @Inject lateinit var api: DogApi - private val rxUnsubscriber by attachFieldToLifecycle(RxUnsubscriber()) - - override fun onCreate(context: Context) { - (context.applicationContext as AppComponentContainer).injector().inject(this) - } - - override fun createView(context: Context): HelpView { - return HelpView(context, this) - } - - override fun onShow(context: Context) { - rxUnsubscriber.autoUnsubscribe( - api.getRandomImageForBreed("husky") - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - view!!.setDogPic(it.message) - } - ) - } - - fun showHelpDialog() { - dialogComponent.showDialog { context -> - AlertDialog.Builder(context) - .setTitle("Hello") - .setMessage("Did you find the dog you were looking for?") - .setPositiveButton("Find all breeds of retriever") { _: DialogInterface, _: Int -> - goToBreedsStep() - } - .create() - } - } -} diff --git a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/tide/HelpView.kt b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/tide/HelpView.kt deleted file mode 100644 index 383d7b9c..00000000 --- a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/tide/HelpView.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.wealthfront.magellan.sample.migration.tide - -import android.annotation.SuppressLint -import android.content.Context -import android.view.LayoutInflater -import android.widget.FrameLayout -import androidx.annotation.VisibleForTesting -import com.bumptech.glide.Glide -import com.wealthfront.magellan.OpenForMocking -import com.wealthfront.magellan.sample.migration.databinding.HelpBinding - -@SuppressLint("ViewConstructor") -@OpenForMocking -class HelpView(context: Context, private val screen: HelpScreen) : FrameLayout(context) { - - @VisibleForTesting - val binding = HelpBinding.inflate(LayoutInflater.from(context), this, true) - @VisibleForTesting - var glideBuilder = Glide.with(this) - - init { - binding.dialog.setOnClickListener { - screen.showHelpDialog() - } - } - - fun setDogPic(dogUrl: String) { - glideBuilder - .load(dogUrl) - .into(binding.dogImage) - } -} diff --git a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/toolbar/ToolbarHelper.kt b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/toolbar/ToolbarHelper.kt index d82d38a4..4c1da9e9 100644 --- a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/toolbar/ToolbarHelper.kt +++ b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/toolbar/ToolbarHelper.kt @@ -2,6 +2,7 @@ package com.wealthfront.magellan.sample.migration.toolbar import android.content.Context import com.wealthfront.magellan.Navigator +import com.wealthfront.magellan.OpenForMocking import com.wealthfront.magellan.lifecycle.LifecycleAwareComponent import com.wealthfront.magellan.navigation.NavigationListener import com.wealthfront.magellan.navigation.NavigationPropagator @@ -12,7 +13,8 @@ import com.wealthfront.magellan.navigation.NavigationPropagator * Ideally, this dependency is provider by dependency injection and configured so that the views are cleaned up when the activity * is destroyed with the help of (subcomponents & custom scopes)[https://dagger.dev/dev-guide/subcomponents]. */ -object ToolbarHelper : LifecycleAwareComponent(), NavigationListener { +@OpenForMocking +class ToolbarHelper : LifecycleAwareComponent(), NavigationListener { private var toolbarView: ToolbarView? = null diff --git a/magellan-sample-migration/src/main/res/layout/dog_breed.xml b/magellan-sample-migration/src/main/res/layout/dog_breed.xml index 6fc82e59..c41fee16 100644 --- a/magellan-sample-migration/src/main/res/layout/dog_breed.xml +++ b/magellan-sample-migration/src/main/res/layout/dog_breed.xml @@ -1,12 +1,18 @@ - + + - \ No newline at end of file + \ No newline at end of file diff --git a/magellan-sample-migration/src/main/res/layout/dog_details.xml b/magellan-sample-migration/src/main/res/layout/dog_details.xml index 56420ccc..7fd32563 100644 --- a/magellan-sample-migration/src/main/res/layout/dog_details.xml +++ b/magellan-sample-migration/src/main/res/layout/dog_details.xml @@ -2,11 +2,12 @@ (relaxed = true) + } +} diff --git a/magellan-sample-migration/src/test/java/com/wealthfront/magellan/sample/migration/TestSampleApplication.kt b/magellan-sample-migration/src/test/java/com/wealthfront/magellan/sample/migration/TestSampleApplication.kt index 76811834..b02378a9 100644 --- a/magellan-sample-migration/src/test/java/com/wealthfront/magellan/sample/migration/TestSampleApplication.kt +++ b/magellan-sample-migration/src/test/java/com/wealthfront/magellan/sample/migration/TestSampleApplication.kt @@ -9,7 +9,9 @@ class TestSampleApplication : Application(), AppComponentContainer { override fun onCreate() { super.onCreate() appComponent = DaggerTestAppComponent.builder() - .testAppModule(TestAppModule()) + .appModule(AppModule) + .testDogApiModule(TestDogApiModule) + .testToolbarHelperModule(TestToolbarHelperModule) .build() } diff --git a/magellan-sample-migration/src/test/java/com/wealthfront/magellan/sample/migration/TestToolbarHelperModule.kt b/magellan-sample-migration/src/test/java/com/wealthfront/magellan/sample/migration/TestToolbarHelperModule.kt new file mode 100644 index 00000000..744ecd17 --- /dev/null +++ b/magellan-sample-migration/src/test/java/com/wealthfront/magellan/sample/migration/TestToolbarHelperModule.kt @@ -0,0 +1,17 @@ +package com.wealthfront.magellan.sample.migration + +import com.wealthfront.magellan.sample.migration.toolbar.ToolbarHelper +import dagger.Module +import dagger.Provides +import io.mockk.mockk +import javax.inject.Singleton + +@Module +object TestToolbarHelperModule { + + @Provides + @Singleton + fun provideToolbarHelper(): ToolbarHelper { + return mockk(relaxed = true) + } +} diff --git a/magellan-sample-migration/src/test/java/com/wealthfront/magellan/sample/migration/tide/DogDetailsScreenTest.kt b/magellan-sample-migration/src/test/java/com/wealthfront/magellan/sample/migration/tide/DogDetailsScreenTest.kt new file mode 100644 index 00000000..5d1e2b2d --- /dev/null +++ b/magellan-sample-migration/src/test/java/com/wealthfront/magellan/sample/migration/tide/DogDetailsScreenTest.kt @@ -0,0 +1,60 @@ +package com.wealthfront.magellan.sample.migration.tide + +import android.app.Application +import android.content.Context +import android.os.Looper.getMainLooper +import androidx.activity.ComponentActivity +import androidx.test.core.app.ApplicationProvider +import com.wealthfront.magellan.lifecycle.LifecycleState +import com.wealthfront.magellan.lifecycle.transitionToState +import com.wealthfront.magellan.sample.migration.AppComponentContainer +import com.wealthfront.magellan.sample.migration.TestAppComponent +import com.wealthfront.magellan.sample.migration.api.DogImageResponse +import io.mockk.coEvery +import io.mockk.mockk +import io.mockk.verify +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric.buildActivity +import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows.shadowOf + +@RunWith(RobolectricTestRunner::class) +class DogDetailsScreenTest { + + private lateinit var screen: DogDetailsScreen + private val activity = buildActivity(ComponentActivity::class.java).get() + private val breedData = DogImageResponse( + message = "image-url", + status = "success" + ) + + private val dogDetailsView = mockk(relaxed = true) + + @Before + fun setup() { + val context = ApplicationProvider.getApplicationContext() + val component = ((context as AppComponentContainer).injector() as TestAppComponent) + + screen = object : DogDetailsScreen( + component.api, + component.toolbarHelper, + "robotic" + ) { + override fun createView(context: Context): DogDetailsView { + super.createView(context) + return dogDetailsView + } + } + + coEvery { component.api.getRandomImageForBreed("robotic") } returns breedData + } + + @Test + fun fetchesDogBreedOnShow() { + screen.transitionToState(LifecycleState.Shown(activity)) + shadowOf(getMainLooper()).idle() + verify { dogDetailsView.setDogPic("image-url") } + } +} diff --git a/magellan-sample-migration/src/test/java/com/wealthfront/magellan/sample/migration/tide/DogDetailsViewTest.kt b/magellan-sample-migration/src/test/java/com/wealthfront/magellan/sample/migration/tide/DogDetailsViewTest.kt new file mode 100644 index 00000000..3dd5958f --- /dev/null +++ b/magellan-sample-migration/src/test/java/com/wealthfront/magellan/sample/migration/tide/DogDetailsViewTest.kt @@ -0,0 +1,43 @@ +package com.wealthfront.magellan.sample.migration.tide + +import android.content.Context +import android.graphics.drawable.Drawable +import android.os.Looper.getMainLooper +import androidx.test.core.app.ApplicationProvider +import com.bumptech.glide.RequestBuilder +import com.bumptech.glide.RequestManager +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows.shadowOf + +@RunWith(RobolectricTestRunner::class) +class DogDetailsViewTest { + + private val glideRequest = mockk(relaxed = true) + private val drawableRequest = mockk>(relaxed = true) + + private lateinit var view: DogDetailsView + private lateinit var context: Context + + @Before + fun setup() { + context = ApplicationProvider.getApplicationContext() + view = DogDetailsView(context).apply { + glideBuilder = glideRequest + } + + every { glideRequest.load(ofType(String::class)) } returns drawableRequest + } + + @Test + fun fetchesDogPicOnShow() { + view.setDogPic("https://dailybeagle.com/latest-picture") + shadowOf(getMainLooper()).idle() + verify { drawableRequest.into(view.viewBinding.dogImage) } + } +} diff --git a/magellan-sample-migration/src/test/java/com/wealthfront/magellan/sample/migration/tide/HelpScreenTest.kt b/magellan-sample-migration/src/test/java/com/wealthfront/magellan/sample/migration/tide/HelpScreenTest.kt deleted file mode 100644 index f26ba9ce..00000000 --- a/magellan-sample-migration/src/test/java/com/wealthfront/magellan/sample/migration/tide/HelpScreenTest.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.wealthfront.magellan.sample.migration.tide - -import android.content.Context -import android.os.Looper.getMainLooper -import androidx.test.core.app.ApplicationProvider -import com.wealthfront.magellan.lifecycle.LifecycleState -import com.wealthfront.magellan.lifecycle.transitionToState -import com.wealthfront.magellan.sample.migration.AppComponentContainer -import com.wealthfront.magellan.sample.migration.TestAppComponent -import com.wealthfront.magellan.sample.migration.TestSampleApplication -import com.wealthfront.magellan.sample.migration.api.DogApi -import com.wealthfront.magellan.sample.migration.api.DogMessage -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` -import org.mockito.MockitoAnnotations.initMocks -import org.robolectric.RobolectricTestRunner -import org.robolectric.Shadows.shadowOf -import org.robolectric.annotation.Config -import rx.Observable -import javax.inject.Inject - -@RunWith(RobolectricTestRunner::class) -@Config(application = TestSampleApplication::class) -class HelpScreenTest { - - @Mock lateinit var helpView: HelpView - - private lateinit var helpScreen: HelpScreen - private lateinit var context: Context - private var goToBreedsStep = false - - @Inject lateinit var api: DogApi - - @Before - fun setup() { - initMocks(this) - context = ApplicationProvider.getApplicationContext() - ((context as AppComponentContainer).injector() as TestAppComponent).inject(this) - - helpScreen = object : HelpScreen({ goToBreedsStep = true }) { - override fun createView(context: Context): HelpView { - return helpView - } - } - - `when`(api.getRandomImageForBreed("husky")).thenReturn( - Observable.just( - DogMessage( - message = "https://dailybeagle.com/latest-picture", - status = "something" - ) - ) - ) - } - - @Test - fun fetchesDogPicOnShow() { - helpScreen.transitionToState(LifecycleState.Shown(context)) - shadowOf(getMainLooper()).idle() - verify(helpView).setDogPic("https://dailybeagle.com/latest-picture") - } -} diff --git a/magellan-sample-migration/src/test/java/com/wealthfront/magellan/sample/migration/tide/HelpViewTest.kt b/magellan-sample-migration/src/test/java/com/wealthfront/magellan/sample/migration/tide/HelpViewTest.kt deleted file mode 100644 index cba847a7..00000000 --- a/magellan-sample-migration/src/test/java/com/wealthfront/magellan/sample/migration/tide/HelpViewTest.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.wealthfront.magellan.sample.migration.tide - -import android.content.Context -import android.graphics.drawable.Drawable -import android.os.Looper.getMainLooper -import androidx.test.core.app.ApplicationProvider -import com.bumptech.glide.RequestBuilder -import com.bumptech.glide.RequestManager -import com.wealthfront.magellan.sample.migration.TestSampleApplication -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.anyString -import org.mockito.Mock -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` -import org.mockito.MockitoAnnotations.initMocks -import org.robolectric.RobolectricTestRunner -import org.robolectric.Shadows.shadowOf -import org.robolectric.annotation.Config - -@RunWith(RobolectricTestRunner::class) -@Config(application = TestSampleApplication::class) -class HelpViewTest { - - @Mock lateinit var screen: HelpScreen - @Mock lateinit var glideRequest: RequestManager - @Mock lateinit var drawableRequest: RequestBuilder - - private lateinit var helpView: HelpView - private lateinit var context: Context - - @Before - fun setup() { - initMocks(this) - context = ApplicationProvider.getApplicationContext() - helpView = HelpView(context, screen).apply { - glideBuilder = glideRequest - } - - `when`(glideRequest.load(anyString())).thenReturn(drawableRequest) - } - - @Test - fun fetchesDogPicOnShow() { - helpView.setDogPic("https://dailybeagle.com/latest-picture") - shadowOf(getMainLooper()).idle() - verify(drawableRequest).into(helpView.binding.dogImage) - } -} diff --git a/magellan-sample-migration/src/test/resources/robolectric.properties b/magellan-sample-migration/src/test/resources/robolectric.properties index 89a6c8b4..c053aedb 100644 --- a/magellan-sample-migration/src/test/resources/robolectric.properties +++ b/magellan-sample-migration/src/test/resources/robolectric.properties @@ -1 +1,2 @@ -sdk=28 \ No newline at end of file +sdk=28 +application=com.wealthfront.magellan.sample.migration.TestSampleApplication \ No newline at end of file diff --git a/magellan-sample-migration/src/testAndroidViews/java/com/wealthfront/magellan/sample/migration/tide/DogListStepTest.kt b/magellan-sample-migration/src/testAndroidViews/java/com/wealthfront/magellan/sample/migration/tide/DogListStepTest.kt new file mode 100644 index 00000000..d903aa9a --- /dev/null +++ b/magellan-sample-migration/src/testAndroidViews/java/com/wealthfront/magellan/sample/migration/tide/DogListStepTest.kt @@ -0,0 +1,46 @@ +package com.wealthfront.magellan.sample.migration.tide + +import android.app.Application +import android.os.Looper.getMainLooper +import androidx.activity.ComponentActivity +import androidx.test.core.app.ApplicationProvider +import com.google.common.truth.Truth.assertThat +import com.wealthfront.magellan.lifecycle.setContentScreen +import com.wealthfront.magellan.sample.migration.AppComponentContainer +import com.wealthfront.magellan.sample.migration.TestAppComponent +import com.wealthfront.magellan.sample.migration.api.DogBreedsResponse +import io.mockk.coEvery +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows.shadowOf + +@RunWith(RobolectricTestRunner::class) +class DogListStepTest { + + private lateinit var dogListStep: DogListStep + private val activityController = Robolectric.buildActivity(ComponentActivity::class.java) + + private var chosenBreed: String? = null + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + val component = ((context as AppComponentContainer).injector() as TestAppComponent) + dogListStep = component.dogListStepFactory.create { chosenBreed = it } + coEvery { component.api.getAllBreeds() } returns + DogBreedsResponse(message = mapOf("akita" to emptyList()), status = "success") + } + + @Test + fun goesToSelectedDogBreed() { + activityController.get().setContentScreen(dogListStep) + activityController.setup() + shadowOf(getMainLooper()).idle() + + dogListStep.viewBinding!!.dogItems.findViewHolderForAdapterPosition(0)!!.itemView.performClick() + assertThat(chosenBreed).isEqualTo("akita") + } +} diff --git a/magellan-sample-migration/src/testAndroidViews/resources/robolectric.properties b/magellan-sample-migration/src/testAndroidViews/resources/robolectric.properties new file mode 100644 index 00000000..c053aedb --- /dev/null +++ b/magellan-sample-migration/src/testAndroidViews/resources/robolectric.properties @@ -0,0 +1,2 @@ +sdk=28 +application=com.wealthfront.magellan.sample.migration.TestSampleApplication \ No newline at end of file diff --git a/magellan-sample-migration/src/testCompose/java/com/wealthfront/magellan/sample/migration/tide/DogBreedsTest.kt b/magellan-sample-migration/src/testCompose/java/com/wealthfront/magellan/sample/migration/tide/DogBreedsTest.kt new file mode 100644 index 00000000..4c87070d --- /dev/null +++ b/magellan-sample-migration/src/testCompose/java/com/wealthfront/magellan/sample/migration/tide/DogBreedsTest.kt @@ -0,0 +1,30 @@ +package com.wealthfront.magellan.sample.migration.tide + +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onChildAt +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class DogBreedsTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun goesToSelectedDogBreed() { + var clicked = false + composeTestRule.setContent { + DogBreeds(dogBreeds = listOf("akita"), onBreedClick = { clicked = true }) + } + + composeTestRule.onNodeWithTag("DogBreeds").onChildAt(0).performClick() + + assertThat(clicked).isTrue() + } +} diff --git a/magellan-sample-migration/src/testCompose/java/com/wealthfront/magellan/sample/migration/tide/DogListStepTest.kt b/magellan-sample-migration/src/testCompose/java/com/wealthfront/magellan/sample/migration/tide/DogListStepTest.kt new file mode 100644 index 00000000..e3cd3d68 --- /dev/null +++ b/magellan-sample-migration/src/testCompose/java/com/wealthfront/magellan/sample/migration/tide/DogListStepTest.kt @@ -0,0 +1,54 @@ +package com.wealthfront.magellan.sample.migration.tide + +import android.app.Application +import android.os.Looper.getMainLooper +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onChildAt +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.test.core.app.ApplicationProvider +import com.google.common.truth.Truth.assertThat +import com.wealthfront.magellan.lifecycle.setContentScreen +import com.wealthfront.magellan.sample.migration.AppComponentContainer +import com.wealthfront.magellan.sample.migration.TestAppComponent +import com.wealthfront.magellan.sample.migration.api.DogBreedsResponse +import io.mockk.coEvery +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows.shadowOf + +@RunWith(RobolectricTestRunner::class) +class DogListStepTest { + + @get:Rule + val composeTestRule = createComposeRule() + + private lateinit var dogListStep: DogListStep + private val activityController = Robolectric.buildActivity(ComponentActivity::class.java) + + private var chosenBreed: String? = null + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + val component = ((context as AppComponentContainer).injector() as TestAppComponent) + dogListStep = component.dogListStepFactory.create { chosenBreed = it } + coEvery { component.api.getAllBreeds() } returns + DogBreedsResponse(message = mapOf("akita" to emptyList()), status = "success") + } + + @Test + fun goesToSelectedDogBreed() { + activityController.get().setContentScreen(dogListStep) + activityController.setup() + shadowOf(getMainLooper()).idle() + + composeTestRule.onNodeWithTag("DogBreeds").onChildAt(0).performClick() + assertThat(chosenBreed).isEqualTo("akita") + } +} diff --git a/magellan-sample/build.gradle b/magellan-sample/build.gradle index 9a115ce1..5c3ccef0 100644 --- a/magellan-sample/build.gradle +++ b/magellan-sample/build.gradle @@ -15,8 +15,8 @@ android { } compileOptions { - setSourceCompatibility(JavaVersion.VERSION_1_8) - setTargetCompatibility(JavaVersion.VERSION_1_8) + setSourceCompatibility(JavaVersion.VERSION_17) + setTargetCompatibility(JavaVersion.VERSION_17) } buildFeatures { @@ -33,27 +33,28 @@ android { minifyEnabled false } } + namespace 'com.wealthfront.magellan.sample' } dependencies { implementation project(':magellan-library') implementation project(':magellan-legacy') - implementation Dependencies.appCompat - implementation Dependencies.material - implementation Dependencies.coroutines - implementation Dependencies.coroutinesAndroid + implementation libs.appCompat + implementation libs.material + implementation libs.coroutines + implementation libs.coroutinesAndroid - implementation Dependencies.dagger - kapt Dependencies.daggerCompiler + implementation libs.dagger + kapt libs.daggerCompiler - testImplementation Dependencies.junit + testImplementation libs.junit - androidTestImplementation Dependencies.testRunner - androidTestImplementation Dependencies.testRules - androidTestImplementation Dependencies.uiAutomator - androidTestImplementation Dependencies.extJunit - androidTestImplementation Dependencies.espressoCore + androidTestImplementation libs.testRunner + androidTestImplementation libs.testRules + androidTestImplementation libs.uiAutomator + androidTestImplementation libs.extJunit + androidTestImplementation libs.espressoCore lintPublish project(':magellan-lint') } diff --git a/magellan-sample/src/androidTest/java/com/wealthfront/magellan/DisableAnimationsAndKeyboardRule.kt b/magellan-sample/src/androidTest/java/com/wealthfront/magellan/DisableAnimationsAndKeyboardRule.kt new file mode 100644 index 00000000..548d5209 --- /dev/null +++ b/magellan-sample/src/androidTest/java/com/wealthfront/magellan/DisableAnimationsAndKeyboardRule.kt @@ -0,0 +1,52 @@ +package com.wealthfront.magellan + +import android.provider.Settings.Global.ANIMATOR_DURATION_SCALE +import android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE +import android.provider.Settings.Global.WINDOW_ANIMATION_SCALE +import android.util.Log +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import androidx.test.uiautomator.UiDevice +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +private const val TRANSITION_ANIMATION_SETTINGS = "settings put global $TRANSITION_ANIMATION_SCALE" +private const val WINDOW_ANIMATION_SETTINGS = "settings put global $WINDOW_ANIMATION_SCALE" +private const val ANIMATOR_ANIMATION_SETTINGS = "settings put global $ANIMATOR_DURATION_SCALE" +private const val DISABLE_KEYBOARD_SETTINGS = "settings put secure show_ime_with_hard_keyboard" + +class DisableAnimationsAndKeyboardRule : TestRule { + + override fun apply(base: Statement, description: Description): Statement { + return object : Statement() { + override fun evaluate() { + disableAnimationsAndKeyboard() + try { + base.evaluate() + } finally { + enableAnimationsAndKeyboard() + } + } + } + } + + private fun enableAnimationsAndKeyboard() { + Log.v(DisableAnimationsAndKeyboardRule::class.java.simpleName, "Enabling animations and keyboard") + executeUiDeviceCommand("$TRANSITION_ANIMATION_SETTINGS 1") + executeUiDeviceCommand("$WINDOW_ANIMATION_SETTINGS 1") + executeUiDeviceCommand("$ANIMATOR_ANIMATION_SETTINGS 1") + executeUiDeviceCommand("$DISABLE_KEYBOARD_SETTINGS 1") + } + + private fun disableAnimationsAndKeyboard() { + Log.v(DisableAnimationsAndKeyboardRule::class.java.simpleName, "Disabling animations and keyboard") + executeUiDeviceCommand("$TRANSITION_ANIMATION_SETTINGS 0") + executeUiDeviceCommand("$WINDOW_ANIMATION_SETTINGS 0") + executeUiDeviceCommand("$ANIMATOR_ANIMATION_SETTINGS 0") + executeUiDeviceCommand("$DISABLE_KEYBOARD_SETTINGS 0") + } + + private fun executeUiDeviceCommand(command: String) { + UiDevice.getInstance(getInstrumentation()).executeShellCommand(command) + } +} diff --git a/magellan-sample/src/androidTest/java/com/wealthfront/magellan/uitest/DialogTest.kt b/magellan-sample/src/androidTest/java/com/wealthfront/magellan/uitest/DialogTest.kt index f5a9b2f9..3c647887 100644 --- a/magellan-sample/src/androidTest/java/com/wealthfront/magellan/uitest/DialogTest.kt +++ b/magellan-sample/src/androidTest/java/com/wealthfront/magellan/uitest/DialogTest.kt @@ -11,6 +11,7 @@ import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.rules.ActivityScenarioRule +import com.wealthfront.magellan.DisableAnimationsAndKeyboardRule import com.wealthfront.magellan.action.orientationLandscape import com.wealthfront.magellan.onDialogView import com.wealthfront.magellan.sample.MainActivity @@ -22,7 +23,9 @@ import org.junit.Test class DialogTest { @get:Rule - var activityRule = ActivityScenarioRule(MainActivity::class.java) + val activityRule = ActivityScenarioRule(MainActivity::class.java) + @get:Rule + val animationsAndKeyboardRule = DisableAnimationsAndKeyboardRule() @Before fun setup() { @@ -59,6 +62,12 @@ class DialogTest { assertDialogContentsShown() } + + @Test + fun showDialog_immediate() { + onView(withId(R.id.dialog3)).perform(click()) + assertDialogContentsShown() + } } private fun assertDialogContentsShown() { diff --git a/magellan-sample/src/androidTest/java/com/wealthfront/magellan/uitest/NavigationTest.kt b/magellan-sample/src/androidTest/java/com/wealthfront/magellan/uitest/NavigationTest.kt index 05fa01a8..9625813d 100644 --- a/magellan-sample/src/androidTest/java/com/wealthfront/magellan/uitest/NavigationTest.kt +++ b/magellan-sample/src/androidTest/java/com/wealthfront/magellan/uitest/NavigationTest.kt @@ -48,6 +48,10 @@ class NavigationTest { assertShown { text(R.string.learn_more_text) } onView(withId(R.id.nextJourney)).perform(click()) assertShown { text(R.string.detail_memo_text) } + onView(withId(R.id.nextJourney)).perform(click()) + assertShown { text(R.string.learn_more_text) } + pressBack() + assertShown { text(R.string.detail_memo_text) } pressBack() assertShown { text(R.string.learn_more_text) } pressBack() diff --git a/magellan-sample/src/main/AndroidManifest.xml b/magellan-sample/src/main/AndroidManifest.xml index 012ca847..a00c4c2a 100644 --- a/magellan-sample/src/main/AndroidManifest.xml +++ b/magellan-sample/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ diff --git a/magellan-sample/src/main/java/com/wealthfront/magellan/sample/DetailStep.kt b/magellan-sample/src/main/java/com/wealthfront/magellan/sample/DetailStep.kt index 2c187284..e4d2c794 100644 --- a/magellan-sample/src/main/java/com/wealthfront/magellan/sample/DetailStep.kt +++ b/magellan-sample/src/main/java/com/wealthfront/magellan/sample/DetailStep.kt @@ -2,6 +2,7 @@ package com.wealthfront.magellan.sample import android.app.AlertDialog import android.content.Context +import android.view.View import com.wealthfront.magellan.DialogCreator import com.wealthfront.magellan.core.Step import com.wealthfront.magellan.lifecycle.attachLateinitFieldToLifecycle @@ -11,11 +12,11 @@ import com.wealthfront.magellan.view.DialogComponent import javax.inject.Inject class DetailStep( - private val startSecondJourney: () -> Unit + private val startSecondJourney: (clickedView: View) -> Unit, + private val startDialogStep: () -> Unit ) : Step(DetailBinding::inflate) { - @set:Inject var dialogComponent1: DialogComponent by attachLateinitFieldToLifecycle() - @set:Inject var dialogComponent2: DialogComponent by attachLateinitFieldToLifecycle() + @set:Inject var dialogComponent: DialogComponent by attachLateinitFieldToLifecycle() override fun onCreate(context: Context) { appComponent.inject(this) @@ -23,14 +24,17 @@ class DetailStep( override fun onShow(context: Context, binding: DetailBinding) { binding.dialog1.setOnClickListener { - dialogComponent1.showDialog(DialogCreator { activity -> getHelloDialog(activity) }) + dialogComponent.showDialog(DialogCreator { activity -> getHelloDialog(activity) }) } binding.dialog2.setOnClickListener { - dialogComponent2.showDialog(DialogCreator { activity -> getOrderDialog(activity) }) + dialogComponent.showDialog(DialogCreator { activity -> getOrderDialog(activity) }) + } + binding.dialog3.setOnClickListener { + startDialogStep() } binding.nextJourney.setOnClickListener { - startSecondJourney() + startSecondJourney(it) } } diff --git a/magellan-sample/src/main/java/com/wealthfront/magellan/sample/DialogStep.kt b/magellan-sample/src/main/java/com/wealthfront/magellan/sample/DialogStep.kt new file mode 100644 index 00000000..60ebf265 --- /dev/null +++ b/magellan-sample/src/main/java/com/wealthfront/magellan/sample/DialogStep.kt @@ -0,0 +1,25 @@ +package com.wealthfront.magellan.sample + +import android.app.AlertDialog +import android.content.Context +import com.wealthfront.magellan.DialogCreator +import com.wealthfront.magellan.core.Step +import com.wealthfront.magellan.lifecycle.attachFieldToLifecycle +import com.wealthfront.magellan.sample.databinding.EmptyBinding +import com.wealthfront.magellan.view.DialogComponent + +class DialogStep : Step(EmptyBinding::inflate) { + + private val dialogComponent by attachFieldToLifecycle(DialogComponent()) + + override fun onShow(context: Context, binding: EmptyBinding) { + dialogComponent.showDialog(DialogCreator { activity -> getHelloDialog(activity) }) + } + + private fun getHelloDialog(context: Context): AlertDialog { + return AlertDialog.Builder(context) + .setTitle("Hello") + .setMessage("Are you sure about this?") + .create() + } +} diff --git a/magellan-sample/src/main/java/com/wealthfront/magellan/sample/SecondJourney.kt b/magellan-sample/src/main/java/com/wealthfront/magellan/sample/SecondJourney.kt index 7779f3e6..26941008 100644 --- a/magellan-sample/src/main/java/com/wealthfront/magellan/sample/SecondJourney.kt +++ b/magellan-sample/src/main/java/com/wealthfront/magellan/sample/SecondJourney.kt @@ -1,9 +1,11 @@ package com.wealthfront.magellan.sample import android.content.Context +import android.view.View import com.wealthfront.magellan.core.Journey import com.wealthfront.magellan.sample.App.Provider.appComponent import com.wealthfront.magellan.sample.databinding.SecondJourneyBinding +import com.wealthfront.magellan.transitions.CircularRevealTransition import javax.inject.Inject class SecondJourney : Journey(SecondJourneyBinding::inflate, SecondJourneyBinding::container) { @@ -12,10 +14,14 @@ class SecondJourney : Journey(SecondJourneyBinding::inflat override fun onCreate(context: Context) { appComponent.inject(this) - navigator.goTo(DetailStep(::startSecondJourney)) + navigator.goTo(DetailStep(::startSecondJourney, ::startDialogStep)) } - private fun startSecondJourney() { - navigator.goTo(LearnMoreStep()) + private fun startSecondJourney(clickedView: View) { + navigator.goTo(LearnMoreStep(), CircularRevealTransition(clickedView)) + } + + private fun startDialogStep() { + navigator.goTo(DialogStep()) } } diff --git a/magellan-sample/src/main/res/layout/detail.xml b/magellan-sample/src/main/res/layout/detail.xml index a32f0935..36450350 100644 --- a/magellan-sample/src/main/res/layout/detail.xml +++ b/magellan-sample/src/main/res/layout/detail.xml @@ -37,6 +37,14 @@ android:text="@string/dialog_2" /> +