O Padrão Event Bus no TKClient: Uma Abordagem para Comunicação entre Componentes

Olá, colegas desenvolvedores!

Hoje, vamos mergulhar no módulo EventBus do framework TKClient (EventBus.java). Gerenciar a comunicação entre componentes em aplicativos complexos pode ser um desafio considerável. O EventBus surge como uma solução elegante para simplificar esse processo.

Por Que Precisamos de um Event Bus?

Em desenvolvimento de aplicativos, como os componentes se comunicam? Por exemplo, ao alterar o tema em uma página de configurações, desejamos que a página inicial atualize sua interface imediatamente. Ou, ao receber uma nova mensagem, queremos que várias telas exibam notificações.

Métodos tradicionais incluem:

  • Callbacks de interface
  • Broadcasts
  • Variáveis globais compartilhadas
  • Transmissão de dados via Intent

No entanto, cada um desses métodos apresenta suas desvantagens: callbacks podem causar vazamentos de memória, broadcasts têm eficiência limitada, variáveis globais são difíceis de gerenciar e Intents são adequados apenas para comunicação entre Activities.

O Event Bus foi projetado para superar essas limitações, oferecendo um mecanismo de comunicação de componentes com baixo acoplamento.

Arquitetura Geral do Event Bus do TKClient

O Event Bus do TKClient adota o padrão Publish-Subscribe, com os seguintes componentes principais:

  • EventBus: A classe central, responsável pelo registro, envio e cancelamento de registro de eventos.
  • Event: Uma classe base para eventos; todos os eventos personalizados devem herdar dela.
  • Subscriber: Uma interface que define métodos de tratamento de eventos.
  • ThreadMode: Um enum que controla em qual thread o tratamento do evento será executado.

Aqui está uma estrutura básica do EventBus:


public class EventBus {
    private static final String TAG = "EventBus";
    
    private static volatile EventBus instance;
    
    private final Map<Class<?>, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType;
    private final Map<Object, List<Class<?>>> typesBySubscriber;
    private final Map<Class<?>, Object> stickyEvents;
    
    private final Executor mainThreadExecutor;
    private final Executor backgroundThreadExecutor;
    private final Executor asyncThreadExecutor;
    
    private EventBus() {
        this.subscriptionsByEventType = new HashMap<>();
        this.typesBySubscriber = new HashMap<>();
        this.stickyEvents = new HashMap<>();
        this.mainThreadExecutor = new MainThreadExecutor(); // Assumindo uma implementação de Executor para a thread principal
        this.backgroundThreadExecutor = Executors.newSingleThreadExecutor();
        this.asyncThreadExecutor = Executors.newCachedThreadPool();
    }
    
    public static EventBus getDefault() {
        if (instance == null) {
            synchronized (EventBus.class) {
                if (instance == null) {
                    instance = new EventBus();
                }
            }
        }
        return instance;
    }
    
    // Outros métodos aqui...
}

Esta é uma implementação típica do padrão Singleton, garantindo uma única instância do Event Bus em toda a aplicação. As estruturas de dados centrais são dois Maps: um mapeia tipos de eventos para seus assinantes, e o outro mapeia assinantes para os tipos de eventos que eles subscrevem. Esse mapeamento bidirecional permite a busca eficiente tanto por tipo de evento quanto por assinante.

Implementação de Registro e Cancelamento de Registro

O primeiro passo para usar o Event Bus é registrar os assinantes. O EventBus do TKClient oferece uma API de registro intuitiva:


/**
 * Registra um assinante.
 */
public void register(Object subscriber) {
    Class<?> subscriberClass = subscriber.getClass();
    
    List<SubscriberMethod> methods = findSubscriberMethods(subscriberClass);
    
    synchronized (this) {
        for (SubscriberMethod method : methods) {
            subscribe(subscriber, method);
        }
    }
}

/**
 * Encontra métodos de tratamento de eventos na classe do assinante.
 */
private List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
    List<SubscriberMethod> methods = new ArrayList<>();
    
    Method[] declaredMethods = subscriberClass.getDeclaredMethods();
    for (Method method : declaredMethods) {
        Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);
        if (subscribeAnnotation != null) {
            Class<?>[] parameterTypes = method.getParameterTypes();
            if (parameterTypes.length != 1) {
                throw new EventBusException("@Subscribe method must have exactly 1 parameter");
            }
            
            Class<?> eventType = parameterTypes[0];
            ThreadMode threadMode = subscribeAnnotation.threadMode();
            
            SubscriberMethod subscriberMethod = new SubscriberMethod(
                    method, 
                    eventType, 
                    threadMode, 
                    subscribeAnnotation.priority(),
                    subscribeAnnotation.sticky()
            );
            methods.add(subscriberMethod);
        }
    }
    
    return methods;
}

/**
 * Assina um método específico.
 */
private void subscribe(Object subscriber, SubscriberMethod method) {
    Class<?> eventType = method.eventType;
    
    Subscription subscription = new Subscription(subscriber, method);
    
    CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
    if (subscriptions == null) {
        subscriptions = new CopyOnWriteArrayList<>();
        subscriptionsByEventType.put(eventType, subscriptions);
    }
    
    // Insere a assinatura priorizando
    int size = subscriptions.size();
    for (int i = 0; i <= size; i++) {
        if (i == size || subscription.priority > subscriptions.get(i).priority) {
            subscriptions.add(i, subscription);
            break;
        }
    }
    
    // Atualiza o mapeamento do assinante para os tipos de evento
    List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber);
    if (subscribedEvents == null) {
        subscribedEvents = new ArrayList<>();
        typesBySubscriber.put(subscriber, subscribedEvents);
    }
    subscribedEvents.add(eventType);
    
    // Lida com eventos pegajosos (sticky)
    if (method.sticky) {
        Object stickyEvent = stickyEvents.get(eventType);
        if (stickyEvent != null) {
            postToSubscription(subscription, stickyEvent, false);
        }
    }
}

O processo de registro envolve:

  1. Encontrar métodos anotados com @Subscribe via reflexão.
  2. Criar e armazenar informações de assinatura nos mapas correspondentes.
  3. Enviar eventos pegajosos em cache para novos assinantes, se aplicável.

Para evitar vazamentos de memória, é crucial cancelar o registro quando não for mais necessário:


/**
 * Cancela o registro de um assinante.
 */
public synchronized void unregister(Object subscriber) {
    List<Class<?>> subscribedTypes = typesBySubscriber.remove(subscriber);
    if (subscribedTypes == null) {
        Log.w(TAG, "Subscriber to unregister was not registered before: " + subscriber.getClass());
        return;
    }
    
    for (Class<?> eventType : subscribedTypes) {
        CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
        if (subscriptions != null) {
            Iterator<Subscription> iterator = subscriptions.iterator();
            while (iterator.hasNext()) {
                Subscription subscription = iterator.next();
                if (subscription.subscriber == subscriber) {
                    iterator.remove();
                }
            }
            
            if (subscriptions.isEmpty()) {
                subscriptionsByEventType.remove(eventType);
            }
        }
    }
}

O cancelamento de registro limpa as estruturas de dados para evitar manter referências a assinantes desnecessários.

Mecanismo de Publicação de Eventos

Após o registro, os eventos podem ser publicados. O EventBus do TKClient oferece vários métodos de publicação:


/**
 * Publica um evento.
 */
public void post(Object event) {
    PostingThreadState postingState = currentPostingThreadState.get();
    
    List<Object> eventQueue = postingState.eventQueue;
    eventQueue.add(event);
    
    if (!postingState.isPosting) {
        postingState.isPosting = true;
        try {
            while (!eventQueue.isEmpty()) {
                postSingleEvent(eventQueue.remove(0), postingState);
            }
        } finally {
            postingState.isPosting = false;
        }
    }
}

/**
 * Publica um único evento.
 */
private void postSingleEvent(Object event, PostingThreadState postingState) {
    Class<?> eventClass = event.getClass();
    boolean posted = false;
    
    posted = postToSubscriptions(subscriptionsByEventType.get(eventClass), event, postingState);
    
    if (!posted) {
        for (Class<?> superClass : eventClass.getSuperclasses()) {
            if (superClass != Object.class) {
                posted |= postToSubscriptions(subscriptionsByEventType.get(superClass), event, postingState);
            }
        }
        
        for (Class<?> interfaceClass : eventClass.getInterfaces()) {
            posted |= postToSubscriptions(subscriptionsByEventType.get(interfaceClass), event, postingState);
        }
    }
    
    if (!posted) {
        Log.d(TAG, "No subscribers registered for event " + eventClass);
    }
}

/**
 * Envia um evento para todos os assinantes na lista.
 */
private boolean postToSubscriptions(CopyOnWriteArrayList<Subscription> subscriptions, Object event, PostingThreadState postingState) {
    if (subscriptions == null || subscriptions.isEmpty()) {
        return false;
    }
    
    for (Subscription subscription : subscriptions) {
        postingState.subscription = subscription;
        postToSubscription(subscription, event, postingState.canceled);
        if (postingState.canceled) {
            break;
        }
    }
    
    return true;
}

/**
 * Envia um evento para um único assinante.
 */
private void postToSubscription(Subscription subscription, Object event, boolean canceled) {
    if (canceled) {
        return;
    }
    
    switch (subscription.subscriberMethod.threadMode) {
        case POSTING:
            invokeSubscriber(subscription, event);
            break;
        case MAIN:
            if (isMainThread()) { // Assume que isMainThread() verifica a thread principal
                invokeSubscriber(subscription, event);
            } else {
                mainThreadExecutor.execute(() -> {
                    if (!canceled) {
                        invokeSubscriber(subscription, event);
                    }
                });
            }
            break;
        case BACKGROUND:
            if (!isMainThread()) {
                invokeSubscriber(subscription, event);
            } else {
                backgroundThreadExecutor.execute(() -> {
                    if (!canceled) {
                        invokeSubscriber(subscription, event);
                    }
                });
            }
            break;
        case ASYNC:
            asyncThreadExecutor.execute(() -> {
                if (!canceled) {
                    invokeSubscriber(subscription, event);
                }
            });
            break;
    }
}

/**
 * Invoca o método do assinante.
 */
private void invokeSubscriber(Subscription subscription, Object event) {
    try {
        subscription.subscriberMethod.method.invoke(subscription.subscriber, event);
    } catch (IllegalAccessException e) {
        throw new IllegalStateException("Unexpected exception", e);
    } catch (InvocationTargetException e) {
        handleSubscriberException(subscription, event, e.getCause());
    }
}

O processo de publicação de eventos envolve:

  1. Adicionar o evento a uma fila.
  2. Localizar todos os assinantes cujos tipos de evento correspondem ao evento publicado.
  3. Invocar os métodos de tratamento de eventos nas threads apropriadas, com base no ThreadMode especificado.

A enumeração ThreadMode oferece flexibilidade:


public enum ThreadMode {
    /** Invoca o método do assinante na thread que publicou o evento. */
    POSTING,
    
    /** Invoca o método do assinante na thread principal. */
    MAIN,
    
    /** Invoca o método do assinante em uma thread de background. Se a thread de publicação já for de background, chama diretamente. */
    BACKGROUND,
    
    /** Invoca o método do assinante sempre em uma nova thread. */
    ASYNC
}

Essa abordagem permite escolher o modo de thread mais adequado para cada cenário:

  • POSTING: Para tratamentos de eventos simples que não envolvem atualizações de UI.
  • MAIN: Para atualizações de UI.
  • BACKGROUND: Para operações demoradas onde a ordem de execução não é crítica.
  • ASYNC: Quando cada evento requer seu próprio thread independente.

Implementação de Eventos Pegajosos (Sticky Events)

Eventos pegajosos permitem que assinantes recebam eventos publicados antes de seu registro. Isso é útil para estados como status de login ou conexão de rede.

O Event Bus do TKClient implementa eventos pegajosos através dos seguintes métodos:


/**
 * Publica um evento pegajoso.
 */
public void postSticky(Object event) {
    synchronized (stickyEvents) {
        stickyEvents.put(event.getClass(), event);
    }
    post(event); // Também publica como um evento normal
}

/**
 * Obtém um evento pegajoso do tipo especificado.
 */
public <T> T getStickyEvent(Class<T> eventType) {
    synchronized (stickyEvents) {
        return eventType.cast(stickyEvents.get(eventType));
    }
}

/**
 * Remove um evento pegajoso do tipo especificado.
 */
public <T> T removeStickyEvent(Class<T> eventType) {
    synchronized (stickyEvents) {
        return eventType.cast(stickyEvents.remove(eventType));
    }
}

/**
 * Remove um evento pegajoso específico.
 */
public boolean removeStickyEvent(Object event) {
    synchronized (stickyEvents) {
        Class<?> eventType = event.getClass();
        Object existingEvent = stickyEvents.get(eventType);
        if (event.equals(existingEvent)) {
            stickyEvents.remove(eventType);
            return true;
        }
        return false;
    }
}

/**
 * Remove todos os eventos pegajosos.
 */
public void removeAllStickyEvents() {
    synchronized (stickyEvents) {
        stickyEvents.clear();
    }
}

A lógica para eventos pegajosos envolve armazenar o evento em um cache. Quando um novo assinante se registra com o modificador sticky = true em sua anotação @Subscribe, ele recebe imediatamente o evento pegajoso em cache.

Exemplo de Uso

Vamos ver um exemplo prático:


// Definição de um evento normal
public class MessageEvent {
    private String message;
    
    public MessageEvent(String message) {
        this.message = message;
    }
    
    public String getMessage() {
        return message;
    }
}

// Definição de um evento pegajoso
public class UserLoginEvent {
    private boolean loggedIn;
    private String username;
    
    public UserLoginEvent(boolean loggedIn, String username) {
        this.loggedIn = loggedIn;
        this.username = username;
    }
    
    public boolean isLoggedIn() {
        return loggedIn;
    }
    
    public String getUsername() {
        return username;
    }
}

