André Alves de Lima

Talking about Software Development and more…

Aplicações Android com Xamarin – Parte 5 de N – Navegação entre telas

Muito dificilmente as nossas aplicações terão somente uma única tela. Em um cenário mais “normal“, nossas aplicações serão compostas de diversas telas e, consequentemente, nós teremos que implementar a navegação entre elas.

Na aplicação que estamos construindo na série de desenvolvimento Android com Xamarin, nós teremos duas telas. A primeira delas é a tela principal, que foi construída nos artigos anteriores e listará os gastos cadastrados na aplicação. Hoje nós construiremos a segunda tela da nossa aplicação, que servirá para editarmos as informações dos gastos existentes ou para criarmos novos gastos.

Além de criarmos o layout dessa nova tela, nós veremos também como implementar a navegação entre telas nas aplicações Android com Xamarin. E não será somente a navegação pura e simples, mas também a troca de informações entre elas. Achou interessante? Então vamos lá!

Acesse os artigos anteriores

Antes de continuar com a leitura deste artigo, acesse os artigos anteriores dessa série utilizando os links abaixo:

Parte 1: Prototipando a aplicação
Parte 2: Ambiente Xamarin Android e Hello World
Parte 3: Classes de modelo e primeira tela
Parte 4: Customizando o controle ListView

Adicionando um novo Layout

Para implementarmos a navegação entre telas na nossa aplicação, nós temos que, obviamente, criar uma nova tela. Como mencionei anteriormente (e como você pode ver no protótipo das telas da aplicação), a segunda tela servirá para editarmos gastos existentes ou criarmos gastos novos.

A primeira coisa que temos que fazer para criarmos uma nova tela em aplicações Android (seja em Xamarin ou java) é a criação do seu layout. As telas do Android são sempre divididas entre o arquivo de layout (axml) e a classe de Activity (código que implementa a lógica da tela, ou seja, o famoso “code behind“).

Para adicionarmos um novo arquivo de layout na nossa aplicação, temos que clicar com o botão direito na pasta “layout” e escolher a opção “Add => New Item“:

Na janela que se abre, escolhemos a opção “Android Layout” e damos o nome de “EditarGasto” para o layout que será criado:

Conforme definimos no protótipo da aplicação, essa tela consistirá de três labels, três campos (para editarmos as informações dos gastos – Data, Estabelecimento, Valor) e dois botões (para confirmar ou cancelar a operação). Para construirmos um layout nesse estilo, utilizaremos um LinearLayout com orientação vertical. Isso fará com que cada item adicionado no layout seja empilhado um embaixo do outro.

A parte dos campos é muito simples. Teremos uma combinação de TextView (que são os labels no Android) e EditText (que são as caixas de texto no Android) para cada campo. A única configuração diferente que faremos nos EditTexts será a propriedade “text” deles, que define o tipo de entrada de dados da caixa de texto. Para o campo “Data“, utilizaremos a opção “data“. Já para o campo “Estabelecimento“, utilizaremos a opção “text“. Por fim, para o campo “Valor“, utilizaremos a opção “numberDecimal“.

Além dos campos, nós temos também os dois botões, que deverão ser exibidos um ao lado do outro. Como estaremos trabalhando com um LinearLayout vertical, se nós simplesmente adicionarmos os botões depois dos campos, eles serão exibidos um embaixo do outro.

Para que os botões sejam exibidos um ao lado do outro, teremos que adicionar um outro LinearLayout dentro do LinearLayout “principal“. Esse segundo LinearLayout interno terá a orientação horizontal e uma soma de pesos (“weightSum“) igual a “2“. Com isso, se adicionarmos os botões dentro desse LinearLayout interno definindo o seu peso (“layout_weight“) igual a “1“, eles serão exibidos um ao lado do outro, dividindo igualitariamente a largura total disponível.

Veja como é que fica o código AXML do layout da nossa segunda tela:

