MVP Android

Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog. Você receberá um email de confirmação. Somente depois de confirma-lo é que poderei lhe enviar os conteúdos exclusivos.

Email inválido.
Blog /Android /MVP Android

MVP Android

Vinícius Thiengo27/01/2017
(4616) (28) (353) (59)
Go-ahead
"Lembremo-nos de que nosso único limite é aquele que fixamos em nossa mente."
Napoleon Hill
Receitas Android
Capa do livro Receitas Para Desenvolvedores Android
TítuloReceitas Para Desenvolvedores Android
CategoriaDesenvolvimento Android
AutorVinícius Thiengo
Edição
Ano2017
Capítulos20
Páginas934
Acessar Livro
Código limpo
Capa do livro Refatorando Para Programas Limpos
TítuloRefatorando Para Programas Limpos
CategoriaEngenharia de Software
AutorVinícius Thiengo
Edição
Ano2017
Capítulos46
Páginas598
Acessar Livro
Conteúdo Exclusivo
Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog.
Email inválido

Opa, blz?

Neste artigo vamos ao estudo e aplicação do padrão de arquitetura Model-View-Presenter (MVP), padrão muito similar ao Model-View-Controller (MVC). Este último que provavelmente você já conhece, principalmente se for também um desenvolvedor Web backend.

No artigo, além da teoria para entendimento e importância do MVP, vamos a construção, refatoração, de um projeto de motocicletas Rod Style. Vamos iniciar com o estudo de todo o código e diagramas e logo depois vamos corrigir a arquitetura do projeto aplicando o MVP, pois a inicial estará "embaralhada" e ruim para uma evolução limpa do aplicativo.

Note que apesar do MVP ter se tornado mais conhecido recentemente devido a aplicação mais adequada a APPs mobile, digo, mais adequada em frente ao uso do conhecido MVC, o MVP veio bem antes do desenvolvimento mobile que temos hoje.

Vamos aos tópicos. Mais sobre o MVP você terá no decorrer do conteúdo. Segue:

Separação de conceitos

O termo Separação de Conceitos, a princípio, foi cunhado primeiramente por Edsger Wybe Dijkstra. Dijkstra foi pioneiro em várias áreas da computação, porém o pensamento dele sobre a separação de conceitos foi um tanto quanto geral, ou seja, algo que é uma escolha inteligente independente se estarmos ou não falando de computação.

Resumidamente Dijkstra disse que o estudo isolado de um aspecto, um conceito, mesmo quando sabendo da importância dos outros e ainda assim os mantendo como irrelevantes para o conceito atual em estudo, esse comportamento é um comportamento inteligente, pois assim é possível ir a fundo no assunto, aspecto, e decifra-lo, resolve-lo, da melhor maneira possível.

Dijkstra citou isso ainda na década de 70, naquela época os softwares já tinham problemas em serem "amontoados". Separação de conceitos foi o ponta-pé inicial para os estudos da construção de softwares em camadas.

Arquitetura multicamadas

O assunto "arquitetura em multicamadas" não tem uma definição explicita de quais são as camadas que certamente devem ter em um software, somente que é uma melhor opção defini-lo, o software, em camadas, nem mesmo o número mínimo e máximo é seguro informar.

Com a divisão do software em layers nós programadores damos o primeiro passo na direção de um software de arquitetura limpa.

Alias, arquitetura limpa em aplicativos Android é algo muito maior do que somente a utilização do MVP. Esse assunto já vem sendo discutido a algum tempo e há excelentes artigos sobre.

A seguir um exemplo comum de arquitetura multicamadas, a arquitetura de um Web site:

Com o software em camadas nós conseguimos melhorar em muito a reutilização de código e manutenção e evolução dele. Ressaltando que o trabalho de softwares em camadas é algo que complementa a estratégia de código limpo juntamente com técnicas de escrita limpa de algoritmos e com o uso de padrões de projeto.

Importante notar que o desenvolvimento em camadas não é uma exclusividade da programação orientada a objetos, qualquer paradigma de desenvolvimento pode trabalhar nessa linha.

Outro ponto importante é que as camadas mais acima na arquitetura tendem a ser dependentes das camadas abaixo, sempre: ou somente da próxima camada abaixo ou da próxima e de mais algumas.

Assim podemos prosseguir a dois conhecidos e muito utilizados padrões de arquitetura: MVC (somente para relembrar) e MVP.

Model-View-Controller (MVC)

MVC é um padrão de arquitetura e provavelmente um dos padrões da computação mais conhecidos, isso ao lado do Singleton e do Factory. Note que um padrão de arquitetura tende a ser definido antes do início da codificação, tanto que não é comum encontrarmos métodos de refatoração para esse tipo de padrão.

O MVC foi criado com o objetivo de dividir as camadas que estavam fortemente acopladas a camada de visualização de softwares gráficos, ou seja, veio para resolver um problema com a interface de usuário.

Ele é bem simples e tive de aborda-lo aqui para você ter ao menos o conhecimento básico do padrão de origem do MVP. Abaixo o diagrama comum do MVC:

Vamos as camadas: 

  • View (Visualização): contém as entidades gráficas, entidades que permitem a saída de dados (tendo como origem o Model ou o Controller) e a entrada de dados (usuário);
  • Controller (Controlador): camada responsável por aceitar as entradas de dados (View) e também as saídas (Model) e trabalha-las para que estas cheguem corretamente as camadas de destino;
  • Model (Modelo): responsável pela lógica e domínio do problema, incluindo a manipulação de dados, mesmo que em seu princípio esse último aspecto não era tratado, persistência de dados.

Quando se tratando de padrão de arquitetura, mais precisamente do padrão MVC, o que temos na figura acima é apenas uma representação dele, pois é possível e viável algumas vezes, balancearmos a lógica entre o Model e o Controller, por exemplo, para melhorar a comunicação entre as camadas ou até mesmo para obter maior performance sem sair do padrão de arquitetura escolhido.

Eu particularmente evito ao máximo o uso do MVC com a característica onde a camada de visualização possa realizar invocações diretas a entidades da camada de modelo, mesmo com essa última camada podendo enviar dados formatados diretamente a camada View.

Com isso podemos seguir com o MVP, o derivado do MVC.

Model-View-Presenter (MVP)

Primeiro, não assuma que o MVP existe devido a uma adaptação necessária do MVC ao Android, pois o MVP é muito anterior ao Android, mais precisamente, da década de 90, onde a união Apple, HP e IBM fez com esse padrão de arquitetura surgisse devido a necessidades que o MVC não cobria.

Alguns conteúdos que consumi informavam que o MVP não era um padrão de arquitetura, você provavelmente pode encontrar isso também.

Mas baseando-se que ele tem como origem o MVC, que nosso conhecimento tácito nos permite o correto entendimento do que é um padrão de arquitetura e que algumas fontes mais seguras confirmam ele como padrão de arquitetura... partindo disso acredito ser seguro assumi-lo como também sendo um architecture pattern.

O MVP permite uma divisão melhor em camadas quando o assunto é isolar camadas superiores de camadas inferiores, mais precisamente, permitir que a camada diretamente relacionada com a interface do usuário somente se comunique (seja dependente) com a camada diretamente abaixo dela, aqui a camada Presenter. Segue diagrama:

A seguir a definição das camadas:

  • View (Visualização): como no MVC, responde a saída e entrada de dados, porém a saída vem do Presenter, a entrada normalmente vem do usuário;
  • Presenter (Apresentador): Camada responsável por responder as invocações da camada de visualização e invocações da camada de modelo, além de também poder invocar ambas as camadas. O Presenter trabalha a formatação dos dados que entram em ambas as camadas paralelas e também pode incluir parte da lógica de negócio que alguns programadores podem pensar que deveria estar somente na camada de modelo;
  • Model (Modelo): camada fornecedora de dados além de conter a lógica de negócio do domínio do problema.

A maior diferença entre o MVP e o MVC é a não possibilidade de comunicação direta entre a camada View e a camada Model. Mesmo que com o objetivo de dividir o software em camadas, há um problema imenso na evolução do software quando essas camadas podem se comunicar diretamente.

Porém, como acontece com o MVC, no MVP também podemos balancear as coisas. Parte da lógica de negócio pode sim ser movida para a camada Presenter. Alias, vamos fazer isso aqui no projeto de exemplo.

O que recomendo é utilizar lógica sempre abaixo da camada de visualização, mesmo sabendo que seu balanceamento na aplicação do MVP, em seu software, pode induzi-lo a colocar ao menos alguns trechos de lógica na camada de visualização. Evite isso.

Agora podemos realmente prosseguir com o projeto de exemplo, assim o entendimento do MVP, em algoritmos de aplicativos em produção, será facilitado.

Mas antes saiba que escolhi apresentar todo esse conteúdo introdutório para que você tenha ciência da importância da definição de uma arquitetura para seu software. O MVP é literalmente a "ponta do iceberg", há inúmeros outros padrões de arquitetura.

Não memorize a importância de uma arquitetura em camadas, entenda ela.

Manter seu software em camadas não somente vai melhorar em muito seus algoritmos e a evolução desses, como também vai lhe induzir a construir códigos que qualquer outro programador consiga continua-lo com o mínimo esforço possível.

Projeto de exemplo, backend Web

O projeto de exemplo apresentado aqui envolve também um backend Web. Este está dividido em camadas, porém não vou afirmar que estou utilizando alguns dos conhecidos padrões (MVC, MVP, MVA, MVVM, ...), pois somente o dividi para melhorar seu entendimento.

Configurações e estrutura

Abaixo a configuração do backend:

  • Apache 2.2.29;
  • PHP 5.6.2;
  • MySQL 5.5.38.

A seguir a imagem da estrutura dos pacotes:

Note que os pacotes de um projeto de software podem ou não estarem refletindo a arquitetura sendo utilizada em código.

Na imagem acima os pacotes /apl, /ctrl e /domain representam a camada de domínio do problema. O pacote /data a camada de dados.

Essa é a divisão do backend, porém fique a vontade para implementar o seu próprio, desde que ele permita as mesmas funcionalidades que estaremos trabalhando aqui: carregamento e atualização de dados.

Caso queira acesso ao código do backend Web sem explicações, entre no GitHub dele em: https://github.com/viniciusthiengo/rod-motors-web.

Banco de dados JSON e classe de gerência de dados

Para facilitar o máximo possível, o banco de dados em uso é um arquivo JSON com dados no mesmo formato. Segue configuração do arquivo /data/motos.json:

[
{
"id": 1,
"modelo": "V-Rod",
"marca": "Harley-Davidson",
"imagem": "http://motordream.uol.com.br/upload/noticia/13895757499537.jpg",
"ehFavorito": false
},
{
"id": 2,
"modelo": "IRON 883",
"marca": "Harley-Davidson",
"imagem": "http://www.motoriorentals.com.br/es/media/k2/items/cache/6f43b5263fbba79c5962514b85d34738_L.jpg",
"ehFavorito": true
},
{
"id": 3,
"modelo": "1200 CUSTOM CB",
"marca": "Harley-Davidson",
"imagem": "http://www.moto.com.br/img/2014/03/12/img75160-1394633809-v580x435.jpg",
"ehFavorito": false
},
{
"id": 4,
"modelo": "FORTY-EIGHT",
"marca": "Harley-Davidson",
"imagem": "http://www.memoriamotor.r7.com/wp-content/uploads/2015/06/Forty-Eight.jpg",
"ehFavorito": false
},
{
"id": 5,
"modelo": "ROADSTER",
"marca": "Harley-Davidson",
"imagem": "http://motorcycle.com.vsassets.com/blog/wp-content/uploads/2016/05/050316-Harley-Davidson-Roadster-1-9.jpg",
"ehFavorito": false
},
{
"id": 6,
"modelo": "STREET BOB",
"marca": "Harley-Davidson",
"imagem": "https://i.ytimg.com/vi/bYHiaKMpSaE/maxresdefault.jpg",
"ehFavorito": false
},
{
"id": 7,
"modelo": "V Star 1300",
"marca": "Yamaha",
"imagem": "https://cloud.yamahamotorsports.com/library/img.jpg?id=26284&w=840",
"ehFavorito": false
},
{
"id": 8,
"modelo": "Bolt R-Spec",
"marca": "Yamaha",
"imagem": "http://s1.cdn.autoevolution.com/images/moto_gallery/YAMAHABoltR-4399_10.jpg",
"ehFavorito": false
},
{
"id": 9,
"modelo": "V Star 950 Tourer",
"marca": "Yamaha",
"imagem": "http://moto.zombdrive.com/images/yamaha-v-star-950-tourer-1.jpg",
"ehFavorito": false
}
]

 

Somente para ter o conteúdo backend completo também aqui, será apresentado o criar-json-database.php. Isso, pois somente o conteúdo acima é o suficiente para você seguir o exemplo em seu ambiente de desenvolvimento:

include 'Database.php';
include '../domain/Moto.php';

$motos = [];

$moto = new Moto();
$moto->setId( 1 );
$moto->setMarca('Harley-Davidson');
$moto->setModelo('V-Rod');
$moto->setImagem('http://motordream.uol.com.br/upload/noticia/13895757499537.jpg');
$moto->setEhFavorito( false );
$motos[] = $moto;

$moto = new Moto();
$moto->setId( 2 );
$moto->setMarca('Harley-Davidson');
$moto->setModelo('IRON 883');
$moto->setImagem('http://www.motoriorentals.com.br/es/media/k2/items/cache/6f43b5263fbba79c5962514b85d34738_L.jpg');
$moto->setEhFavorito( false );
$motos[] = $moto;

$moto = new Moto();
$moto->setId( 3 );
$moto->setMarca('Harley-Davidson');
$moto->setModelo('1200 CUSTOM CB');
$moto->setImagem('http://www.moto.com.br/img/2014/03/12/img75160-1394633809-v580x435.jpg');
$moto->setEhFavorito( false );
$motos[] = $moto;

$moto = new Moto();
$moto->setId( 4 );
$moto->setMarca('Harley-Davidson');
$moto->setModelo('FORTY-EIGHT');
$moto->setImagem('http://www.memoriamotor.r7.com/wp-content/uploads/2015/06/Forty-Eight.jpg');
$moto->setEhFavorito( false );
$motos[] = $moto;

$moto = new Moto();
$moto->setId( 5 );
$moto->setMarca('Harley-Davidson');
$moto->setModelo('ROADSTER');
$moto->setImagem('http://motorcycle.com.vsassets.com/blog/wp-content/uploads/2016/05/050316-Harley-Davidson-Roadster-1-9.jpg');
$moto->setEhFavorito( false );
$motos[] = $moto;

$moto = new Moto();
$moto->setId( 6 );
$moto->setMarca('Harley-Davidson');
$moto->setModelo('STREET BOB');
$moto->setImagem('https://i.ytimg.com/vi/bYHiaKMpSaE/maxresdefault.jpg');
$moto->setEhFavorito( false );
$motos[] = $moto;

$moto = new Moto();
$moto->setId( 7 );
$moto->setMarca('Yamaha');
$moto->setModelo('V Star 1300');
$moto->setImagem('https://cloud.yamahamotorsports.com/library/img.jpg?id=26284&w=840');
$moto->setEhFavorito( false );
$motos[] = $moto;

$moto = new Moto();
$moto->setId( 8 );
$moto->setMarca('Yamaha');
$moto->setModelo('Bolt R-Spec');
$moto->setImagem('http://s1.cdn.autoevolution.com/images/moto_gallery/YAMAHABoltR-4399_10.jpg');
$moto->setEhFavorito( false );
$motos[] = $moto;

$moto = new Moto();
$moto->setId( 9 );
$moto->setMarca('Yamaha');
$moto->setModelo('V Star 950 Tourer');
$moto->setImagem('http://moto.zombdrive.com/images/yamaha-v-star-950-tourer-1.jpg');
$moto->setEhFavorito( false );
$motos[] = $moto;

Database::saveDatabase( 'motos.json', $motos );

 

Assim segue código da classe Database, classe que permitirá o acesso aos dados da base JSON:

class Database
{
public static function saveDatabase( $database, $motos ){
$database = fopen( $database, 'w' );
fwrite( $database, json_encode($motos) );
fclose( $database );
}

public static function getMotos( $database ){
$dadosString = file_get_contents( $database );
$motos = json_decode($dadosString);
return $motos;
}
}

 

Sempre trabalhando com manipulação de arquivos, algo que é simples com o PHP.

Classes de domínio

Com as classes de domínio vamos iniciar com a que tem uma representação equivalente no código Android, a classe Moto:

class Moto
{
public $id;
public $modelo;
public $marca;
public $imagem;
public $ehFavorito;

public function getId()
{
return $this->id;
}
public function setId($id)
{
$this->id = $id;
}

public function setModelo($modelo)
{
$this->modelo = $modelo;
}

public function setMarca($marca)
{
$this->marca = $marca;
}

public function setImagem($imagem)
{
$this->imagem = $imagem;
}

public function getEhFavorito()
{
return $this->ehFavorito;
}
public function setEhFavorito($ehFavorito)
{
if( $ehFavorito === 'true' || $ehFavorito === true || $ehFavorito === 1 ){
$this->ehFavorito = true;
}
else{
$this->ehFavorito = false;
}
}
}

 

Note que não há todos os getters e setters, pois não precisamos de todos. Incluindo a isso que todas as variáveis de instância são públicas. Essa é uma estratégia para facilitar o uso do método nativo json_encode().

Agora a classe AplMoto, responsável por diretamente comunicar com a camada de gerência de dados:

include '../data/Database.php';

class AplMoto
{
public static function getMotosComoJson(){
$motos = Database::getMotos( '../data/motos.json' );
return json_encode( $motos );
}

public static function updateFavoritoMoto( Moto $moto ){
$motos = Database::getMotos( '../data/motos.json' );

foreach( $motos as $m ){
if( $m->id == $moto->getId() ){
$m->ehFavorito = $moto->getEhFavorito();
Database::saveDatabase( '../data/motos.json', $motos );
break;
}
}
return json_encode( $moto );
}
}

 

Dependendo de como você definir sua estrutura backend, terá de alterar o caminho de acesso ao banco de dados JSON.

Assim a classe que permite requisições Android ao lado Web, CtrlMoto:

/*
* Caso queira encontrar alguns erros em sua aplicação backend,
* descomente a linha abaixo.
* */
/* ini_set('display_errors', 1); */

include '../apl/AplMoto.php';
include '../domain/Moto.php';

/*
* A superglobal GET é para quando estiver realizando testes pelo navegador
* para não precisar configurar toda a APP para simples testes no backend.
* */
$dados = isset($_POST['metodo']) ? $_POST : $_GET;

if( strcasecmp( $dados['metodo'], 'get-motos' ) == 0 ){
sleep(2);
$motosJson = AplMoto::getMotosComoJson();
echo $motosJson;
}

else if( strcasecmp( $dados['metodo'], 'update-favorito-moto' ) == 0 ){
$moto = new Moto();
$moto->setId( $dados['id'] );
$moto->setEhFavorito( $dados['eh-favorito'] );

$motoJson = AplMoto::updateFavoritoMoto( $moto );
echo $motoJson;
}

 

Coloquei o trecho com a superglobal $_GET, pois em outros artigos que trabalhei também com o lado Web em PHP alguns seguidores não conseguiram entender o que estava de errado na versão deles, isso com os testes acontecendo diretamente com invocações do código Android (para testes em backend... desnecessário).

Códigos pequenos, como o do projeto aqui, podem ser testados diretamente do navegador, é simples e lhe ajuda a solucionar rápido os problemas.

Bom, o código acima acredito estar autocomentado. Logo, podemos prosseguir para a versão sem MVP do projeto Android.

Alias, será somente no lado Android do software que estaremos aplicando o MVP, pois a divisão em camadas atual no backend Web já atende bem aos requisitos de arquitetura limpa de nosso projeto.

Projeto de exemplo, Android

Inicie criando um novo projeto no Android Studio. Aqui vamos seguir com o nome "Rod Motos". Crie com uma "Basic Activity" caso queira acompanhar o exemplo a risca.

Note que vamos seguir primeiro com a versão sem o MVP, para depois apresentar o que seria o MVP no Android e então as atualizações necessárias no projeto.

Caso já queira acesso completo ao projeto com o MVP implementado, entre no GitHub dele em: https://github.com/viniciusthiengo/rod-motors.

Configurações Gradle

A seguir a configuração do Gradle Top Level ou build.gradle (Project RodMotors):

buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.3'
}
}

allprojects {
repositories {
jcenter()
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}

 

A configuração acima é a padrão de um novo projeto no Android Studio. A versão do Gradle que sofreu alteração foi à seguir, build.gradle (Module: app):

apply plugin: 'com.android.application'

android {
compileSdkVersion 25
buildToolsVersion "24.0.3"
defaultConfig {
applicationId "br.com.thiengo.rodmotors"
minSdkVersion 10
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:25.1.0'
compile 'com.android.support:design:25.1.0'
testCompile 'junit:junit:4.12'
compile 'com.squareup.picasso:picasso:2.5.2' /* PARA CARREGAMENTO DE IMAGENS REMOTAS */
compile 'com.loopj.android:android-async-http:1.4.9' /* PARA COMUNICAÇÃO COM BACKEND WEB */
compile 'com.google.code.gson:gson:2.7' /* PARA O PARSER JSON */
}

 

As libraries a mais que colocamos no projeto foram explicadas em comentários no código acima.

Note que estamos utilizando a library com.loopj.android:android-async-http por recomendações de um dos seguidores do Blog, mais precisamente o Marcos André.

Library muito simples e tão robusta quanto o Retrofit que recomendo frequentemente. Minha única preocupação com ela é o quase um ano sem atualizações, mesmo com mais de 9K estrelas no GitHub. De qualquer forma, aqui funcionou sem problemas.

Para saber mais sobre essa library, entre em Android Asynchronous Http Client - James Smith.

Configurações AndroidManifest

A configuração do manifest é simples, segue:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="br.com.thiengo.rodmotors">

<uses-permission android:name="android.permission.INTERNET" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">

<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

 

Não mais voltaremos ao AndroidManifest.xml e nem aos arquivos gradle, pois a versão MVP que estaremos implementando é sem suporte de library externa, é literalmente na unha, pois como acompanhei, e concordei, em alguns artigos: essa é melhor maneira de entender o MVP no Android.

Configurações de estilo

Com os arquivos XML de estilo do projeto, podemos começar com o que defini as cores do template, /values/color.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#9E9E9E</color>
<color name="colorPrimaryDark">#616161</color>
<color name="colorAccent">#607D8B</color>
</resources>

 

Então o simples /values/strings.xml:

<resources>
<string name="app_name">Rod Motos</string>
</resources>

 

O /values/styles.xml:

<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>

<item name="android:windowBackground">@drawable/background</item>
</style>

<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>

<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />

<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources>

 

E por fim o /values-21/styles.xml para versões de Android API acima da API 20:

<resources>
<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:windowBackground">@drawable/background</item>
</style>
</resources>

Classes do domínio do problema

Como classes do domínio do problema vamos ter, na arquitetura atual do projeto, a classe Moto e a classe MotosAdapter.

A MainActivity também entraria aqui, pois as camadas todas do aplicativo estão fortemente acopladas, porém, devido ao tamanho, vamos trata-la em uma seção única a ela.

Assim segue código da classe Moto:

public class Moto implements Parcelable {
public static final String ID_KEY = "id";
public static final String EH_FAVORITO_KEY = "eh-favorito";

private int id;
private String modelo;
private String marca;
private String imagem;
private boolean ehFavorito;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getModelo() {
return modelo;
}

public void setModelo(String modelo) {
this.modelo = modelo;
}

public String getMarca() {
return marca;
}

public void setMarca(String marca) {
this.marca = marca;
}

public String getImagem() {
return imagem;
}

public void setImagem(String imagem) {
this.imagem = imagem;
}

public boolean isEhFavorito() {
return ehFavorito;
}

public void setEhFavorito(boolean ehFavorito) {
this.ehFavorito = ehFavorito;
}

@Override
public int describeContents() {
return 0;
}

@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.id);
dest.writeString(this.modelo);
dest.writeString(this.marca);
dest.writeString(this.imagem);
dest.writeByte(this.ehFavorito ? (byte) 1 : (byte) 0);
}

public Moto() {
}

protected Moto(Parcel in) {
this.id = in.readInt();
this.modelo = in.readString();
this.marca = in.readString();
this.imagem = in.readString();
this.ehFavorito = in.readByte() != 0;
}

public static final Parcelable.Creator<Moto> CREATOR = new Parcelable.Creator<Moto>() {
@Override
public Moto createFromParcel(Parcel source) {
return new Moto(source);
}

@Override
public Moto[] newArray(int size) {
return new Moto[size];
}
};
}

 

A implementação do Parcelable foi necessária devido ao uso, na MainActivity, do onSaveInstanceState().

Agora a classe MotosAdapter:

public class MotosAdapter extends RecyclerView.Adapter<MotosAdapter.ViewHolder> {
private MainActivity activity;
private ArrayList<Moto> motos;

public MotosAdapter( MainActivity activity, ArrayList<Moto> motos ){
this.activity = activity;
this.motos = motos;
}

@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater
.from( parent.getContext() )
.inflate(R.layout.item_moto, parent, false);
ViewHolder viewHolder = new ViewHolder( view );
return viewHolder;
}

@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.setDados( motos.get( position ) );
}

@Override
public int getItemCount() {
return motos.size();
}


class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
private ImageView ivMoto;
private ImageView ivFavorito;
private TextView tvModelo;

private ViewHolder(View itemView) {
super(itemView);

ivMoto = (ImageView) itemView.findViewById(R.id.iv_moto);
ivFavorito = (ImageView) itemView.findViewById(R.id.iv_favorito);
tvModelo = (TextView) itemView.findViewById(R.id.tv_modelo);
ivFavorito.setOnClickListener( this );
}

private void setDados( Moto moto ){
Picasso.with( ivMoto.getContext() )
.load( moto.getImagem() )
.into( ivMoto );

tvModelo.setText( moto.getModelo() );

if( moto.isEhFavorito() ){
ivFavorito.setImageResource( R.drawable.ic_favorito_marcado );
}
else{
ivFavorito.setImageResource( R.drawable.ic_favorito );
}
}

@Override
public void onClick(View view) {
Moto m = motos.get( getAdapterPosition() );
m.setEhFavorito( !m.isEhFavorito() );
activity.updateEhFavoritoMoto( m );
}
}
}

 

Note que em setDados() e em onClick() há lógica de negócio, fazendo com que a classe MotosAdapter atrapalhe a arquitetura limpa onde a camada de visualização se preocupa somente com a apresentação dos dados, sem uso de lógica.

Digo isso, pois classes adapter juntamente com Activities, Fragments e Dialogs (foram as que lembrei aqui) formariam a camada de visualização de uma arquitetura Android bem dividida.

Caso você tenha ficado um pouco confuso com o código do adapter anterior, ele na verdade faz parte de um conjunto de códigos para o funcionamento de um RecyclerView. Sendo assim, depois deste artigo recomendo que você estude os indicados abaixo:

Antes de prosseguir vamos ao XML de item_moto.xml que é referenciado em MotosAdapter:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/content_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="2dp">

<ImageView
android:id="@+id/iv_moto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentTop="true"
android:scaleType="center" />

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/iv_moto"
android:layout_marginTop="2dp"
android:background="#888"
android:padding="5dp">

<TextView
android:id="@+id/tv_modelo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_toLeftOf="@+id/iv_favorito"
android:layout_toStartOf="@+id/iv_favorito"
android:ellipsize="end"
android:maxLines="1"
android:paddingTop="2dp"
android:textColor="@android:color/white" />

<ImageView
android:id="@+id/iv_favorito"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_marginEnd="2dp"
android:layout_marginRight="2dp"
android:src="@drawable/ic_favorito" />
</RelativeLayout>
</RelativeLayout>

 

E então o diagrama do layout anterior:

Pacote de conexão remota

Abaixo a classe de conexão que utiliza em maioria códigos da library AsyncHttp. Segue JsonHttpRequest:

public class JsonHttpRequest extends JsonHttpResponseHandler {
public static final String URI = "http://SeuEnderecoIPInterno:8888/rod-motors/ctrl/CtrlMoto.php";
public static final String METODO_KEY = "metodo";

private WeakReference<MainActivity> activity;

public JsonHttpRequest( MainActivity activity ){
this.activity = new WeakReference<>(activity);
}

@Override
public void onStart() {
activity.get().showProgressBar( true );
}

@Override
public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
Gson gson = new Gson();
Moto m = gson.fromJson( response.toString(), Moto.class );
activity.get().updateItemRecycler( m );
}

@Override
public void onSuccess(int statusCode, Header[] headers, JSONArray response) {
Gson gson = new Gson();
ArrayList<Moto> motos = new ArrayList<>();
Moto m;

for( int i = 0; i < response.length(); i++ ){
try{
m = gson.fromJson( response.getJSONObject( i ).toString(), Moto.class );
motos.add( m );
}
catch(JSONException e){}
}
activity.get().updateListaRecycler( motos );
}

@Override
public void onFailure(int statusCode, Header[] headers, String responseString, Throwable throwable) {
activity.get().showToast( responseString );
}

@Override
public void onFinish() {
activity.get().showProgressBar( false );
}
}

 

Algo a se notar é que a library AsyncHttp já realiza o trabalho pesado de realizar o acionamento do método onSuccess() correto de acordo com o retorno do backend. Isso sem precisar de Interfaces ou classes de gerenciamento a mais.

Em ambos os métodos utilizamos o Gson para o parser JSON.

Todos os métodos, não somente os onSuccess(), serão invocados na Thread que iniciou a conexão, em nosso caso será sempre a UI Thread.

O WeakReference está sendo utilizado para que não tenhamos problemas com vazamento de memória. Isso, pois, já lhe adianto, não estamos trabalhando a recuperação de objetos na MainActivity, vamos aplicar essa tarefa, opcional, na implementação do MVP.

Configurações da atividade principal

Como essa primeira parte é somente de apresentação do código inicial sem o MVP, vou logo ao código completo da MainActivity:

public class MainActivity extends AppCompatActivity {
private static final String MOTOS_KEY = "motos";

private MotosAdapter adapter;
private ArrayList<Moto> motos = new ArrayList<>();
private AsyncHttpClient asyncHttpClient = new AsyncHttpClient();

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);

retrieveMotos( savedInstanceState );
}

@Override
protected void onStart() {
super.onStart();

RecyclerView rvMotos = (RecyclerView) findViewById(R.id.rv_motos);
rvMotos.setHasFixedSize(true);

StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(2, RecyclerView.VERTICAL);
rvMotos.setLayoutManager( layoutManager );

adapter = new MotosAdapter( this, motos );
rvMotos.setAdapter( adapter );
}

@Override
public void onSaveInstanceState(Bundle outState) {
outState.putParcelableArrayList(MOTOS_KEY, motos);
super.onSaveInstanceState(outState);
}

private void retrieveMotos( Bundle savedInstanceState ){
if( savedInstanceState != null ){
motos = savedInstanceState.getParcelableArrayList( MOTOS_KEY );
return;
}

RequestParams requestParams = new RequestParams(JsonHttpRequest.METODO_KEY, "get-motos");
asyncHttpClient.post(this,
JsonHttpRequest.URI,
requestParams,
new JsonHttpRequest(this));
}

public void updateEhFavoritoMoto( Moto moto ){
RequestParams requestParams = new RequestParams();
requestParams.put( JsonHttpRequest.METODO_KEY, "update-favorito-moto" );
requestParams.put( Moto.ID_KEY, moto.getId() );
requestParams.put( Moto.EH_FAVORITO_KEY, moto.isEhFavorito() );

asyncHttpClient.post(this,
JsonHttpRequest.URI,
requestParams,
new JsonHttpRequest(this));
}

public void updateListaRecycler( ArrayList<Moto> m ){
this.motos.clear();
this.motos.addAll( m );
adapter.notifyDataSetChanged();
}

public void updateItemRecycler( Moto m ){
for( int i = 0; i < motos.size(); i++ ){
if( motos.get(i).getId() == m.getId() ){
motos.get(i).setEhFavorito( m.isEhFavorito() );
adapter.notifyItemChanged( i );
}
}
}

public void showToast( String mensagem ){
Toast.makeText(this, mensagem, Toast.LENGTH_SHORT).show();
}

public void showProgressBar( boolean status ){
int visibilidade = status ? View.VISIBLE : View.GONE;
findViewById(R.id.pb_loading).setVisibility( visibilidade );
}
}

 

Nada que você já não tenha visto: inicialização de Views e uma série de código que permite comunicação com o backend Web, algo que sem o uso do AsyncHttp tende a ter o uso do AsyncTask.

Antes de apresentar os problemas do projeto atual, vamos ao layout /layout/activity_main.xml e logo depois ao diagrama dele:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#444"
android:fitsSystemWindows="true"
tools:context="br.com.thiengo.rodmotors.MainActivity">

<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">

<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />

</android.support.design.widget.AppBarLayout>

<android.support.v7.widget.RecyclerView
android:id="@+id/rv_motos"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />

<ProgressBar
android:id="@+id/pb_loading"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:visibility="gone" />
</android.support.design.widget.CoordinatorLayout>

 

Então o diagrama:

Executando o aplicativo com o código apresentado até aqui temos a seguinte tela:

O ícone de moto, quando amarelo, indica que aquela moto foi marcada como favorita pelo usuário do aplicativo. Essa alteração é permanente, pois altera o valor na base de dados JSON no backend Web, porém reversível, o usuário pode atualizar o status favorito de qualquer moto a qualquer momento.

Com isso, utilizando o aplicativo de exemplo que simula um aplicativo em produção, temos que o sistema funciona, e que os problemas são somente visíveis a desenvolvedores que têm acesso ao código do projeto.

São eles: camadas com dependências de camadas superiores e inferiores, projeto pouco propício a fácil evolução devido ao forte acoplamento do código.

O que temos na verdade é o que parece ser uma única camada, como acima.

MVP Android

Depois de enxergado o problema de arquitetura no projeto de exemplo, devemos saber como iniciar a arquitetura limpa dele, aqui será com o uso do MVP.

No caso do Android as camadas desse padrão de arquitetura tendem a ter as seguintes entidades:

  • View (Visualização): Activitys, Fragments, Dialogs, Widgets e Adapters;
  • Presenter (Apresentador): classes que permitem a comunicação em Model e View. Classes que tendem a ser criadas somente devido ao uso do MVP. Essas têm toda a lógica de formatação de dados dentro delas, lógica antes presente na camada View;
  • Model (Modelo): classes de obtenção de dados, em uma base convencional local ou pela comunicação remota, Network. Classes de domínio e lógica de negócio, de preferência as que não tenham lógica de formatação de dados, essas tendem a estar na camada Presenter.

Note que nas seções de teoria, logo no início do artigo, falei sobre o "balancear" as camadas de acordo com as necessidades do projeto.

Logo que iniciei os estudos do MVP tudo foi tranquilo e fluindo, até o momento da implementação em um aplicativo real. Foi ai que notei a importância de ter lido anteriormente, nos artigos fontes, o termo "balancear".

Pois caso não tivesse mudado uma classe de Model para Presenter, eu teria de ter implementado uma série de métodos para acesso simples a dados que estavam nas instâncias dessa classe.

Assim vamos a atualização do projeto de motocicletas.

Criando Interfaces MVP

Vamos trabalhar com Interfaces, pois assim respeitamos um dos princípios da orientação a objetos que indica o uso de interface ante a herança e também porque é possível melhor ler e entender o MVP em nosso projeto Android.

Segue Interface MVP:

public interface MVP {
/* TODO */
}

 

Dentro dessa Interface vamos definir as outras que representam as camadas do padrão.

Vamos iniciar verificando quais são os métodos que devem entrar na interface que representa a camada Model.

Voltando a MainActivity identificamos rapidamente dois métodos que acessam dados remotos, ou seja, vão estar presentes na camada Model. São eles: retrieveMotos()updateEhFavoritoMoto().

Assim temos:

public interface MVP {
interface ModelImpl {
public void retrieveMotos();
public void updateEhFavoritoMoto( Moto m );
}
}

 

Note que já alterei a assinatura do método retrieveMotos(). No código da camada Presenter você entenderá o porquê.

Para permitir a comunicação limpa entre a camada Model e View, a Interface da camada Presenter será a seguinte:

public interface MVP {
...
interface PresenterImpl {
public void retrieveMotos(Bundle savedInstanceState);
public Context getContext();
public void showProgressBar( boolean status );
public void showToast( String mensagem );
public void updateListaRecycler(ArrayList<Moto> motos);
public void updateItemRecycler(Moto moto);
public void updateEhFavoritoMoto( Moto moto );
public ArrayList<Moto> getMotos();
public void setView( ViewImpl view );
}
}

 

Os métodos retrieveMotos()updateEhFavoritoMoto() são para permitir que a camada View, mais precisamente a MainActivity, invoque os dados por meio do Presenter, sem saber e depender da camada Model.

Os métodos showProgressBar()showToast()updateListaRecycler() são para permitir que o Model realize invocações que impactem na camada View, isso por meio da camada Presenter, sem ter conhecimento de View.

O método getMotos() permite que utilizemos a lista de objetos Moto dentro da camada Presenter, pois há algumas lógicas que não envolvem a camada Model, mas que também não devem, segundo nossa implementação, estarem na camada View.

O método getContext() permite que a camada Model acesse o contexto, em nosso caso a MainActivity, isso por meio da camada Presenter. Estou ressaltando a camada Presenter frequentemente para você ver que não há comunicação direta entre Model e View.

As classes de modelo, em termos de camadas MVP, somente têm referências as classes da camada Presenter. Classes de visualização a mesma coisa, somente referências a classes de Presenter.

Já as classes de Presenter têm referências a ambas as camadas.

O método setView() será útil para que aumentemos a performance do aplicativo, digo, para que não seja necessária a recriação de objetos depois da reconstrução da MainActivity. Isso, pois em nossa camada Model haverá uma classe com o ciclo de vida diferente do ciclo de vida a atividade principal.

Com isso podemos ir a Interface da camada de visualização, ViewImpl:

public interface MVP {
...
interface ViewImpl {
String MOTOS_KEY = "motos";

public void showProgressBar( int visibilidade );
public void showToast( String mensagem );
public void updateListaRecycler();
public void updateItemRecycler( int posicao );
}
}

 

Acima há somente métodos que serão invocados pela camada Presenter, todos devido a invocações primeiro na camada Model. Note que optei por mover a constante MOTOS_KEY da MainActivity para a Interface ViewImpl unicamente por gosto, acredito que o código fica melhor assim.

Agora vamos as implementações das Interfaces e consequentemente a atualização dos códigos já definidos.

Aplicando o MVP

Primeiro a nova classe da camada Model, a classe Model:

public class Model implements MVP.ModelImpl {
private AsyncHttpClient asyncHttpClient = new AsyncHttpClient();
private MVP.PresenterImpl presenter;

public Model( MVP.PresenterImpl presenter ){
this.presenter = presenter;
}

@Override
public void retrieveMotos() {
RequestParams requestParams = new RequestParams(JsonHttpRequest.METODO_KEY, "get-motos");

asyncHttpClient.post(
presenter.getContext(),
JsonHttpRequest.URI,
requestParams,
new JsonHttpRequest( presenter ));
}

@Override
public void updateEhFavoritoMoto( Moto moto ){
RequestParams requestParams = new RequestParams();
requestParams.put( JsonHttpRequest.METODO_KEY, "update-favorito-moto" );
requestParams.put( Moto.ID_KEY, moto.getId() );
requestParams.put( Moto.EH_FAVORITO_KEY, moto.isEhFavorito() );

asyncHttpClient.post(presenter.getContext(),
JsonHttpRequest.URI,
requestParams,
new JsonHttpRequest( presenter ));
}
}

 

Todas as entidades de conexão com dados remotos vieram para essa classe, digo, todas as entidades que estavam na MainActivity.

Note que nossa classe JsonHttpRequest também faz parte da camada Model, mas não houve a necessidade de fazer com que ela implementa-se a interface ModelImpl, principalmente porque somente a nova classe Model acessa ela.

Note o uso de uma instância de PresenterImpl para todas invocações fora da camada Model.

Não se preocupe com o uso da classe Moto também sem implementar uma das Interfaces MVP. Em nosso projeto essa classe faz parte da camada Presenter. Ela é acessada por meio de lógica somente nas camadas Model e Presenter, algo aceitável na implementação do MVP.

A seguir a classe Presenter da camada de mesmo nome. Primeiro as declarações e o construtor:

public class Presenter implements MVP.PresenterImpl {

private ArrayList<Moto> motos = new ArrayList<>();
private MVP.ModelImpl model;
private MVP.ViewImpl view;

public Presenter(){
model = new Model(this);
}

@Override
public void setView(MVP.ViewImpl view) {
this.view = view;
}
...
}

 

Deixei o método setView() pois é ele que faz inútil manter o uso do WeakReference, sempre que necessário vamos alterar a referência de view.

Mas o que tem haver MVP.ViewImpl com o WeakReference? Isso não seria somente uma precaução para evitar o vazamento de memória devido a referência a MainActiviy?

Sim. Mas a MainActivity de nosso projeto é que vai implementar a Interface ViewImpl. Caso não tivéssemos uma lógica que trabalhasse a mudança de referência em view e que também não utilizasse o WeakReference, a antiga Activity somente seria liberada para o Garbage Collection depois do fim da execução da Thread de background gerenciada pela library AsyncHttp.

O método setView() será melhor entendido quando chegarmos as atualizações em MainActivity.

Agora vamos aos métodos que trabalham com invocações a instância de Model:

public class Presenter implements MVP.PresenterImpl {
...
@Override
public void retrieveMotos( Bundle savedInstanceState ) {
if( savedInstanceState != null ){
motos = savedInstanceState.getParcelableArrayList( MVP.ViewImpl.MOTOS_KEY );
return;
}
model.retrieveMotos();
}

@Override
public Context getContext() {
return (Context) view;
}

@Override
public void updateEhFavoritoMoto(Moto moto) {
moto.setEhFavorito( !moto.isEhFavorito() );
model.updateEhFavoritoMoto(moto);
}
...
}

 

Em retrieveMotos() optei por colocar até mesmo a verificação simples ao SaveInstanceState nele para não ter o mínimo código condicional possível na camada View.

Coloquei o getContext() também nessa apresentação, pois ele é utilizado somente pela camada Model, mais precisamente, é um dos argumentos obrigatórios no uso da instância da classe AssyncHttpClient.

Assim podemos prosseguir a apresentação dos métodos que têm referência direta a instância que representa a camada View:

public class Presenter implements MVP.PresenterImpl {
...
@Override
public void showProgressBar(boolean status) {
int visibilidade = status ? View.VISIBLE : View.GONE;
view.showProgressBar( visibilidade );
}

@Override
public void showToast(String mensagem) {
view.showToast( mensagem );
}

@Override
public void updateListaRecycler(ArrayList<Moto> m) {
motos.clear();
motos.addAll( m );
view.updateListaRecycler();
}

@Override
public void updateItemRecycler(Moto moto) {
for( int i = 0; i < motos.size(); i++ ){
if( motos.get(i).getId() == moto.getId() ){
motos.get(i).setEhFavorito( moto.isEhFavorito() );
view.updateItemRecycler( i );
}
}
}

@Override
public ArrayList<Moto> getMotos() {
return motos;
}
}

 

O getMotos() presente é para que não seja necessária uma referência explicita, variável de instância de motos na camada View, na MainActivity.

Agora vamos as atualizações na MainActivity. Vamos em duas partes. Primeiro os métodos e declarações iniciais:

public class MainActivity extends AppCompatActivity implements MVP.ViewImpl {
private MotosAdapter adapter;
private static MVP.PresenterImpl presenter;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);

if( presenter == null ){
presenter = new Presenter();
}
presenter.setView( this );
presenter.retrieveMotos( savedInstanceState );
}

@Override
protected void onStart() {
super.onStart();

RecyclerView rvMotos = (RecyclerView) findViewById(R.id.rv_motos);
rvMotos.setHasFixedSize(true);

StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(2, RecyclerView.VERTICAL);
rvMotos.setLayoutManager( layoutManager );

adapter = new MotosAdapter( this, presenter.getMotos() );
rvMotos.setAdapter( adapter );
}

@Override
public void onSaveInstanceState(Bundle outState) {
outState.putParcelableArrayList(MOTOS_KEY, presenter.getMotos());
super.onSaveInstanceState(outState);
}
...
}

 

Mantivemos o trabalho com o onSaveInstanceState(), a referência a instância de Presenter é por meio de uma variável estática não global (pois ela é private).

Esse trabalho com variável estática é um paliativo que nos permite manter o máximo possível os recursos já instanciados em Presenter. A variável presenter, depois da inicialização do aplicativo, somente será null se o processo dele for removido.

Aqui faz sentido o uso da variável estática, pois no contexto geral o estado da MainActivity pouco importa a lógica de negócio do domínio do problema, além de a adoção desse caminho não atrapalhar a aplicação de testes com a arquitetura atual, algo que o MVP também vem facilitar no Android, fazer com que os aplicativos tenham uma arquitetura testável.

Então por que um paliativo?

Isso, pois sabemos do problema que é trabalhar com entidades estáticas. Com a evolução do aplicativo essa abordagem não seria, muito provavelmente, uma boa escolha.

Vamos entender melhor o trecho de recuperação ou inicialização da variável presenter. Segue:

...
if( presenter == null ){
presenter = new Presenter();
}
presenter.setView( this );
presenter.retrieveMotos( savedInstanceState );
...

 

O código acima diz: Caso não tenha sido possível manter a instância de Presenter em memória ou ela ainda nem mesmo tenha sido criada, então crie uma com new Presenter(); Logo depois, com uma instância nova ou antiga, simplesmente coloque o ViewImpl (contexto) correto vinculada a essa instância de Presenter e então recupere os dados de motos, aqui no savedInstanceState ou na camada de modelo via conexão remota.

Voltando ao código da MainActivity apresentado até o momento, temos que não mais há uma referência direta a uma lista de motos. Em onStart() é possível ver que essa lista é capturada pelo presenter:

...
@Override
protected void onStart() {
...
adapter = new MotosAdapter( this, presenter.getMotos() );
rvMotos.setAdapter( adapter );
}
...

 

Assim podemos ir ao restante do código de MainActivity, que por sinal ficou não somente bem dividido devido ao uso do MVP, mas também bem menor:

public class MainActivity extends AppCompatActivity implements MVP.ViewImpl {
...

public void updateEhFavoritoMoto( Moto moto ){
presenter.updateEhFavoritoMoto( moto );
}

public void updateListaRecycler(){
adapter.notifyDataSetChanged();
}

public void updateItemRecycler( int posicao ){
adapter.notifyItemChanged( posicao );
}

public void showToast( String mensagem ){
Toast.makeText(this, mensagem, Toast.LENGTH_SHORT).show();
}

public void showProgressBar( int visibilidade ){
findViewById(R.id.pb_loading).setVisibility( visibilidade );
}
}

 

Nada de blocos condicionais e cálculos, todo esse trabalho fica com as camadas Presenter e Model.

Agora a atualização que colocamos a classe Moto:

public class Moto implements Parcelable {
...
public int getEhFavoritoIcone(){
if( isEhFavorito() ){
return R.drawable.ic_favorito_marcado;
}
return R.drawable.ic_favorito;
}
...
}

 

A lógica em getEhFavoritoIcone() é aceitável, tendo em mente que a classe Moto faz parte da camada Presenter em nosso aplicativo. Note que mesmo Moto sendo da camada Presenter, não houve a necessidade de implementação de PresenterImpl.

Antes de continuar com o comentário sobre o porquê de Moto em Presenter, vamos ao código de MotosAdapter, na verdade vamos aos trechos atualizados:

public class MotosAdapter extends RecyclerView.Adapter<MotosAdapter.ViewHolder> {
...
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
...
private void setDados( Moto moto ){
Picasso.with( ivMoto.getContext() )
.load( moto.getImagem() )
.into( ivMoto );

tvModelo.setText( moto.getModelo() );
ivFavorito.setImageResource( moto.getEhFavoritoIcone() );
}

@Override
public void onClick(View view) {
activity.updateEhFavoritoMoto( motos.get( getAdapterPosition() ) );
}
}
}

 

Alguns evangelizadores do MVP podem criticar o modelo onde uma classe claramente do domínio do problema esteja sendo utilizada como integrante da camada Presenter.

Na verdade conseguimos remover toda a lógica da camada de visualização e balanceamos de forma aceitável a lógica de negócio entre as camadas Model e Presenter.

Com o final da implementação do MVP, seguramente temos a arquitetura como abaixo:

E a parte Web?

Caso queira integrar no diagrama também o backend Web, eu o colocaria na camada Model.

Como melhoria da divisão aplicada, você poderia explicitar o uso do MVP dividindo as classes em novos pacotes, seriam eles: model, view, presenter.

Ou até mesmo criar novas interfaces MVP para as classes Moto, JsonHttpRequest e MotosAdapter. Pois essas, apesar de bem definidas em suas camadas, não estão explicitamente indicando elas, as camadas. Ou seja, quem tem o conhecimento do MVP tende a entender a arquitetura atual sem muito esforço, outros vão ter que ler um pouco as linhas de código.

Testes e resultados

Dando o rebuild e executando o projeto no emulador AVD temos:

Então tentando favoritar o terceiro item temos:

E assim, para finalizar e verificar que não está tendo recarregamento de recursos da Web, mudamos a orientação da tela:

MVP implementado, código mais propicio a evolução e vida de programador facilitada. Não deixe de se inscrever na lista de emails e no canal do Blog.

Vídeo com implementação passo a passo do projeto com o padrão MVP

Abaixo o vídeo com a implementação passo a passo do projeto do artigo:

Para acesso ao conteúdo completo do projeto, entre nos GitHub dele em:

Ponto negativo

  • O número de classes aumenta, podendo diminuir a performance do projeto em execução.

Pontos positivos

  • O trabalho com a arquitetura MVP bem definida melhora a leitura e manutenção do projeto;
  • Os trabalhos com testes unitários ou em bloco são facilitados, principalmente devido ao isolamento da camada de visualização.

Conclusão

O MVP, como o padrão de arquitetura mais comum em aplicativos Android, deve ser visto como uma maneira de iniciar um projeto com uma estrutura que induz a um projeto limpo.

Isso devido aos benefícios da separação de conceitos que melhora a modularização e consequentemente a reutilização de códigos, encaminhando o projeto para ter também um menor número de scripts repetidos.

Com o entendimento do porquê de uma arquitetura em camadas, o entendimento do MVP e de outras possíveis arquiteturas é ainda mais simples.

Conhecer técnicas de código limpo simples como o CamelCase, além de princípios da orientação a objetos, tende, em conjunto ao MVP, a lhe dar conteúdo suficiente para uma arquitetura limpa.

Você pode optar por implementar o MVP na "unha" ou utilizar alguma library que tende a fazer o trabalho pesado para ti. Aqui, para melhor explicar o padrão, optamos por implementar na unha. Porém, no Android-Arsenal, há inúmeras libraries para lhe ajudar com o MVP.

Caso você tenha dúvidas ou sugestões, deixe-as abaixo, nos comentários. E não se esqueça de se inscrever, logo abaixo, na lista de emails do Blog.

Fontes

Model View Presenter (MVP) in Android, Part 1 - 2 - 3

Architecture of Android Apps

Introduction to Model View Presenter on Android

