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

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); } }); } }