-
Notifications
You must be signed in to change notification settings - Fork 12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
π 3λ¨κ³ - GitHub(UI μν) #33
base: simuelunbo
Are you sure you want to change the base?
Changes from 3 commits
a3b37f8
8326b49
ae15834
e1d476b
a00a8a9
fa8a784
120e696
54780cc
0005fc1
7671241
02bc6e1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,60 +1,80 @@ | ||
package nextstep.github.ui.github | ||
|
||
import android.util.Log | ||
import androidx.compose.foundation.layout.PaddingValues | ||
import androidx.compose.foundation.layout.fillMaxSize | ||
import androidx.compose.foundation.layout.padding | ||
import androidx.compose.foundation.lazy.LazyColumn | ||
import androidx.compose.foundation.lazy.items | ||
import androidx.compose.material3.CenterAlignedTopAppBar | ||
import androidx.compose.material3.Divider | ||
import androidx.compose.material3.ExperimentalMaterial3Api | ||
import androidx.compose.material3.HorizontalDivider | ||
import androidx.compose.material3.Scaffold | ||
import androidx.compose.material3.SnackbarHost | ||
import androidx.compose.material3.SnackbarHostState | ||
import androidx.compose.material3.SnackbarResult | ||
import androidx.compose.material3.Text | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.LaunchedEffect | ||
import androidx.compose.runtime.getValue | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.runtime.rememberCoroutineScope | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.platform.LocalContext | ||
import androidx.compose.ui.res.stringResource | ||
import androidx.compose.ui.tooling.preview.Preview | ||
import androidx.compose.ui.unit.dp | ||
import androidx.lifecycle.compose.collectAsStateWithLifecycle | ||
import androidx.lifecycle.viewmodel.compose.viewModel | ||
import kotlinx.coroutines.launch | ||
import nextstep.github.R | ||
import nextstep.github.model.GithubRepo | ||
import nextstep.github.ui.github.component.GithubRepoItem | ||
import nextstep.github.ui.github.component.GithubRepoLoading | ||
import nextstep.github.ui.github.component.GithubRepoSuccess | ||
import nextstep.github.ui.theme.GithubTheme | ||
|
||
@Composable | ||
fun GithubRepositoryListScreen( | ||
modifier: Modifier = Modifier, | ||
viewModel: GithubRepoViewModel = viewModel(factory = GithubRepoViewModel.Factory), | ||
) { | ||
val repositoryList by viewModel.githubRepositories.collectAsStateWithLifecycle() | ||
GithubRepositoryListScreen(repositories = repositoryList) | ||
val uiState by viewModel.githubUiState.collectAsStateWithLifecycle() | ||
val context = LocalContext.current | ||
val snackBarHostState = remember { SnackbarHostState() } | ||
val coroutineScope = rememberCoroutineScope() | ||
|
||
LaunchedEffect(true) { | ||
viewModel.errorFlow.collect { | ||
coroutineScope.launch { | ||
val snackBarResult = snackBarHostState.showSnackbar( | ||
message = context.getString(R.string.error_message), | ||
actionLabel = context.getString(R.string.error_retry) | ||
) | ||
if (snackBarResult == SnackbarResult.ActionPerformed) run { viewModel::retryGithubRepo } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. λ³΄ν΅ λ²μ μ§μ ν¨μλ μΈμ μ°μλμ?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ν΄λΉ μ½λκ° λμνλμ§ ν μ€νΈν μμλ μ½λλ₯Ό μ§λ΄λ μ’μκ±° κ°μμ :) |
||
} | ||
} | ||
} | ||
GithubRepositoryListScreen( | ||
uiState = uiState, | ||
snackBarHostState = snackBarHostState, | ||
) | ||
} | ||
|
||
@OptIn(ExperimentalMaterial3Api::class) | ||
@Composable | ||
private fun GithubRepositoryListScreen( | ||
repositories: List<GithubRepo>, | ||
uiState: GithubRepoUiState, | ||
snackBarHostState: SnackbarHostState, | ||
modifier: Modifier = Modifier | ||
) { | ||
Scaffold( | ||
topBar = { | ||
CenterAlignedTopAppBar( | ||
title = { Text("NEXTSTEP Repositories") } | ||
title = { Text(stringResource(R.string.top_bar_title)) } | ||
) | ||
} | ||
}, | ||
snackbarHost = { SnackbarHost(hostState = snackBarHostState)} | ||
) { | ||
LazyColumn( | ||
modifier = modifier | ||
.fillMaxSize() | ||
.padding(it), | ||
) { | ||
items(repositories) {repo -> | ||
GithubRepoItem(githubRepo = repo) | ||
HorizontalDivider() | ||
} | ||
if (uiState.isLoading) { | ||
GithubRepoLoading(modifier = Modifier.padding(it)) | ||
} | ||
GithubRepoSuccess( | ||
modifier = Modifier.padding(it), | ||
repositories = uiState.repositories | ||
) | ||
} | ||
} | ||
|
||
|
@@ -63,14 +83,50 @@ private fun GithubRepositoryListScreen( | |
private fun GithubRepositoryListScreenPreview() { | ||
GithubTheme { | ||
GithubRepositoryListScreen( | ||
repositories = listOf( | ||
GithubRepo(1, "NextStep/Test", "ν μ€νΈ μ μ₯μ"), | ||
GithubRepo(2, "NextStep/Test2", "ν μ€νΈ μ μ₯μ2"), | ||
GithubRepo(3, "NextStep/Test3", "ν μ€νΈ μ μ₯μ3"), | ||
GithubRepo(4, "NextStep/Test4", "ν μ€νΈ μ μ₯μ4"), | ||
GithubRepo(5, "NextStep/Test5", "ν μ€νΈ μ μ₯μ5"), | ||
GithubRepo(6, "NextStep/Test6", "ν μ€νΈ μ μ₯μ6"), | ||
uiState = GithubRepoUiState( | ||
isLoading = false, | ||
repositories = listOf( | ||
GithubRepo(1, "NextStep/Test", "ν μ€νΈ μ μ₯μ"), | ||
GithubRepo(2, "NextStep/Test2", "ν μ€νΈ μ μ₯μ2"), | ||
GithubRepo(3, "NextStep/Test3", "ν μ€νΈ μ μ₯μ3"), | ||
GithubRepo(4, "NextStep/Test4", "ν μ€νΈ μ μ₯μ4"), | ||
GithubRepo(5, "NextStep/Test5", "ν μ€νΈ μ μ₯μ5"), | ||
GithubRepo(6, "NextStep/Test6", "ν μ€νΈ μ μ₯μ6"), | ||
), | ||
), | ||
snackBarHostState = SnackbarHostState() | ||
) | ||
} | ||
} | ||
|
||
@Preview(showBackground = true) | ||
@Composable | ||
private fun GithubRepositoryListScreenErrorPreview() { | ||
val coroutineScope = rememberCoroutineScope() | ||
val snackBarHostState = remember { SnackbarHostState() } | ||
LaunchedEffect(Unit) { | ||
coroutineScope.launch { | ||
snackBarHostState.showSnackbar( | ||
message = "μλ¬ λ©μΈμ§", | ||
actionLabel = "μ¬μλ" | ||
) | ||
} | ||
} | ||
Comment on lines
+105
to
+114
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. μ€λ³΄λμ΄ μκ°νμλ Previewμ κΈ°μ€μ 무μμΈκ°μ :) μ΄λ²€νΈλ€μ ui ν μ€νΈλ‘ μμ±ν΄λ³΄λ©΄ μ΄λ¨κΉμ? :) |
||
GithubTheme { | ||
GithubRepositoryListScreen( | ||
uiState = GithubRepoUiState(isLoading = true), | ||
snackBarHostState = snackBarHostState | ||
) | ||
} | ||
} | ||
|
||
@Preview(showBackground = true) | ||
@Composable | ||
private fun GithubRepositoryListScreenLoadingPreview() { | ||
GithubTheme { | ||
GithubRepositoryListScreen( | ||
uiState = GithubRepoUiState(isLoading = true), | ||
snackBarHostState = SnackbarHostState() | ||
) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package nextstep.github.ui.github | ||
|
||
import nextstep.github.model.GithubRepo | ||
|
||
data class GithubRepoUiState ( | ||
val isLoading: Boolean = false, | ||
val repositories: List<GithubRepo> = emptyList(), | ||
) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,25 +5,46 @@ import androidx.lifecycle.ViewModelProvider | |
import androidx.lifecycle.viewModelScope | ||
import androidx.lifecycle.viewmodel.initializer | ||
import androidx.lifecycle.viewmodel.viewModelFactory | ||
import kotlinx.coroutines.flow.SharingStarted | ||
import kotlinx.coroutines.CoroutineExceptionHandler | ||
import kotlinx.coroutines.flow.MutableSharedFlow | ||
import kotlinx.coroutines.flow.MutableStateFlow | ||
import kotlinx.coroutines.flow.SharedFlow | ||
import kotlinx.coroutines.flow.StateFlow | ||
import kotlinx.coroutines.flow.flow | ||
import kotlinx.coroutines.flow.stateIn | ||
import kotlinx.coroutines.flow.asSharedFlow | ||
import kotlinx.coroutines.flow.asStateFlow | ||
import kotlinx.coroutines.flow.update | ||
import kotlinx.coroutines.launch | ||
import nextstep.github.GithubApplication | ||
import nextstep.github.data.repository.GithubRepository | ||
import nextstep.github.model.GithubRepo | ||
|
||
class GithubRepoViewModel( | ||
private val githubRepoRepository: GithubRepository, | ||
) : ViewModel() { | ||
|
||
val githubRepositories: StateFlow<List<GithubRepo>> = flow<List<GithubRepo>> { | ||
emit(githubRepoRepository.getRepositories("next-step")) | ||
}.stateIn( | ||
viewModelScope, | ||
SharingStarted.WhileSubscribed(), | ||
emptyList() | ||
) | ||
private val _githubUiState: MutableStateFlow<GithubRepoUiState> = MutableStateFlow(GithubRepoUiState()) | ||
val githubUiState: StateFlow<GithubRepoUiState> = _githubUiState.asStateFlow() | ||
|
||
private val _errorFlow = MutableSharedFlow<Throwable>() | ||
val errorFlow = _errorFlow.asSharedFlow() | ||
|
||
init { | ||
fetchRepositories() | ||
} | ||
|
||
private fun fetchRepositories() { | ||
viewModelScope.launch(CoroutineExceptionHandler { _, throwable -> | ||
_errorFlow.tryEmit(throwable) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. μλ¬κ° λ°μνλ€λ©΄, tryEmitμ΄ μ μλν κΉμ? :) |
||
}) { | ||
_githubUiState.update { it.copy(isLoading = true) } | ||
val repositories = githubRepoRepository.getRepositories("next-step") | ||
_githubUiState.update { it.copy(repositories = repositories, isLoading = false) } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. μ€λ³΄λμ΄ μκ°νλ UiStateλ₯Ό μ€κ³νλ κΈ°μ€μ΄ 무μμΈκ°μ? |
||
} | ||
} | ||
|
||
fun retryGithubRepo() { | ||
fetchRepositories() | ||
} | ||
|
||
|
||
companion object { | ||
val Factory: ViewModelProvider.Factory = viewModelFactory { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package nextstep.github.ui.github.component | ||
|
||
import androidx.compose.foundation.layout.Box | ||
import androidx.compose.foundation.layout.fillMaxSize | ||
import androidx.compose.material3.CircularProgressIndicator | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.ui.Alignment | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.tooling.preview.Preview | ||
|
||
@Composable | ||
fun GithubRepoLoading( | ||
modifier: Modifier = Modifier, | ||
) { | ||
Box( | ||
modifier = modifier.fillMaxSize(), | ||
contentAlignment = Alignment.Center | ||
) { | ||
CircularProgressIndicator() | ||
} | ||
} | ||
|
||
@Preview(showBackground = true) | ||
@Composable | ||
private fun GithubRepoLoadingPreview() { | ||
GithubRepoLoading() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package nextstep.github.ui.github.component | ||
|
||
import androidx.compose.foundation.layout.fillMaxSize | ||
import androidx.compose.foundation.lazy.LazyColumn | ||
import androidx.compose.foundation.lazy.items | ||
import androidx.compose.material3.HorizontalDivider | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.tooling.preview.Preview | ||
import nextstep.github.model.GithubRepo | ||
import nextstep.github.ui.theme.GithubTheme | ||
|
||
@Composable | ||
fun GithubRepoSuccess( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. GithubRepoSuccessλΌλ μ΄λ¦μ λ°μ΄ν° κ΄μ μ λ€μ΄λ°μλκΉμ? |
||
repositories: List<GithubRepo>, | ||
modifier: Modifier = Modifier, | ||
) { | ||
LazyColumn(modifier = modifier.fillMaxSize()) { | ||
items(repositories) { repo -> | ||
GithubRepoItem(githubRepo = repo) | ||
HorizontalDivider() | ||
} | ||
} | ||
} | ||
|
||
@Preview(showBackground = true) | ||
@Composable | ||
private fun GithubRepoSuccessPreview() { | ||
GithubTheme { | ||
GithubRepoSuccess( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 리μ€νΈκ° λΉμ΄μμλλ μ΄λ€ νλ©΄μ΄ λμ€λμ? |
||
repositories = listOf( | ||
GithubRepo(1, "NextStep/Test", "ν μ€νΈ μ μ₯μ"), | ||
GithubRepo(2, "NextStep/Test2", "ν μ€νΈ μ μ₯μ2"), | ||
GithubRepo(3, "NextStep/Test3", "ν μ€νΈ μ μ₯μ3"), | ||
GithubRepo(4, "NextStep/Test4", "ν μ€νΈ μ μ₯μ4"), | ||
GithubRepo(5, "NextStep/Test5", "ν μ€νΈ μ μ₯μ5"), | ||
GithubRepo(6, "NextStep/Test6", "ν μ€νΈ μ μ₯μ6"), | ||
) | ||
) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,6 @@ | ||
<resources> | ||
<string name="app_name">GitHub</string> | ||
<string name="top_bar_title">"NEXTSTEP Repositories"</string> | ||
<string name="error_message">μμμΉ λͺ»ν μ€λ₯κ° λ°μνμ΅λλ€.</string> | ||
<string name="error_retry">μ¬μλ</string> | ||
</resources> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
κ°μ μκ°μμλ λ€λ£¬ λ΄μ©μ΄κΈ΄νλ°,
https://medium.com/androiddevelopers/viewmodel-one-off-event-antipatterns-16a1da869b95
μ€λ³΄λμ μκ²¬μ΄ κΆκΈν©λλ€! (μκ²¬λ§ μ£Όμ λ μ’μκ±° κ°μμ!)