Skip to content

Commit

Permalink
Merge pull request #300 from wealthfront/takehome-random
Browse files Browse the repository at this point in the history
Compose support for take-home challenges
  • Loading branch information
cmathew committed Apr 26, 2024
2 parents 6037af7 + 588ce22 commit 0a879d7
Show file tree
Hide file tree
Showing 16 changed files with 248 additions and 13 deletions.
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Expand Up @@ -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" }
Expand Down
23 changes: 21 additions & 2 deletions magellan-sample-migration/build.gradle
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
@@ -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()
}
}
}
}
}
@@ -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
)
)
}
@@ -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<String>, onBreedClick: (name: String) -> Unit) {
LazyColumn(modifier = Modifier.testTag("DogBreeds")) {
itemsIndexed(dogBreeds) { index, item ->
DogBreedListItem(item, onBreedClick)
if (index != (dogBreeds.size - 1)) {
HorizontalDivider()
}
}
}
}
@@ -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<String>()

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

Expand All @@ -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)
}
@@ -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
Expand All @@ -12,6 +12,5 @@ interface TestAppComponent : AppComponent {

val toolbarHelper: ToolbarHelper
val api: DogApi

fun inject(test: DogListStepTest)
val dogListStepFactory: DogListStepFactory
}
Expand Up @@ -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
Expand All @@ -31,8 +29,7 @@ class DogListStepTest {
fun setUp() {
val context = ApplicationProvider.getApplicationContext<Application>()
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")
}
Expand Down
@@ -0,0 +1,2 @@
sdk=28
application=com.wealthfront.magellan.sample.migration.TestSampleApplication
@@ -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()
}
}
@@ -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<Application>()
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")
}
}

0 comments on commit 0a879d7

Please sign in to comment.