Skip to content
9 changes: 2 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
# android-payments

## **🚀 4단계 - 페이먼츠(카드 수정)**
## **🚀 5단계 - 페이먼츠(리팩토링)**

---

### 구현 기능 목록

- 카드 수정 기능을 구현한다
- 카드 목록에서 카드를 선택하면 카드 수정 화면으로 이동한다.
- 카드 수정 화면에서 변경사항이 발생하지 않으면 수정이 불가능하다
- 카드가 수정되면 카드 목록 화면에 변경사항이 반영된다.
- 구현에 맞게 리팩토링 한다.
- 리뷰어님의 피드백을 반영한다.
- navigation 을 이용하여 리팩토링 한다
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,7 @@ dependencies {
androidTestImplementation(libs.androidx.espresso.intents)

implementation(libs.kotlinx.serialization.json)

implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.navigation.testing)
}
Original file line number Diff line number Diff line change
@@ -1,45 +1,62 @@
package nextstep.payments

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.isDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.compose.ComposeNavigator
import androidx.navigation.testing.TestNavHostController
import nextstep.payments.data.PaymentCardsRepository
import nextstep.payments.model.BankType
import nextstep.payments.model.Card
import nextstep.payments.navigation.rememberNavigator
import nextstep.payments.ui.cardlist.CardListViewModel
import nextstep.payments.ui.cardlist.navigation.CardList
import nextstep.payments.ui.main.MainScreen
import nextstep.payments.ui.newcard.navigation.NewCard
import nextstep.payments.ui.updatecard.navigation.UpdateCard
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.time.YearMonth

class RouteActivityTest {

class RouteScreenTest {
@get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()
val composeTestRule = createComposeRule()
private lateinit var navController: TestNavHostController

@Before
fun setUp() {
PaymentCardsRepository.clear()
composeTestRule.setContent {
navController = TestNavHostController(LocalContext.current)
navController.navigatorProvider.addNavigator(ComposeNavigator())
MainScreen(navigator = rememberNavigator(navController))
}
}

@Test
fun `시작경로가_올바르게_나와야_한다`() {
assertEquals(navController.currentDestination?.hasRoute<CardList>(), true)
}

@Test
fun `카드가_비어있는_경우_카드추가_버튼을_클릭하면_새로운_카드를_추가하는_화면으로_이동해야_한다`() {

//when
composeTestRule.onNodeWithContentDescription("add_card_icon").performClick()

composeTestRule.waitForIdle()

//then
composeTestRule.onNodeWithContentDescription("BankSelectBottomSheet").isDisplayed()
assertEquals(navController.currentDestination?.hasRoute<NewCard>(), true)
}

@Test
fun `카드가_한개만_있는_경우_카드추가_버튼을_클릭하면_새로운_카드를_추가하는_화면으로_이동해야_한다`() {
//given
// given
val card = Card(
type = BankType.BC,
number = "2345234523452345",
Expand All @@ -49,22 +66,19 @@ class RouteActivityTest {
)
PaymentCardsRepository.upsertCard(card)

val cardListViewModel =
ViewModelProvider(composeTestRule.activity)[CardListViewModel::class.java]

cardListViewModel.fetchCards()
navController.currentBackStackEntry?.let {
ViewModelProvider(it)[CardListViewModel::class.java].fetchCards()
}
composeTestRule.waitForIdle()


//when
composeTestRule.onNodeWithContentDescription("add_card_icon").performClick()

composeTestRule.waitForIdle()

//then
composeTestRule.onNodeWithContentDescription("BankSelectBottomSheet").isDisplayed()
assertEquals(navController.currentDestination?.hasRoute<NewCard>(), true)
}


@Test
fun `카드가_여러개인_경우_추가_텍스트를_클릭하면_새로운_카드를_추가하는_화면으로_이동해야_한다`() {
//given
Expand All @@ -86,19 +100,17 @@ class RouteActivityTest {
)
card.forEach(PaymentCardsRepository::upsertCard)

val cardListViewModel =
ViewModelProvider(composeTestRule.activity)[CardListViewModel::class.java]

cardListViewModel.fetchCards()
navController.currentBackStackEntry?.let {
ViewModelProvider(it)[CardListViewModel::class.java].fetchCards()
}
composeTestRule.waitForIdle()

//when
composeTestRule.onNodeWithText("추가").performClick()

composeTestRule.waitForIdle()

//then
composeTestRule.onNodeWithContentDescription("BankSelectBottomSheet").isDisplayed()
assertEquals(navController.currentDestination?.hasRoute<NewCard>(), true)
}

@Test
Expand All @@ -112,24 +124,17 @@ class RouteActivityTest {
password = ""
)
PaymentCardsRepository.upsertCard(card)

val cardListViewModel =
ViewModelProvider(composeTestRule.activity)[CardListViewModel::class.java]

cardListViewModel.fetchCards()
navController.currentBackStackEntry?.let {
ViewModelProvider(it)[CardListViewModel::class.java].fetchCards()
}
composeTestRule.waitForIdle()


//when
composeTestRule
.onNodeWithText("비씨카드")
.performClick()

composeTestRule.waitForIdle()

//then
composeTestRule
.onNodeWithText("카드 수정")
.assertIsDisplayed()
assertEquals(navController.currentDestination?.hasRoute<UpdateCard>(), true)
}
}
8 changes: 0 additions & 8 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,6 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<activity
android:name=".ui.newcard.NewCardActivity"
android:exported="false" />

<activity
android:name=".ui.updatecard.UpdateCardActivity"
android:exported="false" />
</application>

</manifest>
33 changes: 2 additions & 31 deletions app/src/main/java/nextstep/payments/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,41 +1,12 @@
package nextstep.payments

import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import nextstep.payments.base.BaseComponentActivity
import nextstep.payments.ui.cardlist.CardListScreen
import nextstep.payments.ui.cardlist.CardListViewModel
import nextstep.payments.ui.newcard.NewCardActivity
import nextstep.payments.ui.updatecard.UpdateCardActivity
import nextstep.payments.ui.main.MainScreen

class MainActivity : BaseComponentActivity() {
private val viewModel: CardListViewModel by viewModels()

@Composable
override fun SetContent() {
val launcher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
viewModel.fetchCards()
}
}
val uiState by viewModel.cardListUiState.collectAsStateWithLifecycle()
CardListScreen(
uiState = uiState,
onRouteToNewCard = {
launcher.launch(
NewCardActivity.newInstance(context = this)
)
},
onRouteToUpdateCard = { card ->
launcher.launch(
UpdateCardActivity.newInstance(context = this, item = card)
)
}
)
MainScreen()
}
}
55 changes: 55 additions & 0 deletions app/src/main/java/nextstep/payments/navigation/Navigator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package nextstep.payments.navigation

import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import nextstep.payments.model.Card
import nextstep.payments.ui.cardlist.navigation.CardList
import nextstep.payments.ui.cardlist.navigation.routeToCardList
import nextstep.payments.ui.newcard.navigation.routeToNewCard
import nextstep.payments.ui.updatecard.navigation.routeToUpdateCard


@Composable
fun rememberNavigator(
navController: NavHostController = rememberNavController()
): Navigator = remember(navController) {
NavigatorImpl(navController)
}

@Stable
class NavigatorImpl(
override val navController: NavHostController
) : Navigator {
override val startDestination: Screen
get() = CardList(isFetchCards = false)

override val routeToNewCard: () -> Unit
get() = {
navController.routeToNewCard()
}

override val routeToUpdateCard: (Card) -> Unit
get() = { card ->
navController.routeToUpdateCard(card = card)
}
override val routeToCardList: (isFetchCards: Boolean) -> Unit
get() = navController::routeToCardList
}


interface Navigator {

val navController: NavHostController

val startDestination: Screen

val routeToNewCard: () -> Unit

val routeToUpdateCard: (Card) -> Unit

val routeToCardList: (isFetchCards: Boolean) -> Unit

}
3 changes: 3 additions & 0 deletions app/src/main/java/nextstep/payments/navigation/Screen.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package nextstep.payments.navigation

interface Screen
30 changes: 30 additions & 0 deletions app/src/main/java/nextstep/payments/ui/cardlist/CardListScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,47 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import nextstep.payments.designsystem.theme.PaymentsTheme
import nextstep.payments.model.BankType
import nextstep.payments.model.Card
import nextstep.payments.ui.cardlist.component.CardListTopBar
import nextstep.payments.ui.cardlist.component.EmptyCardContainer
import nextstep.payments.ui.cardlist.component.ManyCardContainer
import nextstep.payments.ui.cardlist.component.OneCardContainer
import nextstep.payments.util.InjectUtil
import java.time.YearMonth


@Composable
fun CardListScreen(
onRouteToNewCard: () -> Unit,
onRouteToUpdateCard: (Card) -> Unit,
cardListViewModel: CardListViewModel = InjectUtil.createCardListViewModel()
) {

val uiState by cardListViewModel.cardListUiState.collectAsStateWithLifecycle()

val isFetchCards by cardListViewModel.isFetchCards.collectAsStateWithLifecycle()

LaunchedEffect(Unit) {
if (isFetchCards) {
cardListViewModel.fetchCards()
}
}

CardListScreen(
uiState = uiState,
onRouteToNewCard = onRouteToNewCard,
onRouteToUpdateCard = onRouteToUpdateCard
)
}


@Composable
fun CardListScreen(
uiState: CardListUiState,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
package nextstep.payments.ui.cardlist

import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import nextstep.payments.data.PaymentCardsRepository
import nextstep.payments.ui.cardlist.navigation.clearIsFetchCards
import nextstep.payments.ui.cardlist.navigation.isFetchCardStateFlow

class CardListViewModel(
private val savedStateHandle: SavedStateHandle,
private val repository: PaymentCardsRepository = PaymentCardsRepository
) : ViewModel() {

val isFetchCards = savedStateHandle.isFetchCardStateFlow()

class CardListViewModel(private val repository: PaymentCardsRepository = PaymentCardsRepository) :
ViewModel() {
private val _cardListUiState: MutableStateFlow<CardListUiState> =
MutableStateFlow(CardListUiState.Empty)
val cardListUiState: StateFlow<CardListUiState> = _cardListUiState.asStateFlow()

fun fetchCards() {
savedStateHandle.clearIsFetchCards()

val cards = repository.cards

val uiState = when (cards.size) {
Expand Down
Loading