Android: Aplicando MVC+Presentation Model

O Problema

Há algum tempo que desenvolvo aplicações em Android e a arquitetura, especificamente a da camada de apresentação, sempre foi um ponto de dúvida e polêmica. Nas minhas primeiras tentativas, não apliquei nenhum padrão. Simplesmente acreditei que a Activity poderia assumir o papel de Controller/Presenter, alterar diretamente no modelo de domínio e atualizar a View, que estaria separada nos arquivos de layout no formato XML, afinal, aplicativos móveis são essencialmente simples. Ilusão. O resultado foi, com uma pitada de generosidade, insatisfatório.

Apesar de podermos argumentar que o Android fornece, mesmo que de maneira rudimentar, uma solução MVC, à medida que as aplicações aumentam em nível de complexidade essa opção se torna menos eficiente.

Com essa constatação em mãos (e uma pequena pressão dos colegas de trabalho), tentei aplicar o MVP. Nesse caso a Activity assume o papel de View, delegando os eventos gerados pela iteração com o usuário ao presenter, que por sua vez ficava responsável pelo trabalho pesado. As melhoras em relação ao approach anterior são incontáveis. Apesar disso, uma das caracteríticas que mais vendem o MVP é a possibilidade de substituir a View de maneira a suportar multiplas interfaces gráficas usando o mesmo back-end, o que não é exatamente útil num ambiente de requisitos tão particulares como o oferecido pelo framework Android. Um problema recorrente era a tentativa quase que fútil de evitar o acoplamento entre presenter e Activity. Praticamente todas as chamadas à API dependem de alguma maneira da Activity. Não fiquei convencido… back to the drawing board it is, then.

Após não ter conseguido enxergar um sentido maior em usar o MVP, decidi retomar as tentativas com o já surrado MVC, mas dessa vez sem cegueira. De cara eu sabia que não ia alcaçar o nível de desacoplamento que o MVP oferece pois teria que retornar valores diretamente para a View, mas as vezes é preciso dar um passo para tráz para, mais tarde, poder dar dois para frente.

A Solução

Presentation Model. Descrito em detalhe por Martin Fowler em 2004 mas ainda marcado como “work in progress”, esse padrão traz o estado da View para uma classe sem relação alguma com a renderização da UI. A idéia é que essa classe contenha todo estado da View, de maneira que a última possa ler esse estado e se renderizar por completo. Por exemplo: se um componente deve estar desabilitado na interface, o Presentation Model deve informar que explicitamente o estado do componente: isNameFieldEnabled(). Claro que se o componente vai estar sempre habilitado não há motivo para ter tal campo.

Tomando o caso descrito como exeplo, assuma que o campo deveria ser habilitado quando um determinado checkbox fosse marcado. O fluxo para que isso acontecesse seria assim:

  • A View recebe o evento de alteração do Checkbox
  • A ação é delegada ao Controller através de um Message
  • O Controller atualiza o PresentationModel de acordo
  • O PresentationModel notifica a View para que ela seja renderizada novamente
  • Usando os dados do PresentationModel a View atualiza seu componentes
  • A View volta a esperar novas iterações do usuário

Implementação

A implementação para o Android se dá da seguinte forma:

Presentation Model
É instanciado pela View e repassado para o Controller.

View
A Activity assume o papel de View; como ela é o “Entry Point” da aplicação, também fica responsável pela instanciação do Presentation Model, nesse momento a View se registra como Observer do Model. Também instancia o Controller, que recebe o Presentation Model como parâmetro do seu construtor. Isso tudo acontece no onCreate() onde normalmente é enviada uma Message de inicialização para o Presenter.

Controller
De posse do Presentation Model, pode atualizar a View e também tem acesso à camada de serviços (persitência, web services) da aplicação. É composto também as Menssages suportadas que serão usadas pela View para repassar os eventos gerados pela iteração do usuário. Expõe apenas do método handle(Message, Object… args) como único ponto de comunicação disponível para a View. Pode ter esse comportamento generalisado em uma classe pai. Mantém uma instância da Activity corrente, mas não referência a Activity que implementa a View diretamente, ou seja, referencia android.os.Activity mas não a mypackage.myapp.MyActivity).

