- RxAndroid로 리액티브 앱 만들기 #1
- RxAndroid로 리액티브 앱 만들기 #2
- RxAndroid로 리액티브 앱 만들기 #3
- RxAndroid로 리액티브 앱 만들기 #4
출처 : https://realm.io/kr/news/rxandroid-3/
RxAndroid
와RxBinding
버전 변경 내역에 대한 피드백을 주신 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.clicks
는View
타입을 인자로 받는 정적 메서드로setOnClickListener
를 통해OnClickListener
에 전달될 이벤트를 옵저버블 형태로 래핑합니다. 대부분의 안드로이드 콤퍼넌트들이View
타입을 상속받고 있기 때문에 이 정적 메서드를 사용하여 간단하게 클릭 이벤트를 처리할 수 있습니다.위 예제의
map
은 전달 받은event
값을 무시하고 랜덤 값으로 바꿉니다. 기존event
가 담긴 옵저버블 대신 랜덤 값이 담긴 새로운observable
이subscribe
에 연결되며 버튼이 클릭될 때 마다 아이디가textView
인TextView
의 값이 임의의 숫자로 변경됩니다.옵저버블 병합
사용자 인터페이스를 작성하다보면 여러 경로로 온 이벤트를 동시에 처리해야 하는 경우가 많습니다. 이런 경우 개별 이벤트를 옵저버블로 받은 후 병합을 시도할 수 있습니다.
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을 통해 이벤트 대신에left
와right
라는 문자열을 반환하는Observable<String>
을 둘 얻습니다. 이를 각기lefts
와rights
에 대입합니다.
Observable.merge
를 통해lefts
와rights
로 전달되온 두 옵저버블을 병합하여 하나의 옵저버블로 묶어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
이벤트에 담고요.다음으로
minuses
와pluses
옵저버블을 하나의 옵저버블로 병합합니다. (Observable.merge(minuses, pluses)
)이제 처음 보는 오퍼레이터
scan
이 나왔습니다. 첫번째 인자는 초기값 (0
), 두번째 인자는 실제 데이터를 처리할 람다 ((sum, number) -> sum + 1
)입니다. 람다는 두가지 인자를 가지고 있는데요. 첫번째는 누적된 값(sum
)이고 두번째는 매번 옵저버블로 부터 들어오는 데이터(number
)입니다.첫번째
scan
은together
옵저버블을 통해 전달되는 값을number
로 받지만 전혀 사용하고 있지 않습니다. 그 값을 버리고sum + 1
로1
을 더하고 있죠. 초기값이0
으로 설정되어 있기 때문에sum
은 처음에0
이 되고 이후에는 누적된 값이sum
에게 전달이 되게 됩니다. 아무 버튼을 누르든sum
은1
씩 증가하게 되고 어떤 버튼이든 누른 횟수가 됩니다.두번째
scan
은number
로 값을 받는 것은 동일합니다만 그 값 자체를sum + number
로 더한다는 차이가 있습니다. 플러스 버튼을 눌렀다면 초기값0
에1
이 더해지고 마이너스 버튼을 눌렀다면-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
는 메서드 레퍼런스라고 부르는 방식인데MainActivity
의isEmpty
를 호출하라는 의미입니다.isEmpty
를 다시 살펴보면 아래와 같습니다.public static boolean isEmpty(CharSequence sequence) { return sequence.length() != 0; }
텍스트를 확인하고 길이가 0인지 확인하는 간단한 유틸리티 정적 메서드입니다.
이제
combineLatest
를 살펴봅시다.Observable<Boolean> textValidations1 = Observable .combineLatest(checks1, textExists1, (check, exist) -> !check || exist);
checks1
과testExists
두가지 옵저버블 값을 가져오는데 둘 중 하나의 값이 변경될 때 마다 뒤의 람다 함수(check, exist) -> !check || exist
가 호출이 되어Observable<Boolean>
에 데이터가 흘러가게 됩니다.결국 이 코드는 두가지 경우에 대해 참이 되는 코드입니다.
- 체크 박스가 꺼져있는 경우
- 체크 박스가 켜져 있으면서 글이 있는 경우
체크 박스는 이 필드가 필수적인 필드인지 체크하는 용도로 쓰인 것입니다.
아래는 비슷한 구성으로 되어 있기 때문에 제일 아래 3가지 벨리데이션을 한번에
combineLatest
한 것만 별도로 보겠습니다.Observable.combineLatest(textValidations1, textValidations2, textValidations3, (validation1, validation2, validation3) -> validation1 && validation2 && validation3) .subscribe(validation -> button.setEnabled(validation));
벨리데이션 값이 변경될때 마다 벨리데이션 값 셋 모두 올바른지 확인하여 버튼의 활성화 여부를 변경하는 코드입니다. 이렇게
combileLatest
를 이용하면 UI가 변경되었을 때 복합적으로 동작하는 UI를 쉽게 다룰 수 있습니다.다음 시리즈에서는
이번 글에서는 RxAndroid가 제공하는 다양한 옵저버블과 오퍼레이터 등을 합성하여 사용자 인터페이스를 효과적으로 구조화하는 법을 알아보았습니다.
다음 시간에는 스케쥴러를 이용해서 비동기 처리등을 하는 방법과 Retrofit, Realm 등의 다른 라이브러리와 함께 쓰는 방법을 다뤄보겠습니다.
'프로그래밍 > Android' 카테고리의 다른 글
jarsigner/apksigner로 apk signing하기 (콘솔에서 Signing 하기) (0) | 2019.01.15 |
---|---|
RxAndroid로 리액티브 앱 만들기 #4 (1) | 2016.10.14 |
RxAndroid로 리액티브 앱 만들기 #2 (0) | 2016.10.14 |
RxAndroid로 리액티브 앱 만들기 #1 (0) | 2016.10.12 |
1. RxJava 와 RxAndroid - RX(Reactive Extensions)의 소개 (0) | 2016.08.31 |