Reactive Programing(1) – 리액티브 프로그래밍 개념잡기
Reactive Programing(2) – Reactive Operator
Reactive Programing(3) – Reactive Operator
Reactive Programing(4) – Scheduler
Reactive Programing(5) – 안드로이드에서의 RxJava 활용


안드로이드에서 RxJava2 사용하기

 
자바는 함수형 프로그래밍을 제대로 지원하지 못하고 있으며, 여전히 Side Effect(부수효과)를 완벽하게 제거하지 못했다.
이러한 이유로 안드로이드에서 RxJava2를 사용할 수 있는 RxAndroid 라이브러리가 등장하게 되었다.
 

RxAndroid란?

기본적으로는 RxJava2가 바탕이 되며, 안드로이드에서 필요한 몇몇 클래스를 추가한 라이브러리 입니다.
안드로이드 앱 개발시 어려움을 겪는 문제 중 하나가 멀티스레드 사용입니다.
멀티스레드 사용시 2개이상의 비동기 처리, 에러처리,핸들러, 콜백, 이벤트 중복 등 복잡한 처리를 할 수 밖에 없고 이로인해 개발에 많은 자원이 투입됩니다.
RxAndroid는 이러한 문제를 쉽게 해결해 줄 것입니다.
 

RxAndroid 설치하기

 
앱레벨에 build.gradle에 다음과 같이 의존성을 추가합니다.

implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
implementation 'io.reactivex.rxjava2:rxjava:2.1.13'

참고로 RxAndroid는 RxJava2를 참조하고 있어 반드시 RxJava2의 의존성을 추가 할 필요는 없습니다.
하지만 최신버전의 RxJava사용을 위해 명시해주는것이 좋습니다.
최신버전의 라이브러리 이용은 아래의 링크를 참조해주세요.
참조링크 : https://github.com/ReactiveX/RxAndroid
 

RxAndroid의 기본

기본적 구성요소는 RxJava2와 같아 내용을 생략하고, RxAndroid에서 추가된 기능 위주로 설명하겠습니다.

RxAndroid의 스케줄러

스케쥴러 설명
newThread() 새로운 스레드 생성
single() 단일 스레드 생성 후 사용
computation() 계산용 스레드
io() 네트워크, 파일 입출력 스레드
trampoline() 현제 스레드에 대기행렬 생성

 

UI 이벤트 처리

사용자는 대부분 View를 클릭하는것으로 애플리케이션과 상호작용하게 됩니다.
다음 예제를 보면서 어떻게 Observable을 활용하는지 살펴보시기 바랍니다.

Observable.create<View> { text_view.setOnClickListener(it::onNext) }
        .map {
            "TextView is clicked"
        }
        .subscribe {
            Log.e(TAG, it)
        }

kotlin-android-extension을 플러그인을 사용하고 있어 id로 View를 참조 할 수 있는 상태입니다.
text_view라는 아이디를 가진 TextView를 미리 만들었습니다.
create함수를 통해 Observable을 생성하고 text_view가 클릭되었을때 데이터를 발행하도록 했습니다.
 

실시간 키워드 검색 구현하기

키워드를 입력하고 제한된 시간내에 입력이 없으면 검색을 시작하는 코드를 만들어보도록 하겠습니다.

val source = Observable.create<CharSequence> { emitter ->
    edit_text.addTextChangedListener(object : TextWatcher {
        override fun afterTextChanged(s: Editable?) {
        }
        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
        }
        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
            s?.let {
                emitter.onNext(it)
            }
        }
    })
}
source.debounce(3000L, TimeUnit.MILLISECONDS)
        .filter { !TextUtils.isEmpty(it) }
        .observeOn(AndroidSchedulers.mainThread()) //Toast must be running on UI Thread
        .subscribe {
            Toast.makeText(this, "searching => $it",Toast.LENGTH_SHORT).show()
        }

debounce함수를 통해 3000ms 내에 입력이 없으면 해당 키워드를 검색합니다.
filter를 통해 빈 문자열은 출력하지 않도록 했습니다.
이번에는 Log가 아닌 Toast를 이용했습니다. 로그는 쓰레드와 관계 없이 출력되지만,
Toast는 UI쓰레드(Main쓰레드) 에서만 작동하기 때문입니다.
스케줄러를 AndroidSchedulers.mainThread() 로 해주지 않으면 에러가 발생하게 됩니다.
Note:액티비티 빠르게 실행했을때중복 실행 되는 문제도 debounce를 사용하면 간단히 해결할 수 있습니다.
 

RecyclerView, RxJava, DataBinding활용하여 갤러리 앱 만들기

소스 : https://github.com/Charlezz/GalleryApp
 
Observable을 활용하여 비동기 이벤트를 처리하는 기본적인 활용방법에 대해서 알아보겠습니다.

MainActivity.kt

package com.charlezz.galleryapp.ui
import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.support.v4.app.ActivityCompat
import android.support.v7.app.AppCompatActivity
import com.charlezz.galleryapp.R
import io.reactivex.rxkotlin.Observables
import io.reactivex.rxkotlin.toObservable
class MainActivity : AppCompatActivity() {
    val TAG = MainActivity::class.java.simpleName
    val permissions = arrayOf(
            Manifest.permission.READ_EXTERNAL_STORAGE
    )
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        permissions.toObservable()
                .filter {
                    ActivityCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
                }
                .toList()
                .map {
                    permissions.sortedArray()
                }
                .subscribe { permissionList: Array<String>?, _: Throwable? ->
                    permissionList?.let {
                        ActivityCompat.requestPermissions(this, it, 0)
                    }
                }
    }
    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        Observables.zip(
                permissions.toObservable(),
                grantResults.toObservable())
                .filter {
                    it.second != PackageManager.PERMISSION_GRANTED
                }.count()
                .subscribe { permissionCount: Long?, t2: Throwable? ->
                    if (permissionCount == 0L) {//zero means all permissions granted
                        supportFragmentManager.beginTransaction()
                                .replace(R.id.container, GalleryFragment.newInstance())
                                .commit()
                    } else {
                        finish()
                    }
                }
    }
}

permission 을 요청할 배열(permissions)을 미리 만듭니다. 갤러리 앱의 경우 READ_EXTERNAL_STORAGE만 있으면 됩니다.
퍼미션 배열을 옵저버블로 변경하여 현재 허가되지 않은 퍼미션을 추려낸 뒤 requestPermissions메소드를 이용하여 권한을 요청하고 있습니다.
요청된 권한은 사용자로 하여금 허용 또는 거부를 할 수 있으며 전부 허용할 시 에만 다음 동작( Fragment  띄우기) 을 수행하도록 하고, 한개라도 권한을 거부 했다면 finish() 로 앱을 종료하는 모습입니다.
 
GalleryFragment.kt

class GalleryFragment : Fragment() {
    val TAG = GalleryFragment::class.java.simpleName
    companion object {
        fun newInstance() = GalleryFragment()
    }
    lateinit var binding: FragmentGalleryBinding
    val adapter = GalleryAdapter()
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        binding = FragmentGalleryBinding.inflate(inflater, container, false)
        return binding.root
    }
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding.recyclerView.adapter = adapter
        binding.recyclerView.layoutManager = GridLayoutManager(context, 3)
    }
    override fun onResume() {
        super.onResume()
        adapter.clearItems()
        getItemsObservable()
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe {
                    adapter.addItem(it, true)
                }
    }
    fun getItemsObservable(): Observable<GalleryData> {
        val cursor = context?.contentResolver?.query(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                arrayOf(
                        MediaStore.Images.Media.DISPLAY_NAME,
                        MediaStore.Images.Media.DATA
                ),
                null,
                null,
                null)
        cursor?.let {
            return Observable.fromIterable(RxCursorIterable.from(cursor))
                    .subscribeOn(Schedulers.io())
                    .doAfterNext {
                        if (it.isLast) {
                            it.close()
                        }
                    }
                    .map { c ->
                        val name = c.getString(c.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME))
                        val path = c.getString(c.getColumnIndex(MediaStore.Images.Media.DATA))
                        GalleryData(name, path)
                    }
        }
        return Observable.empty<GalleryData>()
    }
}

getItemObservable메소드를 보면 비동기적으로 이미지 이름과 경로를 GalleryData라는 데이터클래스에 발행하게 되어있다.
onResume()에서는 mainThread스케줄러를 이용하여 구독을 하고 있다. (View를 건드릴수 있는건 오로지 메인쓰레드 뿐!)
 
GalleryAdapter.kt

class GalleryAdapter : RecyclerView.Adapter<GalleryAdapter.GalleryViewHolder>() {
    val items = ArrayList<GalleryData>()
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GalleryViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        val binding = VhGalleryBinding.inflate(
                inflater,
                parent,
                false)
        return GalleryViewHolder(binding)
    }
    fun addItem(item: GalleryData, update: Boolean) {
        items.add(item)
        if (update) {
            notifyItemChanged(items.lastIndex)
        }
    }
    fun clearItems() {
        items.clear()
        notifyDataSetChanged()
    }
    override fun getItemCount(): Int {
        return items.size
    }
    override fun onBindViewHolder(holder: GalleryViewHolder, position: Int) {
        holder.binding.galleryData = items[position]
        holder.binding.executePendingBindings()
    }
    class GalleryViewHolder(binding: VhGalleryBinding) : RecyclerView.ViewHolder(binding.root) {
        val binding = binding
    }
}

addItem메소드를 통하여 리스트에 이미지를 하나씩 추가 하고 있다.
onBindViewHolder 에서는 데이터 바인딩을 라이브러리를 활용하여 데이터를 주입하고 있는 모습이다.


Reactive Programing(1) – 리액티브 프로그래밍 개념잡기
Reactive Programing(2) – Reactive Operator
Reactive Programing(3) – Reactive Operator
Reactive Programing(4) – Scheduler
Reactive Programing(5) – 안드로이드에서의 RxJava 활용


 

카테고리: RxJava

1개의 댓글

비제이퍼블릭 · 2019년 7월 16일 5:18 오후

안녕하세요 개발자님. 저는 출판사 비제이퍼블릭의 편집자 이동원이라고 합니다.
비제이퍼블릭은 IT 개발서 전문 출판사로, 온라인 서점에서 ‘비제이퍼블릭’으로 검색해보시면 그간의 출간 내역을 확인하실 수 있고,
홈페이지 http://www.bjpublic.co.kr
블로그 http://bjpublic.tistory.com
페이스북 https://www.facebook.com/Bjpublic.co.kr/
을 통해서도 정보를 얻으실 수 있습니다.

개발자님께서 블로그에 올리신 RxJava 관련 포스팅 내용을 보고 콘텐츠가 도서로 출간되어도 경쟁력이 있을 것으로 판단되어 집필을 제의드리게 되었습니다.

지금 제의 드리는 기획 외에도, 개발자님께서 따로 생각 중이신 기획안이 있으시다면 마찬가지로 논의 가능합니다. 집필 기간은 통상 3~6개월로 잡지만 개발자님의 사정에 따라서 더 늘려 잡으셔도 괜찮습니다. 생각해보시고 괜찮으시다면 010-9553-3757이나 whitecooky@bjpublic.co.kr로 회신 부탁드립니다.

아무쪼록 긍정적으로 검토하여 주시기를 바라며 이만 줄이겠습니다. 감사합니다.
이동원 드림

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.