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

Enhancing Your Rx-perience with Kotlin

Enhancing Your Rx-perience with Kotlin

Presented at the New York Kotlin Meetup July 11th, 2018

I take a look at how some feature of the Kotlin language can make working with RxJava really great. I also briefly discuss the RxKotlin project and how/why you might choose to use parts of it.

Stephen D'Amico

July 11, 2018
Tweet

More Decks by Stephen D'Amico

Other Decks in Programming

Transcript

  1. New Language + Old Tool = Awesome Stephen D’Amico -

    @sddamico - NYC Kotlin - July 2018
  2. If the last parameter of a function accepts a function,

    you can move the lambda describing the function outside the parentheses for the call This reduces a couple keystrokes and helps improve readability of scopes, especially when they get nested // Definition: fun filter(predicate: () -> Boolean) = /* impl */ // "Java-style": Observable.just(1,11, 111, 1111, 11111) .filter({ it > 100 }) .subscribe({ System.out.print(it) }) // Kotlin-style: Observable.just(1,11, 111, 1111, 11111) .filter { it > 100 } .subscribe { System.out.print(it) }
  3. Some of RxJava's method signatures can be fairly complex and

    may receive multiple functions or are overloaded with different functionally equivalent types that may behave differently Notable example of this: #startWith(T) vs #startWith(ObservableSource) // Example: var a = Observable.just("b", "c") a.startWith { "a" } .subscribe(System.out::println) // Expected? a b c // Actual: <no emission>
  4. Kotlin's object destructuring can be very useful when needing to

    access data from fields that are in a container object like a Tuple data class Container(val a: String, val b: String, val c: String, val d: String) // Reference the field: Observable.combineLatest(a, b, c, d, ::Container) .doOnNext { container -> System.out.println(container.a) } .subscribe { System.out.println(it.toString()) } // Destructure and access field directly: Observable.combineLatest(a, b, c, d, ::Container) .doOnNext { (a,_,_,_) -> System.out.println(a) } .subscribe { System.out.println(it.toString()) }
  5. One big change in RxJava 2 was the move to

    remove null emissions from the stream. This certainly makes life better on the Java side by better defining the Observable contract. In Kotlin however, we may have preferred to just rely on the type system to enforce the difference between Observable<String> and Observable<String?> // Nullable types? val subject = PublishSubject.create<String?>() subject.subscribe { System.out.println(it.toString()) } subject.onNext("Hello") subject.onNext(null) // nope!
  6. One simple trick in Kotlin is to introduce your own

    Optional type. This can be functionally very similar to the Guava Optional without any baggage (this example inspired by Jake Wharton and Alex Strong's talk at Kotlinconf) sealed class Optional<out T : Any> { data class Present<out T : Any>(val value: T) : Optional<T>() object None : Optional<Nothing>() } fun <T : Any> T?.asOptional() = if (this == null) Optional.None else Optional.Present(this) // Usage: val subject = PublishSubject.create<Optional<String>>() subject.subscribe { when (it) { is Optional.Present<String> -> println("Success") is Optional.None -> println("Failure") } } subject.onNext("Hello".asOptional()) subject.onNext(Optional.None)
  7. In Java, we may represent the state of the music

    player using an object that contains the State of playback as an enum public final class MusicPlayerState { State state; float progress; long trackDurationS; String trackTitle; String trackDescription Throwable error; String errorMessage; } val state = BehaviorSubject.create<MusicPlayerState>() state.onNext(new MusicPlayerState(State.NO_TRACK, 0.0, 0, "", "", null, "")) state.onNext(new MusicPlayerState(State.TRACK_QUEUED, 0.0, 193, "Last Night", "The Strokes - Is This It", null, ""))
  8. Sealed class let us express this state machine significantly more

    cleanly and make our call sites a breeze data class TrackInfo(val durationS: Long, val trackTitle: String, val trackDescription: String) sealed class MusicPlayerState { object NoTrack : MusicPlayerState() data class TrackQueued(val trackInfo: TrackInfo) data class TrackPlaying(val trackInfo: TrackInfo, val progress: Float) data class TrackPaused(val trackInfo: TrackInfo, val progress: Float) data class TrackCompleted(val trackInfo: TrackInfo) data class PlaybackError(val trackInfo: TrackInfo, val error: Throwable, val errorMessage: String) } val state = BehaviorSubject.create<MusicPlayerState>() state.onNext(MusicPlayerState.NoTrack) val trackInfo = TrackInfo(193, "Last Night", "The Strokes - Is This It") state.onNext(MusicPlayerState.TrackQueued(trackInfo)) state.onNext(MusicPlayerState.TrackPlaying(trackInfo, 0.0)) state.onNext(MusicPlayerState.TrackPlaying(trackInfo, 0.05))
  9. A common flow you might see in an MVI style

    framework would be publishing updates to an object that represents the state of a view. For more information about this pattern, come see my talk at Droidcon NYC in September! val subject: BehaviorSubject<StringContainer> = BehaviorSubject.create() fun publishUpdate(updatedString: String) { val value = subject.value val newValue = value?.copy(string = updatedString) ?: StringContainer(updatedString) subject.onNext(newValue) }
  10. Extension functions make it really easy to add more fluent

    APIs for transitioning between data types. Common examples are things like turning collections into streams of their values. You might also see extensions for translating from other asynchronous constructs, e.g. Future.toObservable() // "stock" creating observable from list: val strings: List<String> = loadStrings() Observable.fromIterable(strings) … // then add this: fun <T : Any> Iterable<T>.toObservable(): Observable<T> = Observable.fromIterable(this) // now our call becomes: val strings: List<String> = loadStrings() strings.toObservable() …
  11. Transformer is a clunky interface, you can simplify the experience

    by replacing it with an extension function that's both easier to read and test. fun <T> efficientCollect(listSize: Int): Observable.Transformer<T, List<T>> { return Observable.Transformer { observable -> observable.collect( { ArrayList<T>(listSize) as List<T> }, { listings, listing -> (listings as ArrayList).add(listing) } ) } // call-site: observable.compose(Utils.efficientCollect(size)) // versus fun <T> Observable<T>.efficientCollect(listSize: Int): Observable<List<T>> { return this .collect( { ArrayList<T>(listSize) as List<T> }, { listings, listing -> (listings as ArrayList).add(listing) } ) } // call-site: observable.efficientCollect(size)