(by @andrestaltz)
If you prefer to watch video tutorials with live-coding, then check out this series I recorded with the same contents as in this article: Egghead.io - Introduction to Reactive Programming.
So you're curious in learning this new thing called Reactive Programming, particularly its variant comprising of Rx, Bacon.js, RAC, and others.
Learning it is hard, even harder by the lack of good material. When I started, I tried looking for tutorials. I found only a handful of practical guides, but they just scratched the surface and never tackled the challenge of building the whole architecture around it. Library documentations often don't help when you're trying to understand some function. I mean, honestly, look at this:
Rx.Observable.prototype.flatMapLatest(selector, [thisArg])
Projects each element of an observable sequence into a new sequence of observable sequences by incorporating the element's index and then transforms an observable sequence of observable sequences into an observable sequence producing values only from the most recent observable sequence.
Holy cow.
I've read two books, one just painted the big picture, while the other dived into how to use the Reactive library. I ended up learning Reactive Programming the hard way: figuring it out while building with it. At my work in Futurice I got to use it in a real project, and had the support of some colleagues when I ran into troubles.
The hardest part of the learning journey is thinking in Reactive. It's a lot about letting go of old imperative and stateful habits of typical programming, and forcing your brain to work in a different paradigm. I haven't found any guide on the internet in this aspect, and I think the world deserves a practical tutorial on how to think in Reactive, so that you can get started. Library documentation can light your way after that. I hope this helps you.
There are plenty of bad explanations and definitions out there on the internet. Wikipedia is too generic and theoretical as usual. Stackoverflow's canonical answer is obviously not suitable for newcomers. Reactive Manifesto sounds like the kind of thing you show to your project manager or the businessmen at your company. Microsoft's Rx terminology "Rx = Observables + LINQ + Schedulers" is so heavy and Microsoftish that most of us are left confused. Terms like "reactive" and "propagation of change" don't convey anything specifically different to what your typical MV* and favorite language already does. Of course my framework views react to the models. Of course change is propagated. If it wouldn't, nothing would be rendered.
So let's cut the bullshit.
In a way, this isn't anything new. Event buses or your typical click events are really an asynchronous event stream, on which you can observe and do some side effects. Reactive is that idea on steroids. You are able to create data streams of anything, not just from click and hover events. Streams are cheap and ubiquitous, anything can be a stream: variables, user inputs, properties, caches, data structures, etc. For example, imagine your Twitter feed would be a data stream in the same fashion that click events are. You can listen to that stream and react accordingly.
On top of that, you are given an amazing toolbox of functions to combine, create and filter any of those streams. That's where the "functional" magic kicks in. A stream can be used as an input to another one. Even multiple streams can be used as inputs to another stream. You can merge two streams. You can filter a stream to get another one that has only those events you are interested in. You can map data values from one stream to another new one.
If streams are so central to Reactive, let's take a careful look at them, starting with our familiar "clicks on a button" event stream.
A stream is a sequence of ongoing events ordered in time. It can emit three different things: a value (of some type), an error, or a "completed" signal. Consider that the "completed" takes place, for instance, when the current window or view containing that button is closed.
We capture these emitted events only asynchronously, by defining a function that will execute when a value is emitted, another function when an error is emitted, and another function when 'completed' is emitted. Sometimes these last two can be omitted and you can just focus on defining the function for values. The "listening" to the stream is called subscribing. The functions we are defining are observers. The stream is the subject (or "observable") being observed. This is precisely the Observer Design Pattern.
An alternative way of drawing that diagram is with ASCII, which we will use in some parts of this tutorial:
--a---b-c---d---X---|->
a, b, c, d are emitted values
X is an error
| is the 'completed' signal
---> is the timeline
Since this feels so familiar already, and I don't want you to get bored, let's do something new: we are going to create new click event streams transformed out of the original click event stream.
First, let's make a counter stream that indicates how many times a button was clicked. In common Reactive libraries, each stream has many functions attached to it, such as map, filter, scan, etc. When you call one of these functions, such as clickStream.map(f), it returns a new stream based on the click stream. It does not modify the original click stream in any way. This is a property called immutability, and it goes together with Reactive streams just like pancakes are good with syrup. That allows us to chain functions like clickStream.map(f).scan(g):
clickStream: ---c----c--c----c------c-->
vvvvv map(c becomes 1) vvvv
---1----1--1----1------1-->
vvvvvvvvv scan(+) vvvvvvvvv
counterStream: ---1----2--3----4------5-->
The map(f) function replaces (into the new stream) each emitted value according to a function f you provide. In our case, we mapped to the number 1 on each click. The scan(g) function aggregates all previous values on the stream, producing value x = g(accumulated, current), where g was simply the add function in this example. Then, counterStream emits the total number of clicks whenever a click happens.
To show the real power of Reactive, let's just say that you want to have a stream of "double click" events. To make it even more interesting, let's say we want the new stream to consider triple clicks as double clicks, or in general, multiple clicks (two or more). Take a deep breath and imagine how you would do that in a traditional imperative and stateful fashion. I bet it sounds fairly nasty and involves some variables to keep state and some fiddling with time intervals.
Well, in Reactive it's pretty simple. In fact, the logic is just 4 lines of code. But let's ignore code for now. Thinking in diagrams is the best way to understand and build streams, whether you're a beginner or an expert.
Grey boxes are functions transforming one stream into another. First we accumulate clicks in lists, whenever 250 milliseconds of "event silence" has happened (that's what buffer(stream.throttle(250ms)) does, in a nutshell. Don't worry about understanding the details at this point, we are just demoing Reactive for now). The result is a stream of lists, from which we apply map() to map each list to an integer matching the length of that list. Finally, we ignore 1 integers using the filter(x >= 2) function. That's it: 3 operations to produce our intended stream. We can then subscribe ("listen") to it to react accordingly how we wish.
I hope you enjoy the beauty of this approach. This example is just the tip of the iceberg: you can apply the same operations on different kinds of streams, for instance, on a stream of API responses; on the other hand, there are many other functions available.
Reactive Programming raises the level of abstraction of your code so you can focus on the interdependence of events that define the business logic, rather than having to constantly fiddle with a large amount of implementation details. Code in RP will likely be more concise.
The benefit is more evident in modern webapps and mobile apps that are highly interactive with a multitude of UI events related to data events. 10 years ago, interaction with web pages was basically about submitting a long form to the backend and performing simple rendering to the frontend. Apps have evolved to be more real-time: modifying a single form field can automatically trigger a save to the backend, "likes" to some content can be reflected in real time to other connected users, and so forth.
Apps nowadays have an abundancy of real-time events of every kind that enable a highly interactive experience to the user. We need tools for properly dealing with that, and Reactive Programming is an answer.
Let's dive into the real stuff. A real-world example with a step-by-step guide on how to think in RP. No synthetic examples, no half-explained concepts. By the end of this tutorial we will have produced real functioning code, while knowing why we did each thing.
I picked JavaScript and RxJS as the tools for this, for a reason: JavaScript is the most familiar language out there at the moment, and the Rx* library family is widely available for many languages and platforms (.NET, Java, Scala, Clojure, JavaScript, Ruby, Python, C++, Objective-C/Cocoa, Groovy, etc). So whatever your tools are, you can concretely benefit by following this tutorial.
In Twitter there is this UI element that suggests other accounts you could follow:
We are going to focus on imitating its core features, which are:
- On startup, load accounts data from the API and display 3 suggestions
- On clicking "Refresh", load 3 other account suggestions into the 3 rows
- On click 'x' button on an account row, clear only that current account and display another
- Each row displays the account's avatar and links to their page
We can leave out the other features and buttons because they are minor. And, instead of Twitter, which recently closed its API to the unauthorized public, let's build that UI for following people on Github. There's a Github API for getting users.
The complete code for this is ready at http://jsfiddle.net/staltz/8jFJH/48/ in case you want to take a peak already.
How do you approach this problem with Rx? Well, to start with, (almost) everything can be a stream. That's the Rx mantra. Let's start with the easiest feature: "on startup, load 3 accounts data from the API". There is nothing special here, this is simply about (1) doing a request, (2) getting a response, (3) rendering the response. So let's go ahead and represent our requests as a stream. At first this will feel like overkill, but we need to start from the basics, right?
On startup we need to do only one request, so if we model it as a data stream, it will be a stream with only one emitted value. Later, we know we will have many requests happening, but for now, it is just one.
--a------|->
Where a is the string 'https://api.github.com/users'
This is a stream of URLs that we want to request. Whenever a request event happens, it tells us two things: when and what. "When" the request should be executed is when the event is emitted. And "what" should be requested is the value emitted: a string containing the URL.
To create such stream with a single value is very simple in Rx*. The official terminology for a stream is "Observable", for the fact that it can be observed, but I find it to be a silly name, so I call it stream.
var requestStream = Rx.Observable.just('https://api.github.com/users');But now, that is just a stream of strings, doing no other operation, so we need to somehow make something happen when that value is emitted. That's done by subscribing to the stream.
requestStream.subscribe(function(requestUrl) {
// execute the request
jQuery.getJSON(requestUrl, function(responseData) {
// ...
});
}Notice we are using a jQuery Ajax callback (which we assume you should know already) to handle the asynchronicity of the request operation. But wait a moment, Rx is for dealing with asynchronous data streams. Couldn't the response for that request be a stream containing the data arriving at some time in the future? Well, at a conceptual level, it sure looks like it, so let's try that.
requestStream.subscribe(function(requestUrl) {
// execute the request
var responseStream = Rx.Observable.create(function (observer) {
jQuery.getJSON(requestUrl)
.done(function(response) { observer.onNext(response); })
.fail(function(jqXHR, status, error) { observer.onError(error); })
.always(function() { observer.onCompleted(); });
});
responseStream.subscribe(function(response) {
// do something with the response
});
}What Rx.Observable.create() does is create your own custom stream by explicitly informing each observer (or in other words, a "subscriber") about data events (onNext()) or errors (onError()). What we did was just wrap that jQuery Ajax Promise. Excuse me, does this mean that a Promise is an Observable?
Yes.
Observable is Promise++. In Rx you can easily convert a Promise to an Observable by doing var stream = Rx.Observable.fromPromise(promise), so let's use that. The only difference is that Observables are not Promises/A+ compliant, but conceptually there is no clash. A Promise is simply an Observable with one single emitted value. Rx streams go beyond promises by allowing many returned values.
This is pretty nice, and shows how Observables are at least as powerful as Promises. So if you believe the Promises hype, keep an eye on what Rx Observables are capable of.
Now back to our example, if you were quick to notice, we have one subscribe() call inside another, which is somewhat akin to callback hell. Also, the creation of responseStream is dependent on requestStream. As you heard before, in Rx there are simple mechanisms for transforming and creating new streams out of others, so we should be doing that.
The one basic function that you should know by now is map(f), which takes each value of stream A, applies f() on it, and produces a value on stream B. If we do that to our request and response streams, we can map request URLs to response Promises (disguised as streams).
var responseMetastream = requestStream
.map(function(requestUrl) {
return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
});Then we will have created a beast called "metastream": a stream of streams. Don't panic yet. A metastream is a stream where each emitted value is yet another stream. You can think of it as pointers: each emitted value is a pointer to another stream. In our example, each request URL is mapped to a pointer to the promise stream containing the corresponding response.
A metastream for responses looks confusing, and doesn't seem to help us at all. We just want a simple stream of responses, where each emitted value is a JSON object, not a 'Promise' of a JSON object. Say hi to Mr. Flatmap: a version of map() that "flattens" a metastream, by emitting on the "trunk" stream everything that will be emitted on "branch" streams. Flatmap is not a "fix" and metastreams are not a bug, these are really the tools for dealing with asynchronous responses in Rx.
var responseStream = requestStream
.flatMap(function(requestUrl) {
return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
});Nice. And because the response stream is defined according to request stream, if we have later on more events happening on request stream, we will have the corresponding response events happening on response stream, as expected:
requestStream: --a-----b--c------------|->
responseStream: -----A--------B-----C---|->
(lowercase is a request, uppercase is its response)
Now that we finally have a response stream, we can render the data we receive:
responseStream.subscribe(function(response) {
// render `response` to the DOM however you wish
});Joining all the code until now, we have:
var requestStream = Rx.Observable.just('https://api.github.com/users');
var responseStream = requestStream
.flatMap(function(requestUrl) {
return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
});
responseStream.subscribe(function(response) {
// render `response` to the DOM however you wish
});I did not yet mention that the JSON in the response is a list with 100 users. The API only allows us to specify the page offset, and not the page size, so we're using just 3 data objects and wasting 97 others. We can ignore that problem for now, since later on we will see how to cache the responses.
Everytime the refresh button is clicked, the request stream should emit a new URL, so that we can get a new response. We need two things: a stream of click events on the refresh button (mantra: anything can be a stream), and we need to change the request stream to depend on the refresh click stream. Gladly, RxJS comes with tools to make Observables from event listeners.
var refreshButton = document.querySelector('.refresh');
var refreshClickStream = Rx.Observable.fromEvent(refreshButton, 'click');Since the refresh click event doesn't itself carry any API URL, we need to map each click to an actual URL. Now we change the request stream to be the refresh click stream mapped to the API endpoint with a random offset parameter each time.
var requestStream = refreshClickStream
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
});Because I'm dumb and I don't have automated tests, I just broke one of our previously built features. A request doesn't happen anymore on startup, it happens only when the refresh is clicked. Urgh. I need both behaviors: a request when either a refresh is clicked or the webpage was just opened.
We know how to make a separate stream for each one of those cases:
var requestOnRefreshStream = refreshClickStream
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
});
var startupRequestStream = Rx.Observable.just('https://api.github.com/users');But how can we "merge" these two into one? Well, there's merge(). Explained in the diagram dialect, this is what it does:
stream A: ---a--------e-----o----->
stream B: -----B---C-----D-------->
vvvvvvvvv merge vvvvvvvvv
---a-B---C--e--D--o----->
It should be easy now:
var requestOnRefreshStream = refreshClickStream
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
});
var startupRequestStream = Rx.Observable.just('https://api.github.com/users');
var requestStream = Rx.Observable.merge(
requestOnRefreshStream, startupRequestStream
);There is an alternative and cleaner way of writing that, without the intermediate streams.
var requestStream = refreshClickStream
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
})
.merge(Rx.Observable.just('https://api.github.com/users'));Even shorter, even more readable:
var requestStream = refreshClickStream
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
})
.startWith('https://api.github.com/users');The startWith() function does exactly what you think it does. No matter how your input stream looks like, the output stream resulting of startWith(x) will have x at the beginning. But I'm not DRY enough, I'm repeating the API endpoint string. One way to fix this is by moving the startWith() close to the refreshClickStream, to essentially "emulate" a refresh click on startup.
var requestStream = refreshClickStream.startWith('startup click')
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
});Nice. If you go back to the point where I "broke the automated tests", you should see that the only difference with this last approach is that I added the startWith().
Until now, we have only touched a suggestion UI element on the rendering step that happens in the responseStream's subscribe(). Now with the refresh button, we have a problem: as soon as you click 'refresh', the current 3 suggestions are not cleared. New suggestions come in only after a response has arrived, but to make the UI look nice, we need to clean out the current suggestions when clicks happen on the refresh.
refreshClickStream.subscribe(function() {
// clear the 3 suggestion DOM elements
});No, not so fast, pal. This is bad, because we now have two subscribers that affect the suggestion DOM elements (the other one being responseStream.subscribe()), and that doesn't really sound like Separation of concerns. Remember the Reactive mantra?
So let's model a suggestion as a stream, where each emitted value is the JSON object containing the suggestion data. We will do this separately for each of the 3 suggestions. This is how the stream for suggestion #1 could look like:
var suggestion1Stream = responseStream
.map(function(listUsers) {
// get one random user from the list
return listUsers[Math.floor(Math.random()*listUsers.length)];
});The others, suggestion2Stream and suggestion3Stream can be simply copy pasted from suggestion1Stream. This is not DRY, but it will keep our example simple for this tutorial, plus I think it's a good exercise to think how to avoid repetition in this case.
Instead of having the rendering happen in responseStream's subscribe(), we do that here:
suggestion1Stream.subscribe(function(suggestion) {
// render the 1st suggestion to the DOM
});Back to the "on refresh, clear the suggestions", we can simply map refresh clicks to null suggestion data, and include that in the suggestion1Stream, as such:
var suggestion1Stream = responseStream
.map(function(listUsers) {
// get one random user from the list
return listUsers[Math.floor(Math.random()*listUsers.length)];
})
.merge(
refreshClickStream.map(function(){ return null; })
);And when rendering, we interpret null as "no data", hence hiding its UI element.
suggestion1Stream.subscribe(function(suggestion) {
if (suggestion === null) {
// hide the first suggestion DOM element
}
else {
// show the first suggestion DOM element
// and render the data
}
});The big picture is now:
refreshClickStream: ----------o--------o---->
requestStream: -r--------r--------r---->
responseStream: ----R---------R------R-->
suggestion1Stream: ----s-----N---s----N-s-->
suggestion2Stream: ----q-----N---q----N-q-->
suggestion3Stream: ----t-----N---t----N-t-->
Where N stands for null.
As a bonus, we can also render "empty" suggestions on startup. That is done by adding startWith(null) to the suggestion streams:
var suggestion1Stream = responseStream
.map(function(listUsers) {
// get one random user from the list
return listUsers[Math.floor(Math.random()*listUsers.length)];
})
.merge(
refreshClickStream.map(function(){ return null; })
)
.startWith(null);Which results in:
refreshClickStream: ----------o---------o---->
requestStream: -r--------r---------r---->
responseStream: ----R----------R------R-->
suggestion1Stream: -N--s-----N----s----N-s-->
suggestion2Stream: -N--q-----N----q----N-q-->
suggestion3Stream: -N--t-----N----t----N-t-->
There is one feature remaining to implement. Each suggestion should have its own 'x' button for closing it, and loading another in its place. At first thought, you could say it's enough to make a new request when any close button is clicked:
var close1Button = document.querySelector('.close1');
var close1ClickStream = Rx.Observable.fromEvent(close1Button, 'click');
// and the same for close2Button and close3Button
var requestStream = refreshClickStream.startWith('startup click')
.merge(close1ClickStream) // we added this
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
});That does not work. It will close and reload all suggestions, rather than just only the one we clicked on. There are a couple of different ways of solving this, and to keep it interesting, we will solve it by reusing previous responses. The API's response page size is 100 users while we were using just 3 of those, so there is plenty of fresh data available. No need to request more.
Again, let's think in streams. When a 'close1' click event happens, we want to use the most recently emitted response on responseStream to get one random user from the list in the response. As such:
requestStream: --r--------------->
responseStream: ------R----------->
close1ClickStream: ------------c----->
suggestion1Stream: ------s-----s----->
In Rx* there is a combinator function called combineLatest that seems to do what we need. It takes two streams A and B as inputs, and whenever either stream emits a value, combineLatest joins the two most recently emitted values a and b from both streams and outputs a value c = f(x,y), where f is a function you define. It is better explained with a diagram:
stream A: --a-----------e--------i-------->
stream B: -----b----c--------d-------q---->
vvvvvvvv combineLatest(f) vvvvvvv
----AB---AC--EC---ED--ID--IQ---->
where f is the uppercase function
We can apply combineLatest() on close1ClickStream and responseStream, so that whenever the close 1 button is clicked, we get the latest response emitted and produce a new value on suggestion1Stream. On the other hand, combineLatest() is symmetric: whenever a new response is emitted on responseStream, it will combine with the latest 'close 1' click to produce a new suggestion. That is interesting, because it allows us to simplify our previous code for suggestion1Stream, like this:
var suggestion1Stream = close1ClickStream
.combineLatest(responseStream,
function(click, listUsers) {
return listUsers[Math.floor(Math.random()*listUsers.length)];
}
)
.merge(
refreshClickStream.map(function(){ return null; })
)
.startWith(null);One piece is still missing in the puzzle. The combineLatest() uses the most recent of the two sources, but if one of those sources hasn't emitted anything yet, combineLatest() cannot produce a data event on the output stream. If you look at the ASCII diagram above, you will see that the output has nothing when the first stream emitted value a. Only when the second stream emitted value b could it produce an output value.
There are different ways of solving this, and we will stay with the simplest one, which is simulating a click to the 'close 1' button on startup:
var suggestion1Stream = close1ClickStream.startWith('startup click') // we added this
.combineLatest(responseStream,
function(click, listUsers) {l
return listUsers[Math.floor(Math.random()*listUsers.length)];
}
)
.merge(
refreshClickStream.map(function(){ return null; })
)
.startWith(null);And we're done. The complete code for all this was:
var refreshButton = document.querySelector('.refresh');
var refreshClickStream = Rx.Observable.fromEvent(refreshButton, 'click');
var closeButton1 = document.querySelector('.close1');
var close1ClickStream = Rx.Observable.fromEvent(closeButton1, 'click');
// and the same logic for close2 and close3
var requestStream = refreshClickStream.startWith('startup click')
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
});
var responseStream = requestStream
.flatMap(function (requestUrl) {
return Rx.Observable.fromPromise($.ajax({url: requestUrl}));
});
var suggestion1Stream = close1ClickStream.startWith('startup click')
.combineLatest(responseStream,
function(click, listUsers) {
return listUsers[Math.floor(Math.random()*listUsers.length)];
}
)
.merge(
refreshClickStream.map(function(){ return null; })
)
.startWith(null);
// and the same logic for suggestion2Stream and suggestion3Stream
suggestion1Stream.subscribe(function(suggestion) {
if (suggestion === null) {
// hide the first suggestion DOM element
}
else {
// show the first suggestion DOM element
// and render the data
}
});You can see this working example at http://jsfiddle.net/staltz/8jFJH/48/
That piece of code is small but dense: it features management of multiple events with proper separation of concerns, and even caching of responses. The functional style made the code look more declarative than imperative: we are not giving a sequence of instructions to execute, we are just telling what something is by defining relationships between streams. For instance, with Rx we told the computer that suggestion1Stream is the 'close 1' stream combined with one user from the latest response, besides being null when a refresh happens or program startup happened.
Notice also the impressive absence of control flow elements such as if, for, while, and the typical callback-based control flow that you expect from a JavaScript application. You can even get rid of the if and else in the subscribe() above by using filter() if you want (I'll leave the implementation details to you as an exercise). In Rx, we have stream functions such as map, filter, scan, merge, combineLatest, startWith, and many more to control the flow of an event-driven program. This toolset of functions gives you more power in less code.
If you think Rx* will be your preferred library for Reactive Programming, take a while to get acquainted with the big list of functions for transforming, combining, and creating Observables. If you want to understand those functions in diagrams of streams, take a look at RxJava's very useful documentation with marble diagrams. Whenever you get stuck trying to do something, draw those diagrams, think on them, look at the long list of functions, and think more. This workflow has been effective in my experience.
Once you start getting the hang of programming with Rx*, it is absolutely required to understand the concept of Cold vs Hot Observables. If you ignore this, it will come back and bite you brutally. You have been warned. Sharpen your skills further by learning real functional programming, and getting acquainted with issues such as side effects that affect Rx*.
But Reactive Programming is not just Rx*. There is Bacon.js which is intuitive to work with, without the quirks you sometimes encounter in Rx*. The Elm Language lives in its own category: it's a Functional Reactive Programming language that compiles to JavaScript + HTML + CSS, and features a time travelling debugger. Pretty awesome.
Rx works great for event-heavy frontends and apps. But it is not just a client-side thing, it works great also in the backend and close to databases. In fact, RxJava is a key component for enabling server-side concurrency in Netflix's API. Rx is not a framework restricted to one specific type of application or language. It really is a paradigm that you can use when programming any event-driven software.
If this tutorial helped you, tweet it forward.
© Andre Medeiros (alias "Andre Staltz"), 2014. Unauthorized use and/or duplication of this material without express and written permission from this site’s author and/or owner is strictly prohibited. Excerpts and links may be used, provided that full and clear credit is given to Andre Medeiros and http://andre.staltz.com with appropriate and specific direction to the original content.
"Introduction to Reactive Programming you've been missing" by Andre Staltz is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.
Based on a work at https://gist.github.com/staltz/868e7e9bc2a7b8c1f754.
Created by Yoshifumi Kawai(neuecc)
UniRx (Reactive Extensions for Unity) is a reimplementation of the .NET Reactive Extensions. The Official Rx implementation is great but doesn't work on Unity and has issues with iOS IL2CPP compatibility. This library fixes those issues and adds some specific utilities for Unity. Supported platforms are PC/Mac/Android/iOS/WebGL/WindowsStore/etc and the library.
UniRx is available on the Unity Asset Store (FREE) - http://u3d.as/content/neuecc/uni-rx-reactive-extensions-for-unity/7tT
Blog for update info - https://medium.com/@neuecc
Support thread on the Unity Forums: Ask me any question - http://forum.unity3d.com/threads/248535-UniRx-Reactive-Extensions-for-Unity
Release Notes, see UniRx/releases
UniRx is Core Library (Port of Rx) + Platform Adaptor (MainThreadScheduler/FromCoroutine/etc) + Framework (ObservableTriggers/ReactiveProeperty/etc).
Note: async/await integration(UniRx.Async) is separated to Cysharp/UniTask after ver. 7.0.
Ordinarily, Network operations in Unity require the use of WWW and Coroutine. That said, using Coroutine is not good practice for asynchronous operations for the following (and other) reasons:
- Coroutines can't return any values, since its return type must be IEnumerator.
- Coroutines can't handle exceptions, because yield return statements cannot be surrounded with a try-catch construction.
This kind of lack of composability causes operations to be close-coupled, which often results in huge monolithic IEnumerators.
Rx cures that kind of "asynchronous blues". Rx is a library for composing asynchronous and event-based programs using observable collections and LINQ-style query operators.
The game loop (every Update, OnCollisionEnter, etc), sensor data (Kinect, Leap Motion, VR Input, etc.) are all types of events. Rx represents events as reactive sequences which are both easily composable and support time-based operations by using LINQ query operators.
Unity is generally single threaded but UniRx facilitates multithreading for joins, cancels, accessing GameObjects, etc.
UniRx helps UI programming with uGUI. All UI events (clicked, valuechanged, etc) can be converted to UniRx event streams.
Unity supports async/await from 2017 with C# upgrades, UniRx family prjects provides more lightweight, more powerful async/await integration with Unity. Please see Cysharp/UniTask.
Great introduction to Rx article: The introduction to Reactive Programming you've been missing.
The following code implements the double click detection example from the article in UniRx:
var clickStream = Observable.EveryUpdate()
.Where(_ => Input.GetMouseButtonDown(0));
clickStream.Buffer(clickStream.Throttle(TimeSpan.FromMilliseconds(250)))
.Where(xs => xs.Count >= 2)
.Subscribe(xs => Debug.Log("DoubleClick Detected! Count:" + xs.Count));This example demonstrates the following features (in only five lines!):
- The game loop (Update) as an event stream
- Composable event streams
- Merging self stream
- Easy handling of time based operations
Use ObservableWWW for asynchronous network operations. Its Get/Post functions return subscribable IObservables:
ObservableWWW.Get("http://google.co.jp/")
.Subscribe(
x => Debug.Log(x.Substring(0, 100)), // onSuccess
ex => Debug.LogException(ex)); // onErrorRx is composable and cancelable. You can also query with LINQ expressions:
// composing asynchronous sequence with LINQ query expressions
var query = from google in ObservableWWW.Get("http://google.com/")
from bing in ObservableWWW.Get("http://bing.com/")
from unknown in ObservableWWW.Get(google + bing)
select new { google, bing, unknown };
var cancel = query.Subscribe(x => Debug.Log(x));
// Call Dispose is cancel.
cancel.Dispose();Use Observable.WhenAll for parallel requests:
// Observable.WhenAll is for parallel asynchronous operation
// (It's like Observable.Zip but specialized for single async operations like Task.WhenAll)
var parallel = Observable.WhenAll(
ObservableWWW.Get("http://google.com/"),
ObservableWWW.Get("http://bing.com/"),
ObservableWWW.Get("http://unity3d.com/"));
parallel.Subscribe(xs =>
{
Debug.Log(xs[0].Substring(0, 100)); // google
Debug.Log(xs[1].Substring(0, 100)); // bing
Debug.Log(xs[2].Substring(0, 100)); // unity
});Progress information is available:
// notifier for progress use ScheduledNotifier or new Progress<float>(/* action */)
var progressNotifier = new ScheduledNotifier<float>();
progressNotifier.Subscribe(x => Debug.Log(x)); // write www.progress
// pass notifier to WWW.Get/Post
ObservableWWW.Get("http://google.com/", progress: progressNotifier).Subscribe();Error handling:
// If WWW has .error, ObservableWWW throws WWWErrorException to onError pipeline.
// WWWErrorException has RawErrorMessage, HasResponse, StatusCode, ResponseHeaders
ObservableWWW.Get("http://www.google.com/404")
.CatchIgnore((WWWErrorException ex) =>
{
Debug.Log(ex.RawErrorMessage);
if (ex.HasResponse)
{
Debug.Log(ex.StatusCode);
}
foreach (var item in ex.ResponseHeaders)
{
Debug.Log(item.Key + ":" + item.Value);
}
})
.Subscribe();IEnumerator (Coroutine) is Unity's primitive asynchronous tool. UniRx integrates coroutines and IObservables. You can write asynchronious code in coroutines, and orchestrate them using UniRx. This is best way to control asynchronous flow.
// two coroutines
IEnumerator AsyncA()
{
Debug.Log("a start");
yield return new WaitForSeconds(1);
Debug.Log("a end");
}
IEnumerator AsyncB()
{
Debug.Log("b start");
yield return new WaitForEndOfFrame();
Debug.Log("b end");
}
// main code
// Observable.FromCoroutine converts IEnumerator to Observable<Unit>.
// You can also use the shorthand, AsyncA().ToObservable()
// after AsyncA completes, run AsyncB as a continuous routine.
// UniRx expands SelectMany(IEnumerator) as SelectMany(IEnumerator.ToObservable())
var cancel = Observable.FromCoroutine(AsyncA)
.SelectMany(AsyncB)
.Subscribe();
// you can stop a coroutine by calling your subscription's Dispose.
cancel.Dispose();If in Unity 5.3, you can use ToYieldInstruction for Observable to Coroutine.
IEnumerator TestNewCustomYieldInstruction()
{
// wait Rx Observable.
yield return Observable.Timer(TimeSpan.FromSeconds(1)).ToYieldInstruction();
// you can change the scheduler(this is ignore Time.scale)
yield return Observable.Timer(TimeSpan.FromSeconds(1), Scheduler.MainThreadIgnoreTimeScale).ToYieldInstruction();
// get return value from ObservableYieldInstruction
var o = ObservableWWW.Get("http://unity3d.com/").ToYieldInstruction(throwOnError: false);
yield return o;
if (o.HasError) { Debug.Log(o.Error.ToString()); }
if (o.HasResult) { Debug.Log(o.Result); }
// other sample(wait until transform.position.y >= 100)
yield return this.transform.ObserveEveryValueChanged(x => x.position).FirstOrDefault(p => p.y >= 100).ToYieldInstruction();
}Normally, we have to use callbacks when we require a coroutine to return a value. Observable.FromCoroutine can convert coroutines to cancellable IObservable[T] instead.
// public method
public static IObservable<string> GetWWW(string url)
{
// convert coroutine to IObservable
return Observable.FromCoroutine<string>((observer, cancellationToken) => GetWWWCore(url, observer, cancellationToken));
}
// IObserver is a callback publisher
// Note: IObserver's basic scheme is "OnNext* (OnError | Oncompleted)?"
static IEnumerator GetWWWCore(string url, IObserver<string> observer, CancellationToken cancellationToken)
{
var www = new UnityEngine.WWW(url);
while (!www.isDone && !cancellationToken.IsCancellationRequested)
{
yield return null;
}
if (cancellationToken.IsCancellationRequested) yield break;
if (www.error != null)
{
observer.OnError(new Exception(www.error));
}
else
{
observer.OnNext(www.text);
observer.OnCompleted(); // IObserver needs OnCompleted after OnNext!
}
}Here are some more examples. Next is a multiple OnNext pattern.
public static IObservable<float> ToObservable(this UnityEngine.AsyncOperation asyncOperation)
{
if (asyncOperation == null) throw new ArgumentNullException("asyncOperation");
return Observable.FromCoroutine<float>((observer, cancellationToken) => RunAsyncOperation(asyncOperation, observer, cancellationToken));
}
static IEnumerator RunAsyncOperation(UnityEngine.AsyncOperation asyncOperation, IObserver<float> observer, CancellationToken cancellationToken)
{
while (!asyncOperation.isDone && !cancellationToken.IsCancellationRequested)
{
observer.OnNext(asyncOperation.progress);
yield return null;
}
if (!cancellationToken.IsCancellationRequested)
{
observer.OnNext(asyncOperation.progress); // push 100%
observer.OnCompleted();
}
}
// usecase
Application.LoadLevelAsync("testscene")
.ToObservable()
.Do(x => Debug.Log(x)) // output progress
.Last() // last sequence is load completed
.Subscribe();// Observable.Start is start factory methods on specified scheduler
// default is on ThreadPool
var heavyMethod = Observable.Start(() =>
{
// heavy method...
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(1));
return 10;
});
var heavyMethod2 = Observable.Start(() =>
{
// heavy method...
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(3));
return 10;
});
// Join and await two other thread values
Observable.WhenAll(heavyMethod, heavyMethod2)
.ObserveOnMainThread() // return to main thread
.Subscribe(xs =>
{
// Unity can't touch GameObject from other thread
// but use ObserveOnMainThread, you can touch GameObject naturally.
(GameObject.Find("myGuiText")).guiText.text = xs[0] + ":" + xs[1];
}); UniRx's default time based operations (Interval, Timer, Buffer(timeSpan), etc) use Scheduler.MainThread as their scheduler. That means most operators (except for Observable.Start) work on a single thread, so ObserverOn isn't needed and thread safety measures can be ignored. This is differet from the standard RxNet implementation but better suited to the Unity environment.
Scheduler.MainThread runs under Time.timeScale's influence. If you want to ignore the time scale, use Scheduler.MainThreadIgnoreTimeScale instead.
UniRx can handle MonoBehaviour events with UniRx.Triggers:
using UniRx;
using UniRx.Triggers; // need UniRx.Triggers namespace
public class MyComponent : MonoBehaviour
{
void Start()
{
// Get the plain object
var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
// Add ObservableXxxTrigger for handle MonoBehaviour's event as Observable
cube.AddComponent<ObservableUpdateTrigger>()
.UpdateAsObservable()
.SampleFrame(30)
.Subscribe(x => Debug.Log("cube"), () => Debug.Log("destroy"));
// destroy after 3 second:)
GameObject.Destroy(cube, 3f);
}
}Supported triggers are listed in UniRx.wiki#UniRx.Triggers.
These can also be handled more easily by directly subscribing to observables returned by extension methods on Component/GameObject. These methods inject ObservableTrigger automaticaly (except for ObservableEventTrigger and ObservableStateMachineTrigger):
using UniRx;
using UniRx.Triggers; // need UniRx.Triggers namespace for extend gameObejct
public class DragAndDropOnce : MonoBehaviour
{
void Start()
{
// All events can subscribe by ***AsObservable
this.OnMouseDownAsObservable()
.SelectMany(_ => this.UpdateAsObservable())
.TakeUntil(this.OnMouseUpAsObservable())
.Select(_ => Input.mousePosition)
.Subscribe(x => Debug.Log(x));
}
}Previous versions of UniRx provided
ObservableMonoBehaviour. This is a legacy interface that is no longer supported. Please use UniRx.Triggers instead.
Converting to Observable is the best way to handle Unity events. If the standard triggers supplied by UniRx are not enough, you can create custom triggers. To demonstrate, here's a LongTap trigger for uGUI:
public class ObservableLongPointerDownTrigger : ObservableTriggerBase, IPointerDownHandler, IPointerUpHandler
{
public float IntervalSecond = 1f;
Subject<Unit> onLongPointerDown;
float? raiseTime;
void Update()
{
if (raiseTime != null && raiseTime <= Time.realtimeSinceStartup)
{
if (onLongPointerDown != null) onLongPointerDown.OnNext(Unit.Default);
raiseTime = null;
}
}
void IPointerDownHandler.OnPointerDown(PointerEventData eventData)
{
raiseTime = Time.realtimeSinceStartup + IntervalSecond;
}
void IPointerUpHandler.OnPointerUp(PointerEventData eventData)
{
raiseTime = null;
}
public IObservable<Unit> OnLongPointerDownAsObservable()
{
return onLongPointerDown ?? (onLongPointerDown = new Subject<Unit>());
}
protected override void RaiseOnCompletedOnDestroy()
{
if (onLongPointerDown != null)
{
onLongPointerDown.OnCompleted();
}
}
}It can be used as easily as the standard triggers:
var trigger = button.AddComponent<ObservableLongPointerDownTrigger>();
trigger.OnLongPointerDownAsObservable().Subscribe();When is OnCompleted called? Subscription lifecycle management is very important to consider when using UniRx. ObservableTriggers call OnCompleted when the GameObject they are attached to is destroyed. Other static generator methods (Observable.Timer, Observable.EveryUpdate, etc...) do not stop automatically, and their subscriptions should be managed manually.
Rx provides some helper methods, such as IDisposable.AddTo which allows you to dispose of several subscriptions at once:
// CompositeDisposable is similar with List<IDisposable>, manage multiple IDisposable
CompositeDisposable disposables = new CompositeDisposable(); // field
void Start()
{
Observable.EveryUpdate().Subscribe(x => Debug.Log(x)).AddTo(disposables);
}
void OnTriggerEnter(Collider other)
{
// .Clear() => Dispose is called for all inner disposables, and the list is cleared.
// .Dispose() => Dispose is called for all inner disposables, and Dispose is called immediately after additional Adds.
disposables.Clear();
}If you want to automatically Dispose when a GameObjects is destroyed, use AddTo(GameObject/Component):
void Start()
{
Observable.IntervalFrame(30).Subscribe(x => Debug.Log(x)).AddTo(this);
}AddTo calls facilitate automatic Dispose. If you needs special OnCompleted handling in the pipeline, however, use TakeWhile, TakeUntil, TakeUntilDestroy and TakeUntilDisable instead:
Observable.IntervalFrame(30).TakeUntilDisable(this)
.Subscribe(x => Debug.Log(x), () => Debug.Log("completed!"));If you handle events, Repeat is an important but dangerous method. It may cause an infinite loop, so handle with care:
using UniRx;
using UniRx.Triggers;
public class DangerousDragAndDrop : MonoBehaviour
{
void Start()
{
this.gameObject.OnMouseDownAsObservable()
.SelectMany(_ => this.gameObject.UpdateAsObservable())
.TakeUntil(this.gameObject.OnMouseUpAsObservable())
.Select(_ => Input.mousePosition)
.Repeat() // dangerous!!! Repeat cause infinite repeat subscribe at GameObject was destroyed.(If in UnityEditor, Editor is freezed)
.Subscribe(x => Debug.Log(x));
}
}UniRx provides an additional safe Repeat method. RepeatSafe: if contiguous "OnComplete" are called repeat stops. RepeatUntilDestroy(gameObject/component), RepeatUntilDisable(gameObject/component) allows to stop when a target GameObject has been destroyed:
this.gameObject.OnMouseDownAsObservable()
.SelectMany(_ => this.gameObject.UpdateAsObservable())
.TakeUntil(this.gameObject.OnMouseUpAsObservable())
.Select(_ => Input.mousePosition)
.RepeatUntilDestroy(this) // safety way
.Subscribe(x => Debug.Log(x)); UniRx gurantees hot observable(FromEvent/Subject/ReactiveProperty/UnityUI.AsObservable..., there are like event) have unhandled exception durability. What is it? If subscribe in subcribe, does not detach event.
button.OnClickAsObservable().Subscribe(_ =>
{
// If throws error in inner subscribe, but doesn't detached OnClick event.
ObservableWWW.Get("htttp://error/").Subscribe(x =>
{
Debug.Log(x);
});
});This behaviour is sometimes useful such as user event handling.
All class instances provide an ObserveEveryValueChanged method, which watches for changing values every frame:
// watch position change
this.transform.ObserveEveryValueChanged(x => x.position).Subscribe(x => Debug.Log(x));It's very useful. If the watch target is a GameObject, it will stop observing when the target is destroyed, and call OnCompleted. If the watch target is a plain C# Object, OnCompleted will be called on GC.
Use Subject (or AsyncSubject for asynchronious operations):
public class LogCallback
{
public string Condition;
public string StackTrace;
public UnityEngine.LogType LogType;
}
public static class LogHelper
{
static Subject<LogCallback> subject;
public static IObservable<LogCallback> LogCallbackAsObservable()
{
if (subject == null)
{
subject = new Subject<LogCallback>();
// Publish to Subject in callback
UnityEngine.Application.RegisterLogCallback((condition, stackTrace, type) =>
{
subject.OnNext(new LogCallback { Condition = condition, StackTrace = stackTrace, LogType = type });
});
}
return subject.AsObservable();
}
}
// method is separatable and composable
LogHelper.LogCallbackAsObservable()
.Where(x => x.LogType == LogType.Warning)
.Subscribe();
LogHelper.LogCallbackAsObservable()
.Where(x => x.LogType == LogType.Error)
.Subscribe();In Unity5, Application.RegisterLogCallback was removed in favor of Application.logMessageReceived, so we can now simply use Observable.FromEvent.
public static IObservable<LogCallback> LogCallbackAsObservable()
{
return Observable.FromEvent<Application.LogCallback, LogCallback>(
h => (condition, stackTrace, type) => h(new LogCallback { Condition = condition, StackTrace = stackTrace, LogType = type }),
h => Application.logMessageReceived += h, h => Application.logMessageReceived -= h);
}// using UniRx.Diagnostics;
// logger is threadsafe, define per class with name.
static readonly Logger logger = new Logger("Sample11");
// call once at applicationinit
public static void ApplicationInitialize()
{
// Log as Stream, UniRx.Diagnostics.ObservableLogger.Listener is IObservable<LogEntry>
// You can subscribe and output to any place.
ObservableLogger.Listener.LogToUnityDebug();
// for example, filter only Exception and upload to web.
// (make custom sink(IObserver<EventEntry>) is better to use)
ObservableLogger.Listener
.Where(x => x.LogType == LogType.Exception)
.Subscribe(x =>
{
// ObservableWWW.Post("", null).Subscribe();
});
}
// Debug is write only DebugBuild.
logger.Debug("Debug Message");
// or other logging methods
logger.Log("Message");
logger.Exception(new Exception("test exception"));Debug operator in UniRx.Diagnostics namespace helps debugging.
// needs Diagnostics using
using UniRx.Diagnostics;
---
// [DebugDump, Normal]OnSubscribe
// [DebugDump, Normal]OnNext(1)
// [DebugDump, Normal]OnNext(10)
// [DebugDump, Normal]OnCompleted()
{
var subject = new Subject<int>();
subject.Debug("DebugDump, Normal").Subscribe();
subject.OnNext(1);
subject.OnNext(10);
subject.OnCompleted();
}
// [DebugDump, Cancel]OnSubscribe
// [DebugDump, Cancel]OnNext(1)
// [DebugDump, Cancel]OnCancel
{
var subject = new Subject<int>();
var d = subject.Debug("DebugDump, Cancel").Subscribe();
subject.OnNext(1);
d.Dispose();
}
// [DebugDump, Error]OnSubscribe
// [DebugDump, Error]OnNext(1)
// [DebugDump, Error]OnError(System.Exception)
{
var subject = new Subject<int>();
subject.Debug("DebugDump, Error").Subscribe();
subject.OnNext(1);
subject.OnError(new Exception());
}shows sequence element on OnNext, OnError, OnCompleted, OnCancel, OnSubscribe timing to Debug.Log. It enables only #if DEBUG.
// Unity's singleton UiThread Queue Scheduler
Scheduler.MainThreadScheduler
ObserveOnMainThread()/SubscribeOnMainThread()
// Global StartCoroutine runner
MainThreadDispatcher.StartCoroutine(enumerator)
// convert Coroutine to IObservable
Observable.FromCoroutine((observer, token) => enumerator(observer, token));
// convert IObservable to Coroutine
yield return Observable.Range(1, 10).ToYieldInstruction(); // after Unity 5.3, before can use StartAsCoroutine()
// Lifetime hooks
Observable.EveryApplicationPause();
Observable.EveryApplicationFocus();
Observable.OnceApplicationQuit();UniRx provides a few framecount-based time operators:
| Method |
|---|
| EveryUpdate |
| EveryFixedUpdate |
| EveryEndOfFrame |
| EveryGameObjectUpdate |
| EveryLateUpdate |
| ObserveOnMainThread |
| NextFrame |
| IntervalFrame |
| TimerFrame |
| DelayFrame |
| SampleFrame |
| ThrottleFrame |
| ThrottleFirstFrame |
| TimeoutFrame |
| DelayFrameSubscription |
| FrameInterval |
| FrameTimeInterval |
| BatchFrame |
For example, delayed invoke once:
Observable.TimerFrame(100).Subscribe(_ => Debug.Log("after 100 frame"));Every* Method's execution order is
EveryGameObjectUpdate(in MainThreadDispatcher's Execution Order) ->
EveryUpdate ->
EveryLateUpdate ->
EveryEndOfFrame
EveryGameObjectUpdate invoke from same frame if caller is called before MainThreadDispatcher.Update(I recommend MainThreadDispatcher called first than others(ScriptExecutionOrder makes -32000)
EveryLateUpdate, EveryEndOfFrame invoke from same frame.
EveryUpdate, invoke from next frame.
MicroCoroutine is memory efficient and fast coroutine worker. This implemantation is based on Unity blog's 10000 UPDATE() CALLS, avoid managed-unmanaged overhead so gets 10x faster iteration. MicroCoroutine is automaticaly used on Framecount-based time operators and ObserveEveryValueChanged.
If you want to use MicroCoroutine instead of standard unity coroutine, use MainThreadDispatcher.StartUpdateMicroCoroutine or Observable.FromMicroCoroutine.
int counter;
IEnumerator Worker()
{
while(true)
{
counter++;
yield return null;
}
}
void Start()
{
for(var i = 0; i < 10000; i++)
{
// fast, memory efficient
MainThreadDispatcher.StartUpdateMicroCoroutine(Worker());
// slow...
// StartCoroutine(Worker());
}
}MicroCoroutine's limitation, only supports yield return null and update timing is determined start method(StartUpdateMicroCoroutine, StartFixedUpdateMicroCoroutine, StartEndOfFrameMicroCoroutine).
If you combine with other IObservable, you can check completed property like isDone.
IEnumerator MicroCoroutineWithToYieldInstruction()
{
var www = ObservableWWW.Get("http://aaa").ToYieldInstruction();
while (!www.IsDone)
{
yield return null;
}
if (www.HasResult)
{
UnityEngine.Debug.Log(www.Result);
}
}UniRx can handle UnityEvents easily. Use UnityEvent.AsObservable to subscribe to events:
public Button MyButton;
// ---
MyButton.onClick.AsObservable().Subscribe(_ => Debug.Log("clicked"));Treating Events as Observables enables declarative UI programming.
public Toggle MyToggle;
public InputField MyInput;
public Text MyText;
public Slider MySlider;
// On Start, you can write reactive rules for declaretive/reactive ui programming
void Start()
{
// Toggle, Input etc as Observable (OnValueChangedAsObservable is a helper providing isOn value on subscribe)
// SubscribeToInteractable is an Extension Method, same as .interactable = x)
MyToggle.OnValueChangedAsObservable().SubscribeToInteractable(MyButton);
// Input is displayed after a 1 second delay
MyInput.OnValueChangedAsObservable()
.Where(x => x != null)
.Delay(TimeSpan.FromSeconds(1))
.SubscribeToText(MyText); // SubscribeToText is helper for subscribe to text
// Converting for human readability
MySlider.OnValueChangedAsObservable()
.SubscribeToText(MyText, x => Math.Round(x, 2).ToString());
}For more on reactive UI programming please consult Sample12, Sample13 and the ReactiveProperty section below.
Game data often requires notification. Should we use properties and events (callbacks)? That's often too complex. UniRx provides ReactiveProperty, a lightweight property broker.
// Reactive Notification Model
public class Enemy
{
public ReactiveProperty<long> CurrentHp { get; private set; }
public ReactiveProperty<bool> IsDead { get; private set; }
public Enemy(int initialHp)
{
// Declarative Property
CurrentHp = new ReactiveProperty<long>(initialHp);
IsDead = CurrentHp.Select(x => x <= 0).ToReactiveProperty();
}
}
// ---
// onclick, HP decrement
MyButton.OnClickAsObservable().Subscribe(_ => enemy.CurrentHp.Value -= 99);
// subscribe from notification model.
enemy.CurrentHp.SubscribeToText(MyText);
enemy.IsDead.Where(isDead => isDead == true)
.Subscribe(_ =>
{
MyButton.interactable = false;
});You can combine ReactiveProperties, ReactiveCollections and observables returned by UnityEvent.AsObservable. All UI elements are observable.
Generic ReactiveProperties are not serializable or inspecatble in the Unity editor, but UniRx provides specialized subclasses of ReactiveProperty that are. These include classes such as Int/LongReactiveProperty, Float/DoubleReactiveProperty, StringReactiveProperty, BoolReactiveProperty and more (Browse them here: InspectableReactiveProperty.cs). All are fully editable in the inspector. For custom Enum ReactiveProperty, it's easy to write a custom inspectable ReactiveProperty[T].
If you needs [Multiline] or [Range] attach to ReactiveProperty, you can use MultilineReactivePropertyAttribute and RangeReactivePropertyAttribute instead of Multiline and Range.
The provided derived InpsectableReactiveProperties are displayed in the inspector naturally and notify when their value is changed even when it is changed in the inspector.
This functionality is provided by InspectorDisplayDrawer. You can supply your own custom specialized ReactiveProperties by inheriting from it:
public enum Fruit
{
Apple, Grape
}
[Serializable]
public class FruitReactiveProperty : ReactiveProperty<Fruit>
{
public FruitReactiveProperty()
{
}
public FruitReactiveProperty(Fruit initialValue)
:base(initialValue)
{
}
}
[UnityEditor.CustomPropertyDrawer(typeof(FruitReactiveProperty))]
[UnityEditor.CustomPropertyDrawer(typeof(YourSpecializedReactiveProperty2))] // and others...
public class ExtendInspectorDisplayDrawer : InspectorDisplayDrawer
{
}If a ReactiveProperty value is only updated within a stream, you can make it read only by using from ReadOnlyReactiveProperty.
public class Person
{
public ReactiveProperty<string> GivenName { get; private set; }
public ReactiveProperty<string> FamilyName { get; private set; }
public ReadOnlyReactiveProperty<string> FullName { get; private set; }
public Person(string givenName, string familyName)
{
GivenName = new ReactiveProperty<string>(givenName);
FamilyName = new ReactiveProperty<string>(familyName);
// If change the givenName or familyName, notify with fullName!
FullName = GivenName.CombineLatest(FamilyName, (x, y) => x + " " + y).ToReadOnlyReactiveProperty();
}
}UniRx makes it possible to implement the MVP(MVRP) Pattern.
Why should we use MVP instead of MVVM? Unity doesn't provide a UI binding mechanism and creating a binding layer is too complex and loss and affects performance. Still, Views need updating. Presenters are aware of their view's components and can update them. Although there is no real binding, Observables enables subscription to notification, which can act much like the real thing. This pattern is called a Reactive Presenter:
// Presenter for scene(canvas) root.
public class ReactivePresenter : MonoBehaviour
{
// Presenter is aware of its View (binded in the inspector)
public Button MyButton;
public Toggle MyToggle;
// State-Change-Events from Model by ReactiveProperty
Enemy enemy = new Enemy(1000);
void Start()
{
// Rx supplies user events from Views and Models in a reactive manner
MyButton.OnClickAsObservable().Subscribe(_ => enemy.CurrentHp.Value -= 99);
MyToggle.OnValueChangedAsObservable().SubscribeToInteractable(MyButton);
// Models notify Presenters via Rx, and Presenters update their views
enemy.CurrentHp.SubscribeToText(MyText);
enemy.IsDead.Where(isDead => isDead == true)
.Subscribe(_ =>
{
MyToggle.interactable = MyButton.interactable = false;
});
}
}
// The Model. All property notify when their values change
public class Enemy
{
public ReactiveProperty<long> CurrentHp { get; private set; }
public ReactiveProperty<bool> IsDead { get; private set; }
public Enemy(int initialHp)
{
// Declarative Property
CurrentHp = new ReactiveProperty<long>(initialHp);
IsDead = CurrentHp.Select(x => x <= 0).ToReactiveProperty();
}
}A View is a scene, that is a Unity hierarchy. Views are associated with Presenters by the Unity Engine on initialize. The XxxAsObservable methods make creating event signals simple, without any overhead. SubscribeToText and SubscribeToInteractable are simple binding-like helpers. These may be simple tools, but they are very powerful. They feel natural in the Unity environment and provide high performance and a clean architecture.
V -> RP -> M -> RP -> V completely connected in a reactive way. UniRx provides all of the adaptor methods and classes, but other MVVM(or MV*) frameworks can be used instead. UniRx/ReactiveProperty is only simple toolkit.
GUI programming also benefits from ObservableTriggers. ObservableTriggers convert Unity events to Observables, so the MV(R)P pattern can be composed using them. For example, ObservableEventTrigger converts uGUI events to Observable:
var eventTrigger = this.gameObject.AddComponent<ObservableEventTrigger>();
eventTrigger.OnBeginDragAsObservable()
.SelectMany(_ => eventTrigger.OnDragAsObservable(), (start, current) => UniRx.Tuple.Create(start, current))
.TakeUntil(eventTrigger.OnEndDragAsObservable())
.RepeatUntilDestroy(this)
.Subscribe(x => Debug.Log(x));Note: PresenterBase works enough, but too complex.
You can use simpleInitializemethod and call parent to child, it works for most scenario.
So I don't recommend usingPresenterBase, sorry.
ReactiveCommand abstraction of button command with boolean interactable.
public class Player
{
public ReactiveProperty<int> Hp;
public ReactiveCommand Resurrect;
public Player()
{
Hp = new ReactiveProperty<int>(1000);
// If dead, can not execute.
Resurrect = Hp.Select(x => x <= 0).ToReactiveCommand();
// Execute when clicked
Resurrect.Subscribe(_ =>
{
Hp.Value = 1000;
});
}
}
public class Presenter : MonoBehaviour
{
public Button resurrectButton;
Player player;
void Start()
{
player = new Player();
// If Hp <= 0, can't press button.
player.Resurrect.BindTo(resurrectButton);
}
} AsyncReactiveCommand is a variation of ReactiveCommand that CanExecute(in many cases bind to button's interactable) is changed to false until asynchronous execution was finished.
public class Presenter : MonoBehaviour
{
public UnityEngine.UI.Button button;
void Start()
{
var command = new AsyncReactiveCommand();
command.Subscribe(_ =>
{
// heavy, heavy, heavy method....
return Observable.Timer(TimeSpan.FromSeconds(3)).AsUnitObservable();
});
// after clicked, button shows disable for 3 seconds
command.BindTo(button);
// Note:shortcut extension, bind aync onclick directly
button.BindToOnClick(_ =>
{
return Observable.Timer(TimeSpan.FromSeconds(3)).AsUnitObservable();
});
}
} AsyncReactiveCommand has three constructor.
()- CanExecute is changed to false until async execution finished(IObservable<bool> canExecuteSource)- Mixed with empty, CanExecute becomes true when canExecuteSource send to true and does not executing(IReactiveProperty<bool> sharedCanExecute)- share execution status between multiple AsyncReactiveCommands, if one AsyncReactiveCommand is executing, other AsyncReactiveCommands(with same sharedCanExecute property) becomes CanExecute false until async execution finished
public class Presenter : MonoBehaviour
{
public UnityEngine.UI.Button button1;
public UnityEngine.UI.Button button2;
void Start()
{
// share canExecute status.
// when clicked button1, button1 and button2 was disabled for 3 seconds.
var sharedCanExecute = new ReactiveProperty<bool>();
button1.BindToOnClick(sharedCanExecute, _ =>
{
return Observable.Timer(TimeSpan.FromSeconds(3)).AsUnitObservable();
});
button2.BindToOnClick(sharedCanExecute, _ =>
{
return Observable.Timer(TimeSpan.FromSeconds(3)).AsUnitObservable();
});
}
}MessageBroker is Rx based in-memory pubsub system filtered by type.
public class TestArgs
{
public int Value { get; set; }
}
---
// Subscribe message on global-scope.
MessageBroker.Default.Receive<TestArgs>().Subscribe(x => UnityEngine.Debug.Log(x));
// Publish message
MessageBroker.Default.Publish(new TestArgs { Value = 1000 });AsyncMessageBroker is variation of MessageBroker, can await Publish call.
AsyncMessageBroker.Default.Subscribe<TestArgs>(x =>
{
// show after 3 seconds.
return Observable.Timer(TimeSpan.FromSeconds(3))
.ForEachAsync(_ =>
{
UnityEngine.Debug.Log(x);
});
});
AsyncMessageBroker.Default.PublishAsync(new TestArgs { Value = 3000 })
.Subscribe(_ =>
{
UnityEngine.Debug.Log("called all subscriber completed");
});UniRx.Toolkit includes serveral Rx-ish tools. Currently includes ObjectPool and AsyncObjectPool. It can Rent, Return and PreloadAsync for fill pool before rent operation.
// sample class
public class Foobar : MonoBehaviour
{
public IObservable<Unit> ActionAsync()
{
// heavy, heavy, action...
return Observable.Timer(TimeSpan.FromSeconds(3)).AsUnitObservable();
}
}
public class FoobarPool : ObjectPool<Foobar>
{
readonly Foobar prefab;
readonly Transform hierarchyParent;
public FoobarPool(Foobar prefab, Transform hierarchyParent)
{
this.prefab = prefab;
this.hierarchyParent = hierarchyParent;
}
protected override Foobar CreateInstance()
{
var foobar = GameObject.Instantiate<Foobar>(prefab);
foobar.transform.SetParent(hierarchyParent);
return foobar;
}
// You can overload OnBeforeRent, OnBeforeReturn, OnClear for customize action.
// In default, OnBeforeRent = SetActive(true), OnBeforeReturn = SetActive(false)
// protected override void OnBeforeRent(Foobar instance)
// protected override void OnBeforeReturn(Foobar instance)
// protected override void OnClear(Foobar instance)
}
public class Presenter : MonoBehaviour
{
FoobarPool pool = null;
public Foobar prefab;
public Button rentButton;
void Start()
{
pool = new FoobarPool(prefab, this.transform);
rentButton.OnClickAsObservable().Subscribe(_ =>
{
var foobar = pool.Rent();
foobar.ActionAsync().Subscribe(__ =>
{
// if action completed, return to pool
pool.Return(foobar);
});
});
}
}For Visual Studio 2015 users, a custom analyzer, UniRxAnalyzer, is provided. It can, for example, detect when streams aren't subscribed to.
ObservableWWW doesn't fire until it's subscribed to, so the analyzer warns about incorrect usage. It can be downloaded from NuGet.
- Install-Package UniRxAnalyzer
Please submit new analyzer ideas on GitHub Issues!
See UniRx/Examples
The samples demonstrate how to do resource management (Sample09_EventHandling), what is the MainThreadDispatcher, among other things.
Some interfaces, such as UniRx.IObservable<T> and System.IObservable<T>, cause conflicts when submitting to the Windows Store App.
Therefore, when using NETFX_CORE, please refrain from using such constructs as UniRx.IObservable<T> and refer to the UniRx components by their short name, without adding the namespace. This solves the conflicts.
If you want to pre-build UniRx, you can build own dll. clone project and open UniRx.sln, you can see UniRx, it is fullset separated project of UniRx. You should define compile symbol like UNITY;UNITY_5_4_OR_NEWER;UNITY_5_4_0;UNITY_5_4;UNITY_5; + UNITY_EDITOR, UNITY_IPHONE or other platform symbol. We can not provides pre-build binary to release page, asset store because compile symbol is different each other.
After Unity 2019.3.4f1, Unity 2020.1a21, that support path query parameter of git package. You can add https://github.com/neuecc/UniRx.git?path=Assets/Plugins/UniRx/Scripts to Package Manager
or add "com.neuecc.unirx": "https://github.com/neuecc/UniRx.git?path=Assets/Plugins/UniRx/Scripts" to Packages/manifest.json.
UniRx API documents.
The home of ReactiveX. Introduction, All operators are illustrated with graphical marble diagrams, there makes easy to understand. And UniRx is official ReactiveX Languages.
A great online tutorial and eBook.
Many videos, slides and documents for Rx.NET.
Intro slide by @torisoup
Intro slide and sample game by @Xerios
How to integrate with PlayFab API
Support thread on the Unity forum. Ask me any question - http://forum.unity3d.com/threads/248535-UniRx-Reactive-Extensions-for-Unity
Become a backer, Sponsored, one time donation are welcome, we're using Open Collective - UniRx
We welcome any contributions, be they bug reports, requests or pull request.
Please consult and submit your reports or requests on GitHub issues.
Source code is available in Assets/Plugins/UniRx/Scripts.
LINQ to GameObject is a group of GameObject extensions for Unity that allows traversing the hierarchy and appending GameObject to it like LINQ to XML. It's free and opensource on GitHub.
Yoshifumi Kawai(a.k.a. neuecc) is a software developer in Japan.
Currently founded consulting company New World, Inc.
He is awarding Microsoft MVP for Visual C# since 2011.
Blog: https://medium.com/@neuecc (English)
Blog: http://neue.cc/ (Japanese)
Twitter: https://twitter.com/neuecc (Japanese)
This library is under the MIT License.