Programação para Dispositivos Móveis I

Fagno Alves Fonseca <fagno.fonseca@ifto.edu.br> Mestre em Modelagem Computacional de Sistemas – UFT.

1. Armazenamento no Android

Embora o armazenamento em banco de dados seja mais comum, o Android permite armazenamento em arquivos salvos na memória interna ou no SD card e também por um sistema de persistência de chave e valor chamado de preferências.

1.1. SharedPreferences

A classe SharedPreferences permite salvar dados simples no formato de chave e valor. Ela deve ser utilizada para salvar valores pequenos, como tipos primitivos e strings.

O método getSharedPreferences() busca um arquivo de preferência armazenado no dispostivo caso exista. No exemplo a seguir, buscamos pelo arquivo de nome "arquivo", utilizando o modo de leitura privado, indicado no segundo parâmetro. A seguir, buscamos uma String através da chave "nome".

Caso não exista a chave, ao efetuar o clique no botão imprimir, uma String com chave "nome" é criada tendo como valor o texto digitado no EditText.

Após o commit, que confirma a informação a ser salva no arquivo, atualizamos o texto digital no TextView. Comentários dentro do código foram inserido para facilitar o entendimento.

MainActivity.java
import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    SharedPreferences preferences;
    EditText nome;
    Button imprimir;
    TextView seuNome;
    String nomeusuario;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        nome = findViewById(R.id.nome);
        imprimir = findViewById(R.id.imprimir);
        seuNome = findViewById(R.id.seuNome);

        //busca por um arquivo existente armazenado no dispostivo
        preferences = getSharedPreferences("arquivo",Context.MODE_PRIVATE);
        //busca no arquivo um valor para a chave nome, se não existir recebe nulo
        nomeusuario = preferences.getString("nome",null);
        seuNome.setText(nomeusuario);

        imprimir.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(!nome.getText().toString().equals("")){
                    //cria um arquivo de preferência com o nome "arquivo" com modo de leitura "privado".
                    preferences = getSharedPreferences("arquivo",Context.MODE_PRIVATE);
                    //permite editar o arquivo
                    SharedPreferences.Editor editor = preferences.edit();
                    //inclui ao arquivo, uma String com "chave/valor".
                    editor.putString("nome",nome.getText().toString());
                    //faz com que a informação seja salva no arquivo
                    editor.commit();
                    //atualiza o TextView com o nome armazenado
                    nomeusuario = preferences.getString("nome",null);
                    seuNome.setText(nomeusuario);
                }else{
                    Toast.makeText(MainActivity.this,"Digite seu nome!",Toast.LENGTH_SHORT).show();
                }
            }
        });

    }
}
Ao salvar um valor com a classe SharedPreferences, é criado um objeto do tipo Editor e depois é chamado o métodos commit(), que efetiva as alterações no arquivo.

A seguir é apresentado o arquivo activity_main.xml criado para o exemplo.

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingLeft="10dp"
    android:paddingRight="10dp"
    android:paddingTop="60dp"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Nome?"
        tools:layout_editor_absoluteX="100dp"
        tools:layout_editor_absoluteY="146dp" />

    <EditText
        android:id="@+id/nome"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:ems="10"
        android:inputType="textPersonName"
        tools:layout_editor_absoluteX="100dp"
        tools:layout_editor_absoluteY="174dp" />

    <Button
        android:id="@+id/imprimir"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Imprimir"
        tools:layout_editor_absoluteX="100dp"
        tools:layout_editor_absoluteY="227dp" />

    <TextView
        android:id="@+id/seuNome"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="20dp"
        android:textAlignment="center" />

</LinearLayout>

Na figura a seguir, apresentamos a tela de exemplo do aplicativo. Ao fechar o aplicativo e abrir novamente você vai observar que o nome informado ainda permanece visível no campo de impressão. Se você desinstalar o aplicativo, o nome informado não ficará visível, pois o arquivo de preferência deixa de existir.

spexemplo
Figura 1. Tela do aplicativo

2. SQLite

Salvar dados em um banco de dados é ideal para dados estruturados ou que se repetem, por exemplo, os dados de contato. O Android tem suporte ao SQLite, um leve e poderoso banco de dados.

Basicamente, podemos dizer que existem duas maneiras de criar o banco de dados no aplicativo. Ou utilizamos a API e escrevemos os scripts SQL para criar as tabelas, ou criamos o banco de dados com um ferramenta externa e depois importamos o banco de dados já pronto no projeto. Em nosso exemplo, iremos utilizar a primeira opção.

As APIs necessárias para usar um banco de dados no Android estão disponíveis no pacote android.database.sqlite.

Um banco de dados é visível somente a aplicação que o criou.

2.1. Criando um Banco de Dados

Da mesma forma que você salva arquivos no armazenamento interno do dispositivo, o Android armazena seu banco de dados na pasta privada do app. Seus dados ficam protegidos porque, por padrão, essa área não pode ser acessada por outros apps nem pelo usuário.

A classe SQLiteOpenHelper contém um conjunto de APIs útil para gerenciar seu banco de dados. Quando você usa essa classe para conseguir referências para o banco de dados, o sistema realiza operações de execução possivelmente longas para criar e atualizar o banco de dados apenas quando necessário e não durante a inicialização do app. Você só precisa chamar getWritableDatabase() ou getReadableDatabase().

A seguir iremos definir a classe BDsqlite.java, que tem métodos responsáveis por criar o banco de dados, tabelas e demais operações no banco.

BDsqlite.java
package com.example.exemplo_sqlite.classes;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.widget.Toast;

import androidx.annotation.Nullable;

import java.util.ArrayList;
import java.util.List;

public class BDsqlite extends SQLiteOpenHelper {

    public static final String BD_NAME = "bdsqlite";
    //If you change the database schema, you must increment the database version.
    public static final int BD_VERSAO = 1;

    public BDsqlite(@Nullable Context context) {

        /**
         * super(Context context, String name, SQLiteDatabase.CursorFactory factory, int version)
         * @param context to use for locating paths to the the database This value may be null.
         * @param name of the database file, or null for an in-memory database This value may be null.
         * @param factory SQLiteDatabase.CursorFactory: to use for creating cursor objects, or null for the default This value may be null.
         * @param version int: number of the database (starting at 1); if the database is older, onUpgrade(SQLiteDatabase, int, int) 
         * will be used to upgrade the database; if the database is newer, onDowngrade(SQLiteDatabase, int, int) will be used to downgrade the database
         */
        super(context, BD_NAME, null, BD_VERSAO);
    }

    /**
     * Responsável por GERAR A TABELA TB_PESSOA NO BANCO
     */
    @Override
    public void onCreate(SQLiteDatabase db) {

        StringBuilder query = new StringBuilder();
        query.append("CREATE TABLE TB_PESSOA(");
        query.append(" ID INTEGER PRIMARY KEY AUTOINCREMENT,");
        query.append(" NOME TEXT NOT NULL,");
        query.append(" IDADE INT NOT NULL)");

        db.execSQL(query.toString());
    }

    /**
     * Called when the database needs to be upgraded. The implementation should use this method to drop tables, 
     * add tables, or do anything else it needs to upgrade to the new schema version.
     */
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }

}

Complemente a classe BDsqlite com o métodos descritos a seguir.

2.2. Inserindo informações no banco de dados

No método inserirDados() a seguir, o primeiro argumento de insert() é simplesmente o nome da tabela.

O segundo argumento diz ao framework o que fazer em casos em que os ContentValues estão vazios (ou seja, você não put nenhum valor). Se você especificar o nome de uma coluna, o framework incluirá uma linha e definirá o valor dessa coluna como nula. Se você especificar null, como nesse exemplo de código, o framework não incluirá uma linha quando não houver valores.

Os métodos insert() retornam o código da linha recém-criada ou, caso haja um erro ao inserir os dados, retornarão -1. Isso pode acontecer caso haja um conflito com os dados preexistentes no banco de dados.

BDsqlite.java
...
    /**
     * Responsável por INSERIR DADOS NO BANCO
     * Ao executar o método getWritableDatabase(), o método onCreate() é executado.
     * O método getWritableDatabase() é utilizado para as operações de insert, delete e update.
     */
    public void inserirDados(){

        SQLiteDatabase db = getWritableDatabase();

        ContentValues values = new ContentValues();
        values.put("NOME", "Pessoa 1"); // COLUNA / VALOR
        values.put("IDADE", "22");
        db.insert("TB_PESSOA",null,values);

    }

2.3. Lendo informações do banco de dados

Para ler informações de um banco de dados, use o método query(), transmitindo os seus critérios de seleção e as colunas desejadas. O método combina elementos de insert() e update(), mas a lista de colunas define os dados a serem buscados (a "projeção"), em vez dos dados a serem inseridos. Os resultados da consulta são retornados em um objeto Cursor.

Os terceiro e quarto argumentos (selection e selectionArgs) são combinados para criar uma cláusula WHERE. Como os argumentos são fornecidos separados da consulta de seleção, eles se perdem antes de serem combinados. Isso torna suas declarações de seleção imunes à injeção de SQL.

Para ver uma linha no cursor, use um dos métodos de movimento do Cursor, que sempre precisam ser chamados antes de iniciar a leitura de valores. Como o cursor começa na posição -1, ao chamar moveToNext(), a "posição de leitura" é colocada na primeira entrada nos resultados e retorna se o cursor já passou da última entrada no conjunto de resultados. Para cada linha, é possível ler o valor de uma coluna chamando um dos métodos "get" do cursor Cursor, por exemplo, getString() ou getLong(). Para cada um dos métodos "get", é necessário passar a posição de índice da coluna desejada, que pode ser encontrada chamando getColumnIndex() ou getColumnIndexOrThrow(). Quando a iteração dos resultados for concluída, chame close() no cursor para liberar seus recursos. Por exemplo, o exemplo a seguir mostra como conseguir todos os códigos de itens armazenados em um cursor e adicioná-los a uma lista

BDsqlite.java
...
    /**
     * Responsável por CONSULTAR DADOS NO BANCO
     * Para consultar dados, utilizamos o método getReadableDatabase()
     */
    public List<Pessoa> consultarDados(){
        SQLiteDatabase dbselec = getReadableDatabase();

        String[] colunas = {
                "ID",
                "NOME",
                "IDADE"
        };
        Cursor cursor = dbselec.query(
                "TB_PESSOA",   // The table to query
                colunas,       // The array of columns to return (pass null to get all)
                null,          // The columns for the WHERE clause
                null,          // The values for the WHERE clause
                null,          // don't group the rows
                null,          // don't filter by row groups
                null           // The sort order
        );

        List<Pessoa> pessoas = new ArrayList<>();
        while(cursor.moveToNext()) {
            Pessoa p=new Pessoa();
            p.setId(cursor.getInt(cursor.getColumnIndex("ID")));
            p.setNome(cursor.getString(cursor.getColumnIndex("NOME")));
            p.setIdade(Integer.parseInt(cursor.getString(cursor.getColumnIndex("IDADE"))));
            pessoas.add(p);
        }

        cursor.close();
        return pessoas;
    }

2.4. Excluindo informações do banco de dados

Para excluir linhas de uma tabela, é necessário fornecer critérios de seleção que identifiquem as linhas ao método delete(). O mecanismo funciona da mesma forma que os argumentos seleção para o método query(). Ele divide a especificação de seleção em uma cláusula de seleção e em argumentos de seleção. A cláusula define as colunas a serem verificadas e permite combinar testes de coluna. Os argumentos são valores para testes comparativos que são vinculados à cláusula. Como o resultado não é processado da mesma forma que uma instrução SQL comum, ele é imune à injeção de SQL.

O valor de retorno para o método delete() indica o número de linhas que foram excluídas do banco de dados.

BDsqlite.java
...
    /**
     * Responsável por EXCLUIR DADOS NO BANCO
     */
    public void excluir(int id){
        SQLiteDatabase db = getReadableDatabase();
        // Define 'where' part of query.
        String selection = "ID LIKE ?";
        // Specify arguments in placeholder order.
        String[] selectionArgs = { String.valueOf(id) };
        // Issue SQL statement.
        db.delete("TB_PESSOA", selection, selectionArgs);
    }

2.5. Atualizando o banco de dados

Quando precisar modificar um subconjunto dos valores do banco de dados, use o método update().

A atualização da tabela combina a sintaxe ContentValues de insert() com a sintaxe WHERE de delete().

O valor de retorno do método update() é o número de linhas afetadas no banco de dados.

