π‘ν΄λΉ κΈμ μ± μν€ν μ² κ°μ΄λλ₯Ό μ΄ν΄νκΈ° μ½κ² μ 리ν κΈμ λλ€.
μ΄ κΈμ λ€μκ³Ό κ°μ μ μμ§μμ νμλ‘ν©λλ€.
UI Layerλ Data Layerμμ κ°μ Έμ¨ λ°μ΄ν°λ₯Ό μνλ‘ μ μ₯νκ³ νλ©΄μ λ λλ§νλ μμμ λλ€.
μ΄λ²€νΈ(ν΄λ¦ μ΄λ²€νΈλ μΉ΄μΉ΄μ€ν‘ λ©μΈμ§μ κ°μ λ€νΈμν¬ μλ΅)λ₯Ό μ²λ¦¬νκ³ μνκ° λ³κ²½λλ©΄ νλ©΄μ΄ μλμΌλ‘ μ λ°μ΄νΈ λμ΄μΌν©λλ€.
State Holder(ViewModel)λ νλ©΄μ 그리λλ° νμν λͺ¨λ μνλ₯Ό κ°μ§κ³ μμ΄μΌν©λλ€.
(μ¬κΈ°μμ μνλ, ViewModelμμ κ°μ§κ³ μλ λ³μλ€μ΄λΌκ³ μ΄ν΄ν΄λ λΉμ₯μ ν¬κ² λ¬Έμ λμ§ μμ΅λλ€.)
νμ§λ§ μΌλ°μ μΌλ‘ Data Layerμμ κ°μ Έμ¨ λ°μ΄ν°λ νλ©΄μμ νμλ‘νλ λ°μ΄ν°μ μμ ν κ°μ§λ μμ νμμ λλ€. μ€μ λ‘ λ€μν λ°μ΄ν°λ₯Ό μ‘°ν©ν΄μ μ¬μ©ν΄μΌνλ κ²½μ°κ° λ§μ΅λλ€. λν μ λ ¬ 쑰건과 κ°μ΄ UI Elements(Activity, Fragment)μμ λ°μμ€λ λ°μ΄ν°λ μ‘΄μ¬ν©λλ€.
UI Layerλ λ€μμ κ³Όμ μ λ°λ³΅μ μΌλ‘ μ€νν©λλ€.
μ΄μ λΆν°λ ν΄λΉ λμλ°©μμ ꡬ체μ μΌλ‘ μ΄λ»κ² ꡬννλμ§ μμ보λλ‘ νκ² μ΅λλ€.
data class NewsUiState(
val isSignedIn: Boolean = false,
val isPremium: Boolean = false,
val newsItems: List<NewsItemUiState> = listOf(),
val userMessages: List<Message> = listOf()
)
data class NewsItemUiState(
val title: String,
val body: String,
val bookmarked: Boolean = false,
...
)
class NewsViewModel(...) : ViewModel() {
private val _uiState = MutableStateFlow(NewsUiState())
val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()
...
}
ViewModelμμ λ³κ²½ κ°λ₯ν μ€νΈλ¦Όμ μμ±νκ³ λ³κ²½ λΆκ°λ₯ν μ€νΈλ¦ΌμΌλ‘ λ ΈμΆν©λλ€.
μ΄λ κ²νλ μ΄μ λ Activity, Fragmentμμ μμλ‘ λ³κ²½νλ κ²μ λ°©μ§νκΈ° μν¨μ λλ€.
StateFlow λμ LiveDataλ‘ κ΅¬ν κ°λ₯νμ§λ§, StateFlowλ‘ DataBindingμ΄ κ°λ₯ν΄μ§ μ΄νλ‘, μ μ°¨ StateFlowκ° λ§μ΄ μ°μ΄λ μΆμΈμ λλ€.
μνλ State Holder(ViewModel)μ λΆλ³μ±(val)μΌλ‘ μ μ₯λ©λλ€. λ³κ²½ λΆκ°λ₯ν κ°μ²΄κ° μκ°μ μ ν리μΌμ΄μ μνλ₯Ό 보μ₯νκΈ° λλ¬Έμ UI Elements(Activity, Fragment)λ νλ©΄μ μ λ°μ΄νΈνλ ν κ°μ§ μνμ μ§μ€ν μ μμ΅λλ€.
λν, 리μ€νΈ μ λ ¬ λ°©λ²κ³Ό κ°μ΄ UI Elementsμμλ§ μμ λ μ μλ λ°μ΄ν°λ₯Ό μ μΈνκ³ λ UI Elements(Activity, Fragment)μμ μνλ₯Ό μ§μ μμ ν΄μλ μλ©λλ€. μ΄ μμΉμ μλ°νλ©΄ μ¬λ¬ λ°μ΄ν° μμ€μμμ λ°μ΄ν° λΆμΌμΉμ λ―ΈμΈν λ²κ·Έκ° λ°μν μ μμ΅λλ€.
곡μλ¬Έμμ κ°μ΄λμμλ νλ©΄μ κΈ°λ₯μ΄λ λ¬μ¬λλ νλ©΄μ λΆλΆμ λ°λΌ μν ν΄λμ€μ μ΄λ¦μ λ€μκ³Ό κ°μ΄ μ§μ νλλ‘ κΆμ₯ν©λλ€.
κΈ°λ₯ + UiState
ex) λ΄μ€λ₯Ό νμνλ νλ©΄μ μν : NewsUiState
ex) λ΄μ€ νλͺ©μ μν : NewsItemUiState
μνκ° μλλ‘ ν₯νκ³ μ΄λ²€νΈλ μλ‘ ν₯νλ ν¨ν΄μ λ¨λ°©ν₯ λ°μ΄ν° νλ¦(UDF)μ΄λΌκ³ ν©λλ€.
μ΄ ν¨ν΄μ΄ μ± μν€ν
μ²μ λ―ΈμΉλ μν₯μ λ€μκ³Ό κ°μ΅λλ€.
UI Elements(Activity, Fragment)μμ μνλ₯Ό μ¬μ©νλ €λ©΄ κ΄μ°° κ°λ₯ν λ°μ΄ν° νλμ ν°λ―Έλ μ°μ°μλ₯Ό μ΄μ©ν΄μΌν©λλ€.
LiveDataμ κ²½μ° observe()ν¨μκ° μκ³ StateFlowμ κ²½μ° collect()ν¨μλ μ΄λ₯Ό νμ₯ν ν¨μλ€μ μ¬μ©ν©λλ€.
UI Elements(Activity, Fragment)μμ κ΄μ°° κ°λ₯ν λ°μ΄ν° νλλ₯Ό μ¬μ©ν λλ UI Elements(Activity, Fragment)μ μλͺ μ£ΌκΈ°λ₯Ό κ³ λ €ν΄μΌ ν©λλ€. μλͺ μ£ΌκΈ°λ₯Ό κ³ λ €ν΄μΌ νλ μ΄μ λ μ¬μ©μμκ² νλ©΄μ΄ νμλμ§ μμ λ UI Elements(Activity, Fragment)κ° μνλ₯Ό κ΄μ°°ν΄μλ μ λκΈ° λλ¬Έμ λλ€.
LiveDataμ κ²½μ° LifecycleOwnerκ° μλͺ
μ£ΌκΈ°μ λ¬Έμ λ₯Ό μμμ μΌλ‘ μ²λ¦¬ν©λλ€.
StateFlowμ κ²½μ° μ μ ν μ½λ£¨ν΄ λ²μμ repeatOnLifecycle APIλ‘ μ²λ¦¬νλ κ²μ΄ κ°μ₯ μ’μ΅λλ€.
class NewsActivity : AppCompatActivity() {
private val viewModel: NewsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
...
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect {
// Update UI elements
}
}
}
}
}
UiState ν΄λμ€μ λ‘λ μνλ₯Ό λνλ΄λ κ°λ¨ν λ°©λ²μ Boolean
κ°μ μ¬μ©νλ κ²μ
λλ€.
data class NewsUiState(
val isFetchingArticles: Boolean = false,
...
)
``
```kotlin
class NewsActivity : AppCompatActivity() {
private val viewModel: NewsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
...
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
// Bind the visibility of the progressBar to the state
// of isFetchingArticles.
viewModel.uiState
.map { it.isFetchingArticles }
.distinctUntilChanged()
.collect { progressBar.isVisible = it }
}
}
}
}
μ§ν μ€μΈ μμ νμμ λΉμ·ν©λλ€. νμ§λ§ μ€λ₯μλ μ¬μ©μμκ² λ€μ μ λ¬νλ κ΄λ ¨ λ©μμ§ λλ μ€ν¨ν μμ μ λ€μ μλνλ κ΄λ ¨ μμ μ΄ ν¬ν¨λ μ μμ΅λλ€.
λ°λΌμ λ°μ΄ν°λ₯Ό κ°μ Έμ€κ³ μκ±°λ κ°μ Έμ€κ³ μμ§ μμ λμ μ€λ₯ 컨ν μ€νΈμ μ μ ν λ©νλ°μ΄ν°λ₯Ό νΈμ€ν νλ λ°μ΄ν° ν΄λμ€λ₯Ό μ¬μ©νμ¬ μ€λ₯ μνλ₯Ό λͺ¨λΈλ§ν΄μΌ ν μ μμ΅λλ€.
data class Message(val id: Long, val message: String)
data class NewsUiState(
val userMessages: List<Message> = listOf(),
...
)
μλμ λ€μ΄μ΄κ·Έλ¨μ μ΄λ²€νΈλ₯Ό μ΄λ»κ² μ²λ¦¬ν΄μΌ νλμ§μ λν κ²°μ νΈλ¦¬μ λλ€.
μλμ μμ λ νλ©΄μ λ³κ²½νλ λ°©λ²(UI λ‘μ§)κ³Ό λ°μ΄ν°λ₯Ό λ³κ²½νλ λ°©λ²(λΉμ¦λμ€ λ‘μ§)μ 보μ¬μ€λλ€.
class LatestNewsActivity : AppCompatActivity() {
private lateinit var binding: ActivityLatestNewsBinding
private val viewModel: LatestNewsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
// The expand details event is processed by the UI that
// modifies a View's internal state.
binding.expandButton.setOnClickListener {
binding.expandedSection.visibility = View.VISIBLE
}
// The refresh event is processed by the ViewModel that is in charge
// of the business logic.
binding.refreshButton.setOnClickListener {
viewModel.refreshNews()
}
}
}
μ΄λ²€νΈμ μ΄λ¦μ μ²λ¦¬νλ μμ μ λ°λΌ λμ¬λ₯Ό ν¬ν¨ν΄ μ§μ ν΄μ€λλ€.
ex) addBookmark(id), logIn(username, password)
μΌλ°μ μΌλ‘ λ²νΌμ λλ μλ νλ©΄ μ΄λ λ°©λ²μ λ€μκ³Ό κ°μ΅λλ€.
class LoginActivity : AppCompatActivity() {
private lateinit var binding: ActivityLoginBinding
private val viewModel: LoginViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
binding.helpButton.setOnClickListener {
navController.navigate(...) // Open help screen
}
}
}
λ§μ½, λ‘κ·ΈμΈ μνλ₯Ό νμΈνκ³ μλμΌλ‘ νλ©΄ μ΄λμ νκ³ μΆλ€λ©΄ λ€μκ³Ό κ°μ΄ ꡬνν μ μμ΅λλ€.
class LoginActivity : AppCompatActivity() {
private val viewModel: LoginViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { uiState ->
if (uiState.isUserLoggedIn) {
// Navigate to the Home screen.
}
...
}
}
}
}
}
νμ μ‘΄μ¬ν©λλ€.
μν μμ±μ μΆλ ₯μ λλ€.
μΌμμ μ΄κ³ μμΈ‘ν μ μμΌλ©° μΌμ κΈ°κ° μ‘΄μ¬ν©λλ€.
μν μμ±μ μ λ ₯μ λλ€.
μ λ΄μ©μ νλ§λλ‘ μμ½νλ©΄ μνλ μ‘΄μ¬νκ³ μ΄λ²€νΈλ λ°μνλ€λ κ²μ λλ€. μλμ λ€μ΄μ΄κ·Έλ¨μ νμλΌμΈμμ μ΄λ²€νΈκ° λ°μν λμ μν λ³κ²½μ μκ°ννμ¬ λ³΄μ¬μ€λλ€.
μνμ λν λ³κ²½μ Main λμ€ν¨μ²μμ νλ κ²μ΄ μ’μ΅λλ€.
λ°λΌμ λ°μ΄ν° λ³κ²½ μμ²μ λ³΄λΌ λμλ IO λμ€ν¨μ²λ‘ μ½λ£¨ν΄μ μ€ννκ³ withContext(Dispatchers.Main)λ₯Ό ν΅ν΄ 컨ν μ€νΈ μ€μμΉ ν μνλ₯Ό μ λ°μ΄νΈνλ κ²μ΄ μ’μ΅λλ€.
MutableStateFlowλ₯Ό λ³κ²½ν λμλ μΌλ°μ μΌλ‘ update
ν¨μλ₯Ό μ¬μ©ν©λλ€.
Data Layerμμ κ°μ Έμ¨ λ°μ΄ν°λ νλ©΄μμ νμλ‘νλ λ°μ΄ν°μ μμ ν κ°μ§λ μμ νμμ λλ€.
μ΄λ¬ν λ°μ΄ν°λ€μ μ λ ¬ 쑰건과 κ°μ΄ UI Elements(Activity, Fragment)μμ λ°μμ€λ λ°μ΄ν°λ₯Ό ν¬ν¨νμ¬ μ‘°ν©ν΄μ μ¬μ©ν μ μμ΅λλ€.
μλλ λ€μμ Flowλ₯Ό combineμΌλ‘ κ²°ν©ν ν stateInμ ν΅ν΄ κ΄μ°° κ°λ₯ν μνλ‘ λ³κ²½νλ μμ μ λλ€.
class TaskDetailViewModel @Inject constructor(
private val tasksRepository: TasksRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _isTaskDeleted = MutableStateFlow(false)
private val _task = tasksRepository.getTaskStream(taskId)
val uiState: StateFlow<TaskDetailUiState> = combine(
_isTaskDeleted,
_task
) { isTaskDeleted, task ->
TaskDetailUiState(
task = taskAsync.data,
isTaskDeleted = isTaskDeleted
)
}
// Convert the result to the appropriate observable API for the UI
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = TaskDetailUiState()
)
fun deleteTask() = viewModelScope.launch {
tasksRepository.deleteTask(taskId)
_isTaskDeleted.update { true }
}
}
https://developer.android.com/topic/architecture?hl=ko
https://developer.android.com/courses/android-basics-kotlin/course
https://github.com/android/sunflower/tree/main
https://fastcampus.co.kr/dev_red_ksr