FRP
on
Android
Yaroslav Heriatovych
FRP
Functional - functional decomposition, no side effects
Reactive - data flow, propagation of change
Programming - the way you do it
Why reactive
Observer pattern
Problems
- Side effects
- Encapsulation
- Composability
- Resource management
- Abstraction
- Semantic distance
Reactive
REACTIVE
Execution context
How to deal with execution context ?
In Android world means:
How to execute heavy tasks on background threads
and deliver result (or error) in UI thread?
and deliver result (or error) in UI thread?
THREADS
Handler handler = new Handler();
new Thread(){
@Override
public void run() {
final String result = doHeavyTask();
handler.post(new Runnable() {
@Override
public void run() {
showResult(result);
}
});
}
}.start();
Threads
Pros:
Cons:Simple
Hard way to deliver results in UI threadBroken dataflow
Async task
new AsyncTask<Void, Integer, String>(){
@Override
protected String doInBackground(Void... params) {
return doHeavyTask();
}
@Override
protected void onPostExecute(String s) {
showResult(s);
}
}.execute();
Pros:
Cons:Deal with main thread
No error propogation to main threadNot bound to activity/fragment lifecycleNot composable
(don't tell me about nested async tasks)
Loaders
class MyFragment extends Fragment implements LoaderManager.LoaderCallbacks<String> { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getLoaderManager().initLoader(42, null, this); } @Override public Loader<String> onCreateLoader(int id, Bundle args) { return new AsyncTaskLoader<String>(getActivity()) { @Override public String loadInBackground() { return doHeavyTask(); } }; }
@Override public void onLoadFinished(Loader<String> loader, String data) { showResult(data); } @Override public void onLoaderReset(Loader<String> loader) {} }
Loaders
Pros:
Cons:Deals with main threadDeals with Activity/Fragment lifecycleGood for Cursor fetching
Not composableA lot of boilerplate codeBad for custom background logic
Android Annotations
@Background
void doSomethingInBackground() {
// do something
MyResult result = XXX;
updateUI(result);
}
// Notice that we manipulate the activity ref only from the UI thread
@UiThread
void updateUI(MyResult result) {
activity.showResult(result);
}
Pros:
Cons:Almost no boilerplate
No result propogation (and errors)No background tasks synchronization
So...
Is situation so bad?
And Now for Something Completely Different
RxJava
Observables fill the gap by being the ideal implementation of access to asynchronous sequences of multiple items | ||
---|---|---|
single items | multiple items | |
synchronous |
T getData()
|
Iterable<T> getData()
|
asynchronous |
Future<T> getData()
|
Observable<T> getData()
|
RxJava
An Observable is the asynchronous/push "dual" to the synchronous/pull Iterable | ||
---|---|---|
event | Iterable (pull) | Observable (push) |
retrieve data | T next() |
onNext(T) |
discover error | throws Exception
|
onError(Exception) |
complete | returns | onCompleted() |
RxJava contract
onNext*, (onError | onCompleted)
Primitives
public interface Observer <T> {
void onCompleted();
void onError(java.lang.Throwable throwable);
void onNext(T t);
}
public class Observable <T> {
public final static <T> Observable<T> create(OnSubscribe<T> f)
public rx.Subscription subscribe(rx.Observer<? super T> observer)
// ...
}
public static interface OnSubscribe<T> extends Action1<Subscriber<? super T>> {}
public interface Subscription { public void unsubscribe(); public boolean isUnsubscribed(); }
public abstract class Subscriber<T> implements Observer<T>, Subscription {...}
Observable creation
public Observable<String> getStrings(){
return Observable.create(new Observable.OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> subscriber) {
subscriber.onNext("Hello");
subscriber.onNext("World");
subscriber.onCompleted();
}
});
}
TL;DR:
When Observer subscribes, emit two strings and complete stream
(respect the contract!).
(respect the contract!).
Observable creation (better)
public Observable<String> getStrings() {
return Observable.create(new Observable.OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> subscriber) {
try {
subscriber.onNext("Hello");
subscriber.onNext("World");
subscriber.onCompleted();
} catch (Exception ex) {
subscriber.onError(ex);
}
}
});
}
Observer
Observable<String> strings = getStrings();
Subscription subsctiption = strings.subscribe(new Observer<String>() {
@Override
public void onCompleted() {
Log.d("rx", "no more data");
}
@Override
public void onError(Throwable throwable) {
throwable.printStackTrace();
}
@Override
public void onNext(String s) {
showResult(s);
}
});
TL;DR:
- handle every string;
- log when sequence completes
- print stackTrace in case of error
"Simple" composition
Observable<Integer> charsCounts(Observable<String> strings){ return Observable.create(new Observable.OnSubscribe<Integer>() { @Override public void call(Subscriber<? super Integer> subscriber) { subscriber.add(strings.subscribe(new Observer<String>() { @Override public void onCompleted() { subscriber.onCompleted(); } @Override public void onError(Throwable throwable) { subscriber.onError(throwable); }
@Override public void onNext(String s) { //actual transformation subscriber.onNext(s.length()); } })); } }); }
TL;DR: on every string count chars and deliver it to observer
Map
Ok, we have map:
Observable<Integer> charsCounts(Observable<String> strings){
return strings.map(new Func1<String, Integer>() {
@Override
public Integer call(String s) {
return s.length();
}
});
}
Yep, it's easy!
But...
Problem:
Java don't have lamdas
First solution
Use different language
- Scala
- Clojure
- Kotlin
- Xtend
Second solution
Use the same language
Do you know some Java with lambda support?
Java8
So...
We want to use Java8 lamdas on DalvikVM (Java6)
So we need to compile lamdas to something android can understand
And there is project that do exactly what we want!
Retrolambda
How it works
- Compile Java8 code with Java8 javac
- Convert java8 bytecode to java6 bytecode
- Pack new classes back
Rule:
Any functional interface can be converted to lambda
Functional interface is defined as any interface that has exactly one abstract method.
Config
buildscript {
repositories { mavenCentral() maven { url "https://oss.sonatype.org/content/repositories/snapshots" } } dependencies { classpath 'me.tatarka:gradle-retrolambda:1.2.+' } } // Required because retrolambda is on maven central repositories { mavenCentral() } apply plugin: 'android' apply plugin: 'retrolambda'
Config
Optional
retrolambda {
jdk System.getenv("JAVA8_HOME") javaVersion JavaVersion.VERSION_1_6 } android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } }
Java6
strings.map(new Func1<String, Integer>() {
@Override
public Integer call(String s) {
return s.length();
}
});
Java8
strings.map((String s) -> {
return s.length();
});
strings.map((String s) -> s.length());
strings.map((s) -> s.length());
strings.map(s -> s.length());
strings.map(String::length);
One more thing...
numbers.reduce(new Func2<Integer, Integer, Integer>() {
@Override
public Integer call(Integer i1, Integer i2) {
return i1 + i2;
}
});
!=
numbers.reduce((i1, i2) -> i1 + i2);
So, take quick look on bytecode
Bytecode time!
public class A {
public Observable<Integer> foo(Observable<Integer> numbers){
return numbers.reduce(new Func2<Integer, Integer, Integer>() {
@Override
public Integer call(Integer i1, Integer i2) {
return i1 + i2;
}
});
}
}
public class frp.A {
public frp.A();
Code:
0: aload_0
1: invokespecial #11 // Method java/lang/Object."":()V
4: return
public rx.Observable foo(rx.Observable);
Code:
0: aload_1
1: new #7 // class frp/A$1
4: dup
5: aload_0
6: invokespecial #18 // Method frp/A$1."":(Lfrp/A;)V
9: invokevirtual #24 // Method rx/Observable.reduce:(Lrx/util/functions/Func2;)Lrx/Observable;
12: areturn
}
A$1 inner class
class frp.A$1 implements rx.util.functions.Func2
{ final frp.A this$0; frp.A$1(frp.A); Code: 0: aload_0 1: aload_1 2: putfield #19 // Field this$0:Lfrp/A; 5: aload_0 6: invokespecial #22 // Method java/lang/Object." public java.lang.Integer call(java.lang.Integer, java.lang.Integer);":()V 9: return
Code: 0: aload_1 1: invokevirtual #32 // Method java/lang/Integer.intValue:()I 4: aload_2 5: invokevirtual #32 // Method java/lang/Integer.intValue:()I 8: iadd 9: invokestatic #36 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 12: areturn public java.lang.Object call(java.lang.Object, java.lang.Object); Code: 0: aload_0 1: aload_1 2: checkcast #28 // class java/lang/Integer 5: aload_2 6: checkcast #28 // class java/lang/Integer 9: invokevirtual #42 // Method call:(Ljava/lang/Integer;Ljava/lang/Integer;)Ljava/lang/Integer; 12: areturn }
B class
public class B {
public Observable foo(Observable numbers){
return numbers.reduce((i1, i2) -> i1 + i2);
}
}
public class frp.B { public frp.B(); Code: 0: aload_0 1: invokespecial #14 // Method java/lang/Object."
":()V 4: return public rx.Observable foo(rx.Observable ); Code: 0: aload_1 1: invokestatic #24 // Method frp/B$$Lambda$1.lambdaFactory$:()Lrx/util/functions/Func2; 4: invokevirtual #30 // Method rx/Observable.reduce:(Lrx/util/functions/Func2;)Lrx/Observable; 7: areturn
static java.lang.Integer lambda$foo$0(java.lang.Integer, java.lang.Integer); Code: 0: aload_0 1: invokevirtual #41 // Method java/lang/Integer.intValue:()I 4: aload_1 5: invokevirtual #41 // Method java/lang/Integer.intValue:()I 8: iadd 9: invokestatic #45 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 12: areturn }
B$$Lambda$1 Lambda class
final class frp.B$$Lambda$1 implements rx.util.functions.Func2 { public java.lang.Object call(java.lang.Object, java.lang.Object); Code: 0: aload_1 1: checkcast #14 // class java/lang/Integer 4: aload_2 5: checkcast #14 // class java/lang/Integer 8: invokestatic #20 // Method frp/B.lambda$foo$0: (Ljava/lang/Integer;Ljava/lang/Integer;)Ljava/lang/Integer;
11: areturn static {}; Code: 0: new #2 // class frp/B$$Lambda$1 3: dup 4: invokespecial #24 // Method "<init>":()V 7: putstatic #26 // Field instance:Lfrp/B$$Lambda$1; 10: return
public static rx.util.functions.Func2 lambdaFactory$(); Code: 0: getstatic #26 // Field instance:Lfrp/B$$Lambda$1; 3: areturn }
No reference to outer instance!
No object per call allocation!
RxJava
(again)
Observable<String> strings = Observable.create(
(Subscriber<? super String> subscriber) -> {
subscriber.onNext("Hello");
subscriber.onNext("World");
subscriber.onCompleted();
}
);
strings.map(str -> str.length())
.subscribe(length -> toast("length is " + length));
(Synchronous)
Image loading
Observable<Bitmap> imageObservable = Observable.create(observer -> {
Bitmap bm = downloadBitmap();
return bm;
});
imageObservable.subscribe(image -> loadToImageView(image));
But it will block UI thread!
imageObservable
.subscribeOn(Schedulers.newThread())
.subscribe(image -> loadToImageView(image));
But it will invoke loadToImageView not UI thread!
imageObservable
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(image -> loadToImageView(image));
Subscription
Observable<String> strings = Observable.create(
(Subscriber<? super String> subscriber) -> {
while (!subscriber.isUnsubscribed()){
String result = doHeavyTask();
subscriber.onNext(result);
}
});
Subscription subscription = strings.subscribe(str -> log(s));
public void onDestroy() {
super.onDestroy();
subscription.unsubscribe();
}
Subscription
(old way, before 0.17)
Observable<String> stringsNew = Observable.create(
new Observable.OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> subscriber) {
subscriber.onNext("Hello");
if (subscriber.isUnsubscribed()) return;
subscriber.onNext("World");
subscriber.onCompleted();
}
});
Observable<String> stringsOld = Observable.create(
new Observable.OnSubscribeFunc<String>() {
@Override
public Subscription onSubscribe(Observer<? super String> observer) {
Subscription subscription = BooleanSubscription.create();
observer.onNext("Hello");
if (subscription.isUnsubscribed())
return subscription; //Will not work
observer.onNext("World");
observer.onCompleted();
return subscription;
}
});
RxJava basics
Marble diagrams
map example
Observable creation
Main function
Observable.create(Observable.onSubscribe)
Other
from( ) — convert an Iterable or a Future or single value into an Observable
repeat( ) — create an Observable that emits a particular item or sequence of items repeatedly
timer( ) — create an Observable that emits a single item after a given delay
empty( ) — create an Observable that emits nothing and then completes
error( ) — create an Observable that emits nothing and then signals an error
never( ) — create an Observable that emits nothing at all
Observable transformation
- map( ) — transform the items emitted by an Observable by applying a function to each of them
- flatMap( ) — transform the items emitted by an Observable into Observables, then flatten this into a single Observable
- scan( ) — apply a function to each item emitted by an Observable, sequentially, and emit each successive value
- groupBy( ) and groupByUntil( ) — divide an Observable into a set of Observables that emit groups of items from the original Observable, organized by key
- buffer( ) — periodically gather items from an Observable into bundles and emit these bundles rather than emitting the items one at a time
-
window( ) — periodically subdivide items from an Observable into Observable windows and emit these windows rather than emitting the items one at a time
Map
flatMap
map, then flatten
scan
groupBy
buffer
Observable filtering
- filter( ) — filter items emitted by an Observable
- takeLast( ) — only emit the last n items emitted by an Observable
- takeLastBuffer( ) — emit the last n items emitted by an Observable, as a single list item
- skip( ) — ignore the first n items emitted by an Observable
- take( ) — emit only the first n items emitted by an Observable
- first( ) — emit only the first item emitted by an Observable, or the first item that meets some condition
- elementAt( ) — emit item n emitted by the source Observable
- timeout( ) — emit items from a source Observable, but issue an exception if no item is emitted in a specified timespan
- distinct( ) — suppress duplicate items emitted by the source Observable
filter
skip
take
timeout
distinct
distinctUntilChanged
Cold Observables
Observable<String> cold = Observable.create(observer ->{
observer.onNext("Hello");
observer.onNext("world");
});
Will generate items on every subscription
(lazy)
Subject
Observer + Observable
Channel
"Promise" for Observables
PublishSubject
PublishSubject<Integer> channel = PublishSubject.create();
Subscription a = channel.subscribe(x -> toast("a: "+x));
Subscription b = channel.subscribe(x -> toast("b: "+x));
channel.onNext(42);
a.unsubscribe();
channel.onNext(4711);
channel.onCompleted();
Subscription c = channel.subscribe(x -> toast("c: "+x));
channel.onNext(13);
ReplaySubject
ReplaySubject<Integer> channel = ReplaySubject.create();
Subscription a = channel.subscribe(x -> toast("a: "+x));
Subscription b = channel.subscribe(x -> toast("b: "+x));
channel.onNext(42);
a.unsubscribe();
channel.onNext(4711);
channel.onCompleted();
Subscription c = channel.subscribe(x -> toast("c: "+x));
channel.onNext(13);
4 Subjects
Android Examples
Background task
Observable.create((Subscriber<? super Bitmap> subscriber) -> {
try{
Bitmap image = loadImage();
subscriber.onNext(image);
subscriber.onCompleted();
}catch (Exception ex){
subscriber.onError(ex);
}
}).subscribeOn(Schedulers.io());
Can be simplified to
Async.start(() -> loadImage(), Schedulers.io());
or use
Schedulers.computation()
by default
Async.start(() -> calculateSomething());
Convert view to
Observable of clicks
public static <T extends View> Observable<T> clicksFrom(T view) {
PublishSubject publishSubject = PublishSubject.create();
view.setOnClickListener((v) -> publishSubject.onNext(view));
return publishSubject.asObservable();
}
clicksFrom(button).subscribe(view -> toast("clicked"));
Event composition
final ImageView imageView = (ImageView) findViewById(R.id.image);
Observable<Bitmap> imageObservable = Async.start(() -> loadImage(), Schedulers.io()) .cache() .observeOn(AndroidSchedulers.mainThread()); //show loaded imageObservable imageObservable.subscribe((Bitmap image) -> imageView.setImageBitmap(image));
//handle imageView clicks imageObservable .flatMap(image -> clicksFrom(imageView).map((ImageView view) -> image)) .subscribe((Bitmap image) -> processImage(image));
- No state
- No namespace pollution
- No condition branches
No Loaders
class NoLoadersFragment extends Fragment{ ImageView imageView; Observable<Bitmap> imageObservable; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); imageObservable = Async.start(() -> loadImage(), Schedulers.io()) .cache() .observeOn(AndroidSchedulers.mainThread()); }
@Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_processing, container, false); imageView = (ImageView) rootView.findViewById(R.id.image);
imageObservable.subscribe(image -> imageView.setImageBitmap(image)); return rootView; } }
List Fragment
Observable<String> loadUrl(String url){...}; Observable<Message> parse(String json){...}; class AsyncListFragment extends ListFragment{ Observable<Message> messages; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); String url = getArguments().getString("url"); messages = loadUrl(url) .flatMap(json -> parse(json)) .cache() .observeOn(AndroidSchedulers.mainThread()); }
@Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); MessagesAdapter adapter = new MessagesAdapter(); messages.first().subscribe(any -> setListAdapter(adapter)); messages.subscribe(message -> adapter.add(message)); } }
Drawing example
class CanvasView extends View { private final PublishSubject<MotionEvent> motionSubject = PublishSubject.create(); public final Observable<MotionEvent> motions = motionSubject.asObservable(); private final Paint paint; private Set<Path> paths = new HashSet<>(); public CanvasView(Context context) { super(context); paint = new Paint(); paint.setColor(context.getResources().getColor(android.R.color.black)); paint.setStyle(Paint.Style.STROKE); paint.setAntiAlias(true); setOnTouchListener((View v, MotionEvent event) -> { motionSubject.onNext(event); return true; }); }
public void addPath(Path path) { paths.add(path); this.invalidate(); } @Override protected void onDraw(Canvas canvas) { for (Path path : paths) canvas.drawPath(path, paint); } }
Drawing example
public class CanvasFragment extends Fragment { CanvasView canvas; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { canvas = new CanvasView(getActivity());
Observable<MotionEvent> downs = canvas.motions.filter(ev -> ev.getActionMasked() == MotionEvent.ACTION_DOWN); Observable<MotionEvent> ups = canvas.motions.filter(ev -> ev.getActionMasked() == MotionEvent.ACTION_UP); Observable<MotionEvent> moves = canvas.motions.filter(ev -> ev.getActionMasked() == MotionEvent.ACTION_MOVE); Observable<Path> pathObservable = downs.flatMap(downEvent -> { Path startPath = new Path(); startPath.moveTo(downEvent.getX(), downEvent.getY()); return moves.takeUntil(ups) .scan(startPath, (currentPath, movePoint) -> { currentPath.lineTo(movePoint.getX(), movePoint.getY()); return currentPath; }); });
pathObservable.subscribe(path -> canvas.addPath(path)); return canvas; } }
Multitouch
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { canvas = new CanvasView(getActivity()); Observable<MotionEvent> allDowns = canvas.motions.filter(ev -> ev.getActionMasked() == ACTION_DOWN || ev.getActionMasked() == ACTION_POINTER_DOWN); Observable<MotionEvent> allMoves = canvas.motions.filter(ev -> ev.getActionMasked() == ACTION_MOVE); Observable<MotionEvent> allUps = canvas.motions.filter(ev -> ev.getActionMasked() == ACTION_UP || ev.getActionMasked() == ACTION_POINTER_UP); Observable<Path> pathObservable = allDowns .flatMap(downEvent -> { int downIndex = downEvent.getActionIndex(); int fingerId = downEvent.getPointerId(downIndex); Observable<MotionEvent> ups = allUps.filter(evt -> evt.getPointerId(evt.getActionIndex()) == fingerId);
Path path = new Path(); path.moveTo(downEvent.getX(downIndex), downEvent.getY(downIndex)); return allMoves.filter(evt -> evt.findPointerIndex(fingerId) != -1).takeUntil(ups) .scan(path, (currentPath, movePoint) -> { int moveIndex = movePoint.findPointerIndex(fingerId); currentPath.lineTo(movePoint.getX(moveIndex), movePoint.getY(moveIndex)); return currentPath; }); }); pathObservable.subscribe(path -> canvas.addPath(path)); return canvas; }
Retrofit support
@GET("/user/{id}/photo")
Observable<Photo> getUserPhoto(@Path("id") int id);
Links
Questions?
FRP on Android
By Yaroslav Heriatovych
FRP on Android
Small introduction how to use FRP principles with RxJava library in modern Android applications
- 48,579