관리 메뉴

막내의 막무가내 프로그래밍 & 일상

[안드로이드] 구글 공식 프로젝트 Sunflower 스터디 (6) MVVM - ViewModel 본문

안드로이드/코틀린 & 아키텍처 & Recent

[안드로이드] 구글 공식 프로젝트 Sunflower 스터디 (6) MVVM - ViewModel

막무가내막내 2021. 4. 27. 23:05
728x90

 

 

 

[참고]

github.com/android/sunflower

 

android/sunflower

A gardening app illustrating Android development best practices with Android Jetpack. - android/sunflower

github.com

developer.android.com/kotlin/flow/stateflow-and-sharedflow

 

StateFlow 및 SharedFlow  |  Android 개발자  |  Android Developers

StateFlow와 SharedFlow는 흐름에서 최적으로 상태 업데이트를 내보내고 여러 소비자에게 값을 내보낼 수 있는 Flow API입니다. StateFlow StateFlow는 현재 상태와 새로운 상태 업데이트를 수집기에 내보내

developer.android.com

developer.android.com/topic/libraries/architecture/coroutines?hl=ko

 

아키텍처 구성요소로 Kotlin 코루틴 사용  |  Android 개발자  |  Android Developers

Kotlin 코루틴은 비동기 코드를 작성할 수 있게 하는 API를 제공합니다. Kotlin 코루틴을 사용하면 코루틴이 실행되어야 하는 시기를 관리하는 데 도움이 되는 CoroutineScope를 정의할 수 있습니다. 각

developer.android.com

developer.android.com/kotlin/coroutines?hl=ko

 

Android의 Kotlin 코루틴  |  Android 개발자  |  Android Developers

코루틴은 비동기적으로 실행되는 코드를 간소화하기 위해 Android에서 사용할 수 있는 동시 실행 설계 패턴입니다. 코루틴은 Kotlin 버전 1.3에 추가되었으며 다른 언어에서 확립된 개념을 기반으로

developer.android.com

 

 

[ViewModel]

이전 챕터에서 View에 대해 보았는데 이어서 MVVM에서 비즈니스 로직을 담당하는 ViewModel 클래스를 살펴보려고한다. 

 


먼저 SPA(Single-Page-Application) 구조인 이 프로젝트에서 유일한 액티비티인 GardenActivity의 ViewModel 은 없다.

 

껍데기 역할만 함!

 


 

[PlantListViewModel]

@HiltViewModel
class PlantListViewModel @Inject internal constructor(
    plantRepository: PlantRepository,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    private val growZone: MutableStateFlow<Int> = MutableStateFlow(
        savedStateHandle.get(GROW_ZONE_SAVED_STATE_KEY) ?: NO_GROW_ZONE
    )

    val plants: LiveData<List<Plant>> = growZone.flatMapLatest { zone ->
        if (zone == NO_GROW_ZONE) {
            plantRepository.getPlants()
        } else {
            plantRepository.getPlantsWithGrowZoneNumber(zone)
        }
    }.asLiveData()

    init {
        viewModelScope.launch {
            growZone.collect { newGrowZone ->
                savedStateHandle.set(GROW_ZONE_SAVED_STATE_KEY, newGrowZone)
            }
        }
    }

    fun setGrowZoneNumber(num: Int) {
        growZone.value = num
    }

    fun clearGrowZoneNumber() {
        growZone.value = NO_GROW_ZONE
    }

    fun isFiltered() = growZone.value != NO_GROW_ZONE

    companion object {
        private const val NO_GROW_ZONE = -1
        private const val GROW_ZONE_SAVED_STATE_KEY = "GROW_ZONE_SAVED_STATE_KEY"
    }
}

 

PlantListFragment의 ViewModel인 PlantListViewModel 이다.

 

생성자로 MVVM의 Model 에 해당하는 Repository와 SaveStateHandle을 가지고 있다.

 

 

ViewModel의 데이터나 함수들은 코루틴과 연관이 많이 되어있다. 

(참고로 난 코루틴을 간단하게만 학습해봤고 Flow 를 공부하는 것은 처음이다.)

 

데이터는 MutableStateFlow 를 사용하는데 상태를 업데이트하고 Flow에 전송하는 것이다.

추가로 일반 Flow와 비교하면 Flow는 일반적으로 cold stream이지만, StateFlow hot stream다. 그러므로 일반 Flow는 마지막 값의 개념이 없고 collect 될 때만 활성화 되는 반면 StateFlow는 마지막 값의 개념이 있으며 생성하자마자 활성화된다고 한다.

 

