[안드로이드] Jetpack Compose에 MVVM 적용하기
[안드로이드] Jetpack Compose을 보고 오시면 더 좋습니다.
들어가며
이전 시간에는 Jetpack Compose를 이용하여 UI를 만드는 방법을 배웠습니다.
사실, Jetpack Compose의 진짜 장점은 MVVM 패턴과 결합했을 때 나온다고 생각하는데요. 그만큼 뷰와 데이터를 연결하기 쉽게 느껴졌기 때문인 것 같습니다.
기존 시스템과의 비교
xml 파일과 데이터를 연동하는 기존의 방식은 거쳐야 할 절차가 많았습니다. 기존의 방식의 복잡성을 알기 위해 저번 시간에 구현했던 스톱워치 애플리케이션을 xml로 변환하여, xml파일은 어떻게 뷰와 뷰 모델을 연결했는지 알아봅시다.
xml파일에서 데이터 바인딩
먼저 종속성을 추가합니다
...
buildFeatures {
dataBinding = true
}
MainActivty에서 Viewmodel과 View를 연결합니다.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: StopwatchViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Set up data binding
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
// Bind the ViewModel
binding.viewModel = viewModel
binding.lifecycleOwner = this
}
}
xml 파일에 들어가 Alt+Enter를 입력하여 data binding layout으로 변경 후, data 태그를 이용해서 viewModel과 연동합니다.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.example.stopwatch.StopwatchViewModel" />
</data>
...
</layout>
다음과 같이 연결된 viewmodel을 사용합니다.
...
<!-- Reset Button -->
<Button
android:id="@+id/resetButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Reset"
android:onClick="@{() -> viewModel.resetStopwatch()}" />
</LinearLayout>
MVVM 패턴이 적용된 스톱워치 코드
저번 포스팅에서 MainActivity에 모든 걸 때려 박았던 스톱워치 코드에 MVVM 패턴을 적용해 봅시다.
사실, 이번 포스팅에선 Model은 만들지 않고 뷰 모델을 뷰와 연동하는 것에 초점을 맞추겠습니다.
이번 예제를 통해 MVVM 패턴을 적용하는 과정과 Jetpack Compose를 활용한 데이터 바인딩의 편리성을 알아봅시다.
1) MainActivty
MainActivity에서 뷰 모델을 선언하고 컴포저블에게 ViewModel을 매개변수로 넘깁니다.
class MainActivity : ComponentActivity() {
private val viewModel : StopwatchViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
StopwatchApp(viewModel)
}
}
}
2) ViewModel
뷰에 필요한 기능이 구현된 뷰 모델입니다.
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class StopwatchViewModel : ViewModel() {
// MutableStateFlow를 사용하여 상태를 관리합니다.
private val _timeInMillis = MutableStateFlow(0L)
val timeInMillis: StateFlow<Long> = _timeInMillis
private val _isRunning = MutableStateFlow(false)
val isRunning: StateFlow<Boolean> = _isRunning
fun startStopwatch() {
if (_isRunning.value) return
_isRunning.value = true
val startTime = System.currentTimeMillis() - _timeInMillis.value
viewModelScope.launch {
while (_isRunning.value) {
_timeInMillis.value = System.currentTimeMillis() - startTime
delay(10L)
}
}
}
fun pauseStopwatch() {
_isRunning.value = false
}
fun resetStopwatch() {
_isRunning.value = false
_timeInMillis.value = 0L
}
}
3) MainScreen
필요한 ViewModel을 인자로 정의하여 사용합니다.
viewModel을 받는 컴포저블을 따로 둔 이유는 preview 기능을 사용하기 위함입니다.
@Composable
fun StopwatchApp(viewModel: StopwatchViewModel) {
StopwatchScreen(
elapsedTime = viewModel.timeInMillis.collectAsState().value,
onStart = { viewModel.startStopwatch() },
onPause = { viewModel.pauseStopwatch() },
onReset = { viewModel.resetStopwatch() },
isRunning = viewModel.isRunning.collectAsState().value
)
}
@Composable
fun StopwatchScreen(
elapsedTime: Long,
onStart: () -> Unit,
onPause: () -> Unit,
onReset: () -> Unit,
isRunning: Boolean
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Stopwatch",
fontSize = 32.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = formatTime(elapsedTime),
fontSize = 48.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(16.dp))
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(onClick = {
if (isRunning) onPause() else onStart()
}) {
Text(text = if (isRunning) "Pause" else "Start")
}
Button(onClick = onReset) {
Text(text = "Reset")
}
}
}
}
fun formatTime(timeInMillis: Long): String {
val milliseconds = timeInMillis % 1000 / 10
val seconds = (timeInMillis / 1000) % 60
val minutes = (timeInMillis / 1000) / 60
return "%02d:%02d:%02d".format(minutes, seconds, milliseconds)
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
StopwatchScreen(
elapsedTime = 0,
onStart = { /*TODO*/ },
onPause = { /*TODO*/ },
onReset = { /*TODO*/ },
isRunning = false
)
}
이렇게 하면, (M)VVM 패턴이 적용된 스톱워치 앱을 만들 수 있습니다.
마치며
예시를 통해 알 수 있듯, Jetpack Compose에서는 데이터 바인딩 과정이 매우 매우 쉬워졌다는 것을 알 수 있었습니다. 개인적으로는 Jetpack Compose 업데이트는 매우 만족스럽고, 여러분들도 한번씩 써보셨으면 좋겠습니다.
사실, 이번 포스팅에서는 데이터 바인딩에 초점을 두다 보니 빠진 설명이 많습니다. 예시 코드에서 이건 왜 이렇게 썼지? 하는 부분이 많을 거라 예상되는데, 다음에는 그런 내용들을 보충하는 포스팅으로 돌아오겠습니다.
아마 안드로이드 디자인 패턴, Data Holder, 코루틴 등등 다양한 얘기들을 할 것 같습니다.