MVP Pattern

MVP for Android: how to organize the presentation layer

Separation of concerns

Multitier architecture

Model–view–controller

Model–view–presenter

Vlw.

Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog.
Email inválido

Relacionado

Estudando Android - Lista de Conteúdos do BlogEstudando Android - Lista de Conteúdos do BlogAndroid
AndroidAnnotations, Entendendo e UtilizandoAndroidAnnotations, Entendendo e UtilizandoAndroid
API de Endereços Para Pré-Cadastro em APPs Android - Parte 1API de Endereços Para Pré-Cadastro em APPs Android - Parte 1Android
Como Colocar Notificações Bolha em Seu Aplicativo AndroidComo Colocar Notificações Bolha em Seu Aplicativo AndroidAndroid

Compartilhar

Comentários Facebook (8)

Comentários Blog (20)

Para código / script, coloque entre [code] e [/code] para receber marcação especifica.
Forneça seu nome válido.
Forneça seu email válido.
Forneça o comentário.
Enviando, aguarde...
Nelson Plínio Martins Lisboa (1) (0)
07/07/2017
Bom dia tiengo tudo joia? Eu tenho uma duvida sobre mvp, ela é:
-Para eu começar a utilizar o padrão de projeto MVP se eu tiver 4 telas em uma camada somente eu precisarei escrever uma interface MVP para cada uma?
Exemplo:
   MVPLogin
  MVPMainUser
  MVPTelaDeNotificacoes

E parabéns Tiengo Sou um cara que respeito muito oque você faz e quero comprar o seu livro :D, Abraço
Responder
Vinícius Thiengo (0) (0)
07/07/2017
Nelson, tudo bem?

Na verdade não, não são necessárias interfaces individuais para cada tela, por exemplo: uma tela de login e uma tela de cadastro tendem a ter funcionalidades similares e até as mesmas em alguns pontos. Logo, neste caso seria viável manter uma Interface para ambas.

Você também pode trabalhar com herança, para que não haja código repetido na camada presenter e model, assim as partes específicas ficam nas subclasses.

Abraço e obrigado ao apoio ao Blog e canal.
Responder
Nelson Plínio (1) (0)
07/07/2017
Muito obrigado pelo esclarecimento,
Agradeço muito o conteudo que você disponibiliza em seu canal/blog,
Tudo que eu puder fazer para apoiar o canal/blog eu farei :D
Abraço
Responder
Robson (1) (0)
18/06/2017
Thiengo, nesta lista querendo fazer uma filtragem (em cima dos dados da lista que já foram fornecidos) usando um searchview.
Como implementaste o presenter ele não "conhece" o adapter seria a melhor forma ainda utilizar a view para enviar uma lista e atualizar o adapter? no caso teríamos que expor um método no adapter tipo.... updateAdapterList() ou nada disto e usaríamos de uma outra abordagem?
Responder
Vinícius Thiengo (0) (0)
19/06/2017
Robson, tudo bem?

Neste caso a lista já está na camada apresentadora, é onde a manteria.

O que vejo é uma interface, ao menos um método, sendo criado na camada apresentadora somente para receber o texto de busca da camada de visualização e então aplicando o algoritmo de filtro.

Isso, assumindo que a lista já estaria completa, ou seja, não teria necessidade de acesso a base de dados, algo que colocaria a camada de modelo também na lógica de negócio.

Nesse novo método da camada apresentadora, muito provavelmente você teria de trabalhar com uma lista auxiliar, essa podendo até mesmo ser uma variável de instância, para a de filtro ser vinculada ao adaptar, esse que seria solicitado a atualização por um método na camada de visualização, público, de acesso na camada apresentadora.

Assim deve conseguir prosseguir, sendo uma opção possível. Abraço.
Responder
Robson (1) (0)
15/06/2017
Fala meu caro tudo bem, então estou prosseguindo com o MVP, pondo a mão na massa, este é o segundo comentário aqui no blog e desta vez queria uma opinião sua, encontrei o seguinte comentario:

Imposto pelo padrão: Model <- Presenter <-> View. O Presenter "conhece" o Model e a View, a View "conhece" o Presenter. O Model não tem qualquer conhecimento dos outros dois.

Então o comentário acima ele diverge do seu diagrama MVP concorda? eu tenho me baseado no seu diagrama e não estou encontrando nem vejo problemas o que simplesmente outros poderiam afirmar é que não estou usando o padrão pois eu o violei, mais queria um comentário seu a respeito.
Responder
Vinícius Thiengo (2) (0)
15/06/2017
Robson, tudo bem?

Algumas fontes têm esse modelo de definição do MVP. Outras têm o mesmo que eu apresentei aqui.

Como faço um artigo aqui no Blog?

Estudo várias fontes, não tem um número exato sobre isso. Algumas vezes levo mais em consideração a documentação, quando existe alguma ?oficial?.

Depois desses estudos, faço minha própria síntese do assunto. Novamente: quando há uma documentação oficial, como a linguagem Kotlin, por exemplo, tudo fica mais simples.

No caso do MVP, como eu já vim de um background de padrões de projeto, não tive dúvidas em assumir que na verdade o modelo desse padrão de arquitetura é Model <-> Presenter <-> View.

O que recomendo é que estude ainda mais fontes e dê prioridade àqueles que colocam o código, esses estão, sem sombra de dúvidas, apresentando o que realmente entendem da tecnologia discutida.

Note que ter uma implementação MVP em Model <- Presenter <-> View, não remove o uso desse padrão, até porque o modelo Model <- Presenter <-> View está contido em Model <-> Presenter <-> View.

Abraço.
Responder
Robson (1) (0)
15/06/2017
Show!
Responder
Robson (1) (0)
13/06/2017
Olá Thiengo boa noite, prezado andei estudando o padrão, realmente não achei fácil, até coloquei um comentário lá no canal, com certeza a liberdade de programar no android nos contamina a ter um não padrão e essa liberdade nos vicia de forma errada, embora eu tenha no minimo o cuidado de deixar a camada de dados acessível apenas pelos serviços mais não sei se isto tem sido valido, bem hoje estou aqui porque apos implementar o padrão em uma atividade ainda não sei dizer se o mesmo totalmente adotado, restam dúvidas acredito que no modelo, sei que seu tempo deve ser escasso mais se houver um tempo por gentileza me faz alguma critica do código abaixo é apenas uma atividade de Login e estou usando SQLite, não estou indo buscar o usuário no back-end porque isto eu faço na atividade do registro e que será a próxima refatoração, mais acredito que não ficou ok a parte do Modelo
public interface LoginMVP {

    interface View {

        void setErrorUser();
        void setErrorPassword();
        void showToastError(String mensagem);
        void showToastInfo(String mensagem);
        void showToastWarning(String mensagem);
        void showToastSuccess(String mensagem);
        void setUserNotFound();
        void navigateMain();
    }

    interface Presenter {

        void showToastError(String mensagem);
        void showToastSuccess(String mensagem);
        void login(String login, String password);
        void setView(LoginMVP.View view);
        Context getContext();
    }

    interface Model {
        Usuario BuscaporLogin(String login);
    }
}
public class LoginPresenter implements LoginMVP.Presenter {

    private LoginMVP.View view;
    private LoginMVP.Model model;

    public LoginPresenter(LoginMVP.View view){
        this.view = view;
        this.model = new LoginModel(this);
    }

    @Override
    public void setView(LoginMVP.View view){
        this.view = view;
    }

    @Override
    public Context getContext() {
        return (Context) view;
    }

    @Override
    public void login(String login, String senha){
        if (!validateFields(login, senha)){
            return;
        }
        // Login
        Usuario usuario = model.BuscaporLogin(login);

        if (usuario == null){
            view.setUserNotFound();
        } else {
            if (usuario.getSenha().equals(senha)){

                ((Aplicacao)getContext().getApplicationContext()).setUsername(login);
                ((Aplicacao)getContext().getApplicationContext()).setPassword(senha);

                Util.saveSPInt(getContext().getApplicationContext(), Util.UserID, usuario.getId());
                Util.saveSPString(getContext().getApplicationContext(), Util.UserName, login.toString().trim());
                Util.saveSPLong(getContext().getApplicationContext(), Util.DataLogin, new Date().getTime() );

                showToastSuccess( usuario.getNome() + ", Seja bem vindo!");
                view.navigateMain();
            } else {
                view.showToastError("Senha inválida, tente novamente.");
            }
        }
    }

    private boolean validateFields(String nome, String senha) {
        return (!isEmptyFields(nome,senha));
    }

    private boolean isEmptyFields(String nome, String senha) {
        if(nome.trim().equals("")) {
            view.setErrorUser();
            return true;
        } else if(senha.trim().equals("")){
            view.setErrorPassword();
            return true;
        }

        return false;
    }

    @Override
    public void showToastError(String mensagem) {
        view.showToastError(mensagem);
    }

    @Override
    public void showToastSuccess(String mensagem) {
        view.showToastSuccess(mensagem);
    }

}
public class LoginModel implements LoginMVP.Model {

    private UsuarioService usuarioService;
    private LoginMVP.Presenter presenter;

    public LoginModel(LoginMVP.Presenter presenter) {
        this.presenter = presenter;
        this.usuarioService = new UsuarioService(presenter.getContext());
    }

    @Override
    public Usuario BuscaporLogin(String login) {
        return usuarioService.BuscaporLogin(login);
    }
}
public class UsuarioService {

    private Context context ;
    private DAOUsuario dao;

    public UsuarioService(Context context) {
        this.context = context;
        this.dao = new DAOUsuario(context);
    }

    public Usuario BuscaporLogin(String login){
         return dao.buscaporLogin(login);
Responder
Vinícius Thiengo (1) (0)
15/06/2017
Robson, tudo bem?

Olhando seu código, o que eu não faria é ter o seguinte trecho na camada apresentadora:

if ( usuario.getSenha().equals(senha) ) {

    ((Aplicacao)getContext().getApplicationContext()).setUsername(login);
    ((Aplicacao)getContext().getApplicationContext()).setPassword(senha);

    Util.saveSPInt(getContext().getApplicationContext(), Util.UserID, usuario.getId());
    Util.saveSPString(getContext().getApplicationContext(), Util.UserName, login.toString().trim());
    Util.saveSPLong(getContext().getApplicationContext(), Util.DataLogin, new Date().getTime() );

    showToastSuccess( usuario.getNome() + ", Seja bem vindo!?);
    view.navigateMain();
}

O seu showToastSuccess() e showToastError() podem ser resumidos em apenas um showToast() onde a mensagem a ser apresentada é passada como argumento.

Todos os Util.save podem ser encapsulados em uma interface (classe e método) da camada de modelo, pois mesmo os dados sendo locais, SQLite, eles ainda são da camada de modelo.

Não vejo problemas em colocar a maior parte da lógica de negócio do projeto, não incluindo persistência de dados, na camada apresentadora. Mas não trabalho com persistência nessa camada, não de forma direta, mesmo sendo um simples SharedPreferences.

Um outro ponto, eu encapsularia o seguinte trecho: ((Aplicacao)getContext().getApplicationContext())

Lembre que qualquer padrão pode receber adaptações para se encaixar ao seu projeto, obviamente que alguns características terão de ser preservadas para que você possa garantir que o padrão está sendo respeitado. Por exemplo: nova no do MVP a camada de visualização e a camada de modelo não devem se comunicar.

Abraço.
Responder
Robson (1) (0)
15/06/2017
Olá! Thiengo valei ai as explicações,  então toda esta parte do SharedPreferences fica no Model, correto vou proceder desta forma, já as mensagens eu coloquei assim porque estou usando o Toasty https://github.com/GrenderG/Toasty.
Já sobre encapsular ((Aplicacao)getContext().getApplicationContext()), não entendi bem.
Responder
Vinícius Thiengo (0) (0)
15/06/2017
Coloque dentro de um método privado todo o trecho: ((Aplicacao)getContext().getApplicationContext())

Assim não precisa repeti-lo e quando houver de modificar essa parte, modificará somente no método.
Responder
14/02/2017
Fala Thiengo Blz ? ótimo vídeo, muito bem explicado,  Cara tentei adaptar para o meu projeto, mas na model  quando eu faço a requisição pelo retrofit ele não executa o metodo call.enqueue  para baixar os objetos, sendo que no fragmento ele faz a requisição sem problemas,  vc tem alguma ideia do motivo?  


public class Model implements MVP.ModelImpl {
    private TicketsService mTService;
    TicketsProvider provider = new TicketsProvider();
    private MVP.PresenterImpl presenter;
    private List<Establishment> mEstablishments = new ArrayList<>();

    public Model(MVP.PresenterImpl presenter){
        this.presenter = presenter;
    }

    @Override
    public void getEstablishments(String group) {
        mTService = provider.getmTService();
        Call<List<Establishment>> call = mTService.getEstablishments(group, "aquivaioheader");
        call.enqueue(new Callback<List<Establishment>>() {
            @Override
            public void onResponse(Call<List<Establishment>> call, Response<List<Establishment>> response) {
                presenter.showProgressBar(false);
                if (response.isSuccess()) {
                    mEstablishments = response.body();
                    presenter.updateListaRecycler(mEstablishments);
                    /*mAdapter = new ListEstablishmentAdapter(presenter.getContext(), mEstablishments);
                    mRecyclerView.setAdapter(mAdapter);
                    txtLoading.setVisibility(View.GONE);*/
                  /*  List<Establishment> establishmentList = mEstablishments;
                    for (Establishment establishment : establishmentList) {
                        if (establishment.tags != null) {
                            List<String> tags = establishment.tags;
                            for (String tag : tags) {
                                if (!listTags.contains(tag)) {
                                    listTags.add(tag);
                                }
                            }
                        }
                    }*/

                } else {
                    ErrorModel errorModel = ErrorUtils.parseError(response);
                   // txtLoading.setText(errorModel.message + ("\n") + errorModel.resMessage);
                }
            }

            @Override
            public void onFailure(Call<List<Establishment>> call, Throwable t) {
               // txtLoading.setText(t.getMessage());
            }
        });
    }
Responder
Vinícius Thiengo (0) (0)
18/02/2017
Tiago, tudo bem?

Aparentemente a maneira como você está utilizando o Retrofit é que está errada. Não vejo a interface para envio de dados, digo, a interface que deve ser criada por ti e invocada antes do enqueue().

Dê uma olhada em minha configuração de uso do Retrofit no link a seguir: http://www.thiengo.com.br/gcmnetworkmanager-para-execucao-de-tarefas-no-background-android#title-15

Veja se consegue assim acertar seu código e faze-lo funcionar. Qualquer coisa volte aqui. Abraço.
Responder
rafagets2ah (1) (0)
03/02/2017
Fala mestre! Olha eu novamente te perturbando... rsrs

Pensando na reusabilidade do código, evitando assim reescrita, caso tenha uma outra activity distinta que também necessite dá lista de motos, qual a melhor forma para se fazer essa solicitação?
Responder
Vinícius Thiengo (0) (0)
03/02/2017
Rafa, eu colocaria uma classe na camada Presenter e uma na Model, somente responsáveis por requisição e atualização de lista (status favorito, por exemplo). Assim teria um conjunto de funcionalidades divididos nas classes corretas.

Isso, pois o uso de lista de motos pode implicar no uso também de funcionalidades como no exemplo acima, de favoritar ou até mesmo compartilhar direto da lista. Abraço.
Responder
rafagets2ah (1) (0)
31/01/2017
Olá!... Tenho uma dúvida e já peço perdão se ela for estúpida? rsrs

Suponhamos que tenho MainActivity,  ManterUsuarioActivity  e ManterNoticiaActivity.

A pergunta é: tenho que ter um MVP, uma Model e um Presenter para cada activity? Se sim, qual a melhor forma de se estruturar o projeto (package)?
Responder
Vinícius Thiengo (0) (0)
31/01/2017
Rafa, tudo bem?

Em meu caso optaria por ter Models e Presenters distintos pelo menos em relação a MainActivity, pois, por exemplo, caso tenha uma lista de itens na atividade principal e um ?obter token? nas outras duas atividades, optaria por não manter um único Presenter e Model com métodos dessas funcionalidades.

Isso para não ferir o princípio de responsabilidade única da orientação a objetos, sabendo também que não seguir esse principio indica, muito provavelmente, uma classe ?amontoada? de métodos? atrapalhando a leitura e evolução do código.

Para facilitar a leitura do MVP por parte dos developers do projeto, na estrutura de pacotes optaria por divisões como: /view, /model e /presenter. A opção: /view e /domain também seria uma boa, neste caso com Interfaces explícitas para classes de Model e Presenter, pois elas estariam em /domain. Abraço.
Responder
rafagets2ah (1) (0)
31/01/2017
Olá, acompanho anonimamente suas postagens a muito tempo. Você é fera! qual library mvp você indica é tem algum exemplo (tutorial)?
Responder
Vinícius Thiengo (0) (0)
31/01/2017
Rafa, tudo bem?

Eu particularmente utilizo a implementação na ?unha? como no artigo acima, mas abaixo listo algumas que encontrei no AndroidArsenal e que são bem populares (em número de estrelas):

https://android-arsenal.com/details/3/1514
https://android-arsenal.com/details/1/4579
https://android-arsenal.com/details/3/1515
https://android-arsenal.com/details/1/3776
https://android-arsenal.com/details/1/2971

Abraço.
Responder