diff --git a/README.md b/README.md index 07e2f48c..7f73b497 100644 --- a/README.md +++ b/README.md @@ -20,4 +20,8 @@ - [x] 에러가 발생했을 때 스낵바 노출 구현 - [x] 스낵바 재시도 기능 구현 -### 4단계 \ No newline at end of file +### 4단계 +- [x] 데이터 받아올 때 Star 수도 받아 오도록 수정 +- [x] domain 패키지 생성 후 entity 이동 및 50개 판단 로직 추가 +- [x] UiModel 생성 및 적용 +- [x] Start 수 노출 및 Hot 표시 UI 구현 diff --git a/app/src/androidTest/java/nextstep/github/RepositoryListScreenTest.kt b/app/src/androidTest/java/nextstep/github/RepositoryListScreenTest.kt index c7b66d6f..9cad39ee 100644 --- a/app/src/androidTest/java/nextstep/github/RepositoryListScreenTest.kt +++ b/app/src/androidTest/java/nextstep/github/RepositoryListScreenTest.kt @@ -8,9 +8,8 @@ import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.test.advanceTimeBy -import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest -import nextstep.github.data.entity.Repository +import nextstep.github.domain.entity.Repository import nextstep.github.ui.RepositoryListScreen import nextstep.github.ui.model.RepositoryListScreenUiState import org.junit.Test diff --git a/app/src/main/java/nextstep/github/data/di/AppContainer.kt b/app/src/main/java/nextstep/github/data/di/AppContainer.kt index c92cc060..5f589725 100644 --- a/app/src/main/java/nextstep/github/data/di/AppContainer.kt +++ b/app/src/main/java/nextstep/github/data/di/AppContainer.kt @@ -8,6 +8,7 @@ import nextstep.github.data.GithubNetworkModule.provideJsonConverterFactory import nextstep.github.data.datasource.api.GithubDataSource import nextstep.github.data.repository.api.GithubRepository import nextstep.github.data.service.GithubService +import nextstep.github.domain.usecase.GetRepositoryListUseCase import retrofit2.Converter import retrofit2.Retrofit @@ -17,6 +18,7 @@ interface AppContainer { val githubService: GithubService val githubDataSource: GithubDataSource val githubRepository: GithubRepository + val getRepositoryList: GetRepositoryListUseCase } class AppContainerImpl : AppContainer { @@ -26,4 +28,5 @@ class AppContainerImpl : AppContainer { override val githubService = provideGithubService(retrofit) override val githubDataSource = provideGithubDataSource(githubService) override val githubRepository = provideGithubRepository(githubDataSource) + override val getRepositoryList: GetRepositoryListUseCase = GetRepositoryListUseCase(githubRepository) } \ No newline at end of file diff --git a/app/src/main/java/nextstep/github/data/entity/Repository.kt b/app/src/main/java/nextstep/github/data/entity/Repository.kt deleted file mode 100644 index 3df764ad..00000000 --- a/app/src/main/java/nextstep/github/data/entity/Repository.kt +++ /dev/null @@ -1,6 +0,0 @@ -package nextstep.github.data.entity - -data class Repository( - val fullName: String, - val description: String, -) diff --git a/app/src/main/java/nextstep/github/data/model/RepositoryResponseModel.kt b/app/src/main/java/nextstep/github/data/model/RepositoryResponseModel.kt index 37c537a6..cd86e871 100644 --- a/app/src/main/java/nextstep/github/data/model/RepositoryResponseModel.kt +++ b/app/src/main/java/nextstep/github/data/model/RepositoryResponseModel.kt @@ -2,15 +2,10 @@ package nextstep.github.data.model import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import nextstep.github.data.entity.Repository @Serializable data class RepositoryResponseModel( @SerialName("full_name") val fullName: String?, @SerialName("description") val description: String?, + @SerialName("stargazers_count") val stars: Int?, ) - -fun RepositoryResponseModel.toEntity() = Repository( - fullName = this.fullName.orEmpty(), - description = this.description.orEmpty(), -) \ No newline at end of file diff --git a/app/src/main/java/nextstep/github/data/repository/api/GithubRepository.kt b/app/src/main/java/nextstep/github/data/repository/api/GithubRepository.kt index b05b6b7f..074c463c 100644 --- a/app/src/main/java/nextstep/github/data/repository/api/GithubRepository.kt +++ b/app/src/main/java/nextstep/github/data/repository/api/GithubRepository.kt @@ -1,8 +1,8 @@ package nextstep.github.data.repository.api -import nextstep.github.data.entity.Repository +import nextstep.github.data.model.RepositoryResponseModel interface GithubRepository { - suspend fun getRepos(): List + suspend fun getRepos(): List } \ No newline at end of file diff --git a/app/src/main/java/nextstep/github/data/repository/impl/GithubRepositoryImpl.kt b/app/src/main/java/nextstep/github/data/repository/impl/GithubRepositoryImpl.kt index 3f4be27b..a4f7aa0f 100644 --- a/app/src/main/java/nextstep/github/data/repository/impl/GithubRepositoryImpl.kt +++ b/app/src/main/java/nextstep/github/data/repository/impl/GithubRepositoryImpl.kt @@ -1,14 +1,13 @@ package nextstep.github.data.repository.impl import nextstep.github.data.datasource.api.GithubDataSource -import nextstep.github.data.entity.Repository -import nextstep.github.data.model.toEntity +import nextstep.github.data.model.RepositoryResponseModel import nextstep.github.data.repository.api.GithubRepository class GithubRepositoryImpl( private val dataSource: GithubDataSource, ): GithubRepository { - override suspend fun getRepos(): List { - return dataSource.getRepos().map { it.toEntity() } + override suspend fun getRepos(): List { + return dataSource.getRepos() } } \ No newline at end of file diff --git a/app/src/main/java/nextstep/github/domain/entity/Repository.kt b/app/src/main/java/nextstep/github/domain/entity/Repository.kt new file mode 100644 index 00000000..716580e6 --- /dev/null +++ b/app/src/main/java/nextstep/github/domain/entity/Repository.kt @@ -0,0 +1,8 @@ +package nextstep.github.domain.entity + +data class Repository( + val fullName: String, + val description: String, + val stars: Int, + val isHot: Boolean, +) diff --git a/app/src/main/java/nextstep/github/domain/usecase/CheckIsHotRepoUseCase.kt b/app/src/main/java/nextstep/github/domain/usecase/CheckIsHotRepoUseCase.kt new file mode 100644 index 00000000..0ff01536 --- /dev/null +++ b/app/src/main/java/nextstep/github/domain/usecase/CheckIsHotRepoUseCase.kt @@ -0,0 +1,20 @@ +package nextstep.github.domain.usecase + +import nextstep.github.data.repository.api.GithubRepository +import nextstep.github.domain.entity.Repository + +class GetRepositoryListUseCase( + private val githubRepository: GithubRepository, +) { + + suspend operator fun invoke(): List { + return githubRepository.getRepos().map { + Repository( + fullName = it.fullName.orEmpty(), + description = it.description.orEmpty(), + stars = it.stars ?: 0, + isHot = (it.stars ?: 0) > 50, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/nextstep/github/ui/RepositoryListScreen.kt b/app/src/main/java/nextstep/github/ui/RepositoryListScreen.kt index c3e1e0a1..ef0d1c9f 100644 --- a/app/src/main/java/nextstep/github/ui/RepositoryListScreen.kt +++ b/app/src/main/java/nextstep/github/ui/RepositoryListScreen.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.material3.Snackbar +import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult @@ -18,7 +19,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import kotlinx.collections.immutable.toPersistentList -import nextstep.github.data.entity.Repository +import nextstep.github.domain.entity.Repository import nextstep.github.ui.component.RepositoryListContent import nextstep.github.ui.component.RepositoryListEmptyContent import nextstep.github.ui.component.RepositoryListErrorContent @@ -131,7 +132,9 @@ private fun RepositoryListScreenPreview() { repositoryList = List(10) { Repository( fullName = "nextstep/github", - description = "Github Repository for NextStep" + description = "Github Repository for NextStep", + stars = 50, + isHot = true, ) }.toPersistentList() ) @@ -163,8 +166,19 @@ private fun RepositoryListEmptyScreenPreview() { @Composable private fun RepositoryListErrorScreenPreview() { GithubTheme { + val snackBarHostState = SnackbarHostState() + + LaunchedEffect(Unit) { + snackBarHostState.showSnackbar( + message = "예상치 못한 오류가 발생하였습니다.", + actionLabel = "재시도", + duration = SnackbarDuration.Indefinite, + ) + } + RepositoryListScreen( - uiState = RepositoryListScreenUiState.Error + uiState = RepositoryListScreenUiState.Error, + snackBarHostState = snackBarHostState ) } } \ No newline at end of file diff --git a/app/src/main/java/nextstep/github/ui/RepositoryListViewModel.kt b/app/src/main/java/nextstep/github/ui/RepositoryListViewModel.kt index df5134bc..89a87d1d 100644 --- a/app/src/main/java/nextstep/github/ui/RepositoryListViewModel.kt +++ b/app/src/main/java/nextstep/github/ui/RepositoryListViewModel.kt @@ -16,14 +16,17 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import nextstep.github.NextGitHubApplication import nextstep.github.data.repository.api.GithubRepository +import nextstep.github.domain.usecase.GetRepositoryListUseCase import nextstep.github.ui.model.RepositoryListScreenSideEffect import nextstep.github.ui.model.RepositoryListScreenUiState class RepositoryListViewModel( private val repository: GithubRepository, + private val getRepositoryListUseCase: GetRepositoryListUseCase, ) : ViewModel() { - private val _uiState = MutableStateFlow(RepositoryListScreenUiState.Loading) + private val _uiState = + MutableStateFlow(RepositoryListScreenUiState.Loading) val uiState: StateFlow = _uiState.asStateFlow() private val _sideEffect: Channel = Channel() @@ -36,7 +39,7 @@ class RepositoryListViewModel( fun loadRepositoryList() { viewModelScope.launch(ceh) { - val repositoryList = repository.getRepos().toPersistentList() + val repositoryList = getRepositoryListUseCase().toPersistentList() _uiState.value = if (repositoryList.isEmpty()) { RepositoryListScreenUiState.Empty } else { @@ -48,10 +51,15 @@ class RepositoryListViewModel( companion object { val Factory: ViewModelProvider.Factory = viewModelFactory { initializer { - val githubRepository = (this[APPLICATION_KEY] as NextGitHubApplication) - .appContainer - .githubRepository - RepositoryListViewModel(githubRepository) + val appContainer = (this[APPLICATION_KEY] as NextGitHubApplication).appContainer + + val githubRepository = appContainer.githubRepository + val getRepositoryListUseCase = appContainer.getRepositoryList + + RepositoryListViewModel( + repository = githubRepository, + getRepositoryListUseCase = getRepositoryListUseCase, + ) } } } diff --git a/app/src/main/java/nextstep/github/ui/component/RepositoryItem.kt b/app/src/main/java/nextstep/github/ui/component/RepositoryItem.kt index 824d31c9..c29e4b45 100644 --- a/app/src/main/java/nextstep/github/ui/component/RepositoryItem.kt +++ b/app/src/main/java/nextstep/github/ui/component/RepositoryItem.kt @@ -1,15 +1,18 @@ package nextstep.github.ui.component import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +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.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import nextstep.github.data.entity.Repository +import nextstep.github.domain.entity.Repository import nextstep.github.ui.theme.GithubTheme @Composable @@ -22,6 +25,23 @@ fun RepositoryItem( .background(MaterialTheme.colorScheme.surface) .padding(16.dp) ) { + Box( + modifier = Modifier.fillMaxWidth() + ) { + if (repository.isHot) { + Text( + text = "HOT", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.align(Alignment.TopStart) + ) + } + Text( + text = "★ ${repository.stars}", + style = MaterialTheme.typography.labelLarge, + modifier = Modifier.align(Alignment.TopEnd) + ) + } Text( text = repository.fullName, style = MaterialTheme.typography.titleLarge, @@ -35,6 +55,21 @@ fun RepositoryItem( } } +@Preview +@Composable +private fun RepositoryHotItemPreview() { + GithubTheme { + RepositoryItem( + repository = Repository( + fullName = "nextstep/github", + description = "Github Repository for NextStep", + stars = 50, + isHot = true, + ) + ) + } +} + @Preview @Composable private fun RepositoryItemPreview() { @@ -42,7 +77,9 @@ private fun RepositoryItemPreview() { RepositoryItem( repository = Repository( fullName = "nextstep/github", - description = "Github Repository for NextStep" + description = "Github Repository for NextStep", + stars = 0, + isHot = false, ) ) } diff --git a/app/src/main/java/nextstep/github/ui/component/RepositoryListContent.kt b/app/src/main/java/nextstep/github/ui/component/RepositoryListContent.kt index 327de0d3..973d59c7 100644 --- a/app/src/main/java/nextstep/github/ui/component/RepositoryListContent.kt +++ b/app/src/main/java/nextstep/github/ui/component/RepositoryListContent.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import nextstep.github.data.entity.Repository +import nextstep.github.domain.entity.Repository @Composable fun RepositoryListContent( diff --git a/app/src/main/java/nextstep/github/ui/model/RepositoryListScreenUiState.kt b/app/src/main/java/nextstep/github/ui/model/RepositoryListScreenUiState.kt index e65f7c79..b458d9c6 100644 --- a/app/src/main/java/nextstep/github/ui/model/RepositoryListScreenUiState.kt +++ b/app/src/main/java/nextstep/github/ui/model/RepositoryListScreenUiState.kt @@ -1,7 +1,7 @@ package nextstep.github.ui.model import kotlinx.collections.immutable.PersistentList -import nextstep.github.data.entity.Repository +import nextstep.github.domain.entity.Repository sealed interface RepositoryListScreenUiState{ diff --git a/app/src/main/java/nextstep/github/ui/theme/Color.kt b/app/src/main/java/nextstep/github/ui/theme/Color.kt index 87f77102..951a5114 100644 --- a/app/src/main/java/nextstep/github/ui/theme/Color.kt +++ b/app/src/main/java/nextstep/github/ui/theme/Color.kt @@ -11,11 +11,13 @@ val PurpleGrey40 = Color(0xFF625B71) val Pink40 = Color(0xFF7D5260) object GithubLightColorToken { + val Primary = Color(0xFF6750A4) val Surface = Color(0xFF2C292B) val OutlineVariant = Color(0xFFCAC4D0) } object GithubDarkColorToken { + val Primary = Color(0xFF6750A4) val Surface = Color(0xFFAfAAB4) val OutlineVariant = Color(0xFFAFAAB4) } diff --git a/app/src/main/java/nextstep/github/ui/theme/Theme.kt b/app/src/main/java/nextstep/github/ui/theme/Theme.kt index 22321a77..1353f36d 100644 --- a/app/src/main/java/nextstep/github/ui/theme/Theme.kt +++ b/app/src/main/java/nextstep/github/ui/theme/Theme.kt @@ -10,18 +10,18 @@ import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext -private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80, +private val LightColorScheme = darkColorScheme( + primary = GithubLightColorToken.Primary, + secondary = PurpleGrey40, + tertiary = Pink40, surface = GithubLightColorToken.Surface, outlineVariant = GithubLightColorToken.OutlineVariant, ) -private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40, +private val DarkColorScheme = lightColorScheme( + primary = GithubDarkColorToken.Primary, + secondary = PurpleGrey80, + tertiary = Pink80, surface = GithubDarkColorToken.Surface, outlineVariant = GithubDarkColorToken.OutlineVariant, ) diff --git a/app/src/main/java/nextstep/github/ui/theme/Type.kt b/app/src/main/java/nextstep/github/ui/theme/Type.kt index f1373aa0..6522cc25 100644 --- a/app/src/main/java/nextstep/github/ui/theme/Type.kt +++ b/app/src/main/java/nextstep/github/ui/theme/Type.kt @@ -35,7 +35,14 @@ val Typography = Typography( fontSize = 24.sp, lineHeight = 32.sp, letterSpacing = 0.sp - ) + ), + labelLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp + ), /* labelSmall = TextStyle(