diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a7a24d86..bc4906ad 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -66,6 +66,7 @@ coroutinesTest = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-t 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" } diff --git a/magellan-sample-migration/build.gradle b/magellan-sample-migration/build.gradle index ded2af3d..998e2323 100644 --- a/magellan-sample-migration/build.gradle +++ b/magellan-sample-migration/build.gradle @@ -19,10 +19,26 @@ android { setTargetCompatibility(JavaVersion.VERSION_17) } + flavorDimensions += "ui" + productFlavors { + register("androidViews") { + isDefault = true + dimension = "ui" + } + register("compose") { + dimension = "ui" + } + } + buildFeatures { + compose = true viewBinding = true } + composeOptions { + kotlinCompilerExtensionVersion = "1.4.8" + } + testOptions { unitTests { includeAndroidResources = true @@ -66,12 +82,12 @@ dependencies { implementation libs.jodaTime implementation libs.recyclerView - implementation(enforcedPlatform(libs.compose.bom)) + implementation(platform(libs.compose.bom)) implementation libs.compose.foundation implementation libs.compose.ui + implementation libs.compose.ui.graphics implementation libs.compose.tooling implementation libs.compose.tooling.preview - implementation libs.compose.material implementation libs.compose.material3 kaptTest libs.daggerCompiler @@ -81,6 +97,9 @@ dependencies { testImplementation libs.mockK testImplementation libs.robolectric testImplementation libs.truth +// testImplementation libs.espressoCore + testImplementation libs.compose.junit4 + testImplementation libs.compose.manifest kaptAndroidTest libs.daggerCompiler androidTestImplementation libs.extJunit diff --git a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/tide/DogListAdapter.kt b/magellan-sample-migration/src/androidViews/java/com/wealthfront/magellan/sample/migration/tide/DogListAdapter.kt similarity index 100% rename from magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/tide/DogListAdapter.kt rename to magellan-sample-migration/src/androidViews/java/com/wealthfront/magellan/sample/migration/tide/DogListAdapter.kt diff --git a/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/tide/DogListStep.kt b/magellan-sample-migration/src/androidViews/java/com/wealthfront/magellan/sample/migration/tide/DogListStep.kt similarity index 100% rename from magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/tide/DogListStep.kt rename to magellan-sample-migration/src/androidViews/java/com/wealthfront/magellan/sample/migration/tide/DogListStep.kt diff --git a/magellan-sample-migration/src/main/res/layout/dashboard.xml b/magellan-sample-migration/src/androidViews/res/layout/dashboard.xml similarity index 100% rename from magellan-sample-migration/src/main/res/layout/dashboard.xml rename to magellan-sample-migration/src/androidViews/res/layout/dashboard.xml 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/java/com/wealthfront/magellan/sample/migration/AppComponent.kt b/magellan-sample-migration/src/main/java/com/wealthfront/magellan/sample/migration/AppComponent.kt index 7add1949..09a91461 100644 --- 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 @@ -1,7 +1,5 @@ package com.wealthfront.magellan.sample.migration -import com.wealthfront.magellan.sample.migration.tide.DogDetailsScreen -import com.wealthfront.magellan.sample.migration.tide.DogListStep import dagger.Component import javax.inject.Singleton @@ -10,7 +8,5 @@ import javax.inject.Singleton interface AppComponent { fun inject(activity: MainActivity) - fun inject(step: DogListStep) - fun inject(screen: DogDetailsScreen) fun inject(expedition: Expedition) } diff --git a/magellan-sample-migration/src/test/java/com/wealthfront/magellan/sample/migration/TestAppComponent.kt b/magellan-sample-migration/src/test/java/com/wealthfront/magellan/sample/migration/TestAppComponent.kt index a77c89db..1be7841b 100644 --- a/magellan-sample-migration/src/test/java/com/wealthfront/magellan/sample/migration/TestAppComponent.kt +++ b/magellan-sample-migration/src/test/java/com/wealthfront/magellan/sample/migration/TestAppComponent.kt @@ -1,7 +1,7 @@ package com.wealthfront.magellan.sample.migration import com.wealthfront.magellan.sample.migration.api.DogApi -import com.wealthfront.magellan.sample.migration.tide.DogListStepTest +import com.wealthfront.magellan.sample.migration.tide.DogListStepFactory import com.wealthfront.magellan.sample.migration.toolbar.ToolbarHelper import dagger.Component import javax.inject.Singleton @@ -12,6 +12,5 @@ interface TestAppComponent : AppComponent { val toolbarHelper: ToolbarHelper val api: DogApi - - fun inject(test: DogListStepTest) + val dogListStepFactory: DogListStepFactory } diff --git a/magellan-sample-migration/src/test/java/com/wealthfront/magellan/sample/migration/tide/DogListStepTest.kt b/magellan-sample-migration/src/testAndroidViews/java/com/wealthfront/magellan/sample/migration/tide/DogListStepTest.kt similarity index 90% rename from magellan-sample-migration/src/test/java/com/wealthfront/magellan/sample/migration/tide/DogListStepTest.kt rename to magellan-sample-migration/src/testAndroidViews/java/com/wealthfront/magellan/sample/migration/tide/DogListStepTest.kt index ed89a95e..d903aa9a 100644 --- a/magellan-sample-migration/src/test/java/com/wealthfront/magellan/sample/migration/tide/DogListStepTest.kt +++ b/magellan-sample-migration/src/testAndroidViews/java/com/wealthfront/magellan/sample/migration/tide/DogListStepTest.kt @@ -16,13 +16,11 @@ import org.junit.runner.RunWith import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows.shadowOf -import javax.inject.Inject @RunWith(RobolectricTestRunner::class) class DogListStepTest { private lateinit var dogListStep: DogListStep - @Inject lateinit var dogListStepFactory: DogListStepFactory private val activityController = Robolectric.buildActivity(ComponentActivity::class.java) private var chosenBreed: String? = null @@ -31,8 +29,7 @@ class DogListStepTest { fun setUp() { val context = ApplicationProvider.getApplicationContext() val component = ((context as AppComponentContainer).injector() as TestAppComponent) - component.inject(this) - dogListStep = dogListStepFactory.create { chosenBreed = it } + dogListStep = component.dogListStepFactory.create { chosenBreed = it } coEvery { component.api.getAllBreeds() } returns DogBreedsResponse(message = mapOf("akita" to emptyList()), status = "success") } 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") + } +}