Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Why_use_Reactive_Programming_-_Droidcon.pdf

Ovidiu Latcu
September 17, 2018
78

 Why_use_Reactive_Programming_-_Droidcon.pdf

Ovidiu Latcu

September 17, 2018
Tweet

Transcript

  1. Let’s take an example • we need to show the

    nearby restaurants fit for the logged in user • we need to fetch the user data from DB • get the current location • then fetch nearby stores • so we have 3 async calls to execute
  2. userDb.getUser(object : Callback<User> { override fun onResult(user: User) { locationApi.getLocation(object

    : Callback<Location> { override fun onResult(location: Location) { })
  3. userDb.getUser(object : Callback<User> { override fun onResult(user: User) { locationApi.getLocation(object

    : Callback<Location> { override fun onResult(location: Location) { restaurantsApi.getRestaurants(user, location, object : Callback<List<Restaurant>> { override fun onResult(result: List<Restaurant>) { showRestaurants(result) } override fun onError(exception: Throwable) { showError(exception) } }) } //...code stripped, more error handling })
  4. Results • 3 nested callbacks • too much boilerplate •

    hard to read & browse • hard to handle errors • complicated to cancel calls / subscriptions
  5. What if we need to change it ? • what

    if we want to improve this ? • get location & DB data in parallel • what are our options in Android & Java ?
  6. Thread(Runnable { var user: User? = null var location: Location?

    = null var error: Throwable? = null val latch = CountDownLatch(2) userDb.getUser(object : Callback<User> { override fun onResult(result: User) { user = result latch.countDown() } override fun onError(exception: Throwable) { error = exception latch.countDown() } })
  7. Thread(Runnable { var user: User? = null var location: Location?

    = null var error: Throwable? = null val latch = CountDownLatch(2) userDb.getUser(object : Callback<User> { override fun onResult(result: User) { user = result latch.countDown() } override fun onError(exception: Throwable) { error = exception latch.countDown() } }) locationApi.getLocation(object : Callback<Location> { override fun onResult(result: Location) { location = result latch.countDown() } override fun onError(exception: Throwable) { error = exception latch.countDown() } }) latch.await()
  8. Thread(Runnable { var user: User? = null var location: Location?

    = null var error: Throwable? = null val latch = CountDownLatch(2) userDb.getUser(object : Callback<User> { override fun onResult(result: User) { user = result latch.countDown() } override fun onError(exception: Throwable) { error = exception latch.countDown() } }) locationApi.getLocation(object : Callback<Location> { override fun onResult(result: Location) { location = result latch.countDown() } override fun onError(exception: Throwable) { error = exception latch.countDown() } }) latch.await() if (user != null && location != null) { restaurantsApi.getRestaurants(user!!, location!!, object : Callback<List<Restaurant>> { override fun onResult(result: List<Restaurant>) { uiHandler.post({ showRestaurants(result) }) } override fun onError(exception: Throwable) { uiHandler.post({ showError(exception) }) } }) } else { uiHandler.post({ showError(error!!) }) } }).start()
  9. Implementation using Reactive Programming Observable.zip( locationApi.getLocation(), userDb.getUser(), BiFunction { location:

    Location, user: User -> Data(location, user) }) .flatMap { data -> restaurantsApi.getRestaurants(data.user, data.location) }
  10. Implementation using Reactive Programming Observable.zip( locationApi.getLocation(), userDb.getUser(), BiFunction { location:

    Location, user: User -> Data(location, user) }) .flatMap { data -> restaurantsApi.getRestaurants(data.user, data.location) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ showRestaurants(it) })
  11. Reactive Programming Definition • “Reactive programming is an asynchronous programming

    paradigm concerned with data streams and the propagation of change.”, Wiki
  12. Reactive Programming Definition • “Reactive programming is programming with asynchronous

    data streams.”, GitHub • everything can be a stream of data : location, DB entries, current user, shopping cart, etc. • streams can be combined, transformed and the resulting stream observed by subscribers
  13. Building blocks • Observables - streams of data • Operators

    - create, transform, filter, combine • Subscribers - consumers of data • Schedulers - add threading
  14. Observables - streams of data Observable.zip( locationApi.getLocation(), userDb.getUser(), BiFunction {

    location: Location, user: User -> Data(location, user) }) .flatMap { data -> restaurantsApi.getRestaurants(data.user, data.location) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ showRestaurants(it) }) Observable<Location> Observable<User>
  15. Operators - combine, transform streams Observable.zip( locationApi.getLocation(), userDb.getUser(), BiFunction {

    location: Location, user: User -> Data(location, user) }) .flatMap { data -> restaurantsApi.getRestaurants(data.user, data.location) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ showRestaurants(it) }) ZIP OPERATOR FLAT MAP OPERATOR
  16. Schedulers - add threading Observable.zip( locationApi.getLocation(), userDb.getUser(), BiFunction { location:

    Location, user: User -> Data(location, user) }) .flatMap { data -> restaurantsApi.getRestaurants(data.user, data.location) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ showRestaurants(it) }) BACKGROUND EXECUTION UI THREAD DELIVERY
  17. Subscribers - handle results Observable.zip( locationApi.getLocation(), userDb.getUser(), BiFunction { location:

    Location, user: User -> Data(location, user) }) .flatMap { data -> restaurantsApi.getRestaurants(data.user, data.location) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ showRestaurants(it) }) SUBSCRIBER
  18. Tweets list example • we want to fetch Droidcon Tweets

    • fetch Tweets by @droidconRO • fetch Tweets with #droidconro hashtag • do this in parallel • deliver final results to UI • should we first imagine this in Java, without RX ?
  19. Using RX this is a piece of cake Observable.zip( twitterApi.getTweetsByHashtag("#droidcon"),

    twitterApi.getTweetsByUser("@droidconro"), BiFunction { hashTagTweets: List<Tweet>, tweetsByUser: List<Tweet> -> hashTagTweets.union(tweetsByUser).toList() })
  20. Using RX this is a piece of cake Observable.zip( twitterApi.getTweetsByHashtag("#droidcon"),

    twitterApi.getTweetsByUser("@droidconro"), BiFunction { hashTagTweets: List<Tweet>, tweetsByUser: List<Tweet> -> hashTagTweets.union(tweetsByUser).toList() }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(::showTweets)
  21. TwitterAPI - using Retrofit RX Adapter interface TwitterApi { @GET("/1.1/statuses/user_timeline.json")

    fun getTweetsByHashtag(@Query hashTag: String): Observable<List<Tweet>> @GET("/1.1/statuses/tweets.json") fun getTweetsByUser(@Query user: String): Observable<List<Tweet>> }
  22. Repository pattern example • for the previous example, we also

    want DB storage • implement a repository for Tweets • first fetches items from DB • then fetches new items from API • stores API items in DB • deliver both results to the UI as they arrive
  23. Again, very simple using RX class TweetsRepository(val database: TweetsDatabase, val

    api: TweetsApi) { fun getTweets(): Observable<List<Tweet>> {} }
  24. Again, very simple using RX class TweetsRepository(val database: TweetsDatabase, val

    api: TweetsApi) { fun getTweets(): Observable<List<Tweet>> { return Observable.concatArray( database.getTweets(), api.getTweets() } }
  25. Again, very simple using RX class TweetsRepository(val database: TweetsDatabase, val

    api: TweetsApi) { fun getTweets(): Observable<List<Tweet>> { return Observable.concatArray( database.getTweets(), api.getTweets() .doOnNext { database.storeTweets(it) }) } }
  26. TwitterDatabase - using Room RX Adapters @Dao interface TweetsDatabase {

    @Query("SELECT * FROM tweet") fun getTweets(): Observable<List<Tweet>> @Insert fun storeTweets(users: List<Tweet>) }
  27. Where are those Observable coming from ? • existing “adapters”

    / RX libraries • Retrofit • Room • SQLite • RXLocation • RXNetworking
  28. First “Observable” - RxLocation • there are cases when we

    need to write our own Observable • more advanced, must know what you doing • use Observable.create( subscriber ) • connect to the service / client ( eg: location) • emit data as it arrives via subscriber.onNext(data) • disconnect from the service / client when the subscription is disposed
  29. fun getLocation(): Observable<Location> { return Observable.create<Location>({ subscriber -> val listener

    = object : LocationCallback() { override fun onLocationResult(result: LocationResult) { super.onLocationResult(result) result.lastLocation?.let { subscriber.onNext(it) } } } locationClient.requestLocationUpdates(createLocationRequest(), listener, Looper.myLooper()) } }) }
  30. fun getLocation(): Observable<Location> { return Observable.create<Location>({ subscriber -> val listener

    = object : LocationCallback() { override fun onLocationResult(result: LocationResult) { super.onLocationResult(result) result.lastLocation?.let { subscriber.onNext(it) } } } locationClient.requestLocationUpdates(createLocationRequest(), listener, Looper.myLooper()) } }) } EMIT ITEMS FOR THE SUBSCRIBER
  31. fun getLocation(): Observable<Location> { return Observable.create<Location>({ subscriber -> val listener

    = object : LocationCallback() { override fun onLocationResult(result: LocationResult) { super.onLocationResult(result) result.lastLocation?.let { subscriber.onNext(it) } } } locationClient.requestLocationUpdates(createLocationRequest(), listener, Looper.myLooper()) subscriber.setCancellable { locationClient.removeLocationUpdates(listener) } }) }
  32. fun getLocation(): Observable<Location> { return Observable.create<Location>({ subscriber -> val listener

    = object : LocationCallback() { override fun onLocationResult(result: LocationResult) { super.onLocationResult(result) result.lastLocation?.let { subscriber.onNext(it) } } } locationClient.requestLocationUpdates(createLocationRequest(), listener, Looper.myLooper()) subscriber.setCancellable { locationClient.removeLocationUpdates(listener) } }) } subscription.dispose() → disconnect client/services
  33. fun getLocation(): Observable<Location> { return Observable.create<Location>({ subscriber -> val listener

    = object : LocationCallback() { override fun onLocationResult(result: LocationResult) { super.onLocationResult(result) result.lastLocation?.let { subscriber.onNext(it) } } } locationClient.requestLocationUpdates(createLocationRequest(), listener, Looper.myLooper()) subscriber.setCancellable { locationClient.removeLocationUpdates(listener) } }) }
  34. Final example ( : - Hunters Application • continuously track

    hunter Location • continuously track hunter compass • poll other hunters location from API every 5 seconds • check if hunter direction intersects other hunters • alarm user
  35. Location, API calls & azimuth are Observables interface HuntingApi {

    fun getLocation(): Observable<Location> fun getOrientation(): Observable<Double> fun getHunters(): Observable<List<Hunter>> }
  36. val huntersObservable = Observable .interval(5000, TimeUnit.MILLISECONDS) .flatMap { getHunters() }

    Observable.combineLatest( getLocation(), getOrientation(), huntersObservable, //... COMBINE LATEST LOCATION , ORIENTATION, AND HUNTERS DATA
  37. val huntersObservable = Observable .interval(5000, TimeUnit.MILLISECONDS) .flatMap { getHunters() }

    Observable.combineLatest( getLocation(), getOrientation(), huntersObservable, Function3 { location: Location, azimuth: Double, hunters: List<Hunter> -> //Check if hunter direction intersects other nearby hunters checkHuntersIntersection(location, azimuth, hunters) }) PASS DATA TO A FUNCTION THAT CHECKS IF WE INTERSECT ANY OTHER HUNTER
  38. val huntersObservable = Observable .interval(5000, TimeUnit.MILLISECONDS) .flatMap { getHunters() }

    Observable.combineLatest( getLocation(), getOrientation(), huntersObservable, checkHuntersIntersection()) //... fun checkHuntersIntersection() = Function3 { loc: Location, azimuth: Double, hunters: List<Hunter> -> //...check if hunter direction intersects other nearby hunters true } SIMPLIFIED, EXTRACTED TO A METHOD RETURNING A FUNCTION
  39. val huntersObservable = Observable .interval(5000, TimeUnit.MILLISECONDS) .flatMap { getHunters() }

    Observable.combineLatest( getLocation(), getOrientation(), huntersObservable, checkHuntersIntersection()) .subscribe({ inDanger -> if (inDanger) alarmUser() }) IN THE END, SUBSCRIBE TO THE RESULT
  40. val huntersObservable = Observable .interval(5000, TimeUnit.MILLISECONDS) .flatMap { getHunters() }

    Observable.combineLatest( getLocation().filter { it.accuracy > 85 }, getOrientation(), huntersObservable, checkHuntersIntersection()) .subscribe({ inDanger -> if (inDanger) alarmUser() }) FILTER OUT INACCURATE LOCATION UPDATES
  41. val huntersObservable = Observable .interval(5000, TimeUnit.MILLISECONDS) .flatMap { getHunters() }

    Observable.combineLatest( getLocation().filter { it.accuracy > 85 }, getOrientation().throttleLast(500, TimeUnit.MILLISECONDS), huntersObservable, checkHuntersIntersection()) .subscribe({ inDanger -> if (inDanger) alarmUser() }) THROTTLE ORIENTATION, TO AVOID TOO MANY COMPUTATIONS
  42. One last thing. What about unsubscribing ? • Observable.subscribe() ->

    returns a Subscription • keep a reference to it, and call dispose() • unsubscribes from all observables (location,orientation,etc)
  43. One last thing. What about unsubscribing ? • Observable.subscribe() ->

    returns a Subscription • keep a reference to it, and call dispose() • unsubscribes from all observables (location,orientation,etc) val subscription = Observable.combineLatest( //... .subscribe({ //... }) subscription.dispose()
  44. Reactive Programming Advantages • avoid “callback hell” • a lot

    simpler to do “async” / “threaded” work • complex threading becomes very easy • a lot of operators and adapters • async code more expressive and clean • easy to test • RXJava, RXAndroid, RXSwift, RXJS, RXNet ...
  45. Reactive Programming Disadvantages • steep learning curve • online documentation

    not easy to understand • easy to make mistakes and missuse • not handling subscriptions, can lead to memory leaks • might get hard to onboard people on projects using RX
  46. Thank you. Questions ? • Medium : https://medium.com/@ovidiu.latcu • GitHub

    : https://github.com/ovy9086/ • Speaker Deck : https://speakerdeck.com/ovy9086 • Company Website : https://www.corebuild.eu/ Ovidiu Latcu - Android Tech Lead @