Como Desenvolver as Telas de Configuração de E-mail e Senha - Android M-Commerce
(3390) (1)
CategoriasAndroid, Design, Protótipo
AutorVinÃcius Thiengo
VÃdeo aulas186
Tempo15 horas
ExercÃciosSim
CertificadoSim
CategoriaEngenharia de Software
Autor(es)Vaughn Vernon
EditoraAlta Books
Edição1ª
Ano2024
Páginas160
Tudo bem?
Neste artigo vamos iniciar uma nova fase na parte de interface gráfica do projeto Android BlueShoes, nosso mobile-commerce. Aqui começaremos os trabalhos com formulários também em fragmentos:
Até o momento todos os formulários estão somente em atividades. Porém chegamos em uma parte do projeto onde formulários de dados de mesmo contexto, que precisam estar na mesma área do aplicativo, têm de estar separados por simples seções (tabs).
É, eu sei, é difícil de entender assim, somente com palavras, mas quando na (re-)apresentação do protótipo estático você vai entender melhor.
Antes de continuar, não esqueça de se inscrever 📫na lista de emails do Blog para receber as atualizações do projeto e de outros conteúdos Android exclusivos aqui do Blog.
A seguir os tópicos abordados:
- Iniciando no Android mobile-commerce;
- Estratégia para as telas de dados de conexão:
- Fragmento ancestral de formulários:
- Caixa de diálogo para entrada de senha:
- Formulário de e-mail:
- Formulário de senha:
- Atividade de dados de conexão:
- Testes e resultados;
- Vídeos;
- Conclusão;
- Fontes.
Iniciando no Android mobile-commerce
Hum, sei. Você chegou a este projeto Android somente agora e quer acompanha-lo desde o início, certo?
Sendo assim, não deixe de primeiro consumir as 14 aulas, com vídeos, já disponíveis e anteriores a esta 15ª aula:
- 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;
- 7ª aula - Refatoração do Login, Pavimentando o Caminho Para Outros Formulários - Android M-Commerce;
- 8ª aula - Como Criar a Tela de Recuperação de Acesso - Android M-Commerce;
- 9ª aula - Criando a Tela de Cadastro de Usuário - Android M-Commerce;
- 10ª aula - Aplicando o Padrão Template Method Para Limpar o Login e o Cadastro - Android M-Commerce;
- 11ª aula - Como Criar a UI de Configurações de Conta de Usuário - Android M-Commerce;
- 12ª aula - Entendendo o Bug do Menu, Link de Cadastro e Parcelize - Android M-Commerce;
- 13ª aula - Construindo o Formulário de Atualização de Perfil - Android M-Commerce;
- 14ª aula - Migrando Para o AndroidX e Construindo a Galeria Para Foto de Perfil - Android M-Commerce.
Lembrando que as aulas são liberadas semanalmente para a lista de e-mails 📩 do Blog, logo, não esqueça de se inscrever nela, é gratuito. E... surgindo dúvidas, pode perguntar pelo e-mail ou nos comentários do artigo.
Estratégia para as telas de dados de conexão
Nesta parte do projeto, mesmo já sabendo que teremos inúmeros códigos repetidos (isso será temporário e discutiremos mais sobre), a quantidade de entidades em desenvolvimento será maior do que o normalmente desenvolvido.
Teremos que construir ao menos quatro classes diretamente vinculadas a camada de visualização:
- Fragmento do formulário de e-mail;
- Fragmento do formulário de senha;
- Fragmento que conterá os códigos comuns dos fragmentos de e-mail e de senha, para evitar códigos repetidos;
- Atividade host dos fragmentos de e-mail e de senha.
Também trabalharemos os dados estáticos destas classes, tudo de maneira separada.
O roteiro será o seguinte:
- Primeiro, das classes a serem desenvolvidas, vamos optar pela mais independente;
- Assim vamos à definição das partes estáticas da classe escolhida (strings.xml e layout);
- Por fim vamos ao desenvolvimento das partes de código dinâmico da classe em construção;
- Repetiremos o ciclo até a última entidade ainda vinculada a área de atualização de dados de conexão.
Ressaltando que o projeto Android BlueShoes está disponível no repositório dele em: https://github.com/viniciusthiengo/blueshoes-kotlin-android.
Protótipo estático
A seguir o protótipo estático das telas de atualização de dados de conexão, e-mail e senha:
Atualização e-mail | Segurança de atualização |
Load - atualização em back-end | Erro na atualização |
Atualização bem sucedida | Atualização de senha |
Segurança de atualização de senha | Load - atualização em back-end |
Erro na atualização | Atualização bem sucedida |
Fragmento ancestral de formulários
Lembra de nossa FormActivity que foi desenvolvida para conter os inúmeros códigos que certamente seriam repetidos em atividades de formulários caso ela não existisse?
Então, também precisamos de uma classe assim para os fragmentos de formulários que estaremos desenvolvendo a partir desta 15ª aula.
Uma nova fase no projeto (duplicação)
Thiengo, então vamos modificar ainda mais os códigos da FormActivity para que eles se enquadrem em contextos de formulários em atividades e de formulários em fragmentos?
Sim e não. Na verdade nós vamos primeiro resolver a principal tarefa: terminar esta parte de projeto com a tela de configuração de dados de conexão, e-mail e senha, totalmente funcional.
Ou seja, teremos inúmeros códigos repetidos da FormActivity em nosso fragmento ancestral de fragmentos com formulários.
Posteriormente, em outras aulas, vamos refatorar todas as classes ancestrais de formulários para que os códigos duplicados sejam removidos.
É importante lembrar que: é inteligente refatorar o código depois que ele já está funcional. Perde-se muito tempo quando se tenta desenvolver algo já com padrões e códigos não duplicados, pense na refatoração como a parte final (que se repete) do desenvolvimento.
Definindo o layout container
Como acontece com a FormActivity, aqui também teremos um layout container dos layouts de formulário, assim evitamos ainda mais duplicação de código.
Em /res/layout adicione o layout fragment_form.xml com o código 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">
<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>
Abaixo o simples diagrama do layout anterior:
Criando a FormFragment
Com isso podemos partir para a codificação. No pacote /view (ou em seu pacote onde estão as classes da camada de visualização) crie um novo fragmento com o rótulo FormFragment e com o código como a seguir:
abstract class FormFragment :
Fragment(),
TextView.OnEditorActionListener {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle? ): View? {
val viewContainer = inflater
.inflate(
R.layout.fragment_form,
container,
false
) as ViewGroup
/*
* Colocando a View de um arquivo XML como View filha
* do item indicado no terceiro argumento.
* */
View.inflate(
activity,
getLayoutResourceID(),
viewContainer.findViewById( R.id.fl_form )
)
return viewContainer
}
abstract fun getLayoutResourceID() : Int
/*
* Caso o usuário toque no botão "Done" do teclado virtual
* ao invés de tocar no botão "Entrar". Mesmo assim temos
* de processar o formulário.
* */
override fun onEditorAction(
v: TextView?,
actionId: Int,
event: KeyEvent? ): Boolean {
mainAction()
return false
}
/*
* 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(
activity!!,
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(
com.google.android.material.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()
}
/*
* Método template.
* Responsável por conter o algoritmo de envio / validação
* de dados. Algoritmo vinculado ao menos ao principal
* botão em tela.
* */
fun mainAction( view: View? = null ){
blockFields( true )
isMainButtonSending( true )
showProxy( true )
backEndFakeDelay()
}
/*
* Método único.
* */
abstract fun backEndFakeDelay() : Unit
/*
* 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 )
/*
* Fake method - Somente para testes temporários em atividades
* e fragmentos que contêm formulários.
* */
protected fun backEndFakeDelay(
statusAction: Boolean,
feedbackMessage: String
){
Thread{
run {
/*
* Simulando um delay de latência de
* 1 segundo.
* */
SystemClock.sleep( 1000 )
activity!!.runOnUiThread {
blockFields( false )
isMainButtonSending( false )
showProxy( false )
val containerForm = fl_proxy_container.parent as ViewGroup
snackBarFeedback(
containerForm,
statusAction,
feedbackMessage
)
}
}
}.start()
}
}
Ops! É quase o mesmo código de nossa FormActivity. O que foi possível ser aproveitado foi colocado na FormFragment. Em alguns pontos onde tínhamos this passamos a utilizar activity!!, pois o contexto da atividade ainda é necessário.
Note também que dentro de Thread.run() em backEndFakeDelay() nós não mais estamos utilizando o fl_form_container como primeiro argumento de snackBarFeedback(). Ainda há um ViewGroup sendo acessado, porém sem a dependência de um ID em arquivo estático, o acesso agora é via método e cast: val containerForm = fl_proxy_container.parent as ViewGroup.
Isso é sem sombra de dúvidas uma melhoria que estaremos replicando, em aulas futuras, também na FormActivity. Nunca em desenvolvimento de software a dependência de algoritmos externos é algo positivo, mesmo sabendo que muitas vezes é o caminho mais curto para a solução de problemas.
Assim podemos partir para o desenvolvimento de uma parte essencial em formulários críticos, formulários como os de atualização de e-mail e de senha. A caixa de diálogo de solicitação de senha. Essa parte também vai estar na FormFragment.
Caixa de diálogo para entrada de senha
Alguns formulários do aplicativo Android BlueShoes solicitarão a senha do usuário para que ele possa prosseguir com a ação desejada.
Isso é comum em todos os softwares que têm ao menos área de acesso restrito, que exige dados para login.
Nossa caixa de diálogo é bem simples e já será necessária para os formulários de atualização de e-mail e de senha:
Atualizando o strings.xml
Vamos iniciar pela atualização mais tranquila, com os dados que podemos obter direto do protótipo estático do projeto.
No arquivo /res/values/strings.xml adicione os trechos em destaque:
<resources>
...
<!-- Dialog Password -->
<string name="dialog_password_info">
Entre com a senha para poder prosseguir:
</string>
<string name="dialog_password_hint">Senha</string>
<string name="dialog_password_go">PROSSEGUIR</string>
<string name="dialog_password_cancel">CANCELAR</string>
</resources>
Definindo o layout
O layout da caixa de diálogo é bem simples, principalmente porque ele vem dentro do layout nativo de dialog do Android, digo, layout nativo da classe de dialog que estaremos utilizando, assim não temos que trabalhar os XMLs, por exemplo, dos botões.
Em /res/layout crie o XML dialog_password.xml como a seguir:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="28dp"
android:paddingBottom="10dp"
android:paddingStart="24dp"
android:paddingEnd="24dp">
<TextView
android:id="@+id/tv_password_inform"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textColor="@color/colorText"
android:text="@string/dialog_password_info"/>
<EditText
android:id="@+id/et_password"
style="@style/EditTextFormField"
android:layout_marginTop="16dp"
android:layout_width="match_parent"
android:background="@drawable/bg_form_field"
android:inputType="textPassword"
android:imeOptions="actionDone"
android:hint="@string/dialog_password_hint"/>
</LinearLayout>
Note que no EditText colocamos android:layout_width="match_parent", pois a definição padrão no nosso estilo EditTextFormField é de 300dp, algo que atrapalharia o design pretendido para o campo de senha na caixa de diálogo.
Abaixo o simples diagrama do layout anterior:
Código dinâmico em FormFragment
Até que algum outro código em projeto, fora do contexto de formulários em fragmentos, prove o contrário, o melhor local para o código de abertura de caixa de diálogo de senha é a FormFragment.
Dentro deste fragmento, como último método da classe, adicione o callPasswordDialog() com o código a seguir:
...
/*
* Método responsável por invocar o Dialog de password antes
* que o envio do formulário ocorra. Dialog necessário em
* alguns formulários críticos onde parte da validação é a
* verificação da senha.
* */
protected fun callPasswordDialog(){
val builder = AlertDialog.Builder( activity!! )
val inflater = activity!!.layoutInflater
/*
* Inflando o layout e configurando o AlertDialog. O
* valor null está sendo colocado como segundo argumento
* de inflate(), pois o layout parent do layout que
* está sendo inflado será o layout nativo do dialog.
* */
builder
.setView( inflater.inflate(R.layout.dialog_password, null) )
.setPositiveButton(
R.string.dialog_password_go,
{ dialog, id -> mainAction() }
)
.setNegativeButton(
R.string.dialog_password_cancel,
{ dialog, id -> dialog.cancel() }
)
.setCancelable( false )
val dialog = builder.create()
dialog.setOnShowListener(
object : DialogInterface.OnShowListener{
override fun onShow( d: DialogInterface? ) {
/*
* É preciso colocar qualquer configuração
* extra das Views do Dialog dentro do
* listener de "dialog em apresentação",
* caso contrário uma NullPointerException
* será gerada, tendo em mente que é somente
* quando o "dialog está em apresentação"
* que as Views dele existem como objetos.
* */
dialog
.getButton( AlertDialog.BUTTON_POSITIVE )
.setTextColor( ColorUtils.getColor(R.color.colorText) )
dialog
.getButton( AlertDialog.BUTTON_NEGATIVE )
.setTextColor( ColorUtils.getColor(R.color.colorText) )
val etPassword = dialog.findViewById<EditText>(R.id.et_password)!!
etPassword.validate(
{ it.isValidPassword() },
getString( R.string.invalid_password )
)
etPassword.setOnEditorActionListener{
view, actionId, event ->
dialog.cancel()
mainAction()
false
}
}
}
)
dialog.show()
}
...
Certamente uma de suas dúvidas é: por que a necessidade de atualizar a cor dos botões?
Bom, primeiro: lendo o comentário em onShow() você já deve estar ciente de o porquê de algumas atualizações estarem ocorrendo dentro deste método.
Agora o porquê da atualização das cores dos botões é simples: por padrão o AlertDialog utiliza como cor default dos botões a cor definida em colorAccent. Porém a nossa colorAccent é muito clara, respeitando o que foi definido em protótipo estático, com pouco contraste em fundo branco. Sendo assim foi necessária a atualização da cor, ainda respeitando o que foi definido em protótipo estático para essa caixa de diálogo.
Uma outra informação: foi escolhido o uso de um AlertDialog ante ao DialogFragment, pois o único dado que queremos obter em caixa de diálogo tem a validação dele ocorrendo mesmo em back-end Web. Ou seja, a entidade mais simples que provê um dialog é neste caso uma melhor opção, o AlertDialog.
Thiengo, você sempre fala que não gosta de utilizar lambda, mas como segundo argumento dos métodos setPositiveButton() e setNegativeButton() você utilizou essa sintaxe. Por que isso agora?
Você está certo, eu não curto o uso da sintaxe lambda, pois para mim ela prejudica a leitura do código. Mas neste ponto do projeto a minha outra opção era toda a configuração dos listeners com a Interface DialogInterface.OnClickListener.
Tendo em mente que parte de meu objetivo com a caixa de diálogo de senha era ter todo o código em um único local, método, eu optei por utilizar a sintaxe lambda para assim diminuir todo o código do método final. Isso levando em consideração que não haveria considerável perda na leitura do código quando não utilizando a versão com DialogInterface.OnClickListener.
Ok, Thiengo. Mas me explique melhor o mainAction() sendo invocado em setPositiveButton() e em etPassword.setOnEditorActionListener(), outros pontos que você também optou pelo lambda.
Primeiro, é importante que você saiba que o acesso ao EditText de ID et_password não é possível, dentro de onShow(), utilizando a sintaxe do kotlin-android-extensions.
Por que? Porque é uma limitação do plugin para layouts que não estão em tela quando a atividade ou o fragmento já foram instanciados.
Segundo, o uso do lambda também em códigos dentro de onShow(), e em alguns fora dele, foi feito com base no objetivo já explicado anteriormente: de diminuir todo o algoritmo do método callPasswordDialog().
E por fim a resposta à sua dúvida: o mainAction() está sendo invocado nestas partes do algoritmo, pois em formulários que exigem o dialog de senha, o envio de dados ao back-end Web somente ocorrerá depois do fornecimento da senha do usuário conectado.
Ou seja, o método onEditorAction(), filho direto de FormFragment, passará a invocar callPasswordDialog() ao invés de mainAction(). Então vamos a esta atualização:
abstract class FormFragment : ... {
...
override fun onEditorAction(
... ): Boolean {
callPasswordDialog()
return false
}
...
}
Assim podemos partir para a construção dos formulários de atualização de e-mail e de senha.
Formulário de e-mail
Vamos iniciar pelo primeiro formulário apresentado assim que o usuário acessa a área de atualização de dados de conexão:
Nesta seção ainda não trabalharemos com as tabs de mudança de formulário em tela, mas ao menos o título da tab de e-mail será já configurado aqui.
Atualizando o arquivo de Strings
No arquivo /res/values/strings.xml adicione os trechos em destaque:
<resources>
...
<!-- ConfigEmailFragment -->
<string name="config_connection_data_tab_email">E-MAIL</string>
<string name="hint_current_email">E-mail atual</string>
<string name="hint_new_email">Novo e-mail</string>
<string name="hint_new_email_confirm">Confirmar novo e-mail</string>
<string name="invalid_confirmed_email">
Confirme o e-mail informado no campo acima.
</string>
<string name="update_email_login">Atualizar e-mail de login</string>
<string name="update_email_login_going">Atualizando…</string>
</resources>
Definindo o layout
O arquivo de layout é simples. Em /res/layout adicione o arquivo XML fragment_config_email.xml com o seguinte código:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="@+id/ll_container_fields"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<EditText
android:id="@+id/et_current_email"
style="@style/EditTextFormField"
android:background="@drawable/bg_form_field_top"
android:inputType="textEmailAddress"
android:imeOptions="actionNext"
android:hint="@string/hint_current_email"/>
<EditText
android:id="@+id/et_new_email"
style="@style/EditTextFormField"
android:layout_marginTop="-1dp"
android:background="@drawable/bg_form_field_sqr"
android:inputType="textEmailAddress"
android:imeOptions="actionNext"
android:hint="@string/hint_new_email"/>
<EditText
android:id="@+id/et_new_email_confirm"
style="@style/EditTextFormField"
android:layout_marginTop="-1dp"
android:background="@drawable/bg_form_field_bottom"
android:inputType="textEmailAddress"
android:imeOptions="actionDone"
android:hint="@string/hint_new_email_confirm"/>
</LinearLayout>
<Button
android:id="@+id/bt_update_email_login"
style="@style/ButtonForm"
android:layout_marginBottom="3dp"
android:layout_marginEnd="1dp"
android:layout_marginRight="1dp"
android:layout_gravity="end"
android:text="@string/update_email_login"/>
</LinearLayout>
As margens estratégicas em <Button>, android:layout_margin, são para que as sombras do botão continuem aparecendo:
A seguir o diagrama do layout anterior:
A estrutura de layout utilizada desta vez, com mais de um LinearLayout, teve um melhor resultado em termos de: centralização de formulário.
Definindo uma nova estrutura de pacote
Nosso pacote das classes de camada de visualização já está começando a ficar "embolado". Sendo assim vamos iniciar a criação de pacotes aninhados.
Dentro do pacote /view crie um novo pacote com o rótulo config.connectiondata. Assim teremos:
Ainda criaremos, em outras aulas, outros pacotes aninhados para não deixarmos a estrutura física do projeto como um estrutura bagunçada.
Por hora vamos seguir somente com este subpacote, onde as classes ligadas somente ao contexto "configuração de dados de conexão" serão armazenadas.
Ou seja, o fragmento FormFragment não entra neste subpacote, ele continua sendo um filho direto de /view. Pois apesar de estarmos utilizando o FormFragment somente agora, ele será útil em outros contextos com formulários em fragmentos.
Construindo a ConfigEmailFragment
Por fim o nosso último passo na construção do formulário de atualização de e-mail, a classe ConfigEmailFragment. Adicione esta classe ao pacote criado na seção anterior, /config.connectiondata:
class ConfigEmailFragment :
FormFragment() {
companion object{
const val TAB_TITLE = R.string.config_connection_data_tab_email
}
override fun getLayoutResourceID()
= R.layout.fragment_config_email
override fun onActivityCreated( savedInstanceState: Bundle? ) {
super.onActivityCreated( savedInstanceState )
bt_update_email_login.setOnClickListener{
callPasswordDialog()
}
et_current_email.validate(
{
it.isValidEmail()
},
getString( R.string.invalid_email )
)
et_new_email.validate(
{
it.isValidEmail()
},
getString( R.string.invalid_email )
)
et_new_email_confirm.validate(
{
/*
* O toString() em et_new_email.text.toString() é
* necessário, caso contrário a validação falha
* mesmo quando é para ser ok.
* */
(et_new_email.text.isNotEmpty()
&& it.equals( et_new_email.text.toString() ))
|| et_new_email.text.isEmpty()
},
getString( R.string.invalid_confirmed_email )
)
et_new_email_confirm.setOnEditorActionListener( this )
}
override fun backEndFakeDelay(){
backEndFakeDelay(
false,
getString( R.string.invalid_sign_up_email )
)
}
override fun blockFields( status: Boolean ){
et_current_email.isEnabled = !status
et_new_email.isEnabled = !status
et_new_email_confirm.isEnabled = !status
bt_update_email_login.isEnabled = !status
}
override fun isMainButtonSending( status: Boolean ){
bt_update_email_login.text =
if( status )
getString( R.string.update_email_login_going )
else
getString( R.string.update_email_login )
}
}
Note como o código da classe é simples devido ao isolamento de códigos já aplicado à classe FormFragment.
Estamos trabalhando com o trecho bt_update_email_login.setOnClickListener{}, pois a adição de listener de clique direto no <Button> por meio de android:onClick exigiria o trabalho com Interface para podermos capturar o clique na atividade container dos fragmentos, algo que exigiria ainda mais linhas de código - desnecessário aqui.
Formulário de senha
O trabalho com o formulário de senha será tão simples quanto foi com o formulário de e-mail. Esse tem ainda menos campos:
Atualização do strings.xml
Em /res/values/strings.xml adicione os trechos em destaque:
<resources>
...
<!-- ConfigPasswordFragment -->
<string name="config_connection_data_tab_password">SENHA</string>
<string name="hint_new_password">Nova senha</string>
<string name="hint_new_password_confirm">Confirmar nova senha</string>
<string name="update_password">Atualizar senha</string>
<string name="update_password_going">Atualizando…</string>
</resources>
Definição do layout
O layout é muito similar ao layout de formulário de e-mail. Em /res/layout adicione o XML fragment_config_password.xml com o código estático a seguir:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="@+id/ll_container_fields"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<EditText
android:id="@+id/et_new_password"
style="@style/EditTextFormField"
android:background="@drawable/bg_form_field_top"
android:inputType="textPassword"
android:imeOptions="actionNext"
android:hint="@string/hint_new_password"/>
<EditText
android:id="@+id/et_new_password_confirm"
style="@style/EditTextFormField"
android:layout_marginTop="-1dp"
android:background="@drawable/bg_form_field_bottom"
android:inputType="textPassword"
android:imeOptions="actionDone"
android:hint="@string/hint_new_password_confirm"/>
</LinearLayout>
<Button
android:id="@+id/bt_update_password"
style="@style/ButtonForm"
android:layout_marginBottom="3dp"
android:layout_marginEnd="1dp"
android:layout_marginRight="1dp"
android:layout_gravity="end"
android:text="@string/update_password"/>
</LinearLayout>
Abaixo o diagrama do layout anterior:
Desenvolvendo a ConfigPasswordFragment
Agora, no novo pacote /config.connectiondata, adicione a classe ConfigPasswordFragment com o código a seguir:
class ConfigPasswordFragment :
FormFragment() {
companion object{
const val TAB_TITLE = R.string.config_connection_data_tab_password
}
override fun getLayoutResourceID()
= R.layout.fragment_config_password
override fun onActivityCreated( savedInstanceState: Bundle? ) {
super.onActivityCreated( savedInstanceState )
bt_update_password.setOnClickListener{
callPasswordDialog()
}
et_new_password.validate(
{ it.isValidPassword() },
getString( R.string.invalid_password )
)
et_new_password_confirm.validate(
{
/*
* O toString() em et_new_password.text.toString() é
* necessário, caso contrário a validação falha
* mesmo quando é para ser ok.
* */
(et_new_password.text.isNotEmpty()
&& it.equals( et_new_password.text.toString() ))
|| et_new_password.text.isEmpty()
},
getString( R.string.invalid_confirmed_password )
)
et_new_password_confirm.setOnEditorActionListener( this )
}
override fun backEndFakeDelay(){
backEndFakeDelay(
false,
getString( R.string.invalid_password )
)
}
override fun blockFields( status: Boolean ){
et_new_password.isEnabled = !status
et_new_password_confirm.isEnabled = !status
bt_update_password.isEnabled = !status
}
override fun isMainButtonSending( status: Boolean ){
bt_update_password.text =
if( status )
getString( R.string.update_password_going )
else
getString( R.string.update_password )
}
}
Note que tanto no formulário de senha quanto no formulário de e-mail é o método callPasswordDialog() que é acionado pelos botões e não o método mainAction(). Lembrando que essa ação é necessária, pois estes são formulários com dados críticos que podem ser atualizados somente pelo proprietário da conta conectada ao app.
Assim podemos partir para a atividade e entidades container dos formulários já criados.
Atividade de dados de conexão
Enfim chegamos a atividade container dos formulários de dados de conexão. Infelizmente não é possível ainda criar essa atividade como uma subclasse de FormActivity, a estrutura aqui, devido ao uso de tabs, muda consideravelmente.
Mas, acredite, o código será bem simples, mesmo o código do adapter de fragmentos em ViewPager.
Novos valores em strings.xml
Vamos primeiro a uma nova atualização no arquivo /res/values/strings.xml. Adicione nele o trecho em destaque:
<resources>
...
<!-- ConfigConnectionDataActivity -->
<string name="title_activity_config_connection_data">
Conexão (login)
</string>
</resources>
Definindo o layout principal (com tabs)
Agora o layout da atividade, aquele que contém também as tabs e o ViewPager container de fragmentos.
Em /res/layout adicione o XML activity_config_connection_data.xml com o código estático abaixo:
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.config.connectiondata.ConfigConnectionDataActivity">
<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>
A seguir o diagrama do layout anterior:
Que fadiga! Por causa do <TabLayout> não foi possível aproveitar o layout /res/layout/app_bar.xml como barra de topo em activity_config_connection_data.xml.
Construindo a FragmentPagerAdapter
Assim podemos partir para a classe adaptadora de fragmentos que ficará vinculada ao ViewPager do layout principal.
Vamos utilizar uma subclasse de FragmentPagerAdapter, pois com ela os fragmentos ficam retidos em memória sem o trabalho desnecessário de "reconstrução de fragmento".
Em nosso novo subpacote, /config.connectiondata, crie a classe ConfigConnectionDataSectionsAdapter 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 ConfigConnectionDataSectionsAdapter(
val context: Context,
fm: FragmentManager ) : FragmentPagerAdapter( fm ) {
companion object{
const val TOTAL_PAGES = 2
const val EMAIL_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 ){
EMAIL_PAGE_POS -> ConfigEmailFragment()
else -> ConfigPasswordFragment()
}
override fun getPageTitle( position: Int )
= context.getString(
when( position ){
EMAIL_PAGE_POS -> ConfigEmailFragment.TAB_TITLE
else -> ConfigPasswordFragment.TAB_TITLE
}
)
override fun getCount()
= TOTAL_PAGES
}
Note que temos o EMAIL_PAGE_POS (para evitar o trabalho com valor mágico) e não o PASSWORD_PAGE_POS, pois para o fragmento de atualização de senha vamos utilizar somente a opção else, obrigatória aqui em nossos blocos when().
Desenvolvendo a ConfigConnectionDataActivity
Ainda em nosso novo pacote, /config.connectiondata, adicione a atividade container dos formulários de e-mail e de senha, a atividade ConfigConnectionDataActivity:
class ConfigConnectionDataActivity : AppCompatActivity() {
override fun onCreate( savedInstanceState: Bundle? ) {
super.onCreate( savedInstanceState )
setContentView( R.layout.activity_config_connection_data )
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 =
ConfigConnectionDataSectionsAdapter(
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 )
}
}
Somente código boilerplate, nada de lógica de negócio.
Atualizando o AndroidManifest
Ainda é preciso a atualização do AndroidManifest.xml para que a nossa nova atividade possa ser invocada. Adicione neste arquivo o trecho em destaque:
<?xml version="1.0" encoding="utf-8"?>
<manifest
...>
...
<application
...>
...
<activity
android:name=".view.config.connectiondata.ConfigConnectionDataActivity"
android:label="@string/title_activity_config_connection_data"
android:theme="@style/AppTheme.NoActionBar" />
</application>
</manifest>
Adaptando a AccountSettingItem
A nova atividade ConfigConnectionDataActivity será colocada em um objeto do tipo AccountSettingItem, isso, pois essa atividade é responsável pelas telas de uma das opções de configuração de conta do usuário.
Porém a ConfigConnectionDataActivity não herda de FormActivity, como é esperado em AccountSettingItem:
class AccountSettingItem(
val label: String,
val description: String,
val activityClass: Class<out FormActivity> /* AQUI */
)
Sendo assim vamos atualizar a AccountSettingItem para permitir qualquer subclasse de AppCompatActivity ao invés de FormActivity:
class AccountSettingItem(
val label: String,
val description: String,
val activityClass: Class<out AppCompatActivity>
)
Atualizando a AccountSettingsItemsDataBase
Nosso último passo é atualizar a "base de dados" de itens de configuração de conta. Em AccountSettingsItemsDataBase coloque ConfigConnectionDataActivity, segundo item, como a seguir:
class AccountSettingsItemsDataBase {
companion object{
fun getItems( context: Context )
= listOf(
AccountSettingItem(
...
),
AccountSettingItem(
context.getString( R.string.setting_item_login ),
context.getString( R.string.setting_item_login_desc ),
ConfigConnectionDataActivity::class.java
),
...
)
}
}
Com isso podemos partir para os testes.
Testes e resultados
Com o seu Android Studio aberto, acesse o menu de topo e logo depois clique em "Build", então acione "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 atualização de e-mail e prosseguindo com o envio, temos:
Agora acessando a tela de atualização de senha e prosseguindo com o envio, temos:
Assim finalizamos mais um importante trecho da interface gráfica de nosso projeto. Mesmo tendo que manter alguns códigos repetidos.
Antes de continuar, não esqueça de se inscrever na 📫 lista de emails do Blog para receber semanalmente as aulas do projeto Android BlueShoes.
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 configuração de dados de conexão:
O código do projeto pode ser acessado pelo GitHub dele em: https://github.com/viniciusthiengo/blueshoes-kotlin-android.
Conclusão
Com o início dos trabalhos com formulários em fragmentos nós começamos uma "nova era" em nosso aplicativo Android de mobile-commerce. E como em todo início: algumas coisas ainda não estão "redondas".
Apesar dos códigos duplicados, conseguimos manter os rótulos de maneira autocomentada e todas as telas dentro do contexto de "configuração de dados de conexão" estão com a funcionalidade e o design como definidos em pré-projeto.
Em aulas posteriores certamente trabalharemos também a redução de códigos duplicados.
Caso você tenha dúvidas sobre o projeto, principalmente sobre está 15ª 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 emails, respondo às suas dúvidas também por lá.
Abraço.
Fontes
Caixas de diálogo - Documentação oficial Android
FragmentPagerAdapter - Documentação oficial Android
TextView.OnEditorActionListener.onEditorAction() - Documentação oficial Android
Comentários Facebook