<?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">
    <TextView
        android:text="Data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/textViewData" />
    <EditText
        android:inputType="date"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/editTextData" />
    <TextView
        android:text="Estabelecimento"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/textViewEstabelecimento" />
    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/editTextEstabelecimento"
        android:inputType="text" />
    <TextView
        android:text="Valor"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/textViewValor" />
    <EditText
        android:inputType="numberDecimal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/editTextValor" />
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:id="@+id/linearLayoutBotoes"
        android:weightSum="2">
        <Button
            android:text="Salvar"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:id="@+id/buttonSalvar"
            android:layout_weight="1" />
        <Button
            android:text="Cancelar"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:id="@+id/buttonCancelar"
            android:layout_weight="1" />
    </LinearLayout>
</LinearLayout>

Adicionando uma nova Activity

Como mencionei anteriormente, toda tela em aplicações Android é definida pelo conjunto de layout (AXML que criamos no passo anterior) e Activity (o “code behind” da tela). Agora que já criamos a parte visual da tela, vamos adicionar o nosso arquivo de Activity. Para isso, vamos clicar com o botão direito na raiz do nosso projeto e vamos escolher a opção “Add => New Item“:

Na janela de adição de novos itens, escolheremos a opção “Activity“, dando o nome de “EditarGastoActivity“:

Com isso, nós teremos um novo arquivo cs (C#) adicionado no nosso projeto. Dentro desse arquivo, temos a definição da Activity. A princípio, nós faremos duas alterações nesse arquivo. Primeiro, nós configuraremos um título para ela (decorando a classe com o atributo “Activity“, propriedade “Label“). Em seguida, nós adicionaremos a chamada para “SetContentView“, passando o layout “EditarGasto” que criamos na seção anterior:

    [Activity(Label = "Editar gasto")]
    public class EditarGastoActivity : Activity
    {
        protected override void OnCreate(Bundle savedInstanceState)
        {
            base.OnCreate(savedInstanceState);

            SetContentView(Resource.Layout.EditarGasto);
        }
    }

Essa chamada para “SetContentView” está basicamente falando para o mecanismo do Android carregar o layout “EditarGasto” para esse arquivo de Activity quando a tela for aberta. Como nós não definimos nenhuma lógica adicional (por enquanto), a janela será aberta com os campos em branco.

Abrindo a nova tela

Uma vez definidos os arquivos de layout e Activity da nova tela, vamos voltar para a tela principal, onde teremos que implementar a sua abertura. A ideia é que, ao clicarmos em um gasto na nossa ListView, a segunda tela deverá ser aberta mostrando as informações atuais do gasto clicado.

A minha primeira tentativa foi com o evento “ItemClick” do ListView, já que a maioria dos exemplos de ListView no Android que eu encontrei na internet trabalhavam com esse evento. Porém, como estamos trabalhando com um ExpandableListView, esse evento não é disparado ao clicarmos em um item filho da ListView.

Nesse caso, ao invés de utilizarmos o evento “ItemClick“, temos que utilizar o “ChildClick“. Dito isso, na última linha do método “OnCreate” da nossa Activity principal, vamos adicionar a seguinte linha de código:

            listViewGastos.ChildClick += ListViewGastos_ChildClick;

Se você não estiver familiarizado com essa sintaxe, a linha acima está dizendo que, quando o evento “ChildClick” for disparado pela ListView, o método “ListViewGastos_ChildClick” deverá ser executado. Vamos então adicionar agora o código desse método, que terá a princípio uma chamada para “StartActivity“, passando o tipo “EditarGastoActivity“, que é justamente a Activity que criamos na seção anterior:

        private void ListViewGastos_ChildClick(object sender, ExpandableListView.ChildClickEventArgs e)
        {
            StartActivity(typeof(EditarGastoActivity));
        }

Se executarmos agora a aplicação e clicarmos em algum gasto, veremos que a tela de edição de gastos será aberta, porém, ela estará vazia:

Esse é o comportamento esperado, já que nós ainda não implementamos a passagem de informações da tela principal para a segunda tela.

Passando informações da tela principal para a segunda tela

Para passarmos informações da tela principal para a tela de edição de gastos, teremos que primeiramente fazer algumas alterações no código da nossa MainActivity. Se dermos uma olhada no código final do último artigo, veremos que algumas variáveis estão sendo definidas somente dentro do método em que elas estão sendo utilizadas. Por exemplo, as listas de estabelecimentos, gastos e ListViewGroups não estão acessíveis para outros métodos dentro da MainAcvity.

Como nós precisaremos acessar essas informações em outras partes da Activity, nós teremos que promover essas variáveis para atributos da nossa classe MainActivity. Dito isso, vamos criar os quatro atributos privados no escopo da classe:

        private List<Models.Estabelecimento> _estabelecimentos;
        private List<Models.Gasto> _gastos;
        private List<ListViewGroup> _listViewGroups;
        private ListViewAdapter _adapter;

Em seguida, vamos alterar o código dos métodos “OnCreate” e “CarregarGastos“, de forma que eles utilizem esses atributos, ao invés de criar variáveis locais dentro desses métodos:

        protected override void OnCreate(Bundle bundle)
        {
            var culture = new System.Globalization.CultureInfo("de-DE");
            System.Threading.Thread.CurrentThread.CurrentCulture = culture;
            System.Threading.Thread.CurrentThread.CurrentUICulture = culture;

            base.OnCreate(bundle);

            _gastos = CarregarGastos();

            SetContentView(Resource.Layout.Main);

            var listViewGastos = FindViewById<ExpandableListView>(Resource.Id.listViewGastos);
            _listViewGroups = PrepararListViewGroups(_gastos);
            _adapter = new ListViewAdapter(this, _listViewGroups);
            listViewGastos.SetAdapter(_adapter);

            for (int group = 0; group <= _adapter.GroupCount - 1; group++)
            {
                listViewGastos.ExpandGroup(group);
            }

            listViewGastos.ChildClick += ListViewGastos_ChildClick;
        }

        private List<Models.Gasto> CarregarGastos()
        {
            _estabelecimentos = new List<Models.Estabelecimento>();
            for (int c = 1; c <= 10; c++)
            {
                _estabelecimentos.Add(new Models.Estabelecimento() { Id = c, Nome = string.Format("Estabelecimento {0}", c) });
            }

            var random = new Random();
            var gastos = new List<Models.Gasto>();
            for (int c = 1; c <= 15; c++)
            {
                var data = DateTime.Now.AddDays(random.Next(0, 3));
                var estabelecimento = _estabelecimentos[random.Next(0, _estabelecimentos.Count - 1)];
                var valor = random.Next(1, 50);
                gastos.Add(new Models.Gasto() { Id = c, Data = data, Estabelecimento = estabelecimento, Valor = valor });
            }

            return gastos;
        }

Uma vez realizados esses ajustes, nós limpamos a área para prosseguirmos com a implementação da navegação entre telas.

Como você pode perceber, com o código atual nós estamos simplesmente abrindo a segunda tela, mas nós não estamos passando nenhuma informação para ela. E como é que nós passamos informações de uma tela para outra no Android? Para fazermos isso, nós temos que utilizar os chamados “Intents“.

Através de um “Intent“, nós conseguimos não somente informar qual tela deve ser aberta, mas também, parâmetros adicionais que devem ser enviados no momento da abertura. O construtor da classe “Intent” espera um escopo (que será a tela origem, ou seja, “this“) e o tipo da tela que deverá ser aberta. Além disso, através do método “PutExtra“, nós conseguimos informar os valores que devem ser passados para a segunda tela, que são basicamente combinações de chave e valor.

Por fim, ao invés de simplesmente chamarmos o método “StartActivity” passando o tipo da tela que deve ser aberta, nós chamaremos esse método passando o “Intent“. Veja só como é que fica o código do nosso método “ListViewGastos_ChildClick” com essas alterações:

        private void ListViewGastos_ChildClick(object sender, ExpandableListView.ChildClickEventArgs e)
        {
            var infoGrupo = _listViewGroups[e.GroupPosition];
            var infoGasto = infoGrupo.ListViewItems[e.ChildPosition];
            var intent = new Intent(this, typeof(EditarGastoActivity));
            intent.PutExtra("Id", infoGasto.IdGasto);
            intent.PutExtra("Data", infoGrupo.Data.Ticks);
            intent.PutExtra("Estabelecimento", infoGasto.NomeEstabelecimento);
            intent.PutExtra("Valor", Convert.ToDouble(infoGasto.Valor));
            StartActivity(intent);
        }

Nota: como você pode perceber, nós não conseguimos passar todo e qualquer tipo de variável no “Intent”. Muitas vezes, se quisermos passar objetos complexos para a outra tela, nós teremos que serializar esses objetos em string e depois desserializar na segunda tela. Um exemplo de tipo que não é suportado pelo “Intent” é o DateTime. É por isso que nós estamos passando a informação de “Ticks” da data, pois com ela nós conseguimos reconstruir o objeto DateTime na segunda tela sem problema nenhum.

Uma vez incluídos os valores através do método “PutExtra” do “Intent“, eles estarão disponíveis no outro lado através dos métodos “Intent.Extras.Get*“. Por exemplo, para pegarmos o valor que foi passado para a chave “Data“, nós temos que chamar o método “Intent.Extras.GetLong“, passando a chave “Data” como parâmetro.

Veja como é que fica o código para recuperarmos as informações do “Intent” e jogarmos nos textos dos nossos campos da segunda tela:

        private int _idGasto;
        private EditText _editTextData;
        private EditText _editTextEstabelecimento;
        private EditText _editTextValor;

        protected override void OnCreate(Bundle savedInstanceState)
        {
            base.OnCreate(savedInstanceState);

            SetContentView(Resource.Layout.EditarGasto);

            _idGasto = Intent.Extras.GetInt("Id");

            var data = new DateTime(Intent.Extras.GetLong("Data"));
            _editTextData = FindViewById<EditText>(Resource.Id.editTextData);
            _editTextData.Text = data.ToShortDateString();

            var estabelecimento = Intent.Extras.GetString("Estabelecimento");
            _editTextEstabelecimento = FindViewById<EditText>(Resource.Id.editTextEstabelecimento);
            _editTextEstabelecimento.Text = estabelecimento;

            var valor = Intent.Extras.GetDouble("Valor");
            _editTextValor = FindViewById<EditText>(Resource.Id.editTextValor);
            _editTextValor.Text = valor.ToString(System.Globalization.CultureInfo.InvariantCulture);
        }

Execute a aplicação, clique em um gasto qualquer e veja o resultado:

Obtendo o retorno da segunda tela na tela principal

Até agora nós vimos como mandar informações da tela principal para a tela de edição de gastos. Mas, como é que fazemos para enviar informações de volta para a tela principal? Simples! Através de um “Intent” também!

Primeiramente, temos que alterar um pequeno detalhe na hora de abrirmos a tela de edição de gastos. Ao invés de utilizarmos simplesmente o método “StartActivity“, nós temos que utilizar o método “StartActivityForResult” no método “ListViewGastos_ChildClick“:

            StartActivityForResult(intent, 0);

Temos que utilizar esse método, pois essa é a maneira que nós conseguimos informar para o mecanismo do Android que a tela que está sendo aberta retornará informações para a tela de origem. Se nós simplesmente utilizarmos o método “StartActivity“, o Android concluirá que nós estamos abrindo uma segunda tela independente, que não retornará nenhuma informação para a tela chamadora.

Uma vez que nós utilizamos o método “StartActivityForResult” na tela principal, nós podemos utilizar o método “SetResult” na tela secundária para configurarmos os valores que serão retornados para a tela principal. Dito isso, vamos voltar para o arquivo “EditarGastoActivity” e, no final do método “OnCreate“, vamos adicionar o tratamento para o evento “Click” dos botões “Salvar” e “Cancelar“:

            var cancelar = FindViewById<Button>(Resource.Id.buttonCancelar);
            cancelar.Click += (s, e) => Finish();

            var salvar = FindViewById<Button>(Resource.Id.buttonSalvar);
            salvar.Click += Salvar_Click;

Como você pode perceber, o código para o clique do botão “Cancelar” é muito simples. Basta chamarmos o método “Finish” que a janela atual será fechada e o Android retornará para a tela anterior, enviando a informação que a operação foi cancelada. Já o código do clique do botão “Salvar” é um pouco mais elaborado, por isso vamos separá-lo no método “Salvar_Click“:

        private void Salvar_Click(object sender, EventArgs e)
        {
            var intent = new Intent();
            intent.PutExtra("Id", _idGasto);
            var data = Convert.ToDateTime(_editTextData.Text);
            intent.PutExtra("Data", data.Ticks);
            intent.PutExtra("Estabelecimento", _editTextEstabelecimento.Text);
            intent.PutExtra("Valor", Convert.ToDouble(_editTextValor.Text, System.Globalization.CultureInfo.InvariantCulture));

            SetResult(Result.Ok, intent);
            Finish();
        }

Veja que nós criamos um “Intent” (como fizemos para passar informações da tela principal para a tela secundária) e utilizamos o método “PutExtra” para colocarmos as informações atuais que estão presentes nos editores (ou seja, os novos valores informados pelo usuário). Em seguida, nós chamamos o método “SetResult” informando que a edição foi feita com sucesso (“Result.Ok“) e passando o “Intent” com as informações editadas. Por fim, nós chamamos o método “Finish” para fecharmos a tela atual e voltarmos para a tela anterior.

A última coisa que temos que implementar agora é o tratamento dessas informações retornadas na tela principal. Fazemos isso dando um “override” no método “OnActivityResult“. Um dos parâmetros desse método será o código do resultado (que nós passamos como “Result.Ok” do formulário secundário) e o “Intent” com as informações adicionais. Aí nós só temos que pegar essas informações, encontramos o gasto que foi alterado na nossa lista interna (atributo “_gastos“) e editamos as suas informações, bem como as informações do “ListViewItem“, de forma que o ListView seja atualizado também:

        protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent data)
        {
            base.OnActivityResult(requestCode, resultCode, data);

            if (resultCode == Result.Ok)
            {
                var id = data.Extras.GetInt("Id");
                var gasto = _gastos.FirstOrDefault(g => g.Id == id);

                if (gasto != null)
                {
                    gasto.Valor = Convert.ToDecimal(data.Extras.GetDouble("Valor"));
                    var nomeEstabelecimento = data.Extras.GetString("Estabelecimento");
                    var estabelecimento = _estabelecimentos.FirstOrDefault(e => string.Compare(e.Nome, nomeEstabelecimento, StringComparison.InvariantCultureIgnoreCase) == 0);
                    if (estabelecimento != null)
                    {
                        gasto.Estabelecimento = estabelecimento;
                    }

                    var listViewGroup = _listViewGroups.FirstOrDefault(lvg => lvg.Data.Date == gasto.Data.Date);
                    if (listViewGroup != null)
                    {
                        var listViewItem = listViewGroup.ListViewItems.FirstOrDefault(lvi => lvi.IdGasto == id);
                        listViewItem.Valor = gasto.Valor;
                        listViewItem.NomeEstabelecimento = gasto.Estabelecimento.Nome;
                        _adapter.NotifyDataSetChanged();
                    }
                }
            }
        }

Pendências para o próximo artigo

Como este artigo estava ficando muito longo, resolvi separar algumas pendências para uma próxima parte. O meu plano para o próximo artigo da série era implementar o armazenamento das informações em um banco de dados local (SQLite). Porém, ao invés disso, vou dedicar a próxima parte para “limparmos a casa” e resolvermos as seguintes pendências:

Utilizar DatePicker e Spinner
Até agora nós só utilizamos controles do tipo “EditText” na tela de edição de gastos. Preciso investigar como nós podemos utilizar um “DatePicker” para a data e um “Spinner” (que é tipo um ComboBox) para os estabelecimentos.

Botões de Salvar/Cancelar na parte inferior da tela
No protótipo da aplicação, os botões de “Salvar” e “Cancelar” da tela de edição de gastos estão posicionados na parte inferior da tela, e não logo depois dos editores.

Suporte à alteração de datas
Por enquanto eu não implementei o suporte à alteração das datas dos gastos. Isso porque, caso a data seja alterada, o gasto se encontrará em um outro grupo do ListView. Eu tenho que investigar como é que o ExpandableListView se comportará ao mudar o pai de um item filho.

Suporte à criação de novos gastos
Até agora nós só implementamos a alteração de gastos existentes (clicando nos itens do ListView). Ficou faltando implementar a criação de novos gastos, que será feita através de um botão “Novo Gasto” na tela principal.

Tentar otimizar o ListViewItem de forma que ele mantenha uma referência ao gasto
As informações dos gastos (como ID, nome do estabelecimento e valor) estão sendo repetidas na classe ListViewItem. Preciso investigar se não é possível manter somente uma referência ao gasto, implementando propriedades que retornam os valores do gasto, ao invés de ter que ficar copiando os valores das propriedades de um lado para o outro.

Acesse o código no GitHub

Normalmente eu disponibilizo os códigos de exemplo dos artigos e vídeos somente para as pessoas que se inscrevem na minha newsletter. Porém, como nessa série nós evoluiremos o projeto em cada artigo, achei mais interessante publicar o código no GitHub, onde todos poderão acompanhar a evolução do projeto quando novos artigos forem sendo publicados. Caso você se interesse, acesse o repositório desse projeto no meu GitHub.

Concluindo

No artigo de hoje você conferiu a criação da segunda tela da nossa aplicação de controle de gastos, reforçando o conceito que as telas em aplicações Android são divididas entre o layout (arquivo AXML) e Activity (code-behind da tela). Em seguida, vimos como abrir telas secundárias no Android, tanto de forma independente (sem troca de informações) como dependente (passando informações através de um “Intent“). Por fim, nós vimos como fazer o processo inverso de troca de informações, ou seja, como enviar valores de volta para o formulário principal.

Antes de fechar o artigo, eu listei algumas pendências que nós resolveremos na próxima parte dessa série, para evitar que esse artigo ficasse muito longo e difícil de acompanhar.

E você, já trabalhou com troca de informações entre telas no Android? Como é que você fez? Através de “Intents“, como apresentado nesse artigo? Ou existe alguma outra maneira mais fácil de fazer isso? Você tem alguma ideia de como eu consigo resolver as pendências que eu listei ali em cima? Fico aguardando as suas observações na caixa de comentários!

Por fim, convido você a inscrever-se na minha newsletter. Ao fazer isso, você receberá um e-mail toda semana sobre o artigo publicado e ficará sabendo também em primeira mão sobre o artigo da próxima semana, além de receber dicas “bônus” que eu só compartilho por e-mail. Inscreva-se utilizando o formulário logo abaixo.

Até a próxima!

André Lima

Newsletter do André Lima

* indicates required



Powered by MailChimp

10 thoughts on “Aplicações Android com Xamarin – Parte 5 de N – Navegação entre telas

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *