Como Desenvolver as Telas de Configuração de E-mail e Senha - Android M-Commerce

Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog. Você receberá um email de confirmação. Somente depois de confirma-lo é que poderei lhe enviar os conteúdos exclusivos.

Email inválido.
Blog /Android /Como Desenvolver as Telas de Configuração de E-mail e Senha - Android M-Commerce

Como Desenvolver as Telas de Configuração de E-mail e Senha - Android M-Commerce

Vinícius Thiengo
(837) (1)
Go-ahead
"Quanto tempo você deve tentar? Até chegar lá."
Jim Rohn
Kotlin Android
Capa do livro Desenvolvedor Kotlin Android - Bibliotecas para o dia a dia
TítuloDesenvolvedor Kotlin Android - Bibliotecas para o dia a dia
CategoriasAndroid, Kotlin
AutorVinícius Thiengo
Edição
Capítulos19
Páginas1035
Acessar Livro
Treinamento Oficial
Android: Prototipagem Profissional de Aplicativos
CursoAndroid: Prototipagem Profissional de Aplicativos
CategoriaAndroid
InstrutorVinícius Thiengo
NívelTodos os níveis
Vídeo aulas186
PlataformaUdemy
Acessar Curso
Receitas Android
Capa do livro Receitas Para Desenvolvedores Android
TítuloReceitas Para Desenvolvedores Android
CategoriaDesenvolvimento Android
AutorVinícius Thiengo
Edição
Ano2017
Capítulos20
Páginas936
Acessar Livro
Código Limpo
Capa do livro Refatorando Para Programas Limpos
TítuloRefatorando Para Programas Limpos
CategoriaEngenharia de Software
AutorVinícius Thiengo
Edição
Capítulos46
Páginas599
Acessar Livro
Quer aprender a programar para Android? Acesse abaixo o curso gratuito no Blog.
Conteúdo Exclusivo
Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog.
Email inválido

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:

Animação das telas de configuração de dados de acesso

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

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:

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

Atualização e-mail

Segurança de atualização

 Segurança de atualização

Load - atualização em back-end

Load - atualização em back-end

Erro na atualização

Erro na atualização

Atualização bem sucedida

Atualização bem sucedida

Atualização de senha

Atualização de senha

Segurança de atualização de senha

Segurança de atualização de senha

Load - atualização em back-end

Load - atualização em back-end

Erro na atualização

Erro na atualização

Atualização bem sucedida

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:

Diagrama do layout fragment_form.xml

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:

Caixa de diálogo AlertDialog Android

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:

Diagrama do layout dialog_password.xml

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()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:

Fragmento do formulário de e-mail

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&#8230;</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:

Botão do formulário de atualização de e-mail

A seguir o diagrama do layout anterior:

Diagrama do layout fragment_config_email.xml

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:

Pacotes do projeto Android BlueShoes

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:

Fragmento do formulário de atualização de senha

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&#8230;</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:

Diagrama do layout fragment_config_password.xml

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.

Tabs de seleção de formulário de atualização de dados de conexão

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:

Diagrama do layout activity_config_connection_data.xml

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:

Animação do fragmento do formulário de e-mail

Agora acessando a tela de atualização de senha e prosseguindo com o envio, temos:

Animação do fragmento do formulário de senha

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

Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog.
Email inválido

Relacionado

Colocando Telas de Introdução em Seu Aplicativo AndroidColocando Telas de Introdução em Seu Aplicativo AndroidAndroid
Como Criar Protótipos AndroidComo Criar Protótipos AndroidAndroid
Trabalhando Análise Qualitativa em seu Aplicativo AndroidTrabalhando Análise Qualitativa em seu Aplicativo AndroidAndroid
5 livros que não são de TI, mas que um desenvolvedor deveria ler5 livros que não são de TI, mas que um desenvolvedor deveria lerEmpreendedorismo

Compartilhar

Comentários Facebook

Comentários Blog (1)

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)
05/07/2019
Olá Thiengo boa tarde.
Acabei de receber sua notificação ( email ) cara olha sim já precisei usar Tabs, de duas formas Tabs mesmo onde me solicitaram fazer uma ordem de serviço e ai tive que criar Tabs nomeadas ( Veiculo, Rastreador,  Fotos e Finalização ) foi um terror fazer isto, recente precisei fazer uma aplicação que tinha uma rotina de Reserva de veiculo ai eu já "simulei" Tabs com o Layout FrameLayout e ficou muito bom.
Mais agora vou ver mais este conteúdo e aguardando muito o momento em que estiver comunicando com o Backend
Responder