// Uso em uma Activity
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        EventBus.getDefault().register(this);
        
        findViewById(R.id.sendButton).setOnClickListener(v -> {
            EventBus.getDefault().post(new MessageEvent("Hello from MainActivity"));
        });
        
        findViewById(R.id.loginButton).setOnClickListener(v -> {
            EventBus.getDefault().postSticky(new UserLoginEvent(true, "Alice"));
            startActivity(new Intent(this, ProfileActivity.class));
        });
    }
    
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onMessageEvent(MessageEvent event) {
        Toast.makeText(this, "Received: " + event.getMessage(), Toast.LENGTH_SHORT).show();
    }
    
    @Override
    protected void onDestroy() {
        EventBus.getDefault().unregister(this);
        super.onDestroy();
    }
}

// Uso em outra Activity
public class ProfileActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_profile);
        
        EventBus.getDefault().register(this);
        
        findViewById(R.id.sendButton).setOnClickListener(v -> {
            EventBus.getDefault().post(new MessageEvent("Hello from ProfileActivity"));
        });
    }
    
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onMessageEvent(MessageEvent event) {
        Toast.makeText(this, "Received: " + event.getMessage(), Toast.LENGTH_SHORT).show();
    }
    
    @Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
    public void onUserLoginEvent(UserLoginEvent event) {
        if (event.isLoggedIn()) {
            TextView usernameText = findViewById(R.id.usernameText);
            usernameText.setText("Welcome, " + event.getUsername());
        } else {
            finish(); 
        }
    }
    
    @Override
    protected void onDestroy() {
        EventBus.getDefault().unregister(this);
        super.onDestroy();
    }
}

Este exemplo ilustra:

  • Comunicação inter-Activity usando eventos normais.
  • Transmissão de status de login com eventos pegajosos.
  • Atualizações de UI na thread principal.

Análise de Cenários de Aplicação

O Event Bus é adequado para:

  • Comunicação entre componentes independentes (Activities, Fragments).
  • Desacoplamento de dependências complexas.
  • Notificação de múltiplos componentes não relacionados.
  • Sincronização de estado global.

Pode não ser ideal para:

  • Comunicação simples entre pai e filho (onde chamadas diretas são mais claras).
  • Operações que exigem um valor de retorno imediato.
  • Eventos de alta frequência (como scroll ou touch), que podem impactar o desempenoh.
  • Cenários que exigem ordem estrita de processamento de eventos.

Considerações de Otimização de Desempenho

Ao usar um Event Bus, considere:

  • Custo de Reflexão: O uso extensivo de reflexão pode impactar o desempenho em chamadas de alta frequência.
  • Risco de Vazamento de Memória: Esquecer de cancelar o registro pode impedir a coleta de lixo dos assinantes.
  • Custo de Troca de Threads: A troca entre diferentes modos de thread tem um custo associado.

O Event Bus do TKClient inclui otimizações como cache de métodos para reduzir o custo de reflexão:


// Cache para reduzir o custo de reflexão
private static final Map<Class<?>, List<SubscriberMethod>> METHOD_CACHE = new ConcurrentHashMap<>();

/**
 * Encontra métodos de tratamento de eventos na classe do assinante (versão otimizada).
 */
private List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
    List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass);
    if (subscriberMethods != null) {
        return subscriberMethods;
    }
    
    subscriberMethods = new ArrayList<>();
    // ... código de reflexão ...
    
    METHOD_CACHE.put(subscriberClass, subscriberMethods);
    return subscriberMethods;
}

Práticas recomendadas para uso incluem:

  • Registrar e cancelar o registro em métodos de ciclo de vida apropriados (por exemplo, onCreate e onDestroy de uma Activity).
  • Evitar classes internas anônimas como assinantes para prevenir vazamentos de memória.
  • Utilizar técnicas de throttling ou debouncing para eventos de alta frequência.
  • Escolher o ThreadMode apropriado para minimizar trocas de thread desnecessárias.

Resumo e Melhores Práticas

O Event Bus do TKClient, baseado no padrão Publish-Subscribe, facilita a comunicação desacoplada entre componentes. Suas carcaterísticas incluem:

  • API limpa com reflexão e anotações.
  • Suporte a múltiplos modos de thread.
  • Funcionalidade de evento pegajoso para sincronização de estado.

Melhores práticas:

  • Projete classes de evento claras e com significado de negócio explícito.
  • Use os modos de thread adequados (MAIN parra UI, BACKGROUND/ASYNC para operações demoradas).
  • Sempre chame register e unregister em pares.
  • Use eventos pegajosos com cautela, lembrando-se de removê-los quando não forem mais necessários.
  • Não abuse do Event Bus; prefira chamadas diretas para comunicação simples entre pai e filho.

Tags: event-bus publish-subscribe android component-communication tkclient

Publicado em 7-10 20:10