Refatoração do Login, Pavimentando o Caminho Para Outros Formulários - Android M-Commerce
(3048) (3)
CategoriasAndroid, Design, Protótipo
AutorVinÃcius Thiengo
VÃdeo aulas186
Tempo15 horas
ExercÃciosSim
CertificadoSim
CategoriaDesenvolvimento Web
Autor(es)Robert C. Martin
EditoraAlta Books
Edição1ª
Ano2023
Páginas416
Tudo bem?
Neste artigo, 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.
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;
- Por que refatorar somente a tela de login?:
- Refatorando:
- Estratégia utilizada;
- Encapsulando o código estático de barra de topo;
- Criando o layout único de tela proxy;
- Estilo para os campos de formulário;
- Estilo para os botões de formulário;
- Atividade ancestral de formulários;
- Layout principal de formulários;
- Layout do formulário de login;
- Atualizações de herança em LoginActivity;
- Remoção de closeVirtualKeyBoard();
- Funções estendidas para validações de campos;
- Ouvidores de cliques na LoginActivity;
- Algoritmos do link de políticas de privacidade;
- O problema com o android:parentActivityName;
- A solução para a navegação à atividade anterior;
- Os imports de LoginActivity;
- Os imports de FormActivity.
- Testes e resultados;
- Vídeos;
- Conclusão;
- Fontes.
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:
- 1ª aula - Android Mobile-Commerce, Apresentação e Protótipo do Projeto;
- 2ª aula - Início de Projeto e Menu Gaveta Customizado - Android M-Commerce;
- 3ª aula - Fragmento da Tela Sobre e Links Sociais - Android M-Commerce;
- 4ª aula - Localização com Rota GPS, E-mail e Telefones - Android M-Commerce;
- 5ª aula - Políticas de Privacidade e Porque não a GDPR - Android M-Commerce;
- 6ª aula - Login com ConstraintLayout e TextWatcher Para Validação - Android M-Commerce.
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.
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:
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:
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:
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:
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:
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.
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:
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:
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:
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.
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():
Se voltarmos a MainActivity por meio do back button da barra de fundo, temos o comportamento esperado, estado da MainActivity mantido:
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:
Acessando o link de políticas, temos:
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
What is the `it` in Kotlin lambda body? - Resposta de JAMES HWANG
Comentários Facebook