프로그래밍/안드로이드

[안드로이드] Jetpack Compose에 MVVM 적용하기

강민재02 2024. 7. 24. 18:29
728x90

[안드로이드] 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과 연동합니다.

data binding layout으로 변경

<?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 패턴이 적용된 스톱워치 앱을 만들 수 있습니다.

 

스톱워치 화면 결과물, 3~69 369~

 

마치며

예시를 통해 알 수 있듯, Jetpack Compose에서는 데이터 바인딩 과정이 매우 매우 쉬워졌다는 것을 알 수 있었습니다. 개인적으로는 Jetpack Compose 업데이트는 매우 만족스럽고, 여러분들도 한번씩 써보셨으면 좋겠습니다.

 

사실, 이번 포스팅에서는 데이터 바인딩에 초점을 두다 보니 빠진 설명이 많습니다. 예시 코드에서 이건 왜 이렇게 썼지? 하는 부분이 많을 거라 예상되는데, 다음에는 그런 내용들을 보충하는 포스팅으로 돌아오겠습니다.

 

아마 안드로이드 디자인 패턴, Data Holder, 코루틴 등등 다양한 얘기들을 할 것 같습니다.

728x90