All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.spotify.mobius.MobiusLoop Maven / Gradle / Ivy

The newest version!
/*
 * -\-\-
 * 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;

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 Loop { @Nonnull private final DiscardAfterDisposeWrapper onEventReceived; @Nonnull private final DiscardAfterDisposeWrapper onEffectReceived; @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 enum RunState { // the loop is running normally RUNNING, // the loop is in the process of shutting down DISPOSING, // the loop has been shut down - any further attempts at interacting with it should be // considered to be errors. DISPOSED } private volatile RunState runState = RunState.RUNNING; 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) { onEventReceived = DiscardAfterDisposeWrapper.wrapConsumer( new Consumer() { @Override public void accept(E event) { eventProcessor.update(event); } }); onEffectReceived = DiscardAfterDisposeWrapper.wrapConsumer( 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)); } @Override public void dispatchEvent(E event) { if (runState == RunState.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)); } if (runState == RunState.DISPOSING) { // ignore events received while disposing to avoid races during shutdown return; } try { eventDispatcher.accept(checkNotNull(event)); } catch (RuntimeException e) { throw new IllegalStateException("Exception processing event: " + event, e); } } @Override @Nullable public M getMostRecentModel() { return mostRecentModel; } @Override public Disposable observe(final Consumer observer) { if (runState == RunState.DISPOSED) { throw new IllegalStateException( "This loop has already been disposed. You cannot observe a disposed loop"); } if (runState == RunState.DISPOSING) { // ignore observation requests on a disposing loop return () -> {}; } FireAtLeastOnceObserver wrapped = new FireAtLeastOnceObserver<>(observer); modelObservers.add(wrapped); final M currentModel = mostRecentModel; if (currentModel != null) { // Start by emitting the most recently received model, if one hasn't already been emitted // because of a racing model update wrapped.acceptIfFirst(currentModel); } return new Disposable() { @Override public void dispose() { modelObservers.remove(wrapped); } }; } @Override public synchronized void dispose() { if (runState == RunState.DISPOSED) { return; } runState = RunState.DISPOSING; // Remove model observers so that they receive no further model changes. modelObservers.clear(); // Disable the event and effect handling. This will cause any further // events or effects that are received by the loop to be ignored. onEventReceived.dispose(); onEffectReceived.dispose(); // 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(); runState = RunState.DISPOSED; } /** * 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 { /** * Returns a new {@link Builder} with the supplied {@link Init}, and the same values as the * current one for the other fields. * * @deprecated Pass your initial effects to {@link #startFrom(Object, Set)} instead. */ @Nonnull @Deprecated Builder init(Init init); /** * Returns 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); /** * Returns 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); /** * Returns 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); /** * Returns 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); /** * Returns 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); /** * Returns 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} * @throws IllegalStateException if the loop has been configured with an {@link Init}, since * that would conflict with the initial effects passed in. */ 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); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy