반응형

안드로이드 개발 입문단계에서는,

View( Activity, Fragment )에서 View의 동작과, 데이터 처리까지 모두 작성하는 방법으로

안드로이드 앱을 제작했을 것이다.

 

하지만 이렇게 작성하게 되면 코드 작성은 쉬울 수 있지만,

여러 사람들과 협업을 했을 때, 코드 가독성이나 관리, 로직 구현이 굉장히 까다로워진다.

또한 하나의 class에 모든 처리를 위한 수많은 코드들이 들어가게 되어 스파게티 코드로 변환될 가능성이 있다.

 

그리고 UI에서 모든 걸 처리하기 때문에 비즈니스 로직에 따른 UI의 변화들을

직접 개발자가 바꿔줘야 하는 까다로움이 존재한다.

 

이로 인해 테스트 코드의 작성이 어려워지게 되며 유지보수에 굉장히 큰 어려움이 따를 수 있다.

 

이러한 단점을 극복하고자 나온 패턴이 바로 MVP 패턴과 MVVM 패턴이다.

MVP패턴 또한 장단점을 갖고 있으며, MVP패턴의 단점을 극복하고자 발전한 패턴이 MVVM 패턴이기 때문에

먼저 MVP 패턴부터 살펴보도록 하자.

 

MVP 패턴에 대해

먼저, MVP패턴은 구글에서 제공한 아키텍쳐 설계 코드를 기반으로 공부하였다.

MVC패턴과 MVP패턴의 차이점을 먼저 그림으로 보고, 정리해보고자 한다.

 

MVC 패턴

 

 

MVP패턴

 

MVC패턴에서는 Model과 Activity( View + Controller )로 구성이 되어있고

MVP 패턴은 Model과 View, Presenter로 구성되어 있다.

 

MVC패턴의 Activity에서 UI관련 처리부터 데이터 처리까지 모든 기능을 구현했다면

MVP패턴은 이들을 View와 Presenter로 분할한 것이다.

 

여기서 Presenter의 역할은 View와 Model을 분리하여,

서로간에 상호작용을 Presenter가 담당함으로써 서로간의 영향을 최소화 하는 것이다.

 


작동과정은 다음과 같다.

  1. View에서 사용자 이벤트가 발생하게 되면 이 이벤트를 Presenter로 전달하게 되고
  2. Presenter는 이 이벤트를 받아 이에 필요한 데이터를 Model로 요청하게 된다.
  3. Model은 Presenter의 호출에 따라 필요한 데이터를 Presenter로 전달하게 되고
  4. Presenter는 Model로부터 받은 데이터를 가공하여 View로 전달하게 된다.
  5. View는 이것들을 바탕으로 UI를 갱신하게 된다.

 

그럼 이제, 구글에서 제공한 샘플 코드를 가지고 MVP 패턴을 살펴보자.

(아래의 링크를 통해 전체 코드와 디렉토리 구조를 보면서 아래의 설명을 보도록 하자.)

구글에서 제공한 아키텍쳐 설계 코드

 

먼저 각각의 Task마다 View, Presenter, Contract가 작성된 것을 확인할 수 있는데

여기서 Contract는 View와 Presenter에 대한 interface를 작성하는 부분이다.

 

그래서 View는 Contract.View을 상속받아서 구현하게 되고

Presenter는 Contract.Presenter을 상속받아서 구현하게 된다.

 

Presenter에는 필요한 데이터를 Model로 요청하는 함수와 받아온 데이터를 View로 전달하는 코드들이 구현되어 있고

View에는 UI의 갱신과, Presenter로 이벤트 전달을 하는 코드들이 구현되어 있음을 알 수 있다.

 

하지만 코드를 뜯어보면 알 수 있듯이, View와 Presenter 간의 의존성이 높음을 알 수 있다.

 

View와 Model은 분리가 되었지만, 앱의 크기가 거대해지면서 Model에서 받아온 대량의 데이터에 따라

각각의 다른 UI를 표시해야 한다면, 각각의 조건에 맞는 로직을 추가해주어야 하고,

그렇게 되면 추가된 만큼의 코드가 생겨나면서 로직이 굉장히 거대해지기 때문에

코드의 길이 또한 상당히 길어지게 된다.

 

그렇게 되면 위에서 설명한 것 처럼 코드의 가독성이 떨어지며 테스트 또한 어려워지게 된다.

또한, 각각의 조건에 맞게 UI가 올바르게 변경되었는지 확인작업도 필요할 것이다.

 

그래서 이러한 단점을 극복하기 위해 MVVM 패턴과 Usecase 등이 등장하게 되었다.

 

그래서 다음 포스팅으로 MVVM 패턴을 소개하려고 한다.

반응형
반응형

