com.spotify.mobius.MobiusLoop Maven / Gradle / Ivy
/*
* -\-\-
* Mobius
* --
* Copyright (c) 2017-2018 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;
import static com.spotify.mobius.internal_util.Preconditions.checkNotNull;
import com.spotify.mobius.disposables.Disposable;
import com.spotify.mobius.functions.Consumer;
import com.spotify.mobius.functions.Producer;
import com.spotify.mobius.runners.WorkRunner;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* This is the main loop for Mobius.
*
* It hooks up all the different parts of the main Mobius loop, and dispatches messages
* internally on the appropriate executors.
*/
public class MobiusLoop implements Disposable {
@Nonnull private final MessageDispatcher eventDispatcher;
@Nonnull private final MessageDispatcher effectDispatcher;
@Nonnull private final EventProcessor eventProcessor;
@Nonnull private final Connection effectConsumer;
@Nonnull private final QueuingConnection eventSourceModelConsumer;
@Nonnull private final List> modelObservers = new CopyOnWriteArrayList<>();
@Nullable private volatile M mostRecentModel;
private volatile boolean disposed;
static MobiusLoop create(
Update update,
M startModel,
Iterable startEffects,
Connectable effectHandler,
Connectable eventSource,
WorkRunner eventRunner,
WorkRunner effectRunner) {
return new MobiusLoop<>(
new EventProcessor.Factory<>(
MobiusStore.create(checkNotNull(update), checkNotNull(startModel))),
checkNotNull(startModel),
checkNotNull(startEffects),
checkNotNull(effectHandler),
checkNotNull(eventSource),
checkNotNull(eventRunner),
checkNotNull(effectRunner));
}
private MobiusLoop(
EventProcessor.Factory eventProcessorFactory,
M startModel,
Iterable startEffects,
Connectable effectHandler,
Connectable eventSource,
WorkRunner eventRunner,
WorkRunner effectRunner) {
Consumer onEventReceived =
new Consumer() {
@Override
public void accept(E event) {
eventProcessor.update(event);
}
};
Consumer onEffectReceived =
new Consumer() {
@Override
public void accept(F effect) {
try {
effectConsumer.accept(effect);
} catch (Throwable t) {
throw new ConnectionException(effect, t);
}
}
};
eventSourceModelConsumer = new QueuingConnection<>();
Consumer onModelChanged =
new Consumer() {
@Override
public void accept(M model) {
mostRecentModel = model;
eventSourceModelConsumer.accept(model);
for (Consumer observer : modelObservers) {
observer.accept(model);
}
}
};
this.eventDispatcher = new MessageDispatcher<>(eventRunner, onEventReceived);
this.effectDispatcher = new MessageDispatcher<>(effectRunner, onEffectReceived);
this.eventProcessor = eventProcessorFactory.create(effectDispatcher, onModelChanged);
Consumer eventConsumer =
new Consumer() {
@Override
public void accept(E event) {
dispatchEvent(event);
}
};
this.effectConsumer = effectHandler.connect(eventConsumer);
mostRecentModel = startModel;
onModelChanged.accept(startModel);
for (F effect : startEffects) {
effectDispatcher.accept(effect);
}
this.eventSourceModelConsumer.setDelegate(eventSource.connect(eventConsumer));
}
public void dispatchEvent(E event) {
if (disposed)
throw new IllegalStateException(
String.format(
"This loop has already been disposed. You cannot dispatch events after "
+ "disposal - event received: %s=%s, currentModel: %s",
event.getClass().getName(), event, mostRecentModel));
eventDispatcher.accept(checkNotNull(event));
}
@Nullable
public M getMostRecentModel() {
return mostRecentModel;
}
/**
* Add an observer of model changes to this loop. If {@link #getMostRecentModel()} is non-null,
* the observer will immediately be notified of the most recent model. The observer will be
* notified of future changes to the model until the loop or the returned {@link Disposable} is
* disposed.
*
* @param observer a non-null observer of model changes
* @return a {@link Disposable} that can be used to stop further notifications to the observer
* @throws NullPointerException if the observer is null
* @throws IllegalStateException if the loop has been disposed
*/
public Disposable observe(final Consumer observer) {
if (disposed)
throw new IllegalStateException(
"This loop has already been disposed. You cannot observe a disposed loop");
modelObservers.add(checkNotNull(observer));
final M currentModel = mostRecentModel;
if (currentModel != null) {
// Start by emitting the most recently received model.
observer.accept(currentModel);
}
return new Disposable() {
@Override
public void dispose() {
modelObservers.remove(observer);
}
};
}
@Override
public synchronized void dispose() {
// Remove model observers so that they receive no further model changes.
modelObservers.clear();
// Disable the event and effect dispatchers. This will cause any further
// events or effects posted to the dispatchers to be ignored and logged.
eventDispatcher.disable();
effectDispatcher.disable();
// Stop the event source and effect handler.
eventSourceModelConsumer.dispose();
effectConsumer.dispose();
// Finally clean up the dispatchers that now no longer are needed.
eventDispatcher.dispose();
effectDispatcher.dispose();
disposed = true;
}
/**
* Defines a fluent API for configuring a {@link MobiusLoop}. Implementations must be immutable,
* making them safe to share between threads.
*
* @param the model type
* @param the event type
* @param the effect type
*/
public interface Builder extends Factory {
/**
* @deprecated Pass your initial effects to {@link #startFrom(Object, Set)} instead.
* @return a new {@link Builder} with the supplied {@link Init}, and the same values as the
* current one for the other fields.
*/
@Nonnull
@Deprecated
Builder init(Init init);
/**
* @return a new {@link Builder} with the supplied {@link EventSource}, and the same values as
* the current one for the other fields. NOTE: Invoking this method will replace the current
* {@link EventSource} with the supplied one. If you want to pass multiple event sources,
* please use {@link #eventSources(EventSource, EventSource[])}.
*/
@Nonnull
Builder eventSource(EventSource eventSource);
/**
* @return a new {@link Builder} with an {@link EventSource} that merges the supplied event
* sources, and the same values as the current one for the other fields.
*/
@Nonnull
Builder eventSources(EventSource eventSource, EventSource... eventSources);
/**
* @return a new {@link Builder} with the supplied {@link Connectable}, and the same values
* as the current one for the other fields. NOTE: Invoking this method will replace the
* current event source with the supplied one. If a loop has a {@link Connectable} as
* its event source, it will connect to it and will invoke the {@link Connection} accept
* method every time the model changes. This allows us to conditionally subscribe to
* different sources based on the current state. If you provide a regular {@link
* EventSource}, it will be wrapped in a {@link Connectable} and that implementation will
* subscribe to that event source only once when the loop is initialized.
*/
@Nonnull
Builder eventSource(Connectable eventSource);
/**
* @return a new {@link Builder} with the supplied logger, and the same values as the current
* one for the other fields.
*/
@Nonnull
Builder logger(Logger logger);
/**
* @return a new {@link Builder} with the supplied event runner, and the same values as the
* current one for the other fields.
*/
@Nonnull
Builder eventRunner(Producer eventRunner);
/**
* @return a new {@link Builder} with the supplied effect runner, and the same values as the
* current one for the other fields.
*/
@Nonnull
Builder effectRunner(Producer effectRunner);
}
public interface Factory {
/**
* Start a {@link MobiusLoop} using this factory.
*
* @param startModel the model that the loop should start from
* @return the started {@link MobiusLoop}
*/
MobiusLoop startFrom(M startModel);
/**
* Start a {@link MobiusLoop} using this factory.
*
* @param startModel the model that the loop should start from
* @param startEffects the effects that the loop should start with
* @return the started {@link MobiusLoop}
*/
MobiusLoop startFrom(M startModel, Set startEffects);
}
/**
* Defines a controller that can be used to start and stop MobiusLoops.
*
* If a loop is stopped and then started again, the new loop will continue from where the last
* one left off.
*/
public interface Controller {
/**
* Indicates whether this controller is running.
*
* @return true if the controller is running
*/
boolean isRunning();
/**
* Connect a view to this controller.
*
* Must be called before {@link #start()}.
*
*
The {@link Connectable} will be given an event consumer, which the view should use to send
* events to the MobiusLoop. The view should also return a {@link Connection} that accepts
* models and renders them. Disposing the connection should make the view stop emitting events.
*
*
The view Connectable is guaranteed to only be connected once, so you don't have to check
* for multiple connections or throw {@link ConnectionLimitExceededException}.
*
* @throws IllegalStateException if the loop is running or if the controller already is
* connected
*/
void connect(Connectable view);
/**
* Disconnect UI from this controller.
*
* @throws IllegalStateException if the loop is running or if there isn't anything to disconnect
*/
void disconnect();
/**
* Start a MobiusLoop from the current model.
*
* @throws IllegalStateException if the loop already is running or no view has been connected
*/
void start();
/**
* Stop the currently running MobiusLoop.
*
* When the loop is stopped, the last model of the loop will be remembered and used as the
* first model the next time the loop is started.
*
* @throws IllegalStateException if the loop isn't running
*/
void stop();
/**
* Replace which model the controller should start from.
*
* @param model the model with the state the controller should start from
* @throws IllegalStateException if the loop is running
*/
void replaceModel(M model);
/**
* Get the current model of the loop that this controller is running, or the most recent model
* if it's not running.
*
* @return a model with the state of the controller
*/
@Nonnull
M getModel();
}
/** Interface for logging init and update calls. */
public interface Logger {
/**
* Called right before the {@link Init#init(Object)} function is called.
*
* This method mustn't block, as it'll hinder the loop from running. It will be called on the
* same thread as the init function.
*
* @param model the model that will be passed to the init function
*/
void beforeInit(M model);
/**
* Called right after the {@link Init#init(Object)} function is called.
*
*
This method mustn't block, as it'll hinder the loop from running. It will be called on the
* same thread as the init function.
*
* @param model the model that was passed to init
* @param result the {@link First} that init returned
*/
void afterInit(M model, First result);
/**
* Called if the {@link Init#init(Object)} invocation throws an exception. This is a programmer
* error; Mobius is in an undefined state if it happens.
*
* @param model the model object that led to the exception
* @param exception the thrown exception
*/
void exceptionDuringInit(M model, Throwable exception);
/**
* Called right before the {@link Update#update(Object, Object)} function is called.
*
* This method mustn't block, as it'll hinder the loop from running. It will be called on the
* same thread as the update function.
*
* @param model the model that will be passed to the update function
* @param event the event that will be passed to the update function
*/
void beforeUpdate(M model, E event);
/**
* Called right after the {@link Update#update(Object, Object)} function is called.
*
*
This method mustn't block, as it'll hinder the loop from running. It will be called on the
* same thread as the update function.
*
* @param model the model that was passed to update
* @param event the event that was passed to update
* @param result the {@link Next} that update returned
*/
void afterUpdate(M model, E event, Next result);
/**
* Called if the {@link Update#update(Object, Object)} invocation throws an exception. This is a
* programmer error; Mobius is in an undefined state if it happens.
*
* @param model the model object that led to the exception
* @param exception the thrown exception
*/
void exceptionDuringUpdate(M model, E event, Throwable exception);
}
}