API de Endereços Para Pré-Cadastro em APPs Android - Parte 1
(12269) (2)
CategoriasAndroid, Design, Protótipo
AutorVinÃcius Thiengo
VÃdeo aulas186
Tempo15 horas
ExercÃciosSim
CertificadoSim
CategoriaDesenvolvimento Web
Autor(es)Robert C. Martin
EditoraAlta Books
Edição1ª
Ano2023
Páginas416
Tudo bem?
Neste artigo vamos construir um trecho de código que é útil, principalmente, para aqueles aplicativos Android que trabalham com formulário, onde também são necessários dados de endereço do usuário.
Em alguns domínios do problema os dados de: CEP, rua, cidade, estado, país, ..., entre outros. Estes dados são indispensáveis.
Um e-commerce, por exemplo, para que as entregas sejam realizadas, somente com os dados de endereço presentes (sério!?).
Devido ao tamanho do artigo, ele foi dividido em duas partes.
Nesta parte um vamos apresentar e discutir todo o código do aplicativo até a responsabilidade de realizar a busca do endereço partindo do CEP fornecido pelo user.
Na parte dois vamos construir e discutir todo o código que permite buscar o CEP partindo de dados como: rua, cidade e estado.
Antes de prosseguir, não deixe de se inscrever 📫 na lista de e-mails do Blog para receber, em primeira mão, todos os novos conteúdos de desenvolvimento de aplicativos Android do Blog.
Abaixo os tópicos que estaremos abordando nesta parte um:
- O problema de muitos campos em formulários;
- Apresentação da API de endereços;
- Projeto de exemplo - Android;
- Vídeo com implementação passo a passo do código - Parte um;
- Conclusão;
- Fontes.
O problema de muitos campos em formulários
Não somente com formulários Web mas também com formulários Android. O problema é o seguinte: quanto mais campos presentes no formulário, menor a conversão.
Faça um teste.
Caso você tenha um e-commerce, tente um teste onde os dados de CPF e nome do usuário não mais sejam necessários, apenas os dados de cartão de crédito e endereço.
Muito provavelmente o número de vendas vai aumentar.
Isso, na verdade não é uma teoria desenvolvida por mim, é o resultado de um estudo de um desenvolvedor especialista em conversão, mais precisamente, em conversão na Web.
O nome do camarada em Tim Ash e ele é autor do livro Otimização da Página de Entrada (já falei sobre este livro aqui no Blog).
Apesar do título do livro não dizer muito sobre formulários, um longo trecho dentro dele é somente sobre pesquisas e testes com formulários.
Os resultados mais evidentes estão em torno da quantidade de campos definidos para serem preenchidos.
Sendo: mais campos, menos conversões; menos campos, mais conversões.
Ok, mas eu posso melhorar a promoção e assim aumentar o número de conversões sem precisar remover alguns campos.
Sim, você está certo, mas com a mesma estratégia de promoção e com menos campos as conversões tendem a aumentar.
Note que não estou informando para você remover campos de seus formulários somente baseado em número de conversões. Alguns campos inevitavelmente terão de estar presentes, mas não deixe de otimizar seus formulários removendo alguns campos possíveis.
Nas partes um e dois dessa série de artigos sobre API de busca de endereços, vamos construir um código que você poderá utilizar em seus aplicativos Android para adiantar o processo de cadastro para aqueles formulários que necessitam da parte de endereço.
Com isso podemos amenizar a diminuição das conversões de: vendas, cadastros e outros. Siga com o conteúdo.
Apresentação da API de endereços
Você provavelmente já deve estar ciente que o Correios tem uma API gratuita de consulta de endereço. Isso via CEP ou via endereço auxiliar (rua, cidade e estado, por exemplo).
Até acessei a documentação, porém o que encontrei foi um WebService SOAP, ou seja, trabalho com XML.
E qual o problema disso?
O parser XML é muito mais "pesado" no Android que o parser JSON. Se você é das antigas no desenvolvimento Android já deve ter utilizada uma library chamada ksoap2, ou somente ksoap.
A primeira versão do aplicativo do Blog (que atualmente está precisando de uma nova o quanto antes) utilizava essa library e consequentemente tive de criar uma série de hackcodes para fazer com que ela funcionasse a ponto de conseguir realizar a requisição e em seguida processasse os dados recebidos.
Com a busca de endereços é diferente, pois felizmente há várias APIs gratuitas que utilizam a mesma base de dados de endereços disponibilizada pelo Correios.
O melhor é que muitas delas permitem a requisição de dados com uma simples URI, Rest, e com retorno em JSON.
Testei duas: Postmon e ViaCEP.
A Postmon, apesar de recomendada em fóruns, não está atualizada, alguns CEPs que testei não retornaram dado algum.
Simples teste com a Postmon API - CEP: 29.1667-68
{
"bairro": "Morada de Laranjeiras",
"cidade": "Serra",
"cep": "29166768",
"logradouro": "Rua das Perdizes",
"estado_info": {
"area_km2": "46.096,925",
"codigo_ibge": "32",
"nome": "Espírito Santo"
},
"cidade_info": {
"area_km2": "552,541",
"codigo_ibge": "3205002"
},
"estado": "ES"
}
A ViaCEP funcionou like a charm.
Todos os CEPs e endereços que testei retornaram dados válidos e, diferente da API do Postmon, quando o dado é inválido ou não existe resultado para a busca, neste caso é retornado que não tem dados.
Com o Postmon a página nem mesmo era carregada. Era informado que a página não existia.
Simples teste com a ViaCEP API - CEP: 29.1667-68
{
"cep": "29166-768",
"logradouro": "Rua das Perdizes",
"complemento": "",
"bairro": "Morada de Laranjeiras",
"localidade": "Serra",
"uf": "ES",
"unidade": "",
"ibge": "3205002",
"gia": ""
}
Com a ViaCEP e a lógica de negócio que vamos colocar no aplicativo de exemplo será possível pré-preencher o formulário de cadastro de um APP de Marketplace, nosso business problem aqui.
Com isso podemos seguir com o conteúdo.
Projeto de exemplo - Android
Como informando anteriormente, nosso aplicativo de exemplo é um de Marketplace, na verdade, vamos apenas construir a parte de endereço do formulário, digo, colocar funcional apenas essa parte, isso para que seja apresentada a API de endereços em um contexto válido.
Sendo assim, abra seu Android Studio, crie um novo projeto iniciando com uma Empty Activity e com o seguinte nome: MarketplaceAPP.
Com o final da codificação e testes desta parte um da série, teremos um aplicativo similar ao da figura abaixo:
Podemos prosseguir com os códigos.
Configurações Gradle
A seguir as configurações do Gradle Top Level, ou build.gradle (Project: MarketplaceAPP):
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 no código acima nada foi alterado em relação ao código padrão criado assim que é iniciado um novo projeto no Android Studio.
Abaixo o Gradle APP Level, ou build.gradle (Model: app):
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion "24.0.3"
defaultConfig {
applicationId "br.com.thiengo.marketplaceapp"
minSdkVersion 10
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:25.1.0'
testCompile 'junit:junit:4.12'
compile 'com.android.support:design:25.1.0'
compile 'com.google.code.gson:gson:2.7'
}
Neste adicionamos duas libraries não padrões em um novo projeto. Estão destacadas no código anterior.
A referência a Support Design é para podermos utilizar o TextInputLayout.
A outra referência é para utilizarmos o Gson.
Configurações AndroidManifest
O AndroidManifest.xml pouco muda de acordo com o que já é criado em um novo projeto Android Studio.
Segue configuração:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="br.com.thiengo.marketplaceapp">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".SignUpActivity" />
<activity android:name=".ZipCodeSearchActivity"></activity>
</application>
</manifest>
Configurações de estilo
Abaixo os arquivos XML de estilo utilizados no projeto. Iniciando com o /values/colors.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#009688</color>
<color name="colorPrimaryDark">#00796B</color>
<color name="colorAccent">#ffffff</color>
</resources>
As cores definidas no código anterior são as cores principais do APP, quase sempre monto elas no site Material Palette.
Em seguida o arquivo XML referente a dimensões, mais precisamente as dimensões utilizadas em padding de layouts do projeto.
Segue /values/dimens.xml:
<resources>
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
</resources>
Agora o XML de dados, array de estados e nome da APP.
Segue /values/strings.xml:
<resources>
<string name="app_name">Marketplace APP</string>
<string-array name="states">
<item>"*Estado"</item>
<item>"Acre (AC)"</item>
<item>"Alagoas (AL)"</item>
<item>"Amapá (AP)"</item>
<item>"Amazonas (AM)"</item>
<item>"Bahia (BA)"</item>
<item>"Ceará (CE)"</item>
<item>"Distrito Federal (DF)"</item>
<item>"Espírito Santo (ES)"</item>
<item>"Goiás (GO)"</item>
<item>"Maranhão (MA)"</item>
<item>"Mato Grosso (MT)"</item>
<item>"Mato Grosso do Sul (MS)"</item>
<item>"Minas Gerais (MG)"</item>
<item>"Pará (PA)"</item>
<item>"Paraíba (PB)"</item>
<item>"Paraná (PR)"</item>
<item>"Pernambuco (PE)"</item>
<item>"Piauí (PI)"</item>
<item>"Rio de Janeiro (RJ)"</item>
<item>"Rio Grande do Norte (RN)"</item>
<item>"Rio Grande do Sul (RS)"</item>
<item>"Rondônia (RO)"</item>
<item>"Roraima (RR)"</item>
<item>"Santa Catarina (SC)"</item>
<item>"São Paulo (SP)"</item>
<item>"Sergipe (SE)"</item>
<item>"Tocantins (TO)"</item>
</string-array>
</resources>
E para finalizar, o XML de estilo, /values/styles.xml:
<resources>
<style name="AppTheme" parent="Theme.AppCompat.NoActionBar">
<item name="windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:windowBackground">@drawable/background</item>
</style>
</resources>
Classes de negócio, domínio do problema
As classes de domínio do problema são bem simples.
Lembrando que vamos receber os dados no formato JSON e que já temos vinculada ao projeto uma library que permite o parser JSON de maneira simples.
Vamos recapitular o JSON que será retornado pela API do ViaCEP:
{
"cep": "29166-768",
"logradouro": "Rua das Perdizes",
"complemento": "",
"bairro": "Morada de Laranjeiras",
"localidade": "Serra",
"uf": "ES",
"unidade": "",
"ibge": "3205002",
"gia": ""
}
Vamos construir uma classe POJO, que tem ao menos os getters e setters de todos os atributos dela.
Vamos colocar na classe os atributos mais comuns e utilizados por nós no aplicativo de exemplo.
São eles:
- CEP;
- Logradouro;
- Complemento;
- Bairro;
- Localidade (cidade);
- e UF;
Segue classe Address:
public class Address {
private String bairro;
private String cep;
private String logradouro;
private String localidade;
private String uf;
private String complemento;
public String getBairro() {
return bairro;
}
public void setBairro(String bairro) {
this.bairro = bairro;
}
public String getCep() {
return cep;
}
public void setCep(String cep) {
this.cep = cep;
}
public String getLogradouro() {
return logradouro;
}
public void setLogradouro(String logradouro) {
this.logradouro = logradouro;
}
public String getLocalidade() {
return localidade;
}
public void setLocalidade(String localidade) {
this.localidade = localidade;
}
public String getUf() {
return uf;
}
public void setUf(String uf) {
this.uf = uf;
}
public String getComplemento() {
return complemento;
}
public void setComplemento(String complemento) {
this.complemento = complemento;
}
}
Por que devemos colocar todos os getters e setters de todos os atributos?
Para que o mapeamento via Gson library seja possível.
Note que os atributos também têm que ter o exato mesmo formato que os retornados no JSON. Isso para a versão simples do Gson, a que estaremos utilizando, pois é possível utiliza-lo de maneira mais robusta, veja a página oficial da library.
Uma outra necessidade que teremos será a de requisição de dados a API de endereço. Poderíamos utilizar o Retrofit, por exemplo. Porém achei que seria o equivalente a "matar uma formiga com uma bazuca".
Apesar de eu sempre recomendar o Retrofit para comunicações remotas, desta vez, para não colocar uma quantidade considerável de código (a referente a configuração dessa library), preferi um código simples, mas também eficiente, para requisição remota.
Segue algoritmo da classe JsonRequest:
public class JsonRequest {
public static String request( String uri ) throws Exception {
URL url = new URL( uri );
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
InputStream in = new BufferedInputStream(urlConnection.getInputStream());
BufferedReader r = new BufferedReader(new InputStreamReader(in));
StringBuilder jsonString = new StringBuilder();
String line;
while ((line = r.readLine()) != null) {
jsonString.append(line);
}
urlConnection.disconnect();
return jsonString.toString();
}
}
Simples, não? Receber os dados como String, concatena-la até o fim do envio de dados pelo backend e então retornar a String completa para ser, posteriormente, cliente de um parser JSON.
O código da classe anterior tem um indício de que teremos de trabalhar com ela em alguma entidade que facilmente transita em Thread de background e Thread principal (ou Thread UI).
Para esse aplicativo de exemplo vamos prosseguir com uma classe que estende a AsyncTask.
Segue código de AddressRequest:
public class AddressRequest extends AsyncTask<Void, Void, Address> {
private WeakReference<SignUpActivity> activity;
public AddressRequest( SignUpActivity activity ){
this.activity = new WeakReference<>( activity );
}
@Override
protected void onPreExecute() {
super.onPreExecute();
activity.get().lockFields( true );
}
@Override
protected Address doInBackground(Void... voids) {
try{
String jsonString = JsonRequest.request( activity.get().getUriRequest() );
Gson gson = new Gson();
return gson.fromJson(jsonString, Address.class);
}
catch (Exception e){
e.printStackTrace();
}
return null;
}
@Override
protected void onPostExecute(Address address) {
super.onPostExecute(address);
if( activity.get() != null ){
activity.get().lockFields( false );
if( address != null ){
activity.get().setAddressFields(address);
}
}
}
}
Nada pequeno, certo? Mas mesmo assim fácil de compreender.
Caso ainda não conheça a classe AsyncTask, não deixe de logo depois desse artigo consumir o artigo / vídeo dela aqui do Blog em: AsyncTask no Android, Acesso a Thread Principal de Forma Otimizada.
Recapitulando o trecho do WeakReference:
public class AddressRequest extends AsyncTask<Void, Void, Address> {
private WeakReference<SignUpActivity> activity;
public AddressRequest( SignUpActivity activity ){
this.activity = new WeakReference<>( activity );
}
...
@Override
protected void onPostExecute(Address address) {
super.onPostExecute(address);
if( activity.get() != null ){
...
}
}
}
O WeakReference nos permite ter uma referência fraca a qualquer entidade que seja utilizada nela.
Aqui optamos por essa lógica devido a necessidade de uso de uma instância da Activity em trabalho, SignUpActivity, que tem o ciclo de vida distinto do ciclo de vida da classe que herda de AsyncTask.
Com isso, caso a SignUpActivity seja reconstruída devido a alguma ação do usuário, por exemplo.
Caso isso aconteça, a antiga SignUpActivity e as entidades referenciadas por ela não terão uma referência forte vinculada a elas e consequentemente o coletor de lixo poderá passar e remover todas da memória.
Com isso diminuindo o vazamento de memória e consequentemente as chances de um OutOfMemoryException.
Sempre que for utilizar entidades que têm ciclo de vida distintos, digo, em Thread distintas, busque utilizar o WeakReference caso alguma delas não tenha mais valor quando o ciclo de vida chega ao fim ou há uma troca de instância.
Em nosso caso, a Thread secundária criada em AddressRequest, mais precisamente para o processamento do método doInBackground().
Esta Thread tem um ciclo de vida diferente da Thread onde está a SignUpActivity, a Thread principal.
O trecho activity.get() é o que nos permite acessar a instância de SignUpActivity.
Não se preocupe com os trechos de código presente em AddressRequest e ainda não explanados. Já vamos chegar até eles.
Note abaixo a simplicidade do parser com a library Gson:
public class AddressRequest extends AsyncTask<Void, Void, Address> {
...
@Override
protected Address doInBackground(Void... voids) {
try{
String jsonString = JsonRequest.request( activity.get().getUriRequest() );
Gson gson = new Gson();
return gson.fromJson(jsonString, Address.class);
}
catch (Exception e){
e.printStackTrace();
}
return null;
}
...
}
Nossa próxima classe do domínio do problema é a que vai ativar a requisição remota via instância de AddressRequest. Segue código de ZipCodeListener:
public class ZipCodeListener implements TextWatcher {
private Context context;
public ZipCodeListener( Context context ){
this.context = context;
}
@Override
public void afterTextChanged(Editable editable) {
String zipCode = editable.toString();
if( zipCode.length() == 8 ){
new AddressRequest( (SignUpActivity) context ).execute();
}
}
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
}
A Interface TextWatcher vai nos permitir ouvir o que está sendo digitado em um EditText.
Aqui, seguindo a lógica de negócio deste projeto de exemplo, vamos precisar desta funcionalidade de listener. Mais precisamente, o EditText referente ao CEP será vinculado a ela.
No algoritmo em afterTextChanged() verificamos se já se completaram os oito dígitos, caso sim, podemos prosseguir com uma requisição remota via AddressRequest.
Note que em ZipCodeListener não precisamos de trabalhar com WeakReference, pois o ciclo de vida da instância desta classe é exatamente o mesmo da Activity que vai conter ela. ZipCodeListener não precisa de uma nova Thread.
Nossa última classe de domínio do problema poderia muito bem estar em um pacote utilitário, mas aqui, devido a simplicidade de todo o projeto, não vi a necessidade de mais um package.
Segue código de Util:
public class Util {
private Activity activity;
private int[] ids;
public Util( Activity activity, int... ids ){
this.activity = activity;
this.ids = ids;
}
public void lockFields( boolean isToLock ){
for( int id : ids ){
setLockField( id, isToLock );
}
}
private void setLockField( int fieldId, boolean isToLock ){
activity.findViewById( fieldId ).setEnabled( !isToLock );
}
}
Tanto essa classe quanto a classe JsonRequest serão utilizadas frequentemente nas partes um e dois desta série sobre API de endereços.
Os códigos em Util vão nos permitir travar e destravar as Views informadas, travar para a entrada de dados por parte do usuário.
Atividade principal, MainActivity
Os códigos apresentados nesta seção são somente "códigos figurantes", digo, eles foram colocados no projeto para que este se aproximasse o máximo possível de um aplicativo real, para fazer sentido a aplicação do exemplo.
Vamos iniciar com o XML do layout de MainActivity, activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
tools:context="br.com.thiengo.marketplaceapp.MainActivity">
<RelativeLayout
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
<ImageView
android:id="@+id/imageView"
android:layout_width="270dp"
android:layout_height="122dp"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:layout_marginTop="100dp"
android:contentDescription="Logo MarketplaceAPP"
app:srcCompat="@drawable/logo" />
<android.support.design.widget.TextInputLayout
android:id="@+id/til_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/imageView"
android:layout_centerHorizontal="true"
android:layout_marginEnd="30dp"
android:layout_marginLeft="30dp"
android:layout_marginRight="30dp"
android:layout_marginStart="30dp"
android:layout_marginTop="60dp">
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:ems="10"
android:hint="Email"
android:inputType="textEmailAddress" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:id="@+id/til_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/til_email"
android:layout_centerHorizontal="true"
android:layout_marginEnd="30dp"
android:layout_marginLeft="30dp"
android:layout_marginRight="30dp"
android:layout_marginStart="30dp"
android:layout_marginTop="10dp">
<EditText
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:ems="10"
android:hint="Senha"
android:inputType="textPassword" />
</android.support.design.widget.TextInputLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/til_password"
android:layout_marginEnd="30dp"
android:layout_marginLeft="30dp"
android:layout_marginRight="30dp"
android:layout_marginStart="30dp"
android:layout_marginTop="30dp"
android:gravity="center"
android:onClick="signUpActivity"
android:text="Ainda não tem conta? Cadastre-se"
android:textColor="#f9f9f9"
android:textSize="18sp" />
</RelativeLayout>
</ScrollView>
Para layouts futuros neste artigo e no artigo parte dois estaremos vendo também a imagem da estrutura do layout, como esse é figurante, não há necessidade da estrutura dele aqui, apenas o XML para ajudar a completar o projeto.
Note que todas as imagens e o projeto completo você acessa no GitHub dele em: https://github.com/viniciusthiengo/marketplace-app.
O XML da MainActivity vai lhe dar um layout como a seguir:
Assim vamos ao código Java da MainActivity:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void signUpActivity(View view){
Intent intent = new Intent(this, SignUpActivity.class);
startActivity( intent );
}
}
Como falei, código figurante.
Vamos seguir para a atividade que realmente tem a nossa lógica de negócio aplicada.
O domínio de busca de endereço via CEP, SignUpActivity
Vamos iniciar com o XML do layout de SignUpActivity.
Segue activity_sign_up.xml:
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
tools:context="br.com.thiengo.marketplaceapp.SignUpActivity">
<RelativeLayout
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
<ImageView
android:id="@+id/imageView"
android:layout_width="270dp"
android:layout_height="122dp"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:contentDescription="Logo MarketplaceAPP"
app:srcCompat="@drawable/logo" />
<TextView
android:id="@+id/sub_title_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/imageView"
android:layout_marginBottom="10dp"
android:layout_marginTop="10dp"
android:background="#005740"
android:gravity="center"
android:padding="6dp"
android:text="Dados de acesso"
android:textColor="#f9f9f9"
android:textSize="20sp" />
<ImageView
android:id="@+id/iv_profile"
android:layout_width="140dp"
android:layout_height="140dp"
android:layout_below="@+id/sub_title_1"
android:layout_marginTop="10dp"
android:contentDescription="Image de perfil"
app:srcCompat="@drawable/profile" />
<android.support.design.widget.TextInputLayout
android:id="@+id/til_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/sub_title_1"
android:layout_marginLeft="10dp"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:layout_toEndOf="@+id/iv_profile"
android:layout_toRightOf="@+id/iv_profile">
<EditText
android:id="@+id/et_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:ems="10"
android:hint="*Email"
android:inputType="textEmailAddress" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:id="@+id/til_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/til_email"
android:layout_marginLeft="10dp"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:layout_toEndOf="@+id/iv_profile"
android:layout_toRightOf="@+id/iv_profile">
<EditText
android:id="@+id/et_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:ems="10"
android:hint="*Senha"
android:inputType="textPassword" />
</android.support.design.widget.TextInputLayout>
<TextView
android:id="@+id/sub_title_2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/iv_profile"
android:layout_marginBottom="10dp"
android:layout_marginTop="40dp"
android:background="#005740"
android:gravity="center"
android:padding="6dp"
android:text="Endereço"
android:textColor="#f9f9f9"
android:textSize="20sp" />
<LinearLayout
android:id="@+id/ll_fieldset_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/sub_title_2"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<android.support.design.widget.TextInputLayout
android:id="@+id/til_zip_code"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<EditText
android:id="@+id/et_zip_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:ems="10"
android:hint="*CEP"
android:inputType="number"
android:maxLength="8" />
</android.support.design.widget.TextInputLayout>
<TextView
android:id="@+id/tv_zip_code_search"
android:onClick="searchZipCode"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingTop="15dp"
android:text="Esqueci o CEP"
android:gravity="center"
android:textSize="18sp"
android:textColor="#f9f9f9"/>
</LinearLayout>
<LinearLayout
android:id="@+id/ll_fieldset_2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/ll_fieldset_1"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<android.support.design.widget.TextInputLayout
android:id="@+id/til_street"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<EditText
android:id="@+id/et_street"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:ems="10"
android:hint="*Rua"
android:inputType="text" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:id="@+id/til_complement"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<EditText
android:id="@+id/et_complement"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:ems="10"
android:hint="*Complemento"
android:inputType="text"
android:maxLength="8" />
</android.support.design.widget.TextInputLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/ll_fieldset_3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/ll_fieldset_2"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<android.support.design.widget.TextInputLayout
android:id="@+id/til_number"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<EditText
android:id="@+id/et_number"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:ems="10"
android:hint="*Nº"
android:inputType="number" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:id="@+id/til_neighbor"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<EditText
android:id="@+id/et_neighbor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:ems="10"
android:hint="*Bairro"
android:inputType="text" />
</android.support.design.widget.TextInputLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/ll_fieldset_4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/ll_fieldset_3"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<android.support.design.widget.TextInputLayout
android:id="@+id/til_city"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<EditText
android:id="@+id/et_city"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:ems="10"
android:hint="*Cidade"
android:inputType="text" />
</android.support.design.widget.TextInputLayout>
<Spinner
android:id="@+id/sp_state"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_below="@+id/ll_fieldset_4"
android:layout_marginTop="60dp"
android:background="#cc2a00"
android:gravity="center"
android:text="Enviar cadastro"
android:textColor="#ffffff"
android:textSize="20sp" />
</RelativeLayout>
</ScrollView>
O XML anterior dá origem ao seguinte layout em tela:
Para você não ficar perdido com a quantidade de código XML na marcação anterior, o arquivo activity_sign_up.xml tem exatamente a estrutra como apresentada a seguir:
O trecho mais importante desse XML é o referente ao EditText que permite a entrada de dados de CEP.
Segue:
...
<EditText
android:id="@+id/et_zip_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:ems="10"
android:hint="*CEP"
android:inputType="number"
android:maxLength="8" />
...
Aceitamos somente números e um máximo de oito destes. Isso para facilitar as possíveis validações em um código em produção.
Sempre busque restringir ao máximo, por atributos de seu XML, os dados de entrada, pois assim "sua vida" ficará mais tranquila quando no código que não é de marcação.
Um outro trecho que é importante apresentar é o referente ao TextView que permite a abertura da Activity de busca de CEP (parte dois da série). Segue:
...
<TextView
android:id="@+id/tv_zip_code_search"
android:onClick="searchZipCode"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingTop="15dp"
android:text="Esqueci o CEP"
android:gravity="center"
android:textSize="18sp"
android:textColor="#f9f9f9"/>
...
Quis destacar aqui, pois utilizamos nesta View o atributo android:onClick exatamente como fizemos no layout da MainActivity.
Não há problemas quanto a isso, pois funciona e, a princípio, não há uma regra de negócio falando sobre essa ser uma prática ruim, digo, uma regra na documentação do Android.
Assim podemos prosseguir com os códigos de lógica, Java API, em SignUpActivity. Com essa entidade vamos em partes.
O primeiro trecho a ser apresentado é o referente a inicialização de entidades presentes em SignUpActivity, nada de lógica de negócio.
Segue código:
public class SignUpActivity extends AppCompatActivity {
private EditText etZipCode;
private Util util;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_sign_up);
etZipCode = (EditText) findViewById(R.id.et_zip_code);
etZipCode.addTextChangedListener( new ZipCodeListener(this) );
Spinner spStates = (Spinner) findViewById(R.id.sp_state);
ArrayAdapter<CharSequence> adapter = ArrayAdapter
.createFromResource(this,
R.array.states,
android.R.layout.simple_spinner_item);
spStates.setAdapter(adapter);
util = new Util(this,
R.id.et_street,
R.id.tv_zip_code_search,
R.id.et_complement,
R.id.et_neighbor,
R.id.et_city,
R.id.sp_state);
}
...
}
Precisamos da View referente a código CEP, etZipCode, e a instância de Util como sendo variável de instâncias, pois estas vão ser acessadas em vários trechos da classe.
Note a inicialização do Spinner de estados, é utilizando a entidade padrão no pacote Android, ArrayAdapter e um conteúdo em XML.
Mais precisamente o array abaixo, presente em /values/strings.xml:
<resources>
...
<string-array name="states">
<item>"*Estado"</item>
<item>"Acre (AC)"</item>
<item>"Alagoas (AL)"</item>
<item>"Amapá (AP)"</item>
<item>"Amazonas (AM)"</item>
<item>"Bahia (BA)"</item>
<item>"Ceará (CE)"</item>
<item>"Distrito Federal (DF)"</item>
<item>"Espírito Santo (ES)"</item>
<item>"Goiás (GO)"</item>
<item>"Maranhão (MA)"</item>
<item>"Mato Grosso (MT)"</item>
<item>"Mato Grosso do Sul (MS)"</item>
<item>"Minas Gerais (MG)"</item>
<item>"Pará (PA)"</item>
<item>"Paraíba (PB)"</item>
<item>"Paraná (PR)"</item>
<item>"Pernambuco (PE)"</item>
<item>"Piauí (PI)"</item>
<item>"Rio de Janeiro (RJ)"</item>
<item>"Rio Grande do Norte (RN)"</item>
<item>"Rio Grande do Sul (RS)"</item>
<item>"Rondônia (RO)"</item>
<item>"Roraima (RR)"</item>
<item>"Santa Catarina (SC)"</item>
<item>"São Paulo (SP)"</item>
<item>"Sergipe (SE)"</item>
<item>"Tocantins (TO)"</item>
</string-array>
</resources>
Note também a adição do listener de digitação ao EditText etZipCode e a inicialização da instância de Util, entidade que trabalha com varargs, uma maneira de trabalhar com parâmetros opcionais em Java.
Com isso podemos prosseguir com o restante do código.
Agora trechos referentes a lógica:
public class SignUpActivity extends AppCompatActivity {
...
private String getZipCode(){
return etZipCode.getText().toString();
}
public String getUriRequest(){
return "https://viacep.com.br/ws/"+getZipCode()+"/json/";
}
public void lockFields( boolean isToLock ){
util.lockFields( isToLock );
}
public void searchZipCode(View view){
/* TODO */
}
...
}
Com o objetivo de encapsular ao máximo os atributos da Activity, criamos os métodos getZipCode() e lockFields().
O segundo permite o acesso e mudança de estado das Views de endereço do formulário. Isso sem o código cliente ter conhecimento delas.
O getUriRequest() é para retornamos o URI de requisição de maneira similar a outra atividade com requisição que estaremos construindo na parte dois.
Dessa forma vamos deixar o caminho aberto para possíveis refatorações no software (caso haja uma parte três, por exemplo).
O método searchZipCode() é, na verdade, um listener de clique que está vinculado ao TextView do formulário, o referente a abertura da Activity de busca de CEP, útil para casos onde o usuário sabe o endereço, porém não faz a mínima ideia do CEP.
Acredite, esses são casos ainda mais comuns do que o usuário conhecer o CEP dele.
O código de searchZipCode() vai ser construído somente na parte dois da série.
Assim podemos apresentar a última parte com lógica de negócio de SignUpActivity.
Segue:
public class SignUpActivity extends AppCompatActivity {
...
public void setAddressFields( Address address){
setField( R.id.et_street, address.getLogradouro() );
setField( R.id.et_complement, address.getComplemento() );
setField( R.id.et_neighbor, address.getBairro() );
setField( R.id.et_city, address.getLocalidade() );
setSpinner( R.id.sp_state, R.array.states, address.getUf() );
}
private void setField( int fieldId, String data ){
((EditText) findViewById( fieldId )).setText( data );
}
private void setSpinner( int fieldId, int arrayId, String uf ){
Spinner spinner = (Spinner) findViewById( fieldId );
String[] states = getResources().getStringArray(arrayId);
for( int i = 0; i < states.length; i++ ){
if( states[i].endsWith("("+uf+")") ){
spinner.setSelection( i );
break;
}
}
}
}
Novamente com o objetivo de encapsular o acesso aos atributos da atividade em trabalho, foram criados métodos que permitem, partindo de uma instância Address, preencher todos os campos de endereço.
Essa instância de Address, para recapitular, vem lá do código de AddressRequest. Segue:
public class AddressRequest extends AsyncTask<Void, Void, Address> {
...
@Override
protected Address doInBackground(Void... voids) {
try{
String jsonString = JsonRequest.request( activity.get().getUriRequest() );
Gson gson = new Gson();
/* GERANDO INSTÂNCIA */
return gson.fromJson(jsonString, Address.class);
}
catch (Exception e){
e.printStackTrace();
}
return null;
}
@Override
protected void onPostExecute(Address address) {
super.onPostExecute(address);
if( activity.get() != null ){
activity.get().lockFields( false );
if( address != null ){
/* ENVIANDO INSTÂNCIA */
activity.get().setAddressFields( address );
}
}
}
}
Como no caso do Spinner, o de estados, o dado que vem da ViaCEP API vem como uma simples String UF: ES, por exemplo.
E os dados em nosso array de estados têm o nome e a sigla: Espírito Santo (ES), por exemplo.
Tivemos de criar uma lógica específica somente para esse tipo de View, onde apenas verificamos a presença da sigla no final da String do item em array. Isso, pois essas siglas nunca se repetem. Logo, essa lógica é segura.
Assim terminamos com os códigos de SignUpActivity.
Nas seções a seguir, antes de apresentar os resultados dos testes, quero mostrar algumas particularidades com o uso das APIs Postmon e ViaCEP, mais precisamente particularidades da library Gson com essas como exemplo.
Particularidades Gson - Postmon x ViaCEP
Como já informado, o Gson permite o fácil parser JSON em nossos códigos Android. Para o parser JSON da API de ViaCEP, a simples classe abaixo é o suficiente:
public class Address {
private String bairro;
private String cep;
private String logradouro;
private String localidade;
private String uf;
private String complemento;
private String unidade;
private String ibge;
private String gia;
public String getBairro() {
return bairro;
}
public void setBairro(String bairro) {
this.bairro = bairro;
}
public String getCep() {
return cep;
}
public void setCep(String cep) {
this.cep = cep;
}
public String getLogradouro() {
return logradouro;
}
public void setLogradouro(String logradouro) {
this.logradouro = logradouro;
}
public String getLocalidade() {
return localidade;
}
public void setLocalidade(String localidade) {
this.localidade = localidade;
}
public String getUf() {
return uf;
}
public void setUf(String uf) {
this.uf = uf;
}
public String getComplemento() {
return complemento;
}
public void setComplemento(String complemento) {
this.complemento = complemento;
}
public String getUnidade() {
return unidade;
}
public void setUnidade(String unidade) {
this.unidade = unidade;
}
public String getIbge() {
return ibge;
}
public void setIbge(String ibge) {
this.ibge = ibge;
}
public String getGia() {
return gia;
}
public void setGia(String gia) {
this.gia = gia;
}
}
A classe acima responde sem problemas ao código JSON abaixo:
{
"cep": "29166-768",
"logradouro": "Rua das Perdizes",
"complemento": "",
"bairro": "Morada de Laranjeiras",
"localidade": "Serra",
"uf": "ES",
"unidade": "",
"ibge": "3205002",
"gia": ""
}
Os rótulos de atributos de nossa classe Java são os mesmo dos atributos JSON.
Porém caso você utilize uma API que tenha dados aninhados, objetos JSON, como a Postmon:
{
"bairro": "Morada de Laranjeiras",
"cidade": "Serra",
"cep": "29166768",
"logradouro": "Rua das Perdizes",
"estado_info": {
"area_km2": "46.096,925",
"codigo_ibge": "32",
"nome": "Espírito Santo"
},
"cidade_info": {
"area_km2": "552,541",
"codigo_ibge": "3205002"
},
"estado": "ES"
}
Para estes casos o código é ainda simples, porém envolve algumas novas classes no domínio do problema.
Segue a primeira aqui, EstadoInfo:
public class EstadoInfo {
private String area_km2;
private String codigo_ibge;
private String nome;
public String getArea_km2() {
return area_km2;
}
public void setArea_km2(String area_km2) {
this.area_km2 = area_km2;
}
public String getCodigo_ibge() {
return codigo_ibge;
}
public void setCodigo_ibge(String codigo_ibge) {
this.codigo_ibge = codigo_ibge;
}
public String getNome() {
return nome;
}
public void setNome(String nome) {
this.nome = nome;
}
}
Note em EstadoInfo como os campos têm exatamente a mesma assinatura que o indicado no arquivo JSON: do tipo String e com o mesmo rótulo.
Logo depois CidadeInfo:
public class CidadeInfo {
private String area_km2;
private String codigo_ibge;
public String getArea_km2() {
return area_km2;
}
public void setArea_km2(String area_km2) {
this.area_km2 = area_km2;
}
public String getCodigo_ibge() {
return codigo_ibge;
}
public void setCodigo_ibge(String codigo_ibge) {
this.codigo_ibge = codigo_ibge;
}
}
CidadeInfo segue a mesma linha que EstadoInfo, mesmas assinaturas e uma classe Java POJO.
E então, para finalizar, a classe que deve ter a vinculação direta ao Gson, Address:
public class Address {
private String bairro;
private String cep;
private String logradouro;
private String cidade;
private String estado;
private EstadoInfo estado_info;
private CidadeInfo cidade_info;
public String getBairro() {
return bairro;
}
public void setBairro(String bairro) {
this.bairro = bairro;
}
public String getCep() {
return cep;
}
public void setCep(String cep) {
this.cep = cep;
}
public String getLogradouro() {
return logradouro;
}
public void setLogradouro(String logradouro) {
this.logradouro = logradouro;
}
public String getCidade() {
return cidade;
}
public void setCidade(String cidade) {
this.cidade = cidade;
}
public String getEstado() {
return estado;
}
public void setEstado(String estado) {
this.estado = estado;
}
public EstadoInfo getEstado_info() {
return estado_info;
}
public void setEstado_info(EstadoInfo estado_info) {
this.estado_info = estado_info;
}
public CidadeInfo getCidade_info() {
return cidade_info;
}
public void setCidade_info(CidadeInfo cidade_info) {
this.cidade_info = cidade_info;
}
}
Note que com o JSON sendo utilizado como apresentado aqui, os nomes das classes são questionáveis, você pode utilizar os seus próprios, porém os rótulos têm de ser iguais, os tipos também.
Até mesmo "_" (underline) deve ser utilizado.
Veja que o código Gson continuará sendo o mesmo:
...
String jsonString = JsonRequest.request( activity.get().getUriRequest() );
Gson gson = new Gson();
return gson.fromJson(jsonString, Address.class);
...
Lembrando que essa seção foi apenas uma apresentação a mais do Gson, pois nosso código do aplicativo MarketplaceAPP permanecerá sendo o mesmo, utilizando ViaCEP API.
Assim... seguem testes e resultados.
Testes e resultados
Executando a APP, logo que ela abrir clique em "Ainda não tem conta? Cadastre-se":
Logo em seguida realize o scroll e entre com os dados do CEP no campo dele.
Note os outros campos de endereço sendo travados assim que você entra com os oito dígitos:
Como resultado: os dados apresentados em seus respectivos campos.
Note que em alguns casos é possível que não sejam retornados todos os dados. Uma String vazia será retornada:
Em caso de dado inválido:
Os campos permanecem vazios e liberados.
Com isso finalizamos a parte um do projeto.
Antes de seguir para a conclusão, não esqueça de se inscrever na 📫 lista de e-mails do Blog para receber os novos conteúdos exclusivos do Blog.
Se inscreva também no canal do Blog em YouTube Thiengo.
Vídeo com implementação passo a passo do código - Parte um
No vídeo abaixo é apresentada a implementação passo a passo do projeto, parte um, deste artigo:
Para acessar a versão Android do Projeto, completa (incluindo parte dois), entre no GitHub a seguir: https://github.com/viniciusthiengo/marketplace-app.
Conclusão
Como discutido logo na primeira seção do artigo, "campos em formulário" é um assunto tão sério quanto o próprio código do formulário em projeto.
Caso seu software não tenha conversões o suficiente, ele consequentemente não tem porque continuar ativo.
Para minimizar o número de campos necessários, digo, o preenchimento desses, você pode remover, do formulário inicial, alguns campos que podem ter o preenchimento posterior a conversão ou pode pré-preencher esses de acordo com as entradas do usuário.
O que fizemos aqui, nessa parte um, foi permitir isso, que com o CEP todo o restante do endereço, ao menos a maior parte dele, seja preenchida automaticamente pelo aplicativo.
O restante do código, referente a busca de CEP via dados de endereço, será apresentado na parte dois.
Isso pois, somente este artigo, além do vídeo, já tem mais do que 5000 palavras.
E... parabéns se chegou até aqui. Poucos chegam!
Por fim, não esqueça de se inscrever na 📩 lista de e-mails, respondo às suas dúvidas também por lá, além de lhe enviar conteúdos exclusivos de desenvolvimento Android.
Abraço.
Fontes
ViaCEP - página de apresentação da API
Usando HttpClient e Gson no Android Studio
Comentários Facebook