-
[Android] MVVM 패턴 적용해보며 배우기(1) - ACC, MVC와 MVVM비교, MVVM 장점Client/Android 2020. 4. 7. 01:25
🔥목차🔥
🍓 안드로이드 아키텍처 컴포넌트 (AAC)
🍓 MVC 와 MVVM 차이점
🍓 MVVM 패턴
🍓 MVVM 장점
🐥 실습 - MVVM 패턴, Repository, BataBinding 적용
이어지는 시리즈
👉 [Android] MVVM 패턴 적용해보며 배우기(2) - RxJava2
안드로이드 아키텍쳐 컴포넌트
-
안드로이드 아키텍쳐 컴포넌트(Android Architecture Components, AAC)
-
라이브러리 모음
-
앱 구조를 더 튼튼하도록
-
테스트에 용이하도록
-
유지 보수성이 뛰어나도록
-
-
모듈화된 코딩을 돕기 위해
Databinding
,LiveData
,ViewModel
등의 유용한 라이브러리를 제공이러한 라이브러리의 모음은 MVVM 패턴의 구조의 설계에 최적화되어 있다.
View - 뷰
-
UI Controller를 담당하는 Activity, Fragment이다.
-
화면에 무엇을 그릴지 결정하고, 사용자와 상호작용한다.
-
데이터의 변화를 감지하기 위한 옵저버를 가지고 있다.
ViewModel - 뷰모델
-
뷰모델은 UI를 위한 데이터를 가지고 있다.
-
구성(configuration)이 변경되어도 살아남는다. (예를 들어 화면 회전이라던가, 언어 변경 등)
-
AsyncTask
는 액티비티나 프래그먼트의 생명 주기에서 자유로울 수 없지만,뷰모델은 뷰와 분리되어 있기 때문에 액티비티가 Destroy 되었다가 다시 Create 되어도 종료되지 않고 데이터를 여전히 가지고 있다.
LiveData - 라이브데이터
- 관찰이 가능한(Observable) 데이터 홀더 클래스이다.
- 뷰에서 뷰모델의 라이브데이터를 관찰하게 되면 데이터가 변경될 때 내부적으로 자동으로 알려주게 된다.
- 액티비티나 프래그먼트의 생명 주기를 인지한다. 즉, 액티비티가 화면 위에 활성화되어 있을 때에만 UI변경 등의 기능을 동작하게 되고, Destroy 된 상태에서는 동작하기 않기 때문에 메모리 릭의 발생을 줄여준다.
Repository - 리포지토리
-
뷰모델과 상호작용하기 위해 잘 정리된(Clean) 데이터 API를 들고 있는 클래스이다.
-
앱에 필요한 데이터 (내장DB or 외부DB) 를 가져온다.
뷰모델은 DB나 서버에 직접 접근하지 않고, 리포지토리에 접근하는 것으로 앱의 데이터를 관리한다.
Room - 룸
- SQLite 데이터베이스를 편하게 사용하게 해주는 라이브러리이다.
- SQLite의 코드를 직접 작성하는 경우, 직접 테이블을 Create 하거나 쿼리문을 일일이 변수를 통해 작성해주어야 했지만, Room을 쓰면 조금 더 직관적이고 편리하게 DB를 사용할 수 있다.
아키텍처 적용 전의 문제점
액티비티에 기능을 붙이다보면 액티비티가 무거워지거나 혹은 종속성이 너무 강해
테스트가 힘들고 유지보수가 어려워진다.
MVVM 패턴
View - ViewModel - Model
을 이용해 각각의 역할을 분리하여가독성과 재사용성을 높인 디자인 패턴으로 종속성 및 유지보수의 어려움을 해소해준다.
MVC와 MVVM 차이점
-
MVC
(Model - View - Controller)
-
Controller
Activity 부분에서 View에게는 화면 업데이트, Model에게는 데이터 갱신을 알리며
View와 Mdel을 연결해주며 비즈니스 로직을 처리하기 위해 많은 일을 해야한다.
-
View
activity_main.xml 화면 부분으로 UI 역할 담당한다.
-
Model
Model class로 비즈니스 로직에서의 알고리즘, 데이터 드의 기능 처리한다.
-
-
MVVM
(View - ViewModel - Model)
-
View
Activity 역할을 담당하고 UI를 갱신하는 역할에만 충실히 한다.
뷰모델을 관찰(Observe) 한다.
따라서, 데이터의 변화를 알아차리고 자동으로 화면을 갱신할 수 있다. -
ViewModel
Model에게 데이터 갱신 처리를 요청하고 잘 정리된 데이터를 참조한다.
-
Model
Repository, DataBase 부분으로 데이터 처리 역할을 한다.
-
MVVM 장점
1. 뷰가 데이터를 실시간으로 관찰한다
LiveData (=Observable 패턴) 을 이용해 DB를 관찰하고 자동으로 UI를 갱신한다.
직접 뷰를 바꾸어주는 번거로움을 없애고 데이터와 불일치할 확률이 줄인다.
2. 생명주기로부터 안전하여 메모리 릭을 방지할 수 있다
뷰모델을 통해 데이터를 참조하기 때문에 액티비티/프래그먼트의 생명주기를 따르지 않는다.
=> 화면전환과 같이 액티비티가 파괴된 후 재구성 되어도 뷰모델이 데이터를 홀드하고 있기 때문에
영향을 받지 않는다.
뷰가 활성화되어있을 경우에만 작동하기 때문에 불필요한 메모리 사용을 줄일 수 있다.
*3. 기능별로 모듈화되어 있어 역할 분리를 할 수 있고 유닛 테스트에 한결 용이해진다. *
-
내장 DB를 통째로 바꾸고 싶다고 할 때, 뷰나 다른 코드에 깊게 종속돼있지 않아 DB 교체가 쉽다.
-
뷰모델과 뷰가 1:n 연결이 가능하다.
따라서, 뷰모델에 하나의 메소드를 구현해 놓으면
A 액티비티든 B 액티비티든 여러 뷰에서 호출해 재사용하기 편리하다.
실습 - MVVM 패턴, Repository, DataBinding 적용
얼리버디
프로젝트에 MVVM 아키텍처를 적용해 공부한 내용을 익혀보려 한다.SignupActivity.kt
var vm: SignupViewModel = SignupViewModel() lateinit var binding : ActivitySignupBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //setContentView(R.layout.activity_signup) binding = DataBindingUtil.setContentView(this, R.layout.activity_signup) makeController() binding.signupActivity = this addObserverableData() } ... fun communication(id: String, pw: String) { var jsonObject = JSONObject() jsonObject.put("userId", id) jsonObject.put("userPw", pw) val body = JsonParser().parse(jsonObject.toString()) as JsonObject vm.viewCommunicate(body) } fun click() { communication(id, pw) } private fun View.showOrInvisible(show: Boolean) { visibility = if (show) { View.VISIBLE } else { View.INVISIBLE } } private fun idDuplicate(){ act_signup_tv_id_ment.showOrInvisible(true) } private fun addObserverableData(){ vm.isSuccessNetwork.observe( this, Observer { if(it){ val intent = Intent(this@SignupActivity, SignupSuccessActivity::class.java) startActivity(intent) }else{ idDuplicate() } } ) vm.wifiDisconnect.observe( this, Observer { val intent = Intent(this@SignupActivity, SignupFailActivity::class.java) startActivity(intent) } ) } }
SignupViewModel.kt
class SignupViewModel : ViewModel() { val signupRepository = SignupRepository() internal val disposables = CompositeDisposable() val isSuccessNetwork = MutableLiveData<Boolean>() val wifiDisconnect = MutableLiveData<Unit>() fun viewCommunicate(jsonObject: JsonObject) { val body = JsonParser().parse(jsonObject.toString()) as JsonObject Log.d("test", "postUserData body : " + body) signupRepository.signUp(body = body, onResponse = { //고차함수로 구현, it으로 response에 바로 접근 가능 if (it.isSuccessful) { val intent = Intent(this, SignupSuccessActivity::class.java) startActivity(intent) isSuccessNetwork.value = true } else { //아이디 중복 isSuccessNetwork.value = false Log.d("test", "아이디중복: " + it.message()) } }, onFailure = { //고차함수로 구현, it으로 t에 바로 접근 가능 Log.d("test", "통신 실패 error : " + it.toString()) wifiDisconnect.value = Unit val intent = Intent(this@SignupActivity, SignupFailActivity::class.java) startActivity(intent) } ) } }
SignupRepository.kt
class SignupRepository{ val retrofitRemoteDataSource: RemoteDataSource = RemoteDataSourceImpl() //인스턴스 생성 fun signUp( jsonObject: JsonObject, onResponse: (Response<PostSignupData>) -> Unit, onFailure: (Throwable) -> Unit ){ //생성한 인스턴스로 RempteDataSourceImpl에 구현해놓은 함수 실행 retrofitRemoteDataSource.signUp(jsonObject, onResponse, onFailure) } }
PostSignupData.kt
data class PostSignupData ( @SerializedName("status") val status: Int, @SerializedName("data") val idx: Int, @SerializedName("message") val message: String )
AndBuddyService.kt
interface AndBuddyService { @POST("/users/signup") fun postSignupUser( @Body() body: JsonObject ): Call<PostSignupData> }
AndBuddyServiceImpl.kt
object AndBuddyServiceImpl { private const val BASE_URL = "http://서버주소:3456/" private val okHttpClient: OkHttpClient = OkHttpClient.Builder().addInterceptor(CookiesIntercepter()) .addNetworkInterceptor(CookiesIntercepter()).build() private val retrofit: Retrofit = Retrofit.Builder().baseUrl(BASE_URL).client( okHttpClient ) .addConverterFactory(GsonConverterFactory.create()) .build() val service: AndBuddyService = retrofit.create( AndBuddyService::class.java) }
RemoteDataSource.kt
interface RemoteDataSource { //필요한 함수들을 모아서 정의하는 인터페이스 fun signUp(jsonObject: JsonObject, //post에 필요한 값을 보낼 body onResponse: (Response<PostSignupData>) -> Unit, //통신 성공시 수행할 함수 onFailure: (Throwable) -> Unit) //통신 실패시 수행할 함수 }
RetmoteDataSourceImpl.kt
class RemoteDataSourceImpl : RemoteDataSource { //RemoteDataSource를 상속받는 클래스 override fun signUp( jsonObject: JsonObject, onResponse: (Response<PostSignupData>) -> Unit, onFailure: (Throwable) -> Unit ) { AndBuddyServiceImpl.service.postSignupUser(jsonObject).enqueue(object : //enqueue 함수 실행 Callback<PostSignupData> { override fun onFailure(call: Call<PostSignupData>, t: Throwable) { //통신 실패시 실행되는 함수 구현 onFailure(t) } override fun onResponse( //통신 성공시 실행되는 함수 구현 call: Call<PostSignupData>, response: Response<PostSignupData> ) { onResponse(response) } }) } }
build.gradle (Module: app)
android { ... dataBinding { enabled = true } } dependencies { ... //Retrofit 라이브러리 : https://github.com/square/retrofit implementation 'com.squareup.retrofit2:retrofit:2.6.2' //Retrofit 라이브러리 응답으로 가짜 객체를 만들기 위해 implementation 'com.squareup.retrofit2:retrofit-mock:2.6.2' //객체 시리얼라이즈를 위한 Gson 라이브러리 : https://github.com/google/gson implementation 'com.google.code.gson:gson:2.8.6' //Retrofit 에서 Gson 을 사용하기 위한 라이브러리 implementation 'com.squareup.retrofit2:converter-gson:2.6.2' //okHttp implementation 'com.squareup.okhttp3:logging-interceptor:3.8.1' implementation 'com.squareup.okhttp3:okhttp:3.8.1' //리사이클러뷰 라이브러리 implementation 'androidx.recyclerview:recyclerview:1.1.0-alpha06' }
통신을 위한 퍼미션을 주기 위해 2가지를 추가해줘야한다.
1) AndroidManifest.xml
해당 코드를 추가해줘야 한다.
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.INTERNET" />
2) CookiesIntercepter.kt
class CookiesIntercepter : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request().newBuilder().header("Content-Type", "application/json") .build() return chain.proceed(request) } }
해당 블로그에 설명이 매우 잘 되어있어서 공부 후 정리하는데 많은 도움이 되었다.
반응형'Client > Android' 카테고리의 다른 글
[Android] MVVM 패턴 적용해보며 배우기(2) - RxJava2 (0) 2020.04.08 코틀린 퀴즈 -3 확장함수,제네릭, 고차함수, 람다 (0) 2020.03.12 코틀린 퀴즈 -2 제네릭, 주생성자, 확장함수, 익명함수, 엘비스 연산자 (0) 2020.03.12 코틀린 퀴즈 -1 컬렉션, 생성자, 추상클래스, 인터페이스 (0) 2020.03.12 [Kotlin] 커니의 코틀린-(8) 코틀린 안드로이드 익스텐션 (0) 2020.03.11 -