Como Desenvolver as Telas de Endereço de Entrega - Android M-Commerce
(4494)
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 continuaremos com o desenvolvimento do aplicativo Android BlueShoes, app de mobile-commerce que estamos desenvolvendo em série de artigos e vídeos.
Aqui estaremos construindo as telas de gerência de endereços de entregas do usuário final.
Nesta parte do projeto também utilizaremos uma estratégia ainda não abordada nos projetos aqui do Blog: o trabalho com um fragmento host dentro de uma das páginas do ViewPager.
Antes de prosseguir, não esqueça de se inscrever 📫na lista de e-mails do Blog para receber todas a atualizações do projeto BlueShoes e de outros conteúdos de desenvolvimento Android exclusivos aqui do Blog.
A seguir os tópicos abordados nesta aula:
- Iniciando no Android m-commerce BlueShoes;
- Estratégia para as telas de endereços de entrega:
- Estrutura de domínio para os endereços:
- Lista de endereços de entrega:
- Formulário de novo endereço:
- Formulário de atualização de endereço:
- Atualização do strings.xml;
- Layout;
- Atualizando a DeliveryAddress;
- Criando a ConfigUpdateDeliveryAddressFragment.
- Fragmento host de lista e atualização de endereço:
- Atividade host de endereços:
- Atualização do strings.xml;
- Construindo a ConfigDeliveryAddressesSectionsAdapter;
- Desenvolvendo a ConfigDeliveryAddressesActivity;
- Atualizando o AndroidManifest;
- Atualizando a AccountSettingsItemsDataBase.
- O problema do BackButton:
- Testes e resultados;
- Vídeos;
- Conclusão;
- Fontes.
Iniciando no Android m-commerce BlueShoes
Então você está conhecendo o projeto Android mbile-commerce, do Blog, somente agora?
Ok. Primeiro: os artigos do projeto são todos acompanhados de seus respectivos vídeos, estes que são liberados ao longo da semana de lançamento da aula.
Segundo: ao menos uma aula é liberada por semana, logo, não deixe de se inscrever 📫 na lista de e-mails para receber o conteúdo em primeira mão.
Terceiro: o projeto está sendo desenvolvido em fases, e nesta primeira fase o foco é somente na ui, interface gráfica, do lado Android. Mas também teremos todo o lado Web, back-end e front-end.
Quarto: se você realmente chegou ao projeto somente agora, saiba que já existem outras 16 aulas que devem ser consumidas por você antes desta 17ª aula, isso para que a sua versão do projeto mobile-commerce seja desenvolvida com consistência:
- 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;
- 15ª aula - Como Desenvolver as Telas de Configuração de E-mail e Senha - Android M-Commerce;
- 16ª aula - Desenvolvendo as Telas de Cartão de Crédito Com Máscara de Campo - Android M-Commerce.
Estratégia para as telas de endereços de entrega
Sim, estamos em mais um conjunto de telas muito similar aos outros dois últimos conjuntos desenvolvidos da área de configurações de conta de usuário final.
E sim, a estratégia de desenvolvimento será exatamente a mesma:
- Primeiro vamos a escolha da entidade que têm mais dependentes nesta parte do projeto:
- Assim vamos ao desenvolvimento dos trechos estáticos desta entidade;
- Por fim vamos ao trabalho com os códigos dinâmicos dela.
- Nosso próximo passo será seguir com o passo anterior, porém com as entidades restantes.
Ok, Thiengo. Mas você tem alguma ideia de quais serão essas entidades?
Sim. Pois como falei anteriormente, as telas de gerência de endereços são similares aos dois últimos conjuntos de telas desenvolvidos em projeto. Assim precisaremos:
- De uma classe de domínio para representar cada endereço de entrega;
- Um fragmento para a lista de endereços já cadastrados;
- Um fragmento para a inserção de um novo endereço;
- Um fragmento para a atualização de algum endereço já cadastrado;
- Um fragmento host para conter, em uma mesma página de ViewPager, os fragmentos de lista e de atualização de endereços;
- Uma atividade host, com tabs, para conter todos os fragmentos de gerência de endereços.
O que? Um fragmento host de outros fragmentos?
Sim, mas não vamos entrar nos detalhes ainda nesta seção. Continue com a leitura do artigo, e aplicação do que é apresentado, e logo chegaremos, no momento certo, a este trecho.
Antes de seguir, saiba que você consegue ter acesso a versão mais atual do projeto diretamente do repositório dele em: https://github.com/viniciusthiengo/blueshoes-kotlin-android.
Como informado na última aula: nós vamos primeiro terminar toda a área de configurações de conta de usuário final para depois partirmos para a melhoria do código desta área.
Por que informei isso no parágrafo anterior? Porque ainda teremos inúmeros códigos duplicados nesta parte do projeto.
Protótipo estático
A seguir o protótipo estático das telas de configurações de endereços de entrega do usuário:
Carregando endereços | Sem endereço cadastrado |
Lista de endereços | Segurança de remoção |
Load - remoção em back-end Web | Remoção bem sucedida |
Erro na remoção | Atualização de endereço |
Segurança de atualização | Erro na atualização |
Load - atualização em back-end Web | Atualização bem sucedida |
Novo endereço | Load - inserção em back-end Web |
Inserção bem sucedida | Erro na inserção |
Estrutura de domínio para os endereços
Para a área de gerência de endereços nós também teremos uma nova estrutura de domínio que seguirá os mesmos moldes das estruturas já criadas para as áreas de gerência de cartões de crédito e gerência de dados de conexão de usuário final.
Dados de Estados
Antes de prosseguirmos direto à classe de domínio temos que primeiro colocar em projeto alguns dados que serão necessários desde à classe de domínio até aos códigos estáticos de interface gráfica de endereço de entrega.
Aqui estamos falando dos dados de Estados brasileiros, dados que entrarão no arquivo /res/values/arrays.xml como a seguir:
<?xml version="1.0" encoding="utf-8"?>
<resources>
...
<string-array name="states">
<item>Acre (AC)</item>
<item>Alagoas (AL)</item>
<item>Amapá (AP)</item>
<item>Amazonas (AM)</item>
<item>Bahia (BA)</item>
<item>Ceará (CE)</item>
<item>Distrito Federal (DF)</item>
<item>Espírito Santo (ES)</item>
<item>Goiás (GO)</item>
<item>Maranhão (MA)</item>
<item>Mato Grosso (MT)</item>
<item>Mato Grosso do Sul (MS)</item>
<item>Minas Gerais (MG)</item>
<item>Pará (PA)</item>
<item>Paraíba (PB)</item>
<item>Paraná (PR)</item>
<item>Pernambuco (PE)</item>
<item>Piauí (PI)</item>
<item>Rio de Janeiro (RJ)</item>
<item>Rio Grande do Norte (RN)</item>
<item>Rio Grande do Sul (RS)</item>
<item>Rondônia (RO)</item>
<item>Roraima (RR)</item>
<item>Santa Catarina (SC)</item>
<item>São Paulo (SP)</item>
<item>Sergipe (SE)</item>
<item>Tocantins (TO)</item>
</string-array>
</resources>
Classe de domínio
No pacote de classes de domínio, /domain, crie a classe que representará os endereços de entrega no sistema, a classe DeliveryAddress com o código a seguir:
class DeliveryAddress(
val street: String,
val number: Int,
val complement: String,
val zipCode: String,
val neighborhood: String,
val city: String,
val state: Int ) {
fun getStateName( context: Context )
= context
.resources
.getStringArray( R.array.states )[ state ]
}
E é isso mesmo que você está imaginando: quando o banco de dados do sistema estiver operando o dado de Estado, do endereço de entrega, ele será armazenado como um inteiro (Int), pois não faz sentido salvar este dado como String tendo em mente que ele não muda, nem a posição e nem o rótulo.
Os Estados permanecem os mesmos e um tipo Int, que guardará valores entre de 0-26, ocupa menos espaço em persistência do que um tipo String.
Base de dados temporária
Com a classe de domínio já desenvolvida e os dados de Estados bem alocados em projeto, vamos à base de dados temporária, mock data, que nos permitirá realizar ao menos os testes com a interface gráfica da área de gerência de endereços.
No pacote de dados, /data, adicione a classe DeliveryAddressesDataBase com o código a seguir:
class DeliveryAddressesDataBase {
companion object{
fun getItems()
= mutableListOf(
DeliveryAddress(
"Rua das Oliveiras",
1366,
"Condomínio Aldeias",
"29154-630",
"Colina de Laranjeiras",
"Serra",
7
),
DeliveryAddress(
"Av. Jayme Clayton",
856,
"Alphaville",
"22598-611",
"Limeira",
"Tataupé",
24
),
DeliveryAddress(
"Rua Almeida Presidente",
2563,
"Happy Days",
"25668-178",
"Limeira",
"Sobral",
5
),
DeliveryAddress(
"Rua das Emas",
58,
"Ao lado do Hospital Jorge Santos",
"23665-558",
"Setor Segundo",
"Itajaí",
23
)
)
}
}
Note que todos os dados de endereços são fictícios, digo, o conjunto deles não leva a lugar algum.
Pacote
Aqui também teremos um novo pacote interno para facilitar a divisão física do projeto.
No pacote /view (ou em sua própria versão de pacote de camada de visualização - alguns desenvolvedores utilizam /ui) siga:
- Clique com o botão direito do mouse;
- Então acesse New;
- Clique em Package;
- Em Enter new package name coloque config.deliveryaddress e clique em OK.
Ao final teremos:
Em aulas posteriores iremos melhorar ainda mais toda a estrutura física de projeto e também os rótulos de algumas classes, principalmente os rótulos grandes que deixam o projeto verboso.
Lista de endereços de entrega
O desenvolvimento de todo o algoritmo do fragmento de lista de endereços já cadastrados será tranquilo, pois todo o código é bem similar ao código do fragmento da lista de cartões de crédito, fragmento já desenvolvido em projeto.
Atualizando o arquivo de Strings
Nosso primeiro passo é a atualização do arquivo de Strings, /res/values/strings.xml. Coloque nele os trechos de código em destaque:
<resources>
...
<!-- ConfigDeliveryAddressesListFragment -->
<string name="config_delivery_addresses_tab_list">ENDEREÇOS</string>
<string name="delivery_address_street_label">Logradouro:</string>
<string name="delivery_address_number_label">Número:</string>
<string name="delivery_address_zip_code_label">CEP:</string>
<string name="delivery_address_neighborhood_label">Bairro:</string>
<string name="delivery_address_city_label">Cidade:</string>
<string name="delivery_address_state_label">Estado:</string>
<string name="delivery_address_complement_label">Complemento:</string>
<string name="update_item">Atualizar</string>
<string name="delivery_address_list_empty">
Ainda não há endereços de entrega cadastrados.
</string>
<string name="delivery_address_removed">
Endereço removido com sucesso.
</string>
</resources>
Lembrando que com as Strings estáticas do projeto em arquivos específicos para Strings nós conseguiremos facilitar em muito a manutenção do projeto, principalmente em caso de internacionalização de aplicativo.
Estilo do botão Atualizar
Diferente dos itens de cartões de crédito, para os itens de endereços cadastrados nós temos também a opção de edição, acionada pelo botão "Atualizar":
Para conseguir esta configuração de design no botão "Atualizar" temos de criar, em /res/drawable, o arquivo bt_update.xml com a seguinte configuração:
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!--
Definição da cor de background do shape
retangular.
-->
<solid android:color="@color/colorLightBlue" />
<!--
Definição da curvatura das pontas do shape
retangular.
-->
<corners android:radius="3dp"/>
</shape>
E colocar em /res/values/colors.xml a cor colorLightBlue como a seguir:
<?xml version="1.0" encoding="utf-8"?>
<resources>
...
<color name="colorLightBlue">#00A4FF</color>
</resources>
Layout de item
Em /res/layout crie o layout delivery_address_item.xml com a seguinte estrutura XML:
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_marginTop="6dp"
android:layout_marginBottom="6dp"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
card_view:cardCornerRadius="2dp">
<RelativeLayout
android:padding="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- Logradouro -->
<TextView
android:id="@+id/tv_street_label"
style="@style/TextViewFormItemListLabel"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:text="@string/delivery_address_street_label"/>
<TextView
android:id="@+id/tv_street"
android:layout_toRightOf="@+id/tv_street_label"
android:layout_toEndOf="@+id/tv_street_label"
android:layout_alignTop="@+id/tv_street_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:ellipsize="end"
android:textColor="@color/colorText"/>
<!-- Número -->
<TextView
android:id="@+id/tv_number_label"
style="@style/TextViewFormItemListLabel"
android:layout_below="@+id/tv_street_label"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:text="@string/delivery_address_number_label"/>
<TextView
android:id="@+id/tv_number"
android:layout_toRightOf="@+id/tv_number_label"
android:layout_toEndOf="@+id/tv_number_label"
android:layout_alignTop="@+id/tv_number_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/colorText"/>
<!-- CEP -->
<TextView
android:id="@+id/tv_zip_code_label"
style="@style/TextViewFormItemListLabel"
android:layout_below="@+id/tv_number_label"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:text="@string/delivery_address_zip_code_label"/>
<TextView
android:id="@+id/tv_zip_code"
android:layout_toRightOf="@+id/tv_zip_code_label"
android:layout_toEndOf="@+id/tv_zip_code_label"
android:layout_alignTop="@+id/tv_zip_code_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/colorText"/>
<LinearLayout
android:id="@+id/ll_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_zip_code_label"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:orientation="horizontal">
<RelativeLayout
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginRight="10dp"
android:layout_marginEnd="10dp">
<!-- Bairro -->
<TextView
android:id="@+id/tv_neighborhood_label"
style="@style/TextViewFormItemListLabel"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:text="@string/delivery_address_neighborhood_label"/>
<TextView
android:id="@+id/tv_neighborhood"
android:layout_toRightOf="@+id/tv_neighborhood_label"
android:layout_toEndOf="@+id/tv_neighborhood_label"
android:layout_alignTop="@+id/tv_neighborhood_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:ellipsize="end"
android:textColor="@color/colorText"/>
</RelativeLayout>
<RelativeLayout
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content">
<!-- Cidade -->
<TextView
android:id="@+id/tv_city_label"
style="@style/TextViewFormItemListLabel"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:text="@string/delivery_address_city_label"/>
<TextView
android:id="@+id/tv_city"
android:layout_toRightOf="@+id/tv_city_label"
android:layout_toEndOf="@+id/tv_city_label"
android:layout_alignTop="@+id/tv_city_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:ellipsize="end"
android:textColor="@color/colorText"/>
</RelativeLayout>
</LinearLayout>
<!-- Estado -->
<TextView
android:id="@+id/tv_state_label"
style="@style/TextViewFormItemListLabel"
android:layout_below="@+id/ll_container"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:text="@string/delivery_address_state_label"/>
<TextView
android:id="@+id/tv_state"
android:layout_toRightOf="@+id/tv_state_label"
android:layout_toEndOf="@+id/tv_state_label"
android:layout_alignTop="@+id/tv_state_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/colorText"/>
<!-- Complemento -->
<TextView
android:id="@+id/tv_complement_label"
style="@style/TextViewFormItemListLabel"
android:layout_below="@+id/tv_state_label"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:text="@string/delivery_address_complement_label"/>
<TextView
android:id="@+id/tv_complement"
android:layout_toRightOf="@+id/tv_complement_label"
android:layout_toEndOf="@+id/tv_complement_label"
android:layout_alignTop="@+id/tv_complement_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:ellipsize="end"
android:textColor="@color/colorText"/>
<Button
android:id="@+id/bt_update"
android:layout_below="@+id/tv_complement_label"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_width="wrap_content"
android:layout_height="27dp"
android:layout_marginTop="8dp"
android:layout_marginLeft="10dp"
android:layout_marginStart="10dp"
android:paddingLeft="18dp"
android:paddingRight="18dp"
android:background="@drawable/bt_update"
android:textColor="@android:color/white"
android:textAllCaps="false"
android:text="@string/update_item"/>
<Button
android:id="@+id/bt_remove"
android:layout_alignTop="@+id/bt_update"
android:layout_toLeftOf="@+id/bt_update"
android:layout_toStartOf="@+id/bt_update"
android:layout_width="wrap_content"
android:layout_height="27dp"
android:paddingLeft="18dp"
android:paddingRight="18dp"
android:background="@drawable/bt_remove"
android:textColor="@android:color/white"
android:textAllCaps="false"
android:text="@string/remove_item"/>
</RelativeLayout>
</androidx.cardview.widget.CardView>
Sim, estamos utilizando android:layout_weight em um layout de item de framework de lista. Eu sei que você está preocupado com a performance do layout por causa disso.
Vamos a "minha desculpa" para prosseguir com esta configuração de layout:
A lista de endereços de entrega do usuário final tende a ser uma lista com poucos itens e o uso de LinearLayout com Views filha utilizando o atributo android:layout_weight é uma maneira rápida de desenvolvermos o layout exato apresentado em protótipo estático.
Isso sabendo que os problemas de performance, principalmente por causa da pouca quantidade de itens (menos do que 10, por exemplo) são improváveis.
Se os problemas de processamento surgirem, nesta parte do projeto, quando em produção, certamente o nosso primeiro passo será melhorar a estrutura de layout de item.
A seguir o diagrama do layout delivery_address_item.xml:
Classe adaptadora de itens
A classe adaptadora é bem simples e neste ponto da aula vamos desenvolver apenas 90% dela, pois em outros trechos desta mesma aula colocaremos os códigos de acionamento de caixa de diálogo de remoção e de área de atualização de endereço.
Em /view/config/deliveryaddress crie a classe ConfigDeliveryAddressesListItemsAdapter com o código a seguir:
class ConfigDeliveryAddressesListItemsAdapter(
private val items: MutableList<DeliveryAddress>
) :
RecyclerView.Adapter<ConfigDeliveryAddressesListItemsAdapter.ViewHolder>() {
override fun onCreateViewHolder(
parent: ViewGroup,
type: Int ): ViewHolder {
val layout = LayoutInflater
.from( parent.context )
.inflate(
R.layout.delivery_address_item,
parent,
false
)
return ViewHolder( layout )
}
override fun onBindViewHolder(
holder: ViewHolder,
position: Int ) {
holder.setData( items[ position ] )
}
override fun getItemCount() = items.size
inner class ViewHolder( itemView: View ) :
RecyclerView.ViewHolder( itemView ),
View.OnClickListener {
private val tvStreet : TextView
private val tvNumber : TextView
private val tvZipCode : TextView
private val tvNeighborhood : TextView
private val tvCity : TextView
private val tvState : TextView
private val tvComplement : TextView
private val btUpdate : Button
private val btRemove : Button
init{
tvStreet = itemView.findViewById( R.id.tv_street )
tvNumber = itemView.findViewById( R.id.tv_number )
tvZipCode = itemView.findViewById( R.id.tv_zip_code )
tvNeighborhood = itemView.findViewById( R.id.tv_neighborhood )
tvCity = itemView.findViewById( R.id.tv_city )
tvState = itemView.findViewById( R.id.tv_state )
tvComplement = itemView.findViewById( R.id.tv_complement )
btUpdate = itemView.findViewById( R.id.bt_update )
btUpdate.setOnClickListener( this )
btRemove = itemView.findViewById( R.id.bt_remove )
btRemove.setOnClickListener( this )
}
fun setData( item: DeliveryAddress ){
tvStreet.setText( item.street )
tvNumber.setText( item.number.toString() )
tvZipCode.setText( item.zipCode )
tvNeighborhood.setText( item.neighborhood )
tvCity.setText( item.city )
/*
* Assim que adicionarmos o parâmetro "fragment" ao
* construtor de ConfigDeliveryAddressListItemsAdapter
* o Android Studio IDE vai parar de apontar a linha
* abaixo como uma linha problemática.
**/
tvState.setText( item.getStateName( fragment.context!! ) )
tvComplement.setText( item.complement )
}
override fun onClick( view: View ) {
/* TODO */
}
}
Você deve ter imaginado que no código de onClick() teremos ao menos um bloco condicional, certo?
E é isso mesmo, os dois botões de cada item vão direcionar as ações de clique para o mesmo onClick(), mas será bem simples o script final deste método, acredite.
Layout principal de fragmento
O layout do fragmento container da lista de endereços é bem simples e por sinal já vem com a configuração de mensagem para quando não há endereços cadastrados, configuração discutida na aula sobre a área de gerência de cartões de crédito.
Em /res/layout crie o layout fragment_config_delivery_addresses_list.xml com a seguinte estrutura XML:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:visibility="gone"
android:id="@+id/tv_empty_list"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="30dp"
android:text="@string/delivery_address_list_empty"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_delivery_addresses"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"/>
</FrameLayout>
Nossa, Thiengo. Poderíamos ter aproveitado o layout de ConfigCreditCardsListFragment nesse novo fragmento que estaremos desenvolvendo, certo?
Sim. Mas como falei anteriormente: está e outras melhorias em código vão vir em aulas posteriores. Por agora, vamos persistir com layouts separados, somente devido ao ID do RecyclerView, rv_delivery_addresses, ser diferente.
A seguir o diagrama do layout fragment_config_delivery_addresses_list.xml:
Construindo a ConfigDeliveryAddressesListFragment
Agora vamos a entidade host da lista de endereços de entrega. No novo pacote, /view/config/deliveryaddress, adicione o fragmento ConfigDeliveryAddressesListFragment com o seguinte código:
class ConfigDeliveryAddressesListFragment :
FormFragment() {
companion object{
const val TAB_TITLE = R.string.config_delivery_addresses_tab_list
}
override fun getLayoutResourceID()
= R.layout.fragment_config_delivery_addresses_list
override fun backEndFakeDelay() {
backEndFakeDelay(
true,
getString( R.string.delivery_address_removed )
)
}
override fun blockFields( status: Boolean ) {
/* TODO */
}
override fun isMainButtonSending( status: Boolean ) {
/* TODO */
}
override fun onActivityCreated( savedInstanceState: Bundle? ) {
super.onActivityCreated( savedInstanceState )
updateFlFormToFullFreeScreen()
initItems()
}
/*
* Método que inicializa a lista de endereços de entrega.
* */
private fun initItems(){
rv_delivery_addresses.setHasFixedSize( false )
val layoutManager = LinearLayoutManager( activity )
rv_delivery_addresses.layoutManager = layoutManager
val adapter = ConfigDeliveryAddressesListItemsAdapter(
DeliveryAddressesDataBase.getItems()
)
rv_delivery_addresses.adapter = adapter
}
}
Como ocorreu para os fragmentos da área de cartões de crédito, aqui também tivemos a necessidade de invocar o método updateFlFormToFullFreeScreen() no onActivityCreated() do novo fragmento para que a configuração padrão do layout de FormFragment fosse desativada e assim a lista de endereços corretamente enquadrada em tela.
Callbacks para remoção de item
Os callbacks que adicionaremos nesta parte da aula são inteiramente ligados a ação de exclusão de endereço. Será tudo muito similar ao que já foi abordado na aula de cartões de crédito, na parte de exclusão de cartões.
Nosso primeiro passo é criar no fragmento ConfigDeliveryAddressesListFragment as entidades que permitirão o recebimento das funções diretamente do adapter de endereços de entrega.
Atualize este fragmento com os trechos de código em destaque a seguir:
class ConfigDeliveryAddressesListFragment :
... {
...
private var callbackMainButtonUpdate : (Boolean)->Unit = {}
private var callbackBlockFields : (Boolean)->Unit = {}
private var callbackRemoveItem : (Boolean)->Unit = {}
...
/*
* Método utilizado para receber os callbacks do adapter
* do RecyclerView para assim poder atualizar os itens
* de adapter.
* */
fun callbacksToRemoveItem(
mainButtonUpdate: (Boolean)->Unit,
blockFields: (Boolean)->Unit,
removeItem: (Boolean)->Unit ){
callbackMainButtonUpdate = mainButtonUpdate
callbackBlockFields = blockFields
callbackRemoveItem = removeItem
}
}
As propriedades hosts de funções, callbacks, foram todas colocadas como private, pois com um menor escopo nós garantimos maior facilidade em manutenção de código.
Assim podemos partir para o preenchimento dos métodos blockFields() e isMainButtonSending() presentes no mesmo fragmento. Atualize-os como a seguir:
...
override fun blockFields( status: Boolean ) {
callbackBlockFields( status )
}
override fun isMainButtonSending( status: Boolean ) {
callbackMainButtonUpdate( status )
callbackRemoveItem( status )
}
...
Com isso podemos partir para as atualizações no adapter de itens de endereço, ConfigDeliveryAddressesListItemsAdapter. Começando com a assinatura do construtor deste adapter:
class ConfigDeliveryAddressesListItemsAdapter(
private val fragment : ConfigDeliveryAddressesListFragment,
private val items: MutableList<DeliveryAddress>
) : ... {
....
}
Agora a atualização do trecho de inicialização deste adapter no método initItems() do fragmento ConfigDeliveryAddressesListFragment:
...
private fun initItems(){
...
val adapter = ConfigDeliveryAddressesListItemsAdapter(
this,
DeliveryAddressesDataBase.getItems()
)
...
}
...
Então, de volta ao adapter ConfigDeliveryAddressesListItemsAdapter, vamos criar um método, toRemove(), com os dados de funções callback:
class ConfigDeliveryAddressesListItemsAdapter(
...
) : ... {
...
inner class ViewHolder( itemView: View ) :
... {
...
override fun onClick( view: View ) {
/*
* É preciso salvar em uma nova propriedade a posição do
* item selecionado, pois o valor de adapterPosition está
* sempre sendo atualizado e isso, o acesso a adapterPosition
* diretamente dentro do callback, poderia ocasionar em
* uma Exception.
* */
val selectedItem = adapterPosition
if( view.id == btRemove.id ){
toRemove( selectedItem )
}
}
private fun toRemove( position: Int ){
fragment.callbacksToRemoveItem(
{
status ->
btUpdate.isEnabled = !status
btRemove.isEnabled = !status
},
{
status ->
btRemove.text =
if( status )
fragment.getString( R.string.remove_item_going )
else
fragment.getString( R.string.remove_item )
},
{
status ->
if( !status ){
items.removeAt( position )
notifyItemRemoved( position )
}
}
)
fragment.callPasswordDialog()
}
}
}
A proposta de ter um método toRemove() dentro do ViewHolder do adapter ConfigDeliveryAddressesListItemsAdapter é de aliviar todo o código que entraria no onClick(), pois ainda teremos de desenvolver o algoritmo do botão "Atualizar".
Com esta separação de responsabilidades em mais métodos, a leitura do código fica mais simples, principalmente para outros desenvolvedores que não construíram esta parte do app.
Mensagem de lista vazia
Com os callbacks configurados, ao menos os de remoção de endereço, podemos ir ao trecho de código dinâmico da parte de apresentação de mensagem quando não houver endereços cadastrados.
Somente ao trecho dinâmico?
Sim, pois as partes estáticas, layout e arquivo de Strings, já configuramos em seções anteriores. Logo, em ConfigDeliveryAddressesListFragment, coloque o código do Observer em destaque:
class ConfigDeliveryAddressesListFragment :
... {
...
private fun initItems(){
...
val adapter = ConfigDeliveryAddressesListItemsAdapter(
this,
DeliveryAddressesDataBase.getItems()
)
adapter.registerAdapterDataObserver( RecyclerViewObserver() )
rv_delivery_addresses.adapter = adapter
}
/*
* Com o RecyclerView.AdapterDataObserver é possível
* escutar o tamanho atual da lista de itens vinculada
* ao RecyclerView e caso essa lista esteja vazia, então
* podemos apresentar uma mensagem ao usuário informando
* sobre a lista vazia.
* */
inner class RecyclerViewObserver :
RecyclerView.AdapterDataObserver() {
override fun onItemRangeRemoved( positionStart: Int, itemCount: Int ) {
super.onItemRangeRemoved( positionStart, itemCount )
tv_empty_list.visibility =
if( rv_delivery_addresses.adapter!!.itemCount == 0 )
View.VISIBLE
else
View.GONE
}
}
}
Com isso podemos partir para o fragmento de novo endereço.
Formulário de novo endereço
Como para a lista de endereços de entrega, aqui também teremos uma "vida fácil". O formulário de novo endereço de entrega é até mais simples do que o formulário de novo cartão de crédito:
Com as configurações de design de Spinner já definidas, em "dois tempos" toda a nossa área de adição de endereço de entrega estará pronta.
Vale ressaltar que não é nesta aula que estaremos trabalhando com o algoritmo de localização de CEP, como proposto em protótipo estático. Isto, pois este algoritmo exige lógica de negócio com API de comunicação remota no app Android.
Atualização do strings.xml
Primeiro vamos a atualização do arquivo strings.xml em /res/values. Adicione nele os trechos em destaque:
<resources>
...
<!-- ConfigNewDeliveryAddressFragment -->
<string name="config_delivery_address_tab_new">NOVO</string>
<string name="add_delivery_address_street_label">Logradouro:</string>
<string name="add_delivery_address_number_label">Número:</string>
<string name="add_delivery_address_complement_label">Complemento:</string>
<string name="add_delivery_address_zip_code_label">CEP:</string>
<string name="add_delivery_address_neighborhood_label">Bairro:</string>
<string name="add_delivery_address_city_label">Cidade:</string>
<string name="add_delivery_address_state_label">Estado:</string>
<string name="invalid_delivery_address">
Falhou, tente novamente.
</string>
<string name="add_delivery_address">Adicionar endereço</string>
<string name="add_delivery_address_going">Adicionando…</string>
</resources>
Layout de fragmento
Em /res/layout adicione o layout fragment_config_new_delivery_address.xml com a seguinte configuração XML:
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:baselineAligned="false">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.7"
android:layout_marginRight="12dp"
android:layout_marginEnd="12dp"
android:orientation="vertical">
<TextView
style="@style/TextViewFormLabel"
android:text="@string/add_delivery_address_street_label"/>
<EditText
android:id="@+id/et_street"
style="@style/EditTextFormField"
android:layout_width="match_parent"
android:background="@drawable/bg_form_field"
android:inputType="text"
android:imeOptions="actionNext"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.3"
android:orientation="vertical">
<TextView
style="@style/TextViewFormLabel"
android:text="@string/add_delivery_address_number_label"/>
<EditText
android:id="@+id/et_number"
style="@style/EditTextFormField"
android:layout_width="match_parent"
android:background="@drawable/bg_form_field"
android:inputType="number"
android:imeOptions="actionNext"/>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="vertical">
<TextView
style="@style/TextViewFormLabel"
android:text="@string/add_delivery_address_complement_label"/>
<EditText
android:id="@+id/et_complement"
style="@style/EditTextFormField"
android:layout_width="match_parent"
android:background="@drawable/bg_form_field"
android:inputType="text"
android:imeOptions="actionNext"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:baselineAligned="false">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.35"
android:layout_marginRight="12dp"
android:layout_marginEnd="12dp"
android:orientation="vertical">
<TextView
style="@style/TextViewFormLabel"
android:text="@string/add_delivery_address_zip_code_label"/>
<com.santalu.maskedittext.MaskEditText
android:id="@+id/et_zip_code"
style="@style/EditTextFormField"
android:layout_width="match_parent"
android:background="@drawable/bg_form_field"
android:inputType="number"
android:imeOptions="actionNext"
app:met_mask="#####-###"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.65"
android:orientation="vertical">
<TextView
style="@style/TextViewFormLabel"
android:text="@string/add_delivery_address_neighborhood_label"/>
<EditText
android:id="@+id/et_neighborhood"
style="@style/EditTextFormField"
android:layout_width="match_parent"
android:background="@drawable/bg_form_field"
android:inputType="text"
android:imeOptions="actionNext"/>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:baselineAligned="false">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.5"
android:layout_marginRight="12dp"
android:layout_marginEnd="12dp"
android:orientation="vertical">
<TextView
style="@style/TextViewFormLabel"
android:text="@string/add_delivery_address_city_label"/>
<EditText
android:id="@+id/et_city"
style="@style/EditTextFormField"
android:layout_width="match_parent"
android:background="@drawable/bg_form_field"
android:inputType="text"
android:imeOptions="actionNext"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.5"
android:orientation="vertical">
<TextView
style="@style/TextViewFormLabel"
android:text="@string/add_delivery_address_state_label"/>
<Spinner
android:id="@+id/sp_state"
style="@style/SpinnerForm"
android:entries="@array/states"/>
</LinearLayout>
</LinearLayout>
<Button
android:id="@+id/bt_nu_address"
style="@style/ButtonForm"
android:layout_marginTop="24dp"
android:layout_marginBottom="3dp"
android:layout_marginEnd="1dp"
android:layout_marginRight="1dp"
android:layout_gravity="end"
android:text="@string/add_delivery_address"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
Primeiro, lembre que os dados do Spinner de Estados foram adicionados ao projeto ainda na seção Dados de Estados.
Segundo, a MaskEditText é novamente uma peça fundamental para a boa experiência do usuário, aqui no campo de CEP:
...
<com.santalu.maskedittext.MaskEditText
...
app:met_mask="#####-###"/>
...
E terceiro, o Button de ID bt_nu_address tem este ID porque ele será útil na tela de novo endereço (n = new) e na tela de atualização de endereço (u = update).
É, eu sei. É complicado de entender ainda neste ponto da aula, mas continue com a leitura que já já chegaremos, ainda neste artigo, ao desenvolvimento da tela de edição de endereço.
A seguir o diagrama do layout anterior:
Desenvolvendo a ConfigNewDeliveryAddressFragment
Com o layout já apresentado e com todas as outras partes estáticas do fragmento de novo endereço já desenvolvidas, podemos seguramente, dentro do pacote /view/config/deliveryaddress, criar o fragmento ConfigNewDeliveryAddressFragment como a seguir:
class ConfigNewDeliveryAddressFragment :
FormFragment() {
companion object{
const val TAB_TITLE = R.string.config_delivery_address_tab_new
}
override fun getLayoutResourceID()
= R.layout.fragment_config_new_delivery_address
override fun onActivityCreated( savedInstanceState: Bundle? ){
super.onActivityCreated( savedInstanceState )
updateFlFormToFullFreeScreen()
bt_nu_address.setOnClickListener{
/*
* O método mainAction() é invocado no lugar
* de callPasswordDialog(), pois aqui não há
* necessidade de dialog de senha para a
* adição de endereço de entrega.
* */
mainAction()
}
}
override fun backEndFakeDelay(){
backEndFakeDelay(
false,
getString( R.string.invalid_delivery_address )
)
}
override fun blockFields( status: Boolean ){
et_street.isEnabled = !status
et_number.isEnabled = !status
et_complement.isEnabled = !status
et_zip_code.isEnabled = !status
et_neighborhood.isEnabled = !status
et_city.isEnabled = !status
sp_state.isEnabled = !status
bt_nu_address.isEnabled = !status
}
override fun isMainButtonSending( status: Boolean ){
bt_nu_address.text =
if( status )
getString( R.string.add_delivery_address_going )
else
getString( R.string.add_delivery_address )
}
}
Novamente o método updateFlFormToFullFreeScreen() sendo utilizado em onActivityCreated(), pois precisamos de toda a parte da tela disponibilizada ao fragmento atual.
Assim podemos partir para o fragmento de atualização de endereços que, mesmo que você tenha pensado nesta estratégia, não é o mesmo fragmento que o último desenvolvido.
Mas segue um spoiler 😎: é uma subclasse do fragmento anterior.
Formulário de atualização de endereço
Sim, o formulário de atualização de endereço é quase que o mesmo quando comparado ao formulário de novo endereço de entrega:
As mudanças necessárias vão vir em código dinâmico e em um layout que vai aproveitar todo o layout de novo endereço de entrega.
Atualização do strings.xml
No arquivo de Strings do projeto, /res/values/strings.xml, adicione os trechos em destaque:
<resources>
...
<!-- ConfigUpdateDeliveryAddressFragment -->
<string name="updating_delivery_address">Atualizando endereço.</string>
<string name="update_delivery_address">Atualizar endereço</string>
<string name="update_delivery_address_going">Atualizando…</string>
</resources>
Layout
Antes de irmos ao layout de edição de endereço, vamos primeiro adicionar uma nova cor em /res/values/colors.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
...
<color name="colorTextBoxTransparent">#CCEEEEEE</color>
</resources>
Esta cor é responsável pelo background transparente no informe de topo do formulário de edição de endereço:
Agora em /res/layout adicione o layout fragment_config_update_delivery_address.xml com a seguinte estrutura XML:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:textColor="@color/colorText"
android:textStyle="bold"
android:background="@color/colorTextBoxTransparent"
android:text="@string/updating_delivery_address"/>
<include
layout="@layout/fragment_config_new_delivery_address"/>
</LinearLayout>
Simples, certo?
O único TextView do layout container é o responsável pela mensagem "Atualizando endereço.":
Esta mensagem em topo de formulário é importante para que o usuário fique ciente que aquela é uma área de atualização de endereço e não de novo endereço.
Acredite, este tipo de informação ajuda e muito o usuário final e é sim necessária, mesmo sabendo que as tabs de formulário de novo endereço e de formulário de atualização de endereço são diferentes.
Assim o diagrama do layout anterior:
Atualizando a DeliveryAddress
Antes de seguirmos aos códigos do novo fragmento de edição de endereço, ainda é preciso colocar uma chave de acesso ao objeto DeliveryAddress em envio e adicionar a esta classe a implementação da Interface Parcelable.
Thiengo, "em envio"?
Sim, pois o fragmento de edição de endereço será acionado a partir de algum item da lista de endereços. O item selecionado terá o objeto DeliveryAddress dele sendo enviado, via Intent, ao fragmento de edição.
Sendo assim o melhor local para colocarmos essa chave de acesso é na classe do objeto que será transportado via Intent. Em DeliveryAddress adicione o código em destaque:
@Parcelize
class DeliveryAddress(
val street: String,
val number: Int,
val complement: String,
val zipCode: String,
val neighborhood: String,
val city: String,
val state: Int ) : Parcelable {
fun getStateName( context: Context )
= context
.resources
.getStringArray( R.array.states )[ state ]
companion object {
const val KEY = "delivery-address-key"
}
}
O trabalho com a anotação @Parcelize, como explicado em outras aulas, é a maneira mais simples de adicionarmos o Parcelable a uma classe Kotlin.
Criando a ConfigUpdateDeliveryAddressFragment
Aqui a nossa estratégia será reaproveitar os códigos do fragmento ConfigNewDeliveryAddressFragment e não, como provavelmente esperado por alguns, atualiza-lo colocando inúmeros if()...else.
Alias, está não deixa de ser uma estratégia, utilizar vários if()...else, porém o código da classe ConfigNewDeliveryAddressFragment ficaria bem mais complicado de ser entendido e certamente necessitaria de muitos comentários para ajudar na leitura dele.
Sendo assim, leve essa estratégia de herança de classes também para os seus próprios projetos, pois assim é possível reaproveitar vários trechos de códigos, sem duplicação, e modificar somente o necessário.
Antes de partirmos aos códigos do fragmento de atualização de endereço de entrega, como sabemos que a estratégia utilizada é por meio de herança, vamos primeiro atualizar o fragmento ConfigNewDeliveryAddressFragment como sendo um fragmento open, caso contrário não será possível trabalhar a herança. Lembrando que no Kotlin toda classe é por padrão final. Segue atualização:
open class ConfigNewDeliveryAddressFragment :
... {
...
}
No pacote /view/config/deliveryaddress crie o fragmento ConfigUpdateDeliveryAddressFragment com o código a seguir:
class ConfigUpdateDeliveryAddressFragment :
ConfigNewDeliveryAddressFragment() {
override fun getLayoutResourceID()
= R.layout.fragment_config_update_delivery_address
override fun onActivityCreated( savedInstanceState: Bundle? ) {
super.onActivityCreated( savedInstanceState )
bt_nu_address.text = getString( R.string.update_delivery_address )
bt_nu_address.setOnClickListener{
callPasswordDialog()
}
fillForm()
}
private fun fillForm(){
val address = arguments!!.getParcelable<DeliveryAddress>(
DeliveryAddress.KEY
)
et_street.setText( address.street )
et_number.setText( address.number.toString() )
et_complement.setText( address.complement )
et_zip_code.setText( address.zipCode )
et_neighborhood.setText( address.neighborhood )
et_city.setText( address.city )
sp_state.setSelection( address.state )
}
override fun backEndFakeDelay(){
backEndFakeDelay(
false,
getString( R.string.invalid_delivery_address )
)
}
override fun isMainButtonSending( status: Boolean ){
bt_nu_address.text =
if( status )
getString( R.string.update_delivery_address_going )
else
getString( R.string.update_delivery_address )
}
}
A maior diferença em relação ao fragmento de novo endereço é o método fillForm(), responsável por obter o objeto DeliveryAddress enviado via Intent e então, com o objeto obtido, preencher os campos com os valores do endereço em edição.
Você deve estar se perguntando: aonde o fragmento ConfigUpdateDeliveryAddressFragment será invocado?
Sim, no onClick() do adapter ConfigDeliveryAddressesListItemsAdapter, porém ainda precisamos construir um outro fragmento antes de chegarmos a este código.
Fragmento host de lista e atualização de endereço
Se você notar bem no protótipo estático, tanto o fragmento de lista de endereços cadastrados quanto o fragmento de edição de endereço ficam na mesma página do ViewPager, ViewPager que ainda vamos configurar em atividade host:
Existem inúmeras estratégias para conseguir isso, porém a mais simples e eficiente em meu ponto de vista é utilizando um fragmento host.
Fragmento host?
Sim. Um fragmento que será responsável por gerenciar a mudança de "fragmento de lista" para "fragmento de edição de endereço" e vice-versa.
Acredite, é uma estratégia bem simples e que exige pouco código.
Layout principal
Mesmo sendo um fragmento host, sem lógica de negócio do nosso domínio de problema, ainda temos de ter um layout para conter os fragmentos, para permitir a inserção deles via código dinâmico.
Em /res/layout adicione o layout fragment_config_delivery_address_host.xml com o seguinte código estático:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fl_root"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
E então os códigos dinâmicos.
Configuração da ConfigDeliveryAddressHostFragment
Em nosso novo pacote, /view/config/deliveryaddress, adicione o fragmento host com o rótulo ConfigDeliveryAddressHostFragment e com o seguinte código Kotlin:
/*
* Fragmento com responsabilidade de ser o fragmento
* host de mais de um fragmento e assim permitir a
* fácil alternância de fragmentos dentro de uma mesma
* tela de ViewPager.
* */
class ConfigDeliveryAddressHostFragment
: Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle? ): View? {
/*
* É preciso inflar o layout que vai conter
* os fragmentos.
* */
val view = inflater
.inflate(
R.layout.fragment_config_delivery_address_host,
container,
false
)
/*
* Somente na primeira abertura é que a regra de
* fragmento inicial, do bloco condicional a seguir,
* deve ser seguida.
* */
if( savedInstanceState == null ){
val transaction = activity!!
.supportFragmentManager!!
.beginTransaction()
/*
* Então, aqui no fragmento root (container),
* iniciamos com o primeiro fragmento via
* FragmentTransaction e sem trabalho com pilha
* de fragmentos.
* */
transaction
.replace(
R.id.fl_root,
ConfigDeliveryAddressesListFragment()
)
.commit()
}
return view
}
}
Primeiro: o fragmento inicial contido sempre será o fragmento de lista de endereços, ConfigDeliveryAddressesListFragment.
Segundo e último ponto: o fragmento host não deve herdar de FormFragment, mas os fragmentos que serão contidos devem sim herdar deste fragmento, pois é uma regra de negócio em nosso projeto Android onde fragmentos de formulários herdam de FormFragment.
Método toUpdate() em adapter
Por fim, temos de atualizar o método onClick() de ConfigDeliveryAddressesListItemsAdapter, pois ainda esta faltando o algoritmo que invoca o fragmento de edição de endereço.
Primeiro, dentro do ViewHolder de ConfigDeliveryAddressesListItemsAdapter, adicione o método toUpdate() como a seguir:
...
private fun toUpdate( position: Int ){
val updateFrag = ConfigUpdateDeliveryAddressFragment()
/*
* Colocando como dado de transição o item selecionado para
* atualização.
* */
val bundle = Bundle()
bundle.putParcelable(
DeliveryAddress.KEY,
items[ position ]
)
updateFrag.arguments = bundle
val transaction = fragment
.fragmentManager!!
.beginTransaction()
/*
* O acesso ao FrameLayout root volta a ocorrer para que
* seja possível o replace de fragmentos dentro da mesma
* janela de ViewPager.
* */
transaction
.replace(
R.id.fl_root,
updateFrag
)
/*
* Com o setTransition() e addToBackStack() nós estamos,
* respectivamente permitindo uma transição entre fragmentos
* e os colocando em uma pilha de fragmentos para que seja
* possível voltar ao fragmento anteriormente apresentado
* na mesma janela de ViewPager.
* */
transaction
.setTransition( FragmentTransaction.TRANSIT_FRAGMENT_OPEN )
.addToBackStack( null )
.commit()
}
...
Note que R.id.fl_root é referente ao ViewGroup root do fragmento host, onde ficarão, se alternarão, os fragmentos de lista de endereços e de edição de endereço.
Então no onClick() desta mesma classe adaptadora de itens, adicione o trecho que falta no bloco condicional, como a seguir:
...
override fun onClick( view: View ) {
/*
* É preciso salvar em uma nova propriedade a posição do
* item selecionado, pois o valor de adapterPosition está
* sempre sendo atualizado e isso, o acesso a adapterPosition
* diretamente dentro do callback, poderia ocasionar em
* uma Exception.
* */
val selectedItem = adapterPosition
if( view.id == btRemove.id ){
toRemove( selectedItem )
}
else{
toUpdate( selectedItem )
}
}
...
Atividade host de endereços
A atividade host dos fragmentos desenvolvidos nesta aula será muito similar a todas as outras atividades com tabs desenvolvidas até este ponto do projeto.
Alias, devido a isso o layout será exatamente o mesmo, /res/layout/activity_tabs_user_config.xml, dispensando apresentação individual.
Atualização do strings.xml
Em /res/values/strings.xml adicione o trecho em destaque:
<resources>
...
<!-- ConfigDeliveryAddressesActivity -->
<string name="title_activity_config_delivery_addresses">
Endereços para entrega
</string>
</resources>
Construindo a ConfigDeliveryAddressesSectionsAdapter
No pacote /view/config/deliveraddress adicione o novo adapter, de páginas em ViewPager, ConfigDeliveryAddressesSectionsAdapter 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 ConfigDeliveryAddressesSectionsAdapter(
val context: Context,
fm: FragmentManager ) : FragmentPagerAdapter( fm ) {
companion object{
const val TOTAL_PAGES = 2
const val HOST_DELIVERY_ADDRESSES_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 ){
HOST_DELIVERY_ADDRESSES_PAGE_POS ->
ConfigDeliveryAddressHostFragment()
else ->
ConfigNewDeliveryAddressFragment()
}
override fun getPageTitle( position: Int )
= context.getString(
when( position ){
HOST_DELIVERY_ADDRESSES_PAGE_POS ->
ConfigDeliveryAddressesListFragment.TAB_TITLE
else ->
ConfigNewDeliveryAddressFragment.TAB_TITLE
}
)
override fun getCount()
= TOTAL_PAGES
}
O fragmento host, ConfigDeliveryAddressHostFragment, está sendo utilizado em getItem(), mas para obter o título da tab ainda mantivemos o uso do fragmento ConfigDeliveryAddressesListFragment.
Zero problemas quanto a isto, mesmo em relação a qualidade de leitura de código.
Este adapter de páginas para ViewPager certamente é uma das entidades que mais será modificada devido a atualizações de reaproveitamento de código que ainda realizaremos.
Isso, pois os códigos de adapters de ViewPager desenvolvidos até este ponto do projeto são todos muito similares.
Desenvolvendo a ConfigDeliveryAddressesActivity
Agora o código dinâmico inicial da atividade host dos fragmentos de gerência de endereços de entrega.
No pacote /view/config/deliveryaddress adicione a atividade ConfigDeliveryAddressesActivity com o seguinte código:
class ConfigDeliveryAddressesActivity :
AppCompatActivity() {
override fun onCreate( savedInstanceState: Bundle? ) {
super.onCreate( savedInstanceState )
setContentView( R.layout.activity_tabs_user_config )
setSupportActionBar( toolbar )
/*
* Para liberar o back button na barra de topo da
* atividade.
* */
supportActionBar?.setDisplayHomeAsUpEnabled( true )
supportActionBar?.setDisplayShowHomeEnabled( true )
/*
* Hackcode para que a imagem de background do layout não
* se ajuste de acordo com a abertura do teclado de
* digitação. Caso utilizando o atributo
* android:background, o ajuste ocorre, desconfigurando o
* layout.
* */
window.setBackgroundDrawableResource( R.drawable.bg_activity )
/*
* Criando o adaptador de fragmentos que ficarão expostos
* no ViewPager.
* */
val sectionsPagerAdapter =
ConfigDeliveryAddressesSectionsAdapter(
this,
supportFragmentManager
)
/*
* Acessando o ViewPager e vinculando o adaptador de
* fragmentos a ele.
* */
view_pager.adapter = sectionsPagerAdapter
/*
* Acessando o TabLayout e vinculando ele ao ViewPager
* para que haja sincronia na escolha realizada em
* qualquer um destes componentes visuais.
* */
tabs.setupWithViewPager( view_pager )
}
/*
* 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 ){
onBackPressed()
return true
}
return super.onOptionsItemSelected( item )
}
}
Está é outra classe que terá bastante código reaproveitado em uma superclasse, por exemplo, quando chegarmos à fase de refatoração dos algoritmos da área de configurações de conta de usuário.
Atualizando o AndroidManifest
Assim a atualização do AndroidManifest com a nova atividade:
<?xml version="1.0" encoding="utf-8"?>
<manifest
...>
...
<application
...>
...
<activity
android:name=".view.config.deliveryaddress.ConfigDeliveryAddressesActivity"
android:label="@string/title_activity_config_delivery_addresses"
android:theme="@style/AppTheme.NoActionBar" />
</application>
</manifest>
Atualizando a AccountSettingsItemsDataBase
E por fim a atualização do terceiro item da base de dados de itens de configuração de conta:
class AccountSettingsItemsDataBase {
companion object{
fun getItems( ... )
= listOf(
AccountSettingItem(...),
AccountSettingItem(...),
AccountSettingItem(
...
ConfigDeliveryAddressesActivity::class.java
),
AccountSettingItem(...)
)
}
}
Com isso podemos partir para os testes, certo?
Negativo, ainda temos um problema a resolver.
O problema do BackButton
Executando o aplicativo e seguindo o roteiro:
- Acessar área de configurações de conta de usuário;
- Acessar área de gerência de endereços;
- Abrir a edição de um endereço qualquer;
- Mudar de tab, para a área de novo endereço;
- e Tentar sair da área de gerência de endereços... bug 🦗.
É isso mesmo. A primeira ação que ocorre é remover o "fragmento de atualização de endereço" da pilha de fragmentos da primeira janela do ViewPager e então, na segunda tentativa de deixar a área de gerência de endereços é que o BackButton funciona como esperado, mesmo com a segunda página do ViewPager sendo a página foco em tela.
Veja o fluxograma a seguir sobre o problema atual:
Porém o desejado é o seguinte:
Ou seja, se a aba e seleção for a de "NOVO" endereço, então independente da situação dos fragmentos da aba "ENDEREÇOS" o BackButton, de topo ou de barra de fundo, se acionado deve deixar a área de gerência de endereços de entrega.
Solução com o onBackPressed()
A solução para conseguir o roteiro de funcionamento do último fluxograma apresentado é bem simples.
Primeiro, no fragmento ConfigNewDeliveryAddressFragment, precisamos colocar a informação da posição real dele dentro do ViewPager.
Lembrando que o posicionamento dentro do ViewPager inicia em 0, ou seja, a segunda posição, que é a do fragmento ConfigNewDeliveryAddressFragment, é a de número 1.
Coloque em ConfigNewDeliveryAddressFragment a constante PAGER_POS como a seguir:
...
companion object{
const val TAB_TITLE = R.string.config_delivery_address_tab_new
/*
* A constante abaixo representa a posição
* deste fragmento no ViewPager. Os
* posicionamentos em ViewPager começam
* em 0.
* */
const val PAGER_POS = 1
}
...
Agora na atividade host dos fragmentos de gerência de endereços, vamos trabalhar o método onBackPressed() para saber quando remover ou não esta atividade da pilha de atividades do aplicativo.
Adicione ao final da classe ConfigDeliveryAddressesActivity os métodos a seguir:
...
override fun onBackPressed() {
val fragmentsInStack = supportFragmentManager.backStackEntryCount
/*
* Se houver algum fragmento em pilha de fragmentos
* e o fragmento atual em tela não for o fragment de
* formulário de novo endereço de entrega, então o
* próximo fragmento da pilha de fragmentos é que
* deve ser apresentado.
*
* Caso contrário, volte a atividade anterior via
* finish().
* */
if( fragmentsInStack > 0
&& isNewDeliveryAddressFormNotInScreen() ){
supportFragmentManager.popBackStack()
}
else {
finish()
}
}
private fun isNewDeliveryAddressFormNotInScreen() : Boolean
= view_pager.currentItem != ConfigNewDeliveryAddressFragment.PAGER_POS
...
O método isNewDeliveryAddressFormNotInScreen() foi adicionado para melhorar a leitura de código, não deixando o bloco condicional em onBackPressed() "inchado".
Por fim, ainda na ConfigDeliveryAddressesActivity, atualize o método onOptionsItemSelected() para invocar onBackPressed() no bloco condicional ao invés de finish() diretamente:
...
override fun onOptionsItemSelected( item: MenuItem ): Boolean {
if( item.itemId == android.R.id.home ){
onBackPressed()
return true
}
return super.onOptionsItemSelected(item)
}
...
A atualização acima é necessária, pois caso contrário o acionamento do BackButton da barra de topo, independente da aba em tela e do status dela, removerá a atividade atual, de gerência de endereços, da pilha de atividades, algo não esperado pelo usuário quando o fragmento de atualização de endereço estiver em tela.
Assim 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 clique em "Rebuid project". Ao final do rebuild execute o app em seu AVD Android.
Não esqueça de colocar o objeto user como um "usuário conectado". Este objeto se encontra na MainActivity:
...
val user = User(
"Thiengo Vinícius",
R.drawable.user,
true /* Usuário conectado. */
)
...
Acessando a tela de endereços cadastrados e excluindo o único endereço presente, temos:
Tentando a atualização de um endereço qualquer, temos:
Tentando a inserção de um novo endereço, temos:
Lembrando que nosso objetivo nesta primeira parte de projeto é terminar toda a interface gráfica do lado Android, por isso o "funcionamento limitado" de nossos testes.
Com isso finalizamos o último trecho da área de configurações de conta do usuário.
Antes de prosseguir para a conclusão, não esqueça de se inscrever na 📫 lista de e-mails do Blog para receber semanalmente as aulas do projeto Android BlueShoes e outros conteúdos do dev mobile.
Se inscreva também no canal do Blog em YouTube Thiengo.
Vídeos
A seguir os vídeos com o passo a passo do desenvolvimento da área de gerência de endereços de entrega de nosso app Android de mobile-commerce:
O código atual do projeto pode ser acessado no GitHub dele em: https://github.com/viniciusthiengo/blueshoes-kotlin-android.
Conclusão
Apesar da quantidade de códigos ainda duplicados, finalizamos nesta aula toda a área de configurações de conta do usuário final de nosso aplicativo Android de mobile-commerce.
Aqui também trabalhamos uma estratégia nova no Blog, que ainda não tinha sido abordada em nenhum outro artigo ou vídeo: o uso de fragmento host de outros fragmentos em janela de ViewPager.
Mais uma técnica para ficar como "carta na manga" em seus próprios projetos Android e em projetos futuros aqui do Blog.
Muito provavelmente nosso próximo passo é remover o máximo possível de códigos duplicados na área de configurações de conta e, obviamente, realizar alguns ajustes que forem necessários.
Caso você tenha dúvidas no projeto, ainda sobre esta 17ª aula, não deixe de comentar abaixo que logo eu lhe respondo.
Curtiu o conteúdo? Não esqueça de compartilha-lo. E, por fim, se inscreva na 📩 lista de e-mails, respondo às suas dúvidas também por lá.
Abraço.
Fontes
Android tutorial. How to replace fragments inside a ViewPager
How to Cache Fragments in Android
how to pass object from one fragment to another? - Resposta de Ankush Bist
Understanding Fragment's setRetainInstance(boolean) - Resposta de Alex Lockwood e Willi Mentzel
Comentários Facebook