ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 5. Compose와 MVVM
    안드로이드 학습/Compose 2025. 4. 1. 16:55

    아키텍처를 공부하면서 자주 사용하던 것이 MVVM 패턴이었다.

     

    UI와 데이터 관리를 Activity / Fragment와 ViewModel 그리고 Repository등으로 나눠서 관리해 하나의 class가 하나의 책임만 갖도록 하는 것이 좋다는 것을 유지보수나 협업적인 측면에서 좋다는 것을 학습했었다.

     

    Compose도 마찬가지다. 단지 UI 부분이 xml을 사용하지 않는 것으로 변경되었을뿐 아키텍처 패턱을 적용해 각자의 역할을 분담하도록 하는 것이 좋을 것 같다. 

     

    Compose 자료들을 보니 MVI 아키텍처와 많이 사용한다고 하지만 아직 MVI를 학습하기 전이기 때문에 MVVM과 함께 사용하는 것을 먼저 학습하고 추후 MVI와 같이 사용하는 것을 학습해봐야 겠다. 

     

    예제 코드 :

    실제로 서버와 통신하는 부분은 만들지 않고 간단하게 통신하는 것 같은 느낌으로 만들었다. 

     

    먼저 DataLayer 부분을 보다

    data class ComposeBasicUser(
        val name: String,
        val age: Int
    )
    
    interface ComposeBasicUserRepository {
        suspend fun getUsers(): List<ComposeBasicUser>
        suspend fun addUser(user: ComposeBasicUser): List<ComposeBasicUser>
        suspend fun deleteUser(user: ComposeBasicUser): List<ComposeBasicUser>
        suspend fun clearUsers(): List<ComposeBasicUser>
    }
    class ComposeBasicUserRepositoryImpl : ComposeBasicUserRepository {
        private var users = mutableListOf<ComposeBasicUser>()
    
        override suspend fun getUsers(): List<ComposeBasicUser> {
            delay(200)  // Simulating network delay
            return users
        }
    
        override suspend fun addUser(user: ComposeBasicUser): List<ComposeBasicUser> {
            delay(200)
            users.add(user)
            return users
        }
    
        override suspend fun deleteUser(user: ComposeBasicUser): List<ComposeBasicUser> {
            users.remove(user)
            return users
        }
    
        override suspend fun clearUsers(): List<ComposeBasicUser> {
            delay(200)
            users.clear()
            return users
        }
    }
    

     

     간단하게 CRUD에서 Update 부분 정도만 제외하고 데이터를 서버에서 가져오는 것처럼 만들었다. 

     

    UI 부분

     

    ViewModel

     

    viewmodel부분에서는 UI에서 사용할 데이터를 갖고있다. 어떤 예제에는 데이터에 State를 사용하는 예제도 있었으나 대부분의 예제에서는 StateFlow를 사용하기 때문에 여기서도 StateFlow를 사용했다. 

    class ComposeBasicUserViewModel : ViewModel() {
        private var composeBasicUserRepository: ComposeBasicUserRepository =
            ComposeBasicUserRepositoryImpl()
    
        // UI state
        private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
        val uiState: StateFlow<UiState> = _uiState.asStateFlow()
    
        // Search query and filtered users
        private val _filteredUsers = MutableStateFlow<List<ComposeBasicUser>>(emptyList())
        val filteredUsers: StateFlow<List<ComposeBasicUser>> = _filteredUsers.asStateFlow()
    
        init {
            addAllUser(getTestData())
            fetchUsers()
        }
    
        // Fetch users from the repository
        private fun fetchUsers() {
            viewModelScope.launch {
                _uiState.value = UiState.Loading  // Set loading state
                try {
                    var users: List<ComposeBasicUser>
                    withContext(Dispatchers.IO) {
                        users = composeBasicUserRepository.getUsers()  // Fetch users
                    }
                    _uiState.value = UiState.Success(
                        users,
                        users.isEmpty() // true - empty, false - not empty
                    )
                    _filteredUsers.value = users  // Emit the fetched users
                } catch (e: Exception) {
                    _uiState.value = UiState.Error(e.message ?: "Unknown error")  // Handle error
                }
            }
        }
    
        private fun addUser(user: ComposeBasicUser) {
            viewModelScope.launch(Dispatchers.Main) {
                _uiState.value = UiState.Loading  // Set loading state
                try {
                    var users: List<ComposeBasicUser>
                    withContext(Dispatchers.IO) {
                        users = composeBasicUserRepository.addUser(user)  // Fetch users
                    }
                    _uiState.value = UiState.Success(
                        users,
                        users.isEmpty() // true - empty, false - not empty
                    )
                    _filteredUsers.value = users  // Emit the fetched users
                } catch (e: Exception) {
                    _uiState.value = UiState.Error(e.message ?: "Unknown error")  // Handle error
                }
            }
        }
    
        private fun addAllUser(userList: List<ComposeBasicUser>) {
            for (user in userList) {
                addUser(user)
            }
        }
    
        fun deleteUser(user: ComposeBasicUser) {
            viewModelScope.launch(Dispatchers.Main) {
                _filteredUsers.value = emptyList()
                try {
                    var users: List<ComposeBasicUser>
                    withContext(Dispatchers.IO) {
                        users = composeBasicUserRepository.deleteUser(user)  // Fetch users
                    }
                    _filteredUsers.value = users // Emit the fetched users
                    _uiState.value = UiState.Success(
                        users,
                        users.isEmpty() // true - empty, false - not empty
                    )
                } catch (e: Exception) {
                    _uiState.value = UiState.Error(e.message ?: "Unknown error")  // Handle error
                }
            }
        }
    
        private fun getTestData(): List<ComposeBasicUser> {
            return listOf(
                ComposeBasicUser("A", 19),
                ComposeBasicUser("B", 33),
                ComposeBasicUser("C", 26),
                ComposeBasicUser("D", 27),
                ComposeBasicUser("E", 38),
            )
        }
    }

     

     

    Compose UI 부분 :

     

    viewmodel에서 StateFlow로 가져온 것을 Compose에서는 collectAsState() 메소드로 가져온다. 

    @Composable
    fun ComposeBasicUserScreen(viewModel: ComposeBasicUserViewModel) {
    
        val uiState by viewModel.uiState.collectAsState()  // Collect the UI state
        val filteredUsers by viewModel.filteredUsers.collectAsState()  // Collect filtered users
    
        when (val state = uiState) {
            is UiState.Loading -> {
                LoadingScreen()
            }
    
            is UiState.Success -> {
                Log.d("ComposeBasicUserScreen", "in Success")
    
                if (state.isEmpty) {
                    Text(text = "User list is Empty")
                } else {
                    LazyColumn(
                        modifier = Modifier.padding(top = 10.dp, bottom = 10.dp),
                        verticalArrangement = Arrangement.spacedBy(10.dp), // 아이템 사이의 간격
                        horizontalAlignment = Alignment.CenterHorizontally
                    ) {
                        items(filteredUsers) { user ->
                            UserItem(
                                user,
                                onDeleteUser = {
                                    viewModel.deleteUser(user)
                                    Log.d("ComposeBasicUserScreen", "onDeleteButtonClicked")
                                }
                            )  // Display user items
                        }
                    }
                }
            }
    
            is UiState.Error -> {
                Text(text = (uiState as UiState.Error).message)  // Show error message
            }
        }
    }
    
    @Composable
    fun UserItem(user: ComposeBasicUser, onDeleteUser: () -> Unit) {
        Row(
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.Center,
            modifier = Modifier.fillMaxWidth()
        ) {
            Text(text = user.name)
            Box(
                modifier = Modifier
                    .clip(RoundedCornerShape(8.dp)) // 버튼 모서리 둥글게
                    .background(Brush.horizontalGradient(listOf(Color.Red, Color.Magenta)))
                    .padding(8.dp)
                    .clickable { onDeleteUser() }
            ) {
                Text(
                    text = "삭제",
                    color = Color.White,
                    modifier = Modifier.align(Alignment.Center)
                )
            }
        }
    }
Designed by Tistory.