3 Estratégias Para Informar Sobre Uma Nova Versão de Seu Aplicativo 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 /3 Estratégias Para Informar Sobre Uma Nova Versão de Seu Aplicativo Android

3 Estratégias Para Informar Sobre Uma Nova Versão de Seu Aplicativo Android

Vinícius Thiengo28/02/2017
(1645) (4) (187) (10)
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 passo a passo apresentar três estratégias de código onde será possível comparar a versão atual do aplicativo no device com a versão mais recente dele disponível na Google Play Store.

Depois dessa comparação, caso a versão disponível na Play Store seja mais atual, vamos apresentar um dialog que permite ao usuário, rapidamente, realizar a atualização do aplicativo.

Todas as estratégias apresentadas têm seus lados positivos e negativos, nenhuma é melhor do que a outra, digo, a melhor opção vai depender do domínio do problema de seu projeto.

O vídeo com a implementação, passo a passo, se encontra logo no final do artigo, na seção: Vídeo com implementação passo a passo do projeto.

A seguir os tópicos que estaremos estudando:

Caso queira pular a parte de implementação inicial do aplicativo de exemplo e ir direto as estratégias, depois da seção e subseções de Motivos para forçar ou somente informar sobre uma nova versão do aplicativo, continue a leitura direto da seção Dialog de apresentação de nova versão.

Motivos para forçar ou somente informar sobre uma nova versão do aplicativo

Eu já trabalhei em aplicativo Android onde tive de adicionar o dialog de "informativo de nova versão de App". Porém não existia um real motivo para aquele dialog. Arrisco a dizer que o único motivo da existência daquela funcionalidade, na época, foi porque o WhatsApp fazia isso.

Logo, minha recomendação é que você, ou a empresa onde trabalha, tenha um motivo para que seja colocada essa estratégia de "informe de nova versão" em seu aplicativo, caso contrário, mantenha somente com a versão convencional de informe: notificação automática da Google Play Store.

Mas ok, quais seriam alguns prováveis motivos:

  • As versões anteriores do aplicativo estavam em versão Beta ou Alpha, logo, o aviso de uma versão mais atual pode ser muito importante devido, principalmente, ao número de bugs divulgados e corrigidos;
  • Aplicativo de vendas, m-commerce, onde uma pequena mudança pode aumentar consideravelmente o número de vendas. Isso depois de já ter testado a nova versão com uma pequena parcela de usuários;
  • Quando seu aplicativo teve uma atualização importante de segurança. Segurança sempre é algo a ser corrigido ou melhorado o quanto antes, principalmente em aplicativos que têm transações monetárias.

Note que anteriormente somente listei alguns motivos, que mesmo para você podem não ser fortes o suficiente para justificar o uso de um dialog de informe de nova versão. E, obviamente, você deve ter alguns bons motivos partindo de seu domínio do problema.

O importante é não colocar uma funcionalidade, agora no contexto geral, somente porque um outro aplicativo também a tem.

Modelo convencional, Google Play Store

Como informado anteriormente, o Google Play Store já nos ajuda com o informe de nova versão de aplicativo, somente precisamos fornecer uma nova versão no dashboard de desenvolvedor da Play Store.

Esse informe vai ao device dos usuários, que têm o aplicativo instalado, em formato de notificação:

Mesmo o usuário removendo a notificação sem realizar a atualização, o aplicativo da Play Store volta a informa-lo sobre a necessidade do update.

O lado negativo dessa estratégia de informe é que ela muitas vezes é "postergada", fica para depois. Provavelmente até você que é desenvolvedor Android tem esse tipo de comportamento, atualiza somente quando não há nada para fazer.

O Google, dependendo da atualização do aplicativo, pode, por conta própria, atualiza-lo no device dos usuários, mas tenha em mente que isso é algo raro.

Os users podem também optar por deixar o auto-update de aplicativos ativo, mas isso ele terá de fazer na mão, e mesmo assim não temos uma porcentagem de quantos usuários ativam o auto update.

Modelo alternativo, estratégias de código

Visto que em seu aplicativo a versão de "informe de novo update do App" da Google Play Store não se faz suficiente, você então pode partir para o caminho alternativo: trabalhar no aplicativo o código de comparação de versão e então informar, dentro do aplicativo, que há uma nova versão dele disponível.

Além de informar sobre essa nova versão você pode escolher o grau de importância da atualização.

Como assim "grau de importância"?

Pode definir se a interface que será apresentada, de informe sobre a nova versão, terá ou não um "modo de escape", onde o usuário poderá escolher continuar utilizando o aplicativo e atualizar mais tarde, isso caso tenha o modo de escape. Ou não, somente continuará a consumir o aplicativo se atualiza-lo.

A seguir o print de uma tela sem modo de escape, versão antiga do WhatsApp: 

E por fim uma tela de um informe de atualização com a opção de escape, exatamente a tela do aplicativo que estaremos construindo neste artigo:

Assim podemos prosseguir com o código inicial do projeto de exemplo e logo depois com a apresentação das estratégias.

Projeto de exemplo, lado Web

Para que ao menos uma das estratégias, a de API proprietária, seja passível de ser simulada e também para que nosso aplicativo de exemplo chegue o mais próximo possível de uma aplicativo real, teremos um simples backend Web. O aplicativo é um de vendas de carros da Jaguar.

A seguir a configuração utilizada na construção desse lado do projeto:

  • Apache 2.2.29;
  • PHP 5.6.2;
  • PHPStorm 10.0.3.

Você é livre para utilizar as tecnologias de backend que preferir, somente tente seguir a estrutura de código apresentada aqui, assim poderá manter o estudo com o exemplo sem problemas.

Caso queira acesso rápido ao código do backend, entre no GitHub dele em: https://github.com/viniciusthiengo/jaguar-app-web.

Ao final teremos a seguinte estrutura no lado Web do projeto:

Camada de dados

Como já realizado em outros artigos aqui do Blog, a critérios de simplicidade vamos trabalhar com uma base de dados JSON.

Na camada de dados temos quatro entidades, porém em execução somente serão utilizadas duas: a classe Database e o arquivo jaguars.json. Isso, pois as outras duas são utilizadas somente para que seja possível criar a base JSON para testes, algo comum a se fazer em ambientes de desenvolvimento.

Vamos iniciar com a apresentação da base JSON, /data/jaguars.json:

[
{
"modelo": "XE",
"motor": "2.0L Turbocharged",
"preco": "R$ 147.650,00",
"urlImagem": "http://autopolis.com.br/wp-content/uploads/2015/08/Jaguar-XE_012.jpg"
},
{
"modelo": "XF",
"motor": "3.0 litros V6",
"preco": "R$ 325.603,00",
"urlImagem": "http://froog.com.br/wp-content/uploads/2009/07/jaguar-xf-2010.jpg"
},
{
"modelo": "XJ",
"motor": "3.0 litros V6 Supercharged",
"preco": "R$ 554.310,00",
"urlImagem": "http://cloudlakes.com/data_images/models/jaguar-xj/jaguar-xj-08.jpg"
},
{
"modelo": "F-PACE",
"motor": "V6 de 3.0 litros Supercharged",
"preco": "R$ 309.300,00",
"urlImagem": "http://carroslancamentos.com.br/wp-content/uploads/2016/01/preco-jaguar-f-pace-e1452189923210.jpg"
},
{
"modelo": "F-TYPE",
"motor": "V8 supercharged",
"preco": "R$ 687.700,00",
"urlImagem": "http://s2.glbimg.com/kP95p9PvAa0AGd9jFR3lO_FOSQM=/620x400/e.glbimg.com/og/ed/f/original/2014/04/07/01-salsa-s.jpg"
},
{
"modelo": "I-PACE CONCEPT",
"motor": "Eletric",
"preco": "Consulte um de nossos vendendores",
"urlImagem": "http://autoguide.com.vsassets.com/blog/wp-content/gallery/jaguar-i-pace-concept/2018-Jaguar-I-Pace-Concept-01-1.jpg"
}
]

 

Assim o código da classe que permite o acesso a base anterior, classe Database:

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

public static function getDados($database ){
$dadosString = file_get_contents( $database );
$objetos = json_decode($dadosString);
return $objetos;
}
}

 

Simples, certo?

Para não estender muito o conteúdo do artigo e sabendo que as outras duas entidades da camada de dados são utilizadas somente para a criação da base JSON, vou deixar você acessa-las no GitHub do projeto caso queira entender um pouco mais sobre como criar dados mock para o ambiente de desenvolvimento.

Camada de domínio

Para a camada de domínio temos as classes de dois pacotes: domain e apl. O primeiro pacote tem o que chamamos de "classe clássica de domínio", pois a classe presente nesse pacote tem toda a estrutura de um objeto do mundo real.

Já a classe do pacote apl é uma espécie de classe Presenter do MVP Android, somente tem a lógica de intermédio entre a entidade controladora e a camada de dados.

Vamos iniciar com o código da classe de domain, a classe Jaguar:

class Jaguar
{
public $modelo;
public $motor;
public $preco;
public $urlImagem;

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

public function setMotor($motor)
{
$this->motor = $motor;
}

public function setPreco($preco)
{
$this->preco = $preco;
}

public function setUrlImagem($urlImagem)
{
$this->urlImagem = $urlImagem;
}
}

 

Apresentei a classe anterior, principalmente devido a ela ser parte fundamental do código Android, digo, a versão Java dela. Isso, pois essa classe somente é utilizada no backend Web quando no código de criação da base JSON, devido a isso somente definimos os métodos setters e utilizamos o modificador de acesso public nos atributos, sendo que essa última característica é para permitir fácil parser JSON com o uso do método json_encode().

Por fim a classe que realmente será utilizada quando o código backend estiver sendo requisitado, do pacote apl, classe AplAdmin:

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

class AplAdmin
{
public static function getJaguarsJson(){
$objetos = Database::getDados( '../data/jaguars.json' );
return json_encode( $objetos );
}
}

Classe controladora

Assim o trecho final do backend Web, exatamente o arquivo que aplicaremos algumas atualizações para responder a estratégia de "API proprietária".

Segue código de /ctrl/CtrlAdmin.php:

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

include '../domain/Jaguar.php';
include '../apl/AplAdmin.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-jaguars' ) == 0 ){
/*
* Delay para que o ProgressBar seja apresentado no lado Android
* */
sleep(1);

$jaguarsJson = AplAdmin::getJaguarsJson();
echo $jaguarsJson;
}

 

O código backend trabalha tanto com requisições POST quanto com requisições GET.

Assim, caso o aplicativo Android de exemplo não esteja respondendo como deveria, você pode realizar testes pelo navegador, realizando as mesmas requisições feitas no App Android, porém por meio de variáveis GET no browser.

Caso o teste via GET seja necessário, não esqueça de descomentar a linha com a invocação de ini_set('display_errors', 1), pois assim você terá acesso, em tela, aos problemas de configuração de seu backend.

Projeto de exemplo, Android

Como no lado Web do projeto, o aplicativo Android também é simples. Vamos adicionar algumas APIs para o correto trabalho do domínio do problema, digo, APIs além das necessárias para a apresentação do "informe de uma nova versão de aplicativo", mas todas essas APIs serão comentadas e não atrapalharão no entendimento das estratégias propostas.

Caso queira ter acesso rápido a todo o código do projeto Android, incluindo os arquivos de configuração de IDE, acesse o GitHub dele em: https://github.com/viniciusthiengo/jaguar-app.

Assim, crie um novo projeto no Android Studio, escolha um com uma "Empty Activity" e defina o seguinte nome: Jaguar App.

Ao final da configuração inicial teremos o seguinte aplicativo:

Note que somente vamos construir as partes que nos permitem utilizar, posteriormente, as estratégias de informe de nova versão de aplicativo.

Estaremos dividindo as entidades do projeto de acordo com o padrão de arquitetura MVP, logo, ao final da construção do aplicativo teremos a seguinte estrutura:

Configurações Gradle

A seguir o código do Gradle Project Level, ou build.gradle (Project: JaguarApp):

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

allprojects {
repositories {
jcenter()
}
}

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

 

Note que para essa versão do Gradle e para a versão App Level, caso você esteja com uma configuração mais atual desses, mantenha seu projeto com a configuração mais atual, pois o código inicial de exemplo e as estratégias propostas de algoritmos devem funcionar sem problemas.

A seguir o Gradle App Level, ou build.gradle (Module: app):

apply plugin: 'com.android.application'

android {
compileSdkVersion 25
buildToolsVersion "24.0.3"
defaultConfig {
applicationId "br.com.thiengo.jaguarapp"
minSdkVersion 14
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.2.0'
testCompile 'junit:junit:4.12'

/* PARA USO DO RECYCLERVIEW */
compile 'com.android.support:design:25.2.0'

/* PARA USO DO CARDVIEW */
compile 'com.android.support:cardview-v7:25.2.0'

/* PARA CARREGAMENTO DE IMAGENS REMOTAS */
compile 'com.squareup.picasso:picasso:2.5.2'

/* PARA COMUNICAÇÃO COM BACKEND WEB E PARSER JSON */
compile 'com.loopj.android:android-async-http:1.4.9'
compile 'com.google.code.gson:gson:2.7'
}

 

A essa versão de Gradle vamos estar voltando para adicionar algumas outras referências, isso a partir da seção de implementação do Material Dialog.

Todas as libraries adicionadas até o momento são para que nosso domínio do problema, que envolve dados de texto e imagens remotas, para que esse seja trabalhado corretamente e assim seja possível simular um aplicativo em produção.

Configurações AndroidManifest

AndroidManifest.xml somente teve adicionado a ele a permissão de Internet:

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

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

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

<activity android:name=".view.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

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

 

Posteriormente, com a implementação da estratégia de "informe por meio de push notification", vamos voltar a esse XML para adicionar uma tag <service>.

Configurações de estilo

Os arquivos de estilo são todos bem simples. Em dois deles adicionamos as cores do tema e algumas tags de definição de imagem de background (imagem que você baixa junto ao projeto no GitHub) e definição de não utilização da AppBar padrão do tema em uso.

Vamos iniciar com o código de cores, /res/values/colors.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#5c8678</color>
<color name="colorPrimaryDark">#364f46</color>
<color name="colorAccent">#e5ed00</color>
</resources>

 

Em seguida vamos ao simples arquivo de String, /res/values/strings.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Jaguar App</string>
<string name="dialog_title">Nova Versão do App</string>
<string name="dialog_message">Está disponível a nova versão do aplicativo Jaguar App, clique no botão abaixo para realizar a atualização. Essa nova versão é mais leve e segura.</string>
<string name="dialog_positive_label">Atualizar</string>
<string name="dialog_negative_label">Depois</string>
</resources>

 

Todas as Strings com o prefixo dialog serão utilizadas quando definirmos o uso do MaterialDialog em seções posteriores.

E por fim o arquivo de definição de tema, /res/values/styles.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="Theme.AppCompat">
<item name="android:windowBackground">@drawable/background</item>
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>

<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>

Classes da camada de modelo

Como informado na seção principal do projeto Android, estamos trabalhando com o padrão de arquitetura MVP. Aqui vamos a apresentação inicial das classes que temos na camada de modelo.

Segue código da classe que permite o trabalho anterior e posterior à requisição backend Web, isso utilizando a API do AsyncHttp. Segue classe JsonHttpRequest:

public class JsonHttpRequest extends JsonHttpResponseHandler {
public static final String URI = "http://seu_host/jaguar-app/ctrl/CtrlAdmin.php";
public static final String METODO_KEY = "metodo";
public static final String METODO_JAGUARS = "get-jaguars";
private Presenter presenter;

public JsonHttpRequest( Presenter presenter ){
this.presenter = presenter;
}

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

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

for( int i = 0; i < response.length(); i++ ){
try{
j = gson.fromJson( response.getJSONObject( i ).toString(), Jaguar.class );
jaguars.add( j );
}
catch(JSONException e){}
}
presenter.updateListaRecycler( jaguars );
}

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

 

Simples, certo? Note que em seu_host você pode colocar seu localhost, porém tende a funcionar somente se for um host público ou o IP interno dado a sua máquina pelo seu router.

O uso da library Gson é para que o parser JSON seja simples.

Assim podemos ir a classe que permite a requisição backend junto a uma instância da classe anterior, segue código de Requester:

public class Requester {
private AsyncHttpClient asyncHttpClient;
private Presenter presenter;

public Requester(Presenter presenter ){
asyncHttpClient = new AsyncHttpClient();
this.presenter = presenter;
}

public void retrieveJaguars() {
RequestParams requestParams = new RequestParams();
requestParams.put(
JsonHttpRequest.METODO_KEY,
JsonHttpRequest.METODO_JAGUARS);

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

 

Outro código simples e pequeno, certo?

Caso você ainda não conheça o MVP, estude ele, pelos links indicados, somente depois de terminar com este artigo. Digo isso, pois o uso das estratégias de "informe sobre nova versão de aplicativo" não são dependentes de nenhum padrão de arquitetura, o MVP somente está sendo utilizado para facilitar a leitura do código.

Classes da camada apresentadora

Nesta camada temos novamente duas classes, estas que correspondem exatamente ao que temos na camada de domínio do backend Web. Uma é "uma clássica classe de domínio", que representa um objeto no mundo real. A outra é a que tem a maior parte da lógica de negócio do projeto.

Vamos iniciar com a classe que representa os carros do aplicativo, a classe Jaguar:

public class Jaguar implements Parcelable {
public static final String JAGUARS_KEY = "jaguars_key";

private String modelo;
private String motor;
private String preco;
private String urlImagem;

public String getModelo() {
return modelo;
}

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

public String getMotor() {
return motor;
}

public void setMotor(String motor) {
this.motor = motor;
}

public String getPreco() {
return preco;
}

public void setPreco(String preco) {
this.preco = preco;
}

public String getUrlImagem() {
return urlImagem;
}

public void setUrlImagem(String urlImagem) {
this.urlImagem = urlImagem;
}

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

@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.modelo);
dest.writeString(this.motor);
dest.writeString(this.preco);
dest.writeString(this.urlImagem);
}

public Jaguar() {}

protected Jaguar(Parcel in) {
this.modelo = in.readString();
this.motor = in.readString();
this.preco = in.readString();
this.urlImagem = in.readString();
}

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

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

 

Note que a definição de uma constante, JAGUARS_KEY, e a implementação do Parcelable são para que a lista de objetos Jaguar possa ser trabalhada junto ao onSaveInstanceState() da atividade principal do projeto.

Quando você estiver implementando o projeto, notará que alguns métodos getters e setters serão sinalizados como "não sendo utilizados", ignore essa sinalização, pois todos estão sim sendo utilizados, ao menos pelo parser da Gson API.

Agora vamos a classe responsável pela maior parte da lógica de negócio, a classe Presenter:

public class Presenter {
private static Presenter instance;
private Requester model;
private MainActivity activity;
private ArrayList<Jaguar> jaguars = new ArrayList<>();

private Presenter(){
model = new Requester( this );
}

public static Presenter getInstance(){
if( instance == null ){
instance = new Presenter();
}
return instance;
}

public void setActivity(MainActivity activity){
this.activity = activity;
initLocalBroadcast();
}

public Activity getContext() {
return activity;
}

public void retrieveJaguars(Bundle savedInstanceState) {
if( savedInstanceState != null ){
jaguars = savedInstanceState.getParcelableArrayList( Jaguar.JAGUARS_KEY );
return;
}
model.retrieveJaguars();
}

public void showProgressBar(boolean status) {
int visibilidade = status ? View.VISIBLE : View.GONE;
activity.showProgressBar( visibilidade );
}

public void updateListaRecycler(Object object) {
List<Jaguar> postsCarregados = (List<Jaguar>) object;
jaguars.clear();
jaguars.addAll( postsCarregados );
activity.updateListaRecycler();
showProgressBar( !(jaguars.size() > 0) );
}

public ArrayList<Jaguar> getJaguars() {
return jaguars;
}
}

 

Código simples para uma classe que faz o intermédio na comunicação entre as classes das camadas acima e abaixo da camada dela.

Classes da camada de visualização

Na camada de visualização também temos duas classes, uma adaptadora e a outra sendo a atividade principal.

Vamos iniciar com o código XML de layout que é utilizado em cada item do adapter, segue /layout/item_jaguar.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="160dp"
android:layout_gravity="center"
android:layout_marginBottom="4dp"
android:layout_marginTop="4dp">

<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="160dp">

<ImageView
android:id="@+id/iv_jaguar"
android:layout_width="match_parent"
android:layout_height="160dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:scaleType="centerCrop" />

<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:background="@drawable/sombra_item" />

<TextView
android:id="@+id/tv_modelo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/tv_motor"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:ellipsize="end"
android:maxLines="2"
android:paddingLeft="8dp"
android:textColor="@android:color/white"
android:textSize="20sp" />

<TextView
android:id="@+id/tv_motor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/tv_preco"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:paddingLeft="8dp"
android:textColor="@android:color/white"
android:textSize="14sp" />

<TextView
android:id="@+id/tv_preco"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:paddingBottom="8dp"
android:paddingLeft="8dp"
android:textColor="#f00"
android:textSize="18sp"
android:textStyle="bold" />
</RelativeLayout>
</android.support.v7.widget.CardView>

 

Para facilitar a leitura do XML anterior, segue o diagrama dele:

Você provavelmente deve estar se perguntando sobre o porquê da tag <View>

O background que definimos a esta tag nos permite colocar um gradiente em cada item da lista, juntando a isso o posicionamento dela no layout de item, conseguimos colocar o gradiente em cima do ImageView e abaixo dos outros TextView.

Segue código de /res/drawable/sombra_item.xml:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="90"
android:centerColor="#66000000"
android:endColor="#00ffffff"
android:startColor="#bb000000" />

<corners android:radius="0dp" />
</shape>

 

Com essa técnica no XML conseguimos o seguinte efeito gradiente (mais escuro na parte de baixo e mais claro na parte superior): 

E assim o código Java, que por sinal é somente de atribuição e acesso a dados, da classe JaguarAdapter:

public class JaguarAdapter extends RecyclerView.Adapter<JaguarAdapter.ViewHolder> {
private MainActivity activity;
private ArrayList<Jaguar> jaguars;

public JaguarAdapter( MainActivity activity, ArrayList<Jaguar> jaguars){
this.activity = activity;
this.jaguars = jaguars;
}

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

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

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


class ViewHolder extends RecyclerView.ViewHolder {
private ImageView ivJaguar;
private TextView tvModelo;
private TextView tvMotor;
private TextView tvPreco;

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

ivJaguar = (ImageView) itemView.findViewById(R.id.iv_jaguar);
tvModelo = (TextView) itemView.findViewById(R.id.tv_modelo);
tvMotor = (TextView) itemView.findViewById(R.id.tv_motor);
tvPreco = (TextView) itemView.findViewById(R.id.tv_preco);
}

private void setDados( Jaguar jaguar ){
Picasso.with( activity )
.load( jaguar.getUrlImagem() )
.into( ivJaguar );

tvModelo.setText( jaguar.getModelo() );
tvMotor.setText( jaguar.getMotor() );
tvPreco.setText( jaguar.getPreco() );
}
}
}

 

Devido ao uso do MVP, tanto a classe adaptadora como a MainActivity são simples, pois o "core" da lógica fica nas camadas abaixo.

A seguir o código XML do layout da atividade principal, segue /layout/activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#1a2521"
android:padding="8dp"
tools:context="br.com.thiengo.jaguarapp.view.MainActivity">

<android.support.v7.widget.RecyclerView
android:id="@+id/rv_jaguars"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<ProgressBar
android:id="@+id/pb_loading"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_centerInParent="true"
android:visibility="gone" />
</RelativeLayout>

 

Então o diagrama do layout anterior:

E por fim, o código Java da MainActivity

public class MainActivity extends AppCompatActivity {

private Presenter presenter;
private JaguarAdapter adapter;

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

presenter = Presenter.getInstance();
presenter.setActivity( this );
initRecycler();
presenter.retrieveJaguars( savedInstanceState );
}

@Override
public void onSaveInstanceState(Bundle outState) {
outState.putParcelableArrayList( Jaguar.JAGUARS_KEY, presenter.getJaguars() );
super.onSaveInstanceState(outState);
}

private void initRecycler(){
RecyclerView rvMotos = (RecyclerView) findViewById(R.id.rv_jaguars);
rvMotos.setHasFixedSize(true);

LinearLayoutManager layoutManager = new LinearLayoutManager(this);
rvMotos.setLayoutManager( layoutManager );

adapter = new JaguarAdapter( this, presenter.getJaguars() );
rvMotos.setAdapter( adapter );
}

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

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

 

Com isso finalizamos toda a apresentação inicial do projeto. Podemos executar o aplicativo, alterar a versão dele na Play Store (não será necessário enviar o aplicativo a Play Store aqui no exemplo) e nada será sinalizado, a não ser o informe padrão da Google Play Store App.

Como os usuários podem comprar pelo nosso Jaguar App, nós, desenvolvedores do aplicativo, em reunião definimos que é importante reforçar ao usuário sobre uma nova versão deste. Logo, vamos a implementação desta funcionalidade.

Dialog de apresentação de nova versão

Poderíamos ter optado por abrir uma nova atividade somente para informar sobre a nova versão do aplicativo, não há uma melhor maneira, você é quem defini.

Pode ser que uma nova atividade, para uma implementação mais rigorosa da funcionalidade de "informe sobre a atualização do aplicativo", onde o usuário terá sim de atualizar, caso contrário não continua utilizando o App. Pode ser que nesse contexto a abertura de uma nova atividade seja sim a melhor opção.

Aqui vamos seguir com o uso de uma library de dialog, mais precisamente a library MaterialDialog. O critério de escolha foi unicamente a popularidade da library.

Atualização Gradle

Atualize seu Gradle App Level, build.gradle (Module: app). Adicione o código destacado a seguir:

...
dependencies {
...

/* PARA USO DO MATERIAL DIALOG */
compile 'me.drakeet.materialdialog:library:1.3.1'
}

 

Para essa library a Android API mínima é a 8, Android FROYO.

Atualização atividade principal

Na MainActivity vamos adicionar um método que tem o código de acionamento não somente do MaterialDialog, mas também da abertura do aplicativo da Google Play Store na página correta para que a atualização aconteça.

Adicione o seguinte método e variável de instância a atividade principal:

...
private MaterialDialog mMaterialDialog;

public void showUpdateAppDialog(){

mMaterialDialog = new MaterialDialog(this)
.setTitle( R.string.dialog_title )
.setMessage( R.string.dialog_message )
.setCanceledOnTouchOutside(false)
.setPositiveButton( R.string.dialog_positive_label, new View.OnClickListener() {
@Override
public void onClick(View v) {
String packageName = getPackageName();
Intent intent;

try {
intent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + packageName));
startActivity( intent );
}
catch (android.content.ActivityNotFoundException e) {
intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + packageName));
startActivity( intent );
}
}
});
.setNegativeButton( R.string.dialog_negative_label, new View.OnClickListener() {
@Override
public void onClick(View v) {
mMaterialDialog.dismiss();
}
});

mMaterialDialog.show();
}
...

 

Recapitulando o código de onClick() em setPositiveButton():

...
String packageName = getPackageName();
Intent intent;

try {
intent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + packageName));
startActivity( intent );
}
catch (android.content.ActivityNotFoundException e) {
intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + packageName));
startActivity( intent );
}
...

 

Esse código é padrão para abertura do App da Play Store diretamente na página do aplicativo em uso.

Dentro do try estamos primeiro tentando a abertura pelo aplicativo da Play Store. No catch, caso esse aplicativo da Play não esteja no device, nós iniciamos a abertura da página pelo site da Play Store, isso utilizando o aplicativo de navegador.

Todos os outros trechos de código são simples de entender, principalmente pelos rótulos dos métodos que estão sendo utilizados.

Note que devido ao aplicativo de exemplo não precisar ser enviado a Play Store, os testes sempre abrirão uma página de "conteúdo não encontrado".

Para mudar esse comportamento, utilize o nome de package de algum aplicativo já presente na Play. Em meu caso vou utilizar no lugar de getPackageName() a String "br.thiengocalopsita" que é o package do aplicativo do Blog.

Estratégia de atualização com API proprietária

Essa é a estratégia que mais recomendo caso seu aplicativo também tenha um lado Web e você seja o administrador também desse lado do software.

Isso, pois é inevitável que o aplicativo tenha de acessar o backend para obtenção de dados. Assim é certo que o "informe de uma nova versão de aplicação" seja lido e processado junto aos dados de conteúdo que foram enviados ao App.

Digo "é certo", pois as outras duas estratégias, mesmo tendo o benefício de não lhe exigir um backend Web, têm pontos de falha que essa, de API proprietária, não tem.

Nosso fluxo para essa estratégia será o seguinte:

Atualização no backend Web

No arquivo /ctrl/CtrlAdmin.php adicione o código destacado:

...
if( strcasecmp( $dados['metodo'], 'get-jaguars' ) == 0 ){
/*
* Delay para que o ProgressBar seja apresentado no lado Android
* */
sleep(1);

$jaguarsJson = AplAdmin::getJaguarsJson();
$obj = new stdClass(); /* Classe padrão no PHP */
$obj->version = 2;
$obj->jaguars = json_decode($jaguarsJson);
echo json_encode($obj);
}

 

Agora estamos também retornando o código de versão. Porém foi necessário ir ao script backend e então adicionar a mão a versão atual do aplicativo.

Caso você tenha uma interface Web de administração de seu aplicativo, realize algumas modificações nela para que essa definição de versão de aplicativo Android seja feita diretamente de lá.

Provavelmente você deve ter notado um overhead nas duas últimas linhas:

...
$obj->jaguars = json_decode($jaguarsJson);
echo json_encode($obj);
...

 

Isso foi necessário, digo, a volta de um dado JSON para objeto PHP e logo depois a volta desse mesmo objeto a novamente um dado JSON. Fiz isso, pois caso contrário eu teria de ter modificado o código do método getJaguarsJson(), que, acredite, será exatamente o mesmo para as outras duas estratégias.

Em seu projeto você escolheria primeiro a melhor estratégia a ele de "informe de nova versão" e então não deixaria esse tipo de overhead ocorrer, isso, pois com inúmeras requisições esse código aparentemente "inofensivo" diminuiria consideravelmente a performance do software.

Atualização na camada de modelo

Nossa primeira atualização no Android é na camada de modelo. Se notar bem o código atualizado no backend Web, nós não mais estamos trabalhando com o retorno em JSONArray e sim em JSONObject.

Com a API AsyncHttp somente temos de sobrescrever o método onSuccess() que responde a esse tipo de retorno. Segue código adicionado em JsonHttpRequest:

public class JsonHttpRequest extends JsonHttpResponseHandler {
...

@Override
public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
try{
presenter.showUpdateAppDialog( response.getInt("version") );
onSuccess(statusCode, headers, response.getJSONArray("jaguars"));
}
catch(JSONException e){}
}

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

for( int i = 0; i < response.length(); i++ ){
try{
j = gson.fromJson( response.getJSONObject( i ).toString(), Jaguar.class );
jaguars.add( j );
}
catch(JSONException e){}
}
presenter.updateListaRecycler( jaguars );
}
...
}

 

Ainda temos de atualizar o código na camada apresentadora, mas já temos em response.getInt("version") o acesso a versão atual do aplicativo, digo, versão atual na Play Store, porém definida em nosso backend Web.

Atualização na camada apresentadora

Na classe Presenter é que vamos colocar o código de comparação de versão e assim o acionamento do dialog da atividade principal.

Adicione os seguintes métodos a Presenter:

...
private PackageInfo packageInfo(){
PackageInfo pinfo = null;
try {
String packageName = getContext().getPackageName();
pinfo = getContext().getPackageManager().getPackageInfo(packageName, 0);
}
catch(PackageManager.NameNotFoundException e){}

return pinfo;
}

public void showUpdateAppDialog( int actuallyAppVersion ){
int versionNumber = packageInfo().versionCode;

if( actuallyAppVersion > versionNumber ){

activity.showUpdateAppDialog();
}
}
...

 

packageInfo() será utilizado em todas as outras estratégias, isso para não ficarmos repetindo o trecho de código dele.

showUpdateAppDialog() realiza a comparação de versão para saber se já existe uma mais atual disponível, se sim o dialog da atividade principal é acionado.

Adição da classe de controle de informação de atualização

Antes de partirmos para os testes com essa primeira estratégia, vamos adicionar uma nova classe ao pacote model, uma que vai permitir somente apresentarmos o dialog de informe de atualização depois de um certo tempo de ele já ter sido apresentado.

Essa técnica somente é útil caso a atualização do aplicativo, em seu domínio do problema, seja opcional.

Segue código da classe SPLocalBase:

public class SPLocalBase {
private static final String PREF = "PREFERENCES";
private static final String TIME_KEY = "time";
private static final long DELAY = 24*60*60*1000; /* 24 HORAS */

private static void saveTime(Context context){
SharedPreferences sp = context.getSharedPreferences(PREF, Context.MODE_PRIVATE);
sp.edit().putLong(TIME_KEY, System.currentTimeMillis() + DELAY).apply();
}

public static boolean is24hrsDelayed(Context context){
SharedPreferences sp = context.getSharedPreferences(PREF, Context.MODE_PRIVATE);
Long time = sp.getLong(TIME_KEY, 0);
Long timeCompare = System.currentTimeMillis();

if( time < timeCompare ){
saveTime(context);
return true;
}
return false;
}
}

 

O SP é de SharedPreferences. Veja que toda a atualização de "último tempo de acesso" é feita diretamente na classe, sem necessidade de invocação explícita por parte de código cliente.

Nesta estratégia somente utilizaremos em Presenter o método is24hrsDelayed(), segue atualização:

...
public void showUpdateAppDialog( int actuallyAppVersion ){
int versionNumber = packageInfo().versionCode;

if( actuallyAppVersion > versionNumber
&& SPLocalBase.is24hrsDelayed(activity) ){

activity.showUpdateAppDialog();
}
}
...

 

Assim vamos aos testes.

Testes e resultados

Abra o emulador ou utilize um device real. Não deixe de definir no backend Web uma versão superior a versão atual do aplicativo. Em seguida execute o App. Deverá ter a seguinte tela: 

Apesar da definição de setCanceledOnTouchOutside(false) na MainActivity, a atualização de nosso aplicativo não é algo que deve acontecer com alto rigor. Por isso temos a opção de sair pelo label "DEPOIS".

Mas caso seu domínio seja diferente, simplesmente remova todo o código de setNegativeButton().

Clicando em "ATUALIZAR" no emulador AVD temos a seguinte tela: 

A página Web, na Play Store, do aplicativo de package referenciado em código, é aberta. Em nosso caso estou utilizando o package name do aplicativo do Blog, pois utilizando o do aplicativo de testes temos:

Note que se você reabrir o aplicativo, devido ao delay de 24 horas que definimos, o dialog não será apresentado até o delay ser esgotado.

Agora voltando a referência a um package que existe na Play Store, br.thiengocalopsita, executando o aplicativo em um device real e abrindo a página para atualização, temos:

Dessa vez a abertura foi da página do aplicativo no App da Google Play Store.

Note que apesar de termos trabalhado as atualizações nas camadas do padrão MVP, o que você tem que internalizar é o fluxo da estratégia, onde já tínhamos uma conexão remota com o backend de nosso aplicativo e assim somente decidimos retornar também a versão atual dele, do aplicativo na Play Store.

O uso de padrão de arquitetura, o uso de library específica de comunicação com a Internet, entre outros... é opcional. Assim podemos seguir com as próximas estratégias.

Estratégia de atualização com JSOUP API

Com a library JSOUP nós podemos obter o código de versão do aplicativo direto de um site de nosso controle, assim não teremos de lhe dar com o problema de não sermos proprietários da estrutura HTML de algum outro site.

Ou podemos acessar o código de versão do aplicativo diretamente da página dele no site da Google Play Store, porém aqui teremos de ficar vigiando o aplicativo para que ele seja atualizado rapidamente assim que a mudança na estrutura HTML do site da Play Store interfira na verificação interna de versão do aplicativo.

Como já apresentamos uma estratégia que utiliza um backend de nosso controle, aqui vamos ao trabalho com a página da Play Store.

O fluxo será como o do diagrama a seguir:

Note que caso seu aplicativo tenha mais de uma versão para responder a diferentes dispositivos e versões do Android, essa estratégia de verificação na página Web da Play Store não funcionará, pois não será exibido nenhum código de versão nessa página.

Estudando a estrutura HTML da página

Acessando a página do aplicativo na Play Store, junto ao debugador HTML do Chrome, temos o seguinte:

A tag que precisamos selecionar no JSOUP e então acessar o conteúdo, essa pode ser acessada pelo seguinte seletor: "div[itemprop=\"softwareVersion\"]". Period! É isso que faremos.

Atualização no backend Web

Antes de iniciarmos os novos códigos no Android, volte o código backend de /ctrl/CtrlAdmin.php para a versão antes da última atualização nele, retornando um JSONArray e não um JSONObject:

...
if( strcasecmp( $dados['metodo'], 'get-jaguars' ) == 0 ){
/*
* Delay para que o ProgressBar seja apresentado no lado Android
* */
sleep(1);

$jaguarsJson = AplAdmin::getJaguarsJson();
echo $jaguarsJson;
}

Atualização Gradle

No Gradle App Level, ou build.gradle (Module: app), adicione a referência em destaque:

...
dependencies {
...

/* PARA APLICARMOS O PARSER HTML */
compile 'org.jsoup:jsoup:1.10.2'
}

 

Logo depois sincronize o projeto.

Atualização da camada de modelo

Precisamos de uma classe que permita que o uso do JSOUP seja em uma Thread de background e que o retorno para a Thread principal seja em um passo simples. Sendo assim vamos criar uma nova classe que herda de AsyncTask.

No pacote model adicione a classe VersionRequester:

public class VersionRequester extends AsyncTask<Void, Void, String> {
private WeakReference<Presenter> presenter;

public VersionRequester( Presenter p ){
presenter = new WeakReference<>( p );
}

@Override
protected String doInBackground(Void... voids) {
String version = null;
try{
version = Jsoup
.connect("https://play.google.com/store/apps/details?id=br.thiengocalopsita")
.get()
.select("div[itemprop=\"softwareVersion\"]")
.text()
.trim();/* REMOVENDO OS ESPAÇOS EM BRANCO DAS LIMITAÇÕES DA STRING */
}
catch (IOException e){}

return version;
}

@Override
protected void onPostExecute(String version) {
super.onPostExecute(version);

if( presenter.get() != null ){
presenter.get().showUpdateAppDialog( version );
}
}
}

 

Note que dessa vez vamos estar obtendo a versão String do código de versão, pois é essa que fica disponível na página Web na Play Store.

Como é sempre a instância de Presenter que verifica se deve ou não ser apresentado o dialog de nova versão, vamos trabalhar aqui com uma referência fraca, WeakReference, a essa instância. Fazendo assim o possível para evitarmos vazamento de memória.

Ressaltando que devido a estarmos em um aplicativo de exemplo, para conferir versão com dados da Play Store estou utilizando a referência ao aplicativo do Blog, por isso o uso do package br.thiengocalopsita.

Agora a atualização da classe JsonHttpRequest:

public class JsonHttpRequest extends JsonHttpResponseHandler {
...

@Override
public void onSuccess(int statusCode, Header[] headers, JSONArray response) {
...

new VersionRequester(presenter).execute();
}
...
}

 

Lembrando que somente o onSuccess() de JSONArray é que será invocado pela AsyncHttp API.

Assim respeitamos o fluxo apresentado inicialmente para esta estratégia.

Atualização da camada apresentadora

Na classe Presenter vamos adicionar uma sobrecarga ao método showUpdateAppDialog(), uma que recebe uma String como parâmetro:

...
public void showUpdateAppDialog( String actuallyAppVersion ){
String versionName = packageInfo().versionName;

if( !actuallyAppVersion.equals(versionName)
&& SPLocalBase.is24hrsDelayed(activity) ){

activity.showUpdateAppDialog();
}
}
...

 

Dessa vez somente verificamos se a versão é diferente, se sim podemos assumir que a diferença é devido a uma nova versão presente na Play Store.

Lembrando que temos duas maneiras de acessar e informar a versão atual do aplicativo, por String via versionName e por int via versionCode.

Devido a todo o código que adicionamos na estratégia de API proprietária, pouco código foi adicionado nesta estratégia com uso da JSON API.

Testes e resultados

Os resultados dos testes aqui são similares ao da estratégia anterior, logo, vamos apenas apresentar o aplicativo depois de estar em execução e então identificado que as versões não são as mesmas:

Note que apesar do fluxo apresentado, você poderia, sem problema algum, altera-lo, digo, a requisição via JSOUP, por exemplo, poderia ser em paralelo a requisição ao nosso backend Web. Neste caso a classe Requester é que receberia o código de requisição com API JSOUP:

public class Requester {
...

public void retrieveJaguars() {
...

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

new VersionRequester(presenter).execute();
}
}

Estratégia de atualização com Notificação Push

Caso você não tenha um backend Web para seu aplicativo, uma excelente opção para "informe de atualização de versão" é utilizando um serviço de notificação push.

Você pode optar por vários serviços de push message disponíveis, aqui vamos utilizar um simples na instalação e simples no uso, o serviço do OneSignal. Serviço que já falamos sobre no artigo a seguir: OneSignal Para Notificações em Massa no Android.

Caso queira seguir com o uso do OneSignal, vá ao artigo e veja como instala-lo, pois aqui somente mostrarei as modificações necessárias no código Android.

Com essa estratégia teremos o seguinte fluxo:

Atualização Gradle

No Gradle App Level, ou build.gradle (Module: app), adicione as configurações em destaque:

android {
compileSdkVersion 25
buildToolsVersion "24.0.3"
defaultConfig {
...
manifestPlaceholders = [manifestApplicationId: "${applicationId}",
onesignal_app_id: "seu_onesignal_app_id",
onesignal_google_project_number: "seu_sender_project_id"]
}
...
}

dependencies {
...

/* PARA UTILIZARMOS O ONESIGNAL */
compile 'com.onesignal:OneSignal:3.+@aar'
compile 'com.google.android.gms:play-services-gcm:+'
}

 

Logo depois sincronize o projeto.

Atualização da camada de modelo

Nossa lógica de negócio, quando trabalhando com um serviço de notificação push, vai se basear nos seguintes passos:

  • Obter o número de versão do aplicativo via push message;
  • Salvar o número de versão no SharedPreferences;
  • Assim que o aplicativo for aberto, comparar as versões e assim tomar as ações necessárias.

Logo, nossa primeira atualização na camada de modelo é na classe SPLocalBase. Vamos adicionar o código que permite a persistência do número de versão que vai chegar via notificação push:

public class SPLocalBase {
private static final String PREF = "PREFERENCES";
private static final String TIME_KEY = "time";
private static final String VERSION_KEY = "version";
private static final long DELAY = 24*60*60*1000;
...

public static void saveVersion(Context context, int version){
SharedPreferences sp = context.getSharedPreferences(PREF, Context.MODE_PRIVATE);
sp.edit().putInt(VERSION_KEY, version).apply();
}

public static int getVersion(Context context){
SharedPreferences sp = context.getSharedPreferences(PREF, Context.MODE_PRIVATE);
return sp.getInt(VERSION_KEY, 0);
}
}

 

Código simples, certo? Algo similar aos getters e setters que frequentemente utilizamos em classes.

Como estaremos acessando dados que vão vir da rede, vamos criar uma nova classe na camada de modelo, na verdade um Service. A seguir o código de CustomNotificationExtenderService:

public class CustomNotificationExtenderService extends NotificationExtenderService {
@Override
protected boolean onNotificationProcessing(OSNotificationReceivedResult notification) {
try {
SPLocalBase.saveVersion(this, notification.payload.additionalData.getInt("version") );
}
catch (JSONException e) {
e.printStackTrace();
}

return true;
}
}

 

Com o OneSignal podemos trabalhar o Service NotificationExtenderService para criarmos o nosso próprio serviço de recepção de mensagens push.

Quando retornamos true em onNotificationProcessing() estamos informando para a OneSignal API que não é para ela tomar nenhuma atitude quanto a nova push message que chegou, nem mesmo apresenta-la na status bar.

Isso, pois o true indica que já nos responsabilizamos pelo processamento dessa messagem.

Atualização manifest

No AndroidManifest somente temos de definir a tag do novo Service que adicionamos:

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

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

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

<service
android:name=".model.CustomNotificationExtenderService"
android:exported="false">
<intent-filter>
<action android:name="com.onesignal.NotificationExtender" />
</intent-filter>
</service>
</application>
</manifest>

Atualização da camada apresentadora

Na classe Presenter vamos adicionar uma nova sobrecarga para o método showUpdateAppDialog(), dessa vez uma versão sem parâmetros. Segue:

...
public void showUpdateAppDialog(){
int versionNumber = packageInfo().versionCode;
int versionToCompare = SPLocalBase.getVersion(getContext());

if( versionToCompare > versionNumber ){
&& SPTimer.is24hrsDelayed(activity) ){

activity.showUpdateAppDialog();
}
}
...

 

Exatamente como indicado nos passos de lógica: acessando o número de versão direto do SharedPreferences.

Atualização da camada de visualização

No método onCreate() da MainActivity vamos adicionar o código de inicialização da OneSignal API e logo em seguida o código de invocação do método de verificação de versão, a nova sobrecarga de showUpdateAppDialog():

...
@Override
protected void onCreate(Bundle savedInstanceState) {
...

OneSignal.startInit(this).init();
presenter.showUpdateAppDialog();
}
...

 

Note que para essa estratégia e todas as outras poderem ser testadas sem inconsistências, é preciso remover o aplicativo do device de testes, mesmo esse sendo um emulador, e depois instalar a nova versão dele.

Para o teste desta última estratégia funcionar sem problemas, também é preciso a remoção do código de new VersionRequester(presenter).execute() do método onSuccess() da classe JsonHttpRequest.

LocalBroadcastManager para informe quando o aplicativo estiver em uso

Antes de prosseguir para os testes, vamos adicionar o código que permitirá que o dialog de informe seja apresentado ao usuário mesmo se ele estiver, no momento, utilizando o aplicativo.

No package presenter adicione a classe PresenterLB:

public class PresenterLB extends BroadcastReceiver {
public static final String FILTER_KEY = "PresenterLB";
private Presenter presenter;

public PresenterLB( Presenter p ){
presenter = p;
}

@Override
public void onReceive(Context context, Intent intent) {
presenter.showUpdateAppDialog();
}
}

 

Uma típica classe para trabalho com mensagens broadcast locais. Caso não conheça esse modo de comunicação no Android, logo depois deste artigo, não deixe de acessar o seguinte conteúdo: Como Utilizar o LocalBroadcastManager Para Comunicação no Android.

Assim devemos atualizar o código da classe Presenter para ativar o broadcast PresenterLB:

public class Presenter {
...

public void setActivity(MainActivity activity){
this.activity = activity;
initLocalBroadcast();
}

private void initLocalBroadcast(){
PresenterLB broadcast = new PresenterLB(this);
IntentFilter intentFilter = new IntentFilter( PresenterLB.FILTER_KEY );
LocalBroadcastManager
.getInstance(activity)
.registerReceiver( broadcast, intentFilter );
}
...
}

 

Com isso somente precisamos atualizar o código de CustomNotificationExtenderService para que o LocalBroadcast seja acionado assim que uma nova push notification chegue e o App esteja aberto:

public class CustomNotificationExtenderService extends NotificationExtenderService {
@Override
protected boolean onNotificationProcessing(OSNotificationReceivedResult notification) {
try {
SPLocalBase.saveVersion(this, notification.payload.additionalData.getInt("version") );

/* NECESSÁRIO PARA QUANDO O APLICATIVO ESTIVER ABERTO */
Intent intent = new Intent( PresenterLB.FILTER_KEY );
LocalBroadcastManager.getInstance( this ).sendBroadcast( intent );
}
catch (JSONException e) {
e.printStackTrace();
}

return true;
}
}

 

Fique tranquilo quanto ao contexto "aplicativo não aberto", nenhuma Exception será gerada devido a esse estado. Com isso podemos seguir para os testes.

Testes e resultados

Diferente das outras estratégias, vamos aqui executar o aplicativo e então somente depois de o App estar aberto é que enviaremos uma notificação push.

Depois do aplicativo já aberto, vá ao OneSignal e crie uma nova push message no dashboard dele:

  • No menu lateral clique em "New Message";
  • No formulário carregado, coloque qualquer coisa nos campos "Title" e "Content";
  • Expanda "Options" e clique no botão "no" de "Include Additional Data?";
  • No campo "key" coloque version e no campo "data" coloque 2;
  • Clique em "Preview";
  • Em seguida clique em "Send".

Assim deverá ter uma tela similar a seguinte:

O lado negativo dessa versão é que mesmo o servidor de push message do Google sendo utilizado no background, não é garantido que a mensagem será entregue a todos os devices, porém devido ao "silêncio" que estamos tratando a nova push message, podemos enviar várias para aumentar a possiblidade de entrega.

Com isso finalizamos a apresentação das estratégias para o "informe de nova versão disponível do aplicativo".

Apesar do artigo ter sido grande, as técnicas são todas simples e válidas para quando se tem ou não um backend Web.

Não se esqueça de se inscrever na lista de emails do Blog logo abaixo (ou ao lado) para receber os conteúdos em primeira mão. Se inscreva também no canal YouTube Thiengo Calopsita.

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

Abaixo o vídeo com a implementação passo a passo das estratégias de apresentação de nova versão de aplicativo:

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

Conclusão

Sabendo da necessidade de informar ao usuário sobre uma nova versão do aplicativo, implemente algum algoritmo de comparação e informe de versão. Os códigos são simples e rapidamente os usuários do APP estarão migrando para a versão mais atual dele.

Você pode aprimorar ainda mais o algoritmo de verificação. Trabalhando com mais dados de comparação é possível saber se o informe atual é de uma atualização que realmente deve acontecer ou apenas uma opcional.

No primeiro caso não seria fornecido ao usuário uma maneira de continuar utilizando o aplicativo sem que a atualização ocorresse.

Essa mesma técnica de apresentação de "dialog de nova versão" é utilizada também para outros contextos. Pesquisas de satisfação, por exemplo.

Qualquer dúvida ou sugestão, não deixe de comentar logo abaixo. Se conhece alguma outra maneira de informar sobre a nova versão de aplicativo, sinta-se a vontade em informar nos comentários. Não se esqueça de se inscrever na lista de emails do Blog.

Abraço.

Fontes

OneSignal Para Notificações em Massa no Android

Atualizar apps transferidos

Stackoverflow: Programmatically check Play Store for app updates - Primeira resposta

Stackoverflow: Programmatically check Play Store for app updates - Segunda resposta

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

Relacionado

Como Colocar Notificações Bolha em Seu Aplicativo AndroidComo Colocar Notificações Bolha em Seu Aplicativo AndroidAndroid
MVP AndroidMVP AndroidAndroid
Como Utilizar o LocalBroadcastManager Para Comunicação no AndroidComo Utilizar o LocalBroadcastManager Para Comunicação no AndroidAndroid
Como Construir Aplicativos Android Com HTML e JSOUPComo Construir Aplicativos Android Com HTML e JSOUPAndroid

Compartilhar

Comentários Facebook (2)

Comentários Blog (2)

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...
Rodrigo (1) (0)
20/08/2017
Muito bom o conteúdo. E se o app não estiver na playstore como fazer esse aviso de atualização?
1 - Como enviar um alerta da nova versão entre o app e o servidor web?
2 - A versão instalada 1.0, no servidor crio uma pasta update, como o app pode verificar se nesta pasta tem a versão 1.1?
É possivel?
obrigado
Responder
Vinícius Thiengo (0) (0)
24/08/2017
Rodrigo, tudo bem?

Na dúvida um, o alerta pode ser enviado utilizando algum serviço de notificação push, como o OneSignal na terceira estratégia demonstrada.

Na dúvida dois você deve prosseguir como na primeira estratégia: ter uma API de conexão remota vinculada ao Android e assim colocar o algoritmo de verificação backend para acessar algum arquivo de seu folder /update e então verificar nele qual a versão atual disponível.

Em ambos os casos você deve divulgar o link oficial de download do APP, pois sem ele estar em alguma loja de apps autorizada pelo Google o download da nova versão terá de ser manual.

Abraço.
Responder