Skip to content

Instantly share code, notes, and snippets.

@charlag
Last active May 21, 2017 10:39
Show Gist options
  • Save charlag/725d8790eee956a397ade890c31ca69f to your computer and use it in GitHub Desktop.
Save charlag/725d8790eee956a397ade890c31ca69f to your computer and use it in GitHub Desktop.
Helper to reduce boilderplate when creating presenters / viewModels.
package com.charlag.rx.inputs
import rx.Observable
import rx.Subscription
import rx.subjects.PublishSubject
import rx.subscriptions.CompositeSubscription
import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
import java.lang.reflect.Proxy
/**
* Holder for the [Observable] inputs. Creates subjects for you so you don't have to create a
* subject for every view input.
* Usually when there are reactive relationships between presenter and view, presenter has to
* create a subject for every input of the view because view can be detached in every moment but we
* don't want to terminate our observables. Also presenter should take care of not forwarding
* any of the onCompleted/onError events to the subject (or use RxRelay).
* This snippet does exactly that. It lets you create a proxy for your inputs which is backed
* by the subjects and never terminates. It also subscribes to all of the [Observable] inputs
* at once. You can describe all your logic/observables in your presenter's init and let
* this holder to take care of forwarding any observables from the view.
*
* It doesn't use kotlin-reflect to avoid heavy dependencies. It's written for RxJava 1.* but can
* be easily ported to RxJava 2.*.
*
* @param T type of input
*/
interface InputsHolder<T : Any> {
/**
* Contains an object what implements [T] using subjects.
*/
val proxiedInputs: T
/**
* For every method that returns [Observable] and doesn't have have parameters it forwards
* onNext events to the proxy.
* @param inputs to forward to the proxy
*/
fun subscribe(inputs: T)
/**
* Unsubscribes from the input so no more events from any added inputs will be forwarded to the
* proxy. New inputs will be forwarded.
*/
fun clear()
}
/**
* Creates a new [InputsHolder] instance for the given [klass].
* It scans for every method without arguments and with return type of
* [Observable] (or it's subtype) and adds it to the [InputsHolder] (so, Kotlin properties work as
* well becasause they're just getters for Java). All calls to such methods will be intercepted
* and a subject from the holder will be returned instead of an actual value.
* @param klass class of which proxy will be created
* @param T type of the input
* @return holder for the inputs
*/
fun <T : Any> rxInputOf(klass: Class<T>): InputsHolder<T> {
val observableMethods = klass.declaredMethods.filter {
Observable::class.java.isAssignableFrom(it.returnType) &&
it.parameterTypes.isEmpty()
}
val subjects = mutableMapOf<Method, PublishSubject<Any>>()
observableMethods
.forEach { method ->
subjects[method] = PublishSubject.create()
}
val handler = InvocationHandler { proxy: Any, method: Method, args: Array<out Any>? ->
subjects[method] ?: method.invoke(proxy, args)
}
@Suppress("UNCHECKED_CAST")
val inputs = Proxy.newProxyInstance(klass.classLoader, arrayOf(klass), handler) as T
return InputsHolderImpl(inputs, observableMethods.toList(), subjects)
}
private class InputsHolderImpl<T : Any>(override val proxiedInputs: T,
private val methods: List<Method>,
private val subjects: Map<Method, PublishSubject<Any>>)
: InputsHolder<T> {
val subscription = CompositeSubscription()
override fun subscribe(inputs: T) {
methods.forEach { method ->
@Suppress("UNCHECKED_CAST")
val observable = method.invoke(inputs) as Observable<Any>
val subject = subjects[method]
observable.subscribe { subject?.onNext(it) }.addTo(subscription)
}
}
override fun clear() {
subscription.clear()
}
}
private fun Subscription.addTo(compositeSubscription: CompositeSubscription) =
compositeSubscription.add(this)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment