com.spotify.mobius.extras.Connectables Maven / Gradle / Ivy
/*
* -\-\-
* 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.extras;
import com.spotify.mobius.Connectable;
import com.spotify.mobius.Connection;
import com.spotify.mobius.extras.connections.ContramapConnection;
import com.spotify.mobius.extras.connections.DisconnectOnNullDimapConnection;
import com.spotify.mobius.extras.connections.MergeConnectablesConnection;
import com.spotify.mobius.functions.Consumer;
import com.spotify.mobius.functions.Function;
import com.spotify.mobius.internal_util.Preconditions;
import java.util.ArrayList;
import java.util.Collections;
import javax.annotation.Nonnull;
/** Contains utility functions for working with {@link Connectables}. */
public final class Connectables {
private Connectables() {
// prevent instantiation
}
/**
* Convert a {@code Connectable} to a {@code Connectable} by applying the supplied
* function from J to I for each J received, before passing it on to a {@code Connection}
* received from the underlying {@code Connectable}. This makes {@link Connectable} a contravariant
* functor in functional programming terms.
*
* The returned {@link Connectable} doesn't enforce a connection limit, but of course the
* connection limit of the wrapped {@link Connectable} applies.
*
*
This is useful for instance if you want your UI to use a subset or a transformed version of
* the full model used in the {@code MobiusLoop}. As a simplified example, suppose that your model
* consists of a {@code Long} timestamp that you want to format to a {@code String} before
* rendering it in the UI. Your UI could then implement {@code Connectable}, and
* you could create a {@code NullValuedFunction} that does the formatting. The
* {@link com.spotify.mobius.MobiusLoop} would be outputting {@code Long} models that you need to
* convert to Strings before they can be accepted by the UI.
*
* {@code
* public class Formatter {
* public static String format(Long timestamp) { ... }
* }
*
* public class MyUi implements Connectable {
* // other things in the UI implementation
*
* {@literal @}Override
* public Connection connect(Consumer output) {
* return new Connection() {
* {@literal @}Override
* public void accept(String value) {
* // bind the value to the right UI element
* }
*
* {@literal @}Override
* public void dispose() {
* // dispose of any resources, if needed
* }
* }
* }
* }
*
* // Then, to connect the UI to a MobiusLoop.Controller with a Long model:
* MobiusLoop.Controller controller = ... ;
* MyUi myUi = ... ;
*
* controller.connect(Connectables.contramap(Formatter::format, myUi));
* }
*
* @param mapper the mapping function to apply
* @param connectable the underlying connectable
* @param the type the underlying connectable accepts
* @param the type the resulting connectable accepts
* @param the output type; usually the event type
*/
@Nonnull
public static Connectable contramap(
final Function mapper, final Connectable connectable) {
return SimpleConnectable.withConnectionFactory(
new Function, Connection>() {
@Nonnull
@Override
public Connection apply(Consumer output) {
return ContramapConnection.create(mapper, connectable, output);
}
});
}
/**
* Convert a {@link Connectable} of one type pair to another type pair by converting every
* incoming A to a B using the provided function. The function can then return either a B or null.
* On the first B returned, a connection to the provided connectable is established, and that B is
* passed through. If the aToB function returns null, this connection will be disposed. Whenever
* the provided connectable dispatches a C through the consumer it receives, that C is converted
* to a D and is then dispatched to whomever connected to the resulting connectable. The mechanism
* described is the dimap function from Haskell's Profunctor
* typeclass
*
* {@code
* class A {
* final B b;
* A(B b) { this.b = b; }
* B b() { return b }
* }
*
* abstract class B {
* abstract int x();
* }
*
* abstract class C {
* abstract String y();
* }
*
* class D {
* final C c;
* D(C c) { this.c = c; }
* }
*
* Connectable innerConnectable = o -> new Connection() {
* public void accept(B b) {
* o.accept(new C(b.x().toString()));
* }
*
* public void dispose() {
*
* }
* }
*
* Connectable outerConnectable = dimap(A::b, D::new, innerConnectable);
* RecordingConsumer consumer = new RecordingConsumer<>();
* Connection connection = outerConnectable.connect(consumer);
* connection.accept(new A(new B(5))); // connects to innerConnectable and forwards value
* consumer.assertValues(new D(new C("5")))
*
* connection.accept(new A(null)); // disconnects from innerConnectable
* connection.dispose(); // also disconnects from inner connectable
* }
*/
@Nonnull
public static com.spotify.mobius.Connectable dimap(
final NullValuedFunction aToB,
final Function cToD,
final Connectable connectable) {
return SimpleConnectable.withConnectionFactory(
new Function, Connection>() {
@Nonnull
@Override
public Connection apply(Consumer output) {
return DisconnectOnNullDimapConnection.create(aToB, cToD, connectable, output);
}
});
}
/**
* Merges all provided connectables into one. The resulting connectable will invoke all
* connectables sequentially on connection, on receiving items, and will forward any generated
* items to the receiver. Children with blocking connections must process incoming items on
* separate threads or they will block the parent from dispatching inputs to the rest of the
* siblings.
*/
@SafeVarargs
@Nonnull
public static Connectable merge(
final Connectable fst, final Connectable snd, final Connectable... cs) {
return SimpleConnectable.withConnectionFactory(
new Function, Connection>() {
@Nonnull
@Override
public Connection apply(Consumer output) {
final ArrayList> connectables = new ArrayList<>(cs.length + 2);
connectables.add(fst);
connectables.add(snd);
Collections.addAll(
connectables, (Connectable[]) Preconditions.checkArrayNoNulls(cs));
return MergeConnectablesConnection.create(connectables, output);
}
});
}
}