본문 바로가기

[Android]/App UI 따라 만들기

[배달의 민족] UI 따라만들기 3편 (Viewpager2, Coroutine, LifecycleScope)

안녕하세요 허접샴푸입니다~!

 

배달의 민족 따라만들기 3편입니다! 3편에서는 자동으로 스크롤 되는 ViewPager2에 대해 알아 보도록 하겠습니다.

구글에서 밀고 있는 Coroutine을 사용하도록 하겠습니다.

 

[자료]

지난 2편에서는 좌우로 Swipe 되는 이미지 배너를 구현하였죠? 이번 편에서는 좌우로 손을 대지 않아도 알아서 Swipe 되도록 하겠습니다. 잘 아시겠지만 자동이라고 하면 결국 thread 를 사용해야 하는데 구글에서 권장하고 있는 놈이 바로 Coroutine입니다. 즉 유저는 앱을 사용하고 있으면서, 이미지 배너는 자동으로 Swipe 되는 비동기 처리가 되어야 합니다. 이에 적합한 것이 바로 Coroutine 입니다. 그리고 LifecycleScope를 사용하여 편하게 만들어 보도록 하겠습니다. 

 

LifecycleScope를 사용한 이유는, 아래 설명에 나와있듯이 Lifecycle이 끝나면 알아서 코루틴이 취소되기 때문에 따로 관리해줄 필요가 없다는 것이죠. 직접 thread를 만들어서 어떠한 비동기 처리를 한다고 가정하면, Android lifecycle에 맞춰서 thread를 생성해주었다가 없애주었다가 프로세스를 진행했다가 멈추었다가 하는 등의 관리를 직접해주는 골치 아픈 코딩을 했다면, LifecycleScope을 사용하면 그런 골치 아픈 관리를 해줄 필요가 없다는 것입니다. 얼마나 편리합니까? 

 

LifecycleScope

LifecycleScope는 각 Lifecycle 개체에서 정의됩니다. 이 범위에서 시작된 모든 코루틴은 Lifecycle이 끝날 때 취소됩니다. lifecycle.coroutineScope 또는 lifecycleOwner.lifecycleScope 속성을 통해 Lifecycle의 CoroutineScope에 액세스할 수 있습니다.

 

Coroutine : https://developer.android.com/kotlin/coroutines

 

Kotlin 코루틴으로 앱 성능 향상  |  Android 개발자  |  Android Developers

코루틴은 비동기적으로 실행되는 코드를 간소화하기 위해 Android에서 사용할 수 있는 동시 실행 설계 패턴입니다. 코루틴은 Kotlin 버전 1.3에 추가되었으며 다른 언어에서 확립된 개념을 기반으로 합니다. Android에서 코루틴은 다음 두 가지 기본 문제를 해결하는 데 도움이 됩니다. 기본 스레드를 차단하여 앱이 정지될 수 있는 장기 실행 작업을 관리합니다. 기본 안전, 즉 기본 스레드에서 네트워크 또는 디스크 작업을 안전하게 호출하는 기능을 제공

developer.android.com

LifecycleScope: https://developer.android.com/topic/libraries/architecture/coroutines

 

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

Kotlin 코루틴은 비동기 코드를 작성할 수 있게 하는 API를 제공합니다. Kotlin 코루틴을 사용하면 코루틴이 실행되어야 하는 시기를 관리하는 데 도움이 되는 CoroutineScope를 정의할 수 있습니다. 각 비동기 작업은 특정 범위 내에서 실행됩니다. 아키텍처 구성 요소는 LiveData와의 상호운용성 레이어는 물론 앱의 논리적 범위에 대한 코루틴을 가장 잘 지원합니다. 본 항목에서는 아키텍처 구성요소를 통해 코루틴을 효과적으로 사용하는 방법

developer.android.com


LETS GO!

[시작]

(사전작업)

build.gradle(Module: app)의 dependencies{}에 아래 dependencies를 추가해주시기 바랍니다.

// coroutine --> Coroutine 사용하기 위해(지금은 딱히 필요하진 않습니다만, 미래를 위해 미리 추가를 합시다 ^_^)
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.4'

// lifecyclescope --> LifecycleScope 사용하기 위해
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha01'

 

(1) MainActivityViewModel.kt 수정하기

- currentPosition 라이브 데이터를 만드는 이유는, 흠 따지고 보면 딱히 만들 필요는 없습니다. 왜냐하면 지금 저희가 만든 화면은 Activity이며, Activity는 화면 전환간에 View가 새롭게 생성되지 않기 때문입니다. 만약 Fragment 라면, fragment 특성상 화면 전환간에 view가 늘 새롭게 생성되기 되기 때문에 이미지 배너의 현재 position을 알고 있지 않으면 position은 늘 0이라 화면 전환할 때마다 이미지 배너는 0번째 부터 swipe를 시작할 것입니다. 또한 fragment 에서는 viewpager가 view이기 때문에 재생성될 것이기 때문이죠.

- setter와 getter을 만들어 놓아서 필요할 때 사용할 수 있도록 미리 코딩해 놓습니다.

 

(2) Interaction.kt 생성하기, ViewPagerAdapter 수정하기 (Click Listener)

[Interaction.kt]

- 해당 interface를 만들어서 이미지 배너를 클릭했을 때 동작이 이루어지도록 만듭니다.

 

[ViewPagerAdapter.kt]

- 위에 만든 interaction을 생성자에 선언하여 인자로 받을 수 있도록 합니다.

- bind 함수 안에 itemView.setOnClickListener{}를 작성하면 바로 배너의 항목을 클릭하면 이 내부 동작이 수행됩니다. 

- interaction.onBannerItemClicked(bannerItem)이라고 작성하여 MainActivity.kt에서 해당 동작을 수행할 수 있도록 만들어 놓습니다.

 

(3) MainActivity.kt 수정하기 

- 먼저 autoScrollViewPager() 함수를 만듭니다. 여기서 lifecycleScope.launch를 사용하여 비동기 처리인 코루틴을 사용하도록 합니다. 해당 Activity가 destroy되지 않는 이상 해당 코루틴은 계속 존재하기 때문에 autoScrollViewPager() 함수를 onCreate에서 한번만 호출해줍니다. 

- isRunning  변수를 만들어, onPause, onResume 때는 멈춤과 재실행이 가능하도록 합니다. 왜냐하면 만약 사용자가 다른 앱을 사용하기 위해 잠시 현재 앱을 비활성화 해도, destroy가 되지 않으면 해당 coroutine은 살아있으며 while문은 계속 돌기 때문입니다. 그럼 쓸데없이 계속 while 문이 뒤에서 실행되어 배터리를 갉아먹을 필요는 없으니 onPause에서는 실행되지 않도록 isRunning=false로 만들어주는 것입니다.

- 다시 lifecycleScope.launch 내부를 살펴보면, while(isRunning)을 통해 isRunning의 값이  true 일 때 계속 코루틴이 실행할 수 있도록 합니다. delay(3000)은 3초 뒤에 실행하라는 뜻이므로, 3초 뒤에 이미지 배너가 자동으로 Swipe된다고 보면 됩니다.

- viewModel.getcurrentPosition()?.let {} 은 단순히 현재 이미지 배너의 position값을 null 체크를 하여 it로 받아오는 것이며, viewModel.setCurrentPosition((it.plus(1)) % 5)에서 it.plus(1)은 현재 position에 1을 더하라는 뜻이며 5로 나눠준 나머지 값으로 set한 이유는 이미지 배너 item 개수가 5개 이기 때문입니다. 0, 1, 2, 3, 4, 5, 6, 7 ... 이런식으로 계속해서 1씩 증감이 될텐데, 5로 나눈 나머지로 설정하면 0, 1, 2, 3, 4, 0, 1, 2, 3 .... 이 되기 때문입니다.

- viewModel.setCurrentPosition((it.plus(1)) % 5) 로 설정해놓으면 currentPosition을 편하게 observe할 수 있게 됩니다. 그래서 subscribeObservers() 함수 내부에 currentPosition을 observe할 수 있도록 코딩을 한 뒤, currentPosition이 바뀔 때마다 viewPager2(이미지 배너)의 현재 item을 설정해줍니다. (setCurrentItem()) 이를 통해 이미지 배너는 해당 position으로 자동 swipe되게 됩니다.

- initViewPager2() 함수 내부에 onPageSelected 오버라이드 함수 안에 viewModel.setCurrentPosition(position)를 선언해주셔야 합니다. 그렇지 않으면 현재 position이 3이라고 가정했을 때, coroutine에 의해 3초 뒤면 3이 4가 됩니다. 그런데 유저가 만약 이미지 배너를 직접 swipe하여 현재 position이 3이 아니고 2라고 생각해 보세요. 이를 뷰모델에 알려주지 않으면, 눈에 보여지는 position은 2이지만, 실제 뷰모델이 기억하는 position은 3이기 때문에 2에서 4로 자동 swipe이 되게 됩니다. 이를 막기 위해서는 저 코드를 작성해줍니다.

- viewPager2.apply에서 viewPagerAdapter = ViewPagerAdapter(this@MainActivity) 로 수정을 하여 현재 class인 MainActivity가 Interaction을 implement하도록 만들어 줍니다. 그리고 onBannerItemClicked 오버라이드 함수를 추가해 주신다음 startActivity(Intent(this@MainActivity, EventActivity::class.java))를 작성합니다. 이를 통해 이미지 배너의 특정 아이템을 클릭하면 EventActivity라는 새로운 Activity가 열리게 됩니다.

- 참고로 onPageSelected 에서 isRunning=true라고 작성한 이유는 이 코드가 없으면 클릭 이벤트를 통해 EventActivity를 열고 닫고나면 한번씩 자동 swipe가 멈춰버립니다. 저의 잘못된 설계 때문인 것 같습니다. isRunning 도한 뷰모델에서 관리하면 이런 문제가 없었을 텐데(아니려나?... ㅋㅋ), EventActivity가 열리고 닫히면 MainActivity의 onPause와 onResume이 불리게 되는데, 여기서 isRunning이 가끔씩 false로 나타납니다. 즉 onResume에서 isRunning=true가 한번씩 안불리는 것 같은데 이는 제가 좀더 디버깅해서 다음 편에서 수정 및 보완하도록 하겠습니다.

 

(4) EventActivity.kt 생성 (Activity이므로 Manifest에 추가해주시기 바랍니다!)

[activity_event.xml]

[EventActivity.kt]

- 단순합니다. activity_event.xml에서도 ActionBar를 커스텀하게 만들어줍니다. 왜냐하면 아래 그림을 보면 배달의민족 앱에서 그렇게 하였기 때문입니다! 

- 꼽표 누르면 해당 activity가 꺼지도록 클릭 리스터를 구현하면 끗!

 

 

(결과 영상)

 

[Youtube] 처음부터 끝까지 만드는 영상을 준비해 보았습니다.

https://www.youtube.com/watch?v=vP9_aTm5XzM

 

[Github]

https://github.com/DJDrama/BaeminPractice/tree/ViewPager2-Coroutine-LifecycleScope

 

DJDrama/BaeminPractice

Contribute to DJDrama/BaeminPractice development by creating an account on GitHub.

github.com

 

이로써 자동 스크롤, 스와이프 되는 이미지 배너의 구현을 끝냈습니다!

참 쉽죠? 다음 4편에서는 이미지 배너의 밑에 놈들을 싹 구현할 수 있도록 해보겠습니다~!

많은 관심과 사랑 부탁드립니다...

혹시 이 외에도 만들고 싶으신 UI가 있는데 도저히 어떻게 만드는지 모르겠으면 댓글로 남겨주시면 제가 해결해드리겠습니다!

그럼 4편에서 봐요~!