Refatoração do Login, Pavimentando o Caminho Para Outros Formulários - 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 /Refatoração do Login, Pavimentando o Caminho Para Outros Formulários - Android M-Commerce

Refatoração do Login, Pavimentando o Caminho Para Outros Formulários - Android M-Commerce

Vinícius Thiengo
(2942) (3)
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ítuloManual de DevOps: como obter agilidade, confiabilidade e segurança em organizações tecnológicas
CategoriaEngenharia de Software
Autor(es)Gene Kim, Jez Humble, John Willis, Patrick Debois
EditoraAlta Books
Edição1ª
Ano2018
Páginas464
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, que dá continuidade a série sobre o desenvolvimento de um app Android de mobile-commerce, vamos refatorar a atividade de login, pois nela tem inúmeros códigos que serão úteis em outros pontos do projeto.

Animação pela tela de login refatorada

Antes de prosseguir, não deixe de se inscrever 📩 na lista de emails do Blog para ter acesso exclusivo às novas aulas do projeto.

A seguir os tópicos abordados:

Estou iniciando agora no projeto BlueShoes

Se este é o seu primeiro conteúdo do projeto Android BlueShoes, app de mobile- commerce, então saiba que outras seis aulas anteriores estão disponíveis e precisam ser consumidas antes da aula deste artigo:

Por que refatorar somente a tela de login?

A partir deste ponto do projeto teremos algumas outras telas contendo formulários, todos seguindo padrões de design como definido em protótipo estático. Algumas das telas são:

  • Cadastro de novo usuário;
  • Edição de e-mail / senha de usuário;
  • Recuperação de acesso.

Sendo assim, depois do desenvolvimento da tela de login é prudente ao menos encapsular o máximo possível de códigos dinâmicos e estáticos que podem ser reutilizados nos próximos formulários do aplicativo.

Ainda teremos inúmeras outras refatorações de código no decorrer do desenvolvimento deste projeto de mobile-commerce, mas está refatoração da tela de login se fez necessária primeiro devido às próximas telas que estaremos desenvolvendo.

Antes de prosseguir, saiba que você tem acesso completo aos fontes do projeto em: https://github.com/viniciusthiengo/blueshoes-kotlin-android.

Importante!

Mesmo que já informado na seção Estou iniciando agora no projeto BlueShoes, é muito importante que antes de prosseguir com o estudo deste artigo você tenha primeiro consumido o conteúdo de desenvolvimento da tela de login: Login com ConstraintLayout e TextWatcher Para Validação.

Somente assim você entenderá o que está sendo realizado aqui.

Refatorando

A partir deste ponto começaremos as modificações em código. Saiba que refatorar projetos de software é algo comum, principalmente quando o primeiro release do projeto já foi liberado.

Quando se falando em indústria, software não acadêmico, é normal termos nos primeiros releases códigos pouco estruturados e limpos. A melhoria vem com a evolução do projeto, isso principalmente devido a algo comum em projetos de software na industria: pouco tempo para entrega.

Estratégia utilizada

Desta vez, mesmo que começando com o encapsulamento de códigos estáticos, nossa estratégia será: começar pelo mais simples. E em alguns pontos o mais simples será trabalhar em códigos dinâmicos antes de em códigos estáticos.

Note que a refatoração aqui tem como foco: encapsular e melhorar todos os algoritmos que poderão ser reaproveitados em telas que contenham formulário.

Encapsulando o código estático de barra de topo

O primeiro trecho que deve ser encapsulado é o trecho estático de barra de topo, trecho comum nas duas atividades já presentes em projeto:

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

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

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

 

O código estático anterior é exatamente o mesmo para os layouts:

  • /res/layout/app_bar_main.xml;
  • /res/layout/activity_login.xml.

E ele também estará presente, da mesma maneira, em outras atividades com formulário.

Sendo assim, vamos encapsular essa parte. Em /res/layout:

  • Clique com o botão direito do mouse e acesse New;
  • Então clique em Layout resource file;
  • Em File name coloque app_bar.xml;
  • Clique em OK.

Criando o layout app_bar.xml

No novo layout coloque o seguinte código XML como conteúdo:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.AppBarLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:theme="@style/AppTheme.AppBarOverlay">

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

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

 

Agora a atualização de app_bar_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_activity"
tools:context=".view.MainActivity">

<include layout="@layout/app_bar" />

<FrameLayout
android:id="@+id/fl_fragment_container"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_width="match_parent"
android:layout_height="match_parent" />

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

 

A seguir o novo diagrama do layout app_bar_main.xml:

Diagrama do layout app_bar_main.xml