오늘 안드로이드 코틀린의 코루틴에 대해 알아보고자 한다.

 

  • Coroutine vs Thread를 통해 Coroutine을 사용하는 이유 알아보기
  • Coroutine Dispatcher
  • Coroutine Context
  • Coroutine Scope
  • Coroutine Builder + Job
  • suspend(일시 중단 함수)
  • Coroutine 직접 사용해보기

Coroutine vs Thread

Thread

 

Task단위 => Thread

  • 각 작업에 Thread를 할당
  • 각 Thread는 자체 Stack 메모리를 가지며, JVM Stack 영역 차지

Context Switching

  • Blocking
    (Thread1이 Thread2의 결과가 나올 때 까지 기다려야 한다면 Thread1은 Blocking되어 사용하지 못함)

Coroutines

 

Task 단위 => Object(Coroutine)

  • 각 작업에 Object(Coroutine)을 할당
  • Coroutine은 객체를 담는 JVM Heap에 적재

Context Switching

  • No Context Switching
  • 코드를 통해 Switching 시점을 보장함
  • Suspend is NonBlocking : Coroutine1이 Coroutine2의 결과가 나올 때 까지 기다려야 한다면 Coroutine1은 Suspend 되지만, Coroutine1을 수행하던 Thread는 유효함
  • Coroutine2도 Coroutine1과 동일한 Thread에서 실행할 수 있음

 

Coroutine Dispatcher

  • 코루틴을 시작하거나 재개할 스레드를 결정하기 위한 도구
  • 모든 Dispatcher는 CoroutineDispatcher 인터페이스를 구현해야 함

 

Coroutine Context

Coroutine을 운동 선수에 비유하고, CoroutineContext를 경기장으로 비유해보자.

축구선수는 축구장에 배치되어야 할 것이고, 농구선수는 농구장에 배치되어야 할 것이다.

 

이처럼, Coroutine의 실행 목적에 맞게 실행될 특정 ThreadPool을 지정해 주어야 한다.

CoroutineContext에는 Dispatchers.Main / Dispatcher.IO / Dispatchers.Default ... 가 있는데

  • Main에는 UI 관련 작업이 모여있는 쓰레드풀이며
  • IO에는 데이터를 읽고 쓰는 작업이 모여있는 쓰레드풀이며
  • Default는 기본 쓰레드풀로, CPU 사용량이 많은 작업에 적합한 쓰레드풀이다.

그래서 각각의 Coroutine의 목적에 맞게 CoroutineContext를 지정해 주면 된다.

 

 

Coroutine Scope

위의 Coroutine Context로 Coroutine이 어디서 실행될지를 결정했다면,

이제 Coroutine을 제어할 수 있는 범위( 즉, Scope ) 를 지정해주어야 한다.

 

제어는 작업을 취소시키거나, 작업이 끝날 때 까지 기다리는 것을 의미하는데

이러한 제어를 할 범위를 정해주는 것이다.

 

이러한 Coroutine Scope의 종류에는 크게 두 가지가 있다.

  • 사용자 지정 Coroutine Scope
  • GlobalScope

먼저, 사용자 지정 Coroutine Scope는 가장 기본적인 방식의 CoroutineScope 이다.

val scope = CoroutineScope(CoroutineContext /* Dispatchers.Main과 같은*/ )
val job = scope.launch{
   
}

이렇게 특정 Coroutine이 필요할 때 마다 새로 선언하여 사용하고,

필요 없어지면 종료하도록 할 수 있다.

 

예를 들어, 어떤 Activity에 보여줄 데이터를 코루틴을 통해 불러오고 있다고 생각해보자.

근데 해당 Activity가 도중에 종료가 되버린다면 불러오고 있는 데이터는 필요가 없게 되므로

코루틴도 함께 종료되어야 한다.

 

이 때, Coroutine Scope를 Activity의 LifeCycle에 맞춰주면 Activity가 종료될 때 코루틴도 함께 종료되도록 만들 수 있다.

 

반면, GlobalScope는 앱이 실행될 때 부터 종료될 때 까지 코루틴을 실행시킬 수 있는 Scope이다.

GlobalScope.launch {
	
}

그래서 어떤 Activity에서 GlobleScope를 통해 실행된 Coroutine은 해당 Activity가 종료되어도

코루틴이 완료될 때 까지 동작하게 된다.

 

따라서, 앱이 실행되는 동안 혹은 장시간 실행되어야 하는 코루틴의 경우에는 이 GlobalScope를 사용하고

필요할 때만 수행되어야 하는 코루틴의 경우에는 사용자 지정 Coroutine을 사용하자.

 

 

Coroutine Builder

CoroutineBuilder에는 async{} , launch{}, runBlocking{} 이 있다.

Coroutine Scope의 확장함수로써 { 내부 코드 } 내부의 코드를 Coroutine으로 실행시켜주는 역할을 한다.

 

  • async()
    • 결과가 예상되는 코루틴 시작에 사용(결과 반환)
    • 전역으로 예외 처리 가능
    • 결과, 예외 반환 가능한 Deferred 반환
  • launch()
    • 결과를 반환하지 않는 코루틴 시작에 사용(결과 반환X)
    • 자체/ 자식 코루틴 실행을 취소할 수 있는 Job 반환
  • runBlocking()
    • Blocking 코드를 일시 중지(Suspend) 가능한 코드로 연결하기 위한 용도
    • main함수나 Unit Test때 많이 사용됨
    • 코루틴의 실행이 끝날 때 까지 현재 스레드를 차단함

CoroutineBuilder에서 반환된 Job, Deferred 객체를 이용하여 각각의 Coroutine을 제어할 수 있다.

 

예를 들어, job.join()은 코루틴이 완료될 때 까지 기다리는 함수이다.

이러한 job의 유용성 때문에 GlobalScope사용을 지양하는 편이다.

GlobalScope에는 job이 없기 때문에 구조화된 동시성이 느슨해지게 된다.

 

다음으로 넘어가기 전에, runBlocking{}과 async{}을 비교해보고 가자.

 

먼저, runBlocking()

fun main() = runBlocking {
	val name = getFirstName()
	val lastName = getLastName()
	println("Hello, $name $lastName")
}

suspend fun getFirstName() : String{
	delay(1000)
	return "Hong"
}

suspend fun getLastName() : String{
	delay(1000)
	return "Coding"
}

결과값은 Hello, Hong Coding => 총 2초가 소요된다.

 

아래는 async() 이다.

fun main() = runBlocking {
	val name = Defered<String> = async { getFirstName() }
	val lastName = Defered<String> = async { getLastName() }
	println("Hello, ${name.await()} ${lastName.await()}")
}

suspend fun getFirstName() : String{
	delay(1000)
	return "Hong"
}

suspend fun getLastName() : String{
	delay(1000)
	return "Coding"
}

결과값은 Hello, Hong Eunho => 총 1초가 소요됨

async()는 각각의 함수가 동시에 실행되어 반환되므로 두 과정 모두 1초의 delay를 동시에 겪지만

runblocking() 은 한 코루틴이 끝날 때 까지 현재 스레드를 차단하기 때문에 2초가 소요된다.

 

Suspend(일시 중단 함수)

  • Coroutine 혹은 suspend func 내부에서 사용하기 위해 만드는 함수
  • 앞에 suspend 키워드를 붙여서 함수를 구성하며
  • 이렇게 suspend를 붙이게 되면 하나의 코루틴으로 동작하기 위한 자격을 얻게 됨.
  • 람다를 구성하여 다른 suspend 함수를 호출함 ex) runBlocking{ ... }

 

Coroutine 직접 사용해보기

먼저, 아래와 같이 종속성을 추가하여 코루틴을 사용할 셋팅을 하자.

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1"

 

다음, 아래와 같은 Coroutine을 만들어 보자.

val scope = CoroutineScope(Dispatchers.Main)
scope.launch {
	// Coroutine1
}
scope.launch(Dispatchers.Default) {
	// Coroutine2
}

먼저 Coroutine1쪽의 위를 보면 CoroutineContext를 Dispatchers.Main으로 지정함을 알 수 있다.

그리고 여기서 Coroutine1이 실행된다.

 

하지만 Coroutine2쪽을 보면 다시 scope.launch(Dispatchers.Default)를 통해 Context를 재지정해줌을 알 수 있다.

 

이렇게 CoroutineContext는 CoroutineScope에서 지정될 수도 있고 CoroutineBuilder에서 지정될 수도 있는데,

Coroutine2처럼 CoroutineBuilder에서 따로 CoroutineContext를 설정해준다면,

해당 CoroutineBuilder로 실행되는 코루틴은 그 ContextContext를 따라가게 된다.

 

즉, 코루틴2는 Dispatchers.Default를 따라가게 되는 것이다.

그래서, 코루틴1은 MainThread, 코루틴2는 DefaultThread(background)에서 실행된다.

 

이렇게 UI작업을 진행하는 CoroutineScope에서 하나의 코루틴만 Background 작업으로 돌리고 싶을 때 이러한 방법을 사용한다.

 

이번엔 Job을 사용해보자.

val job = scope.launch {
	// todo
}

위의 Coroutine Builder에서 설명한 것 처럼 각 코루틴에 대한 Job 객체를 반환받아 각각의 코루틴을 제어할 수 있다.

 

근데 만약 하나의 코루틴 Scope내에 여러 자식 코루틴이 있을 때, 이 코루틴들을 한번에 제어하려면 어떻게 해야할까?

각각의 코루틴들에 job1, job2, job3 ... 으로 각각의 job을 연결해주고 마지막에 cancel을 해야할까?

 

아니다, 다음과 같은 방법을 사용하자.

suspend fun main() = coroutineScope {
	val job = Job()
	CoroutineScope(Dispatchers.Default + job).launch {
		launch {
			println("Coroutine1 start")
			delay(1000)
			println("Coroutine1 end")
		}
		launch {
			println("Coroutine2 start")
			delay(1000)
			println("Coroutine2 end")
		}
	}
	delay(1000)
	job.cancel()
	delay(2000)
	println("Finish")
}

위의 코드처럼 하나의 Job 객체를 생성하고 새로 선언될 CoroutineScope에서 객체를 초기화 하면

이 CoroutineScope의 자식들에게 까지 모두 영향을 주는 Job으로 활용이 가능하다.

 

CoroutineScope 내부의 코루틴들은 기본적으로 자신이 속한 CoroutineScope의 Context를 상속받기 때문이다.

그럼 코루틴의 부모- 자식 간의 관계를 살펴보고 가자.

 

기본적으로 부모 코루틴이 취소되면 자식 코루틴도 취소되며

부모 코루틴은 자식 코루틴이 모두 완료될 때 까지 대기한다.

 

하지만, 어떤 CoroutineScope안에 있다고 해서 모두 자식 Coroutine은 아니다.

 

다음과 같은 코드를 살펴보자.

suspend fun main() = coroutineScope{
	val job = Job()
    CoroutineScope(Dispatchers.Default+job).launch {
        launch{ // Child O
            println("coroutine1 start")
            delay(1000)
            println("coroutine1 end")
        }
        CoroutineScope(Dispatchers.IO).launch { // Child X
            println("coroutine2 start")
            delay(1000)
            println("coroutine2 end")
        }
        CoroutineScope(Dispatchers.IO + job).launch { // Child O
            println("coroutine2 start")
            delay(1000)
            println("coroutine2 end")
        }
    }
    delay(300)
    job.cancel()
    delay(3000)
    println("Finish")
}

위의 코드를 실행시켜보면 다음과 같은 결과를 얻을 수 있다.

Coroutine1 Start
Coroutine2 Start
Coroutine3 Start
Coroutine2 End
Finish

하나의 launch안에 여러개의 Coroutine이 존재하는데, Coroutine2만 end되고 끝나버렸다.

 

왜냐하면, Coroutine1,3 / Coroutine2 의 제어범위가 다르기 때문이다.

Coroutine2는 기존의 Scope와는 상관없이 새로운 CoroutineScope(Dispatcher.IO) 를 생성하여

코루틴을 실행시킨다.

 

그래서 외부 CoroutineScope.launch의 취소 여부는 Coroutine2에 어떠한 영향도 미치지 않는다.

 

하지만 Coroutine3도 새로운 Scope를 생성하는 방식인데 취소가 되었다.

왜냐하면 CoroutineContext + job 때문이다.

 

job으로 인해 상위 코루틴과 연결이 되면서 상위 코루틴의 영향을 받게 된다.

반응형
반응형

ListAdapter + DiffUtil 사용 이유

기존에는 RecyclerView의 Adapter는 RecyclerView.Adapter를 이용하여 구성하였다.

 

이는 RecyclerView에서 데이터가 변경되었을 경우 notifyDatasetChanged()를 사용하게 되는데

이 경우, 리스트 내의 데이터를 모두 바꾸게 된다.

따라서, 데이터가 매우 많을 경우 시간 지연이 발생하게 된다.

 

이러한 불편한 점을 해소하기 위해 ListAdapter + DiffUtil을 활용하게 된다.

 

DiffUtil은 현재 데이터 리스트와 교체할 데이터 리스트를 비교하여

변경이 필요한 부분만을 뽑아내어 변경을 하기 때문에

notifyDatasetChanged() 보다 훨씬 빠른 시간내에 리스트를 변경할 수 있게 되기 때문이다.

그럼 RecyclerViewAdapter와 코드를 비교해보며 사용법을 알아보자.

 

// RecyclerView.Adapter 클래스 선언 부분
class QuotesPagerAdapter(private val quotes: List<Quote>, private val isNameRevealed: Boolean
): RecyclerView.Adapter<QuotesPagerAdapter.QuoteViewHolder>() {
	...

// ListView.Adapter + DiffUtil 클래스 선언 부분
class HouseListAdapter: ListAdapter<HouseModel,HouseListAdapter.ItemViewHolder>(diffUtil) { 
	inner class ItemViewHolder(private val view: View): RecyclerView.ViewHolder(view) { 
    ...

ListAdapter는 클래스 선언시 따로 리스트를 인자로 넘겨주지 않아도 된다.

 

다만, 리스트의 데이터를 교체해야 할 경우에는 adapter를 사용하는 클래스에서

adapter.submitList(it.books)

이렇게 리스트를 넘겨주어 교체하게 된다.

 

DiffUtil은 리스트 변경 전 후의 데이터를 비교하여 변경이 필요한 부분만 변경한다고 했는데,

그 기능을 하는 코드가 다음 코드이다.

companion object {
    val diffUtil = object: DiffUtil.ItemCallback<HouseModel>() {
        override fun areItemsTheSame(oldItem: HouseModel, newItem: HouseModel): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: HouseModel, newItem: HouseModel): Boolean {
            return oldItem == newItem
        }

    }
}
 

areItemsTheSame()은 두 아이템이 동일한 아이템인지 체크한다.

 

위에서는 id를 기준으로 두 아이템의 id가 같다면 동일한 아이템으로 체크하는것을 알 수 있다.

areContentsTheSame()은 두 아이템이 동일한 컨텐츠를 가지고 있는지 체크한다.

이 함수는 위의 areItemsTheSame()이 같은 경우에만 호출하게 된다.

이렇게 두 함수를 통해 변경이 필요한 부분에 대해서만 List변경을 하게 되는 것이다.

 

따라서 기존의 RecyclerViewAdapter보다 효율적이라고 볼 수 있기 때문에

앞으로는 RecyclerView를 구성할 때 ListAdapter+DiffUtil을 활용할 예정이다.

반응형
반응형

오늘은 안드로이드의 MVVM 패턴의 핵심이라고 할 수 있는 ViewModel에 대해 알아보려고 한다.

학습내용

  • ViewModel의 개념
  • ViewModel과 생명주기
  • ViewModel의 사용목적
  • 직접 ViewModel 사용해보기

 

ViewModel

  • UI관련 데이터 저장 및 UI로직을 처리 및 관리하기 위해 만들어짐
  • LifeCycle 패키지에 포함된 것에서 알 수 있듯이 생명주기를 고려해서 동작하도록 구현됨

 

ViewModel과 생명주기

  • View의 생명주기는 안드로이드 FrameWork에 의해 관리됨
  • 화면 회전이나 글씨 크기 변경 등 구성 변경 발생시 View는 Destroy되고 다시 재생성
  • ViewModel은 View의 Lifecycle에 scoping되어 View가 완전히 종료될 때 까지
  • 객체가 유지됨
  • View -> ViewModel -> Repository의 구조로 의존성이 단방향으로만 생성
  • UI데이터 저장과 로직 처리를 View에서 분리함

 

ViewModel의 사용 목적

  • View는 최대한 모르게. 멍청하게(?)
  • Android의 까다로운 View 생명주기 때문에 실수하기 쉬운 코드를 예방할 수 있음
  • 테스트 코드 작성이 간편해짐
  • 동일한 Activity에 attach된 Fragment간 데이터 공유가 편리함

 

ViewModel 사용해보기

ViewModel을 사용하기 위해 먼저 implementation를 추가하자.

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1' (최신버전에 맞춰 수정)


그리고 다음과 같이 viewModel 변수를 정의하여 사용할 준비를 한다.

private val viewModel: MainViewModel by lazy{
	// onCreate 이후에 호출되어야 함
	ViewModelProvider(this)[MainViewModel::class.java]
}

ViewModel 객체는 이렇게 ViewModelProvider를 통해 받아야 한다.

 

ViewModel 사용 전과 후의 코드 비교

<전>

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private val count = 0L

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)

        setContentView(binding.root)
        initViews()
    }

    private fun initViews() {
        binding.increaseButton.setOnClickListener {
            count++
            binding.countText.text = count.toString()
        }
    }
}

<후>

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    private val viewModel: MainViewModel by lazy{
        ViewModelProvider(this)[MainViewModel::class.java]
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)

        setContentView(binding.root)
        initViews()
    }

    private fun initViews() {
        binding.increaseButton.setOnClickListener {
            viewModel.increaseCount()
        }
        viewModel.count.observe(this) {
            binding.countText.text = it.toString()
        }
    }
}

class MainViewModel: ViewModel() {
    private val _count = MutableLiveData<Long>().apply {
        value = 0
    }
    
    val count: LiveData<Long>
        get() = _count

    fun increaseCount() {
        _count.value = (count.value ?: 0) + 1
    }
}
 

<전>과 <후>의 코드를 보면 알 수 있듯이

ViewModel을 이용하면 View가 직접 데이터 처리를 하지 않는다.

 

