Model View Presenter Class Diagram

Model View Presenter (MVP) no Android, Parte 2

No artigo anterior introduzimos os conceitos do padrão Model View Presenter (MVP), e as principais vantagens de sua utilização para desenvolvimento de aplicações Android. Na segunda parte da série vamos colocar a mão na massa e implementar, de forma canónica, sem uso de qualquer library que não faça parte da SDK Android/Java, nossa própria versão do MVP.

O código que iremos desenvolver é simples, mas considerando a quantidade de objetos envolvidos, ele pode parecer complexo, entretanto uma vez você tenha compreendido a idéia geral, a coisa toda fica bem clara. Caso prefira ler a aprender diretamente do código, pode dar uma conferida direta no projeto completo.

Planejamento do padrão Model View Presenter (MVP)

[expand title=”clique para visualizar conceitos do padrão MVP no Android” tag=”h5″]

Presenter: O presenter age como intermediário entre a view e o model. Ele retira os dados do modelo e retorna para a view. Mas, diferente de típicos MVC, ele também decide o que acontece quando usuário interage com a view.

View: O view, normalmente implementado por uma Atividade (também pode ser um Fragmento ou qualquer elemento UI, dependendo da estrutura do aplicativo), vai conter uma referência para o presenter.  O presenter pode ser criado pela Activity ou fornecido via injeção de dependência. A única responsabilidade da View é chamar métodos no Presenter toda vez que o usuário interage com ela.

Model: O model é responsável pelos dados que serão exibidos na interface do usuário. Poderíamos considerar como modelo, além dos dados, qualquer lógica de manipulação e acesso destes dados.

As definições acima foram extraídas em livre interpretação do excelente artigo de Antonio Leiva.

[/expand]

Nosso maior objetivo na implementação deste padrão arquitetônico será criar a melhor separação de conceitos possível. Portanto vamos garantir que as camadas Model, View e Presenter estejam  devidamente isoladas. O Presenter funcionará como intermediário de todas as interações e as camadas View e Model não podem se comunicar diretamente.

Diagrama de ação do Model View Presenter

Vamos imaginar uma aplicação bastante simples, que permite ao usuário escrever notas em um diário. Basicamente o usuário entra estas notas, o app salva e as exibe. Se considerarmos que o aplicativo foi desenvolvido dentro do padrão MVP, quando traçamos a ação de inserir nova nota, chegamos ao seguinte diagrama.

Diagrama de Acão Model View Presenter
Diagrama de ação no padrão Model View Presenter (MVP)
  1. Usuário clica em “inserir nota”. View envia nova nota para Presenter getPresenter.novaNota(textoNota)
  2. Presenter cria uma nova Nota utilizando a String enviada e invoca em Model método responsável por inserir nota no DB getModel.insereNota(nota, this)
  3. Model insere nota no DB e informa ao Presenter sobre o sucesso utilizando o callback fornecido callback.onSuccess()
  4. Presenter processa o resultado de sucesso e requisita ao View a exibição de um Toast com uma mensagem de sucesso getView.showToast(msg)

Este mapeamento nos dá uma idéia melhor para o planejamento correto de nossas classes. O processo de comunicação definido acima poderia ser feito de diferentes maneiras: com acesso direto ao métodos do objeto, através de interface ou utilizando EventBus. Como nossa implementação preza pelo isolamento de conceitos e será desenvolvida com um código canónico, nós utilizaremos interfaces.

Diagrama de Classe Model View Presenter

Utilizando nosso mapeamento de ações, vamos construir nosso diagrama de classe para o padrão Model View Presenter. Faremos uma pequena alteração conceitual em nosso projeto. Criaremos uma interface com operação do Presenter permitidas para o Model, abrindo mão da necessidade de callbacks. Acredito que este caminho seja mais eficaz, mas há que diga que os callbacks permitiram um isolamento de conceitos ainda maior.

Diagrama de classe Model View Presenter (MVP)
Diagrama de Classe do padrão Model View Presenter (MVP)

 

  1. Presenter implementa interface PresenterOps
  2. View recebe referência de PresenterOps para acessar Presenter
  3. Model implementa interface ModelOps
  4. Presenter recebe referência de ModelOps para acessar Model
  5. Presenter implementa RequiredPresenterOps
  6. Model recebe referência de RequiredPresenterOps para acessar Presenter
  7. View implementa RequiredViewOps
  8. Presenter recebe referência de RequiredViewOps para acessar View

Implementando Model View Presenter no Android

Então mãos a obra! Começaremos definindo as operações de nosso aplicativo. Em nome da organização, vamos criar um interface guarda-chuva, que mantém todos métodos responsáveis pela comunicação entre as partes do padrão.

Obs: Como a implementação do padrão MVP já é complexa o suficiente, nenhuma função que não esteja diretamente conectada ao seu funcionamento não será desenvolvida. Tomarei como base que os leitores deste artigo já estão em um nível mais avançado de conhecimento do sdk android, portando a lógica fora do padrão MVP não será considerada.

Interface MainMVP

/*
 * Interface guarda-chuva do padrão MVP, agrega todas as operações de
 * comunicação entre os diferentes layer do padrão: MODEL, VIEW, PRESENTER
 */
public interface MainMVP {

    /**
     * Métodos obrigatórios em View, disponíveis para Presenter
     *      Presenter -> View
     */
    interface RequiredViewOps {
        void showToast(String msg);
        void showAlert(String msg);
        // qualquer outra operação na UI
    }

    /**
     * operações oferecidas ao layer View para comunicação com Presenter
     *      View -> Presenter
     */
    interface PresenterOps{
        void onConfigurationChanged(RequiredViewOps view);
        void onDestroy(boolean isChangingConfig);
        void novaNota(String textoNota);
        void deletaNota(Nota nota);
        // qualquer outra operação a ser chamada pelo View
    }

    /**
     * operações oferecidas pelo layer Presenter para comunicações com Model
     *      Model -> Presenter
     */
    interface RequiredPresenterOps {
        void onNotaInserida(Nota novaNota);
        void onNotaRemovida(Nota notaRemovida);
        void onError(String errorMsg);
        // qualquer operação de retorno Model -> Presenter
    }

    /**
     * operações oferecidos pelo layer Model para comunicações com Presenter
     *      Presenter -> Model
     */
    interface ModelOps {
        void insereNota(Nota nota);
        void removeNota(Nota nota);
        void onDestroy();
        // Qualquer operação referente à dados a ser chamado pelo Presenter
    }
}

Classe MainPresenter

public class MainPresenter
        implements MainMVP.RequiredPresenterOps, MainMVP.PresenterOps {

    // Referência para layer View
    private WeakReference<MainMVP.RequiredViewOps> mView;
    // Referência para o layer Model
    private MainMVP.ModelOps mModel;

    // Estado da mudança de configuração
    private boolean mIsChangingConfig;

    public MainPresenter(MainMVP.RequiredViewOps mView) {
        this.mView = new WeakReference<>(mView);
        this.mModel = new MainModel(this);
    }

    /**
     * Disparado por Activity após mudança de configuração
     * @param view  Referência para View
     */
    @Override
    public void onConfigurationChanged(MainMVP.RequiredViewOps view) {
        mView = new WeakReference<>(view);
    }

    /**
     * Recebe evento {@link MainActivity#onDestroy()}
     * @param isChangingConfig  Se está mudando de config
     */
    @Override
    public void onDestroy(boolean isChangingConfig) {
        mView = null;
        mIsChangingConfig = isChangingConfig;
        if ( !isChangingConfig ) {
            mModel.onDestroy();
        }
    }

    /**
     * Chamado por {@link MainActivity} com a
     * interação do usuário de pedido para inserção de
     * nova nota
     */
    @Override
    public void novaNota(String textoNota) {
        Nota nota = new Nota();
        nota.setText(textoNota);
        nota.setDate(getDate());
        mModel.insereNota(nota);
    }

    /**
     * Chamado por {@link MainActivity}, pedido
     * para remoção de nota
     */
    @Override
    public void deletaNota(Nota nota) {
        mModel.removeNota(nota);
    }

    /**
     * Recebe chamado de {@link MainModel} quando
     * Nota for inserida com sucesso no DB
     */
    @Override
    public void onNotaInserida(Nota novaNota) {
        mView.get().showToast("Novo registro " + novaNota.getDate());
    }

    /**
     * Recebe chamado de {@link MainModel} quando
     * Nota for removida do DB
     */
    @Override
    public void onNotaRemovida(Nota notaRemovida) {
        mView.get().showToast("Nota de " + notaRemovida.getDate() + " removida");
    }

    /**
     * Recebe eventuais error de modelo,
     * e repassa mensagem para usuário
     */
    @Override
    public void onError(String errorMsg) {
        mView.get().showAlert(errorMsg);
    }


    /**
     * Retorna data atual
     */
    private String getDate(){
       String hoje = "hoje";
        return hoje;
    }
}

Classe MainModel

public class MainModel
        implements MainMVP.ModelOps {

    // Referência para layer Presenter
    private MainMVP.RequiredPresenterOps mPresenter;

    public MainModel(MainMVP.RequiredPresenterOps mPresenter) {
        this.mPresenter = mPresenter;
    }

    /**
     * Disparada por {@link MainPresenter#onDestroy(boolean)}
     * para as operações necessárias que eventualmente
     * estiverem executando no BG
     */
    @Override
    public void onDestroy() {
        // ações para destruir objeto
    }

    // insere Nota no DB
    @Override
    public void insereNota(Nota nota) {
        // lógica de inserção
        // ...
        mPresenter.onNotaInserida(nota);
    }

    // remove Nota do DB
    @Override
    public void removeNota(Nota nota) {
        // lógica de remoção
        // ...
        mPresenter.onNotaRemovida(nota);
    }
}

Lidando com particularidades do Android

Em nossa visão do MVP o layer View é o responsável por criar o layer Presenter, que por sua vez cria o Model. Considerando que uma Activity recebe o papel de View, precisamos levar em consideração alguns detalhes característicos do ambiente Android, especialmente o ciclo de vida, que destrói e cria a atividade e todos os seus objetos durante mudanças de configuração.

Sendo assim, adicionamos um quarto elemento, o StateMaintainer, responsável por manter o estado das instância do PresenterModel durante as mudanças de configuração do device. Utilizaremos um fragmento que mantém seu estado, mesmo durante mudança de configurações, como base para este objeto. Vamos analisar uma simplificação das mudanças do ciclo de vida, no que diz respeito ao MVP.

Ciclo de vida de View (Activity)
Construção e reconstrução de objetos do MVP durante mudanças de ciclo de vida da Atividade
  1. Activity cria uma instância do Presenter, salvando uma referência de PresenterOps. A instância Presenter é salva no recém criado StateMaintainer
  2. Presenter recebe RequiredViewOps e cria uma nova instância de Model
  3. Model recebe RequiredPresenterOps
  4. Quando a Atividade está sendo destruída, informa ao Presenter sobre seu estado.
  5. Presenter processa informação, e toma ações necessárias em Model
  6. Activity recupera a referência do Presenter em StateMaintainer e informa ao Presenter de seu atual estado Ativo, repassando RequiredViewOps

Classe StateMaintainer

Esta implementação de StateMaintainer pode ser utilizada para armazenar estado de qualquer objeto. Como o código está minimizado , pois é muito extenso. Para visualizar clique na seta.

public class StateMaintainer {
    protected final String TAG = getClass().getSimpleName();

    private final String mStateMaintenerTag;
    private final WeakReference<FragmentManager> mFragmentManager;
    private StateMngFragment mStateMaintainerFrag;

    /**
     * Construtor
     * @param fragmentManager       repassa uma referência do FragmentManager
     * @param stateMaintainerTAG      a TAG utilizada para inserir o fragmento responsável
     *                              por manter os objetos "vivos"
     */
    public StateMaintainer(FragmentManager fragmentManager, String stateMaintainerTAG) {
        mFragmentManager = new WeakReference<>(fragmentManager);
        mStateMaintenerTag = stateMaintainerTAG;
    }

    /**
     * cria o fragmento responsável por armazenar o objetos
     * @return  true: criou o framentos e rodou pela primeira vez
     *          false: o objeto já foi criado, portanto é apenas recuperado
     */
    public boolean firstTimeIn() {
        try {
            // Recuperando referência
            mStateMaintainerFrag = (StateMngFragment)
                    mFragmentManager.get().findFragmentByTag(mStateMaintenerTag);

            // Criando novo RetainedFragment
            if (mStateMaintainerFrag == null) {
                Log.d(TAG, "Criando novo RetainedFragment " + mStateMaintenerTag);
                mStateMaintainerFrag = new StateMngFragment();
                mFragmentManager.get().beginTransaction()
                        .add(mStateMaintainerFrag, mStateMaintenerTag).commit();
                return true;
            } else {
                Log.d(TAG, "Retornando retained fragment existente " + mStateMaintenerTag);
                return false;
            }
        } catch (NullPointerException e) {
            Log.w(TAG, "Erro firstTimeIn()");
            return false;
        }
    }


    /**
     * Insere objeto a serem presenrvados durante mudanças de configuração
     * @param key   TAG de referência para recuperação do objeto
     * @param obj   Objeto a ser mantido
     */
    public void put(String key, Object obj) {
        mStateMaintainerFrag.put(key, obj);
    }

    /**
     * Insere objeto a serem presenrvados durante mudanças de configuração.
     * Utiliza a classe do Objeto como referência futura.
     * Só deve ser utilizado somente uma vez por classe, caso contrário haverá
     * possíveis conflitos na recuperação dos dados
     * @param obj   Objeto a ser mantido
     */
    public void put(Object obj) {
        put(obj.getClass().getName(), obj);
    }


    /**
     * Recupera o objeto salvo
     * @param key   Chave de referência do obj
     * @param <T>   tipo genérico de retorno
     * @return      Objeto armazenado
     */
    @SuppressWarnings("unchecked")
    public <T> T get(String key)  {
        return mStateMaintainerFrag.get(key);

    }

