RxAndroid로 리액티브 앱 만들기 #3

  1. RxAndroid로 리액티브 앱 만들기 #1
  2. RxAndroid로 리액티브 앱 만들기 #2
  3. RxAndroid로 리액티브 앱 만들기 #3
  4. RxAndroid로 리액티브 앱 만들기 #4

RxAndroidRxBinding 버전 변경 내역에 대한 피드백을 주신 Park ChulWoo님에게 감사드립니다.


사용자 인터페이스는 한 쪽을 움직이면 다른 한 곳이 바뀌고, 다른 한 곳을 바꾸면 또 다른 곳이 바뀌는 복잡한 상호작용의 연속입니다. RxAndroid가 제공하는 다양한 옵저버블과 오퍼레이터 등을 합성하여 사용자 인터페이스를 효과적으로 구조화할 수 있습니다. 다양한 옵저버블과 오퍼레이터를 하나씩 살펴봅시다.

클릭의 추상화

안드로이드에서 필수적인 옵저버블은 RxBinding에 구현되어 있습니다. (1.0.0 이전에 RxAndroid에 포함되어 있었으나 분리되었습니다.) 이를 사용하기 위해 build.gradle에 아래 부분을 추가하십시요.

compile 'com.jakewharton.rxbinding:rxbinding:0.3.0'

그 중 가장 유용한 도구는 RxView.clicks입니다.

RxView
    .clicks(findViewById(R.id.button))
    .map(event -> new Random().nextInt())
    .subscribe(value -> {
        TextView textView = (TextView) findViewB yId(R.id.textView);
        textView.setText("number: " + value.toString());
    }, throwable -> {
        Log.e(TAG, "Error: " + throwable.getMessage());
        throwable.printStackTrace();
    });

RxView.clicksView 타입을 인자로 받는 정적 메서드로 setOnClickListener를 통해 OnClickListener에 전달될 이벤트를 옵저버블 형태로 래핑합니다. 대부분의 안드로이드 콤퍼넌트들이 View 타입을 상속받고 있기 때문에 이 정적 메서드를 사용하여 간단하게 클릭 이벤트를 처리할 수 있습니다.

위 예제의 map은 전달 받은 event 값을 무시하고 랜덤 값으로 바꿉니다. 기존 event가 담긴 옵저버블 대신 랜덤 값이 담긴 새로운 observablesubscribe에 연결되며 버튼이 클릭될 때 마다 아이디가 textViewTextView의 값이 임의의 숫자로 변경됩니다.

옵저버블 병합

이런 개발 뉴스를 더 만나보세요

사용자 인터페이스를 작성하다보면 여러 경로로 온 이벤트를 동시에 처리해야 하는 경우가 많습니다. 이런 경우 개별 이벤트를 옵저버블로 받은 후 병합을 시도할 수 있습니다.

Observable<String> lefts = RxView.clicks(findViewById(R.id.leftButton))
        .map(event -> "left");

Observable<String> rights = RxView.clicks(findViewById(R.id.rightButton))
        .map(event -> "right");

Observable<String> together = Observable.merge(lefts, rights);

together.subscribe(text -> ((TextView) findViewById(R.id.textView)).setText(text));

together.map(text -> text.toUpperCase())
        .subscribe(text -> Toast.makeText(this, text, Toast.LENGTH_SHORT).show());

먼저 두개의 Button에 대해 RxView.clicks를 적용하여 이벤트를 리턴하는 Observable을 얻었습니다. 이를 map을 통해 이벤트 대신에 leftright라는 문자열을 반환하는 Observable<String>을 둘 얻습니다. 이를 각기 leftsrights에 대입합니다.

Observable.merge를 통해 leftsrights로 전달되온 두 옵저버블을 병합하여 하나의 옵저버블로 묶어 together에 넣어둡니다. 이렇게 묶으면 together에는 왼쪽 버튼이 클릭되었을 때 left란 글이 오른쪽 버튼이 클릭되었을 때는 right라는 글자가 흘러다니게 됩니다.

together를 두가지 용도로 활용을 합니다. 첫번째는 subscribe 메서드를 통해 바로 사용합니다. R.id.textView의 뷰를 가져와서 거기에 넘겨온 값을 설정합니다. 왼쪽 버튼이 클릭되었으면 left이 해당 뷰에 오른 버튼이 클릭되었으면 right가 해당 뷰에 표시됩니다.

두번째 용도는 먼저 map을 통해 한단계 가공을 거칩니다. 이 가공에서 글귀를 모두 대문자로 바꿉니다. (text -> text.toUpperCase()) 대문자로 바꾸고 난 다음에는 그것을 넘겨 받아 subscribe에서 사용합니다. 대문자로 바귄 글귀들은 Toast.makeText를 통해 토스트로 출력이 되지요.

데이터를 전체적으로 다루는 스캔

병합 된 데이터를 하나씩 처리해야할까요? 병합 된 데이터를 누적적으로 처리할 수 있는 도구 스캔을 알아봅시다.

Observable<Integer> minuses = RxView.clicks(findViewById(R.id.minusButton))
    .map(event -> -1);
Observable<Integer> pluses = RxView.clicks(findViewById(R.id.plusButton))
    .map(event -> 1);
Observable<Integer> together = Observable.merge(minuses, pluses);
together.scan(0, (sum, number) -> sum + 1)
    .subscribe(count ->
        ((TextView) findViewById(R.id.count)).setText(count.toString()));
together.scan(0, (sum, number) -> sum + number)
    .subscribe(number ->
        ((TextView) findViewById(R.id.number)).setText(number.toString()));

먼저 RxView.clicks 부터 살펴보겠습니다. R.id.minusButton 버튼의 이벤트를 -1이라는 숫자로 바꾸어서 minuses에 담습니다. R.id.plusButton 버튼의 이벤트를 1이란 숫자로 바꾸어 pluses 이벤트에 담고요.

다음으로 minusespluses 옵저버블을 하나의 옵저버블로 병합합니다. (Observable.merge(minuses, pluses))

이제 처음 보는 오퍼레이터 scan이 나왔습니다. 첫번째 인자는 초기값 (0), 두번째 인자는 실제 데이터를 처리할 람다 ((sum, number) -> sum + 1)입니다. 람다는 두가지 인자를 가지고 있는데요. 첫번째는 누적된 값(sum)이고 두번째는 매번 옵저버블로 부터 들어오는 데이터(number)입니다.

첫번째 scantogether 옵저버블을 통해 전달되는 값을 number로 받지만 전혀 사용하고 있지 않습니다. 그 값을 버리고 sum + 11을 더하고 있죠. 초기값이 0으로 설정되어 있기 때문에 sum은 처음에 0이 되고 이후에는 누적된 값이 sum에게 전달이 되게 됩니다. 아무 버튼을 누르든 sum1씩 증가하게 되고 어떤 버튼이든 누른 횟수가 됩니다.

두번째 scannumber로 값을 받는 것은 동일합니다만 그 값 자체를 sum + number로 더한다는 차이가 있습니다. 플러스 버튼을 눌렀다면 초기값 01이 더해지고 마이너스 버튼을 눌렀다면 -1이 더해집니다. 결국 플러스 버튼을 누를때 마다 값은 1씩 증가하고 -1을 누를 때 마다 값은 감소합니다.

한번에 처리하는 컴바인

여러 조건이 완성될 때 처리해야할 이벤트들이 있습니다. 예를 들면 패스워드 재확인은 패스워드가 입력되어있고 패스워드 재확인 폼에도 글자가 채워져 있을 때만 우리게에 유의미한 데이터를 전달 할 수 있습니다. 이런 종류의 데이터를 처리하기 위해 컴바인을 알아봅시다.

CheckBox checkBox1 = (CheckBox) findViewById(R.id.checkBox1);
EditText editText1 = (EditText) findViewById(R.id.editText1);

Observable<Boolean> checks1 = RxCompoundButton.checkedChanges(checkBox1);

checks1.subscribe(check -> editText1.setEnabled(check));

Observable<Boolean> textExists1 = RxTextView.text(editText1)
    .map(MainActivity::isEmpty);

Observable<Boolean> textValidations1 = Observable
    .combineLatest(checks1, textExists1, (check, exist) -> !check || exist);

CheckBox checkBox2 = (CheckBox) findViewById(R.id.checkBox2);
EditText editText2 = (EditText) findViewById(R.id.editText2);

Observable<Boolean> checks2 = RxCompoundButton.checkedChanges(checkBox2);

checks2.subscribe(check -> editText2.setEnabled(check));

Observable<Boolean> textExists2 = RxTextView.text(editText2)
    .map(MainActivity::isEmpty);

Observable<Boolean> textValidations2 = Observable
    .combineLatest(checks2, textExists2, (check, exist) -> !check || exist);

CheckBox checkBox3 = (CheckBox) findViewById(R.id.checkBox3);
EditText editText3 = (EditText) findViewById(R.id.editText3);

Observable<Boolean> checks3 = RxCompoundButton.checkedChanges(checkBox3);

checks3.subscribe(check -> editText3.setEnabled(check));

Observable<Boolean> textExists3 = RxTextView.text(editText3)
    .map(MainActivity::isEmpty);

Observable<Boolean> textValidations3 = Observable
    .combineLatest(checks3, textExists3, (check, exist) -> !check || exist);

Button button = (Button) findViewById(R.id.button);

Observable.combineLatest(textValidations1, textValidations2, textValidations3,
    (validation1, validation2, validation3) ->
            validation1 && validation2 && validation3)
        .subscribe(validation -> button.setEnabled(validation));

위와 별도로 유틸리티 정적 메서드 isEmpty가 있습니다.

public static boolean isEmpty(CharSequence sequence) {
    return sequence.length() != 0;
}

이번의 코드는 조금 길지만 천천히 하나씩 살펴봅시다.

Observable<Boolean> checks1 = RxCompoundButton.checkedChanges(checkBox1);

checks1.subscribe(check -> editText1.setEnabled(check));

RxCompoundButton.checkedChanges을 이용해서 체크박스의 이벤트를 처리합니다. 이 Observable<Boolean>을 이용해서 editText1의 활성화 여부를 결정합니다.

Observable<Boolean> textExists1 = RxTextView.text(editText1)
    .map(MainActivity::isEmpty);

RxTextView.text는 에디트 텍스트를 옵저버블로 만들 수 있습니다. 에디트 텍스트의 텍스트 값과 여러 이벤트를 사용할 수 있겠지만 저희는 map(MainActivity::isEmpty)을 통해서 가공한 정보만을 사용할 것입니다. MainActivity::isEmpty는 메서드 레퍼런스라고 부르는 방식인데 MainActivityisEmpty를 호출하라는 의미입니다. isEmpty를 다시 살펴보면 아래와 같습니다.

public static boolean isEmpty(CharSequence sequence) {
    return sequence.length() != 0;
}

텍스트를 확인하고 길이가 0인지 확인하는 간단한 유틸리티 정적 메서드입니다.

이제 combineLatest를 살펴봅시다.

Observable<Boolean> textValidations1 = Observable
    .combineLatest(checks1, textExists1, (check, exist) -> !check || exist);

checks1testExists 두가지 옵저버블 값을 가져오는데 둘 중 하나의 값이 변경될 때 마다 뒤의 람다 함수 (check, exist) -> !check || exist가 호출이 되어 Observable<Boolean>에 데이터가 흘러가게 됩니다.

결국 이 코드는 두가지 경우에 대해 참이 되는 코드입니다.

  1. 체크 박스가 꺼져있는 경우
  2. 체크 박스가 켜져 있으면서 글이 있는 경우

체크 박스는 이 필드가 필수적인 필드인지 체크하는 용도로 쓰인 것입니다.

아래는 비슷한 구성으로 되어 있기 때문에 제일 아래 3가지 벨리데이션을 한번에 combineLatest한 것만 별도로 보겠습니다.

Observable.combineLatest(textValidations1, textValidations2, textValidations3,
    (validation1, validation2, validation3) ->
            validation1 && validation2 && validation3)
        .subscribe(validation -> button.setEnabled(validation));

벨리데이션 값이 변경될때 마다 벨리데이션 값 셋 모두 올바른지 확인하여 버튼의 활성화 여부를 변경하는 코드입니다. 이렇게 combileLatest를 이용하면 UI가 변경되었을 때 복합적으로 동작하는 UI를 쉽게 다룰 수 있습니다.

다음 시리즈에서는

이번 글에서는 RxAndroid가 제공하는 다양한 옵저버블과 오퍼레이터 등을 합성하여 사용자 인터페이스를 효과적으로 구조화하는 법을 알아보았습니다.

다음 시간에는 스케쥴러를 이용해서 비동기 처리등을 하는 방법과 Retrofit, Realm 등의 다른 라이브러리와 함께 쓰는 방법을 다뤄보겠습니다.

컨텐츠에 대하여

이 컨텐츠는 저자의 허가 하에 이곳에서 공유합니다.


Leonardo YongUk Kim

Leonardo YongUk Kim is a software developer with extensive experience in mobile and embedded projects, including: several WIPI modules (Korean mobile platform based on Nucleus RTOS), iOS projects, a scene graph engine for Android, an Android tablet, a client utility for black boxes, and some mini games using Cocos2d-x.

4 design patterns for a RESTless mobile integration »

close