데이터의 저장과 로직 처리는 모두 ViewModel에서 이루어지고 있고,

View는 뷰모델을 observe하다가 변화가 일어나면 단지 view를 업데이트 하는 역할을 한다.

 

즉, 그동안 View에서 UI 데이터의 저장과 로직처리를 모두 했다면

이 역할을 ViewModel로 분리시킨 것이다.

반응형
반응형

오늘은 안드로이드의 Room을 이용해 LocalDB를 구성한 예제를 소개하려고 한다.

 

Room에는 다음과 같은 3가지 개념이 존재한다.

  • Database (데이터베이스)
  • Entity ( 데이터베이스 내의 테이블 )
  • DAO ( 데이터베이스에 접근하는 함수. insert, update, delete 등등 )

이 개념을 가지고 Room을 이용해 보자.

먼저, Room을 이용하기 위해선, build.gradle에 다음과 같이 추가하자.

implementation 'androidx.room:room-runtime:2.2.6'
kapt 'androidx.room:room-compiler:2.2.6'

 

그리고 Entity를 구성해보자.

다음과 같이 History라는 데이터클래스 파일을 만들고 class위에 Entity 어노테이션을 추가하여

Entity로 사용할 것을 명시하자.

@Entity
data class History (
    @PrimaryKey val uid: Int?,
    @ColumnInfo(name = "keyword") val keyword: String?
)

 

그리고 DAO를 만들어 데이터베이스 테이블에 쿼리로 접근할 수 있도록 인터페이스를 만들자.

 

다음과 같이 @Dao 어노테이션을 추가하여 Dao로 사용할 것을 명시한 후

HistoryDao 인터페이스를 만들어 주었다.

@Dao
interface HistoryDao {

    @Query("SELECT * FROM history")
    fun getAll() : List<History>

    @Insert
    fun insertHistory(history: History)

    @Query("DELETE FROM history WHERE keyword == :keyword")
    fun delete(keyword: String)
}

 

마지막으로 AppDatabase를 만들어주자.

AppDatabase는 실질적으로 Room을 구현하는 부분이며 RoomDatabase를 상속받는다.

 

코드의 @Database(...) 부분의 version은 테이블이 추가되거나 변경되면 바꿔주어야 한다.

이 때, 마이그레이션 과정이 필요하다.

그 마이그레이션 함수가 밑에 위치한 Migration(1,2) 이다. version1 -> version2

 

AppDatabase에 추상 클래스로 historyDao와 reviewDao를 명시함으로써

각각의 Dao에 접근할 수 있도록 했다.

 

마지막으로 Builder를 통해 실질적으로 RoomDatabase를 만들어 주었다.

@Database(entities = [History::class, Review::class], version = 2)
abstract class AppDatabase: RoomDatabase() {
    abstract fun historyDao(): HistoryDao
    abstract fun reviewDao() : ReviewDao
}

val migration_1_2 = object : Migration(1,2) {
        override fun migrate(database: SupportSQLiteDatabase) {
            database.execSQL("CREATE TABLE 'REVIEW' ('id' INTEGER, 'review' TEXT," + "PRIMARY KEY('id'))")
        }
    }

fun getAppDatabase(context: Context): AppDatabase {
	return Room.databaseBuilder(
        context, AppDatabase::class.java, "BookSearchDB"
    )
        .addMigrations(migration_1_2)
        .build()
}

 

이렇게 만든 Local DB를 액티비티에서 실제로 사용해보자.

private lateinit var db: AppDatabase
db = getAppDatabase(this)
db.historyDao().getAll()

이렇게 함으로써 위에서 만들어놓은 AppDatabase와

각각의 Dao, Entity에 접근하여 원하는 DB를 얻을 수 있게 된다.

 

Local DB를 이용하면

서버를 이용하지 않고 자체적으로 DB를 생성하기 때문에

서버 의존도와, 서버의 용량을 줄일 수 있어 작은 프로젝트에 효율적으로 사용할 수 있다.

 

반응형
반응형

오늘은 안드로이드의 View Binding(뷰바인딩)에 대해 알아보려고 한다.

 

ViewBinding

ViewBinding(뷰 바인딩)은 뷰와 상호작용하는 코드를 보다 쉽게 작성할 수 있는 기능이다.

뷰바인딩을 사용함으로써, 기존에 사용하던 findViewById를 대체할 수 있다.

 

뷰 바인딩을 이용하기 위해서는 build.gradle에 다음과 같이 명시하여야 한다.

viewBinding {
    enabled = true
}

 

그럼 이제 실제로 뷰 바인딩을 이용해보자.

먼저 예시에서는 메인 액티비티에서 사용을 하려고 한다.

 

기존에는 MainActivity와 activity_main_xml을 연결하기 위해

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

위 코드와 같이 작성하여 activity_main 레이아웃 파일과 액티비티를 연결하였고

 

button = findViewById(R.id.button)

이러한 형식으로 버튼 등의 뷰를 연결했을 것이다.

 

하지만 뷰 바인딩을 이용하면 다음과 같이 코드가 바뀐다.

private lateinit var binding: ActivityMainBinding
....
 override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
     	setContentView(binding.root)

binding이라는 변수에 Activity 뷰 전체가 연결되어 들어가는 형식이다.

 

그래서 binding.root를 하게 되면 activity_main 뷰 전체가 해당이 되고

binding.button을 하게 되면 activity_main 뷰 안의 버튼을 가져올 수 있게 된다.

 

ActivityMainBinding은 임의로 이름을 지은것이 아니라,

레이아웃 파일의 이름에 맞게 탄생한다.

 

activity_main.xml 이면 ActivityMainBinding이 되는 것고

item_book.xml 이면 ItemBookBinding이 되는 것이다.

반응형
반응형

오늘은 간단한 예제를 통해, 안드로이드에서 Retrofit을 사용하는 방법에 대해 알아보려고 한다.

 

학습내용

  • Kotlin을 이용한 Retrofit 사용법 숙지
  • Retrofit을 이용해 인터파크 도서 api 데이터 가져오기
  • 받아온 api 데이터를 바탕으로 안드로이드 뷰 그려보기

코틀린에서 Retrofit 사용

먼저 사용하기 전에, gradle에 코드를 추가하여 셋팅을 하자.

implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

그럼 본격적으로, Retrofit을 이용해 인터파크 도서 api의 데이터를 받아오자.

먼저, 이번 학습에서는 데이터를 받아오기만 할 것이기 때문에 GET만을 이용하였다.

 

GET이외에도 POST, PUT, PATCH, DELETE 등이 존재하는데

  • POST는 자원을 생성(Create)할 때
  • PUT은 자원을 수정할 때 ( 전체 수정 )
  • PATCH은 자원을 수정할 때 ( 일부 수정)
  • DELETE은 자원을 삭제할 때

사용하게 된다.

 

먼저, 나는 BookService 라는 인터페이스 안에

베스트셀러 목록을 받아올 함수(getBestSellerBooks)와

키워드 검색 결과로 책 목록을 받아올 함수(getBooksByName)를 만들었다.

interface BookService {

    @GET("/api/bestSeller.api?output=json&categoryId=100")
    fun getBestSellerBooks(
            @Query("key") apiKey: String
    ): Call<BestSellerDto>

	@GET("/api/search.api?output=json")
    fun getBooksByName(
            @Query("key") apiKey: String,
            @Query("query") keyword: String
    ): Call<SearchBookDto>
}

이렇게 인터페이스에 함수를 만들어 놓으면

실제 api를 호출할 액티비티에서 편리하게 이 함수를 호출하여 결과값을 받아올 수 있다.

 

위 코드를 자세히 설명하자면,

@GET( " 여기에 baseurl을 제외한 api 주소를 기입 ") ,

 

getBestSellerBooks 함수와

getBooksByName 함수를 호출하여 인터파크 API로부터 json 데이터를 받아온다.

 

이 때, 받아온 json 데이터를 코틀린에서 사용하기 위해서는

코틀린 형식에 맞게 가공해야 하는데,

이를 위해 BestSellerDto와 SearchBookDto 라는 데이터 클래스를 만들어 변환하였다.

 

작성한 BestSellerDto의 데이터 클래스는 다음과 같다.

data class BestSellerDto(
    @SerializedName("title") val title: String,
    @SerializedName("item") val books: List<Book>
)

api의 수많은 데이터 중, 내가 받아오고 싶은 것들만 추려서 데이터 클래스로 만들었다.

@SerializedName을 통해 실제 api에서 전달해주는 파라미터 명을 일치시킨 후

뒤의 변수명은 내가 짓고싶은 것으로 지으면 되는데, 어느정도 연관성 있게 짓는것이 좋다.

 

그럼 이제, 액티비티에서 위의 자료들을 가지고 실제 api를 호출하여 데이터를 얻어와 보자.

먼저 다음과 같이 Retrofit을 빌드하여 retrofit이라는 변수에 저장을 해주자.

val retrofit = Retrofit.Builder()
    .baseUrl("https://book.interpark.com")
    .addConverterFactory(GsonConverterFactory.create())
    .build()

 

그리고, 이 빌드한 retrofit을 create() 하여 생성을 하자.

bookService = retrofit.create(BookService::class.java)

 

그럼 이제 아까 BookService 인터페이스 안에 정의해두었던 함수들을 호출하여 사용할 수 있다.

bookService.getBestSellerBooks(getString(R.string.interParkAPIKey))
        .enqueue(object: Callback<BestSellerDto> {
            override fun onResponse(call: Call<BestSellerDto>, response: Response<BestSellerDto>) {
                if (response.isSuccessful.not()) {
                    return
                }
                response.body()?.let {
                    Log.d(TAG, it.toString())
                    it.books.forEach { book ->
                        Log.d(TAG, book.toString())
                    }
                    adapter.submitList(it.books)
                }
            }

            override fun onFailure(call: Call<BestSellerDto>, t: Throwable) {
                // TODO 실패처리
            }

        })

 

Retrofit을 사용하는 부분에 있어서는 자바와 크게 다른점은 없는 것 같다.

기본적인 자바와 코틀린의 함수 정의 방법이나 호출, 매개변수, 널처리 등등에 있어서 차이가 났던 부분만 신경 써주면 금방 적용할 수 있을 것 같다.

반응형
반응형

https://developer.android.com/topic/libraries/view-binding

 

뷰 결합  |  Android 개발자  |  Android Developers

뷰 결합 뷰 결합 기능을 사용하면 뷰와 상호작용하는 코드를 쉽게 작성할 수 있습니다. 모듈에서 사용 설정된 뷰 결합은 모듈에 있는 각 XML 레이아웃 파일의 결합 클래스를 생성합니다. 바인딩

developer.android.com

Fragment에서 ViewBinding을 사용할 경우 메모리 누수(Memory leak)를 조심해야 합니다.

그 이유를 Fragment의 생명주기와 함께 알아봅시다.

 

먼저, Fragment의 생명주기를 보면 다음과 같습니다.

Fragment Lifecycle

위 Fragment의 생명주기를 보면 onCreate 이후에 onCreateView가 호출되는 것을 알 수 있습니다.

onCreate는 Fragment가 생성될 때 호출되며, onCreateView는 Fragment의 뷰를 구성할 때 호출됩니다.

 

즉, onCreate는 아직 화면이 보이지 않은 상태(View가 생성되지 않은 상태)에서 실행되고

onCreateView는 화면(뷰)을 구성할 때 실행됩니다.

 

그럼 반대로, Fragment을 닫을때는 View를 먼저 닫고 Fragment를 닫아야 하겠죠? ( 괄호를 먼저 열고 늦게 닫는 개념 )

그래서 onDestroyView가 호출된 후에 onDestroy가 호출됩니다.

 

따라서, Fragment는 View 보다 생명주기가 오래 지속되게 됩니다.

 

이 내용을 기억한 상태로, 다음 코드를 봅시다.

    private var _binding: ResultProfileBinding? = null
    // This property is only valid between onCreateView and
    // onDestroyView.
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = ResultProfileBinding.inflate(inflater, container, false)
        val view = binding.root
        return view
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

위 코드의 onCreateView에서 binding을 선언하여 뷰를 결합함으로써 view에 대한 reference를 참조하게 됩니다.

이 때, 만약 Fragment A에서 Fragment B로 전환이 된다면

Fragment A의 View는 사라지게 되지만 Fragment A는 아직 종료되지 않은 상태로 남게 됩니다.

그래서 Fragment는 binding변수를 계속 갖고있는 상태로 view에 대한 reference를 참조하고 있게 됩니다.

 

즉, Fragment A의 View요소들은 쓰이지 않음에도 불구하고 아직 view에 대한 reference가 남아 있기 때문에

메모리 누수가 발생하게 됩니다.

 

그래서 이를 방지하기 위해 바로 아랫 부분의 onDestroyView에서 binding변수를 null로 만들어 view에 대한 reference의 참조를 해제해 주는 겁니다.

 

위의 설명이 바로 상단의 안드로이드 공식 문서 링크에 나와있는 설명인

"프래그먼트는 뷰보다 오래 지속됩니다. 프래그먼트의 onDestroyView() 메서드에서 결합 클래스 인스턴스 참조를 정리해야 합니다." 입니다.

 

따라서, Fragment에서 ViewBinding을 사용할 때는

메모리누수를 방지하기 위해 위의같이 onDestroyView에서 binding을 null로 만들어 참조를 해제해주는 습관을 가지는 것이 좋습니다.

 

 

 

이 방법 외에도, Binding 사용시 메모리 누수를 방지하는 것에 대한 여러가지 방법이 있지만

오늘은 위와 같은 방법을 소개를 드렸습니다.

 

안드로이드 개발에 관련한 사항들은 github에도 주기적으로 업로드 하니 언제나 팔로우&맞팔은 환영입니다!

https://github.com/HongEunho/

반응형

+ Recent posts