E então a atualização de activity_login.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.LoginActivity">

<include layout="@layout/app_bar" />

<include layout="@layout/content_login"/>

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

 

E por fim o novo diagrama de activity_login.xml:

Diagrama do layout activity_login.xml

Note que na MainActivity será necessário trocar o import que dá acesso a toolbar:

  • De kotlinx.android.synthetic.main.app_bar_main.*;
  • Para kotlinx.android.synthetic.main.app_bar.*.

Ainda não atualize o import em LoginActivity, pois está atividade passará por outras modificações que não exigirão essa atualização de import.

Criando o layout único de tela proxy

A partir de LoginActivity, mais precisamente do XML content_login.xml, já tínhamos definido o layout mínimo da tela de proxy, layout mínimo que também será utilizado em todos os formulários do projeto:

...
<FrameLayout
android:id="@+id/fl_proxy_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:background="@color/colorBackgroundProxy">

<ProgressBar
android:layout_gravity="center"
android:layout_width="50dp"
android:layout_height="50dp"
android:theme="@style/ProgressBarGreyProxy"/>
</FrameLayout>
...

 

Nós realmente não queremos ter de atualizar cada layout de formulário caso seja necessária alguma alteração nas Views de proxy.

Sendo assim, em /res/layout, crie um novo layout (pode ser com Ctrl + C e Ctrl + V) com o rótulo proxy_screen.xml e com o código XML a seguir:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fl_proxy_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:background="@color/colorBackgroundProxy">

<ProgressBar
android:layout_gravity="center"
android:layout_width="50dp"
android:layout_height="50dp"
android:theme="@style/ProgressBarGreyProxy"/>
</FrameLayout>

 

Agora, em /res/layout/content_login.xml, haverá a seguinte referência ao layout proxy:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView
...>

<FrameLayout
...>

<android.support.constraint.ConstraintLayout
...>
...
</android.support.constraint.ConstraintLayout>

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

</FrameLayout>

</android.support.v4.widget.NestedScrollView>

 

Abaixo o novo diagrama de content_login.xml:

Diagrama da primeira versão do layout content_login.xml

Thiengo, e o FrameLayout container, de ID fl_form_container, em content_login.xml, não faz parte do proxy?

Sim, faz, mas a tela de proxy, já encapsulada, poderá ser utilizada em outros pontos do projeto, que não são formulários, e consequentemente dispensam este FrameLayout container.

De qualquer forma esse FrameLayout container será ainda encapsulado, posteriormente nesta aula.

Não se preocupe agora com os imports de LoginActivity, vamos prosseguir com a codificação.

Estilo para os campos de formulário

Os campos de formulário presentes em content_login.xml têm alguns pontos iguais em termos de estilo e estrutura, pontos iguais que também serão aproveitados em outros campos de formulário do projeto:

Campos de formulário

Sendo assim podemos criar em /res/values/styles.xml o seguinte novo estilo:

<resources>
...

<style name="EditTextFormField">
<item name="android:layout_width">300dp</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:paddingTop">13dp</item>
<item name="android:paddingBottom">13dp</item>
<item name="android:paddingLeft">17dp</item>
<item name="android:paddingRight">17dp</item>
<item name="android:textSize">14sp</item>
</style>
</resources>

 

Com isso, em content_login.xml, podemos atualizar os dois EditTexts colocando o novo estilo:

...
<EditText
android:id="@+id/et_email"
style="@style/EditTextFormField"
android:background="@drawable/bg_form_field_top"
android:inputType="textEmailAddress"
android:imeOptions="actionNext"
android:hint="@string/hint_email"/>

<EditText
android:id="@+id/et_password"
style="@style/EditTextFormField"
android:layout_marginTop="-1dp"
android:background="@drawable/bg_form_field_bottom"
android:inputType="textPassword"
android:imeOptions="actionDone"
android:hint="@string/hint_password"/>
...

 

Note que somente os atributos que tendem a ter sempre os mesmos valores em campos de formulário do projeto é que foram encapsulados.

Estilo para os botões de formulário

O botão principal em content_login.xml também tem características que serão úteis em outros botões de formulário do projeto:

Botão principal do formulário de login

Alias, o botão de login presente no cabeçalho do menu gaveta de usuário não conectado já faz uso de algumas características.

Em /res/values/styles.xml adicione o estilo a seguir:

<resources>
...

<style name="ButtonForm">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:background">@drawable/bt_nav_header_login_bg</item>
<item name="android:textColor">@android:color/white</item>
<item name="android:textAllCaps">false</item>
</style>
</resources>

 

Agora em content_login.xml atualize o Button como a seguir:

...
<Button
android:id="@+id/bt_login"
style="@style/ButtonForm"
android:layout_marginTop="12dp"
android:paddingLeft="38dp"
android:paddingRight="38dp"
app:layout_constraintTop_toBottomOf="@+id/ll_container_fields"
app:layout_constraintRight_toRightOf="@+id/ll_container_fields"
android:onClick="mainAction"
android:text="@string/sign_in"/>
...

 

Então em /res/layout/nav_header_user_not_logged.xml atualize o Button de acesso a tela de login como abaixo:

...
<Button
android:id="@+id/bt_login"
style="@style/ButtonForm"
android:paddingLeft="30dp"
android:paddingRight="30dp"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:text="@string/tx_login"
android:onClick="callLoginActivity"/>
...

Atividade ancestral de formulários

A atividade de login contém muitos métodos que serão: ou por inteiro úteis a todos os formulários do projeto; ou parcialmente, tendo ao menos a assinatura de método sendo a mesma.

Com isso, criaremos uma nova atividade, abstrata, com métodos completos e métodos abstratos para assim evitar a repetição de código em atividades que contenham a responsabilidade de também terem formulários.

No pacote /view:

  • Clique com o botão direito do mouse e acesse New;
  • Clique em Kotlin File/Class;
  • Em Name coloque FormActivity;
  • Em Kind coloque Class;
  • Clique em OK.

Criando a FormActivity

Então, na nova atividade, coloque o código a seguir:

abstract class FormActivity : AppCompatActivity() {

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

supportActionBar?.setDisplayHomeAsUpEnabled( 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 )
}

/*
* Apresenta a tela de bloqueio que diz ao usuário que
* algo está sendo processado em background e que ele
* deve aguardar.
* */
protected fun showProxy( status: Boolean ){
fl_proxy_container.visibility =
if( status )
View.VISIBLE
else
View.GONE
}

/*
* Método responsável por apresentar um SnackBar com as
* corretas configurações de acordo com o feedback do
* back-end Web.
* */
protected fun snackBarFeedback(
viewContainer: ViewGroup,
status: Boolean,
message: String ){

val snackBar = Snackbar
.make(
viewContainer,
message,
Snackbar.LENGTH_LONG
)

/*
* Criando o objeto Drawable que entrará como ícone
* inicial no texto do SnackBar.
* */
val iconResource =
if( status )
R.drawable.ic_check_black_18dp
else
R.drawable.ic_close_black_18dp

val img = ResourcesCompat
.getDrawable(
resources,
iconResource,
null
)
img!!.setBounds(
0,
0,
img.intrinsicWidth,
img.intrinsicHeight
)

val iconColor =
if( status )
ContextCompat
.getColor(
this,
R.color.colorNavButton
)
else
Color.RED

img.setColorFilter(
iconColor,
PorterDuff.Mode.SRC_ATOP
)

/*
* Acessando o TextView padrão do SnackBar para assim
* colocarmos um ícone nele via objeto Spannable.
* */
val textView = snackBar.view.findViewById(
android.support.design.R.id.snackbar_text
) as TextView

/*
* O espaçamento aplicado como parte do argumento
* de SpannableString() é para que haja um espaço
* entre o ícone e o texto do SnackBar, como
* informado em protótipo estático.
* */
val spannedText = SpannableString( " ${textView.text}" )
spannedText.setSpan(
ImageSpan( img, ImageSpan.ALIGN_BOTTOM ),
0,
1,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)

textView.setText( spannedText, TextView.BufferType.SPANNABLE )

snackBar.show()
}

/*
* Responsável por conter o algoritmo de envio / validação
* de dados. Algoritmo vinculado ao menos ao principal
* botão em tela.
* */
abstract fun mainAction( view: View? = null )

/*
* Necessário para que os campos de formulário não possam
* ser acionados depois de enviados os dados.
* */
abstract fun blockFields( status: Boolean )

/*
* Muda o rótulo do botão principal de acordo com o status
* do envio de dados.
* */
abstract fun isMainButtonSending( status: Boolean )
}

 

Dos métodos que permaneceram até mesmo com o mesmo algoritmo de corpo, somente o snackBarFeedback() passou por algumas melhorias. A principal delas foi a remoção da propriedade snackBarView.

Dos métodos abstratos, dois sofreram mudança de rótulo:

  • login() agora é mainAction(). Com o termo mais genérico, outros códigos de formulário, que não são a respeito de login, não ficarão com a leitura prejudicada;
  • isSignInGoing() agora é isMainButtonSending(). O porquê é o mesmo da mudança do rótulo do método login(). Aqui está sendo utilizado o MainButton no rótulo do método para deixar claro que é um método específico para o botão principal do formulário.

Você deve estar se perguntando: e o layout, continuará sendo o activity_login.xml?

Não, vamos agora a essa atualização.

Layout principal de formulários

O layout principal da atividade ancestral das atividades com formulário terá a mesma característica de todos os layouts de atividades desenvolvidos até aqui: um layout principal contendo uma barra de todo e a parte de conteúdo.

Sendo assim, nosso primeiro passo neste tópico é criar primeiro o layout de conteúdo com uma estrutra genérica para poder conter qualquer formulário e também a tela de proxy.

Em /res/layout crie um novo layout com o rótulo content_form.xml e com o seguinte código XML:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView
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"
android:scrollbars="vertical"
android:fillViewport="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior">

<FrameLayout
android:id="@+id/fl_form_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">

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

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

</FrameLayout>

</android.support.v4.widget.NestedScrollView>

 

O NestedScrollView foi mantido, pois nós não temos controle sobre como será o tamanho, altura, do conteúdo em tela, digo, se ele vai ou não precisar de scroll.

O FrameLayout container, de ID fl_form_container, finalmente está em um local apropriado e que evitará a repetição dele em outros pontos do projeto.

O FrameLayout de ID fl_form conterá os layouts de conteúdo das subclasses de FormActivity.

E por fim, como último filho de fl_form_container, o include de proxy_screen.xml para que está tela fique sobre o formulário incluído em fl_form, digo, sobre ele quando estiver com a visibilidade em View.VISIBLE.

Abaixo o diagrama do layout anterior:

Diagrama do layout content_form.xml

E então o layout principal de FormActivity. Em /res/layout crie um novo layout com o rótulo activity_form.xml e com o código a seguir:

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

<include layout="@layout/app_bar" />

<include layout="@layout/content_form"/>

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

 

A seguir o diagrama do layout activity_form.xml:

Diagrama do layout activity_form.xml

Por fim, em FormActivity, atualize o layout invocado em setContentView() dentro do onCreate():

...
setContentView( R.layout.activity_form )
...

Layout do formulário de login

Antes de partirmos para as atualizações em código dinâmico, digo, atualizações em LoginActivity, vamos primeiro atualizar o layout content_login.xml.

Este layout agora tem a seguinte configuração XML:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:padding="16dp"
android:layout_width="match_parent"
android:layout_height="match_parent">

<LinearLayout
android:id="@+id/ll_container_fields"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent">

<EditText
android:id="@+id/et_email"
style="@style/EditTextFormField"
android:background="@drawable/bg_form_field_top"
android:inputType="textEmailAddress"
android:imeOptions="actionNext"
android:hint="@string/hint_email"/>

<EditText
android:id="@+id/et_password"
style="@style/EditTextFormField"
android:layout_marginTop="-1dp"
android:background="@drawable/bg_form_field_bottom"
android:inputType="textPassword"
android:imeOptions="actionDone"
android:hint="@string/hint_password"/>
</LinearLayout>

<TextView
android:id="@+id/tv_forgot_password"
style="@style/TextViewLink"
android:layout_marginTop="12dp"
app:layout_constraintTop_toBottomOf="@+id/ll_container_fields"
app:layout_constraintLeft_toLeftOf="@+id/ll_container_fields"
android:text="@string/forgot_my_password"/>

<Button
android:id="@+id/bt_login"
style="@style/ButtonForm"
android:layout_marginTop="12dp"
android:paddingLeft="38dp"
android:paddingRight="38dp"
app:layout_constraintTop_toBottomOf="@+id/ll_container_fields"
app:layout_constraintRight_toRightOf="@+id/ll_container_fields"
android:onClick="mainAction"
android:text="@string/sign_in"/>

<TextView
android:id="@+id/tv_or"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="26dp"
app:layout_constraintTop_toBottomOf="@+id/bt_login"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:textColor="@color/colorText"
android:text="@string/or"/>

<TextView
android:id="@+id/tv_sign_up"
style="@style/TextViewLink"
app:layout_constraintTop_toBottomOf="@+id/tv_or"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:text="@string/sign_up"/>

<include layout="@layout/text_view_privacy_policy_login"/>

</android.support.constraint.ConstraintLayout>

 

A seguir o novo diagrama de content_login.xml:

Diagrama da segunda versão do layout content_login.xml

Lembrando que content_login.xml deverá ser carregado dentro do FrameLayout fl_form que está em content_form.xml.

Atualizações de herança em LoginActivity

Com a nova FormActivity, temos de atualizar alguns trechos de código em LoginActivity assim que a herança for adicionada.

Nosso primeiro passo é colocar a nova herança na assinatura da LoginActivity:

class LoginActivity :
FormActivity(),
TextView.OnEditorActionListener,
KeyboardUtils.OnSoftInputChangedListener {

...
}

 

Depois devemos adicionar o código que coloca o novo layout content_login.xml dentro de content_form.xml. No onCreate() de LoginActivity adicione o código em destaque a seguir:

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

/*
* Colocando a View de um arquivo XML como View filha
* do item indicado no terceiro argumento.
* */
View.inflate(
this,
R.layout.content_login,
fl_form
)

...
}
...

 

Note que o trecho View.inflate(...) entra no lugar de:

...
setContentView( R.layout.activity_form )
setSupportActionBar( toolbar )
supportActionBar?.setDisplayHomeAsUpEnabled( true )
...

 

Códigos que agora estão na FormActivity.

Agora, ainda na LoginActivity, as três novas assinaturas dos métodos abstratos de FormActivity que devem ser implementados pelas suas subclasses:

...
override fun mainAction( view: View? ){ /* Antigo login() */
blockFields( true )
isMainButtonSending( true )
showProxy( true )
backEndFakeDelay()
}

override fun blockFields( status: Boolean ){
et_email.isEnabled = !status
et_password.isEnabled = !status
bt_login.isEnabled = !status
}

override fun isMainButtonSending( status: Boolean ){ /* Antigo isSignInGoing() */
bt_login.text =
if( status )
getString( R.string.sign_in_going )
else
getString( R.string.sign_in )
}
...

 

Assim a atualização em onEditorAction(), que agora deve referenciar à mainAction() ao invés de login():

...
override fun onEditorAction(
view: TextView,
actionId: Int,
event: KeyEvent? ): Boolean {

if( actionId == EditorInfo.IME_ACTION_DONE ){
closeVirtualKeyBoard( view )
mainAction()
return true
}
return false
}
...

 

Note que o novo layout content_login.xml, adicionado em seção anterior, já está com o Button principal configurado para mainAction() ao invés de login():

...
<Button
...
android:onClick="mainAction"
.../>
...

Remoção de closeVirtualKeyBoard()

Vamos atualizar o algoritmo de onEditorAction() em LoginActivity. Agora ele deve ser da seguinte maneira:

...
override fun onEditorAction(
view: TextView,
actionId: Int,
event: KeyEvent? ): Boolean {

mainAction()
return false
}
...

 

O return false indica à API interna que o listener de toque em algum botão de action no teclado virtual não foi consumido e que o processamento interno deve prosseguir. Porém, segundo testes, o processamento interno é apenas o fechamento do teclado virtual.

Sendo assim, o método closeVirtualKeyBoard() pode ser seguramente removido da LoginActivity.

Note que como o código de onEditorAction() ficou como código específico de domínio para a atividade de login, não foi mais necessário o uso de um condicional nele, pois o único campo de formulário que está com este listener ativo é o de senha, et_password, que tem vinculado a ele o actionDone.

Funções estendidas para validações de campos

Os atuais códigos de validação de e-mail e de senha são grandes e contém trechos repetidos:

...
et_email.addTextChangedListener( object: TextWatcher {
override fun afterTextChanged( content: Editable ) {
val message = getString( R.string.invalid_email )

et_email.error =
if( content.isNotEmpty()
&& Patterns.EMAIL_ADDRESS.matcher( content ).matches() )
null
else
message
}

override fun beforeTextChanged(
content: CharSequence?,
start: Int,
count: Int,
after: Int ) {}
override fun onTextChanged(
content: CharSequence?,
start: Int,
before: Int,
count: Int ) {}
} )

et_password.addTextChangedListener( object: TextWatcher {
override fun afterTextChanged( content: Editable ) {
val message = getString( R.string.invalid_password )

et_password.error =
if( content.length > 5 )
null
else
message
}

override fun beforeTextChanged(
content: CharSequence?,
start: Int,
count: Int,
after: Int ) {}
override fun onTextChanged(
content: CharSequence?,
start: Int,
before: Int,
count: Int ) {}
} )
...

 

Ambos estão no onCreate() de LoginActivity.

Mesmo sabendo que estes códigos serão utilizados somente em formulários que contenham ou o campo de e-mail ou o campo de senha, vamos coloca-los encapsulados como funções estendidas, pois como métodos de FormActivity seria menos produtivo em termos de "não repetir código".

No pacote /util:

  • Clique com o botão direito do mouse e acesse New;
  • Clique em Kotlin File/Class;
  • Em Name coloque extension_functions;
  • Em Kind permaneça com File;
  • Clique em OK.

Criando o arquivo extension_functions.kt

Dentro deste novo arquivo vamos primeiro colocar a função estendida responsável pela implementação de TextWatcher:

private fun EditText.afterTextChanged( invokeValidation: (String) -> Unit ){

this.addTextChangedListener( object: TextWatcher{

override fun afterTextChanged( content: Editable? ) {
invokeValidation( content.toString() )
}

override fun beforeTextChanged(
content: CharSequence?,
start: Int,
count: Int,
after: Int) {}

override fun onTextChanged(
content: CharSequence?,
start: Int,
before: Int,
count: Int) {}
} )
}

 

O callback invokeValidation passado como parâmetro tem a responsabilidade de invocar o código de validação e apresentação de mensagem de erro, caso necessário.

Note que afterTextChanged() é privado, não poderá ser invocado dentro da LoginActivity. A seguir criaremos a função estendida que poderá ser invocada na atividade de login para validação dos campos.

Ainda em extension_functions.kt adicione o método a seguir:

...
fun EditText.validate(
validator: (String) -> Boolean,
message: String ){

this.afterTextChanged {
this.error =
if( validator(it) )
null
else
message
}
}

 

Neste método devemos passar a função de validação e a mensagem de erro. Veja a invocação de afterTextChanged() recebendo como argumento um lambda com o algoritmo de validação e apresentação de mensagem de erro, como esperado em invokeValidation.

Agora, como teremos em outros pontos do projeto os campos de e-mail e senha, vamos também encapsular estas validações específicas.

Ainda em extension_functions.kt adicione:

...
fun String.isValidEmail() : Boolean
= this.isNotEmpty() &&
Patterns.EMAIL_ADDRESS.matcher( this ).matches()

fun String.isValidPassword() : Boolean
= this.length > 5

 

Por fim podemos ir direto ao onCreate() de LoginActivity e atualizar os códigos de validação de e-mail e senha em tempo de digitação:

...
override fun onCreate( ... ) {
...

/*
* Colocando configuração de validação de campo de email
* para enquanto o usuário informa o conteúdo deste campo.
* */
et_email.validate(
{
it.isValidEmail()
},
getString( R.string.invalid_email )
)

/*
* Colocando configuração de validação de campo de senha
* para enquanto o usuário informa o conteúdo deste campo.
* */
et_password.validate(
{
it.isValidPassword()
},
getString( R.string.invalid_password )
)

...
}
...

Ouvidores de cliques na LoginActivity

Ainda temos de adicionar os listeners de clique dos links da tela de login. Em LoginActivity, adicione:

...
fun callForgotPasswordActivity( view: View ){
Toast
.makeText(
this,
"TODO: callForgotPasswordActivity()",
Toast.LENGTH_SHORT
)
.show()
}

fun callSignUpActivity( view: View ){
Toast
.makeText(
this,
"TODO: callSignUpActivity()",
Toast.LENGTH_SHORT
)
.show()
}

fun callPrivacyPolicyFragment( view: View ){
/* TODO */
}
...

 

O listener callPrivacyPolicyFragment() permaneceu com TODO, pois já temos o fragmento de políticas de privacidade pronto. Sendo assim, no próximo tópico, estaremos colocando um algoritmo funcional neste método.

Ainda temos de adicionar estes listeners de cliques aos seus respectivos links, TextViews, em content_login.xml:

...
<TextView
android:id="@+id/tv_forgot_password"
...
android:onClick="callForgotPasswordActivity"/>

...

<TextView
android:id="@+id/tv_sign_up"
...
android:onClick="callSignUpActivity"/>
...

 

Agora a adição nos layouts do link de políticas de privacidade. Primeiro em /res/layout/text_view_privacy_policy_login.xml:

<?xml version="1.0" encoding="utf-8"?>
<TextView
...
android:onClick="callPrivacyPolicyFragment"/>

 

Por fim em /res/layout-land/text_view_privacy_policy_login.xml:

<?xml version="1.0" encoding="utf-8"?>
<TextView
...
android:onClick="callPrivacyPolicyFragment"/>

Algoritmos do link de políticas de privacidade

Para que seja possível abrir o fragmento de políticas de privacidade na MainActivity assim que o link de políticas da LoginActivity é acionado, nós vamos utilizar Intent e dados em Intent, mais precisamente, como dado em Intent, o ID do item de políticas de privacidade.

Vamos iniciar preparando os algoritmos na MainActivity.

Primeiro a criação de uma nova constante para não trabalharmos com valor mágico como chave de acesso ao ID de fragmento em Intent.

Na MainActivity adicione "frag-id":

...
companion object {
...
const val FRAGMENT_ID = "frag-id"
}
...

 

Agora a atualização do código de seleção de item em initNavMenu():

...
private fun initNavMenu( ... ){

...

if( ... ){
...
}
else{
/*
* Verificando se há algum item ID em intent. Caso não,
* utilize o ID do primeiro item.
* */
var fragId = intent?.getIntExtra( FRAGMENT_ID, 0 )
if(fragId == 0){
fragId = R.id.item_all_shoes
}

/*
* O primeiro item do menu gaveta deve estar selecionado
* caso não seja uma reinicialização de tela / atividade
* ou o envio de um ID especifico de fragmento a ser aberto.
* O primeiro item aqui é o de ID R.id.item_all_shoes.
* */
selectNavMenuItems.select( fragId!!.toLong() )
}
}
...

 

Ainda na MainActivity, vamos agora a atualização do método responsável pela abertura de fragmento de início, o método initFragment():

...
private fun initFragment(){
...

if( fragment == null ){

/*
* Caso haja algum ID de fragmento em intent, então
* é este fragmento que deve ser acionado. Caso
* contrário, abra o fragmento comum de início.
* */
var fragId = intent?.getIntExtra( FRAGMENT_ID, 0 )
if( fragId == 0 ){
fragId = R.id.item_about
}

fragment = getFragment( fragId!!.toLong() )
}

replaceFragment( fragment )
}
...

 

Por fim, somente temos de colocar as configurações corretas em callPrivacyPolicyFragment() na LoginActivity:

...
fun callPrivacyPolicyFragment( view: View ){
val intent = Intent(
this,
MainActivity::class.java
)

/*
* Para saber qual fragmento abrir quando a
* MainActivity voltar ao foreground.
* */
intent.putExtra(
MainActivity.FRAGMENT_ID,
R.id.item_privacy_policy
)

/*
* Removendo da pilha de atividades a primeira
* MainActivity aberta (e a LoginActivity), para
* deixar somente a nova MainActivity com uma nova
* configuração de fragmento aberto.
* */
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP

startActivity( intent )
}
...

 

Assim nosso link de políticas está 100% funcional. Em Testes e resultados passaremos por este trecho.

O problema com o android:parentActivityName

Você lembra de nossa configuração de atividade parent no AndroidManifest.xml? Está configuração:

...
<activity
android:name=".view.LoginActivity"
android:label="@string/title_activity_login"
android:parentActivityName=".view.MainActivity"
android:theme="@style/AppTheme.NoActionBar">

<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="thiengo.com.br.blueshoes.view.MainActivity"/>
</activity>
...

 

Então, quando executando o aplicativo e voltando, da LoginActivity para a MainActivity, foi percebido que o estado da MainActivity não se manteve, ela é invocada como se estivéssemos utilizando o startActivity():

Animação do problema do back button da barra de topo

Se voltarmos a MainActivity por meio do back button da barra de fundo, temos o comportamento esperado, estado da MainActivity mantido:

Animação do back button da barra de fundo

Com isso teremos de realizar algumas modificações, incluindo em AndroidManifest.xml, para conseguir o resultado esperado também em back button de topo.

A solução para a navegação à atividade anterior

Nosso primeiro passo é atualizar a configuração de LoginActivity no AndroidManifest.xml. Deixe como a seguir:

...
<activity
android:name=".view.LoginActivity"
android:label="@string/title_activity_login"
android:theme="@style/AppTheme.NoActionBar"/>
...

 

Assim, em FormActivity, adicione os códigos em destaque:

abstract class FormActivity : AppCompatActivity() {


override fun onCreate( ... ) {
...

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

...
}
...

/*
* 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 )
}
}

 

O trecho de código:

...
supportActionBar?.setDisplayHomeAsUpEnabled( true )
supportActionBar?.setDisplayShowHomeEnabled( true )
...

 

Entrou no lugar da única linha:

...
supportActionBar?.setDisplayHomeAsUpEnabled( true )
...

Os imports de LoginActivity

Caso você esteja tendo problemas com os imports na LoginActivity, eles ficaram como a seguir:

...
import android.content.Intent
import android.os.Bundle
import android.os.SystemClock
import android.support.constraint.ConstraintLayout
import android.support.constraint.ConstraintSet
import android.view.KeyEvent
import android.view.View
import android.widget.TextView
import android.widget.Toast
import com.blankj.utilcode.util.KeyboardUtils
import com.blankj.utilcode.util.ScreenUtils
import kotlinx.android.synthetic.main.content_form.*
import kotlinx.android.synthetic.main.content_login.*
import kotlinx.android.synthetic.main.text_view_privacy_policy_login.*
import thiengo.com.br.blueshoes.R
import thiengo.com.br.blueshoes.util.isValidEmail
import thiengo.com.br.blueshoes.util.isValidPassword
import thiengo.com.br.blueshoes.util.validate
...

Os imports de FormActivity

A seguir a configuração de imports em FormActivity:

...
import android.graphics.Color
import android.graphics.PorterDuff
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v4.content.ContextCompat
import android.support.v4.content.res.ResourcesCompat
import android.support.v7.app.AppCompatActivity
import android.text.SpannableString
import android.text.Spanned
import android.text.style.ImageSpan
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import kotlinx.android.synthetic.main.app_bar.*
import kotlinx.android.synthetic.main.proxy_screen.*
import thiengo.com.br.blueshoes.R
...

 

Assim podemos partir para os testes.

Testes e resultados

Abra o Android Studio, no menu de topo dele acesse "Build", então clique em "Rebuid project". Ao final do rebuild execute o aplicativo em seu aparelho ou emulador Android de testes.

Coloque em teste o usuário com o status "não conectado", objeto presente na MainActivity:

...
val user = User(
"Thiengo Vinícius",
R.drawable.user,
false
)
...

 

Acessando a área de login, temos:

Animação de navegação da tela de login refatorada

Acessando o link de políticas, temos:

Animação de acesso ao link de políticas de privacidade

Assim concluímos a primeira refatoração do projeto Android BlueShoes.

Antes de prosseguir, não deixe de se inscrever na 📩 lista de emails do Blog para receber todas as aulas do projeto Android de mobile-commerce.

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

Vídeos

A seguir os vídeos com o passo a passo da refatoração da tela de login.

O projeto também pode ser acessado pelo GitHub dele em: https://github.com/viniciusthiengo/blueshoes-kotlin-android.

Conclusão

Refatorar a atividade de login é algo necessário, tendo em mente que todos os outros formulários do projeto se beneficiarão dos códigos encapsulados a partir dessa atividade.

Com isso estamos evitando repetição de código e consequentemente uma evolução de projeto mais demorada, com mais de um ponto de atualização para o mesmo algoritmo.

Caso você tenha dúvidas ou dicas para este projeto, deixe logo abaixo nos comentários.

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

Abraço.

Fontes 

Easy EditText content validation with Kotlin

Kotlin Abstract Class

Android programmatically include layout (i.e. without XML) - Resposta de Yoni Samlan e de Peter Haddad

Android: Remove all the previous activities from the back stack - Resposta de kgiannakakis e de amitabha2715

What is the `it` in Kotlin lambda body? - Resposta de JAMES HWANG

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

Android Mobile-Commerce, Apresentação e Protótipo do ProjetoAndroid Mobile-Commerce, Apresentação e Protótipo do ProjetoAndroid
Início de Projeto e Menu Gaveta Customizado - Android M-CommerceInício de Projeto e Menu Gaveta Customizado - Android M-CommerceAndroid
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
Login com ConstraintLayout e TextWatcher Para Validação - Android M-CommerceLogin com ConstraintLayout e TextWatcher Para Validação - Android M-CommerceAndroid

Compartilhar

Comentários Facebook

Comentários Blog (3)

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...
Robson (1) (0)
24/05/2019
Olá Thiengo boa noite, cara estou nesta maratona das aulas do Android-M-Commerce passei um tempo sem poder acessar e agora estou acompanhando, como sempre nível lá em cima.
Por conta da linguagem utilizada eu estou tendo alguns problemas e se puderes me ajudar na atividade ancestral o código abaixo que esta na FormActivity em Kotlin você inseriu e fechou com um import em Java não tem import para isto

View.inflate(this, getLayoutResourceID(), fl_form);

aqui da erro porque não encontra  fl_form em java eu faria com um findViewById?

Obrigado.
Responder
Vinícius Thiengo (0) (0)
31/05/2019
Robson, tudo bem?

Exatamente, você faria como abaixo:

View.inflate( this, getLayoutResourceID(), findViewById( R.id.fl_form ));

Abraço.
Responder
Robson (1) (0)
31/05/2019
Opa Thiengo cara eu "resolvi" desta forma

no onCreate do FormActivity recuperei a view do frameLayout
  FrameLayout frameLayout = findViewById(R.id.fl_form);

View.inflate(
                this,
                getLayoutResourceID(),
                frameLayout)
Responder