diff --git a/README.md b/README.md index 46bbfe5a..acd86126 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,17 @@ - 로딩 UI를 노출할 때 CircularProgressIndicator를 활용한다. ## 기능 요구사항 -- [] 목록이 로딩되기 전에는 로딩 UI를 노출한다. -- [] 목록이 빈 경우에는 빈 화면 UI를 노출한다. -- [] 오류가 발생한 경우 재시도 가능한 스낵바를 노출한다. +- [O] 목록이 로딩되기 전에는 로딩 UI를 노출한다. +- [O] 목록이 빈 경우에는 빈 화면 UI를 노출한다. +- [O] 오류가 발생한 경우 재시도 가능한 스낵바를 노출한다. + +# Step 4 - GitHub(인기 저장소) + +## 프로그래밍 요구사항 + - domain 패키지를 만들어 비즈니스 로직을 캡슐화한다. + - 저장소의 Star 개수가 50개 이상인지 판단하는 로직도 도메인 레이어에 포함될지 스스로 판단한다. + +## 기능 요구사항 +- [O] 저장소의 Star 개수를 노출한다 +- [O] 저장소의 Star 개수가 50개 이상이면 HOT 텍스트를 노출한다. + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 43ee14d1..3e3c532d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,7 +12,7 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.Github" - android:name="MainApplication" + android:name=".di.MainApplication" tools:targetApi="31"> (GithubRepositoryUiState.Loading) - val uiState: StateFlow = _uiState - - fun getRepositories(organization: String) { - _uiState.value = GithubRepositoryUiState.Loading - - viewModelScope.launch(Dispatchers.IO) { - try { - when (val result = githubRepository.getRepositories(organization)) { - is ApiResult.Success -> { - val githubRepositories = result.value.map { - GithubRepositoryInfo( - fullName = it.fullName ?: "", - description = it.description ?: "" - ) - } - if (githubRepositories.isEmpty()) { - _uiState.value = GithubRepositoryUiState.Empty - } else { - _uiState.value = - GithubRepositoryUiState.Success( - githubRepositories = githubRepositories - ) - } - } - - is ApiResult.Error -> { - _uiState.value = GithubRepositoryUiState.Error - Log.e("GithubViewModel", "Error: ${result.code} ${result.exception}") - } - } - } catch (e: Exception) { - _uiState.value = GithubRepositoryUiState.Error - Log.e("GithubViewModel", "Error: ${e.message}") - } - } - } - - - companion object { - val Factory: ViewModelProvider.Factory = viewModelFactory { - initializer { - val githubRepository = (this[APPLICATION_KEY] as MainApplication) - .appContainer - .githubRepository - - GithubViewModel(githubRepository) - } - } - } -} diff --git a/app/src/main/java/nextstep/github/MainActivity.kt b/app/src/main/java/nextstep/github/MainActivity.kt index 497f8f13..f851af1e 100644 --- a/app/src/main/java/nextstep/github/MainActivity.kt +++ b/app/src/main/java/nextstep/github/MainActivity.kt @@ -6,12 +6,9 @@ import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.lifecycle.compose.collectAsStateWithLifecycle +import nextstep.github.ui.screen.github.GithubViewModel import nextstep.github.ui.screen.github.MainScreen import nextstep.github.ui.theme.GithubTheme @@ -28,12 +25,8 @@ class MainActivity : ComponentActivity() { modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { - val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val snackbarHostState = remember { SnackbarHostState() } MainScreen( - uiState = uiState, - snackbarHostState = snackbarHostState, - onClickSnackBar = { viewModel.getRepositories("next-step") } + viewModel = viewModel ) } } diff --git a/app/src/main/java/nextstep/github/core/data/GithubRepository.kt b/app/src/main/java/nextstep/github/core/data/GithubRepository.kt deleted file mode 100644 index 106f6fe8..00000000 --- a/app/src/main/java/nextstep/github/core/data/GithubRepository.kt +++ /dev/null @@ -1,9 +0,0 @@ -package nextstep.github.core.data - -import nextstep.github.core.model.RepositoryEntity -import nextstep.github.core.network.ApiResult - - -interface GithubRepository { - suspend fun getRepositories(organization: String): ApiResult> -} diff --git a/app/src/main/java/nextstep/github/core/data/GithubRepositoryImpl.kt b/app/src/main/java/nextstep/github/core/data/GithubRepositoryImpl.kt deleted file mode 100644 index d06b4b83..00000000 --- a/app/src/main/java/nextstep/github/core/data/GithubRepositoryImpl.kt +++ /dev/null @@ -1,20 +0,0 @@ -package nextstep.github.core.data - -import nextstep.github.core.model.RepositoryEntity -import nextstep.github.core.network.ApiResult -import nextstep.github.core.network.GithubService - -class GithubRepositoryImpl( - private val githubService: GithubService -) : GithubRepository { - - override suspend fun getRepositories(organization: String): ApiResult> { - val response = githubService.getRepositories(organization) - - return if (response.isSuccessful && response.body() != null) { - ApiResult.Success(response.body()!!) - } else { - ApiResult.Error(response.code(), Throwable(response.message())) - } - } -} diff --git a/app/src/main/java/nextstep/github/core/data/GithubRepositoryInfo.kt b/app/src/main/java/nextstep/github/core/data/GithubRepositoryInfo.kt deleted file mode 100644 index c413ce7b..00000000 --- a/app/src/main/java/nextstep/github/core/data/GithubRepositoryInfo.kt +++ /dev/null @@ -1,6 +0,0 @@ -package nextstep.github.core.data - -data class GithubRepositoryInfo( - val fullName: String, - val description: String, -) diff --git a/app/src/main/java/nextstep/github/core/model/RepositoryEntity.kt b/app/src/main/java/nextstep/github/core/model/RepositoryEntity.kt deleted file mode 100644 index 99462e96..00000000 --- a/app/src/main/java/nextstep/github/core/model/RepositoryEntity.kt +++ /dev/null @@ -1,16 +0,0 @@ -package nextstep.github.core.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import nextstep.github.core.data.GithubRepositoryInfo - -@Serializable -data class RepositoryEntity( - @SerialName("full_name") val fullName: String?, - @SerialName("description") val description: String?, -) { - fun RepositoryEntity.toRepositoryInfo() = GithubRepositoryInfo( - fullName = fullName ?: "", - description = description ?: "" - ) -} diff --git a/app/src/main/java/nextstep/github/data/model/GithubRepositoryData.kt b/app/src/main/java/nextstep/github/data/model/GithubRepositoryData.kt new file mode 100644 index 00000000..2e7f4243 --- /dev/null +++ b/app/src/main/java/nextstep/github/data/model/GithubRepositoryData.kt @@ -0,0 +1,11 @@ +package nextstep.github.data.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class GithubRepositoryData( + @SerialName("full_name") val fullName: String?, + @SerialName("description") val description: String?, + @SerialName("stargazers_count") val stars: Int?, +) diff --git a/app/src/main/java/nextstep/github/core/network/ApiResult.kt b/app/src/main/java/nextstep/github/data/network/ApiResult.kt similarity index 84% rename from app/src/main/java/nextstep/github/core/network/ApiResult.kt rename to app/src/main/java/nextstep/github/data/network/ApiResult.kt index c67b5b53..1e15ecc3 100644 --- a/app/src/main/java/nextstep/github/core/network/ApiResult.kt +++ b/app/src/main/java/nextstep/github/data/network/ApiResult.kt @@ -1,4 +1,4 @@ -package nextstep.github.core.network +package nextstep.github.data.network sealed class ApiResult { data class Success(val value: T) : ApiResult() diff --git a/app/src/main/java/nextstep/github/core/network/GithubService.kt b/app/src/main/java/nextstep/github/data/network/GithubService.kt similarity index 59% rename from app/src/main/java/nextstep/github/core/network/GithubService.kt rename to app/src/main/java/nextstep/github/data/network/GithubService.kt index 9177785e..10dd1313 100644 --- a/app/src/main/java/nextstep/github/core/network/GithubService.kt +++ b/app/src/main/java/nextstep/github/data/network/GithubService.kt @@ -1,6 +1,6 @@ -package nextstep.github.core.network +package nextstep.github.data.network -import nextstep.github.core.model.RepositoryEntity +import nextstep.github.data.model.GithubRepositoryData import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Path @@ -8,5 +8,5 @@ import retrofit2.http.Path interface GithubService { @GET("orgs/{organization}/repos") - suspend fun getRepositories(@Path("organization") organization: String): Response> + suspend fun getRepositories(@Path("organization") organization: String): Response> } diff --git a/app/src/main/java/nextstep/github/data/repository/GithubRepository.kt b/app/src/main/java/nextstep/github/data/repository/GithubRepository.kt new file mode 100644 index 00000000..b570fbbf --- /dev/null +++ b/app/src/main/java/nextstep/github/data/repository/GithubRepository.kt @@ -0,0 +1,9 @@ +package nextstep.github.data.repository + +import nextstep.github.data.model.GithubRepositoryData +import retrofit2.Response + + +interface GithubRepository { + suspend fun getRepositories(organization: String): Response> +} diff --git a/app/src/main/java/nextstep/github/data/repository/GithubRepositoryImpl.kt b/app/src/main/java/nextstep/github/data/repository/GithubRepositoryImpl.kt new file mode 100644 index 00000000..e1379f42 --- /dev/null +++ b/app/src/main/java/nextstep/github/data/repository/GithubRepositoryImpl.kt @@ -0,0 +1,14 @@ +package nextstep.github.data.repository + +import nextstep.github.data.model.GithubRepositoryData +import nextstep.github.data.network.GithubService +import retrofit2.Response + +class GithubRepositoryImpl( + private val githubService: GithubService +) : GithubRepository { + + override suspend fun getRepositories(organization: String): Response> { + return githubService.getRepositories(organization) + } +} diff --git a/app/src/main/java/nextstep/github/core/di/AppContainer.kt b/app/src/main/java/nextstep/github/di/AppContainer.kt similarity index 87% rename from app/src/main/java/nextstep/github/core/di/AppContainer.kt rename to app/src/main/java/nextstep/github/di/AppContainer.kt index e0b2975f..530a0905 100644 --- a/app/src/main/java/nextstep/github/core/di/AppContainer.kt +++ b/app/src/main/java/nextstep/github/di/AppContainer.kt @@ -1,10 +1,10 @@ -package nextstep.github.core.di +package nextstep.github.di import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import kotlinx.serialization.json.Json -import nextstep.github.core.data.GithubRepository -import nextstep.github.core.data.GithubRepositoryImpl -import nextstep.github.core.network.GithubService +import nextstep.github.data.repository.GithubRepository +import nextstep.github.data.repository.GithubRepositoryImpl +import nextstep.github.data.network.GithubService import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor diff --git a/app/src/main/java/nextstep/github/MainApplication.kt b/app/src/main/java/nextstep/github/di/MainApplication.kt similarity index 62% rename from app/src/main/java/nextstep/github/MainApplication.kt rename to app/src/main/java/nextstep/github/di/MainApplication.kt index 0cce1f91..91156bdd 100644 --- a/app/src/main/java/nextstep/github/MainApplication.kt +++ b/app/src/main/java/nextstep/github/di/MainApplication.kt @@ -1,7 +1,6 @@ -package nextstep.github +package nextstep.github.di import android.app.Application -import nextstep.github.core.di.AppContainer class MainApplication : Application() { diff --git a/app/src/main/java/nextstep/github/domain/entity/RepositoryEntity.kt b/app/src/main/java/nextstep/github/domain/entity/RepositoryEntity.kt new file mode 100644 index 00000000..100ebbd2 --- /dev/null +++ b/app/src/main/java/nextstep/github/domain/entity/RepositoryEntity.kt @@ -0,0 +1,13 @@ +package nextstep.github.domain.entity + +data class RepositoryEntity( + val fullName: String, + val description: String, + val stars: Int +) { + val isHot: Boolean by lazy { stars > HOT_COUNT } + + companion object { + private const val HOT_COUNT = 50 + } +} diff --git a/app/src/main/java/nextstep/github/domain/usecase/GetGithubRepoUseCase.kt b/app/src/main/java/nextstep/github/domain/usecase/GetGithubRepoUseCase.kt new file mode 100644 index 00000000..fb40eae6 --- /dev/null +++ b/app/src/main/java/nextstep/github/domain/usecase/GetGithubRepoUseCase.kt @@ -0,0 +1,26 @@ +package nextstep.github.domain.usecase + +import nextstep.github.data.repository.GithubRepository +import nextstep.github.domain.entity.RepositoryEntity +import nextstep.github.data.network.ApiResult + +class GetGithubRepoUseCase( + private val githubRepository: GithubRepository +) { + suspend operator fun invoke(organization: String): ApiResult> { + val response = githubRepository.getRepositories(organization) + + return if (response.isSuccessful && response.body() != null) { + val repositoryEntityList = response.body()!!.map { + RepositoryEntity( + fullName = it.fullName ?: "", + description = it.description ?: "", + stars = it.stars ?: 0 + ) + } + ApiResult.Success(repositoryEntityList) + } else { + ApiResult.Error(response.code(), Throwable(response.message())) + } + } +} diff --git a/app/src/main/java/nextstep/github/ui/screen/github/GithubViewModel.kt b/app/src/main/java/nextstep/github/ui/screen/github/GithubViewModel.kt new file mode 100644 index 00000000..faff55f2 --- /dev/null +++ b/app/src/main/java/nextstep/github/ui/screen/github/GithubViewModel.kt @@ -0,0 +1,69 @@ +package nextstep.github.ui.screen.github + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import nextstep.github.data.network.ApiResult +import nextstep.github.di.MainApplication +import nextstep.github.domain.usecase.GetGithubRepoUseCase +import nextstep.github.ui.screen.github.list.GithubRepositoryUiState + +class GithubViewModel( + private val getGithubRepoUseCase: GetGithubRepoUseCase +) : ViewModel() { + private val _uiState = MutableStateFlow(GithubRepositoryUiState.Loading) + val uiState: StateFlow = _uiState + + fun getRepositories(organization: String) { + + viewModelScope.launch(Dispatchers.IO) { + try { + _uiState.value = GithubRepositoryUiState.Loading + delay(300) + when (val result = getGithubRepoUseCase(organization)) { + is ApiResult.Success -> { + _uiState.value = GithubRepositoryUiState.Ready( + githubRepositories = result.value, + isError = false + ) + } + + is ApiResult.Error -> { + _uiState.value = GithubRepositoryUiState.Ready( + githubRepositories = emptyList(), + isError = true + ) + } + } + } catch (e: Exception) { + _uiState.value = GithubRepositoryUiState.Ready( + githubRepositories = emptyList(), + isError = true + ) + } + } + } + + companion object { + val Factory: ViewModelProvider.Factory = viewModelFactory { + initializer { + val githubRepository = (this[APPLICATION_KEY] as MainApplication) + .appContainer + .githubRepository + + val getGithubRepoUseCase = GetGithubRepoUseCase(githubRepository) + + GithubViewModel(getGithubRepoUseCase) + } + } + } +} diff --git a/app/src/main/java/nextstep/github/ui/screen/github/MainScreen.kt b/app/src/main/java/nextstep/github/ui/screen/github/MainScreen.kt index 71fae95c..f57d9c0a 100644 --- a/app/src/main/java/nextstep/github/ui/screen/github/MainScreen.kt +++ b/app/src/main/java/nextstep/github/ui/screen/github/MainScreen.kt @@ -2,69 +2,89 @@ package nextstep.github.ui.screen.github import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel import nextstep.github.R -import nextstep.github.core.data.GithubRepositoryInfo -import nextstep.github.ui.screen.github.component.MainTopBar +import nextstep.github.domain.entity.RepositoryEntity import nextstep.github.ui.screen.github.list.GithubRepositoryUiState -import nextstep.github.ui.screen.github.list.component.ErrorSnackbar import nextstep.github.ui.screen.github.list.component.GithubRepositoryEmpty import nextstep.github.ui.screen.github.list.component.GithubRepositoryList import nextstep.github.ui.screen.github.list.component.LoadingProgress +import nextstep.github.ui.screen.github.list.component.MainTopBar @Composable fun MainScreen( - uiState: GithubRepositoryUiState = GithubRepositoryUiState.Loading, + viewModel: GithubViewModel = viewModel(), +) { + // stateful + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val snackbarHostState = remember { SnackbarHostState() } + + MainScreen( + uiState = uiState, + snackbarHostState = snackbarHostState, + onRetry = { viewModel.getRepositories("next-step") } + ) +} + +@Composable +fun MainScreen( + uiState: GithubRepositoryUiState, snackbarHostState: SnackbarHostState, - onClickSnackBar: () -> Unit = {} + modifier: Modifier = Modifier, + onRetry: () -> Unit = {} ) { + // stateless Scaffold( - topBar = { MainTopBar() } + topBar = { MainTopBar() }, + snackbarHost = { SnackbarHost(hostState = snackbarHostState) } ) { paddingValues -> when (uiState) { is GithubRepositoryUiState.Loading -> { - // 로딩 화면 LoadingProgress( modifier = Modifier.padding(paddingValues) ) } - is GithubRepositoryUiState.Error -> { - // 에러 화면 - LoadingProgress( - modifier = Modifier.padding(paddingValues), - snackBar = { - ErrorSnackbar( - errorMessage = stringResource(id = R.string.text_snackbar_network_error), - actionString = stringResource(id = R.string.text_snackbar_action_retry), - snackbarHostState = snackbarHostState, - modifier = Modifier.padding(paddingValues), - onClickAction = { onClickSnackBar() } + is GithubRepositoryUiState.Ready -> { + if (uiState.isError) { + val errorMsg = stringResource(id = R.string.text_snackbar_network_error) + val actionLabel = stringResource(id = R.string.text_snackbar_action_retry) + + LaunchedEffect(snackbarHostState) { + val snackbarResult = snackbarHostState.showSnackbar( + message = errorMsg, + actionLabel = actionLabel, ) - } - ) - } - is GithubRepositoryUiState.Empty -> { - GithubRepositoryEmpty( - modifier = Modifier.padding(paddingValues) - ) - } + if (snackbarResult == SnackbarResult.ActionPerformed) { + onRetry() + } + } + } - is GithubRepositoryUiState.Success -> { - GithubRepositoryList( - githubRepositoryInfoList = uiState.githubRepositories, - modifier = Modifier.padding(paddingValues) - ) + if (uiState.isEmpty) { + GithubRepositoryEmpty( + modifier = Modifier.padding(paddingValues) + ) + } else { + GithubRepositoryList( + repositoryEntityList = uiState.githubRepositories, + modifier = Modifier.padding(paddingValues) + ) + } } - - } } } @@ -82,7 +102,10 @@ private fun LoadingMainScreenPreview() { @Composable private fun EmptyMainScreenPreview() { MainScreen( - uiState = GithubRepositoryUiState.Empty, + uiState = GithubRepositoryUiState.Ready( + githubRepositories = emptyList(), + isError = true + ), snackbarHostState = remember { SnackbarHostState() } ) } @@ -91,26 +114,33 @@ private fun EmptyMainScreenPreview() { @Composable private fun SuccessMainScreenPreview() { val githubRepositoryList = listOf( - GithubRepositoryInfo( + RepositoryEntity( fullName = "next-step/nextstep-study", - description = "코드숨과 함께하는 NextStep" + description = "코드숨과 함께하는 NextStep", + stars = 2 ), - GithubRepositoryInfo( + RepositoryEntity( fullName = "next-step/nextstep-study", - description = "코드숨과 함께하는 NextStep" + description = "코드숨과 함께하는 NextStep", + stars = 20 ), - GithubRepositoryInfo( + RepositoryEntity( fullName = "next-step/nextstep-study", - description = "코드숨과 함께하는 NextStep" + description = "코드숨과 함께하는 NextStep", + stars = 2 ), - GithubRepositoryInfo( + RepositoryEntity( fullName = "next-step/nextstep-study", - description = "코드숨과 함께하는 NextStep" + description = "코드숨과 함께하는 NextStep", + stars = 3 ) ) MainScreen( - uiState = GithubRepositoryUiState.Success(githubRepositoryList), + uiState = GithubRepositoryUiState.Ready( + githubRepositories = githubRepositoryList, + isError = false + ), snackbarHostState = remember { SnackbarHostState() } ) } diff --git a/app/src/main/java/nextstep/github/ui/screen/github/list/GithubRepositoryUiState.kt b/app/src/main/java/nextstep/github/ui/screen/github/list/GithubRepositoryUiState.kt index fec92ba2..ecd5a322 100644 --- a/app/src/main/java/nextstep/github/ui/screen/github/list/GithubRepositoryUiState.kt +++ b/app/src/main/java/nextstep/github/ui/screen/github/list/GithubRepositoryUiState.kt @@ -1,15 +1,14 @@ package nextstep.github.ui.screen.github.list -import nextstep.github.core.data.GithubRepositoryInfo +import nextstep.github.domain.entity.RepositoryEntity sealed class GithubRepositoryUiState { - - data object Error : GithubRepositoryUiState() - data object Loading : GithubRepositoryUiState() - - data object Empty : GithubRepositoryUiState() - - data class Success(val githubRepositories: List) : - GithubRepositoryUiState() + data class Ready( + val githubRepositories: List, + val isError: Boolean = false + ) : GithubRepositoryUiState() { + val isEmpty: Boolean + get() = githubRepositories.isEmpty() + } } diff --git a/app/src/main/java/nextstep/github/ui/screen/github/list/component/GithubRepositoryList.kt b/app/src/main/java/nextstep/github/ui/screen/github/list/component/GithubRepositoryList.kt index b9383fc9..f37b7d3f 100644 --- a/app/src/main/java/nextstep/github/ui/screen/github/list/component/GithubRepositoryList.kt +++ b/app/src/main/java/nextstep/github/ui/screen/github/list/component/GithubRepositoryList.kt @@ -5,19 +5,19 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview -import nextstep.github.core.data.GithubRepositoryInfo +import nextstep.github.domain.entity.RepositoryEntity @Composable fun GithubRepositoryList( - githubRepositoryInfoList: List, + repositoryEntityList: List, modifier: Modifier = Modifier ) { LazyColumn( modifier = modifier.fillMaxWidth() ) { - items(githubRepositoryInfoList.size) { + items(repositoryEntityList.size) { RepositoryColumn( - repositoryInfo = githubRepositoryInfoList[it], + repositoryInfo = repositoryEntityList[it], modifier = Modifier ) } @@ -28,26 +28,30 @@ fun GithubRepositoryList( @Composable private fun GithubRepositoryListPreview() { val githubRepositoryList = listOf( - GithubRepositoryInfo( + RepositoryEntity( fullName = "next-step/nextstep-study", - description = "코드숨과 함께하는 NextStep" + description = "코드숨과 함께하는 NextStep", + stars = 3 ), - GithubRepositoryInfo( + RepositoryEntity( fullName = "next-step/nextstep-study", - description = "코드숨과 함께하는 NextStep" + description = "코드숨과 함께하는 NextStep", + stars = 2 ), - GithubRepositoryInfo( + RepositoryEntity( fullName = "next-step/nextstep-study", - description = "코드숨과 함께하는 NextStep" + description = "코드숨과 함께하는 NextStep", + stars = 20 ), - GithubRepositoryInfo( + RepositoryEntity( fullName = "next-step/nextstep-study", - description = "코드숨과 함께하는 NextStep" + description = "코드숨과 함께하는 NextStep", + stars = 2 ) ) GithubRepositoryList( - githubRepositoryInfoList = githubRepositoryList, + repositoryEntityList = githubRepositoryList, modifier = Modifier ) } diff --git a/app/src/main/java/nextstep/github/ui/screen/github/component/MainTopBar.kt b/app/src/main/java/nextstep/github/ui/screen/github/list/component/MainTopBar.kt similarity index 93% rename from app/src/main/java/nextstep/github/ui/screen/github/component/MainTopBar.kt rename to app/src/main/java/nextstep/github/ui/screen/github/list/component/MainTopBar.kt index e2722856..c023e15d 100644 --- a/app/src/main/java/nextstep/github/ui/screen/github/component/MainTopBar.kt +++ b/app/src/main/java/nextstep/github/ui/screen/github/list/component/MainTopBar.kt @@ -1,4 +1,4 @@ -package nextstep.github.ui.screen.github.component +package nextstep.github.ui.screen.github.list.component import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.ExperimentalMaterial3Api diff --git a/app/src/main/java/nextstep/github/ui/screen/github/list/component/RepositoryColumn.kt b/app/src/main/java/nextstep/github/ui/screen/github/list/component/RepositoryColumn.kt index 757d5026..0afe8ec3 100644 --- a/app/src/main/java/nextstep/github/ui/screen/github/list/component/RepositoryColumn.kt +++ b/app/src/main/java/nextstep/github/ui/screen/github/list/component/RepositoryColumn.kt @@ -11,15 +11,17 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import nextstep.github.core.data.GithubRepositoryInfo +import nextstep.github.domain.entity.RepositoryEntity @Composable -fun RepositoryColumn(repositoryInfo: GithubRepositoryInfo, modifier: Modifier = Modifier) { +fun RepositoryColumn(repositoryInfo: RepositoryEntity, modifier: Modifier = Modifier) { Column( modifier = modifier .padding(vertical = 16.dp) .fillMaxWidth() ) { + RepositoryColumnHeader(repositoryInfo = repositoryInfo) + Text( text = repositoryInfo.fullName, style = MaterialTheme.typography.titleLarge, @@ -44,9 +46,10 @@ fun RepositoryColumn(repositoryInfo: GithubRepositoryInfo, modifier: Modifier = @Composable private fun RepositoryColumnPreview() { RepositoryColumn( - repositoryInfo = GithubRepositoryInfo( + repositoryInfo = RepositoryEntity( fullName = "next-step/nextstep-study", - description = "코드숨과 함께하는 NextStep" + description = "코드숨과 함께하는 NextStep", + stars = 3 ) ) } diff --git a/app/src/main/java/nextstep/github/ui/screen/github/list/component/RopositoryColoumnHeader.kt b/app/src/main/java/nextstep/github/ui/screen/github/list/component/RopositoryColoumnHeader.kt new file mode 100644 index 00000000..3cf8e9c8 --- /dev/null +++ b/app/src/main/java/nextstep/github/ui/screen/github/list/component/RopositoryColoumnHeader.kt @@ -0,0 +1,62 @@ +package nextstep.github.ui.screen.github.list.component + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Star +import androidx.compose.material3.Icon +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.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import nextstep.github.R +import nextstep.github.domain.entity.RepositoryEntity + +@Composable +fun RepositoryColumnHeader( + repositoryInfo: RepositoryEntity, + modifier: Modifier = Modifier +) { + Row( + modifier = Modifier + .padding(horizontal = 16.dp) + ) { + if(repositoryInfo.isHot) { + Text( + modifier = Modifier.align(Alignment.CenterVertically), + text = stringResource(id = R.string.text_hot), + style = MaterialTheme.typography.labelLarge, + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + Icon( + imageVector = Icons.Filled.Star, + contentDescription = stringResource(id = R.string.text_description_repository_star_icon) + ) + + Text( + modifier = Modifier.align(Alignment.CenterVertically), + text = "${repositoryInfo.stars}", + style = MaterialTheme.typography.labelLarge, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun RepositoryColumnHeaderPreview() { + RepositoryColumnHeader( + repositoryInfo = RepositoryEntity( + fullName = "next-step/nextstep-study", + description = "코드숨과 함께하는 NextStep", + stars = 32 + ) + ) +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c35dba8a..10675077 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,4 +4,6 @@ 목록이 비었습니다. 예상치 못한 오류가 발생했습니다 재시도 + HOT + NEW