Facilitando o Desenvolvimento de Apps Android Com a Biblioteca AndroidUtilCode

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 /Facilitando o Desenvolvimento de Apps Android Com a Biblioteca AndroidUtilCode

Facilitando o Desenvolvimento de Apps Android Com a Biblioteca AndroidUtilCode

Vinícius Thiengo04/07/2017
(2440) (2) (188) (23)
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, tudo bem?

Neste artigo vamos trabalhar a biblioteca AndroidUtilCode, em um domínio de problema real, para apresentar o potencial desta library na diminuição de nosso tempo de desenvolvimento de aplicativos Android, isso, pois ela nos fornece funcionalidades comuns em poucas linhas de código.

O projeto de exemplo será similar ao que frequentemente temos no dev Android quando há um projeto Web já em produção, incluindo a responsividade de layout, porém temos pouco tempo para o desenvolvimento da versão mobile.

Aqui utilizaremos um site de resultados de jogos de futebol: 

Como nos últimos artigos apresentados, neste também vamos seguir com a linguagem Kotlin. Caso ainda não a conheça, acesse primeiro o artigo, com vídeo, a seguir: Kotlin Android, Entendendo e Primeiro Projeto.

Abaixo os tópicos que estaremos abordando:

Biblioteca AndroidUtilCode

A biblioteca AndroidUtilCode é uma espécie de library wrapper, ou seja, ela encapsula a codificação comum e pesada de algumas APIs comumente utilizadas no desenvolvimento de aplicativos Android e assim nos permite o uso dessas mesmas APIs com poucas linhas de código.

Como no artigo anterior, novamente estou falando sobre uma biblioteca de terceiro devido a aceitação desta na comunidade, mais de 11.000 estrelas no GitHub, e também devido ao potencial dela em nos permitir uma menor codificação quando em desenvolvimento, consequentemente gerando ganho de tempo.

A library é de origem chinesa e foi criada em 2016 por Blankj, que aparentemente é uma pessoa. Para acesso a documentação da biblioteca, entre no GitHub a seguir: https://github.com/Blankj/AndroidUtilCode.

Configurações iniciais

Para uso da AndroidUtilCode primeiro devemos atualizar o Gradle App Level, ou build.gradle (Module: app), com a seguinte referência:

...
dependencies {
...
compile 'com.blankj:utilcode:1.7.1'
}
...

 

Observação: Até a construção deste artigo, a versão 1.7.1 era a versão mais atual da library.

Depois é preciso construir uma classe de aplicação customizada e então colocar o código de inicialização da biblioteca no onCreate() desta classe:

class MyApplication : Application() {

override fun onCreate() {
super.onCreate()

Utils.init(this)
}
}

 

Ainda é preciso referenciar a nova classe de aplicação no AndroidManifest.xml do projeto:

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

 

Agora a library está pronta para ser consumida em código.

Utilizando a biblioteca

Em um domínio do problema onde é preciso uma requisição remota que pode levar alguns minutos, podemos estar priorizando uma conexão WiFi para isso, assim não será consumida a banda 3G do usuário.

Assim podemos seguramente verificar se há esse tipo de conexão disponível no device, com o seguinte trecho de código:

...
if( NetworkUtils.isWifiConnected() ){
startDownloadDados()
}
...

 

Dependendo da interface utilitária pública que você estiver utilizando, algumas permissões serão necessárias, caso contrário uma Exception é gerada.

O código anterior exige as seguintes permissões no AndroidManifest.xml:

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

 

Nenhuma delas é uma permissão perigosa, que exige a solicitação em tempo de execução, mas caso haja alguma de acordo com a interface pública em uso, coloque também o código de solicitação de permissão em seu projeto, aliás esse é um ponto fraco da biblioteca, mesmo sendo extensa, não há uma interface para solicitação de permissão em tempo de execução, ao menos até a construção deste artigo não existia alguma.

Como informado anteriormente: a biblioteca AndroidUtilCode é extensa. Logo, para uma descrição completa de todas as APIs dentro dessa library, não deixe de acessar o GitHub dela em: https://github.com/Blankj/AndroidUtilCode.

Projeto Android de exemplo

Nosso projeto de exemplo para consumo da library em estudo é um de resultados de jogos de futebol, simulando um projeto Web que deve ser trabalhado também em aplicativo Android.

O site que será utilizado já é um conhecido pelos seguidores do Blog e canal, o Super Placar. Será possível carrega-lo em um WebView, apresentar algumas limitações do projeto e assim partir para o uso da AndroidUtilCode para a construção de soluções as limitações.

Caso queira acesso direto aos fontes do projeto, incluindo os arquivos de imagem e de configuração de IDE, acesse o GitHub dele em: https://github.com/viniciusthiengo/super-placar-web-version. Mesmo assim não deixe de seguir o artigo, ou o vídeo, para ter o uso da library em um projeto similar a um em produção.

Em seu Android Studio, crie um novo projeto Android, com uma Empty Activity e com o nome "SuperPlacar Web Version".

Caso você esteja com o Android Studio 3+ já inicie com um projeto Kotlin. Caso contrário, aplique as configurações apresentadas no artigo Kotlin Android, Entendendo e Primeiro Projeto para ter um projeto Kotlin.

Nessa primeira parte vamos apenas construir o projeto inicial, sem uso da AndroidUtilCode. Posteriormente apresentaremos algumas funcionalidades desejadas e limitações e assim partiremos para o uso da biblioteca.

Ao final desta primeira parte teremos o seguinte aplicativo:

E estrutura de projeto em IDE:

Configurações Gradle

Para o Gradle Project Level, ou build.gradle (Project: SuperPlacarWebVersion), temos, além da configuração inicial, a configuração necessária para uso do Kotlin:

buildscript {
ext.kotlin_version = '1.1.3'
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

allprojects {
repositories {
jcenter()
}
}

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

 

Caso na época em que você esteja consumindo este conteúdo haja uma versão mais atual do Kotlin ou do Gradle, siga com a versão mais atual, pois não haverá problemas com o projeto de exemplo.

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

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
compileSdkVersion 26
buildToolsVersion "26.0.0"
defaultConfig {
applicationId "br.com.thiengo.thiengowebblog"
minSdkVersion 15
targetSdkVersion 26
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:26.+'
testCompile 'junit:junit:4.12'
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
}
repositories {
mavenCentral()
}

 

Os códigos em destaque são todos necessários ao Kotlin. Voltaremos ao Gradle App Level para adicionar outras libraries que serão necessárias ao nosso projeto.

Estamos com a minSdkVersion em 15, pois essa é a versão mínima de trabalho com a AndroidUtilCode que adicionaremos na segunda parte do artigo.

Configurações AndroidManifest

A seguir o código do AndroidManifest.xml que somente teve adicionado, por enquanto, 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.thiengowebblog">

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

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

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

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

Configurações de estilo

Para os arquivos de estilo, vamos iniciar com o de cores, /res/values/colors.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorBackground">#140c23</color>
<color name="colorPrimary">#5b5566</color>
<color name="colorPrimaryDark">#4e4a58</color>
<color name="colorAccent">#ecc835</color>
</resources>

 

A seguir o simples arquivo de String, /res/values/strings.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">SuperPlacar Web Version</string>
</resources>

 

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

<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="Theme.AppCompat.NoActionBar">

<item name="android:windowBackground">@color/colorBackground</item>
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>

 

Note que estamos utilizando Theme.AppCompat.NoActionBar para não termos barra de topo em um aplicativo que tem todo o conteúdo em um WebView.

Pacote extra

O pacote /extra terá somente uma classe, UrlUtil. Nela colocaremos, posteriormente na aplicação da library AndroidUtilCode, alguns métodos para trabalhar com a API de SharedPreferences da biblioteca.

De início essa classe somente tem a referência a url do projeto Web que será carregada em nosso aplicativo de exemplo. Segue código:

class UrlUtil {
companion object{
@JvmField val URL = "http://superplacar.com.br/"
}
}

 

Estamos fazendo o uso do companion object para podermos realizar o acesso a propriedade URL similar ao acesso a um atributo estático no Java. O @JvmField é para que essa propriedade seja sim trabalhada como um dado estático pelo JVM.

Classe de domínio

Nossa classe de domínio inicial, no pacote /domain, é a CustomWebViewClient, que tem como "missão" evitar que o navegador padrão do device do usuário seja aberto quando houver a solicitação de carregamento de outra página e também para que possamos apresentar / esconder um ProgressBar na tela, esse responsável por mostrar ao usuário que o conteúdo ainda está sendo carregado.

Segue código:

class CustomWebViewClient(val activity: MainActivity) : WebViewClient() {

override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
activity.fl_page_load.visibility = View.VISIBLE
}

@TargetApi(Build.VERSION_CODES.M)
override fun onPageCommitVisible(view: WebView?, url: String?) {
super.onPageCommitVisible(view, url)
activity.fl_page_load.visibility = View.GONE
}

override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
activity.fl_page_load.visibility = View.GONE
}

@TargetApi(Build.VERSION_CODES.N)
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?) = false

@SuppressWarnings("deprecation")
override fun shouldOverrideUrlLoading(view: WebView?, url: String?) = false
}

 

Note que como estamos utilizando também o plugin kotlin-android-extensions, referenciado no Gradle App Level, podemos facilmente acessar as Views da atividade em trabalho, aqui a MainActivity, somente utilizando a instância da activity e então o ID da View desejada.

Por isso temos uma propriedade preenchida, no construtor primário, com uma instância da atividade principal de nosso projeto, a que contém o WebView e também as Views que controlam a apresentação do "loading" em tela.

O método onPageCommitVisible() foi adicionado ao Android somente a partir da API 23, por isso estamos utilizando a anotação @TargetApi(Build.VERSION_CODES.M) para que o code inspector do IDE não sinalize problema no uso deste método, pois a API mínima suportada em nosso aplicativo de exemplo é a 15.

O método onPageCommitVisible() é invocado assim que o conteúdo da página solicitada em WebView já começa a ser estruturado em tela, com isso podemos remover a View de load no momento ideal.

Isso, pois o método onPageFinished() tende a demorar muito para ser invocado, mesmo quando todos os componentes visuais da página já estão em tela pode haver alguns carregamentos ainda acontecendo no JavaScript, por exemplo.

Ok, mas então por que utilizar também uma versão de "esconder load View" com o onPageFinished()?

Essa é uma estratégia para que o "esconder load View" também funcione em devices com o Android inferior a versão 23, Marshmallow, pois nessas versões o método onPageCommitVisible() não funcionará.

Não há problemas quanto a isso, digo, sobrescrever um método que não há em outras versões do Android?

Não da maneira que estamos fazendo, o método que não existe em alguma versão simplesmente será ignorado nela, isso, pois nunca haverá invocação interna para esse método.

Invocação interna?

Sim, quando não é o algoritmo que construímos que invoca diretamente o método e sim os algoritmos internos das APIs Android.

Caso fosse algum dos trechos de nosso código que estivesse realizando a invocação ao método, então teríamos de ter ao menos um condicional de verificação de versão de Android para então saber se é ou não possível prosseguir com a invocação. Condicional como à seguir:

...
if( android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M ){
// TODO
}
...

 

Para os métodos shouldOverrideUrlLoading(), que têm o código que evita o carregamento de novo conteúdo em um navegador externo ao app, já colocamos a sobrescrita para a versão anterior ao Android N, API 24, e também para a versão a partir do Android N, assim não teremos problemas com novas versões do Android.

Atividade principal

Nossa atividade principal é simples, ao menos nesta primeira parte, contém poucas Views, pois o pesado da apresentação da página é realizado no backend Web do site Super Placar.

Vamos iniciar com o layout, alias, dessa vez são dois layouts. Segue o primeiro, /res/layout/activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="br.com.thiengo.thiengowebblog.MainActivity">

<FrameLayout
android:id="@+id/fl_page_load"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorBackground"
android:visibility="gone">

<ProgressBar
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center" />
</FrameLayout>

<WebView
android:id="@+id/wv_site"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>

 

Abaixo o diagrama do layout anterior:

Esse primeiro layout será utilizado somente nos devices com o Android abaixo da API 23. Isso, pois para esses devices o método da CustomWebViewClient que será utilizado para esconder a View de load, aqui o container FrameLayout com ID fl_page_load, será o onPageFinished().

Como informado em sessão anterior: o método onPageFinished() pode ter um delay alto de invocação, mesmo quando todos os elementos visuais já estão em página.

Com esse layout, tendo como container um FrameLayout e o WebView vindo logo depois da View que controla a apresentação do ProgressBar, a apresentação da página será muito similar quando em devices com o Android M ou superior.

Assim que o conteúdo começar a ser carregado para renderização, a página já vai tomar o lugar do ProgressBar, mesmo que esse ainda fique sendo utilizado "por debaixo dos panos", atrás do WebView.

Com isso podemos ir a versão dois do layout, somente para devices com o Android M ou superior. Segue /res/layout-v23/activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="br.com.thiengo.thiengowebblog.MainActivity">

<WebView
android:id="@+id/wv_site"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<FrameLayout
android:id="@+id/fl_page_load"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorBackground"
android:visibility="gone">

<ProgressBar
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center" />
</FrameLayout>
</FrameLayout>

 

A seguir o diagrama do layout anterior:

Ok, mas por que não já utilizou o primeiro activity_main.xml para todas as versões do Android?

Porque mesmo que não esteja evidente, tem um problema na primeira versão de layout, problema que somente é notado assim que não mais há conexão com a Internet no device e o código de abertura de página de recarregamento já está configurado, código que colocaremos somente na segunda parte do artigo.

O problema é que quando a Internet volta e o recarregar é acionado, rapidamente, por poucos segundos, fica sendo apresentado em tela um código informando sobre a não possibilidade de carregamento de pagina, porém ela na verdade está sendo sim recarregada, logo depois é apresentada.

Com o layout criado somente para versões do Android iguais ou acima da API 23 esse código não é apresentado, não com o delay de segundos. Porém com o layout anterior, para Android API abaixo da 23, ele aparece, mesmo que pouco notável por ser de fundo transparente.

A solução mais simples para isso seria sempre utilizar o segundo layout apresentado aqui, mas ainda cairíamos no problema de delay do "esconder View de load" da tela para as versões de Android abaixo da API Marshmallow.

Aqui, em nosso domínio do problema de exemplo, como o código de aviso de "não carregamento de página" não tem contraste com a cor de background, azul escuro, optei por seguir com essa estratégia, dando prioridade ao "esconder View de load".

Mas em seu projeto você é que escolhe como seguir, inclusive pode até mesmo utilizar uma lógica de negócio completamente diferente.

Assim podemos ir ao código Kotlin da MainActivity:

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

wv_site.settings.javaScriptEnabled = true
wv_site.setBackgroundColor( android.R.color.transparent )
wv_site.webViewClient = CustomWebViewClient(this)
wv_site.loadUrl( UrlUtil.URL )
}

override fun onBackPressed() {
if( wv_site.canGoBack() ){
wv_site.goBack()
}
else{
super.onBackPressed()
}
}
}

 

Note que sobrescrevemos o onBackPressed() para que seja possível voltar a página no WebView assim que o usuário acionar o back button do device dele. Sem esse algoritmo o aplicativo seria fechado, algo provavelmente não esperado pelo usuário depois de ter navegado por algumas outras telas.

Assim terminamos a parte inicial de nosso projeto e podemos partir para algumas atualizações devido a limitações existentes.

Evoluindo o aplicativo com uso da biblioteca AndroidUtilCode

Nossa meta principal com a integração da AndroidUtilCode é permitir que o usuário consiga recarregar a página, que ele queria acessar anteriormente, caso a conexão com a Internet falhe e logo depois volte a funcionar.

Porém dentro desse "recarregar a página" tem de ter a apresentação de uma tela amigável ao usuário, tela informando sobre a não existência de conexão com a Internet. É preciso também saber qual página recarregar, além de permitir que o usuário consiga navegar de volta as páginas anteriormente visualizadas.

Com isso, o que precisaremos nessa segunda parte é:

  • Código HTML para nossa página personalizada de recarregamento de conteúdo e informe de não conexão com a Internet;
  • Algoritmo verificador de conexão com a Internet, incluindo algumas permissões não perigosas;
  • Uma base local para sempre salvar a última página carregada ou que houve ao menos a tentativa de carrega-la, tentativa falha devido a não conexão com a Internet.

Vamos prosseguir com os códigos para a implementação da página de recarregamento junto a library AndroidUtilCode.

Códigos frontend da página de informe de "não há conexão com a Internet"

Vamos começar com um trecho estático de nosso projeto, o código de nossa página Web que deverá estar interno ao aplicativo para que quando não houver conexão ela possa ser apresentada sem problemas.

Essa página também terá um link, tag âncora, onde ao clique / touch do usuário deverá solicitar aos códigos Android a verificação de conexão para posterior recarregamento de página de conteúdo, isso caso haja conexão novamente.

Com a implementação atual, quando não há Internet, o que acontece é o seguinte:

Antes de prosseguir com nossos códigos frontend, saiba que a página HTML que você utilizará em seu projeto Android, para a apresentação amigável da informação de não conexão com a Internet, deverá seguir o design das páginas carregadas em seu WebView.

Assim, acesse seu projeto no Android Studio no modo "Project", logo depois expanda: /app/src/main. Clicando com o botão direito do mouse em /main, escolha "New" e em seguida clique em "Directory". Digite assets e clique em "Ok", terá algo como:

Em /assets ficarão todos os códigos frontend. Aqui vou criar ainda outros três folders. Clique com o botão direito em /assets, vá em "New" e depois clique em "Directory". Agora crie um folder com o nome html. Logo depois um outro, ainda em /assets, com o nome javascript. E por fim um com o nome image:

Como conteúdo do folder /image, temos o logo do Super Placar que será utilizado no código HTML: https://github.com/viniciusthiengo/super-placar-web-version/blob/master/app/src/main/assets/image/super-placar-logo.png.

Em /javascript temos o código jQuery para facilitar nossa codificação JavaScript na página HTML: https://github.com/viniciusthiengo/super-placar-web-version/blob/master/app/src/main/assets/javascript/jquery-3.2.1.min.js.

Tivemos de colocar o jQuery como código interno, sem uso de CDN, pois a página será apresentada somente quando não houver conexão com a Internet.

E por fim o código HTML inicial, você pode cria-lo em qualquer editor de texto e depois copia-lo e cola-lo dentro de /html. Segue:

<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="content-language" content="pt-br">
<meta name="generator" content="HTML-Kit 292">
<meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>
Sem conexão com a Internet
</title>

<style type="text/css">
body{
background: #140c23;
color: #fff;
text-align: center;
font-family: Arial, sans-serif;
}
div {
padding-top: 15%;
}
a{
display: inline-block;
border-radius: 4px;
padding: 8px 16px;
color: #fff;
border: 1px solid #fff;
text-decoration: none;
font-size: 1.2em;
}
a:active{
background: #fff;
color: #140c23;
}
</style>
</head>
<body>
<div>
<img
src="../image/super-placar-logo.png"
alt="SuperPlacar Logo" />
<br><br>

<p>
Sem conexão com a Internet, tente novamente assim que ela retornar.
</p>
<br><br>

<a href="#" title="Recarregar aplicativo">
Recarregar aplicativo
</a>
</div>

<script type="text/javascript" src="../javascript/jquery-3.2.1.min.js"></script>
</body>
</html>

 

Posteriormente voltaremos ao código HTML para adicionar o código JavaScript / jQuery que permitirá a comunicação da página com o código Kotlin Android.

Nossa estrutura /assets agora está:

A seguir a página de informe, quando carregada em um navegador desktop / laptop:

WebView de carregamento de página interna

Nossa página de informe de "não conexão com a Internet" será carregada em um outro WebView, um que ainda adicionaremos em cada um dos layouts activity_main.xml.

Por que isso?

Parte de nossa meta principal é manter o WebView de conteúdo com o histórico de páginas já carregadas, digo, com esse histórico de forma intocada.

Caso seja optado por carregar a página de informe também nesse WebView, teremos um baita trabalho para administrar em código a pilha correta de páginas já navegadas.

Uma maneira fácil de resolver esse problema é utilizar um outro WebView, um para somente carregar nossa página de informe, pois o histórico nele pouco nos interessa, até porque somente uma página será utilizada nesse novo WebView.

O que teremos de ter em código é o algoritmo de apresentar / esconder os WebViews no momento certo, seguindo o fluxograma abaixo:

No código XML de /res/layout/activity_main.xml adicione:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="br.com.thiengo.thiengowebblog.MainActivity">
...

<WebView
android:id="@+id/wv_no_internet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</FrameLayout>

 

Já iniciando escondida, android:visibility="gone", a nova WebView.

Realize a mesma atualização em /res/layout-v23/activity_main.xml. O código adicionado ficará exatamente como no layout anterior.

Ainda precisamos do algoritmo que representa o fluxograma anterior, de esconder / apresentar a WebView correta.

Para isso precisaremos também da API de verificação de conexão com a Internet, logo, vamos a configuração de AndroidUtilCode ao projeto.

Configuração AndroidUtilCode

Primeiro, no Gradle App Level do projeto, adicione a seguinte referência em negrito:

...
dependencies {
...

/* ANDROID UTIL CODE */
compile 'com.blankj:utilcode:1.7.1'
}
...

 

Agora na raíz do projeto, onde se encontra a MainActivity, crie uma nova classe Kotlin, com o nome CustomApplication e com o código de inicialização da biblioteca AndroidUtilCode:

class CustomApplication : Application() {
override fun onCreate() {
super.onCreate()
Utils.init(this)
}
}

 

Para que nossa application customizada seja utilizada, devemos ainda atualizar o AndroidManifest.xml:

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

 

Assim podemos voltar a lógica de negócio das WebViews, pois a library está configurada.

Lógica de troca de WebView

Aqui devemos primeiro definir, em UrlUtil, o path de acesso a página HTML interna:

class UrlUtil {
companion object{
@JvmField val URL = "http://superplacar.com.br/"
@JvmField val URL_NO_INTERNET = "file:///android_asset/html/no-internet.html"
}
}

 

Assim podemos, na MainActivity, colocar o código de inicialização da WebView que carregará a página interna, a wv_no_internet. Esse código vai vir no onCreate():

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

wv_no_internet.settings.javaScriptEnabled = true
wv_no_internet.setBackgroundColor( android.R.color.transparent )
wv_no_internet.loadUrl( UrlUtil.URL_NO_INTERNET )

wv_site.settings.javaScriptEnabled = true
wv_site.setBackgroundColor( android.R.color.transparent )
wv_site.webViewClient = CustomWebViewClient(this)
wv_site.loadUrl( UrlUtil.URL )
}
...
}

 

Quase bom. Temos ainda de colocar a lógica de apresentar / esconder WebView. Para isso vamos criar um novo método na MainActivity e então colocar essa responsabilidade nesse método. Segue:

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

wv_no_internet.settings.javaScriptEnabled = true
wv_no_internet.setBackgroundColor( android.R.color.transparent )

wv_site.settings.javaScriptEnabled = true
wv_site.setBackgroundColor( android.R.color.transparent )
wv_site.webViewClient = CustomWebViewClient(this)

loadPage()
}

fun loadPage(){
if( NetworkUtils.isConnected() ){
wv_no_internet.visibility = View.GONE
wv_site.loadUrl( UrlUtil.URL )
wv_site.visibility = View.VISIBLE
}
else{
wv_site.visibility = View.GONE
wv_no_internet.loadUrl( UrlUtil.URL_NO_INTERNET )
wv_no_internet.visibility = View.VISIBLE
}
}
...
}

 

O método loadPage() responde exatamente ao último fluxograma apresentado. Porém o link / botão "Recarregar aplicativo" de nossa página Web ainda não tem funcionalidade alguma, vamos a esse trecho.

Classe de conexão JavaScript Web com Kotlin Android

Para que seja possível criar uma comunicação partindo do código Web JavaScript para nosso código Kotlin Android é preciso uma classe que trabalhará como interface pública de acesso ao código Android.

No pacote /domain do projeto adicione a classe JavaScriptConnect:

class JavaScriptConnect( val activity : MainActivity ) {

companion object{
@JvmField val INTERFACE_NAME = "AndroidInstance"
}

@JavascriptInterface
fun reloadPage(){
activity.loadPage()
}
}

 

INTERFACE_NAME contém o nome da interface pública de acesso que poderá ser referenciada no código Web JavaScript.

Essa String poderia vir em qualquer lugar, mas coloquei ela em JavaScriptConnect, pois está fortemente ligada a essa entidade e também facilita a leitura do código evitando que nós tenhamos de utilizar uma "String solta" (ou valor mágico) na MainActivity quando configurando uma instância desta classe no WebView de informe, wv_no_internet.

A anotação @JavascriptInterface é necessária a partir do Android API 17, Jelly Bean, para todos os métodos que puderem ser invocados a partir de códigos JavaScript Web.

Antes de prosseguir já lhe adianto que o código de JavaScriptConnect está incompleto, isso, pois quando há no código JavaScript a invocação de um método presente no código Android, uma outra Thread é utilizada, a Thread JavaBridge.

Com isso não podemos invocar loadPage() sem antes ter acesso a Thread principal, pois loadPage() trabalha com Views já iniciadas na Thread UI.

Vamos utilizar uma API da library Anko que nos permite o fácil acesso ao Thread UI em qualquer ponto do projeto.

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

...
dependencies {
...

/* ANKO COROUTINES */
compile 'org.jetbrains.anko:anko-coroutines:0.10.1'
}
...

 

Sincronize o projeto.

Voltando a classe JavaScriptConnect, atualize como a seguir:

class JavaScriptConnect( val weakActivity : WeakReference<MainActivity> ) {

companion object{
@JvmField val INTERFACE_NAME = "AndroidInstance"
}

@JavascriptInterface
fun reloadPage(){
async(UI){
weakActivity.get()?.loadPage()
}
}
}

 

Não teremos nenhuma Exception de acesso a um componente na UI sem estar nessa Thread. O método async(), com o argumento UI, nos permite acesso a Thread principal do aplicativo.

Estamos com uma WeakReference para evitarmos ao máximo o vazamento de memória e posteriormente um OutOfMemoryException. Isso é necessário, pois o ciclo de vida entre as instâncias de JavaScriptConnect e MainActivity são diferentes, devido ao uso de Threads diferentes.

Agora devemos acrescentar o código de interface pública de comunicação ao WebView de informe, na MainActivity:

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
...

wv_no_internet.settings.javaScriptEnabled = true
wv_no_internet.setBackgroundColor( android.R.color.transparent )
wv_no_internet.addJavascriptInterface(
JavaScriptConnect( WeakReference(this) ),
JavaScriptConnect.INTERFACE_NAME
)

...
}
...
}

 

Por fim o código JavaScript / jQuery da página HTML para interceptar corretamente o clique / touch no link "Recarregar aplicativo" e então invocar o método reloadPage() de JavaScriptConnect.

Em /assets/html/no-internet.html adicione o código em destaque:

<!DOCTYPE html>
<html lang="pt-br">
...
<body>
...
<script type="text/javascript" src="../javascript/jquery-3.2.1.min.js"></script>
<script type="text/javascript">
/*
* LISTENER DE CLIQUE EM TAGS ÂNCORA DA PÁGINA
* (HÁ SOMENTE UMA EM NOSSO HTML)
* */
$('a').click( function( e ){
/*
* REMOVE O COMPORTAMENTO PADRÃO DE UMA
* TAG ÂNCORA QUANDO ACIONADA
* */
e.preventDefault();

/*
* SOMENTE TENTA O ACIONAMENTO DO reloadPage()
* CASO A INSTÂNCIA AndroidInstance EXISTA NO
* FRONTEND WEB E SE O RELOAD JÁ NÃO ESTIVER
* EM ANDAMENTO
* */
if( $.trim($(this).text()) == 'Recarregar aplicativo'
&& AndroidInstance ){

/*
* EVITA QUE O USUÁRIO CONTINUE SOLICITANDO
* O RELOAD DEPOIS DE JÁ TER SOLICITADO E
* ELE AINDA NÃO TENHA FINALIZADO
* */
$(this).text('Recarregando...');

AndroidInstance.reloadPage();
}
});
</script>
</body>
</html>

 

Com esse novo código nosso fluxograma evoluiu:

Agora sim nossa comunicação JavaScript Web com Kotlin Android deve funcionar sem problemas.

Note que ainda temos alguns pontos a corrigir para atendermos a meta principal.

Trabalhando a apresentação de página quando não há Internet durante a navegação

E quando a Internet cair durante a navegação do usuário no aplicativo? O que fazer?

Pergunto isso, pois até o momento o loadPage() é invocado somente na MainActivity e em nenhum outro ponto estratégico do projeto para proteger essa situação.

Com isso, em nossa classe CustomWebViewClient, vamos sobrescrever dois métodos que nos permitirão acionar corretamente o método loadPage() caso haja problemas com a navegação. Segue:

class CustomWebViewClient(val activity: MainActivity) : WebViewClient() {
...

@TargetApi(Build.VERSION_CODES.M)
override fun onReceivedError(
view: WebView?,
request: WebResourceRequest?,
error: WebResourceError? ) {
super.onReceivedError(view, request, error)

activity.loadPage()
}

@SuppressWarnings("deprecation")
override fun onReceivedError(
view: WebView?,
errorCode: Int,
description: String?,
failingUrl: String? ) {
super.onReceivedError(view, errorCode, description, failingUrl)

activity.loadPage()
}
}

 

Como o nome indica, onReceiverError(), assim que houver problema no carregamento da página esse método será invocado. E, como acontece com o método shouldOverrideUrlLoading(), discutido na primeira parte do artigo, temos duas versões de onReceiverError(), uma a partir da API 23 e outra para todas as versões do Android anteriores a essa API.

Para oferecer suporte a todas as APIs, sobrescreva ambas as versões.

Novamente o fluxograma passa por atualizações:

Com isso resolvemos o problema de carregamento correto de página mesmo quando a falha de conexão ocorre durante a navegação do usuário.

Assim podemos ir a um outro problema, aliás, à apresentação dele e posteriormente a solução.

Persistência local para recarregamento correto de página

Veja o código atual de loadPage() na MainActivity:

...
fun loadPage(){
if( NetworkUtils.isConnected() ){
wv_no_internet.visibility = View.GONE
wv_site.loadUrl( UrlUtil.URL )
wv_site.visibility = View.VISIBLE
}
else{
wv_site.visibility = View.GONE
wv_no_internet.loadUrl( UrlUtil.URL_NO_INTERNET )
wv_no_internet.visibility = View.VISIBLE
}
}
...

 

Mais precisamente o trecho a seguir: wv_site.loadUrl( UrlUtil.URL ).

O que ele nos indica é o seguinte:

Quando o aplicativo for aberto e houver Internet, tudo vai sair como desejado, a página inicial será carregada. Porém quando a Internet falhar enquanto o usuário estiver navegando e logo depois voltar, a página inicial será novamente carregada, mesmo com o histórico de navegação ainda presente.

Sempre a página inicial é que é a carregada quando há Internet. O correto é carregar a última página na tentativa de acesso, isso se o onCreate() não tiver sido invocado, pois com o onCreate() sendo invocado o usuário tem sim de começar na página inicial, para simular um aplicativo nativo, sem códigos Web.

O que devemos ter é uma persistência simples e local para podermos salvar, e obter, a última página em tentativa de carregamento.

Por que tentativa?

Por que mesmo quando não houver Internet, devemos salvar a página que o usuário tentou acessar, para depois recarregar ela.

Para "nossa alegria" o método sobrescrito onPageStarted() em CustomWebViewClient é acionado mesmo quando não há Internet.

O que precisamos agora é trabalhar o SharedPreferences, pela API de AndroidUtilCode, para poder persistir e recuperar a última URL.

Segue atualização na classe UrlUtil:

class UrlUtil {
companion object{
@JvmField val URL = "http://superplacar.com.br/"
@JvmField val URL_NO_INTERNET = "file:///android_asset/html/no-internet.html"
@JvmField val SP = SPUtils.getInstance("PREF")

fun saveLastUrl( url : String? ){
SP.put("url", url ?: URL)
}

fun getLastUrl() = SP.getString("url", URL)

fun clear(){
SP.clear()
}
}
}

 

Os métodos saveLastUrl() e getLastUrl() são simples de entender, incluindo a inicialização de SP, mas para que o método clear()?

Lembre que informei sobre a invocação do onCreate() da MainActivity. Caso esse método seja invocado, devemos abrir sempre a página inicial, pois assim estaremos simulando um aplicativo nativo, não deve ter histórico para este caso.

O clear() será o método que garantirá que partindo do onCreate() nós estaremos carregando a url da página inicial, pois ele será invocado nesse método da atividade principal.

Essa parte da lógica de negócio é para simular um app de código nativo, em alguma das linguagens oficiais do Android, mas você pode alterar de acordo com suas necessidades, não há problemas quanto a isso.

Assim podemos invocar o saveLastUrl() no local correto. Segue:

class CustomWebViewClient(val activity: MainActivity) : WebViewClient() {

override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
activity.fl_page_load.visibility = View.VISIBLE
UrlUtil.saveLastUrl( url )
}
...
}

 

E então as atualizações na MainActivity para trabalhar com os métodos getLastUrl() e clear():

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

UrlUtil.clear()
...
}

fun loadPage(){
if( NetworkUtils.isConnected() ){
wv_no_internet.visibility = View.GONE
wv_site.loadUrl( UrlUtil.getLastUrl() )
wv_site.visibility = View.VISIBLE
}
else{
wv_site.visibility = View.GONE
wv_no_internet.loadUrl( UrlUtil.URL_NO_INTERNET )
wv_no_internet.visibility = View.VISIBLE
}

}
...
}

 

Projeto quase pronto para os testes.

Ajustes extras

Antes de prosseguirmos para os testes, saiba que em nosso domínio do problema, caso o usuário acione o back button do device quando a página de informe estiver na tela, devemos realmente fechar o aplicativo, para isso atualize o método onBackPressed() na MainActivity:

...
override fun onBackPressed() {
if( wv_site.visibility == View.VISIBLE
&& wv_site.canGoBack() ){
wv_site.goBack()
}
else{
super.onBackPressed()
}
}
...

 

Se o WebView de conteúdo não estiver visível, então podemos fechar o app, no acionamento do back button.

Nosso outro pequeno problema é que caso o usuário coloque o aplicativo em background para realizar outra tarefa e esse esteja com a tela de informe aberta, devido a não conexão, quando ele voltar ao aplicativo, mesmo com conexão ativa, terá de clicar em "Recarregar aplicativo".

Podemos atualizar o código de invocação de loadPage() na MainActivity para corrigirmos isso. Remova a invocação de loadPage() em onCreate() e coloque em onResume():

...
override fun onResume() {
super.onResume()
loadPage()
}
...

 

Assim podemos seguramente partir para os testes.

Testes e resultados

Com o Android Studio aberto, vá ao menu e então clique em Build  e logo depois em Rebuild Project. Posteriormente execute o aplicativo em seu device de testes ou em seu emulador. Assim terá:

Agora expanda o menu lateral e navegue, clicando em "Série A" e logo depois em "Série B":

Assim desative a Internet do device e então tente navegar para a "Série C":

Ative novamente a Internet e então clique em "Recarregar aplicativo", terá:

Clicando sequencialmente no back button do device você terá a seguinte navegação de volta:

Assim nossas metas e o objetivo principal foi atingido. Ainda é possível melhorias, incluindo o continuo uso da biblioteca AndroidUtilCode, mas aqui já conseguimos um grande avanço em nosso aplicativo Android com código Web.

Não deixe de se inscrever na lista de emails do Blog para receber em primeira mão as novidades no dev Android.

Se inscreva também no canal do Blog: YouTube Channel Thiengo Calopsita.

Vídeo com a implementação da biblioteca

A seguir o vídeo com o exemplo de implementação da biblioteca AndroidUtilCode:

Para acesso ao conteúdo completo do projeto, entre no GitHub a seguir: https://github.com/viniciusthiengo/super-placar-web-version.

Conclusão

O trabalho com bibliotecas externas e que são bem aceitas na comunidade de desenvolvedores, somente nos dá ganho de tempo no desenvolvimento das lógicas próprias de nosso domínio do problema.

A library AndroidUtilCode tem uma série de wrappers, ou invólucros, para facilitar ainda mais a codificação que necessita de APIs nativas, permitindo que com poucas linhas de código, algumas vezes somente uma, faça com que a funcionalidade da API esteja disponível.

Aparentemente a biblioteca ainda está em evolução, mais APIs Android serão atendidas, isso porque algumas esperadas ainda não estão presentes, como: solicitação de permissão em tempo de execução e SQLite.

Note que no artigo também abordamos algumas regras de negócio para trabalho com o WebView de maneira mais completa, regras que ainda não tínhamos abordado aqui no Blog. Uma delas é o trabalho com mais de uma WebView para manter a pilha de histórico de navegação da WebView principal.

Caso você tenha alguma dúvida ou sugestão, não deixe de comentar logo abaixo. Não se esqueça também de se inscrever na lista de emails do Blog para receber em primeira mão os novos conteúdos sobre o dev Android.

E, caso você esteja com algumas limitações aparentemente insuperáveis em seu WebView, dê uma olhada na library AdvancedWebView, ela poderá ser ao menos um bom paliativo até a solução final não ser desenvolvida.

Abraço.

Fontes

Documentação AndroidUtilCode

Documentação Android: WebViewClient

Is “shouldOverrideUrlLoading” really deprecated? What can I use instead?: Resposta CommonsWare

Documentação Android: SuppressWarnings

WebView no Android, Entendendo e Utilizando

Integrando WebView Android Com JavaScript de Uma WebPage

SharedPreferences no Android, Entendendo e Utilizando

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

Relacionado

FCM Android - Domínio do Problema, Implementação e Testes Com Servidor de Aplicativo [Parte 1]FCM Android - Domínio do Problema, Implementação e Testes Com Servidor de Aplicativo [Parte 1]Android
Kotlin Android, Entendendo e Primeiro ProjetoKotlin Android, Entendendo e Primeiro ProjetoAndroid
Iniciando com Anko Kotlin. Intenções no AndroidIniciando com Anko Kotlin. Intenções no AndroidAndroid
Colocando Telas de Introdução em Seu Aplicativo AndroidColocando Telas de Introdução em Seu Aplicativo AndroidAndroid

Compartilhar

Comentários Facebook (2)

Comentários Blog

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...