BDsqlite.java
...
    /**
     * Responsável por ATUALIZAR DADOS NO BANCO
     */
    public void update(int id, String coluna, String valor){
        SQLiteDatabase db = getWritableDatabase();

        ContentValues values = new ContentValues();
        values.put(coluna, valor);

        // Which row to update, based on the title
        String selection = "ID LIKE ?";
        String[] selectionArgs = { String.valueOf(id) };

        int count = db.update(
            "TB_PESSOA",
            values,
            selection,
            selectionArgs);
    }

2.6. Conexão persistente do banco de dados

Visto que é caro chamar getWritableDatabase() e getReadableDatabase() quando o banco de dados está fechado, deixe a conexão do banco de dados aberta durante todo o período de tempo em que possivelmente será necessário acessá-lo. Normalmente, é ideal fechar o banco de dados no onDestroy() da atividade de chamada.

    @Override
    protected void onDestroy() {
        dbHelper.close();
        super.onDestroy();
    }

2.7. Executando a aplicação

Após definir nossa classe BDsqlite.java, no método onCreate() da sua Activity crie uma instância da classeBDsqlite.java e execute os métodos.

MainActivity.java
    BDsqlite bd = new BDsqlite(this);
    bd.inserirDados();
    ...

2.8. Ver os arquivos no dispositivo com o Device File Explorer

O Device File Explorer permite que você veja, copie e exclua arquivos em um dispositivo Android. Isso é útil ao examinar arquivos criados pelo app ou se você quiser transferir arquivos de e para um dispositivo.

a maioria dos dados do dispositivo não fica visível, a menos que você use um dispositivo com acesso root ou um emulador com uma imagem do sistema Android padrão (AOSP), e não uma API do Google ou uma imagem de sistema do Google Play. Ative a depuração USB quando usar um dispositivo conectado.

Para trabalhar com o sistema de arquivos de um dispositivo, faça o seguinte:

  1. Clique em View > Tool Windows > Device File Explorer ou no botão Device File Explorer na barra da janela de ferramentas para abrir o Device File Explorer.

  2. Selecione um dispositivo na lista suspensa.

  3. Interaja com o conteúdo do dispositivo na janela do File Explorer. Clique com o botão direito do mouse em um arquivo ou diretório para criar um novo, salvá-lo na sua máquina, fazer upload, excluir ou sincronizar. Clique duas vezes em um arquivo para abri-lo no Android Studio.

O Android Studio salvará os arquivos que você abrir dessa maneira em um diretório temporário fora do seu projeto. Se você fizer modificações em um arquivo aberto com o Device File Explorer e quiser salvá-las no dispositivo, será necessário fazer o upload manual da versão modificada do arquivo no dispositivo.

devicefileexplorer
Figura 2. Janela da ferramenta Device File Explorer

Ao analisar os arquivos de um dispositivo, os seguintes diretórios são particularmente úteis:

data/data/app_name/

Contém arquivos de dados para seu app armazenados no armazenamento interno

sdcard/
O banco de dados fica no diretório data/data/app_name/databases.

Contém arquivos do usuário salvos no armazenamento externo do usuário (imagens etc.)

nem todos os arquivos em um dispositivo de hardware ficam visíveis no Device File Explorer. Por exemplo, no diretório data/data/, as entradas correspondentes aos apps do dispositivo que não são depuráveis não podem ser expandidas no Device File Explorer.

2.9. Visualizando o banco de dados com um cliente SQLite

Caso você deseja utilizar algum software cliente de SQLite para visualizar o conteúdo do banco de dados, siga os passos do item 2.8 e baixe os arquivos da pasta databases conforme a seguir.

savefiledb

Após baixar os arquivos, abra o seu cliente SQLite, neste exemplo estou utilizando o DB Browser for SQLite, ferramenta que você pode baixar através do endereço https://sqlitebrowser.org/.

Ao abrir o arquivo do banco de dados, você irá ver as inforamções do banco conforme figura a seguir.

estruturabanco

A próxima figura apresenta os dados da tabela, funcionalidade disponível através do DB Browser.

navegardadostabela

3. Referências

  1. LECHETA, Ricardo R. Google Android: Aprenda a criar aplicações para dispositivos móveis com o Android SDK. 5ª ed. – São Paulo: Novatec, 2016.

  2. https://developer.android.com/training/data-storage/sqlite

  3. https://developer.android.com/studio/debug/device-file-explorer

  4. https://sqlitebrowser.org/