-
MVVM 적용기(2)Andorid 2022. 4. 2. 21:26
이전 글에서는 MVVM에 대한 대략적인 개념과 그것을 구현하기 위한 컴포넌트들에 대해 소개하였다.
이번에는 실제 코드를 통해 내가 MVVM을 어떻게 구현했는 지에 대해 보여주려고 한다.
내가 사이드 프로젝트로 시작한 어플에 MVVM을 적용했는데,
일반적으로는 Repository에 Room을 사용해서 내부데이터를 저장한다고 하지만,
내가 사용할 어플에서는 그 정도로 많은 양의 데이터를 저장할 필요가 없었고,
굳이 MVVM을 구현하기 위해서 Room을 사용하는 것 자체가
소 잡는데 닭 잡는 칼을 쓰는 모양새가 되는 것 같아서,
이전에 사용하던 SharedPreperence를 그대로 사용하기로 했다.
서버에 데이터를 요청하는 로직도 Model의 역할로서 같은 Repository에 포함시키도록 했다.
object MisoRepository { fun checkRegistered( socialId: String, socialType: String, onSuccessful: ( Call<GeneralResponseDto>, Response<GeneralResponseDto> ) -> Unit, onFail: ( Call<GeneralResponseDto>, Response<GeneralResponseDto> ) -> Unit, onError: ( Call<GeneralResponseDto>, Throwable ) -> Unit?, ) { val callCheckRegistered = TransportManager.getRetrofitApiObject<GeneralResponseDto>() .checkRegistered(socialId, socialType) TransportManager.requestApi(callCheckRegistered, { call, response -> if (response.isSuccessful) onSuccessful(call, response) else onFail(call, response) }, { call, throwable -> onError(call, throwable) }) } .... }
내가 만든 Repository의 일부 코드이다.
서버와 통신하기 위해 사용한 API 요청 코드를 대신 수행해주는 역할을 한다.
Retrofit의 객체를 만드는 과정은 많은 보일러플레이트 코드를 만들어내므로,
공통되는 코드는 다음과 같이
class TransportManager { companion object { fun <T> getRetrofitApiObject(): MisoWeatherAPI { var gson = GsonBuilder().setLenient().create() val retrofit = Retrofit.Builder() .baseUrl(MisoActivity.MISOWEATHER_BASE_URL) .addConverterFactory(GsonConverterFactory.create(gson)) .build() val api = retrofit.create(MisoWeatherAPI::class.java) return api } fun <T> requestApi( callApi: Call<T>, onResponse: (Call<T>, Response<T>) -> Unit, onFailure: (Call<T>, Throwable) -> Unit ) { callApi.enqueue(object : Callback<T> { override fun onResponse( call: Call<T>, response: Response<T> ) { onResponse(call, response) } override fun onFailure(call: Call<T>, t: Throwable) { onFailure(call, t) } }) } } }
TransportManager라는 클래스에 제네릭으로 받아 대신 만들어주도록 했다.
그리고 SharedPreperence를 사용하기 위해서
class MisoRepository private constructor() { companion object { private var instance: MisoRepository? = null private lateinit var context: Context private lateinit var prefs: SharedPreferences private lateinit var pairList: ArrayList<Pair<String, String>> fun getInstance(_context: Context): MisoRepository { return instance ?: synchronized(this) { instance ?: MisoRepository().also { context = _context instance = it prefs = context.getSharedPreferences("misoweather", Context.MODE_PRIVATE) pairList = ArrayList() } } } } fun addPreferencePair(first: String, second: String) { val pair = Pair(first, second) pairList.add(pair) } fun removePreference(pref: String) { val pair = Pair(pref, "") pairList.add(pair) } fun removePreference(vararg pref: String) { for (i in 0..pref.size - 1) { val pair = Pair(pref[i], "") pairList.add(pair) } } fun getPreference(pref: String): String? { return prefs!!.getString(pref, "") } fun savePreferences() { var edit = prefs!!.edit() for (i in 0..pairList.size - 1) { val pair = pairList.get(i) edit.putString(pair.first, pair.second) } edit.apply() pairList.clear() } .... }
위와 같은 코드로 변경했다.
SharedPreference에 접근을 위해 Context를 매개변수로 받는 싱글톤 생성자를 만들었다.
리포지토리는 어떤 곳이든 동일한 객체가 반환되어야 하므로,
싱글톤으로 구현하는 것이 정석이라고 생각한다.
그리고 항목을 받아 저장하는 메소드와,항목의 내용을 지우는 메소드들도 추가했다.
repository = MisoRepository.getInstance(applicationContext) repository.getBriefForecast( regionId, { call, response -> forecastBriefResponse.value = response }, { call, response -> forecastBriefResponse.value = response }, { call, t -> forecastBriefResponse.value = null } )
실제 호출 부분은 위와 같다.
사용하는 입장에서는 불러올 데이터를 가져오는 함수를 호출하고,각각 성공했을 때,실패했을 때,예외가 발생했을 때에 대한 메소드를 넣어주면 된다.
이제 Repository가 만들어졌으니,
각 뷰의 데이터를 저장할 ViewModel을 구현해야 한다.
일반적으로 안드로이드에서 ViewModel하면 기본적으로 따라오는 것이 LiveData이므로,
ViewModel을 구현하려면 LiveData도 함께 알아야한다.
LiveData는 관찰 가능한 데이터 홀더 클래스로,
저장된 데이터의 변경을 감지할 수 있는 observer를 붙일 수 있어,
실제 데이터와 UI 상의 데이터를 일치시킬 수 있는 장점이 있다.
class HomeViewModel(private val repository: MisoRepository) : ViewModel() { val memberInfoResponse: MutableLiveData<Response<MemberInfoResponseDto>?> = MutableLiveData() val forecastBriefResponse: MutableLiveData<Response<ForecastBriefResponseDto>?> = MutableLiveData() val commentListResponse: MutableLiveData<Response<CommentListResponseDto>?> = MutableLiveData() val surveyResultResponse: MutableLiveData<Response<SurveyResultResponseDto>?> = MutableLiveData() fun getUserInfo(serverToken: String) { repository.getUserInfo( serverToken, { call, response -> memberInfoResponse.value = response }, { call, response -> memberInfoResponse.value = response }, { call, throwable -> memberInfoResponse.value = null }) } fun getBriefForecast(regionId: Int) { repository.getBriefForecast( regionId, { call, response -> forecastBriefResponse.value = response }, { call, response -> forecastBriefResponse.value = response }, { call, t -> forecastBriefResponse.value = null } ) } fun getCommentList(commentId: Int?, size: Int) { repository.getCommentList( commentId, size, { call, response -> commentListResponse.value = response }, { call, response -> commentListResponse.value = response }, { call, throwable -> } ) } fun getSurveyResult(shortBigScale: String) { repository.getSurveyResults( shortBigScale, { call, reponse -> surveyResultResponse.value = reponse!! }, { call, reponse -> surveyResultResponse.value = reponse!! }, { call, t -> }, ) } }
메인화면의 날씨정보들을 서버로부터 불러와 데이터를 저장하는 역할을 하고 있다.
위에서 보는 것과 마찬가지로,UI에서 저장할 정보들을 LiveData 객체로 선언한 뒤,
API 메소드에서 응답을 받는 경우 선언된 LiveData의 객체들 또한 변경시킨다.
이정도만 해도,ViewModel로의 역할은 수행할 수 있다고 볼 수 있다.
repository = MisoRepository.getInstance(applicationContext) viewModel = HomeViewModel(repository)
실제 Activity 호출 부에서는 이렇게 Repository를 싱글톤으로 넣어 구현하였다.
viewModel.getBriefForecast(getPreference("defaultRegionId")!!.toInt()) viewModel.forecastBriefResponse.observe(this, { if (it == null) { Log.i("getBriefForecast", "실패") repeatRequest() } else { if (it.isSuccessful) { try { Log.i("결과", "성공") val forecastBriefResponseDto = it.body()!! var forecast = forecastBriefResponseDto.data.forecast var region = forecastBriefResponseDto.data.region addPreferencePair("bigScale", region.bigScale) addPreferencePair( "midScale", if (region.midScale.equals("선택 안 함")) "전체" else region.midScale ) addPreferencePair( "smallScale", if (region.smallScale.equals("선택 안 함")) "전체" else region.smallScale ) savePreferences() txtLocation.text = region.bigScale + " " + getPreference("midScale") + " " + if (getPreference("midScale").equals("전체")) "" else getPreference( "smallScale" ) txtWeatherEmoji.setText(forecast.sky) txtWeatherDegree.setText(forecast.temperature + "˚") setupSurveyResult() } catch (e: Exception) { repeatRequest() e.printStackTrace() Log.i("getBriefForecast", "excepted") } } else { repeatRequest() } } })
그리고 위와 같이 데이터를 뿌려야할 항목별로 초기화를 하도록 했다.
우선 데이터를 가져올 함수를 호출한뒤,가져올 LiveData 변수에 observe 함수를 이용하여
값이 변경될 때에 실행할 코드를 진행하도록 했다.
위와 같은 코드로 View와 ViewModel 간에 데이터를 요청하고 업데이트하는 과정은 어느정도 구현되었다고 볼 수 있지만, 아직은 SharedPreference를 뷰인 액티비티에서 접근하고 있기에,
ViewModel이 SharedPreperence의 접근도 함께 담당하도록 ViewModel에 Repository를 생성시에 함께 넣는 것으로 변경하였다.
class HomeViewModel(private val repository: MisoRepository) : ViewModel() { val memberInfoResponse: MutableLiveData<Response<MemberInfoResponseDto>?> = MutableLiveData() val forecastBriefResponse: MutableLiveData<Response<ForecastBriefResponseDto>?> = MutableLiveData() val commentListResponse: MutableLiveData<Response<CommentListResponseDto>?> = MutableLiveData() val surveyResultResponse: MutableLiveData<Response<SurveyResultResponseDto>?> = MutableLiveData() val isSurveyed: MutableLiveData<String?> = MutableLiveData() val lastSurveyedDate: MutableLiveData<String?> = MutableLiveData() val defaultRegionId: MutableLiveData<String?> = MutableLiveData() val misoToken: MutableLiveData<String?> = MutableLiveData() val bigScale: MutableLiveData<String?> = MutableLiveData() val midScale: MutableLiveData<String?> = MutableLiveData() val smallScale: MutableLiveData<String?> = MutableLiveData() val logoutResponseString: MutableLiveData<String?> = MutableLiveData() fun updateProperties() { setupBigScale() setupMidScale() setupSmallScale() setupDefaultRegionId() setupDefaultRegionId() setupSurveyed() setupMisoToken() setupLastSurveyedDate() } fun setupSurveyed() { isSurveyed.value = repository.getPreference("isSurveyed") } fun setupLastSurveyedDate() { lastSurveyedDate.value = repository.getPreference("LastSurveyedDate") } fun setupDefaultRegionId() { defaultRegionId.value = repository.getPreference("defaultRegionId") } fun setupMisoToken() { misoToken.value = repository.getPreference("misoToken") } fun setupBigScale() { bigScale.value = repository.getPreference("bigScaleRegion") } fun setupMidScale() { midScale.value = repository.getPreference("MidScaleRegion") } fun setupSmallScale() { smallScale.value = repository.getPreference("SmallScaleRegion") } ... }
기존에 외부 API를 호출하여 데이터를 가져오는 로직이외에,
SharedPreperence를 호출하여 내부 데이터를 저장하는 메소드도 추가하였다.
fun initializeProperties() { fun checkinitializedAll() { if ( this::isSurveyed.isInitialized && this::lastSurveyedDate.isInitialized && this::defaultRegionId.isInitialized && this::misoToken.isInitialized && this::bigScale.isInitialized && this::midScale.isInitialized && this::smallScale.isInitialized ) setupData() } viewModel.updateProperties() viewModel.isSurveyed.observe(this, { isSurveyed = it!! checkinitializedAll() }) viewModel.lastSurveyedDate.observe(this, { lastSurveyedDate = it!! checkinitializedAll() }) viewModel.defaultRegionId.observe(this, { defaultRegionId = it!! checkinitializedAll() }) viewModel.misoToken.observe(this, { misoToken = it!! checkinitializedAll() }) viewModel.bigScale.observe(this, { bigScale = it!! checkinitializedAll() }) viewModel.midScale.observe(this, { midScale = it!! checkinitializedAll() }) viewModel.smallScale.observe(this, { smallScale = it!! checkinitializedAll() }) }
기존 액티비티에는 실행 초기에 먼저 내부 SharedPreperence 데이터부터 먼저 초기화를 시켜,
ViewModel의 데이터와 값이 일치하는 변수를 가지도록 해 액티비티에서 따로 Repository에 접근할 필요가 없도록 했다.
그리고 각각 실행되는 데이터 초기화 요청은 비동기적으로 실행되므로 각 변수들이 전부 초기화된 이후에 API를 호출하는 로직을 실행하도록 했는데, 위 부분이 중복된 코드가 많이 발생하나 어떻게 해결해야 할 지를 우선 고민중에 있다.
위와 같이 구현함으로서 대략적인 MVVM 구현은 끝났다고 볼 수 있다.
데이터를 불러오는 역할은 오로지 ViewModel에서 담당하고,
액티비티는 업데이트된 데이터를 UI에 뿌려주기만 하면 되므로 각 클래스가 담당해야 할 역할은 분명해졌고 유지보수는 편리해졌다.
MVVM을 적용하는 것에 조금이나마 도움이 됐기 바란다.
참조
https://blog.yena.io/studynote/2019/03/27/Android-MVVM-AAC-2.html
'Andorid' 카테고리의 다른 글
MVVM 적용기(1) (0) 2022.04.02 [Android]Firebase로 채팅 앱 만들기(6) (0) 2022.03.31 [Android]Firebase로 채팅 앱 만들기(5) (0) 2022.03.31 [Android]Firebase로 채팅 앱 만들기(4) (2) 2022.03.31 [Android]Firebase로 채팅 앱 만들기(3) (0) 2022.03.31