Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

드로이드 나이츠 2018: RxJava 적용 팁 및 트러블 슈팅

1,714 views

Published on

2018년 4월 22일 드로이드 나이츠 2108에서 발표한 RxJava 세션 프리젠테이션입니다.

Published in: Mobile
  • Be the first to comment

드로이드 나이츠 2018: RxJava 적용 팁 및 트러블 슈팅

  1. 1. 다시 보는 RxJava 네이버 노재춘
  2. 2. Rx Contract #1-1 no more messages should arrive after an onError or onComplete message RxView.clicks(button) .flatMap(ignored -> getBestSeller() .subscribeOn(Schedulers.io())) .observeOn(AndroidSchedulers.mainThread()) .subscribe(bookTitle -> title.setText(bookTitle), e -> Toast.makeText(this, "문제 발생", Toast.LENGTH_LONG).show() ); 무한 이벤트에서 문제 발생 가능
  3. 3. Rx Contract #1-2 RxView.clicks(button) .flatMap(ignored -> getBestSeller() .subscribeOn(Schedulers.io()) .map(title -> new ViewState.Result(title)) // (1) .cast(ViewState.class) // (2) .onErrorReturn(e -> new ViewState.Error(e))) // (3) .observeOn(AndroidSchedulers.mainThread()) .subscribe(viewState -> { if (viewState instanceof ViewState.Result) { // (4) title.setText(((ViewState.Result) viewState).title); } else if (viewState instanceof ViewState.Error) { // (5) Toast.makeText(this, "문제 발생", Toast.LENGTH_LONG).show(); } });
  4. 4. Rx Contract #1-3 interface ViewState { class Error implements ViewState { Throwable e; Error(Throwable e) { this.e = e; } } class Result implements ViewState { String title; Result(String title) { this.title = title; } } } http://hannesdorfmann.com/android/ mosby3-mvi-1 visual bugs such as displaying a loading indicator (“loading state”) and error indicator (“error state”) at the same time => exactly one output
  5. 5. Rx Contract #2-1 스트림에 널(null) 허용 안됨 @Test(expected = NullPointerException.class) public void nullEvent() { Observable.just("Hello", null, "RxJava") .subscribe(System.out::println, System.err::println); } Observable 생성 에서 에러 발생
  6. 6. Rx Contract #2-2 @Test public void nullEventOnCreate() { Observable.create(emitter -> { emitter.onNext("Hello"); emitter.onNext(null); emitter.onNext("RxJava"); }).subscribe(System.out::println, System.err::println); } java.lang.NullPointerException: onNext called with null. Null values are generally not allowed in 2.x operators and sources.
  7. 7. Rx Contract #2-3 @Test public void nullEventPossible() { Observable.just(1, 2, -1, 1, 2) .map(dollar -> getCurrentPrice(dollar)) .subscribe(System.out::println, System.err::println); } private String getCurrentPrice(int dollar) { if (dollar < 0) { return null; } return (dollar * 1000) + " won"; } 1000 won 2000 won java.lang.NullPointerException: The mapper function returned a null value.
  8. 8. Rx Contract #2-4 @Test public void nullEventPossible3() { Observable.just(1, 2, -1, 1, 2) .map(dollar -> getCurrentPrice3(dollar)) .onErrorReturnItem("0 won") .subscribe(System.out::println, System.err::println, () -> System.out.println("onComplete")); } private String getCurrentPrice3(int dollar) { if (dollar < 0) { throw new IllegalArgumentException("dollar should be bigger than 0"); } return (dollar * 1000) + " won"; } [1000 won, 2000 won, 0 won, onComplete]
  9. 9. Rx Contract #2-5 @Test public void nullEventPossible4() { Observable.just(1, 2, -1, 1, 2) .flatMap(dollar -> getCurrentPrice4(dollar) .onErrorReturnItem("0 won")) // (1) .subscribe(System.out::println, System.err::println); } private Observable getCurrentPrice4(int dollar) { if (dollar < 0) { return Observable.error( new IllegalArgumentException("dollar should be bigger than 0")); // (2) } return Observable.just((dollar * 1000) + " won"); // (3) }
  10. 10. Rx Contract #3-1 Assume observer instances are called in a serialized fashion Observable obs = Observable.create(emitter -> { new Thread(() -> { emitter.onNext(1); emitter.onNext(3); . . . . emitter.onNext(9); // emitter.onComplete(); // (1) }).start(); new Thread(() -> { emitter.onNext(2); emitter.onNext(4); . . . . emitter.onNext(10); // emitter.onComplete(); // (2) }).start(); }); onComplete 넣 을 수 없음
  11. 11. Rx Contract #3-2 obs.subscribe(value -> System.out.println( Thread.currentThread().getName() + ":" + value), System.err::println, () -> System.out.println("onComplete")); onComplete 불 리지 않음
  12. 12. Rx Contract #3-3 Observable obs1 = Observable.create(emitter -> { new Thread(() -> { emitter.onNext(1); . . . . emitter.onNext(9); emitter.onComplete(); // (1) }).start(); }); Observable obs2 = Observable.create(emitter -> { new Thread(() -> { emitter.onNext(2); . . . . emitter.onNext(10); emitter.onComplete(); // (2) }).start(); }); (1), (2) 모두 필요
  13. 13. Rx Contract #3-4 Observable.merge(obs1, obs2).subscribe(value -> System.out.println(Thread.currentThread().getName() + ":" + value), System.err::println, () -> System.out.println("onComplete") ); onComplete 정 상 호출
  14. 14. Rx Contract #3-5 Observable obs1 = Observable.create(emitter -> { emitter.onNext(1); emitter.onNext(3); . . . . emitter.onNext(9); emitter.onComplete(); }).subscribeOn(Schedulers.computation()); Observable obs2 = Observable.create(emitter -> { emitter.onNext(2); emitter.onNext(4); . . . . emitter.onNext(10); emitter.onComplete(); }).subscribeOn(Schedulers.computation());
  15. 15. Rx Contract #3-6 Observable.merge(obs1, obs2).subscribe(value -> System.out.println( Thread.currentThread().getName() + ":" + value), System.err::println, () -> System.out.println("onComplete") );
  16. 16. flatMap thread #1-1 repository.getBookCategories().toObservable() .flatMapIterable(categories -> categories) .flatMapSingle(category -> repository.getCategoryBooks(category.categoryId)) .subscribeOn(Schedulers.io()) .collect(ArrayList::new, ArrayList::addAll) .observeOn(AndroidSchedulers.mainThread()) .subscribe(books -> title.setText(books.toString() repository.getBookCategories().toObservable() .subscribeOn(Schedulers.io()) .flatMapIterable(categories -> categories) .flatMapSingle(category -> repository.getCategoryBooks(category.categoryId) .subscribeOn(Schedulers.io())) .collect(ArrayList::new, ArrayList::addAll) .observeOn(AndroidSchedulers.mainThread()) .subscribe(books -> title.setText(books.toString()));
  17. 17. flatMap thread #1-2
  18. 18. flatMap thread #2 단일 이벤트 연결 repository.getRegion(location) .map(region -> region.areaCode) .flatMap(repository::getWeather) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(weather -> title.setText(weather.toString())); repository.getRegion(location) .subscribeOn(Schedulers.io()) .map(region -> region.areaCode) .flatMap(areaCode -> repository.getWeather(areaCode) .subscribeOn(Schedulers.io())) .observeOn(AndroidSchedulers.mainThread()) .subscribe(weather -> title.setText(weather.toString()));
  19. 19. merge thread #1-1 Single.merge(repository.getBestSeller(), repository.getRecommendBooks(), repository.getCategoryBooks(7)) .subscribeOn(Schedulers.io()) .collect(ArrayList::new, ArrayList::addAll) .observeOn(AndroidSchedulers.mainThread()) .subscribe(books -> title.setText(books.toString())); Single.merge(repository.getBestSeller().subscribeOn(Schedulers.io()), repository.getRecommendBooks().subscribeOn(Schedulers.io()), repository.getCategoryBooks(7).subscribeOn(Schedulers.io())) .collect(ArrayList::new, ArrayList::addAll) .observeOn(AndroidSchedulers.mainThread()) .subscribe(books -> title.setText(books.toString()));
  20. 20. merge thread #1-2
  21. 21. zip thread #1-1 Single.zip(repository.getWeather(regionCode), repository.getWeatherDetail(regionCode), Pair::new) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(pair -> title.setText(pair.first + ", " + pair.second)); Single.zip(repository.getWeather(regionCode).subscribeOn(Schedulers.io()), repository.getWeatherDetail(regionCode).subscribeOn(Schedulers.io()), Pair::new) .observeOn(AndroidSchedulers.mainThread()) .subscribe(pair -> title.setText(pair.first + ", " + pair.second));
  22. 22. zip thread #1-2
  23. 23. RetryWhen #1 - SocketTimeout에 재시도(3번 재시도) - 401 status code에는 인증 API 호출
  24. 24. RetryWhen #2 public Single<Profile> getProfile() { return profileRepository.getProfile().retryWhen(errors -> errors.zipWith(Flowable.range(0, LIMIT), this::handleRetryAttempt).flatMap(x -> x)); } private Flowable<Long> handleRetryAttempt(Throwable throwable, int attempt) { if (throwable instanceof SocketTimeoutException) { switch (attempt) { case LIMIT: return Flowable.error(throwable); default: return Flowable.timer(attempt * 3, TimeUnit.SECONDS); } } else {else if (throwable instanceof HttpException) { // 다음 슬라이드 } OkHttpClient Interceptor도 가능? 원래 흐름으로 돌아올 방법 없음
  25. 25. RetryWhen #3 private Flowable<Long> handleRetryAttempt(Throwable throwable, int attempt) { if (throwable instanceof SocketTimeoutException) { ... } else if (throwable instanceof HttpException) { int statusCode = ((HttpException) throwable).code(); if (statusCode == 401) { return profileRepository.refresh().retry((refreshAttempt, e) -> refreshAttempt < LIMIT && e instanceof SocketTimeoutException) .map(x -> 1L).toFlowable(); } } return Flowable.error(throwable); }
  26. 26. RetryWhen #4 public class RetryTransformer<T> implements SingleTransformer<T, T> { . . . . @Override public SingleSource apply(Single<T> upstream) { return upstream.retryWhen(errors -> errors.zipWith(Flowable.range(0, LIMIT), this::handleRetryAttempt).flatMap(x -> x)); } } public Single<Profile> getProfile() { return profileRepository.getProfile().compose(retryTransformer); }
  27. 27. 예외 처리 #1 public long getTimeInMillis1(String input) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT); return sdf.parse(input).getTime(); } public long getTimeInMillis2(String input) { SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT); try { return sdf.parse(input).getTime(); } catch (ParseException e) { e.printStackTrace(); return -1L; // -1을 상수로 해도 큰 차이가 없음 } }
  28. 28. 예외 처리 #2 public long getTimeInMillis3(String input) { SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT); try { return sdf.parse(input).getTime(); } catch (ParseException e) { throw new IllegalArgumentException("Illegal input pattern"); } }
  29. 29. 예외 처리 #3 public Single<Long> getTimeInMillis(String input) { return Single.fromCallable(() -> { SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT); return sdf.parse(input).getTime(); }); } String input = "2017-xx-30 02:20:20"; DateFormatter formatter = new DateFormatter(); long start = formatter.getTimeInMillis(input) .onErrorReturnItem(0L) .blockingGet(); long end = formatter.getTimeInMillis(input) .onErrorReturnItem(new Date().getTime()) .blockingGet(); System.out.println("start=" + start + ", end=" + end);
  30. 30. flatMap BiFunction Observable.range(2, 8) .flatMap(row -> Observable.range(1, 9) .map(col -> String.format("%d x %d = %d", row, col, row * col))) .subscribe(System.out::println); Observable.range(2, 8) .flatMap(row -> Observable.range(1, 9), (row, col) -> String.format("%d x %d = %d", row, col, row * col)) .subscribe(System.out::println);
  31. 31. collect Observable<String> obs = Observable.fromIterable(searchOptions) .filter(SearchOption::isInUse) .map(SearchOption::getName); obs.subscribe(name -> { applySearchOptions.add(name); }, e -> { }, () -> { showData(applySearchOptions); }); Single<ArrayList<String>> obs = Observable.fromIterable(searchOptions) .filter(SearchOption::isInUse) .map(SearchOption::getName) .collect(ArrayList<String>::new, List::add); obs.subscribe(this::showData);
  32. 32. Q & A

×