// View =========================================
package com.zbra.articles.pmvc.demo;

import java.util.Observable;
import java.util.Observer;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.CheckBox;
import android.widget.EditText;

public class MainActivity extends Activity implements Observer {
    private MainModel model;
    private MainController controller;

    private CheckBox checkBox;
    private EditText nameEditText;
    private EditText optionalEditText;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        nameEditText = (EditText) findViewById(R.id.editTextNome);
        optionalEditText = (EditText) findViewById(R.id.editTextOpcional);
        checkBox = (CheckBox) findViewById(R.id.checkBox);

        model = new MainModel();
        model.addObserver(this);

        controller = new MainController(model, this);
        controller.handle(MainController.MESSAGE_INIT);
    }

    public void checkBoc_onClick(View view) {
        controller.handle(MainController.MESSAGE_CHECK_CHANGED, checkBox.isChecked());
    }

    public void button_onClick(View view) {
        controller.handle(MainController.MESSAGE_SUBMIT, nameEditText.getText().toString(), optionalEditText.getText().toString(), checkBox.isChecked());
    }

    @Override
    public void update(Observable observable, Object data) {
        optionalEditText.setEnabled(model.isOptionalFieldEnabled());
    }
}

// Controller =========================================
package com.zbra.articles.pmvc.demo;

import android.app.Activity;
import android.content.Intent;

public class MainController {
    public static final int MESSAGE_INIT = 0;
    public static final int MESSAGE_CHECK_CHANGED = 1;
    public static final int MESSAGE_SUBMIT = 2;

    private Activity activity;
    private MainModel model;

    public MainController(MainModel model, Activity activity)	{
        this.activity = activity;
        this.model = model;
    }

    public Activity getActivity() {
        return activity;
    }

    public MainModel getModel() {
        return model;
    }

    // handle event
    public void handle(int message, final Object... data) {
        switch (message) {
        case MESSAGE_INIT:
            doInitialize(data);
            break;

        case MESSAGE_CHECK_CHANGED:
            doCheckChanged(data);
            break;

        case MESSAGE_SUBMIT:
            doSubmit(data);
            break;
        }
    }

    private void doSubmit(Object[] data) {
        Intent i = new Intent(activity, AnotherActivity.class);
        activity.startActivity(i);
    }

    private void doCheckChanged(Object[] data) {
        boolean checked = (Boolean) data[0];
        model.setOptionalFieldEnabled(checked);
        model.notifyObservers();
    }

    private void doInitialize(Object[] data) {
        model.setOptionalFieldEnabled(false);
        model.notifyObservers();
    }
}

// Presentation Model =====================================
package com.zbra.articles.pmvc.demo;

import java.util.Observable;

public class MainModel extends Observable {

    private boolean optionalFieldEnabled;

    public void setOptionalFieldEnabled(boolean enabled) {
        optionalFieldEnabled = enabled;
        this.setChanged();
    }

    public boolean isOptionalFieldEnabled() {
        return optionalFieldEnabled;
    }
}

Essa arquitetura acaba trazendo muitos dos benefícios que o MVP têm e alguns bônus:

  • Promove desacoplamento entre Controller e View
  • Chamadas ao Controller não retornam dados o que reforça via código o principio pregado pelo Presenter
  • O estado da View fica desacoplado de ambos (View e Controller) o que deixa ambos mais focados e simples, facilitando a manutenção
  • A arquitertura fica reforçada em código em vez de apenas em boas práticas

Mas, como todas as soluções, tem também seus trade-offs:

  • Os parâmetros passados pela View são fracamente tipados
  • Há um certo overhead que precisa ser gerenciado, já que pequenas alterações ao Presentation Model vão resultar em uma renderização completa da View
  • Views muito complexas podem gerar Presentation Models muito grandes

Na solução proposta, existe ainda uma dependência indireta da Activity, dado que a iteração com a mesma é necessária para estabelecer conexão com o banco de dados, acessar serviços, Content Providers e por ai vai. A parte boa é que, como definido pela arquitetura, não há acoplamento com a View em si, apenas com a classe Activity, ainda possibilitando criar muitiplas renderizações para um mesmo par de PresentationModel-Controller.

Escrever uma aplicação complexa de maneira desacoplada da Activity ainda é um problema aqui, como você já deve ter notado. Todo tipo de interação com o Framework exige uma instância de Context para ser executada, e um Context só pode ser instanciado pela plataforma no formato de um Activity, Serviço ou Content Provider. Qualquer uma das opções gera algum grau de acoplamento no Controller. Mas isso já é assunto para outro artigo…

Truques Adicionais

No caso de implementarmos um Controller genérico, o que é um passo óbvio, é possível usar as classes Handler e HandlerThread para executar todas as Messages passadas para o Controller de forma assíncrona. Nesse caso as implementações do método que trata a notificação da View pelo Presentation Model devem ser retificadas para invocarem o método Activity.runOnUIThread() para que tudo funcione bem. Fazer isso vai trazer benefícios consideráveis à sensação de performance,  já que garante que nenhuma tarefa trave a Thread da UI, melhorando a experiência do usuário.

Essa melhoria destaca uma vantagem do handle() centralizado no Controller. O senso comum recomendaria expor vários métodos especializados, mas no nosso caso, estamos fazendo um trade-off: perde-se os parâmetros fortemente tipados mas, por outro lado, ganha-se a habilidade implementar comportamentos gerais a todas as chamadas, usada aqui para implementar a paralelização que isola a thread de UI dos processamentos realizados no Controller. Esse caso é tipicamente resolvido usando AOP, mas talvez isso seja um overhead muito grande para um dispositivo embarcado, isso se o AspectJ é sequer compatível com a plataforma  (mais assuntos para outros artigos…). Dadas as limitações, acho essa estratégia é justificada e cabe ao desenvolvedor “medir” se ela é adequada ou não ao seu cenário. Vale notar que fazer o expor métodos específicos no Controller não viola em nenhum aspecto a arquitetura proposta. Fica registrada então, mais uma maneira de se implementar essa comunicação para mantermos nossas mentes refrescadas e abertas a paradigmas diferentes.

Usar implementações de classes anonimas como delegates no lugar de fazer um switch-case nos eventos é uma boa melhoria. Vamos colocar esse delegate nas nossas constantes e depois é só invocá-los no handle(). Mas ainda não está bom, pois como as constantes são públicas nada impede que a View invoque o run() diretamente. Para evitar isso, basta colocar as constantes numa enum que implementa uma interface protected.

// Controller genérico =========================
package com.zbra.articles.pmvc;

import android.app.Activity;
import android.os.Handler;
import android.os.HandlerThread;

public class Controller {
    private M model;
    private Activity activity;
    private Handler handler;
    private HandlerThread handlerThread;

    public Controller(M model, Activity activity)	{
        this.activity = activity;
        this.model = model;
        this.handlerThread = new HandlerThread(getClass().getSimpleName() +  " Thread");
        this.handlerThread.start();
        this.handler = new Handler(handlerThread.getLooper());
    }

    public void dispose() {
        handlerThread.getLooper().quit();
    }

    public Activity getActivity() {
        return activity;
    }

    public M getModel() {
        return model;
    }

    // handle event
    public void handle(final Message message, final Object... data) {
        handler.post(new Runnable() {
            @SuppressWarnings("unchecked")
            public void run() {
                message.getTask().run((T)Controller.this, data);
            }
        });
    }

    protected static interface Message {
        MessageTask getTask();
    }

    protected static interface MessageTask {
        void run(T sender, Object data);
    }
}

// Presentation Model Interface
package com.zbra.articles.pmvc;

public interface PresentationModel {
    void notifyObservers();
    void notifyObservers(Object arg);
}

// View Interface
package com.zbra.articles.pmvc;

import java.util.Observable;
import java.util.Observer;

public interface View extends Observer {
    void update(Observable observable, Object data);
}

Usando essa nova organização o Controller fica ainda mais simples, View e Presentation Model permanecem inalterados, exceto por implementar as novas interfaces.

package com.zbra.articles.pmvc.demo;

import com.zbra.articles.pmvc.Controller;

import android.app.Activity;
import android.content.Intent;

public class MainController extends Controller<MainModel> {

	// constructor
	public MainController(MainModel model, Activity activity)	{
		super(model, activity);
	}

	// Event implementations
	private static final MessageTask<MainController> initializeTask = new MessageTask<MainController>() {
		public void run(MainController sender, Object[] data) {
			MainModel model = sender.getModel();
			model.setOptionalFieldEnabled(false);
			model.notifyObservers();
		}
	};

	private static final MessageTask<MainController> checkChangedTask = new MessageTask<MainController>() {
		public void run(MainController sender, Object[] data) {
			MainModel model = sender.getModel();
			boolean checked = (Boolean) data[0];
			model.setOptionalFieldEnabled(checked);
			model.notifyObservers();
		}
	};

	private static final MessageTask<MainController> submitTask = new MessageTask<MainController>() {
		public void run(MainController sender, Object[] data) {
			Activity activity = sender.getActivity();
			Intent i = new Intent(activity, AnotherActivity.class);
			activity.startActivity(i);
		}
	};

	// Messages enum
	public enum Messages implements Message<MainController> {
		Initialize(initializeTask),
		CheckChanged(checkChangedTask),
		Submit(submitTask);

		// Message<T> implementation
		private MessageTask<MainController> task;
		private Messages(MessageTask<MainController> task) {
			this.task = task;
		}

		@Override
		public MessageTask<MainController> getTask() {
			return task;
		}
	}
}

Conclusão

O conceito do Presentation Model apresenta uma abordagem interessante e nova ao problema que já está mais do que batido e é possível aplicar essa mesma arquitetura fora do Android, talvez até com maior sucesso. Vale ressaltar que essa proposta de arquitetura não é obra do Fowler e o artigo do Presentation Model ainda está lá como “work in progress”. Lembre-se disso e caso resolva aplica-la, faça-o com um olhar crítico. Alias olhar crítico é essencial. Nenhuma solução vai ser ideal para TODOS os problemas, então olho aberto pois mesmo as propostas pelo Fowler podem não ser idéais para o seu problema e particularidades.

Quanto ao Android, não tenho certeza se essa é a solução definitiva para o problema da arquitetura dos aplicativos Adroid, mas acho que é um passo na direção correta. No momento estou acreditando nesse modelo. Daqui a alguns meses, quando tiver em mãos uma aplicação pronta, baseada nesse modelo, volto aqui e dou o meu veredito final a respeito.

O que vocês acham? Deixem suas dúvidas e críticas nos comentários…

Referências

Joshua Musselwhite’s Android Architecture Series
http://www.therealjoshua.com/2011/11/android-architecture-part-1-intro/

Martin Fowler’s Presentation Model
http://martinfowler.com/eaaDev/PresentationModel.html

MVC on Wikipedia
http://en.wikipedia.org/wiki/Model%E2%80%93View%E2%80%93Controller

2 Comentários

  • Reply

    Por Eduardo Folly em 29 de May de 2012 às 7:56

    Excelente artigo, Bruno.

    Com certeza é uma solução para ser seguida e muito bem trabalhada.

    Parabéns.

  • Reply

    Por Marcio Duran em 1 de November de 2012 às 8:40

    Posso usar android com vaadin framework ?





Desenvolvido por hacklab/ com WordPress