Observável RxJava One, vários assinantes

Observável RxJava One, vários assinantes

1. Visão geral

O comportamento padrão de vários assinantes nem sempre é desejável. Neste artigo, abordaremos como alterar esse comportamento e lidar com vários assinantes de maneira adequada.

Mas primeiro, vamos dar uma olhada no comportamento padrão de vários assinantes.

2. Comportamento padrão

Digamos que temos o seguinte Observável:

private static Observable getObservable() {
    return Observable.create(subscriber -> {
        subscriber.onNext(gettingValue(1));
        subscriber.onNext(gettingValue(2));

        subscriber.add(Subscriptions.create(() -> {
            LOGGER.info("Clear resources");
        }));
    });
}

Isso emite dois elementos assim que o Subscribers assina.

No nosso exemplo, temos dois Subscribers:

LOGGER.info("Subscribing");

Subscription s1 = obs.subscribe(i -> LOGGER.info("subscriber#1 is printing " + i));
Subscription s2 = obs.subscribe(i -> LOGGER.info("subscriber#2 is printing " + i));

s1.unsubscribe();
s2.unsubscribe();

Imagine que obter cada elemento é uma operação cara - pode incluir, por exemplo, um cálculo intensivo ou abrir uma conexão de URL.

Para simplificar, basta retornar um número:

private static Integer gettingValue(int i) {
    LOGGER.info("Getting " + i);
    return i;
}

Aqui está a saída:

Subscribing
Getting 1
subscriber#1 is printing 1
Getting 2
subscriber#1 is printing 2
Getting 1
subscriber#2 is printing 1
Getting 2
subscriber#2 is printing 2
Clear resources
Clear resources

Como podemos ver a obtenção de cada elemento e a limpeza dos recursos são executadas duas vezes por padrão - uma vez para cada Subscriber. Não é isso que queremos. A classe ConnectableObservable ajuda a corrigir o problema.

3. ConnectableObservable

A classe ConnectableObservable permite compartilhar a assinatura com vários assinantes e não executar as operações subjacentes várias vezes.

Mas primeiro, vamos criar um ConnectableObservable.

3.1. publicar()

O método publish () _ é o que cria um _ConnectableObservable a partir de um Observable:

ConnectableObservable obs = Observable.create(subscriber -> {
    subscriber.onNext(gettingValue(1));
    subscriber.onNext(gettingValue(2));
    subscriber.add(Subscriptions.create(() -> {
        LOGGER.info("Clear resources");
    }));
}).publish();

Mas, por enquanto, não faz nada. O que o faz funcionar é o método _connect () _.

3.2. conectar()

*Até que o método _ConnectableObservable_‘s _connect () _ não seja chamado _Observable_s _onSubcribe () _ callback não seja acionado* , mesmo que haja alguns assinantes.

Vamos demonstrar isso:

LOGGER.info("Subscribing");
obs.subscribe(i -> LOGGER.info("subscriber #1 is printing " + i));
obs.subscribe(i -> LOGGER.info("subscriber #2 is printing " + i));
Thread.sleep(1000);
LOGGER.info("Connecting");
Subscription s = obs.connect();
s.unsubscribe();

Assinamos e aguardamos um segundo antes de conectar. A saída é:

Subscribing
Connecting
Getting 1
subscriber #1 is printing 1
subscriber #2 is printing 1
Getting 2
subscriber #1 is printing 2
subscriber #2 is printing 2
Clear resources

Como podemos ver:

  • {em branco}

  • Obter elementos ocorre apenas uma vez como queríamos

  • Os recursos de compensação ocorrem apenas uma vez também

  • A obtenção de elementos inicia um segundo após a assinatura.

  • A inscrição não aciona mais a emissão de elementos. Somente _connect () _ faz isso

Esse atraso pode ser benéfico - às vezes precisamos fornecer a todos os assinantes a mesma sequência de elementos, mesmo que um deles assine antes do outro.

3.3. A Visão Consistente dos Observáveis ​​- _connect () _ Após _subscribe () _

Este caso de uso não pode ser demonstrado em nosso Observable anterior, pois fica frio e os dois assinantes recebem toda a sequência de elementos de qualquer maneira.

Imagine, em vez disso, que um elemento emissor não dependa do momento da assinatura, eventos emitidos com cliques do mouse, por exemplo. Agora imagine também que um segundo Subscriber assine um segundo após o primeiro.

O primeiro Subscriber receberá todos os elementos emitidos durante este exemplo, enquanto o segundo Subscriber receberá apenas alguns elementos.

Por outro lado, o uso do método connect () _ no lugar certo pode fornecer aos dois assinantes a mesma exibição na sequência _Observable.

*Exemplo de Hot _Observable_*

Vamos criar um Observável quente. Ele emitirá elementos com os cliques do mouse em JFrame.

Cada elemento será a coordenada x do clique:

private static Observable getObservable() {
    return Observable.create(subscriber -> {
        frame.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                subscriber.onNext(e.getX());
            }
        });
        subscriber.add(Subscriptions.create(() {
            LOGGER.info("Clear resources");
            for (MouseListener listener : frame.getListeners(MouseListener.class)) {
                frame.removeMouseListener(listener);
            }
        }));
    });
}
*O comportamento padrão de Hot _Observable_*

Agora, se assinarmos dois Subscribers um após o outro com um segundo intervalo, executar o programa e começar a clicar, veremos que o primeiro Subscriber terá mais elementos:

public static void defaultBehaviour() throws InterruptedException {
    Observable obs = getObservable();

    LOGGER.info("subscribing #1");
    Subscription subscription1 = obs.subscribe((i) ->
        LOGGER.info("subscriber#1 is printing x-coordinate " + i));
    Thread.sleep(1000);
    LOGGER.info("subscribing #2");
    Subscription subscription2 = obs.subscribe((i) ->
        LOGGER.info("subscriber#2 is printing x-coordinate " + i));
    Thread.sleep(1000);
    LOGGER.info("unsubscribe#1");
    subscription1.unsubscribe();
    Thread.sleep(1000);
    LOGGER.info("unsubscribe#2");
    subscription2.unsubscribe();
}
subscribing #1
subscriber#1 is printing x-coordinate 280
subscriber#1 is printing x-coordinate 242
subscribing #2
subscriber#1 is printing x-coordinate 343
subscriber#2 is printing x-coordinate 343
unsubscribe#1
clearing resources
unsubscribe#2
clearing resources
*_connect () _ Após _subscribe () _*

Para fazer com que os dois assinantes obtenham a mesma sequência, converteremos esse Observable em ConnectableObservable e chamaremos _connect () _ após a assinatura, ambos os Subscribers:

public static void subscribeBeforeConnect() throws InterruptedException {

    ConnectableObservable obs = getObservable().publish();

    LOGGER.info("subscribing #1");
    Subscription subscription1 = obs.subscribe(
      i -> LOGGER.info("subscriber#1 is printing x-coordinate " + i));
    Thread.sleep(1000);
    LOGGER.info("subscribing #2");
    Subscription subscription2 = obs.subscribe(
      i ->  LOGGER.info("subscriber#2 is printing x-coordinate " + i));
    Thread.sleep(1000);
    LOGGER.info("connecting:");
    Subscription s = obs.connect();
    Thread.sleep(1000);
    LOGGER.info("unsubscribe connected");
    s.unsubscribe();
}

Agora eles terão a mesma sequência:

subscribing #1
subscribing #2
connecting:
subscriber#1 is printing x-coordinate 317
subscriber#2 is printing x-coordinate 317
subscriber#1 is printing x-coordinate 364
subscriber#2 is printing x-coordinate 364
unsubscribe connected
clearing resources

Portanto, o objetivo é aguardar o momento em que todos os assinantes estejam prontos e chamar _connect () _.

Em um aplicativo Spring, podemos inscrever todos os componentes durante a inicialização do aplicativo, por exemplo, e chamar _connect () _ em _onApplicationEvent () _.

Mas vamos voltar ao nosso exemplo; observe que todos os cliques antes do método connect () _ são perdidos. Se não queremos perder elementos, mas, pelo contrário, processá-los, podemos colocar _connect () _ anteriormente no código e forçar o _Observable a produzir eventos na ausência de qualquer Subscriber.

3.4. Forçando assinatura na ausência de qualquer Subscriber - _connect () _ Antes de _subscribe () _

Para demonstrar isso, vamos corrigir o nosso exemplo:

public static void connectBeforeSubscribe() throws InterruptedException {
    ConnectableObservable obs = getObservable()
      .doOnNext(x -> LOGGER.info("saving " + x)).publish();
    LOGGER.info("connecting:");
    Subscription s = obs.connect();
    Thread.sleep(1000);
    LOGGER.info("subscribing #1");
    obs.subscribe((i) -> LOGGER.info("subscriber#1 is printing x-coordinate " + i));
    Thread.sleep(1000);
    LOGGER.info("subscribing #2");
    obs.subscribe((i) -> LOGGER.info("subscriber#2 is printing x-coordinate " + i));
    Thread.sleep(1000);
    s.unsubscribe();
}

Os passos são relativamente simples:

  • Primeiro, conectamos

  • Aguardamos um segundo e assinamos o primeiro Subscriber

  • Por fim, aguardamos mais um segundo e assinamos o segundo Subscriber

Observe que adicionamos o operador _doOnNext () _. Aqui, podemos armazenar elementos no banco de dados, por exemplo, mas em nosso código, apenas imprimimos "saving …​".

Se lançarmos o código e começarmos a clicar, veremos que os elementos são emitidos e processados ​​imediatamente após a chamada _connect () _:

connecting:
saving 306
saving 248
subscribing #1
saving 377
subscriber#1 is printing x-coordinate 377
saving 295
subscriber#1 is printing x-coordinate 295
saving 206
subscriber#1 is printing x-coordinate 206
subscribing #2
saving 347
subscriber#1 is printing x-coordinate 347
subscriber#2 is printing x-coordinate 347
clearing resources

Se não houvesse assinantes, os elementos ainda seriam processados.

*Portanto, o método _connect () _ começa a emitir e processar elementos, independentemente de alguém estar inscrito* como se houvesse um _Subscriber_ artificial com uma ação vazia que consumisse os elementos.

E se alguns Assinantes reais se inscreverem, esse mediador artificial propaga apenas elementos para eles.

Para cancelar a assinatura do Subscriber artificial, realizamos:

s.unsubscribe();

Onde:

Subscription s = obs.connect();

3.5. _autoConnect () _

*Este método implica que _connect () _ não é chamado antes ou depois das assinaturas, mas automaticamente quando o primeiro _Subscriber_ assina* .

Usando esse método, não podemos chamar connect () _ nós mesmos, pois o objeto retornado é um _Observable usual, que não possui esse método, mas usa um ConnectableObservable subjacente:

public static void autoConnectAndSubscribe() throws InterruptedException {
    Observable obs = getObservable()
    .doOnNext(x -> LOGGER.info("saving " + x)).publish().autoConnect();

    LOGGER.info("autoconnect()");
    Thread.sleep(1000);
    LOGGER.info("subscribing #1");
    Subscription s1 = obs.subscribe((i) ->
        LOGGER.info("subscriber#1 is printing x-coordinate " + i));
    Thread.sleep(1000);
    LOGGER.info("subscribing #2");
    Subscription s2 = obs.subscribe((i) ->
        LOGGER.info("subscriber#2 is printing x-coordinate " + i));

    Thread.sleep(1000);
    LOGGER.info("unsubscribe 1");
    s1.unsubscribe();
    Thread.sleep(1000);
    LOGGER.info("unsubscribe 2");
    s2.unsubscribe();
}

Observe que também não podemos cancelar a assinatura do Subscriber artificial. Podemos cancelar a inscrição de todos os assinantes reais, mas o assinante artificial ainda processará os eventos.

Para entender isso, vejamos o que está acontecendo no final após o cancelamento da inscrição do último assinante:

subscribing #1
saving 296
subscriber#1 is printing x-coordinate 296
saving 329
subscriber#1 is printing x-coordinate 329
subscribing #2
saving 226
subscriber#1 is printing x-coordinate 226
subscriber#2 is printing x-coordinate 226
unsubscribe 1
saving 268
subscriber#2 is printing x-coordinate 268
saving 234
subscriber#2 is printing x-coordinate 234
unsubscribe 2
saving 278
saving 268

Como podemos ver, a limpeza de recursos não acontece e o salvamento de elementos com doOnNext () _ continua após o segundo cancelamento da inscrição. Isso significa que o _Subscriber artificial não cancela a inscrição, mas continua a consumir elementos.

3.6. _refCount () _

*_refCount () _ é semelhante a _autoConnect () _, pois a conexão também acontece automaticamente assim que o primeiro _Subscriber_ assina.*

Ao contrário de autoconnect () _, a desconexão também acontece automaticamente quando o último _Subscriber cancela a inscrição:

public static void refCountAndSubscribe() throws InterruptedException {
    Observable obs = getObservable()
      .doOnNext(x -> LOGGER.info("saving " + x)).publish().refCount();

    LOGGER.info("refcount()");
    Thread.sleep(1000);
    LOGGER.info("subscribing #1");
    Subscription subscription1 = obs.subscribe(
      i -> LOGGER.info("subscriber#1 is printing x-coordinate " + i));
    Thread.sleep(1000);
    LOGGER.info("subscribing #2");
    Subscription subscription2 = obs.subscribe(
      i -> LOGGER.info("subscriber#2 is printing x-coordinate " + i));

    Thread.sleep(1000);
    LOGGER.info("unsubscribe#1");
    subscription1.unsubscribe();
    Thread.sleep(1000);
    LOGGER.info("unsubscribe#2");
    subscription2.unsubscribe();
}
refcount()
subscribing #1
saving 265
subscriber#1 is printing x-coordinate 265
saving 338
subscriber#1 is printing x-coordinate 338
subscribing #2
saving 203
subscriber#1 is printing x-coordinate 203
subscriber#2 is printing x-coordinate 203
unsubscribe#1
saving 294
subscriber#2 is printing x-coordinate 294
unsubscribe#2
clearing resources

4. Conclusão

A classe ConnectableObservable ajuda a lidar com vários assinantes com pouco esforço.

Seus métodos parecem semelhantes, mas alteram muito o comportamento dos assinantes devido às sutilezas da implementação, o que significa que mesmo a ordem dos métodos é importante.

O código fonte completo de todos os exemplos usados ​​neste artigo pode ser encontrado no GitHub project.