Desenvolvendo as Telas de Cartão de Crédito Com Máscara de Campo - Android M-Commerce

Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba grátis conteúdos Android sem precedentes! Você receberá um email de confirmação. Somente depois de confirma-lo é que eu poderei lhe enviar os conteúdos semanais exclusivos. Os artigos em PDF são entregues somente para os inscritos na lista.

Email inválido.
Blog /Android /Desenvolvendo as Telas de Cartão de Crédito Com Máscara de Campo - Android M-Commerce

Desenvolvendo as Telas de Cartão de Crédito Com Máscara de Campo - Android M-Commerce

Vinícius Thiengo
(8826)
Go-ahead
"O método consciente de tentativa e erro é mais bem-sucedido que o planejamento de um gênio isolado."
Peter Skillman
Prototipagem Android
Capa do curso Prototipagem Profissional de Aplicativos
TítuloAndroid: Prototipagem Profissional de Aplicativos
CategoriasAndroid, Design, Protótipo
AutorVinícius Thiengo
Vídeo aulas186
Tempo15 horas
ExercíciosSim
CertificadoSim
Acessar Curso
Quer aprender a programar para Android? Acesse abaixo o curso gratuito no Blog.
Lendo
TítuloDomain-driven Design Destilado
CategoriaEngenharia de Software
Autor(es)Vaughn Vernon
EditoraAlta Books
Edição1ª
Ano2024
Páginas160
Conteúdo Exclusivo
Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba gratuitamente conteúdos Android sem precedentes!
Email inválido

Tudo bem?

Neste artigo continuaremos com o desenvolvimento de nosso aplicativo Android de mobile-commerce, o Android BlueShoes.

Nesta aula vamos ao desenvolvimento das telas de configuração de cartões de crédito do usuário. Uma tela de acesso aos cartões, permitindo também a remoção deles, e outra tela de adição de novos cartões.

Animação da área de configurações de cartões de crédito

Aqui também utilizaremos uma API de máscara de campo para que as telas fiquem exatamente como definidas em protótipo.

Antes de prosseguir, não esqueça de se inscrever ðŸ“«na lista de e-mails do Blog para receber as atualizações do projeto BlueShoes e de outros conteúdos de desenvolvimento Android, conteúdos exclusivos aqui do Blog.

A seguir os pontos abordados nesta aula:

Começando no Android mobile-commerce

Se você chegou neste projeto Android de mobile-commerce somente nesta aula, então saiba que está já é a 16ª aula.

Ou seja, você precisa primeiro assistir às outras aulas se quiser seguir o desenvolvimento passo a passo com consistência:

Caso você queira apenas aprender a utilizar a API de máscara de campos, EditText, então vá direto a seção Máscara para campos de cartão de crédito.

E para se manter em atualização com as novas aulas do projeto Android mobile-commerce, não deixe de se inscrever ðŸ“«na lista de e-mails do Blog.

Estratégia para as telas de cartões de crédito

Como em todas as outras partes do projeto, vamos seguir o roteiro: primeiro o fácil, depois o mais complicado.

Aqui, como foi para a área de configuração de dados de acesso (e-mail e senha), teremos uma série de novas entidades:

  • Um fragmento para conter a lista de cartões de crédito cadastrados;
  • Uma nova classe de domínio para representar os cartões de crédito do sistema;
  • Uma classe adaptadora para ser vinculada ao framework de lista utilizado no fragmento de cartões de crédito;
  • Uma base de dados temporária, mock data, para conter os cartões de crédito em teste;
  • Um fragmento para conter o formulário de adição de cartão de crédito;
  • Uma atividade host, com tabs, para conter todos os fragmentos de cartões de crédito.

O desenvolvimento será no mesmo esquema da tela de configuração de dados de conexão:

  • Primeiro o trabalho com as entidades que têm mais dependentes em projeto, seguindo:
    • Com o desenvolvimento da parte estática, se houver alguma;
    • Então com o desenvolvimento da parte dinâmica.
  • Repetindo o item anterior para cada entidade restante para a finalização da UI da área de configurações de cartões de crédito.

Lembrando que o projeto Android BlueShoes está disponível no repositório dele em: https://github.com/viniciusthiengo/blueshoes-kotlin-android.

Note que ainda nesta aula manteremos uma série de códigos repetidos, isso, pois ainda não é o momento de refatorarmos toda a área de configurações de conta, vamos primeiro até o fim do desenvolvimento dela.

Protótipo estático

Abaixo o protótipo estático das telas de configurações de cartões de crédito:

Carregando cartões

Carregando cartões

Sem cartão cadastrado

Sem cartão cadastrado

Lista de cartões

Lista de cartões

Segurança de remoção

Segurança de remoção

Load - remoção em back-end Web

Load - remoção em back-end Web

Erro na remoção

Erro na remoção

Remoção bem sucedida

Remoção bem sucedida

Novo cartão

Novo cartão

Load - inserção em back-end Web

Load - inserção em back-end Web

Erro na inserção

Erro na inserção

Inserção bem sucedida

Inserção bem sucedida

 

Atualizando o Fragmento de formulários

Apesar de o FormFragment já estar bem completo, ele não atende a algumas configurações de interface gráfica quando o formulário é apenas uma lista de itens que podem ser atualizados por meio de botões ou quando o formulário não tem de estar centralizado em tela:

Formulários de gerência de cartões de crédito

Devido às essas duas necessidades em especifico teremos que realizar uma pequena atualização em FormFragment.

Método de (re)configuração de layout

No fragmento ancestral de fragmentos de formulário temos o seguinte layout container (/res/layout/fragment_form.xml):

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

<FrameLayout
android:id="@+id/fl_form"
android:padding="16dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />

<include layout="@layout/proxy_screen" />
</FrameLayout>

 

FrameLayout de ID fl_form é exatamente o ViewGroup container dos layouts das subclasses de FormFragment.

E é neste ViewGroup que temos os atributos android:padding="16dp"android:layout_gravity="center", android:layout_width="wrap_content" e android:layout_height="wrap_content" que não devem estar presentes em formulários de lista ou em formulários que não devem estar centralizados e consequentemente ocuparão todo o espaço disponível em tela.

Sendo assim, em FormFragment, adicione o método updateFlFormToFullFreeScreen() como a seguir:

...
/*
* Método necessário para atualizar o ViewGroup
* fl_form, que é container dos layouts de formulários
* carregados em fragment_form, deixando ele
* pronto para receber uma lista de itens ou formulários
* que têm os próprios padding e posicionamento.
* */
fun updateFlFormToFullFreeScreen(){

fl_form.setPadding( 0,0,0,0 )

val layoutParams = (fl_form.layoutParams as FrameLayout.LayoutParams)
layoutParams.gravity = Gravity.NO_GRAVITY
layoutParams.width = FrameLayout.LayoutParams.MATCH_PARENT
layoutParams.height = FrameLayout.LayoutParams.MATCH_PARENT
}
...

 

Este novo método deverá ser invocado antes do método onResume() dos fragmentos que necessitarão de todo o espaço disponível em tela para a apresentação dos formulários deles.

Nova estrutura de domínio para cartões

Antes de iniciarmos já com os fragmentos e a atividade específicos de gerência de cartões de crédito, vamos primeiro adiantar o desenvolvimento de entidades que são dependências de todas essas.

Classe de domínio

Primeiro vamos a nova classe de domínio, responsável por representar cartões de crédito no sistema BlueShoes mobile-commerce. Em /domain adicione a classe CreditCard com os scripts a seguir:

class CreditCard(
/*
* number contém somente o último conjunto de
* números do cartão (que são 4 ou 3 números).
* */
val number : String,
val enterprise: String,
val ownerFullName: String,
val ownerRegNumber: String = "",
val expiryMonth: Int = 0,
val expiryYear: Int = 0,
val securityNumber: String = ""
){
fun getNumberAsHidden()
= String.format("**** **** **** %s", number)

fun getOwnerFullNameAsHidden() : String {
val nameList = ownerFullName.split(" ")

val firstName = nameList.first().substring(0, 2)
val lastName = nameList.last()

return String.format("%s... %s", firstName, lastName)
}
}

 

Importante lembrar que em nossa base de dados, quando em produção, de cada cartão de crédito nós salvaremos apenas:

  • O último conjunto de dígitos do número do cartão, que pode ser quatro ou três dígitos;
  • A bandeira do cartão (Visa, Mastercard, ...);
  • E o nome do proprietário do cartão.

Com estes dados é possível permitir que o usuário identifique o cartão cadastrado e saiba qual escolher em uma compra ou remoção.

Isso sem que o aplicativo precise de certificações PCI-DSS (Payment Card Industry Data Security Standard) para processar os pagamentos.

Pois com os dados informados não é possível um attacker realizar qualquer compra, apenas confirmar que são apenas dados de identificação de cartão.

Os métodos getNumberAsHidden()getOwnerFullNameAsHidden() são necessários para formatações aguardadas na lista de cartões de crédito:

Itens de lista, cartões de crédito

Base de dados (temporária)

Vamos precisar de uma base de dados temporária, mock data, para conter alguns cartões de crédito fictícios para testes.

Em /data adicione a classe CreditCardsDataBase como a seguir:

class CreditCardsDataBase {

companion object{

fun getItems()
= listOf(
CreditCard(
"6502",
"Visa",
"Tony Stark"
),
CreditCard(
"9270",
"Mastercard",
"Scarlett Johansson"
),
CreditCard(
"661",
"American Express",
"Margot Robbie"
),
CreditCard(
"8738",
"Visa",
"Vivian Hernandez"
),
CreditCard(
"9011",
"Visa",
"Andrew Jackson"
)
)
}
}

 

Quando em produção, provavelmente removeremos está classe do projeto.

Definindo novo pacote

E por fim, como passo inicial da nova estrutura de gerência dos cartões de crédito do usuário, vamos criar um novo pacote dentro do nosso pacote que representa a camada de visualização do projeto.

Em /view:

  • Clique com o botão direito do mouse;
  • Então acesse New;
  • Clique em Package;
  • Em Enter new package name coloque config.creditcard e clique em OK.

Assim teremos:

Estrutura física do projeto Android BlueShoes

Você certamente já deve ter entendido que no Android Studio IDE cada ponto em nome de pacote (nome1.nome2.nome3) representa na verdade a hierarquia dos pacotes (/nome1/nome2/nome3), certo?

Em aulas futuras organizaremos ainda mais a estrutura física de nosso projeto.

Lista de cartões de crédito

Nosso primeiro fragmento, em área de configurações de cartões de crédito, é simples de ser desenvolvido devido ao uso de framework de lista:

Lista de cartões de crédito

Porém o funcionamento dele em relação aos métodos de implementação obrigatória do FormFragment não é tão trivial quanto parece. Vamos trabalhar muito com "funções como parâmetro", os callbacks.

Atualizando o arquivo de Strings

Vamos direto a parte que pode ser facilmente extraída ainda no protótipo estático do projeto. Em /res/values/strings.xml adicione os trechos em destaque:

<resources>
...

<!-- ConfigCreditCardsListFragment -->
<string name="config_credit_cards_tab_list">CARTÕES</string>

<string name="credit_card_list_empty">
Ainda não há cartões de crédito cadastrados.
</string>

<string name="credit_card_removed">
Cartão de crédito removido com sucesso.
</string>

<string name="credit_card_enterprise_label">Bandeira:</string>
<string name="credit_card_number_label">Número:</string>
<string name="credit_card_owner_name_label">Nome:</string>

<string name="remove_item">Remover</string>
<string name="remove_item_going">Removendo&#8230;</string>
</resources>

 

Lembrando que colocar Strings estáticas em arquivos XML de Strings é essencial para que haja reaproveitamento de alguns textos e, principalmente, para facilitar o processo de internacionalização do aplicativo.

Definindo o estilo do botão Remover

Aquele pequeno botão de cada item de cartão de crédito:

Botão Remover de itens de lista

Este botão que também dá à lista de cartões a característica de um formulário, pois ele permite a remoção de qualquer cartão. Este <Button> tem seu próprio estilo.

Antes de definirmos o drawable do estilo do botão "Remover", vamos primeiro colocar em /res/values/colors.xml a cor de background que será utilizada:

<?xml version="1.0" encoding="utf-8"?>
<resources>
...

<color name="colorLightRed">#F85F62</color>
</resources>

 

Agora em /res/drawable crie o arquivo bt_remove.xml como a seguir:

<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">

<!--
Definição da cor de background do shape
retangular.
-->
<solid android:color="@color/colorLightRed" />

<!--
Definição da curvatura das pontas do shape
retangular.
-->
<corners android:radius="3dp"/>
</shape>

Tema para rótulo de item

Veja o design dos rótulos de valores em itens de lista, os cartões de crédito:

Rótulos dos itens de lista

Todos são iguais, consequentemente eles também têm inúmeras configurações de estilo iguais. Sendo assim, em /res/values/styles.xml adicione o tema TextViewFormItemListLabel como a seguir:

<resources>
...

<style name="TextViewFormItemListLabel">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_marginRight">4dp</item>
<item name="android:layout_marginEnd">4dp</item>
<item name="android:textColor">@color/colorLightGrey</item>
</style>
</resources>

 

Mas Thiengo, como você já até sabia das configurações de estilo das Views rótulos antes mesmo de acessarmos a parte de layout de item de lista?

É, eu confesso. Já coloquei aqui em artigo quase todas as partes estáticas refatoradas, utilizando temas e evitando ao máximo duplicações.

Isso para nos poupar tempo quando formos refatorar os códigos dinâmicos, com lógicas de negócio, em aulas futuras. Pois em lógica de negócio teremos de utilizar, muito provavelmente, alguns padrões de projeto.

Definindo o layout de item

Aqui no layout de item de cartões de crédito iniciaremos os trabalhos com o CardView. Em /res/layout crie o layout credit_card_item.xml com as seguintes configurações:

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_marginTop="6dp"
android:layout_marginBottom="6dp"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
card_view:cardCornerRadius="2dp">

<RelativeLayout
android:padding="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<!-- Bandeira cartão. -->
<TextView
android:id="@+id/tv_enterprise_label"
style="@style/TextViewFormItemListLabel"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:text="@string/credit_card_enterprise_label"/>

<TextView
android:id="@+id/tv_enterprise"
android:layout_toRightOf="@+id/tv_enterprise_label"
android:layout_toEndOf="@+id/tv_enterprise_label"
android:layout_alignTop="@+id/tv_enterprise_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/colorText"/>

<!-- Número cartão. -->
<TextView
android:id="@+id/tv_number_label"
style="@style/TextViewFormItemListLabel"
android:layout_below="@+id/tv_enterprise_label"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:text="@string/credit_card_number_label"/>

<TextView
android:id="@+id/tv_number"
android:layout_toRightOf="@+id/tv_number_label"
android:layout_toEndOf="@+id/tv_number_label"
android:layout_alignTop="@+id/tv_number_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/colorText"/>

<!-- Nome proprietário cartão. -->
<TextView
android:id="@+id/tv_owner_name_label"
style="@style/TextViewFormItemListLabel"
android:layout_below="@+id/tv_number_label"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:text="@string/credit_card_owner_name_label"/>

<TextView
android:id="@+id/tv_owner_name"
android:layout_toRightOf="@+id/tv_owner_name_label"
android:layout_toEndOf="@+id/tv_owner_name_label"
android:layout_alignTop="@+id/tv_owner_name_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/colorText"/>

<Button
android:id="@+id/bt_remove"
android:layout_below="@+id/tv_owner_name_label"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_width="wrap_content"
android:layout_height="27dp"
android:paddingLeft="18dp"
android:paddingRight="18dp"
android:background="@drawable/bt_remove"
android:textColor="@android:color/white"
android:textAllCaps="false"
android:text="@string/remove_item"/>
</RelativeLayout>
</androidx.cardview.widget.CardView>

 

A seguir o diagrama do layout anterior:

Diagrama do layout credit_card_item.xml

Classe adaptadora de itens

Com o layout de item já definido podemos ir direto aos códigos dinâmicos da classe adaptadora que ficará vinculada ao RecyclerView do fragmento de lista de cartões.

Em nosso novo pacote, /view/config/creditcard, crie a classe ConfigCreditCardsListItemsAdapter como a seguir:

class ConfigCreditCardsListItemsAdapter(
private val items: List<CreditCard>
) :
RecyclerView.Adapter<ConfigCreditCardsListItemsAdapter.ViewHolder>() {

override fun onCreateViewHolder(
parent: ViewGroup,
type: Int ): ViewHolder {

val layout = LayoutInflater
.from( parent.context )
.inflate(
R.layout.credit_card_item,
parent,
false
)

return ViewHolder( layout )
}

override fun onBindViewHolder(
holder: ViewHolder,
position: Int ) {

holder.setData( items[ position ] )
}

override fun getItemCount() = items.size

inner class ViewHolder( itemView: View ) :
RecyclerView.ViewHolder( itemView ),
View.OnClickListener {

private val tvEnterprise : TextView
private val tvNumber : TextView
private val tvOwnerName : TextView
private val btRemove : Button

init{
tvEnterprise = itemView.findViewById( R.id.tv_enterprise )
tvNumber = itemView.findViewById( R.id.tv_number )
tvOwnerName = itemView.findViewById( R.id.tv_owner_name )

btRemove = itemView.findViewById( R.id.bt_remove )
btRemove.setOnClickListener( this )
}

fun setData( item: CreditCard ){

tvEnterprise.setText( item.enterprise )
tvNumber.setText( item.getNumberAsHidden() )
tvOwnerName.setText( item.getOwnerFullNameAsHidden() )
}

override fun onClick( view: View ) {
/* TODO */
}
}
}

 

Quase que somente código boilerplate, certo?

Ainda voltaremos a está classe, pois será nela que colocaremos algoritmos de atualização de dados em cada item da lista de cartões de crédito. Mas antes ainda precisamos configurar o fragmento container de lista.

Definindo o layout principal

Agora o layout do fragmento container da lista de cartões de crédito.

Em /res/layout adicione o XML fragment_config_credit_cards_list.xml com o código a seguir:

<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rv_credit_cards"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical" />

 

Muito simples, certo? Dispensa até mesmo um diagrama, figura comum nos layouts Android aqui do Blog.

Construindo a ConfigCreditCardsListFragment

Assim podemos ir direto aos códigos dinâmicos do fragmento de lista.

Em nosso novo pacote, /view/config/creditcard, crie o fragment ConfigCreditCardsListFragment como a seguir:

class ConfigCreditCardsListFragment :
FormFragment() {

companion object{
const val TAB_TITLE = R.string.config_credit_cards_tab_list
}

override fun getLayoutResourceID()
= R.layout.fragment_config_credit_cards_list

override fun backEndFakeDelay() {
backEndFakeDelay(
true,
getString( R.string.credit_card_removed )
)
}

override fun blockFields( status: Boolean ) {
/* TODO */
}

override fun isMainButtonSending( status: Boolean ) {
/* TODO */
}

override fun onActivityCreated( savedInstanceState: Bundle? ) {
super.onActivityCreated( savedInstanceState )

updateFlFormToFullFreeScreen()
initItems()
}

/*
* Método que inicializa a lista de cartões de crédito.
* */
private fun initItems(){
rv_credit_cards.setHasFixedSize( false )

val layoutManager = LinearLayoutManager( activity )
rv_credit_cards.layoutManager = layoutManager

val adapter = ConfigCreditCardsListItemsAdapter(
this,
CreditCardsDataBase.getItems()
)
rv_credit_cards.adapter = adapter
}
}

 

Você lembra do novo método adicionado em FormFragment? O método updateFlFormToFullFreeScreen().

Então, olha ele sendo invocado em onActivityCreated(), antes do onResume() e depois do onCreateView() - neste último caso para permitir acesso às Views via sintaxe kotlin-android-extensions.

Note que os métodos blockFields()isMainButtonSending() estão com o comentário /* TODO */, pois os algoritmos que vão rodar nestes métodos estão diretamente ligados ao item de lista acionado pelo usuário, ou seja, esses algoritmos vão vir da classe adaptadora de cartões.

Então... vamos a eles.

Criando vinculo via callbacks

Primeiro, a lista de cartões de crédito vinculada ao adapter do RecyclerView poderá ter cartões removidos, sendo assim ela precisa ser uma MutableList ao invés de uma List, caso contrário não será possível utilizar, por exemplo, a invocação items.remove( position ).

Sabendo desta limitação de List, temos de realizar uma atualização em getItems() de CreditCardsDataBase:

...
fun getItems()
= mutableListOf(
...
)
...

 

Agora é mutableListOf() ao invés de listOf().

E também uma atualização no construtor de ConfigCreditCardsListItemsAdapter:

class ConfigCreditCardsListItemsAdapter(
private val items: MutableList<CreditCard>
) ... {
...
}

 

Agora é MutableList<CreditCard> ao invés de List<CreditCard>.

Note que para conseguirmos passar os algoritmos de ConfigCreditCardsListItemsAdapter para ConfigCreditCardsListFragment temos inúmeras estratégias.

Porém a mais simples em termos de "não ser preciso atualizar qualquer método em FormFragment" é utilizando funções como argumento, o conhecido callback.

Sendo assim, em ConfigCreditCardsListFragment crie o novo método callbacksToUpdateItem() e três novas propriedades de classe que vão servir como "entidades hosts" dos algoritmos que serão enviados de ConfigCreditCardsListItemsAdapter:

class ConfigCreditCardsListFragment :
FormFragment() {
...

var callbackMainButtonUpdate : (Boolean)->Unit = {}
var callbackBlockFields : (Boolean)->Unit = {}
var callbackRemoveItem : (Boolean)->Unit = {}
...

/*
* Método utilizado para receber os callbacks do adapter
* do RecyclerView para assim poder atualizar os itens
* de adapter.
* */
fun callbacksToUpdateItem(
mainButtonUpdate: (Boolean)->Unit,
blockFields: (Boolean)->Unit,
removeItem: (Boolean)->Unit ){

callbackMainButtonUpdate = mainButtonUpdate
callbackBlockFields = blockFields
callbackRemoveItem = removeItem
}
}

 

Quando atualizarmos os métodos blockFields()isMainButtonSending() você vai entender o porquê de todos os callbacks aguardarem um algoritmo que tem sempre como parâmetro de entrada um dado do tipo Boolean.

Alias, vamos a essas atualizações. Ainda em ConfigCreditCardsListFragment atualize os métodos citados anteriormente, atualize como a seguir:

...
override fun blockFields( status: Boolean ) {
callbackBlockFields( status )
}

override fun isMainButtonSending( status: Boolean ) {
callbackMainButtonUpdate( status )
callbackRemoveItem( status )
}
...

 

É, eu sei. Você ainda não entendeu muito bem o porquê do uso de status também como parâmetro nos callbacks, certo?

Então vamos a parte final desta atualização com callbacks, que certamente deixará isso claro.

Para ser possível o acesso ao novo método criado em ConfigCreditCardsListFragment, precisamos deste fragmento como uma das propriedades em ConfigCreditCardsListItemsAdapter, logo, no construtor deste adapter adicione a atualização, fragmento como parâmetro, a seguir:

class ConfigCreditCardsListItemsAdapter(
private val fragment : ConfigCreditCardsListFragment,
private val items: MutableList<CreditCard>
) ... {
...
}

 

Agora no método onClick(), do ViewHolder de ConfigCreditCardsListItemsAdapter, adicione os algoritmos em destaque a seguir:

...
override fun onClick( view: View ) {

/*
* É preciso salvar em uma nova propriedade a posição do
* item selecionado, pois o valor de adapterPosition está
* sempre sendo atualizado e isso, o acesso a adapterPosition
* diretamente dentro do callback, poderia ocasionar em
* uma Exception.
* */
val selectedItem = adapterPosition

fragment.callbacksToUpdateItem(
{
status ->
btRemove.text =
if( status )
fragment.getString( R.string.remove_item_going )
else
fragment.getString( R.string.remove_item )
},
{
status -> btRemove.isEnabled = !status
},
{
status ->
if( !status ){
items.removeAt( selectedItem )
notifyItemRemoved( selectedItem )
}
}
)

fragment.callPasswordDialog()
}
...

 

Done! Desta forma estamos passando:

  • Primeiro argumento âž™ algoritmo responsável por atualizar o status do botão "Remover" do item em processo de remoção (única ação possível para os itens de cartão);
  • Segundo argumento âž™ algoritmo responsável por atualizar o status de "possibilidade de acionamento do botão 'Remover' quando em processo de remoção", se ele será passivo de acionamento por meio do toque ou não;
  • Terceiro argumento âž™ algoritmo responsável por remover, ou não, o cartão de crédito (item em lista) depois do feedback do back-end Web.

Note a importância de selectedItem, importância explicada em formato de comentário no código. Sem está propriedade teríamos grandes chances de gerar exceções com o uso direto de adapterPosition.

Ops! Você deve ter notado que o Lint do Android Studio IDE reclamou sobre o trecho de código fragment.callPasswordDialog() em onClick(), certo?

Isso ocorreu porque o método callPasswordDialog() ainda está como protected. Logo, em FormFragment, atualize o método callPasswordDialog() removendo da assinatura dele o protected, como a seguir:

...
fun callPasswordDialog(){
...
}
...

E quando não houver cartões cadastrados?

Ok, Thiengo. Mas notei que não há nenhum algoritmo sobre o status do ReyclerView: sem cartões de crédito cadastrados. É isso, mesmo?

Mensagem de lista sem cartões de crédito

Sim, pois vamos colocar essa algoritmo agora em ConfigCreditCardsListFragment.

Basicamente vamos trabalhar com um observer no adapter do RecyclerView para saber quando há ou não itens em lista:

  • Caso não haja itens, apresentaremos uma mensagem em tela informando isso, por meio de um TextView;
  • Caso contrário o TextView é escondido e somente os cartões ficam em tela.

Primeiro temos de atualizar o layout de ConfigCreditCardsListFragment. Em /res/layout/fragment_config_credit_cards_list.xml coloque o código XML como o novo a seguir:

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

<TextView
android:visibility="gone"
android:id="@+id/tv_empty_list"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="30dp"
android:text="@string/credit_card_list_empty"/>

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_credit_cards"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"/>
</FrameLayout>

 

A parte adicionada está em destaque.

Em ConfigCreditCardsListFragment acrescente o código de RecyclerView.AdapterDataObserver como a seguir:

class ConfigCreditCardsListFragment : ... {
...

private fun initItems(){
...

adapter.registerAdapterDataObserver( RecyclerViewObserver() )
rv_credit_cards.adapter = adapter
}

/*
* Com o RecyclerView.AdapterDataObserver é possível
* escutar o tamanho atual da lista de itens vinculada
* ao RecyclerView e caso essa lista esteja vazia, então
* podemos apresentar uma mensagem ao usuário informando
* sobre a lista vazia.
* */
inner class RecyclerViewObserver :
RecyclerView.AdapterDataObserver() {

override fun onItemRangeRemoved( positionStart: Int, itemCount: Int ) {
super.onItemRangeRemoved( positionStart, itemCount )

tv_empty_list.visibility =
if( rv_credit_cards.adapter!!.itemCount == 0 )
View.VISIBLE
else
View.GONE
}
}
}

 

Simples, certo? Confesso que, como você, eu esperava algo ainda mais tranquilo, tipo: somente vincular uma mensagem ao RecyclerView para ser apresentada em tela quando este estivesse sem itens em adapter... mas enfim...

Quando não houver itens em lista, teremos:

Nova configuração de mensagem de lista sem cartões de crédito

Formulário de novo cartão de crédito

O formulário de novo cartão de crédito tende a ser bem mais simples em desenvolvimento do que o de lista de cartões, isso, pois boa parte do código é muito similar ao que já foi desenvolvido nos fragmentos de e-mail e de senha da área de atualização de dados de conexão.

Formulário de novo cartão de crédito

Porém também teremos de trabalhar com máscara em campo, algo que eu achei melhor colocar em um conjunto separado de seções, ainda neste artigo.

Atualização do strings.xml

Vamos iniciar pela parte fácil. Em /res/values/strings.xml acrescente os trechos em destaque:

<resources>
...

<!-- ConfigNewCreditCardFragment -->
<string name="config_credit_cards_tab_new">NOVO</string>

<string name="add_credit_card_number_label">Número:</string>
<string name="add_credit_card_enterprise_label">Bandeira:</string>
<string name="add_credit_card_owner_name_label">
Nome (como está no cartão):
</string>
<string name="add_credit_card_owner_reg_number_label">
CPF / CNPJ do proprietário do cartão:
</string>
<string name="add_credit_card_expiry_label">Vencimento:</string>
<string name="add_credit_card_expiry_month_label">Mês:</string>
<string name="add_credit_card_expiry_year_label">
Ano (quatro dígitos):
</string>
<string name="add_credit_card_security_code_label">
Código de segurança:
</string>

<string name="add_credit_card">Adicionar cartão</string>
<string name="add_credit_card_going">Adicionando&#8230;</string>

<string name="invalid_credit_card">
Cartão inválido, verifique os dados novamente.
</string>
</resources>

Configuração dos Spinners

Para os Spinners teremos algumas configurações a mais e ainda não construídas em projeto. Isso, para que os campos de Select, ou ComboBox, sejam muito similares aos simples campos de entrada de texto.

Background drawable

O background dos Spinners trabalhará com um <layer-list>, pois além de definir configurações de estilo (curvatura de cantos, cor de background e espessura de borda) também vai definir as configurações da imagem que indica ao usuário que aquele campo é um Spinner.

Em resumo: o <layer-list> permite o uso de outros objetos desenháveis em um único objeto desenhável, o empilhamento é de acordo com o posicionamento em lista, onde o último <item> é o que aparece no topo da visualização.

Antes de irmos direto ao XML que contém o <layer-list>, devemos primeiro adicionar ao projeto a imagem que será apresentada como a "seta do Spinner".

Seta Spinner

Acesse está imagem direto do GitHub do projeto em:

Coloque cada versão do ícone ic_keyboard_arrow_down_white_18dp.png em seus respectivos folders drawable.

Assim, em /res/drawable adicione o XML bg_form_spinner.xml com a seguinte configuração:

<?xml version="1.0" encoding="utf-8"?>
<layer-list
xmlns:android="http://schemas.android.com/apk/res/android">

<!--
Item de definição de background de shape.
-->
<item>
<!--
Definindo cor de background; curvatura de pontas;
e largura e cor de borda.
-->
<shape android:shape="rectangle">
<solid android:color="@android:color/white" />
<corners android:radius="5dp" />
<stroke
android:color="@color/colorViewLine"
android:width="1dp" />
</shape>
</item>

<!--
Item de definição de imagem de background, posicionada
a 1dp à direita da View.
-->
<item android:right="1dp">
<!--
Definindo a imagem e o posicionamento dentro da View.
-->
<bitmap
android:gravity="center_vertical|right"
android:tint="@color/colorText"
android:src="@drawable/ic_keyboard_arrow_down_white_18dp" />
</item>
</layer-list>

Tema (estilo)

Ainda em relação ao design dos Spinners, tendo em mente que ao menos os Spinners de formulário terão o mesmo design, precisamos agora de um tema somente para este tipo de componente.

Em /res/values/styles.xml adicione o tema SpinnerForm como a seguir:

<resources>
...

<style name="SpinnerForm">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:paddingBottom">12dp</item>
<item name="android:paddingTop">11dp</item>
<item name="android:background">@drawable/bg_form_spinner</item>
</style>
</resources>

Dados em array

Por fim os dados que serão vinculados aos Spinners do formulário de novo cartão de crédito. Em /res/values crie o XML arrays.xml com os dados a seguir:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="credit_card_enterprise">
<item>Visa</item>
<item>Mastercard</item>
<item>Discover</item>
<item>American Express</item>
</string-array>

<string-array name="months_number">
<item>01</item>
<item>02</item>
<item>03</item>
<item>04</item>
<item>05</item>
<item>06</item>
<item>07</item>
<item>08</item>
<item>09</item>
<item>10</item>
<item>11</item>
<item>12</item>
</string-array>
</resources>

Estilo dos rótulos de campos

Antes de irmos direto ao layout, ainda precisamos de um novo estilo para os rótulos de campos do formulário de novo cartão. Todos são de design igual:

rótulos de campos do formulário de novo cartão

Em /res/values/styles.xml adicione o tema TextViewFormLabel como a seguir:

<resources>
...

<style name="TextViewFormLabel">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_marginBottom">2dp</item>
<item name="android:textColor">@color/colorText</item>
</style>
</resources>

Definição do layout

Por fim o XML principal do formulário de novo cartão. Em /res/layout adicione o layout fragment_config_new_credit_card.xml com o código a seguir:

<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:orientation="vertical">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:baselineAligned="false">

<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.7"
android:layout_marginRight="12dp"
android:layout_marginEnd="12dp"
android:orientation="vertical">

<TextView
style="@style/TextViewFormLabel"
android:text="@string/add_credit_card_number_label"/>

<EditText
android:id="@+id/et_credit_card_number"
style="@style/EditTextFormField"
android:layout_width="match_parent"
android:background="@drawable/bg_form_field"
android:inputType="number"
android:imeOptions="actionNext"/>
</LinearLayout>

<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.3"
android:orientation="vertical">

<TextView
style="@style/TextViewFormLabel"
android:text="@string/add_credit_card_enterprise_label"/>

<Spinner
android:id="@+id/sp_credit_card_enterprise"
style="@style/SpinnerForm"
android:entries="@array/credit_card_enterprise"/>
</LinearLayout>
</LinearLayout>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="vertical">

<TextView
style="@style/TextViewFormLabel"
android:text="@string/add_credit_card_owner_name_label"/>

<EditText
android:id="@+id/et_credit_card_owner_name"
style="@style/EditTextFormField"
android:layout_width="match_parent"
android:background="@drawable/bg_form_field"
android:inputType="textPersonName"
android:imeOptions="actionNext"/>
</LinearLayout>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="vertical">

<TextView
style="@style/TextViewFormLabel"
android:text="@string/add_credit_card_owner_reg_number_label"/>

<EditText
android:id="@+id/et_credit_card_owner_reg"
style="@style/EditTextFormField"
android:layout_width="match_parent"
android:background="@drawable/bg_form_field"
android:inputType="number"
android:imeOptions="actionNext"/>
</LinearLayout>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="horizontal">

<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.6"
android:orientation="vertical">

<TextView
style="@style/TextViewFormLabel"
android:text="@string/add_credit_card_expiry_label"/>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:baselineAligned="false">

<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.3"
android:layout_marginRight="12dp"
android:layout_marginEnd="12dp"
android:orientation="vertical">

<TextView
style="@style/TextViewFormLabel"
android:text="@string/add_credit_card_expiry_month_label"/>

<Spinner
android:id="@+id/sp_credit_card_expiry_month"
style="@style/SpinnerForm"
android:entries="@array/months_number"/>
</LinearLayout>

<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.7"
android:orientation="vertical">

<TextView
style="@style/TextViewFormLabel"
android:text="@string/add_credit_card_expiry_year_label"/>

<EditText
android:id="@+id/et_credit_card_expiry_year"
style="@style/EditTextFormField"
android:layout_width="match_parent"
android:background="@drawable/bg_form_field"
android:inputType="number"
android:maxLength="4"
android:imeOptions="actionNext"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>

<View
android:background="@color/colorViewLine"
android:layout_width="1dp"
android:layout_height="match_parent"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"/>

<LinearLayout
android:layout_gravity="bottom"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.3"
android:orientation="vertical">

<TextView
style="@style/TextViewFormLabel"
android:text="@string/add_credit_card_security_code_label"/>

<EditText
android:id="@+id/et_credit_card_security_code"
style="@style/EditTextFormField"
android:layout_width="match_parent"
android:background="@drawable/bg_form_field"
android:inputType="number"
android:imeOptions="actionDone"/>
</LinearLayout>
</LinearLayout>

<Button
android:id="@+id/bt_add_credit_card"
style="@style/ButtonForm"
android:layout_marginTop="24dp"
android:layout_marginBottom="3dp"
android:layout_marginEnd="1dp"
android:layout_marginRight="1dp"
android:layout_gravity="end"
android:text="@string/add_credit_card"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

 

Certamente você está se perguntando: que ridículo, para que essa quantidade absurda de LinearLayout se um único ConstraintLayout certamente resolveria todos os posicionamentos?

Na verdade eu optei pela velocidade no desenvolvimento deste layout sem perder na qualidade do que foi definido em protótipo estático.

Eu concordaria contigo sobre "que ridículo ..." se este fosse um layout de itens de framework de lista, mas não é!

Ou seja, o layout será criado uma única vez, sem pesar na parte de memória disponível ao aplicativo como pesaria se fosse criado inúmeras vezes em framework de lista.

E utilizando vários LinearLayouts é bem provável que o desenvolvimento seja mais simples, pois com o RelativeLayout ou com o ConstraintLayout seria necessário "quebrar a cabeça" com os inúmeros atributos de posicionamento e tamanho de View para conseguir exatamente o que é esperado em protótipo.

Em resumo: utilizar vários LinearLayouts nesta parte do projeto foi uma escolha, a principio, eficiente e eficaz.

A seguir o grande diagrama do layout anterior:

Diagrama do layout fragment_config_new_credit_card.xml

Desenvolvendo a ConfigNewCreditCardFragment

Acredite, o fragmento ConfigNewCreditCardFragment é bem simples, ao menos até este ponto do artigo.

Em nosso novo pacote, /view/config/creditcard, crie um novo fragmento com o rótulo ConfigNewCreditCardFragment e com a seguinte configuração de código:

class ConfigNewCreditCardFragment :
FormFragment() {

companion object{
const val TAB_TITLE = R.string.config_credit_cards_tab_new
}

override fun getLayoutResourceID()
= R.layout.fragment_config_new_credit_card

override fun onActivityCreated( savedInstanceState: Bundle? ) {
super.onActivityCreated( savedInstanceState )

updateFlFormToFullFreeScreen()

bt_add_credit_card.setOnClickListener{
/*
* O método mainAction() é invocado no lugar
* de callPasswordDialog(), pois aqui não há
* necessidade de dialog de senha para a
* adição de cartão de crédito.
* */
mainAction()
}

et_credit_card_security_code.setOnEditorActionListener( this )
}

override fun backEndFakeDelay(){
backEndFakeDelay(
false,
getString( R.string.invalid_credit_card )
)
}

override fun blockFields( status: Boolean ){
et_credit_card_number.isEnabled = !status
sp_credit_card_enterprise.isEnabled = !status
et_credit_card_owner_name.isEnabled = !status
et_credit_card_owner_reg.isEnabled = !status
sp_credit_card_expiry_month.isEnabled = !status
et_credit_card_expiry_year.isEnabled = !status
et_credit_card_security_code.isEnabled = !status
bt_add_credit_card.isEnabled = !status
}

override fun isMainButtonSending( status: Boolean ){
bt_add_credit_card.text =
if( status )
getString( R.string.add_credit_card_going )
else
getString( R.string.add_credit_card )
}
}

 

Como não há adapter e framework de lista neste fragmento, também não há os temidos, por alguns, callbacks.

Mas ainda falta algo neste fragmento de formulário de novo cartão.

Máscara para campos de cartão de crédito

Se fosse já possível executar nosso projeto com a área de configurações de cartões de crédito funcional e assim acessando a tela de novo cartão de crédito e a preenchendo, teríamos:

Campos de formulário sem máscara

Porém o aguardado nos campos de número de cartão e de número de registro (CPF ou CNPJ) do proprietário do cartão é:

Campos de formulário com máscara

Ou seja, ainda não temos o trabalho com máscara em campo em nosso formulário. Para isso utilizaremos alguma biblioteca externa ao Android, uma biblioteca simples e robusta.

Isso, pois a outra opção seria aplicar a máscara na "unha" utilizando expressão regular. Acredite, na unha nós teríamos de investir um bom tempo em uma solução que certamente a comunidade Android já tem pronta e com a "magnifica" licença Apache 2.0.

Biblioteca utilizada

Aqui utilizaremos a biblioteca Mask-EditText que está entre as bibliotecas de máscara para campos Android mais utilizadas pela comunidade de desenvolvedores.

Máscara de campo com a API Mask-EditText

A documentação oficial está em inglês, mas é bem simples de entender e utilizar a Mask-EditText: https://github.com/santalu/mask-edittext.

Configuração em arquivos Gradle

No arquivo Gradle de Nível de Projeto, ou build.gradle (Project: BlueShoes), seria necessário, caso não houvesse já configurado, o seguinte trecho em destaque:

...
allprojects {
repositories {
...
maven { url "https://jitpack.io" }
}
}
...

 

Como aqui nós tínhamos está configuração, podemos partir direto para a segunda parte da implantação da biblioteca.

No Gradle Nível de Aplicativo, ou build.gradle (Module: app), adicione a referência à Mask-EditText como em destaque a seguir:

...
dependencies {
...

/* Máscara EditText API */
implementation 'com.github.santalu:mask-edittext:1.1.1'
}
...

 

Por fim, sincronize o projeto.

Máscara para o campo de número

Nosso primeiro uso da View <com.santalu.maskedittext.MaskEditText> da biblioteca Mask-EditText será para o campo de número de cartão, pois a configuração é bem simples.

Em /res/layout/fragment_config_new_credit_card.xml coloque o campo de número de cartão, de ID et_credit_card_number, com a seguinte nova configuração:

...
<com.santalu.maskedittext.MaskEditText
android:id="@+id/met_credit_card_number"
style="@style/EditTextFormField"
android:layout_width="match_parent"
android:background="@drawable/bg_form_field"
android:inputType="number"
android:imeOptions="actionNext"
app:met_mask="#### #### #### ####"/>
...

 

O que realmente mudou em relação ao antigo campo foi:

  • Agora a View é uma subclasse de EditText, MaskEditText, ao invés do EditText direto em layout;
  • O ID agora é met_credit_card_number ao invés de et_credit_card_number, respeitando um padrão para IDs de Views adotado em projeto;
  • O atributo app:met_mask defini o padrão aguardado em campo. O símbolo de jogo-da-velha, #, na verdade representa qualquer caractere em campo e não somente números. O que defini que somente números são aceitos, digo, aceitos em digitação pelo usuário, é o atributo android:inputType="number".

Devido a mudança de ID do campo de número de cartão, ainda precisamos de uma pequena atualização no método blockFields() do fragmento ConfigNewCreditCardFragment. Atualização em destaque a seguir:

...
override fun blockFields( status: Boolean ){
met_credit_card_number.isEnabled = !status
...
}
...

 

Com isso a máscara para número de cartão está definida.

Máscara para o campo de CPF / CNPJ

A máscara para o campo de número de registro do proprietário do cartão é um pouco mais crítica de ser configurada, pois há dois padrões possíveis:

  • CPF;
  • e CNPJ.

A API Mask-EditText, até o momento da construção deste artigo, não tinha a possibilidade de trabalho com mais de um padrão em um mesmo campo.

Sendo assim a nossa estratégia será:

  • Quando o campo de número de registro do proprietário do cartão estiver com foco, não haverá máscara alguma aplicada;
  • Assim que o campo perder o foco, será verificado o tipo de dado dentro dele;
  • Se for um CPF válido, está máscara será aplicada em código dinâmico para que seja refletido na tela;
  • O mesmo roteiro do item anterior ocorrerá para um CNPJ válido, porém com a máscara de CNPJ;
  • Caso não seja nem um CPF e nem um CNPJ, então o campo permanece sem máscara definida.

O fluxograma a seguir vai auxiliar no entendimento da lógica que aplicaremos:

Fluxograma do campo da máscara do campo de CPF / CNPJ

Sendo assim, vamos aos códigos.

Configuração em layout

Primeiro a configuração no XML do formulário. Muito similar ao que fizemos com o campo de número de cartão de crédito.

Em /res/layout/fragment_config_new_credit_card.xml coloque no lugar do EditText de ID et_credit_card_owner_reg o MaskEditText com o ID met_credit_card_owner_reg como a seguir:

...
<com.santalu.maskedittext.MaskEditText
android:id="@+id/met_credit_card_owner_reg"
style="@style/EditTextFormField"
android:layout_width="match_parent"
android:background="@drawable/bg_form_field"
android:inputType="number"
android:imeOptions="actionNext"/>
...

 

Note que desta vez não definimos o atributo app:met_mask, pois esta parte será via lógica de negócio em código dinâmico.

Não esqueça de atualizar o método blockFields() do fragmento ConfigNewCreditCardFragment com o novo ID de campo:

...
override fun blockFields( status: Boolean ){
...
met_credit_card_owner_reg.isEnabled = !status
...
}
...

Funções estendidas para validação de CPF e de CNPJ

Vamos aproveitar o arquivo extension_functions.kt, presente no pacote /util, que já contém funções estendidas especificas para formulários, funções para validação de e-mail e senha.

Vamos aproveitar esse espaço e adicionar os populares algoritmos de validação de CPF e CNPJ respectivamente.

Em extension_functions.kt adicione os trechos a seguir:

...
/*
* A seguir códigos de validação de CPF e CNPJ.
*
* Fontes:
* Código: https://www.vivaolinux.com.br/script/Codigo-para-validar-CPF-e-CNPJ-otimizado
* Explicação: https://www.geradorcpf.com/algoritmo_do_cpf.htm
* */

private val weightCPF = intArrayOf(11, 10, 9, 8, 7, 6, 5, 4, 3, 2)
private val weightCNPJ = intArrayOf(6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2)

fun String.isValidCPF() : Boolean {
if( this.length != 11 )
return false

val digit1 = digitCalc( this.substring(0, 9), weightCPF )
val digit2 = digitCalc(this.substring(0, 9) + digit1, weightCPF )

return this == (this.substring(0, 9) + digit1.toString() + digit2.toString())
}

fun String.isValidCNPJ() : Boolean {
if( this.length != 14 )
return false

val digit1 = digitCalc( this.substring(0, 12), weightCNPJ )
val digit2 = digitCalc( this.substring(0, 12) + digit1, weightCNPJ )

return this == (this.substring(0, 12) + digit1.toString() + digit2.toString())
}

private fun digitCalc(
str: String,
weight: IntArray ): Int {

var sum = 0
var index = str.length - 1
var digit: Int

while (index >= 0) {
digit = Integer.parseInt( str.substring(index, index + 1) )
sum += digit * weight[ weight.size - str.length + index ]
index--
}

sum = 11 - sum % 11

return if(sum > 9)
0
else
sum
}

 

Vale ressaltar que em outras partes do aplicativo (tela de checkout, por exemplo) será possível adicionar um novo cartão de crédito, ou seja, um outro formulário que utilizará os algoritmos de validação de CPF e CNPJ.

Este é na verdade o principal motivo de termos colocado estes algoritmos em extension_functions.kt. Eles servirão a áreas diferentes em projeto, por isso a necessidade de estarem em um local genérico.

Não iria fazer sentido, em leitura de código, colocarmos no outro formulário de adição de novo cartão de crédito, formulário acionado na tela de checkout, algo como: ConfigNewCreditCardFragment.isValidCPF().

O rótulo ConfigNewCreditCardFragment em um dialog, por exemplo, não faria sentido para quem estivesse lendo o código para uma eventual manutenção.

Configuração em código dinâmico

Nosso primeiro passo em ConfigNewCreditCardFragment é adicionar o listener de "mudança de status de foco" ao campo de número de registro do proprietário do cartão.

Em ConfigNewCreditCardFragment adicione os trechos em destaque:

class ConfigNewCreditCardFragment :
FormFragment(),
View.OnFocusChangeListener {
...

override fun onActivityCreated( ... ) {
...

met_credit_card_owner_reg.setOnFocusChangeListener( this )
}

override fun onFocusChange(
view: View,
hasFocus: Boolean) {

/* TODO */
}
}

 

Com isso somente precisamos colocar a lógica de negócio em onFocusChange() que atribuirá a máscara correta ao campo de acordo com o valor dentro dele:

...
override fun onFocusChange(
view: View,
hasFocus: Boolean ) {

var mask = "" /* Sem máscara. */
val editText = view as MaskEditText

/*
* Garantindo que o conteúdo em teste terá apenas
* números.
* */
val pattern = Regex("[^0-9 ]")
val content = editText
.text
.toString()
.replace( pattern, "" )

if( !hasFocus ){
if( content.isValidCPF() ){
/* Máscara CPF. */
mask = "###.###.###-##"
}
else if( content.isValidCNPJ() ){
/* Máscara CNPJ. */
mask = "##.###.###/####-##"
}
}

/*
* Aplicando a nova máscara de acordo com o
* padrão de código presente em campo.
* */
editText.mask = mask

/*
* Para que a nova máscara seja aplicada é preciso
* forçar uma atualização no campo.
* */
editText.setText( content )
}
...

 

Done! Agora podemos seguramente ir às atualizações finais desta área do projeto, trabalhar a atividade host dos fragmentos criados.

Atividade de cartões de crédito

A atividade host dos fragmentos de gerência de cartões de crédito é simples como a atividade dos fragmentos da área de configuração de dados de acesso.

Novamente uma FragmentPagerAdapter será utilizada para que os fragmentos sejam mantidos em memória o tempo que for necessário.

Valores em arquivo de Strings

No arquivo de Strings, /res/values/strings.xml, adicione o trecho em destaque:

<resources>
...

<!-- ConfigCreditCardsActivity -->
<string name="title_activity_config_credit_cards">
Cartões de crédito
</string>
</resources>

Definindo o layout principal

O layout, pode acreditar, é exatamente o mesmo da atividade ConfigConnectionDataActivity. O que primeiro vamos fazer aqui é dar a ele um nome genérico, pois ele passará a ser utilizado em mais de um contexto.

Acesse layout activity_config_connection_data.xml em /res/layout, então:

  • Clique com o botão direito do mouse no nome do layout;
  • Acesse Refactor;
  • Clique em Rename;
  • No campo do dialog aberto coloque como nome o rótulo activity_tabs_user_config.xml;
  • Clique em Refactor.

Renomeando o layout activity_config_connection_data.xml

Desta forma, aonde havia referência a activity_config_connection_data.xml no projeto passará a ser para activity_tabs_user_config.xml.

Com isso, recapitulando o layout principal, temos em /res/layout/activity_tabs_user_config.xml a seguinte estrutura:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<com.google.android.material.appbar.AppBarLayout
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:theme="@style/AppTheme.AppBarOverlay">

<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/Toolbar"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:titleTextAppearance="@style/TitleAppearance"/>

<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
app:tabIndicatorColor="@color/colorPrimaryDark"
app:tabSelectedTextColor="@color/colorText"
app:tabTextColor="@color/colorPrimaryDark"/>

</com.google.android.material.appbar.AppBarLayout>

<androidx.viewpager.widget.ViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

 

Note que seguramente foi removido o atributo tools:context, pois o layout agora atende a mais de um contexto.

A seguir o diagrama do layout activity_tabs_user_config.xml:

Diagrama do layout activity_tabs_user_config.xml

Construindo a ConfigCreditCardsSectionsAdapter

ConfigCreditCardsSectionsAdapter é a entidade adaptadora de fragmentos para o ViewPager da atividade principal.

Ela é muito similar ao adapter de fragmentos da área de configuração de dados de conexão. Sendo assim é bem provável que todas as classes adaptadoras de fragmentos, ao menos as classes da área de configurações de conta de usuário, se tornem uma só em futuras refatorações.

Enquanto isso, vamos criando a classe ConfigCreditCardsSectionsAdapter no pacote /view/config/creditcard com o código a seguir:

/**
* Um FragmentPagerAdapter que retorna um fragmento correspondendo
* a uma das sections/tabs/pages.
*
* Mesmo que o método getItem() indique que mais de uma instância
* do mesmo fragmento será criada, na verdade objetos
* FragmentPagerAdapter mantêm os fragmentos em memória para que
* eles possam ser utilizados novamente, isso enquanto houver
* caminho de volta a eles (transição entre Tabs, por exemplo).
*/
class ConfigCreditCardsSectionsAdapter(
val context: Context,
fm: FragmentManager ) : FragmentPagerAdapter( fm ) {

companion object{
const val TOTAL_PAGES = 2
const val CREDIT_CARDS_PAGE_POS = 0
}

/*
* getItem() é invocado para devolver uma instância do
* fragmento correspondendo a posição (seção/página)
* informada.
* */
override fun getItem( position: Int )
= when( position ){
CREDIT_CARDS_PAGE_POS -> ConfigCreditCardsListFragment()
else -> ConfigNewCreditCardFragment()
}

override fun getPageTitle( position: Int )
= context.getString(
when( position ){
CREDIT_CARDS_PAGE_POS -> ConfigCreditCardsListFragment.TAB_TITLE
else -> ConfigNewCreditCardFragment.TAB_TITLE
}
)

override fun getCount()
= TOTAL_PAGES
}

Desenvolvendo a ConfigCreditCardsActivity

Por fim a atividade container dos fragmentos de gerência de cartões de crédito do usuário.

Em /view/config/creditcard adicione a atividade ConfigCreditCardsActivity com o código a seguir:

class ConfigCreditCardsActivity :
AppCompatActivity() {

override fun onCreate( savedInstanceState: Bundle? ) {
super.onCreate( savedInstanceState )
setContentView( R.layout.activity_tabs_user_config )
setSupportActionBar( toolbar )

/*
* Para liberar o back button na barra de topo da
* atividade.
* */
supportActionBar?.setDisplayHomeAsUpEnabled( true )
supportActionBar?.setDisplayShowHomeEnabled( true )

/*
* Hackcode para que a imagem de background do layout não
* se ajuste de acordo com a abertura do teclado de
* digitação. Caso utilizando o atributo
* android:background, o ajuste ocorre, desconfigurando o
* layout.
* */
window.setBackgroundDrawableResource( R.drawable.bg_activity )

/*
* Criando o adaptador de fragmentos que ficarão expostos
* no ViewPager.
* */
val sectionsPagerAdapter =
ConfigCreditCardsSectionsAdapter(
this,
supportFragmentManager
)

/*
* Acessando o ViewPager e vinculando o adaptador de
* fragmentos a ele.
* */
val viewPager: ViewPager = findViewById( R.id.view_pager )
viewPager.adapter = sectionsPagerAdapter

/*
* Acessando o TabLayout e vinculando ele ao ViewPager
* para que haja sincronia na escolha realizada em
* qualquer um destes componentes visuais.
* */
val tabs: TabLayout = findViewById( R.id.tabs )
tabs.setupWithViewPager( viewPager )
}

/*
* Para permitir que o back button tenha a ação de volta para
* a atividade anterior.
* */
override fun onOptionsItemSelected( item: MenuItem ): Boolean {

if( item.itemId == android.R.id.home ){
finish()
return true
}
return super.onOptionsItemSelected( item )
}
}

 

Muito similar a atividade ConfigConnectionDataActivity. Ou seja, em refatoração muito código será ao menos movido para uma atividade ancestral dessas duas.

Atualizando o AndroidManifest

Ainda é preciso configurar a nova atividade no AndroidManifest.xml para que ela se torne acessível:

<?xml version="1.0" encoding="utf-8"?>
<manifest
...>
...

<application
...>
...

<activity
android:name=".view.config.creditcard.ConfigCreditCardsActivity"
android:label="@string/title_activity_config_credit_cards"
android:theme="@style/AppTheme.NoActionBar" />
</application>
</manifest>

Atualizando a AccountSettingsItemsDataBase

Agora em AccountSettingsItemsDataBase, mais precisamente no quarto (e último) item do método getItems(), vamos adicionar a referência a atividade ConfigCreditCardsActivity:

class AccountSettingsItemsDataBase {

companion object{

fun getItems( ... )
= listOf(
...
AccountSettingItem(
...
ConfigCreditCardsActivity::class.java
)
)
}
}

 

So, let's test it.

Testes e resultados

Com a sua instalação do Android Studio aberta, vá ao menu de topo e logo depois clique em "Build". Então clique em "Rebuid project". Ao final do rebuild execute o aplicativo em seu emulador Android.

Não esqueça de colocar o objeto user como um "usuário conectado". Este objeto está na MainActivity:

...
val user = User(
"Thiengo Vinícius",
R.drawable.user,
true /* Usuário conectado. */
)
...

 

Acessando a tela de cartões de crédito cadastrados e removendo todos eles, temos:

Animação da remoção de um cartão crédito

Agora acessando a tela de novo cartão e tentando adicionar um novo, temos:

Animação da tentativa de cadastro de um novo cartão de crédito

Com isso finalizamos mais um trecho importante da área de configurações de conta do usuário de nosso aplicativo Android de mobile-commerce, o BlueShoes.

Antes de seguir para a conclusão não esqueça de se inscrever na ðŸ“« lista de e-mails do Blog para receber semanalmente as aulas do projeto Android BlueShoes e outras atualizações do dev mobile.

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

Vídeos

A seguir os vídeos com o desenvolvimento, passo a passo, das telas de gerência de cartões de crédito:

O código do projeto pode ser acessado pelo GitHub dele em: https://github.com/viniciusthiengo/blueshoes-kotlin-android.

Conclusão

A criação de todas as telas e entidades da área de configurações de cartões de crédito foi simples devido a já existência das telas e classes da área de configurações de dados de conexão.

Mas a facilidade de desenvolvimento nesta aula teve um preço: ainda tivemos de manter uma série de códigos repetidos, códigos que certamente deverão ser otimizados logo na próxima refatoração.

Nesta parte do projeto também tivemos de trabalhar com um tipo de API que é útil em inúmeros projetos Android, uma API de máscara de campo para os campos de "número de cartão de crédito" e "CPF / CNPJ".

Sem sombra de dúvidas que o trabalho com máscara de campo trás uma melhor experiência ao usuário final.

Ainda temos de desenvolver as telas de gerência de endereços do usuário, para assim finalizarmos a interface gráfica da área de configurações de conta.

Caso você tenha dúvidas sobre o projeto, principalmente sobre está 16ª aula, não deixe de comentar abaixo que logo eu lhe respondo.

Curtiu o conteúdo? Não esqueça de compartilha-lo. E, por fim, se inscreva na 📩 lista de e-mails, respondo às suas dúvidas também por lá.

Abraço.

Fontes

Create a Card-Based Layout

Data Binding Para Vinculo de Dados na UI Android

Trabalhando Análise Qualitativa em seu Aplicativo Android

Código para validar CPF e CNPJ otimizado

Decifrando o Algoritmo do CPF

No animation on item removal on RecyclerView - Resposta de Praveen Balaji

Can/should RecyclerView.Adapter have access to the ViewHolders? - Resposta de maciekjanusz

How to show an empty view with a RecyclerView? - Resposta de wonsuc e jungledev

Kotlin Remove all non alphanumeric characters - Resposta de hasen

Event for Handling the Focus of the EditText - Resposta de Wajid khan

Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba grátis conteúdos Android sem precedentes!
Email inválido

Relacionado

Fontes em XML, Android O. Configuração e UsoFontes em XML, Android O. Configuração e UsoAndroid
BottomNavigationView Android, Como e Quando UtilizarBottomNavigationView Android, Como e Quando UtilizarAndroid
PhotoView Android Para a Completa Implementação de ZoomPhotoView Android Para a Completa Implementação de ZoomAndroid
Políticas de Privacidade e Porque não a GDPR - Android M-CommercePolíticas de Privacidade e Porque não a GDPR - Android M-CommerceAndroid

Compartilhar

Comentários Facebook

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