
com.spotify.mobius.rx.RxMobius Maven / Gradle / Ivy
Show all versions of mobius-rx Show documentation
/*
* -\-\-
* Mobius
* --
* Copyright (c) 2017-2020 Spotify AB
* --
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* -/-/-
*/
package com.spotify.mobius.rx;
import static com.spotify.mobius.internal_util.Preconditions.checkNotNull;
import com.spotify.mobius.ConnectionException;
import com.spotify.mobius.Mobius;
import com.spotify.mobius.MobiusLoop;
import com.spotify.mobius.Update;
import com.spotify.mobius.functions.Function;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import rx.Observable;
import rx.Observable.Transformer;
import rx.Scheduler;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.plugins.RxJavaHooks;
/** Factory methods for wrapping Mobius core classes in observable transformers. */
public final class RxMobius {
private RxMobius() {
// prevent instantiation
}
/**
* Create an observable transformer that starts from a given model.
*
* Every time the resulting observable is subscribed to, a new MobiusLoop will be started from
* the given model.
*
* @param loopFactory gets invoked for each subscription, to create a new MobiusLoop instance
* @param startModel the starting point for each new loop
* @param the model type
* @param the event type
* @param the effect type
* @return a transformer from event to model that you can connect to your UI
*/
public static Observable.Transformer loopFrom(
final MobiusLoop.Factory loopFactory, final M startModel) {
return new RxMobiusLoop<>(loopFactory, startModel, null);
}
/**
* Create an observable transformer that starts from a given model and given effects.
*
* Every time the resulting observable is subscribed to, a new MobiusLoop will be started from
* the given model and the given effects.
*
* @param loopFactory gets invoked for each subscription, to create a new MobiusLoop instance
* @param startModel the starting point for each new loop
* @param startEffects the starting effects for each new loop
* @param the model type
* @param the event type
* @param the effect type
* @return a transformer from event to model that you can connect to your UI
*/
public static Observable.Transformer loopFrom(
final MobiusLoop.Factory loopFactory,
final M startModel,
final Set startEffects) {
return new RxMobiusLoop<>(loopFactory, startModel, startEffects);
}
/**
* Create a {@link MobiusLoop.Builder} to help you configure a MobiusLoop before starting it.
*
* Once done configuring the loop you can start the loop using {@link
* MobiusLoop.Factory#startFrom(Object)}.
*
* @param update the {@link Update} function of the loop
* @param effectHandler the {@link Transformer} effect handler of the loop
* @param the model type
* @param the event type
* @param the effect type
* @return a {@link MobiusLoop.Builder} instance that you can further configure before starting
* the loop
*/
public static MobiusLoop.Builder loop(
Update update, Transformer effectHandler) {
return Mobius.loop(update, RxConnectables.fromTransformer(effectHandler));
}
/**
* Create an {@link SubtypeEffectHandlerBuilder} for handling effects based on their type.
*
* @param the effect type
* @param the event type
*/
public static SubtypeEffectHandlerBuilder subtypeEffectHandler() {
return new SubtypeEffectHandlerBuilder<>();
}
/**
* Builder for a type-routing effect handler.
*
* Register handlers for different subtypes of F using the add(...) methods, and call {@link
* #build()} to create an instance of the effect handler. You can then create a loop with the
* router as the effect handler using {@link #loop(Update, Observable.Transformer)}.
*
*
The handler will look at the type of each incoming effect object and try to find a
* registered handler for that particular subtype of F. If a handler is found, it will be given
* the effect object, otherwise an exception will be thrown.
*
*
All the classes that the effect router know about must have a common type F. Note that
* instances of the builder are mutable and not thread-safe.
*/
public static class SubtypeEffectHandlerBuilder {
private final Map, Transformer> effectPerformerMap = new HashMap<>();
private Func1, Action1> onErrorFunction =
new DefaultOnError();
private SubtypeEffectHandlerBuilder() {}
/**
* Add an {@link Observable.Transformer} for handling effects of a given type. The handler will
* receive all effect objects that extend the given class.
*
* Adding handlers for two effect classes where one is a super-class of the other is
* considered a collision and is not allowed. Registering the same class twice is also
* considered a collision.
*
* @param effectClass the effect class to handle
* @param effectHandler the effect handler for the given effect class
* @param the effect class as a type parameter
* @return this builder
* @throws IllegalArgumentException if there is a handler collision
* @deprecated use {@link #addTransformer(Class, Transformer)}
*/
@Deprecated
public SubtypeEffectHandlerBuilder add(
final Class effectClass, final Transformer effectHandler) {
return addTransformer(effectClass, effectHandler);
}
/**
* Add an {@link Action0} for handling effects of a given type. The action will be invoked once
* for every received effect object that extends the given class.
*
* Adding handlers for two effect classes where one is a super-class of the other is
* considered a collision and is not allowed. Registering the same class twice is also
* considered a collision.
*
* @param effectClass the class to handle
* @param action the action that should be invoked for the effect
* @param the effect class as a type parameter
* @return this builder
* @throws IllegalArgumentException if there is a handler collision
* @deprecated use {@link #addAction(Class, Action0)}
*/
@Deprecated
public SubtypeEffectHandlerBuilder add(
final Class effectClass, final Action0 action) {
return addAction(effectClass, action);
}
/**
* Add an {@link Action0} for handling effects of a given type. The action will be invoked once
* for every received effect object that extends the given class.
*
* Adding handlers for two effect classes where one is a super-class of the other is
* considered a collision and is not allowed. Registering the same class twice is also
* considered a collision.
*
* @param effectClass the class to handle
* @param action the action that should be invoked for the effect
* @param scheduler the scheduler that should be used to invoke the action
* @param the effect class as a type parameter
* @return this builder
* @throws IllegalArgumentException if there is a handler collision
* @deprecated use {@link #addAction(Class, Action0, Scheduler)}
*/
@Deprecated
public SubtypeEffectHandlerBuilder add(
final Class effectClass, final Action0 action, Scheduler scheduler) {
return addAction(effectClass, action, scheduler);
}
/**
* Add an {@link Action1} for handling effects of a given type. The action will be invoked once
* for every received effect object that extends the given class.
*
* Adding handlers for two effect classes where one is a super-class of the other is
* considered a collision and is not allowed. Registering the same class twice is also
* considered a collision.
*
* @param effectClass the class to handle
* @param action the action that should be invoked for the effect
* @param the effect class as a type parameter
* @return this builder
* @throws IllegalArgumentException if there is a handler collision
* @deprecated use {@link #addConsumer(Class, Action1)}
*/
@Deprecated
public SubtypeEffectHandlerBuilder add(
final Class effectClass, final Action1 action) {
return addConsumer(effectClass, action);
}
/**
* Add an {@link Action1} for handling effects of a given type. The action will be invoked once
* for every received effect object that extends the given class.
*
* Adding handlers for two effect classes where one is a super-class of the other is
* considered a collision and is not allowed. Registering the same class twice is also
* considered a collision.
*
* @param effectClass the class to handle
* @param action the action that should be invoked for the effect
* @param scheduler the scheduler that should be used to invoke the action
* @param the effect class as a type parameter
* @return this builder
* @throws IllegalArgumentException if there is a handler collision
* @deprecated use {@link #addConsumer(Class, Action1, Scheduler)}
*/
@Deprecated
public SubtypeEffectHandlerBuilder add(
final Class effectClass, final Action1 action, Scheduler scheduler) {
return addConsumer(effectClass, action, scheduler);
}
/**
* Add a {@link Func1} for handling effects of a given type. The function will be invoked once
* for every received effect object that extends the given class. The returned event will be
* forwarded to the Mobius loop.
*
* Adding handlers for two effect classes where one is a super-class of the other is
* considered a collision and is not allowed. Registering the same class twice is also
* considered a collision.
*
* @param effectClass the class to handle
* @param function the function that should be invoked for the effect
* @param the effect class as a type parameter
* @return this builder
* @throws IllegalArgumentException if there is a handler collision
*/
public SubtypeEffectHandlerBuilder addFunction(
final Class effectClass, final Function function) {
//noinspection ResultOfMethodCallIgnored
checkNotNull(effectClass);
//noinspection ResultOfMethodCallIgnored
checkNotNull(function);
return addTransformer(effectClass, Transformers.fromFunction(function));
}
/**
* Add a {@link Func1} for handling effects of a given type. The function will be invoked once
* for every received effect object that extends the given class. The returned event will be
* forwarded to the Mobius loop.
*
* Adding handlers for two effect classes where one is a super-class of the other is
* considered a collision and is not allowed. Registering the same class twice is also
* considered a collision.
*
* @param effectClass the class to handle
* @param function the function that should be invoked for the effect
* @param scheduler the scheduler that should be used when invoking the function
* @param the effect class as a type parameter
* @return this builder
* @throws IllegalArgumentException if there is a handler collision
*/
public SubtypeEffectHandlerBuilder addFunction(
final Class effectClass, final Function function, Scheduler scheduler) {
//noinspection ResultOfMethodCallIgnored
checkNotNull(effectClass);
//noinspection ResultOfMethodCallIgnored
checkNotNull(function);
return addTransformer(effectClass, Transformers.fromFunction(function, scheduler));
}
/**
* Add an {@link Observable.Transformer} for handling effects of a given type. The handler will
* receive all effect objects that extend the given class.
*
* Adding handlers for two effect classes where one is a super-class of the other is
* considered a collision and is not allowed. Registering the same class twice is also
* considered a collision.
*
* @param effectClass the effect class to handle
* @param effectHandler the effect handler for the given effect class
* @param the effect class as a type parameter
* @return this builder
* @throws IllegalArgumentException if there is a handler collision
*/
public SubtypeEffectHandlerBuilder addTransformer(
final Class effectClass, final Transformer effectHandler) {
//noinspection ResultOfMethodCallIgnored
checkNotNull(effectClass);
//noinspection ResultOfMethodCallIgnored
checkNotNull(effectHandler);
for (Class> cls : effectPerformerMap.keySet()) {
if (cls.isAssignableFrom(effectClass) || effectClass.isAssignableFrom(cls)) {
throw new IllegalArgumentException(
"Effect classes may not be assignable to each other, collision found: "
+ effectClass.getSimpleName()
+ " <-> "
+ cls.getSimpleName());
}
}
effectPerformerMap.put(
effectClass,
new Transformer() {
@Override
public Observable call(Observable effects) {
return effects
.ofType(effectClass)
.compose(effectHandler)
.doOnError(onErrorFunction.call(effectHandler));
}
});
return this;
}
/**
* Add an {@link Action0} for handling effects of a given type. The action will be invoked once
* for every received effect object that extends the given class.
*
* Adding handlers for two effect classes where one is a super-class of the other is
* considered a collision and is not allowed. Registering the same class twice is also
* considered a collision.
*
* @param effectClass the class to handle
* @param action the action that should be invoked for the effect
* @param the effect class as a type parameter
* @return this builder
* @throws IllegalArgumentException if there is a handler collision
*/
public SubtypeEffectHandlerBuilder addAction(
final Class effectClass, final Action0 action) {
//noinspection ResultOfMethodCallIgnored
checkNotNull(effectClass);
//noinspection ResultOfMethodCallIgnored
checkNotNull(action);
return addTransformer(effectClass, Transformers.fromAction(action));
}
/**
* Add an {@link Action0} for handling effects of a given type. The action will be invoked once
* for every received effect object that extends the given class.
*
* Adding handlers for two effect classes where one is a super-class of the other is
* considered a collision and is not allowed. Registering the same class twice is also
* considered a collision.
*
* @param effectClass the class to handle
* @param action the action that should be invoked for the effect
* @param scheduler the scheduler that should be used to invoke the action
* @param the effect class as a type parameter
* @return this builder
* @throws IllegalArgumentException if there is a handler collision
*/
public SubtypeEffectHandlerBuilder addAction(
final Class effectClass, final Action0 action, Scheduler scheduler) {
//noinspection ResultOfMethodCallIgnored
checkNotNull(effectClass);
//noinspection ResultOfMethodCallIgnored
checkNotNull(action);
return addTransformer(effectClass, Transformers.fromAction(action, scheduler));
}
/**
* Add an {@link Action1} for handling effects of a given type. The action will be invoked once
* for every received effect object that extends the given class.
*
* Adding handlers for two effect classes where one is a super-class of the other is
* considered a collision and is not allowed. Registering the same class twice is also
* considered a collision.
*
* @param effectClass the class to handle
* @param consumer the consumer that should be invoked for the effect
* @param the effect class as a type parameter
* @return this builder
* @throws IllegalArgumentException if there is a handler collision
*/
public SubtypeEffectHandlerBuilder addConsumer(
final Class effectClass, final Action1 consumer) {
//noinspection ResultOfMethodCallIgnored
checkNotNull(effectClass);
//noinspection ResultOfMethodCallIgnored
checkNotNull(consumer);
return addTransformer(effectClass, Transformers.fromConsumer(consumer));
}
/**
* Add an {@link Action1} for handling effects of a given type. The action will be invoked once
* for every received effect object that extends the given class.
*
* Adding handlers for two effect classes where one is a super-class of the other is
* considered a collision and is not allowed. Registering the same class twice is also
* considered a collision.
*
* @param effectClass the class to handle
* @param consumer the consumer that should be invoked for the effect
* @param scheduler the scheduler that should be used to invoke the action
* @param the effect class as a type parameter
* @return this builder
* @throws IllegalArgumentException if there is a handler collision
*/
public SubtypeEffectHandlerBuilder addConsumer(
final Class effectClass, final Action1 consumer, Scheduler scheduler) {
//noinspection ResultOfMethodCallIgnored
checkNotNull(effectClass);
//noinspection ResultOfMethodCallIgnored
checkNotNull(consumer);
return addTransformer(effectClass, Transformers.fromConsumer(consumer, scheduler));
}
/**
* Optionally set a shared error handler in case a handler throws an uncaught exception.
*
* The default is to use {@link RxJavaHooks#onError(Throwable)}. Note that any exception
* thrown by a handler is a fatal error and this method doesn't enable safe error handling, only
* configurable crash reporting.
*
* @param onErrorFunction a function that gets told which sub-transformer failed and should
* return an appropriate handler for exceptions thrown.
*/
public SubtypeEffectHandlerBuilder withFatalErrorHandler(
Func1, Action1> onErrorFunction) {
this.onErrorFunction = checkNotNull(onErrorFunction);
return this;
}
public Observable.Transformer build() {
return new MobiusEffectRouter<>(effectPerformerMap.keySet(), effectPerformerMap.values());
}
private class DefaultOnError implements Func1, Action1> {
@Override
public Action1 call(final Transformer extends F, E> effectHandler) {
return new Action1() {
@Override
public void call(Throwable throwable) {
RxJavaHooks.onError(
new ConnectionException(effectHandler.getClass().toString(), throwable));
}
};
}
}
}
}