처음 본 나의 느낌을 말하자면 관찰하고 스트림하고 데이터를 저장한다는 기능으로 기존의 LiveData + RxJava 같은 느낌이 들었다. (Flow는 Cold니 Subject, StateFlow는 Hot이니 Observable)

 

LiveData와 Flow의 여러 차이점이 있지만 그 중 하나가 위 코드 사진의 47라인 코드에서 보듯이 flatMapLatest 와 같은 Straem 함수들을 제공한다는 것이다. 

 

그리고 viewModelScope 라는게 보이는데 개념은 다음과 같다.

ViewModelScope는 앱의 각 ViewModel을 대상으로 정의됩니다. 이 범위에서 시작된 모든 코루틴은 ViewModel이 삭제되면 자동으로 취소됩니다. 코루틴은 ViewModel이 활성 상태인 경우에만 실행해야 할 작업이 있을 때 유용합니다. 예를 들어 레이아웃의 일부 데이터를 계산한다면 작업의 범위를 ViewModel로 지정하여 ViewModel을 삭제하면 리소스를 소모하지 않도록 작업이 자동으로 취소됩니다. 

- RxJava의 CompositeDisposable 와 좀 비슷하다고 느꼇다.

 

자세한 내용들은 다음 공식 문서 링크에서 볼 수 있다. 

StateFlow, Flow, LiveData에 대한 차이점도 적혀있고 Flow 에 대한 개념설명이 잘 되어 있다. 안드로이드 공식문서(developer.android.com/kotlin/flow/stateflow-and-sharedflow)와 여러 블로그에(yoon-dailylife.tistory.com/72) Flow 기본에 대해 다음과 같이 설명이 잘 되어 있어 따로 설명은 생략하겠다.

 

viewModelScope는 다음 링크에서 볼 수 있다. developer.android.com/topic/libraries/architecture/coroutines?hl=ko

공식문서 짱짱!

 

StateFlow, MutableStateFlow란? _uiState, uiState를 보면 기존 MutableLiveData, LiveData 두 개를 두는 것과 비슷해보인다.

 

기존의 LiveData와의 차이점

 

 

 

 

 


 

[PlantDetailViewModel]

PlantDetailFragment의 ViewModel인 PlantDetailViewModel이다.

/**
 * The ViewModel used in [PlantDetailFragment].
 */
class PlantDetailViewModel @AssistedInject constructor(
    plantRepository: PlantRepository,
    private val gardenPlantingRepository: GardenPlantingRepository,
    @Assisted private val plantId: String
) : ViewModel() {

    val isPlanted = gardenPlantingRepository.isPlanted(plantId).asLiveData()
    val plant = plantRepository.getPlant(plantId).asLiveData()

    fun addPlantToGarden() {
        viewModelScope.launch {
            gardenPlantingRepository.createGardenPlanting(plantId)
        }
    }

    fun hasValidUnsplashKey() = (BuildConfig.UNSPLASH_ACCESS_KEY != "null")

    companion object {
        fun provideFactory(
            assistedFactory: PlantDetailViewModelFactory,
            plantId: String
        ): ViewModelProvider.Factory = object : ViewModelProvider.Factory {
            @Suppress("UNCHECKED_CAST")
            override fun <T : ViewModel?> create(modelClass: Class<T>): T {
                return assistedFactory.create(plantId) as T
            }
        }
    }
}

@AssistedFactory
interface PlantDetailViewModelFactory {
    fun create(plantId: String): PlantDetailViewModel
}

@AssistedFactory나 @Assisted에 관해서는 이전의 Hilt, View 편 블로그 포스팅에서 다뤘으므로 생략하고 보면 크게 특이점은 없다.

Repository로부터 데이터를 불러오고 Flow의 확장함수인 asLiveData()를 사용하여 Flow -> LiveData로 변환시켜준다.

 

 


 

 

[GardenPlantingListViewModel]

 

GardenFragment의 ViewModel인 GardenPlantingListViewModel 이다.

@HiltViewModel
class GardenPlantingListViewModel @Inject internal constructor(
    gardenPlantingRepository: GardenPlantingRepository
) : ViewModel() {
    val plantAndGardenPlantings: LiveData<List<PlantAndGardenPlantings>> =
        gardenPlantingRepository.getPlantedGardens().asLiveData()
}

이게 끝이다. 할말은 딱히 없다. 

 

 

 


 

 

 

 

 

 

이상 Sunlfower의 ViewModel 관련 클래스들에 대해 알아봤다.

 

 

댓글과 공감은 큰 힘이 됩니다. 감사합니다. !!

728x90
Comments