    /**
     * Verifica a existência de um objeto com a chave fornecida
     * @param key   Chave para verificação
     * @return      true: obj existe
     *              false: obj insexistente
     */
    public boolean hasKey(String key) {
        return mStateMaintainerFrag.get(key) != null;
    }


    /**
     * Armazena e administra os objetos que devem ser preservados
     * durante mudanças de configuração.
     * É instanciado somente uma vez e utiliza um
     * <code>HashMap</code> para salvar os objetos e suas
     * chaves de referência.
     */
    public static class StateMngFragment extends Fragment {
        private HashMap<String, Object> mData = new HashMap<>();

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            // Garante que o Fragmento será preservado
            // durante mudanças de configuração
            setRetainInstance(true);
        }

        /**
         * Insere objetos no hashmap
         * @param key   Chave de referência
         * @param obj   Objeto a ser salvo
         */
        public void put(String key, Object obj) {
            mData.put(key, obj);
        }

        /**
         * Insere objeto utilizando o nome da classe como referência
         * @param object    Objeto a ser salvo
         */
        public void put(Object object) {
            put(object.getClass().getName(), object);
        }

        /**
         * Recupera objeto salvo no hashmap
         * @param key   Chave de referência
         * @param <T>   Classe
         * @return      Objeto salvo
         */
        @SuppressWarnings("unchecked")
        public <T> T get(String key) {
            return (T) mData.get(key);
        }
    }

}

Class MainActivity (View layer)

Vamos finalizar com a classe MainActivity, responsável pela criação dos demais objetos do layer Model View Presenter.

public class MainActivity extends AppCompatActivity
        implements MainMVP.RequiredViewOps {

    protected final String TAG = getClass().getSimpleName();

    // Responsável por manter estado dos objetos inscritos
    // durante mudanças de configuração
    private final StateMaintainer mStateMaintainer =
            new StateMaintainer( this.getFragmentManager(), TAG );

    // Operações no Presenter
    private MainMVP.PresenterOps mPresenter;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        startMVPOps();
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
    }


    /**
     * Inicia e reinicia o Presenter. Este método precisa ser chamado
     * após {@link Activity#onCreate(Bundle)}
     */
    public void startMVPOps() {
        try {
            if ( mStateMaintainer.firstTimeIn() ) {
                Log.d(TAG, "onCreate() chamado pela primera vez");
                initialize(this);
            } else {
                Log.d(TAG, "onCreate() chamado mais de uma vez");
                reinitialize(this);
            }
        } catch ( InstantiationException | IllegalAccessException e ) {
            Log.d(TAG, "onCreate() " + e );
            throw new RuntimeException( e );
        }
    }


    /**
     * Inicializa os objetos relevantes para o MVP.
     * Cria uma instância do Presenter, salva o presenter
     * no {@link StateMaintainer} e informa à instância do
     * presenter que objeto foi criado.
     * @param view      Operações no View exigidas pelo Presenter
     */
    private void initialize( MainMVP.RequiredViewOps view )
            throws InstantiationException, IllegalAccessException{
        mPresenter = new MainPresenter(view);
        mStateMaintainer.put(MainMVP.PresenterOps.class.getSimpleName(), mPresenter);
    }

    /**
     * Recupera o presenter e informa à instância que houve uma mudança
     * de configuração no View.
     * Caso o presenter tenha sido perdido, uma nova instância é criada
     */
    private void reinitialize( MainMVP.RequiredViewOps view)
            throws InstantiationException, IllegalAccessException {
        mPresenter = mStateMaintainer.get( MainMVP.PresenterOps.class.getSimpleName() );

        if ( mPresenter == null ) {
            Log.w(TAG, "recriando o Presenter");
            initialize( view );
        } else {
            mPresenter.onConfigurationChanged( view );
        }
    }


    // Exibe AlertDialog
    @Override
    public void showAlert(String msg) {
        // show alert Box
    }

    // Exibe Toast
    @Override
    public void showToast(String msg) {
        Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show;
    }
}

Cheque o projeto do tutorial completo no github

O que vem pela frente

Sei que este post ficou meio grande, mas espero que tenha ajudado alguém por aí! No próximo texto desta série discutiremos como utilizar o framework final, que possui algumas abstrações que facilitam bastante a implementação do MVP, mas que podem confundir um pouco em uma primeira análise.

Até a próxima!

 


Also published on Medium.

3 thoughts on “Model View Presenter (MVP) no Android, Parte 2”

  1. Excelente artigo!

    So um reparo, no código de exemplo não aparece o caractere “>” ou “<", por exemplo na classe "StateMngFragment" o método: "public <T> T get(String key) ".

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *