우선 코루틴에 대해서 기본은 알고 있다고 생각하고 넘어가겠다. 또한, 코루틴에 대한 기본 및 심화는 추가로 올리는걸로..

Android Kotlin Flow Definition

코루틴에서의 Flow는 단일 값만 반환하는 suspend function과 달리 여러 값을 순차적으로 내보낼 수 있는 유형이다. 

Ex) Flow를 사용하여 데이터베이스에서 실시간 업데이트를 수신할 수 있다.

 

Flow는 코루틴을 기반으로 빌드되며 어려 값을 제공할 수 있다. 또한! 내보낸 값은 동일한 유형이어야 한다.

Ex) Flow<Int>는 정수 값을 내보내는 Flow이다.

 

Flow는 값 시퀀스를 생성하는 Iterator와 매우 비슷하지만 suspend function을 사용하여 값을 비동기적으로 생성하고 사용한다.

Ex) Flow는 기본 스레드를 차단하지 않고 다음 값을 생성할 네트워크 요청을 안전하게 만들 수 있다.

 

즉, Coroutine의 Flow는 Data Stream이며, Coroutine에서 리엑티브 프로그래밍을 할 수 있도록 지원하기 위한 구성요소이다.

 

간단하게 리엑티브 프로그래밍에 대해 알아보자.

What is Reactive Programming?

리엑티브 프로그래밍이란 데이터가 변경될 때 이벤트를 발생시켜 데이터를 계속해서 전달하도록 하는  프로그래밍 방식을 뜻한다.

기존 명령형 프로그래밍에서는 Data Consumer는 데이터를 요청한 후 받은 결과값을 일회성으로 수신한다. 하지만, 이러한 방식은 데이터가 필요할 때마다 결과값을 매번 요청해야한다는 점에서 매우 비효율적이다. 

리엑티브 프로그래밍에서는 데이터를 발행하는 발행자가 있고, 데이터의 컨슈머는 데이터의 발행자에게 구독 요청을 한다.

그러면! 데이터의 발행자는 새로운 데이터가 들어오면 데이터의 소비자에게 지속적으로 발행한다.

 

즉, Reactive Programming에는 하나의 데이터를 발행하는 발행자가 있고 해당 발행자는 데이터의 소비자에게 지속적으로 데이터를 전달하는 역할을 한다. 이를!! Data Stream이라 한다.

 

데이터 스트림에는 다음과 같은 세 가지 항목이 있다.

1. 생산자(Producer)는 스트림에 추가되는 데이터를 생산한다. 코루틴 덕분에 흐름에서 비동기적으로 데이터가 생산될 수 있다.

2. (선택사항) 중개자(Intermediary)는 스트림에 내보내는 각각의 값이나 스트림 자체를 수정할 수 있다.

3. 소비자(Consumer)는 스트림의 값을 사용한다.

 

예를들어, 날씨 앱을 만든다고 가정한다면 아래와 같은 과정을 거친다.

1. Flow 선언

2. 날씨 데이터를 서버로부터 가져온다.

3. Producer가 데이터르를 생성한다.

4. 2,3번 과정을 30초마다 반복하여 데이터를 계속 생성

 

class WeatherRemoteDataSource(private val weatherApi: WeatherAPI){
	fun getWeatherFlow(): Flow<List<Wheather>> = flow{
    	while(true){
        	val weeklyWeather = weatherApi.fetchWeekWeather()
            emit(weeklyWeather)
           	delay(INTERVAL_REFRESH)
        }
    }
    
    companion object{
    	private const val INTERVAL_REFRESH: Long = 30000
    }
}

 

Intermediary(중간 연산자)

생산자가 데이터를 생성했으면 중간 연산자는 생성된 데이터를 수정한다. 

Ex) 예를들어 생성자는 A라는 객체로 이루어진 데이터를 발행했는데 B라는 객체가 필요한 경우 Flow에서 지원하는 중간 연산자를 이용해 A객체를 B객체로 바꿀 수 있다.

 

중간연산자로는 map, filter, onEach 등이 있다.

 

다시 앞선 날씨 앱을 보면, View에 전달할 때는 processing된 데이터가 전달되어야 한다.

사실 Wahter Data Class에 11개의 변수가 있다고 가정하자. 그 중 전부가 필요하지 않고 특정 위치(locale)의 날씨 데이터만 필요하다.

따라서 중간 연산자를 통해 원하는 데이터만 가져온다.

class WeatherRepository(private val weatherRemoteDataSource: WeatherRemoteDataSource){
	fun getWeatherItem(locale: Locale) = 
    	weatherRemoteDataSource.getWeatherFlow().map{ it.filter{ this.locale == locale}}
}

 

Consumer(소비자)

Intermediate가 데이터를 수정한 후 소비자에게 데이터를 전달한다. 이후 Flow에서는 collect를 이용해 전달된 데이터를 소비할 수 있다.

 

안드로이드 다큐먼트에 따르면 Data Consumer는 UI Layer에 있는 UI Component가 데이터를 소비하여 이에 맞는 UI를 그린다.

이제 받은 날씨 데이터를 이용해 ViewModel에서 필요한 처리를 한 후 UI State에 띄어준다.(UI Layer 작용 방식)

 

class WeatherViewModel(private val weatherRepository: WeatherRepository): ViewModel(){
	fun collectWeather(locale: Locale) = 
    	viewModelScope.launch{
        	weatherRepository.getWeather(locale).collect{ weatherInfo -> 
            	//...///
            }
        }
    }
}

 

Flow의 한계

Flow는 앞서 코루틴이 리엑티브 프로그래밍을 지원하기 위해 만든 Data Stream이다. 

하지만, Flow는 Data Stream을 발생시키기만 할 뿐 데이터가 저장되지 않는다...

 

따라서, Flow만을 사용해서 Android UI State에 띄우기 위한 방법으로는 두 가지가 있다.

1. 화면이 refresh 될때마다 다시 Server, DB로부터 데이터 가져오기

-> 상기 방식은 비효율적이다. 안드로이드의 생명주기를 따라 화면이 회전되거나 다른 행위를 하고 다시 돌아왔을때 onDestroy가 호출된 후 onCreate가 호출되는데, 이 과정마다 새로운 데이터를 가져와야 하기 때문이다..서버 리소스 비용 급상승....

2. Flow로부터 collect한 데이터를 ViewModel에 저장해놓고 사용하기

-> ViewModel은 생명주기를 고려해 onDestroy가 호출되더라도 살아있기에 데이터를 저장시킬 수 있다!

 

그러나, 2번 방법인 viewModel 저장에서 데이터를 저장하고 있으려면 별도의 Holder를 생성해야 한다. 또한 Holder는 Reactive하지 않기 때문에 UI에서 해당 Holder를 사용하기 위해 별도의 fetching function을 만들어야 한다..

또 다른 방법으로는 viewModel에서 Holder와 flow를 같이 사용하는 방법이다. flow를 사용하고 Holder는 flow에서 마지막으로 발행한 데이터를 저장하고 있으면 된다. 따라서 UI에서는 flow에서 값을 발행하기 전에는 Holder의 데이터를 사용하면 된다.

이 방식으로 만들면 Holder가 마지막 데이터를 갖고 있어 다시 서버로 데이터를 요청할 필요가 없어진다.

하지만! 위 두 방식은 Boiler Plate Code를 만든다는 것이다..... 안드로이드에서 UIState가 단일이 아닌데 모두를 갖기 위해 유사 코드를 매번 작성해 가독성을 떨어뜨릴 수 있기 때문..

 

상기 문제점들을 해결해 줄 수 있는 것이 State Flow이다.

 

StateFlow Definition

StateFlow는 현재 상태와 새로운 상태 업데이트를 Collector에 내보내는 Observable한 StateHolder Flow이다. 

즉, StateFlow는 Data Holder(Repository) 역할을 하면서 Flow의 Data Stream 역할까지 한다는것.

UI Layer에서 StateFlow를 갖고 UIState에 업데이트 하면 화면이 재구성될 때마다 다시 서버로 데이터를 요청할 필요가 없어진다.

결국, UI는 단순히 StateFlow를 구독만 하고 있으면 된다는 것..

 

Flow to StateFlow

보통 ReativeProgramming을 할 때 다양한 Flow를 하나의 Flow로 합친다.

예를 들어 피트니스 회원 주간 성과표를 만들 경우, 1주일간의 운동 데이터, 1주일간의 식단 데이터, 1주일 간의 섭취 칼로리 등 성과표를 만드는 데이터를 갖기위한 정보를 각 테이블에서 가져와 하나의 객체로 만들어줘야 한다..

 

이렇게 합쳐 만들어진 Flow는 UI가 단순히 구독하기 위한 StateFlow로 변환해주어야 한다. 

추가로 StateFlow가 Flow를 계속 구독하고 있으면 Memory Leak가 생기므로 Coroutine Scope를 통해 StateFlow의 수명을 관리하어야 한다.

 

우선 Flow를 StateFlow로 변환하려면 stateIn 중간연산자를 사용한다.

stateIn Definition

fun <T> Flow<T>.stateIn(
    scope: CoroutineScope, 
    started: SharingStarted, 
    initialValue: T
): StateFlow<T>

ColdFlow를 지정된 코루틴 범위에서 시작되는 hotStateFlow로 변환하여 업스트림 흐름의 단일 실행 인스턴스에서 가장 최근에 내보낸 값을 여러 다운스트림 구독자와 공유합니다.

 

다음을 사용하기 위해 

scope: StateFlow가 Flow로부터 데이터를 구독받을 CoroutineScope를 명시한다.

stated: Flow로부터 언제 구독을 할지 명시한다.

initialValue: StateFlow에 저장될 초기값을 설정한다.

 

사용방법은 다음과 같다.

val exampleFlow: FLow<Int> = flow{
	for(i in 0..10){
    	emit(i*i)
        delay(400)
    }
}

상기 flow를 stateIn 중간 연산자를 사용해 StateFlow로 변환해보자

val stateFlow = stringFlow.stateIn(
	initialValue = 0,
    started = SharingStarted.WhileSubscribed(1000),
    scope = viewModelScope
)

이로써, 초기 저장 값은 이고, 구독 후 1초 뒤 처음 발행, 또한, viewModel의 생명주기만큼 구독받는 행동을 하는 StateFlow가 만들어진다.

'Computer engineering > Android Programming' 카테고리의 다른 글

REST API_HttpURLConnection_Coroutine  (0) 2022.07.13
AAC_ViewModel2  (0) 2022.07.12
AAC_ViewModel_1  (0) 2022.07.12
MVVM with Clean Architecture  (0) 2022.07.11
4. Architecture Component UI Layer Data Binding_3  (1) 2022.07.10

Rest API Definition

REST(Representational State Transfer) API 란 REST 아키텍처의 제약 조건을 준수하는 어플리케이션 프로그래밍 인터페이스를 뜻한다.

 

Rest API Feature

1. Server 와 Client의 구조

-> 좀 더 편하게 생각해 본다면, DB를 통신할 수 있는 서버와 앱

2. Socket 통신과 다르게 양방향이 아닌 단방향 통신

3. Request와 Response로 이루어짐

4. Get, Post, Put, Delete 등의 메소드를 사용한다.

 

결국, 클라이언트 즉 앱에서 서버에 원하는 것을 요청하면 서버는 그에 맞는 원하는 것을 주는 구조이다.

 

Rest API를 사용하는 라이브러리는 다음과 같다.

1. HttpURLConnection, HttpsURLConnection

2. OKHttp

3. Retrofit1, 2

 

1. HttpURLConnection

보통 안드로이드 스튜디오 내에서 AsyncTask를 사용하지만 deprecated되었다..

그래서 다음 예제는 Coroutine을 사용하여 비동기 처리작업을 하였다.

여기서 잠깐, 코루틴을 왜쓰느냐...? 

-> 코루틴은 장기 테스크를 백그라운드 스레드에서 관리할 수 있도록 비동기로 처리할 수 있게 해준다.

좀더 디테일 하게 얘기해보면

CPU 집약적 작업, 네트워크 요청, DISK IO와 같은 장기 테스크는 메인스레드에서 실행하면 안된다.

메인 스레드에서 이러한 작업을 실행하면 UI 상호 작용이 차단될 수 있다. 

또한, 메인 스레드에서 네트워크 요청을 실행하는 것은 Android SDK에 의해 방지되고 Exception Throw를 한다.

이는 Android Docs에도 나와 있는 설명이다.

HTTP GET Request

1. 네트워크 작동의 가장 간단한 예는 HTTP get 요청이다.

2. 생성자에 url 문자열을 전달하여 새로운 URL 객체를 생성.

3. URL 객체는 HttpURLConnection을 생성하기 위해 사용된다.

4. 그 후, 실행한 뒤 InputStream으로 response를 받는다.

5. 마지막으로, heplper 메소드를 사용해 inputStream -> string으로 변환한다.

 

private fun httpGet(myURL: String?): String {

    val inputStream:InputStream
    val result:String

    // URL 생성
    val url:URL = URL(myURL)

    // HttpURLConnection 생성
    val conn:HttpURLConnection = url.openConnection() as HttpURLConnection

    // 주어진 URL로 get을 요청
    conn.connect()

    // inputStream으로 response를 받기
    inputStream = conn.inputStream

    // inputStream을 String으로 변환
    if(inputStream != null)
        result = convertInputStreamToString(inputStream)
    else
        result = "잉!? 안되잖아!"
    
    return result
}

Perform Network Operations on a Seperate Thread Using AsyncTask

1. 네트워크 작동은 UI가 멈추는 것을 막기 위해 메인이 아닌 백그라운드 쓰레드에서 사용한다.

2. AsyncTask 클래스는 UI Thread(메인)로부터 분리시킬 수 있는 가장 간단한 방법 중 하나이다.

3. AsncTask를 확장한 innerClass 하나 만들어주자.

4. 그 후, doInBackground() & onPostExecute를 오버라이딩 하자.

inner class HTTPAsyncTask : AsyncTask<String, Void, String>() {
    override fun doInBackground(vararg urls: String?): String {
        return HttpGet(urls[0])
    }
    override fun onPostExecute(result: String?) {
        tvResult.setText(result)
    }
}

이제, HTTP request를 다음과 같이 사용할 수 있다!

HTTPAsyncTask().execute("http://hmkcode-api.appspot.com/rest/api/hello/Android")

 

Perform Network Operations on a Seperate Thread Using Coroutine

suspend 함수를 사용해 보자. 

suspend로 표시된 함수는 결과가 반환될 때까지 실행을 일시 중단한다는 특징을 갖는다.

결과값을 기다리는 동안, 다른 함수나 코루틴이 실행될 수 있도록 실행 중인 차단을 해제한다.

private suspend fun httpGet(myURL: String?): String? {...}

 

Dispatcher

Dispatcher는 코루틴 내에서 어떤 쓰레드를 돌릴지 결정한다.

Dispatcher에는 Default, IO, Main이 있다.

지금은 HTTP request 예제이기 때문에 withContext(Dispatchers.IO)를 통해 IO Thread를 사용한다.

 

private suspend fun httpGet(myURL: String?): String? {

        val result = withContext(Dispatchers.IO) {
           
           // HTTP GET request code... 

        }
        return result
    }

Coroutine Scope

Coroutine은 CoroutineScope에서 작동한다.

Scope는 Coroutine의 생명주기를 관리한다.

또한, GlobalScope를 사용하여 코루틴을 시작할 수 있다.

 

GlobalScope는 백그라운드에서 작업을 실행하기 좋지만, 비추천한다.

GlobalScope.launch(Dispatchers.Main) {
                val result = httpGet("http://hmkcode-api.appspot.com/rest/api/hello/Android")
                tvResult.setText(result)
            }

또한, GlobalScope를 LifecycleScope로 대체할 수 있다.

 

LifecycleScope는 KTX 확장에 포함된 AAC의 세가지 기본 제공 코루틴 scope중 하나이다.

자 이제 설명은 끝났으니 변경해보자

 lifecycleScope.launch {
                val result = httpGet("http://hmkcode-api.appspot.com/rest/api/hello/Android")
                tvResult.setText(result)
            }

'Computer engineering > Android Programming' 카테고리의 다른 글

Coroutine_Flow  (0) 2022.07.14
AAC_ViewModel2  (0) 2022.07.12
AAC_ViewModel_1  (0) 2022.07.12
MVVM with Clean Architecture  (0) 2022.07.11
4. Architecture Component UI Layer Data Binding_3  (1) 2022.07.10

프래그먼트 간  데이터 공유

Activity에 포함된 둘 이상의 프래그먼트는 흔히 서로 커뮤니케이션 한다고 알려져 있다. 사용자가 목록에서 항목을 선택하는 프래그먼트와 선택된 항목의 컨텐츠를 표시하는 또 다른 프래그먼트가 있는 split-view(list-detail) 프래그먼트의 일반적인 사례를 가정해보자.

두 프래그먼트가 모두 인터페이스 설명을 정의해야 하고 메인 엑티비티가 두 프래그먼트를 함께 결합해야 하므로 이 사례는 간단히 처리할 수 있는 작업이 아니다.

또한 두 프레그먼트는 모두 다른 프래그먼트가 아직 생성되지 않았거나 표시되지 않은 시나리오도 처리해야 한다.

 

이러한 일반적인 고충은 ViewModel 객체를 사용하면 해결할 수 있다. 이러한 프래그먼트는 다음 샘플 코드와 같이 이 커뮤니케이션 처리하기 위한 활동범위를 사용하여 ViewModel을 공유할 수 있다.

 

class SharedViewModel : ViewModel() {
    val selected = MutableLiveData<Item>()

    fun select(item: Item) {
        selected.value = item
    }
}

class ListFragment : Fragment() {

    private lateinit var itemSelector: Selector

    // Use the 'by activityViewModels()' Kotlin property delegate
    // from the fragment-ktx artifact
    private val model: SharedViewModel by activityViewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        itemSelector.setOnClickListener { item ->
            // Update the UI
        }
    }
}

class DetailFragment : Fragment() {

    // Use the 'by activityViewModels()' Kotlin property delegate
    // from the fragment-ktx artifact
    private val model: SharedViewModel by activityViewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        model.selected.observe(viewLifecycleOwner, Observer<Item> { item ->
            // Update the UI
        })
    }
}

두 프래그먼트는 모두 자신이 포함된 엑티비티를 검색한다. 그러면 각 프래그먼트는 ViewModelProvider를 가져올 때 이 엑티비티 범위가 지정된 동일한 SharedViewModel 인스턴스를 받는다.

이 접근 방법에는 다음과 같은 이점이 있다.

1. Activity는 아무것도 할 필요가 없거나 이 커뮤니케이션에 관해 어떤 것도 알 필요가 없다.

2. Fragment는 SharedViewModel 계약 외에 서로 알 필요가 없다. Fragment 중 하나가 사라져도 다른 Fragment는 계속 평소대로 작동한다.

3. 각 Fragment는 자체 수명 주기가 있으며, 다른 Fragment 수명 주기의 영향을 받지 않는다. 한 Fragment가 다른 Fragment를 대체해도, UI는 아무 문제 없이 계속 작동한다.

 

ViewModel로 로더 대체하기

CursorLoader와 같은 로더 클래스는 앱 UI의 데이터와 데이터베이스 간의 동기화를 유지하는 데 자주 사용된다. ViewModel을 몇 가지 클래스와 함께 사용하여 로더를 대체할 수 있다. ViewModel을 사용하면 UI 컨트롤러가 데이터 로드 작업에서 분리된다.

즉, 클래스 간에 강력한 참조가 적어진다.

 

일반적인 로더 사용 방법 중 하나로, 앱이 CursorLoader를 사용하여 데이터베이스의 내용을 관찰할 수 있다. 데이터베이스에서 값이 변경되면 로더가 자동으로 데이터 새로고침을 트리거하고 UI를 업데이트 한다.

ViewModel은 Room 및 LiveData와 함께 작업하여 로더를 대체한다. ViewModel은 기기 설정이 변경되어도 데이터가 유지되도록 보장한다. 데이터베이스가 변경되면 Room에서 LiveData에 변경을 알리고, 알림을 받은 LiveData는 수정된 데이터로 UI를 업데이트 한다.

 

'Computer engineering > Android Programming' 카테고리의 다른 글

Coroutine_Flow  (0) 2022.07.14
REST API_HttpURLConnection_Coroutine  (0) 2022.07.13
AAC_ViewModel_1  (0) 2022.07.12
MVVM with Clean Architecture  (0) 2022.07.11
4. Architecture Component UI Layer Data Binding_3  (1) 2022.07.10

개요

ViewModel 클래스는 수명 주기를 고려하여 UI 관련 데이터를 저장하고 관리하도로고 설계되었다. 

Android Framework는 Activity나 Fragment 같은 UI Controller의 수명 주기를 관리한다. Framework는 특정 사용자 작업이나 완전히 통제할 수 없는 기기 이벤트에 대한 응답으로 UI 컨트롤러를 제거하거나 다시 만들도록 결정할 수 있다.

 

시스템에서 UI Controller를 제거하거나 다시 만드는 경우, Controller에 저장된 모든 일시적인 UI 관련 Data는 삭제된다.

 

물론, 데이터가 단순한 경우 Acitivity는 onSaveInstanceState() 메소드를 사용하여 onCreate()의 번들에서 데이터를 복원할 수 있다.

-> 하지만 에로사항들이 있는데,

1. 이 접근 방법은 사용자 목록이나 비트맵과 같은 대용량일 가능성이 높은 데이터가 아닌, 직렬화 했다가 다시 역직렬화할 수 있는 소량의 데이터에만 적합하다.

2. UI Controller가 반환하는 데 시간이 걸릴 수 있는 비동기 호출을 자주 해야 한다는 점이다. UI Controller는 비동기 호출을 관리해야 하며, 메모리 누수 가능성을 방지하기 위해 호출이 제거된 후 시스템에서 호출을 정리하는지 확인해야 한다.

*관리에는 많은 유지관리 필요, 구성 변경 시 개체가 다시 생성되는 경우 개체가 이미 수행된 호출을 다시 호출해야 할 수 있으므로 리소스가 낭비된다.

 

ViewModel 구현

아키텍처 구성요소는 UI의 데이터 준비를 담당하는 UI Controller에 ViewModel 클래스를 제공한다. 

ViewModel 객체는 구성이 변경되는 동안 자동으로 보관되므로, 이러한 객체가 보유한 데이터는 다음 Activity, Fragment에서 즉시 사용할 수 있다.

Ex) 앱에서 사용자 목록을 표시해야 한다면 다음 코드 대로 사용자 목록을 확보하여 Activity나 Fragment 대신 ViewModel에 보관하도록 책임을 할당한다.

class MyViewModel : ViewModel() {
    private val users: MutableLiveData<List<User>> by lazy {
        MutableLiveData<List<User>>().also {
            loadUsers()
        }
    }

    fun getUsers(): LiveData<List<User>> {
        return users
    }

    private fun loadUsers() {
        // Do an asynchronous operation to fetch users.
    }
}

이후 다음과 같이 Activity에서 목록에 엑세스 할 수 있다.

class MyActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same MyViewModel instance created by the first activity.

        // Use the 'by viewModels()' Kotlin property delegate
        // from the activity-ktx artifact
        val model: MyViewModel by viewModels()
        model.getUsers().observe(this, Observer<List<User>>{ users ->
            // update UI
        })
    }
}

Activity가 다시 생성되면 첫 번째 Activity에서 생성된 동일한 MyViewModel 인스턴스를 받는다. Activity가 완료되면 프레임워크는 리소스를 정리할 수 있도록 ViewModel 객체의 onCleared() 메소드를 호출한다.

 

ViewModel 객체는 뷰 또는 LifecycleOwners의 특정 인스턴스화보다 오래 지속되도록 설계되었다. 이러한 설계로 인해 뷰 및 Lifecycle 객체에 관해 알지 못할 때도 ViewModel을 다루는 테스트를 더 쉽게 작성할 수 있다. ViewModel 객체에는 LiveData 객체와 같은 LifecycleObservers가 포함될 수 있다. 그러나 ViewModel 객체는 LiveData 객체와 같이 수명 주기를 인식하는 Observable의 변경사항을 관찰해서는 안된다.

 

ViewModel의 수명 주기

ViewModel 객체의 범위는 ViewModel을 가져올 때 ViewModelProvider에 전달되는 Lifecycle로 지정된다.

ViewModel은 범위가 지정된 Lifecycle이 영구적으로 경과될 때까지, 즉 Activity에서 Activity가 끝날 때까지 그리고 프래그먼트에서 프래그먼트가 분리될 때까지 메모리에 남아 있다.

일반적으로 시스템에서 Activity 객체의 onCreate() 메소드를 처음 호출할 때 ViewModel을 요청한다. 시스템은 Activity 기간 내내 onCreate() 메소드를 여러 번 호출할 수 있다. ViewModel이 처음 요청되었을 때부터 활동이 끝나고 폐기될 때까지 ViewModel은 존재한다.

 

 

Intro

만일, 안드로이드 개발 시 정확한 아키텍처를 사용하지 않는다면, 코드가 많아지고 팀이 확장됨에 따라 유지보수가 어려워진다.

MVVM과 Clean Architecture를 통해 분리되고, 테스트 가능성을 높이고, 유지보수가 용이한 코드를 작성하는 방법을 알아보자.

 

왜 MVVM과 Clean Architecture인가?

MVVM(Model + View + ViewModel)은 View(ex: Activity, Fragment 등)와 비즈니스 로직을 분리해준다. MVVM은 간단한 ㅂ=프로젝트로는 충분히 괜찮지만 코드가 점점 커지면 ViewModel 또한 커진다. 그렇다면 각자의 역할을 분리하기가 어려워지는데..

 

MVVM과 Clean Architecture는 코드가 점점 커지는 것과 같은 문제에 아주 효율적이다. 이 조합은 우리의 코드의 역할 분리를 한단계 넘어선 방법이다. 즉, 앱에서 수행할 수 있는 작업의 논리를 명확하게 추상화한다.

 

*물론, MVP(Model-View-Presenter)과 Clean Architecture도 좋다. 하지만 AAC(Android Architecture Components)는 이미 ViewModel 클래스를 내장해 제공해줘 MVVM 프레임워크가 필요하지 않은 MVP를 통한 MVVM을 사용할 것이다.

 

Clean Architecture의 장점

1. 일반 MVVM 보다 훨씬 더 테스트를 쉽게 할 수 있다.

2. 코드가 더 잘 분리된다. **굉장히 큰 장점

3. 패키지 구조를 찾기가 더 쉽다.

4. 프로젝트 유지보수가 더 쉽다.

5. 팀원이 새로운 기능을 추가하는데 더 빨라진다.

 

Clean Architecture의 단점

1. 공부하는데 꽤 걸린다..간단한 MVVM, MVP와 같은 패턴만 공부한경우 모든 레이어의 작동방식을 이해하는데 시간이 걸린다..

2. 많은 부가적인 클래스가 추가됨으로써, 간단한 프로젝트에는 적합하지 않다.

 

이번 예제의 데이터 플로우는 다음과 같다.

비즈니스 로직은 UI와 완전 분리된다. 이는 코드를 아주 쉽게 유지보수하고 테스트 할 수 있도록 만들어준다.

예제는 아주 간단하다.. 사용자가 새 게시물을 만들고 그들이 만든 게시물 목록을 볼 수 있다. 

물론, 이 예제는 라이브러리(Dagger, RxJava등)를 사용하지 않는다.

 

MVVM의 레이어와 Clean Architecture

코드는 다음 세가지로 분리된다.

1. Presentation Layer(=UI Layer)

2. Domain Layer

3. Data Layer

 

아래는 각 레이어에 대한 패키지 상세 내용이다. 

우리가 사용하는 Android 앱 아키텍처 내에서도 파일/폴더 계층 구조를 구성하는 다양한 방법들이 있다.

Presentation Layer(=UI Layer)

Presentation Layer는 View(Activity, Fragment)와, 그리고 ViewModel들을 포함한다. 

**View는 가능한 단순해야된다. 절대 절대 비즈니스 로직을 넣지말아야 한다....** 

안드로이드 개발 문서 AAC에도 나와 있다.. 간단한 UI Logic은 괜찮지만, 비즈니스 로직은 놉.

 

Activity는 ViewModel과 소통하고, ViewModel은 액션을 취하기 위해 Domain Layer와 소통한다.

 

우선, 하나의 UseCaseHandler과 두개의 Usecase를 ViewModel에 전달한다. 아래 자세히 다루겠지만, 이 아키텍처에서 UseCase는 ViewModel이 Data Layer과 상호작용하는 방식을 정의하는 작업이다.

class PostListViewModel(
        val useCaseHandler: UseCaseHandler,
        val getPosts: GetPosts,
        val savePost: SavePost): ViewModel() {


    fun getAllPosts(userId: Int, callback: PostDataSource.LoadPostsCallback) {
        val requestValue = GetPosts.RequestValues(userId)
        useCaseHandler.execute(getPosts, requestValue, object :
        UseCase.UseCaseCallback<GetPosts.ResponseValue> {
            override fun onSuccess(response: GetPosts.ResponseValue) {
                callback.onPostsLoaded(response.posts)
            }

            override fun onError(t: Throwable) {
                callback.onError(t)
            }
        })
    }

    fun savePost(post: Post, callback: PostDataSource.SaveTaskCallback) {
        val requestValues = SavePost.RequestValues(post)
        useCaseHandler.execute(savePost, requestValues, object :
        UseCase.UseCaseCallback<SavePost.ResponseValue> {
            override fun onSuccess(response: SavePost.ResponseValue) {
                callback.onSaveSuccess()
            }
            override fun onError(t: Throwable) {
                callback.onError(t)
            }
        })
    }

}

Domain Layer

Domain Layer는 어플리케이션의 모든 UseCase를 갖고있다. 이 예에서는 추상화 클래스인 UseCase를 갖고 있으며, 모든 UseCase들은 이 클래스를 확장할 것이다.

abstract class UseCase<Q : UseCase.RequestValues, P : UseCase.ResponseValue> {

    var requestValues: Q? = null

    var useCaseCallback: UseCaseCallback<P>? = null

    internal fun run() {
        executeUseCase(requestValues)
    }

    protected abstract fun executeUseCase(requestValues: Q?)

    /**
     * Data passed to a request.
     */
    interface RequestValues

    /**
     * Data received from a request.
     */
    interface ResponseValue

    interface UseCaseCallback<R> {
        fun onSuccess(response: R)
        fun onError(t: Throwable)
    }
}

또한, UseCaseHandler는 UseCase의 핸들링을 담당한다. 절대 데이터베이스나 서버로부터 데이터를 가져오는 과정에서 UI가 멈추면 안된다. 

다음 코드는 백그라운드 스레드에서 UseCase를 실행하고 메인 스레드에서 응답을 수신하기로 결정하는 곳이다.

 

class UseCaseHandler(private val mUseCaseScheduler: UseCaseScheduler) {

    fun <T : UseCase.RequestValues, R : UseCase.ResponseValue> execute(
            useCase: UseCase<T, R>, values: T, callback: UseCase.UseCaseCallback<R>) {
        useCase.requestValues = values
        useCase.useCaseCallback = UiCallbackWrapper(callback, this)

        mUseCaseScheduler.execute(Runnable {
            useCase.run()
        })
    }

    private fun <V : UseCase.ResponseValue> notifyResponse(response: V,
                                                   useCaseCallback: UseCase.UseCaseCallback<V>) {
        mUseCaseScheduler.notifyResponse(response, useCaseCallback)
    }

    private fun <V : UseCase.ResponseValue> notifyError(
            useCaseCallback: UseCase.UseCaseCallback<V>, t: Throwable) {
        mUseCaseScheduler.onError(useCaseCallback, t)
    }

    private class UiCallbackWrapper<V : UseCase.ResponseValue>(
    private val mCallback: UseCase.UseCaseCallback<V>,
    private val mUseCaseHandler: UseCaseHandler) : UseCase.UseCaseCallback<V> {

        override fun onSuccess(response: V) {
            mUseCaseHandler.notifyResponse(response, mCallback)
        }

        override fun onError(t: Throwable) {
            mUseCaseHandler.notifyError(mCallback, t)
        }
    }

    companion object {

        private var INSTANCE: UseCaseHandler? = null
        fun getInstance(): UseCaseHandler {
            if (INSTANCE == null) {
                INSTANCE = UseCaseHandler(UseCaseThreadPoolScheduler())
            }
            return INSTANCE!!
        }
    }
}

이름에서 알 수 있듯이 GetPosts UseCase는 사용자의 모든 게시물을 가져오는 역할을 한다.

class GetPosts(private val mDataSource: PostDataSource) :
UseCase<GetPosts.RequestValues, GetPosts.ResponseValue>() {

    protected override fun executeUseCase(requestValues: GetPosts.RequestValues?) {
        mDataSource.getPosts(requestValues?.userId ?: -1, object :
        PostDataSource.LoadPostsCallback {
            override fun onPostsLoaded(posts: List<Post>) {
                val responseValue = ResponseValue(posts)
                useCaseCallback?.onSuccess(responseValue)
            }
            override fun onError(t: Throwable) {
                // Never use generic exceptions. Create proper exceptions. Since
                // our use case is different we will go with generic throwable
                useCaseCallback?.onError(Throwable("Data not found"))
            }
        })
    }
    class RequestValues(val userId: Int) : UseCase.RequestValues
    class ResponseValue(val posts: List<Post>) : UseCase.ResponseValue
}

UseCase의 목적은 ViewModel과 Repository 사이의 중재자 역할이다.

만약, 나중에 "게시물 작성" 기능을 추가한다고 가정해보자. 해야할것은 새로운 "EditPost" UseCase이고, 모든 코드는 다른 UseCase들로부터 완전 분리되어질 것이다.

아마, 공부를 좀 해보고 예제를 만들어봤다면 새로운 기능이 도입되고 기존 코드가 망가지는 현상을 많이 봤을 것이다...

분리된 UseCase는 이를 피할 수 있도록 만들어준다.

물론, 100퍼센트 이런 문제를 제거할 수는 없지만 최소화 할 수 있다. 

이것이 Clean Architecture를 다른 패턴과 구분하는 이유다. 코드를 완전히 분리시켜 모든 레이어를 블랙박스로 취급할 수 있다.

Data Layer

이 레이어는 도메인 레이어에서 사용할 수 있는 모든 레포지토리(저장소)를 갖고 있다. 이 레이어는 데이터 소스 API들을 외부 클래스로 노출시킨다.

interface PostDataSource {

    interface LoadPostsCallback {
        fun onPostsLoaded(posts: List<Post>)
        fun onError(t: Throwable)
    }

    interface SaveTaskCallback {
        fun onSaveSuccess()
        fun onError(t: Throwable)
    }

    fun getPosts(userId: Int, callback: LoadPostsCallback)
    fun savePost(post: Post)
}

PostDataRepository는 PostDataSouce를 시행한다. 이는 우리가 로컬 데이터베이스로부터 데이터를 가져오는지 혹은 서버로부터 요청하는지를 결정한다.

class PostDataRepository private constructor(
        private val localDataSource: PostDataSource,
        private val remoteDataSource: PostDataSource): PostDataSource {

    companion object {
        private var INSTANCE: PostDataRepository? = null
        fun getInstance(localDataSource: PostDataSource,
        remoteDataSource: PostDataSource): PostDataRepository {
            if (INSTANCE == null) {
                INSTANCE = PostDataRepository(localDataSource, remoteDataSource)
            }
            return INSTANCE!!
        }
    }
    var isCacheDirty = false
    override fun getPosts(userId: Int, callback: PostDataSource.LoadPostsCallback) {
        if (isCacheDirty) {
            getPostsFromServer(userId, callback)
        } else {
            localDataSource.getPosts(userId, object : PostDataSource.LoadPostsCallback {
                override fun onPostsLoaded(posts: List<Post>) {
                    refreshCache()
                    callback.onPostsLoaded(posts)
                }
                override fun onError(t: Throwable) {
                    getPostsFromServer(userId, callback)
                }
            })
        }
    }
    override fun savePost(post: Post) {
        localDataSource.savePost(post)
        remoteDataSource.savePost(post)
    }
    private fun getPostsFromServer(userId: Int, callback: PostDataSource.LoadPostsCallback) {
        remoteDataSource.getPosts(userId, object : PostDataSource.LoadPostsCallback {
            override fun onPostsLoaded(posts: List<Post>) {
                refreshCache()
                refreshLocalDataSource(posts)
                callback.onPostsLoaded(posts)
            }
            override fun onError(t: Throwable) {
                callback.onError(t)
            }
        })
    }
    private fun refreshLocalDataSource(posts: List<Post>) {
        posts.forEach {
            localDataSource.savePost(it)
        }
    }
    private fun refreshCache() {
        isCacheDirty = false
    }
}

이 클래스에는 localDataSource와 remoteDataSource라는 두개의 변수가 있다. 이 타입들은 PostDataSource이므로 실제로 내부적 구현 방식은 신경 쓰지 않는다.

상기 작가는 이렇게 말을한다.

 

[In my personal experience, this architecture has proved to be invaluable. In one of my apps, I started with Firebase on the back end which is great for quickly building your app. I knew eventually I’d have to shift to my own server.

When I did, all I had to do was change the implementation in RemoteDataSource. I didn’t have to touch any other class even after such a huge change. That is the advantage of decoupled code. Changing any given class shouldn’t affect other parts of your code.]

-> 개인적인 경험에 따르면, 이 아키텍처는 아주 귀중한 것으로 증명되었다. 나의 앱들 중 하나는, 백엔드에서 Firebase를 시작했는데 이는 앱을 빠르게 빌드하는데 유용하다. 하지만 결국 내 서버로 옮겨야 한다는 것을 알았다.

내가 해본결과, RemoteDataSource에서 구현을 변경하기만 하면된다. 이렇게 큰 변화를 겪은 뒤에도 다른 클래스를 건드릴 필요가 없었다. 이것이 분리된 코드의 장점이다. 주어진 클래스를 변경해도 코드의 다른 부분에는 영향을 미치지 않는다.

 

이거를 보고, 내 프로젝트 경험과 빗대어 생각해 보니 명확히 이해가 가며 이 아키텍처의 소중함을 다시한번 깨닫게 되었다.....와...

 

몇몇 부가적인 클래스는 다음과 같다.

interface UseCaseScheduler {

    fun execute(runnable: Runnable)

    fun <V : UseCase.ResponseValue> notifyResponse(response: V,
                                                   useCaseCallback: UseCase.UseCaseCallback<V>)

    fun <V : UseCase.ResponseValue> onError(
            useCaseCallback: UseCase.UseCaseCallback<V>, t: Throwable)
}


class UseCaseThreadPoolScheduler : UseCaseScheduler {

    val POOL_SIZE = 2

    val MAX_POOL_SIZE = 4

    val TIMEOUT = 30

    private val mHandler = Handler()

    internal var mThreadPoolExecutor: ThreadPoolExecutor

    init {
        mThreadPoolExecutor = ThreadPoolExecutor(POOL_SIZE, MAX_POOL_SIZE, TIMEOUT.toLong(),
                TimeUnit.SECONDS, ArrayBlockingQueue(POOL_SIZE))
    }

    override fun execute(runnable: Runnable) {
        mThreadPoolExecutor.execute(runnable)
    }

    override fun <V : UseCase.ResponseValue> notifyResponse(response: V,
                                                   useCaseCallback: UseCase.UseCaseCallback<V>) {
        mHandler.post { useCaseCallback.onSuccess(response) }
    }

    override fun <V : UseCase.ResponseValue> onError(
            useCaseCallback: UseCase.UseCaseCallback<V>, t: Throwable) {
        mHandler.post { useCaseCallback.onError(t) }
    }

}

UseCaseThreadPoolScheduler는 ThreadPoolExecuter를 사용해 비도기 작업을 실행하는 역할을 한다.

class ViewModelFactory : ViewModelProvider.Factory {


    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass == PostListViewModel::class.java) {
            return PostListViewModel(
                    Injection.provideUseCaseHandler()
                    , Injection.provideGetPosts(), Injection.provideSavePost()) as T
        }
        throw IllegalArgumentException("unknown model class $modelClass")
    }

    companion object {
        private var INSTANCE: ViewModelFactory? = null
        fun getInstance(): ViewModelFactory {
            if (INSTANCE == null) {
                INSTANCE = ViewModelFactory()
            }
            return INSTANCE!!
        }
    }
}

상기 viewModelFactory를 통해 우리는 viewModel 생성자에서 인수를 전달하려면 이를 생성해야 한다.

 

Dependency Injection

다음 예를 통해 depenjency injection을 설명해본다. PostDataRepository 클래스를 보면 LocalDataSource와 RemoteDataSource가 있다. 우리는 Injection클래스를 통해 이러한 디펜던시를 PostDataRepository 클래스에 제공해준다.

 

디펜던시를 injectiong하는 것은 두 가지 이점이 있다.

1. 전체 코드베이스에 걸쳐 개체를 분산시키는 대신 중앙 위치에서 개체의 인스턴스화를 제어할 수 있다는 것이다.

2. PostDataRepository에 대한 단위 테스트를 작성하는 데 도움이 된다. 왜냐하면 실제 값 대신 PostDataRepository 생성자에 LocalDataSource, RemoteDataSource의 모의 버전을 전달할 수 있기 때문이다.

object Injection {

    fun providePostDataRepository(): PostDataRepository {
        return PostDataRepository.getInstance(provideLocalDataSource(), provideRemoteDataSource())
    }

    fun provideViewModelFactory() = ViewModelFactory.getInstance()

    fun provideLocalDataSource(): PostDataSource = LocalDataSource.getInstance()

    fun provideRemoteDataSource(): PostDataSource = RemoteDataSource.getInstance()

    fun provideGetPosts() = GetPosts(providePostDataRepository())

    fun provideSavePost() = SavePost(providePostDataRepository())

    fun provideUseCaseHandler() = UseCaseHandler.getInstance()
}

[출처]

https://www.toptal.com/android/android-apps-mvvm-with-clean-architecture

Observable Data Object Work

식별 가능성은 객체가 데이터 변경에 관해 다른 객체에 알릴 수 있는 기능을 의미한다. 데이터 바인딩 라이브러리를  통해 객체, 필드 또는 컬렉션을 식별 가능하게 만들 수 있다.

 

간단한 기존 객체를 데이터 바인딩에 사용할 수는 있지만 객체를 수정해도 UI가 자동으로 업데이트 되지는 않는다. 데이터 바인딩을 사용하면 데이터 변경 시 리스너라는 다른 객체에 알리는 기능을 데이터 객체에 제공할 수 있다.

식별 가능한 클래스에는 세 가지 유형, 즉 객체, 필드 및 컬레션이 있다.

식별 가능한 데이터 객체 중 하나가 UI에 결합되고 데이터 객체의 속성이 변경되면 UI가 자동으로 업데이트 된다.

 

식별 가능한 필드

일부 작업은 Observable 인터페이스를 구현하는 클래스를 생성하는 작업과 관련이 있지만 클래스에 몇 가지 속성만 있다면 그다지 애쓸 필요 없다. 이러한 상황에서는 일반 Observable 클래스 및 다음과 같은 Primitive 관련 클래스를 사용하여 필드를 식별 가능하게 만들 수 있다.

식별 가능한 필드는 단일 피드가 있는 독립적인 식별 가능한 객체이다. Primitive 버전은 엑세스 작업 중에 박싱 및 언박싱을 방지한다. 이 메커니즘을 사용하려면 다음과 같이 자바언어로는 public final, Kotlin으로는 읽기 전용 속성을 만들어야 한다.

    class User {
        val firstName = ObservableField<String>()
        val lastName = ObservableField<String>()
        val age = ObservableInt()
    }

    
    private static class User {
        public final ObservableField<String> firstName = new ObservableField<>();
        public final ObservableField<String> lastName = new ObservableField<>();
        public final ObservableInt age = new ObservableInt();
    }

    

 필드 값에 엑세스 하려면 다음과 같이 set() 및 get() 접근자 메소드를 사용하거나 Kotlin 속성 구문을 사용한다.

    user.firstName = "Google"
    val age = user.age

    

식별 가능한 컬렉션

일부 앱은 동적 구조를 사용하여 데이터를 보유한다. 식별 가능한 컬렉션을 통해 키를 사용하여 이러한 구조에 엑세스 할 수 있다.

Ex) 다음은 키가 String과 같은 참조 유형일 때는 ObservableArrayMap 클래스가 유용하다.

    ObservableArrayMap<String, Any>().apply {
        put("firstName", "Google")
        put("lastName", "Inc.")
        put("age", 17)
    }

    

다음 과 같이 레이아웃에서 문자열 키를 사용하여 맵을 찾을 수 있다.

<data>
        <import type="android.databinding.ObservableMap"/>
        <variable name="user" type="ObservableMap<String, Object>"/>
    </data>
    …
    <TextView
        android:text="@{user.lastName}"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView
        android:text="@{String.valueOf(1 + (Integer)user.age)}"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    

Ex) 다음과 같이 레이아웃에서 색인을 통해 목록에 엑세스 할 수 있다.

<data>
        <import type="android.databinding.ObservableList"/>
        <import type="com.example.my.app.Fields"/>
        <variable name="user" type="ObservableList<Object>"/>
    </data>
    …
    <TextView
        android:text='@{user[Fields.LAST_NAME]}'
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView
        android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}'
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    

식별 가능한 객체

Observable 인터페이스를 구현하는 클래스를 사용하면 식별 가능한 객체의 속성 변경에 관해 알림을 받으려는 리스너를 등록할 수 있다.

Observable 인터페이스에 리스너를 추가 및 삭제하는 매커니즘이 있지만 알림이 전송되는 시점은 개발자가 직접 결정해야 한다.

더 쉽게 개발할 수 있도록 데이터 바인딩 라이브러리는 리스너 등록 메커니즘을 구현하는 BaseObservable 클래스를 제공한다.

BaseObservable을 구현하는 데이터 클래스는 속성이 변경될 때 알리는 역할을 한다. 

Ex) 다음과 같이 Bindable 주석을 getter에 할당하고 setter의 notifyPropertyChanged()메소드를 호출함으로써 이 작업을 완료한다.

    class User : BaseObservable() {

        @get:Bindable
        var firstName: String = ""
            set(value) {
                field = value
                notifyPropertyChanged(BR.firstName)
            }

        @get:Bindable
        var lastName: String = ""
            set(value) {
                field = value
                notifyPropertyChanged(BR.lastName)
            }
    }

데이터 바인딩은 데이터 바인딩에 사용된 리소스의 ID를 포함하는 모듈 패키지에 이름이 BR인 클래스를 생성한다. Binable 주석은 컴파일 중에 BR 클래스 파일에 항목을 생성한다. 데이터 클래스의 기본 클래스를 변경할 수 없으면 PropertyChangeRegistry 객체를 사용하여 Observable 인터페이스를 구현함으로써 효육적으로 리스너를 등록하고 리스너에 알림을 제공할 수 있다.

생성된 결합 클래스

데이터 바인딩 라이브러리는 레이아웃의 변수 및 뷰에 엑세스하는 데 사용되는 바인딩 클래스를 생성한다. 이번에는 생성된 바인딩 클래스를 만들고 커스텀하는 방법을 알아보자.

 

생성된 바인딩 클래스는 레이아웃 변수를 레이아웃 내의 뷰와 연결한다. 바인딩 클래스의 이름 및 패키지는 커스텀할 수 있다. 생성된 모든 바인딩 클래스는 ViewDataBinding 클래스에서 상속된다.

 

각 레이아웃 파일의 결합 클래스가 생성된다. 기본적으로 클래스 이름은 레이아웃 파일 이름을 기반으로 하여 파스칼 표기법으로 변환하고 Binding 접미사를 추가한다.

Ex) 상기 레이아웃 파일 이름은 activity_main.xml이므로 생성되는 클래스는 ActivityMainBinding이다. 이 클래스는 레이아웃 속성(Ex: user 변수)에서 레이아웃 뷰까지 모든 결합을 보유하며 결합 표현식에 값을 할당하는 방법도 인식한다.

 

결합 객체 만들기

레이아웃 내에서 표현식을 통해 뷰에 결합되기 전에는 뷰 계층 구조가 수정되지 않도록 하기 위해, 레이아웃 확장 후에 바로 결합 객체가 생성된다. 객체를 레이아웃에 결합하는 가장 일반적인 방법은 결합 클래스에서 정적 메소드를 사용하는 것이다.

Ex) 다음은 결합 클래스의 inflate() 메소드를 사용하여 뷰 계층 구조를 확장하고 객체를 뷰 계층 구조에 결합한다.

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val binding: MyLayoutBinding = MyLayoutBinding.inflate(layoutInflater)

        setContentView(binding.root)
    }

    

다음과 같이 LayoutInflater 객체 외에도 ViewGroup 객체를 사용하는 inflate() 메소드의 대체 버전이 있다.

    val binding: MyLayoutBinding = MyLayoutBinding.inflate(getLayoutInflater(), viewGroup, false)

    

레이아웃이 다른 매커니즘을 사용하여 확장되었다면 다음과 같이 별도로 결합될 수 있다.

    val binding: MyLayoutBinding = MyLayoutBinding.bind(viewRoot)

    

결합 유형을 미리 알 수 없는 상황도 있다. 그런 상황에서는 다음과 같이 DataBindingUtil 클래스를 사용하여 결합을 생성할 수 있다.

    val viewRoot = LayoutInflater.from(this).inflate(layoutId, parent, attachToParent)
    val binding: ViewDataBinding? = DataBindingUtil.bind(viewRoot)

    

Faagment, ListView 또는 RecyclerView Adapter에서 데이터 바인딩 항목을 사용하고 있다면, 다음 코드와 같이 바인딩 클래스 또는 DataBindingUtil 클래스의 inflate() 메소드를 사용할 수 있다.

    val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false)
    // or
    val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)

    

ID가 있는 뷰

데이터 바인딩 라이브러리는 레이아웃에 ID가 있는 각 뷰의 결합 클래스에 불변 필드를 생성한다.

Ex) 데이터 바인딩 라이브러리는 다음 레이아웃에서 TextView 유형의 firstName 및 lastName 필드를 생성한다.

<layout xmlns:android="http://schemas.android.com/apk/res/android">
       <data>
           <variable name="user" type="com.example.User"/>
       </data>
       <LinearLayout
           android:orientation="vertical"
           android:layout_width="match_parent"
           android:layout_height="match_parent">
           <TextView android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:text="@{user.firstName}"
       android:id="@+id/firstName"/>
           <TextView android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:text="@{user.lastName}"
      android:id="@+id/lastName"/>
       </LinearLayout>
    </layout>
    

라이브러리는 단일 패스로 뷰 계층 구조에서 ID가 포함된 뷰를 추출한다. 이 메커니즘은 레이아웃의 모든 뷰에 findViewById() 메소드를 호출하는 것보다 속도가 더 빠를 수 있다.

ID는 데이터 바인딩이 없을 때만큼 필요하지 않지만 코드에서 계속 뷰에 엑세스해야 하는 상황이 여전히 있다.

 

변수

데이터 바인딩 라이브러리는 레이아웃에 선언된 각 변수의 접근자 메소드를 생성한다.

Ex) 다음 레이아웃은 user, image, note 변수의 결합 클래스에 setter 및 getter 메서드를 생성한다.

<data>
       <import type="android.graphics.drawable.Drawable"/>
       <variable name="user" type="com.example.User"/>
       <variable name="image" type="Drawable"/>
       <variable name="note" type="String"/>
    </data>
    

ViewStub

일반 뷰와 달리 ViewStub 객체는 보이지 않는 뷰로 시작된다. 이 객체는 가시적으로 표시되거나 확장을 명시적으로 지시받으면 또 다른 레이아웃을 확장함으로써 레이아웃의 자체 뷰를 대체한다.

 

ViewStub이 기본적으로 뷰 계층 구조에서 사라지기 때문에 가비지 컬렉션을 통해 메모리 회수가 가능하도록 결합 객체의 뷰도 사라져야한다.

뷰가 최종적이므로 ViewStubProxy 객체는 생성된 결합 클래스에서 ViewStub를 대체하며, 이에 따라 개발자는 ViewStub가 존재할 경우 이에 엑세스 할 수 있고 ViewSub가 확장된 경우 확장된 뷰 계층 구조에도 엑세스 할 수 있다.

 

또 다른 레이아웃을 확장할 때 새 레이아웃의 결합을 설정해야 한다. 따라서 ViewStubProxy는 ViewStub OnInflateListener를 수신 대기하고 필요할 때 결합을 설정해야 한다. 지정된 시간에 하나의 리스너만 존재할 수 있으므로 ViewStubProxy를 통해 OnInflateListener를 설정할 수 있다. 설정된 이 리스너는 설정하면 호출된다.

즉시 결합

변수 또는 관찰 가능한 객체가 변경될  때 결합은 다음 프레임 이전에 변경되도록 예약된다. 하지만 결합이 즉시 실행되어야 하는 때도 있다. 이럴 때 강제로 실행하려면 executePendingBindings() 메소드를 사용하라.

 

고급 결합

동적 변수

구체적인 바인딩 클래스를 알 수 없을때도 있다.

Ex) 임의의 레이아웃에 작동하는 RecyclerView.Adapter는 특정 바인딩 클래스를 인식하지 못한다. 따라서 이 어댑터는 onBindViewHolder() 메소드를 호출하는 동안에도 계속해서 결합 값을 할당해야 한다.

Ex) 다음은 RecyclerView가 결합되는 모든 레이아웃에 item 변수가 있다.

BindingHolder 객체에는 ViewDataBinding 기본 클래스를 반환하는 getBinding() 메소드가 있다.

    override fun onBindViewHolder(holder: BindingHolder, position: Int) {
        item: T = items.get(position)
        holder.binding.setVariable(BR.item, item);
        holder.binding.executePendingBindings();
    }

    

* 데이터 바인딩 라이브러리는 모듈 패키지에 BR이라는 클래스를 생성한다. 이 클래스에는 데이터 바인딩에 사용된 리소스의 ID가 포함되어 있다. 위 예에서 라이브러리는 BR.item 변수를 자동으로 생성한다.

 

백그라운드 스레드

컬렉션이 아닌 한 백그라운드 스레드에서 데이터 모델을 변경할 수 있다. 데이터 바인딩은 계산 중에 각 변수/필드를 Localize하여 동시 실행 문제를 방지한다.

커스텀 바인딩 클래스 이름

기본적으로 바인딩 클래스는 레이아웃 파일 이름을 기반으로 하여 대문자로 시작하고 밑줄(_)을 삭제하며 다음 문자를 대문자로 표기하고 Binding이라는 단어를 접미사로 추가하는 방법으로 생성된다. 이 클래스는 모듈 패키지 아래의 databinding 패키지에 배치된다. 

Ex) 레이아웃 파일 contact_item.xml 이 ContactItemBinding클래스를 생성합니다. 모듈 패키지가 com.example.my.app 이면 바인딩 클래스가 com.example.my.app.databinding 패키지에 배치된다.

 

data요소의 class속성을 조정하여 바인딩 클래스의 이름을 바꾸거나 바인딩 클래스를 다른 패키지에 배치할 수 있다.

Ex) 다음 레이아웃은 현재 모듈의 databinding 패키지에 ContactItem 결합 클래스를 생성한다.

<data class="ContactItem">
        …
    </data>
    

클래스 이름 앞에 마침표를 접두사로 추가하여 다른 패키지에서 바인딩 클래스를 생성할 수 있다.

다음은 모듈 패키지에 바인딩 클래스를 생성한다.

<data class=".ContactItem">
        …
    </data>
    

또한 바인딩 클래스를 생성할 패키지의 전체 이름을 사용할 수 있다.

다음은 com.example 패키지에 ContactItem 결합 클래스를 만드는 예이다.

<data class="com.example.ContactItem">
        …
    </data>
    

+ Recent posts