2022 最新 Android 基础教程,从开发入门到项目实战【b站动脑学院】学习笔记——第六章:数据存储

第 6 章 数据存储

本章介绍Android 4种存储方式的用法,包括共享参数SharedPreferences、数据库SQLite、存储卡文 件、App的全局内存,另外介绍Android重要组件—应用Application的基本概念与常见用法。最后,结 合本章所学的知识演示实战项目“购物车”的设计与实现。

6.1 共享参数SharedPreferences

本节介绍Android的键值对存储方式——共享参数SharedPreferences的使用方法,包括:如何将数据保存到共享参数,如何从共享参数读取数据,如何使用共享参数实现登录页面的记住密码功能,如何利用设备浏览器找到共享参数文件。

6.1.1 共享参数的用法

SharedPreferences是Android的一个轻量级存储工具,它采用的存储结构是Key-Value的键值对方式,类似于Java的Properties,二者都是把Key-Value的键值对保存在配置文件中。不同的是,Properties的文件内容形如Key=Value,而SharedPreferences的存储介质是XML文件,且以XML标记保存键值对。保存共享参数键值对信息的文件路径为:/data/data/应用包名/shared_prefs/文件名.xml。下面是一个

共享参数的XML文件例子:



Mr Lee 

 


基于XML格式的特点,共享参数主要用于如下场合:

( 1 )简单且孤立的数据。若是复杂且相互关联的数据,则要保存于关系数据库。

( 2 )文本形式的数据。若是二进制数据,则要保存至文件。

( 3 )需要持久化存储的数据。App退出后再次启动时,之前保存的数据仍然有效。

实际开发中,共享参数经常存储的数据包括:App的个性化配置信息、用户使用App的行为信息、临时需要保存的片段信息等。共享参数对数据的存储和读取操作类似于Map,也有存储数据的put方法,以及读取数据的get方法。调用getSharedPreferences方法可以获得共享参数实例,获取代码示例如下:

// 从share.xml获取共享参数实例
SharedPreferences shared = getSharedPreferences("share", MODE_PRIVATE);

由以上代码可知,getSharedPreferences方法的第一个参数是文件名,填share表示共享参数的文件名是share.xml;第二个参数是操作模式,填MODE_PRIVATE表示私有模式。往共享参数存储数据要借助于Editor类,保存数据的代码示例如下:

SharedPreferences.Editor editor = shared.edit();  // 获得编辑器的对象
editor.putString("name", "Mr Lee");  // 添加一个名为name的字符串参数
editor.putInt("age", 30);  // 添加一个名为age的整型参数
editor.putBoolean("married", true);  // 添加一个名为married的布尔型参数
editor.putFloat("weight", 100f);  // 添加一个名为weight的浮点数参数
editor.commit();  // 交编辑器中的修改

从共享参数读取数据相对简单,直接调用共享参数实例的get 方法即可读取键值,注意 get方法的第二个参数表示默认值,读取数据的代码示例如下:

String name = shared.getString ( "name.","");//从共享参数获取名为name的字符串 
int age = shared.getInt ("age",0);// 从共享参数获取名为age 的整型数
boolean married = shared.getBoolean ( "married", false);//从共享参数获取名为married 
的布尔数
float weight = shared.getFloat ( "weight",0);//从共享参数获取名为weight的浮点数

下面通过测试页面演示共享参数的存取过程,先在编辑页面录入用户注册信息,点击保存按钮把数据提交至共享参数,如图所示。再到查看页面浏览用户注册信息,App从共享参数中读取各项数据,并将注册信息显示在页面上,如图所示。

2022 最新 Android 基础教程,从开发入门到项目实战【b站动脑学院】学习笔记——第六章:数据存储_第1张图片

2022 最新 Android 基础教程,从开发入门到项目实战【b站动脑学院】学习笔记——第六章:数据存储_第2张图片

6.1.2 实现记住密码功能

上一章末尾的实战项目,登录页面下方有一个“记住密码”复选框,当时只是为了演示控件的用法,并未真正记住密码。因为用户退出后重新进入登录页面,App没有回忆起上次的登录密码。现在利用共享参数改造该项目,使之实现记住密码的功能。

改造内容主要有下列 3 处:

( 1 )声明一个共享参数对象,并在onCreate中调用getSharedPreferences方法获取共享参数的实例。

( 2 )登录成功时,如果用户勾选了“记住密码”,就使用共享参数保存手机号码与密码。也就是在

loginSuccess方法中增加以下代码:

// 如果勾选了“记住密码”,就把手机号码和密码都保存到共享参数中 
if (isRemember) {
   SharedPreferences.Editor editor = mShared.edit(); // 获得编辑器的对象
   editor.putString("phone", et_phone.getText().toString()); // 添加名叫phone的手机号码
   editor.putString("password", et_password.getText().toString()); // 添加名叫 
password的密码
   editor.commit(); // 交编辑器中的修改 
}

( 3 )再次打开登录页面时,App从共享参数读取手机号码与密码,并自动填入编辑框。也就是在onCreate方法中增加以下代码:

// 从share_login.xml获取共享参数对象
mShared = getSharedPreferences("share_login", MODE_PRIVATE); 
// 获取共享参数保存的手机号码
String phone = mShared.getString("phone", ""); 
// 获取共享参数保存的密码
String password = mShared.getString("password", "");
et_phone.setText(phone); // 往手机号码编辑框填写上次保存的手机号 
et_password.setText(password); // 往密码编辑框填写上次保存的密码

代码修改完毕,只要用户上次登录成功时勾选“记住密码”,下次进入登录页面后App就会自动填写上次登录的手机号码与密码。具体的效果如图所示。其中,用户首次登录成功的界面,此时勾选了“记住密码”;用户再次进入登录的界面,因为上次登录成功时已经记住密码,所以这次页面会自动填充保存的登录信息。

2022 最新 Android 基础教程,从开发入门到项目实战【b站动脑学院】学习笔记——第六章:数据存储_第3张图片

6.1.3 利用设备浏览器寻找共享参数文件

前面的“6.1.1 共享参数的基本用法”提到,参数文件的路径为“/data/data/应用包名/shared_prefs/* * .xml”,然而使用手机自带的文件管理器却找不到该路径,data下面只有空目录而已。这是因为手机厂商加了层保护,不让用户查看App的核心文件,否则万一不小心误删了,App岂不是运行报错了?当然作为开发者,只要打开了手机的USB调试功能,还是有办法拿到测试应用的数据文件。首先打开Android Studio,依次选择菜单Run→Run '**',把测试应用比如chapter06安装到手机上。接着单击Android Studio左下角的logcat标签,找到已连接的手机设备和测试应用,如图6-5所示。

注意到logcat窗口的右边,也就是Android Studio右下角有个竖排标签“Device File Explorer”,翻译过来叫设备文件浏览器。单击该标签按钮,此时主界面右边弹出名为“Device File Explorer”的窗口,如图6-6

Image From 笔记-Android 开发从入门到实战

在图6-6的窗口中依次展开各级目录,进到/data/data/com.example.chapter06/shared_prefs目录,在该目录下看到了参数文件share.xml。右击share.xml,并在右键菜单中选择“Save As”,把该文件保存到电脑中,之后就能查看详细的文件内容了。不仅参数文件,凡是保存在“/data/data/应用包名/”下面的所有文件,均可利用设备浏览器导出至电脑,下一节将要介绍的数据库db文件也可按照以上步骤导出。

6.2 数据库SQLite

本节介绍Android的数据库存储方式—SQLite的使用方法,包括:SQLite用到了哪些SQL语法,如何使用数据库管理器操纵SQLite,如何使用数据库帮助器简化数据库操作等,以及如何利用SQLite改进登录页面的记住密码功能。

6.2.1 SQL的基本语法

SQL本质上是一种编程语言,它的学名叫作“结构化查询语言”(全称为Structured Query Language,简称SQL)。不过SQL语言并非通用的编程语言,它专用于数据库的访问和处理,更像是一种操作命令,所以常说SQL语句而不说SQL代码。标准的SQL语句分为 3 类:数据定义、数据操纵和数据控制,但不同的数据库往往有自己的实现。

SQLite是一种小巧的嵌入式数据库,使用方便、开发简单。如同MySQL、Oracle那样,SQLite也采用SQL语句管理数据,由于它属于轻型数据库,不涉及复杂的数据控制操作,因此App开发只用到数据定义和数据操纵两类SQL。此外,SQLite的SQL语法与通用的SQL语法略有不同,接下来介绍的两类SQL语法全部基于SQLite。

1 .数据定义语言

数据定义语言全称Data Definition Language,简称DDL,它描述了怎样变更数据实体的框架结构。就SQLite而言,DDL语言主要包括 3 种操作:创建表格、删除表格、修改表结构,分别说明如下。

( 1 )创建表格

表格的创建动作由create命令完成,格式为“CREATE TABLE IF NOT EXISTS 表格名称(以逗号分隔的各字段定义);”。以用户信息表为例,它的建表语句如下所示:

CREATE TABLE IF NOT EXISTS user_info (
_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 
  name VARCHAR NOT NULL,
  age INTEGER NOT NULL, 
  height LONG NOT NULL, 
  weight FLOAT NOT NULL,
married INTEGER NOT NULL,
  update_time VARCHAR NOT NULL);

上面的SQL语法与其他数据库的SQL语法有所出入,相关的注意点说明见下:

①SQL语句不区分大小写,无论是create与table这类关键词,还是表格名称、字段名称,都不区分大小写。唯一区分大小写的是被单引号括起来的字符串值。

②为避免重复建表,应加上IF NOT EXISTS关键词,例如CREATE TABLE IF NOT EXISTS 表格名称…

③SQLite支持整型INTEGER、长整型LONG、字符串VARCHAR、浮点数FLOAT,但不支持布尔类型。布尔类型的数据要使用整型保存,如果直接保存布尔数据,在入库时SQLite会自动将它转为 0 或 1 ,其中 0表示false, 1 表示true。

④建表时需要唯一标识字段,它的字段名为 id 。创建新表都要加上该字段定义,例如id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL。

( 2 )删除表格
表格的删除动作由drop命令完成,格式为“DROP TABLE IF EXISTS 表格名称;”。下面是删除用户信息表的SQL语句例子:

DROP TABLE IF EXISTS user_info; 

( 3 )修改表结构

表格的修改动作由alter命令完成,格式为“ALTER TABLE 表格名称 修改操作;”。不过SQLite 只支持增加字段 ,不支持修改字段,也不支持删除字段。对于字段增加操作,需要在alter之后补充add命令,具体格式如“ALTER TABLE 表格名称 ADD COLUMN 字段名称 字段类型;”。下面是给用户信息表增加手机号字段的SQL语句例子:

ALTER TABLE user_info ADD COLUMN phone VARCHAR;

注意,SQLite的ALTER语句每次只能添加一列字段,若要添加多列,就得分多次添加。

2 .数据操纵语言

数据操纵语言全称Data Manipulation Language,简称DML,它描述了怎样处理数据实体的内部记录。表格记录的操作类型包括添加、删除、修改、查询 4 类,分别说明如下:

( 1 )添加记录

记录的添加动作由insert命令完成,格式为“INSERT INTO 表格名称(以逗号分隔的字段名列表)VALUES(以逗号分隔的字段值列表);”。下面是往用户信息表插入一条记录的SQL语句例子:

INSERT INTO user_info (name,age,height,weight,married,update_time) 
VALUES ('张三',20,170,50,0,'20200504');

( 2 )删除记录

记录的删除动作由delete命令完成,格式为“DELETE FROM 表格名称 WHERE 查询条件;”,其中查询条件的表达式形如“字段名=字段值”,多个字段的条件交集通过“AND”连接,条件并集通过“OR”连接。下面是从用户信息表删除指定记录的SQL语句例子:

DELETE FROM user_info WHERE name='张三';

( 3 )修改记录
记录的修改动作由update命令完成,格式为“UPDATE 表格名称 SET 字段名=字段值 WHERE 查询条件;”。下面是对用户信息表更新指定记录的SQL语句例子:

UPDATE user_info SET married=1 WHERE name='张三';

( 4 )查询记录

记录的查询动作由select命令完成,格式为“SELECT 以逗号分隔的字段名列表 FROM 表格名称 WHERE 查询条件;”。如果字段名列表填星号“*”,则表示查询该表的所有字段。下面是从用户信息表查询指定记录的SQL语句例子:

SELECT name FROM user_info WHERE name='张三';

查询操作除了比较字段值条件之外,常常需要对查询结果排序,此时要在查询条件后面添加排序条件,对应的表达式为“ORDER BY 字段名 ASC或者DESC”,意指对查询结果按照某个字段排序,其中ASC代表升序,DESC代表降序。下面是查询记录并对结果排序的SQL语句例子:

SELECT * FROM user_info ORDER BY age ASC;

如果读者之前不熟悉SQL语法,建议下载一个SQLite管理软件,譬如SQLiteStudio,先在电脑上多加练习SQLite的常见操作语句。

6.2.2 数据库管理器SQLiteDatabase

SQL语句毕竟只是SQL命令,若要在Java代码中操纵SQLite,还需专门的工具类。SQLiteDatabase便是Android提供的SQLite数据库管理器,开发者可以在活动页面代码调用openOrCreateDatabase方法获取数据库实例,参考代码如下:

(完整代码见chapter06\src\main\java\com\example\chapter06\DatabaseActivity.java)

// 创建名为test.db的数据库。数据库如果不存在就创建它,如果存在就打开它 
SQLiteDatabase db = openOrCreateDatabase(getFilesDir() + "/test.db", 
Context.MODE_PRIVATE, null);
String desc = String.format("数据库%s创建%s", db.getPath(), (db!=null)?"成功":"失 
败");
tv_database.setText(desc);
// deleteDatabase(getFilesDir() + "/test.db"); // 删除名为test.db数据库

首次运行测试App,调用openOrCreateDatabase方法会自动创建数据库,并返回该数据库的管理器实例,创建结果如图所示。
2022 最新 Android 基础教程,从开发入门到项目实战【b站动脑学院】学习笔记——第六章:数据存储_第4张图片

获得数据库实例之后,就能对该数据库开展各项操作了。数据库管理器SQLiteDatabase提供了若干操作数据表的API,常用的方法有 3 类,列举如下:

1 .管理类,用于数据库层面的操作

openDatabase:打开指定路径的数据库。

isOpen:判断数据库是否已打开。

close:关闭数据库。

getVersion:获取数据库的版本号。

setVersion:设置数据库的版本号。

2 .事务类,用于事务层面的操作

beginTransaction:开始事务。

setTransactionSuccessful:设置事务的成功标志。

endTransaction:结束事务。执行本方法时,系统会判断之前是否调用了

setTransactionSuccessful方法,如果之前已调用该方法就提交事务,如果没有调用该方法就回滚事务。

3 .数据处理类,用于数据表层面的操作

execSQL:执行拼接好的SQL控制语句。一般用于建表、删表、变更表结构。

delete:删除符合条件的记录。

update:更新符合条件的记录信息。

insert:插入一条记录。

query:执行查询操作,并返回结果集的游标。

rawQuery:执行拼接好的SQL查询语句,并返回结果集的游标。

在实际开发中,比较经常用到的是查询语句,建议先写好查询操作的select语句,再调用rawQuery方法执行查询语句。

6.2.3 数据库帮助器SQLiteOpenHelper

由于SQLiteDatabase存在局限性,一不小心就会重复打开数据库,处理数据库的升级也不方便;因此Android提供了数据库帮助器SQLiteOpenHelper,帮助开发者合理使用SQLite。SQLiteOpenHelper的具体使用步骤如下:

步骤一,新建一个继承自SQLiteOpenHelper的数据库操作类,按提示重写onCreate和onUpgrade两个方法。其中,onCreate方法只在第一次打开数据库时执行,在此可以创建表结构;而onUpgrade方法在数据库版本升高时执行,在此可以根据新旧版本号变更表结构。

步骤二,为保证数据库安全使用,需要封装几个必要方法,包括获取单例对象、打开数据库连接、关闭数据库连接,说明如下:

  • 获取单例对象:确保在App运行过程中数据库只会打开一次,避免重复打开引起错误。

  • 打开数据库连接:SQLite有锁机制,即读锁和写锁的处理;故而数据库连接也分两种,读连接可调用getReadableDatabase方法获得,写连接可调用getWritableDatabase获得。

  • 关闭数据库连接:数据库操作完毕,调用数据库实例的close方法关闭连接。

步骤三, 提供对表记录增加、删除、修改、查询的操作方法。能被SQLite直接使用的数据结构是ContentValues类,它类似于映射Map,也提供了put和get方法存取键值对。区别之处在于:ContentValues的键只能是字符串,不能是其他类型。ContentValues主要用于增加记录和更新记录,对应数据库的insert和update方法。记录的查询操作用到了游标类Cursor,调用query和rawQuery方法返回的都是Cursor对象,若要获取全部的查询结果,则需根据游标的指示一条一条遍历结果集合。Cursor的常用方法可分为 3 类,说明如下:

1 .游标控制类方法,用于指定游标的状态

close:关闭游标。

isClosed:判断游标是否关闭。

isFirst:判断游标是否在开头。

isLast:判断游标是否在末尾。

2 .游标移动类方法,把游标移动到指定位置

moveToFirst:移动游标到开头。

moveToLast:移动游标到末尾。

moveToNext:移动游标到下一条记录。

moveToPrevious:移动游标到上一条记录。

move:往后移动游标若干条记录。

moveToPosition:移动游标到指定位置的记录。

3 .获取记录类方法,可获取记录的数量、类型以及取值

getCount:获取结果记录的数量。

getInt:获取指定字段的整型值。

getLong:获取指定字段的长整型值。

getFloat:获取指定字段的浮点数值。

getString:获取指定字段的字符串值。

getType:获取指定字段的字段类型。

鉴于数据库操作的特殊性,不方便单独演示某个功能,接下来从创建数据库开始介绍,完整演示一下数据库的读写操作。

用户注册信息的演示页面包括两个,分别是记录保存页面和记录读取页面。
其中记录保存页面通过insert方法向数据库添加用户信息,完整代码见

                // 以下声明一个用户信息对象,并填写它的各字段值
                user = new User(name,
                        Integer.parseInt(age),
                        Long.parseLong(height),
                        Float.parseFloat(weight),
                        ck_married.isChecked());
                if (mHelper.insert(user) > 0) {
                    ToastUtil.show(this, "添加成功");
                }

而记录读取页面通过query方法从数据库读取用户信息,完整代码见

                List list = mHelper.queryAll();
                //List list = mHelper.queryByName(name);
                for (User u : list) {
                    Log.d("ning", u.toString());
                }

运行测试App,先打开记录保存页面,依次录入并将两个用户的注册信息保存至数据库。再打开记录读取页面,从数据库读取用户注册信息并展示在页面上,如图所示。
2022 最新 Android 基础教程,从开发入门到项目实战【b站动脑学院】学习笔记——第六章:数据存储_第5张图片

上述演示页面主要用到了数据库记录的添加、查询和删除操作,对应的数据库帮助器关键代码如下所示,尤其关注里面的insert、delete、update和query方法:

package com.dongnaoedu.chapter06.database;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.view.WindowAnimationFrameStats;

import com.dongnaoedu.chapter06.enity.User;

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

public class UserDBHelper extends SQLiteOpenHelper {

    private static final String DB_NAME = "user.db";
    private static final String TABLE_NAME = "user_info";
    private static final int DB_VERSION = 2;
    private static UserDBHelper mHelper = null;
    private SQLiteDatabase mRDB = null;
    private SQLiteDatabase mWDB = null;

    private UserDBHelper(Context context) {
        super(context, DB_NAME, null, DB_VERSION);
    }

    // 利用单例模式获取数据库帮助器的唯一实例
    public static UserDBHelper getInstance(Context context) {
        if (mHelper == null) {
            mHelper = new UserDBHelper(context);
        }
        return mHelper;
    }

    // 打开数据库的读连接
    public SQLiteDatabase openReadLink() {
        if (mRDB == null || !mRDB.isOpen()) {
            mRDB = mHelper.getReadableDatabase();
        }
        return mRDB;
    }

    // 打开数据库的写连接
    public SQLiteDatabase openWriteLink() {
        if (mWDB == null || !mWDB.isOpen()) {
            mWDB = mHelper.getWritableDatabase();
        }
        return mWDB;
    }

    // 关闭数据库连接
    public void closeLink() {
        if (mRDB != null && mRDB.isOpen()) {
            mRDB.close();
            mRDB = null;
        }

        if (mWDB != null && mWDB.isOpen()) {
            mWDB.close();
            mWDB = null;
        }
    }

    // 创建数据库,执行建表语句
    @Override
    public void onCreate(SQLiteDatabase db) {
        String sql = "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" +
                "_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
                " name VARCHAR NOT NULL," +
                " age INTEGER NOT NULL," +
                " height LONG NOT NULL," +
                " weight FLOAT NOT NULL," +
                " married INTEGER NOT NULL);";
        db.execSQL(sql);
    }
    //升级数据库会执行
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        String sql = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN phone VARCHAR;";
        db.execSQL(sql);
        sql = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN password VARCHAR;";
        db.execSQL(sql);
    }

    public long insert(User user) {
        ContentValues values = new ContentValues();
        values.put("name", user.name);
        values.put("age", user.age);
        values.put("height", user.height);
        values.put("weight", user.weight);
        values.put("married", user.married);
        // 执行插入记录动作,该语句返回插入记录的行号
        // 如果第三个参数values 为Null或者元素个数为0, 由于insert()方法要求必须添加一条除了主键之外其它字段为Null值的记录,
        // 为了满足SQL语法的需要, insert语句必须给定一个字段名 ,如:insert into person(name) values(NULL),
        // 倘若不给定字段名 , insert语句就成了这样: insert into person() values(),显然这不满足标准SQL的语法。
        // 如果第三个参数values 不为Null并且元素的个数大于0 ,可以把第二个参数设置为null 。
        //return mWDB.insert(TABLE_NAME, null, values);

        try {
            mWDB.beginTransaction();
            mWDB.insert(TABLE_NAME, null, values);
            //int i = 10 / 0;
            mWDB.insert(TABLE_NAME, null, values);
            mWDB.setTransactionSuccessful();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            mWDB.endTransaction();
        }

        return 1;
    }

    public long deleteByName(String name) {
        //删除所有
        //mWDB.delete(TABLE_NAME, "1=1", null);
        return mWDB.delete(TABLE_NAME, "name=?", new String[]{name});
    }

    public long update(User user) {
        ContentValues values = new ContentValues();
        values.put("name", user.name);
        values.put("age", user.age);
        values.put("height", user.height);
        values.put("weight", user.weight);
        values.put("married", user.married);
        return mWDB.update(TABLE_NAME, values, "name=?", new String[]{user.name});
    }

    public List queryAll() {
        List list = new ArrayList<>();
        // 执行记录查询动作,该语句返回结果集的游标
        Cursor cursor = mRDB.query(TABLE_NAME, null, null, null, null, null, null);
        // 循环取出游标指向的每条记录
        while (cursor.moveToNext()) {
            User user = new User();
            user.id = cursor.getInt(0);
            user.name = cursor.getString(1);
            user.age = cursor.getInt(2);
            user.height = cursor.getLong(3);
            user.weight = cursor.getFloat(4);
            //SQLite没有布尔型,用0表示false,用1表示true
            user.married = (cursor.getInt(5) == 0) ? false : true;
            list.add(user);
        }
        return list;
    }

    public List queryByName(String name) {
        List list = new ArrayList<>();
        // 执行记录查询动作,该语句返回结果集的游标
        Cursor cursor = mRDB.query(TABLE_NAME, null, "name=?", new String[]{name}, null, null, null);
        // 循环取出游标指向的每条记录
        while (cursor.moveToNext()) {
            User user = new User();
            user.id = cursor.getInt(0);
            user.name = cursor.getString(1);
            user.age = cursor.getInt(2);
            user.height = cursor.getLong(3);
            user.weight = cursor.getFloat(4);
            //SQLite没有布尔型,用0表示false,用1表示true
            user.married = (cursor.getInt(5) == 0) ? false : true;
            list.add(user);
        }
        return list;
    }
}

6.2.4 优化记住密码功能

在“6.1.2 实现记住密码功能”中,虽然使用共享参数实现了记住密码功能,但是该方案只能记住一个用户的登录信息,并且手机号码跟密码没有对应关系,如果换个手机号码登录,前一个用户的登录信息就被覆盖了。真正的记住密码功能应当是这样的:先输入手机号码,然后根据手机号码匹配保存的密码,一个手机号码对应一个密码,从而实现具体手机号码的密码记忆功能。现在运用数据库技术分条存储各用户的登录信息,并支持根据手机号查找登录信息,从而同时记住多个手机号的密码。具体的改造主要有下列 3 点:

( 1 )声明一个数据库的帮助器对象,然后在活动页面的onResume方法中打开数据库连接,在onPasue

方法中关闭数据库连接,示例代码如下:

private UserDBHelper mHelper; // 声明一个用户数据库的帮助器对象 
@Override
protected void onResume() { 
   super.onResume();
   mHelper = UserDBHelper.getInstance(this, 1); // 获得用户数据库帮助器的实例 
   mHelper.openWriteLink(); // 恢复页面,则打开数据库连接
}
@Override
protected void onPause() { 
   super.onPause();
   mHelper.closeLink(); // 暂停页面,则关闭数据库连接 
}
( 2 )登录成功时,如果用户勾选了“记住密码”,就将手机号码及其密码保存至数据库。也就是在

loginSuccess方法中增加如下代码:

// 如果勾选了“记住密码”,则把手机号码和密码保存为数据库的用户表记录 
if (isRemember) {
   UserInfo info = new UserInfo(); // 创建一个用户信息对象 
   info.phone = et_phone.getText().toString();
   info.password = et_password.getText().toString();
   info.update_time = DateUtil.getNowDateTime("yyyy-MM-dd HH:mm:ss"); 
   mHelper.insert(info); // 往用户数据库添加登录成功的用户信息
}
( 3 )再次打开登录页面,用户输入手机号再点击密码框的时候,App根据手机号到数据库查找登录信

息,并将记录结果中的密码填入密码框。其中根据手机号码查找登录信息,要求在帮助器代码中添加以

下方法,用于找到指定手机的登录密码:

// 根据手机号码查询指定记录
public UserInfo queryByPhone(String phone) { 
   UserInfo info = null;
   List infoList = query(String.format("phone='%s'", phone)); 
   if (infoList.size() > 0) { // 存在该号码的登录信息
       info = infoList.get(0); 
 }
   return info; 
}

此外,上面第 3 点的点击密码框触发查询操作,用到了编辑框的焦点变更事件,有关焦点变更监听器的详细用法参见第 5 章的“5.3.2 焦点变更监听器”。就本案例而言,光标切到密码框触发焦点变更事件,具体处理逻辑要求重写监听器的onFocusChange方法,重写后的方法代码如下所示:

@Override
public void onFocusChange(View v, boolean hasFocus) {
   String phone = et_phone.getText().toString();
   // 判断是否是密码编辑框发生焦点变化
   if (v.getId() == R.id.et_password) {
       // 用户已输入手机号码,且密码框获得焦点
       if (phone.length() > 0 && hasFocus) {
           // 根据手机号码到数据库中查询用户记录
           UserInfo info = mHelper.queryByPhone(phone);
           if (info != null) {
               // 找到用户记录,则自动在密码框中填写该用户的密码
               et_password.setText(info.password);
      }
    }
 } 
}

重新运行测试App,先打开登录页面,勾选“记住密码”,并确保本次登录成功。然后再次进入登录页面,输入手机号码后光标还停留在手机框,如图所示。接着点击密码框,光标随之跳到密码框,此时密码框自动填入了该号码对应的密码串,如图所示。由效果图可见,这次实现了真正意义上的记住密码功能。

6.3 存储卡的文件操作

本节介绍Android的文件存储方式—在存储卡上读写文件,包括:公有存储空间与私有存储空间有什么区别、如何利用存储卡读写文本文件、如何利用存储卡读写图片文件等。

6.3.1 私有存储空间与公共存储空间

为了更规范地管理手机存储空间,Android从7.0开始将存储卡划分为私有存储和公共存储两大部分,也就是分区存储方式,系统给每个App都分配了默认的私有存储空间。App在私有空间上读写文件无须任何授权,但是若想在公共空间读写文件,则要在AndroidManifest.xml里面添加下述的权限配置。


 

但是即使App声明了完整的存储卡操作权限,系统仍然默认禁止该App访问公共空间。
2022 最新 Android 基础教程,从开发入门到项目实战【b站动脑学院】学习笔记——第六章:数据存储_第6张图片

打开手机的系统设置界面,进入到具体应用的管理页面,会发现该应用的存储访问权限被禁止了。当然禁止访问只是不让访问存储卡的公共空间,App自身的私有空间依旧可以正常读写。这缘于Android把存储卡分成了两块区域,一块是所有应用均可访问的公共空间,另一块是只有应用自己才可访问的专享空间。虽然Android给每个应用都分配了单独的安装目录,但是安装目录的空间很紧张,所以Android在存储卡的“Android/data”目录下给每个应用又单独建了一个文件目录,用来保存应用自己需要处理的临时文件。这个目录只有当前应用才能够读写文件,其他应用是不允许读写的。由于私有空间本身已经加了访问权限控制,因此它不受系统禁止访问的影响,应用操作自己的文件目录自然不成问题。因为私有的文件目录只有属主应用才能访问,所以一旦属主应用被卸载,那么对应的目录也会被删掉

既然存储卡分为公共空间和私有空间两部分,它们的空间路径获取也就有所不同。若想获取公共空间的存储路径,调用的是Environment.getExternalStoragePublicDirectory方法;若想获取应用私有空间的存储路径,调用的是getExternalFilesDir方法。下面是分别获取两个空间路径的代码例子:

// 获取系统的公共存储路径 
String publicPath =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).t 
oString();
// 获取当前App的私有存储路径 
String privatePath =
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString(); 
boolean isLegacy = true;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
   // Android10的存储空间默认采取分区方式,此处判断是传统方式还是分区方式 
   isLegacy = Environment.isExternalStorageLegacy();
}
String desc = "系统的公共存储路径位于" + publicPath + 
   "\n\n当前App的私有存储路径位于" + privatePath + 
   "\n\nAndroid7.0之后默认禁止访问公共存储目录" +
   "\n\n当前App的存储空间采取" + (isLegacy?"传统方式":"分区方式"); 
tv_path.setText(desc);

该例子运行之后获得的路径信息如图所示,可见应用的私有空间路径位于“存储卡根目录/Android/data/应用包名/files/Download”这个目录中。

6.3.2 在存储卡上读写文本文件

文本文件的读写借助于文件IO流FileOutputStream和FileInputStream。其中,FileOutputStream用于写文件,FileInputStream用于读文件,它们读写文件的代码例子如下:

// 把字符串保存到指定路径的文本文件
public static void saveText(String path, String txt) { 
   // 根据指定的文件路径构建文件输出流对象
   try (FileOutputStream fos = new FileOutputStream(path)) { 
       fos.write(txt.getBytes()); // 把字符串写入文件输出流
  } catch (Exception e) { 
       e.printStackTrace(); 
 }
}
// 从指定路径的文本文件中读取内容字符串
public static String openText(String path) {
   String readStr = "";
   // 根据指定的文件路径构建文件输入流对象
   try (FileInputStream fis = new FileInputStream(path)) {
       byte[] b = new byte[fis.available()];
       fis.read(b); // 从文件输入流读取字节数组
       readStr = new String(b); // 把字节数组转换为字符串
  } catch (Exception e) {
       e.printStackTrace();
 }
   return readStr; // 返回文本文件中的文本字符串 
}

方式二:使用字符流存储读取文件

// 把字符串保存到指定路径的文本文件
    public static void saveText(String path, String txt) {
        BufferedWriter os = null;
        try {
            os = new BufferedWriter(new FileWriter(path));
            os.write(txt);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (os != null) {
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 从指定路径的文本文件中读取内容字符串
    public static String openText(String path) {
        BufferedReader is = null;
        StringBuilder sb = new StringBuilder();
        try {
            is = new BufferedReader(new FileReader(path));
            String line = null;
            while ((line = is.readLine()) != null) {
                sb.append(line);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return sb.toString();
    }

接着分别创建写文件页面和读文件页面,其中写文件页面调用saveText方法保存文本

而读文件页面调用readText方法从指定路径的文件中读取文本内容

然后运行测试App,先打开文本写入页面,录入注册信息后保存为私有目录里的文本文件,此时写入界面如图所示。再打开文本读取页面,App自动在私有目录下找到文本文件列表,并展示其中一个文件的文本内容,此时读取界面如图所示。

6.3.3 在存储卡上读写图片文件

文本文件读写可以转换为对字符串的读写,而图片文件保存的是图像数据,需要专门的位图工具Bitmap处理。位图对象依据来源不同又分成 3 种获取方式,分别对应位图工厂BitmapFactory的下列 3 种方法:

decodeResource:从指定的资源文件中获取位图数据。例如下面代码表示从资源文件huawei.png获取位图对象:

Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.huawei);

decodeFile:从指定路径的文件中获取位图数据。注意从Android 10开始,该方法只适用于私有目录下的图片,不适用公共空间下的图片。

decodeStream:从指定的输入流中获取位图数据。比如使用IO流打开图片文件,此时文件输入流

// 从指定路径的图片文件中读取位图数据
    public static Bitmap openImage(String path) {
        Bitmap bitmap = null;
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(path);
            bitmap = BitmapFactory.decodeStream(fis);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return bitmap;
    }

对象即可作为decodeStream方法的入参,读取代码如下:

// 从指定路径的图片文件中读取位图数据
public static Bitmap openImage(String path) {
   Bitmap bitmap = null; // 声明一个位图对象
   // 根据指定的文件路径构建文件输入流对象
   try (FileInputStream fis = new FileInputStream(path)) {
       bitmap = BitmapFactory.decodeStream(fis); // 从文件输入流中解码位图数据
  } catch (Exception e) {
       e.printStackTrace();
 }
   return bitmap; // 返回图片文件中的位图数据 
}

得到位图对象之后,就能在图像视图上显示位图。图像视图ImageView提供了下列方法显示各种来源的图片:

setImageResource:设置图像视图的图片资源,该方法的入参为资源图片的编号,形如“R.drawable.去掉扩展名的图片名称”。

setImageBitmap:设置图像视图的位图对象,该方法的入参为Bitmap类型。

setImageURI:设置图像视图的路径对象,该方法的入参为Uri类型。字符串格式的文件路径可通过代码“Uri.parse(file_path)”转换成路径对象。

读取图片文件的花样倒是挺多,把位图数据写入图片文件却只有一种,即通过位图对象的compress方法

将位图数据压缩到文件输出流。具体的图片写入代码如下所示:

// 把位图数据保存到指定路径的图片文件
    public static void saveImage(String path, Bitmap bitmap) {
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(path);
            // 把位图数据压缩到文件输出流中
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

接下来完整演示一遍图片文件的读写操作,首先创建图片写入页面,从某个资源图片读取位图数据,再把位图数据保存为私有目录的图片文件,相关代码示例如下:

// 获取当前App的私有下载目录
String path = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() + 
"/";
// 从指定的资源文件中获取位图对象
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.huawei); 
String file_path = path + DateUtil.getNowDateTime("") + ".jpeg";
FileUtil.saveImage(file_path, bitmap); // 把位图对象保存为图片文件 
tv_path.setText("图片文件的保存路径为:\n" + file_path);

然后创建图片读取页面,从私有目录找到图片文件,并挑出一张在图像视图上显示,相关代码示例如下:


// 获取当前App的私有下载目录
mPath = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() + "/"; 
// 获得指定目录下面的所有图片文件
mFilelist = FileUtil.getFileList(mPath, new String[]{".jpeg"}); 
if (mFilelist.size() > 0) {
   // 打开并显示选中的图片文件内容
   String file_path = mFilelist.get(0).getAbsolutePath();
   tv_content.setText("找到最新的图片文件,路径为"+file_path);
   // 显示存储卡图片文件的第一种方式:直接调用setImageURI方法
   //iv_content.setImageURI(Uri.parse(file_path)); // 设置图像视图的路径对象
   // 第二种方式:先调用BitmapFactory.decodeFile获得位图,再调用setImageBitmap方法
   //Bitmap bitmap = BitmapFactory.decodeFile(file_path);
   //iv_content.setImageBitmap(bitmap); // 设置图像视图的位图对象
   // 第三种方式:先调用FileUtil.openImage获得位图,再调用setImageBitmap方法
   Bitmap bitmap = FileUtil.openImage(file_path);
   iv_content.setImageBitmap(bitmap); // 设置图像视图的位图对象 
}

运行测试App,先打开图片写入页面,点击保存按钮把资源图片保存到存储卡,此时写入界面如图所示。再打开图片读取页面,App自动在私有目录下找到图片文件列表,并展示其中一张图片,此时读取界面如图所示。
2022 最新 Android 基础教程,从开发入门到项目实战【b站动脑学院】学习笔记——第六章:数据存储_第7张图片

6.4 应用组件Application

本节介绍Android重要组件Application的基本概念和常见用法。首先说明Application的生命周期贯穿了App的整个运行过程,接着利用Application实现App全局变量的读写,然后阐述了如何借助App实例来操作Room数据库框架。

6.4.1 Application的生命周期

Application是Android的一大组件,在App运行过程中有且仅有一个Application对象贯穿应用的整个生命周期。打开AndroidManifest.xml,发现activity节点的上级正是application节点,不过该节点并未指定name属性,此时App采用默认的Application实例。

注意到每个activity节点都指定了name属性,譬如常见的name属性值为.MainActivity,让人知晓该activity的入口代码是MainActivity.java。现在尝试给application节点加上name属性,看看其庐山真面目,具体步骤说明如下:

( 1 )打开AndroidManifest.xml,给application节点加上name属性,表示application的入口代码是MainApplication.java。修改后的application节点示例如下:


( 2 )在Java代码的包名目录下创建MainApplication.java,要求该类继承Application,继承之后可供重写的方法主要有以下 3 个。

  • onCreate:在App启动时调用。

  • onTerminate:在App终止时调用(按字面意思)。

  • onConfigurationChanged:在配置改变时调用,例如从竖屏变为横屏。

光看字面意思的话,与生命周期有关的方法是onCreate和onTerminate,那么重写这两个方法,并在重写后的方法中打印日志,修改后的Java代码如下所示:

(完整代码见chapter06\src\main\java\com\example\chapter06\MainApplication.java)

public class MainApplication extends Application {
   @Override
   public void onCreate() {
       super.onCreate();
       Log.d(TAG, "onCreate");
 }
   @Override
   public void onTerminate() {
       super.onTerminate();
       Log.d(TAG, "onTerminate");
 } 
}

( 3 )运行测试App,在logcat窗口观察应用日志。但是只在启动一开始看到MainApplication的onCreate日志(该日志先于MainActivity的onCreate日志),却始终无法看到它的onTerminate日志,无论是自行退出App还是强行杀掉App,日志都不会打印onTerminate。无论你怎么折腾,这个onTerminate日志都不会出来。Android明明提供了这个方法,同时提供了关于该方法的解释,说明文字如下:This method is for use in emulated process environments.It willnever be called on a production Android device, where processes are removed by simply killing them; no user code (including this callback) is executed when doing so。这段话的意思是:该方法供模拟环境使用,它在真机上永远不会被调用,无论是直接杀进程还是代码退出;执行该操作时,不会执行任何用户代码。现在很明确了,onTerminate方法就是个摆设,中看不中用。如果读者想在App退出前回收系统资源,就不能指望onTerminate方法的回调了。

6.4.2 Application操作全局变量

C/C++有全局变量的概念,因为全局变量保存在内存中,所以操作全局变量就是操作内存,显然内存的读写速度远比读写数据库或读写文件快得多。所谓全局,指的是其他代码都可以引用该变量,因此全局变量是共享数据和消息传递的好帮手。不过Java没有全局变量的概念,与之比较接近的是类里面的静态成员变量,该变量不但能被外部直接引用,而且它在不同地方引用的值是一样的(前提是在引用期间不能改动变量值),所以借助静态成员变量也能实现类似全局变量的功能。根据上一小节的介绍可知,Application的生命周期覆盖了App运行的全过程。不像短暂的Activity生命周期,一旦退出该页面,Activity实例就被销毁。因此,利用Application的全生命特性,能够在Application实例中保存全局变量。

适合在Application中保存的全局变量主要有下面 3 类数据:

( 1 )会频繁读取的信息,例如用户名、手机号码等。(不推荐把太多数据放在全局变量中,更不要用静态变量去存储)

( 2 )不方便由意图传递的数据,例如位图对象、非字符串类型的集合对象等。

( 3 )容易因频繁分配内存而导致内存泄漏的对象,例如Handler处理器实例等。

要想通过Application实现全局内存的读写,得完成以下 3 项工作:

( 1 )编写一个继承自Application的新类MainApplication。该类采用单例模式,内部先声明自身类的一个静态成员对象,在创建App时把自身赋值给这个静态对象,然后提供该对象的获取方法getInstance。

具体实现代码示例如下:

public class MainApplication extends Application {
   private final static String TAG = "MainApplication";
   private static MainApplication mApp; // 声明一个当前应用的静态实例
   // 声明一个公共的信息映射对象,可当作全局变量使用
   public HashMap infoMap = new HashMap();
   // 利用单例模式获取当前应用的唯一实例
   public static MainApplication getInstance() {
       return mApp;
 }
   @Override
   public void onCreate() {
       super.onCreate();
       Log.d(TAG, "onCreate");
       mApp = this; // 在打开应用时对静态的应用实例赋值
 } 
}

( 2 )在活动页面代码中调用MainApplication的getInstance方法,获得它的一个静态对象,再通过该对象访问MainApplication的公共变量和公共方法。

( 3 )不要忘了在AndroidManifest.xml中注册新定义的Application类名,也就是给application节点增加android:name属性,其值为.MainApplication。

接下来演示如何读写内存中的全局变量,首先分别创建写内存页面和读内存页面,其中写内存页面把用户的注册信息保存到全局变量infoMap,完整代码见

chapter06\src\main\java\com\example\chapter06\AppWriteActivity.java;

String name = et_name.getText().toString();
        String age = et_age.getText().toString();
        String height = et_height.getText().toString();
        String weight = et_weight.getText().toString();
        app = MyApplication.getInstance();
        app.infoMap.put("name", name);
        app.infoMap.put("age", age);
        app.infoMap.put("height", height);
        app.infoMap.put("weight", weight);
        app.infoMap.put("married", ck_married.isChecked() ? "是" : "否");

而读内存页面从全局变量infoMap读取用户的注册信息,完整代码见

chapter06\src\main\java\com\example\chapter06\AppReadActivity.java。

private void reload() {
        String name = app.infoMap.get("name");
        if (name == null) {
            return;
        }
        String age = app.infoMap.get("age");
        String height = app.infoMap.get("height");
        String weight = app.infoMap.get("weight");
        String married = app.infoMap.get("married");
        et_name.setText(name);
        et_age.setText(age);
        et_height.setText(height);
        et_weight.setText(weight);
        if ("是".equals(married)) {
            ck_married.setChecked(true);
        } else {
            ck_married.setChecked(false);
        }
    }

然后运行测试App,先打开内存写入页面,录入注册信息后保存至全局变量,此时写入界面如图所示。再打开内存读取页面,App自动从全局变量获取注册信息,并展示拼接后的信息文本,此时读取界面如图所示。

6.4.3 利用Room简化数据库操作

虽然Android提供了数据库帮助器,但是开发者在进行数据库编程时仍有诸多不便,比如每次增加一张新表,开发者都得手工实现以下代码逻辑:

( 1 )重写数据库帮助器的onCreate方法,添加该表的建表语句。

( 2 )在插入记录之时,必须将数据实例的属性值逐一赋给该表的各字段。

( 3 )在查询记录之时,必须遍历结果集游标,把各字段值逐一赋给数据实例。

( 4 )每次读写操作之前,都要先开启数据库连接;读写操作之后,又要关闭数据库连接。

上述的处理操作无疑存在不少重复劳动,数年来引得开发者叫苦连连。为此各类数据库处理框架纷纷涌现,包括GreenDao、OrmLite、Realm等,可谓百花齐放。眼见SQLite渐渐乏人问津,谷歌公司干脆整了个自己的数据库框架—Room,该框架同样基于SQLite,但它通过注解技术极大地简化了数据库操作,减少了原来相当一部分编码工作量。

由于Room并未集成到SDK中,而是作为第三方框架提供,因此要修改模块的build.gradle文件,往dependencies节点添加下面两行配置,表示导入指定版本的Room库:

implementation 'androidx.room:room-runtime:2.2.5' 
annotationProcessor 'androidx.room:room-compiler:2.2.5'

导入Room库之后,还要编写若干对应的代码文件。以录入图书信息为例,此时要对图书信息表进行增删改查,则具体的编码过程分为下列 5 个步骤:

1 .编写图书信息表对应的实体类假设图书信息类名为BookInfo,且它的各属性与图书信息表的各字段一一对应,那么要给该类添加“@Entity”注解,表示该类是Room专用的数据类型,对应的表名称也叫BookInfo。如果BookInfo表的name字段是该表的主键,则需给BookInfo类的name属性添加“@PrimaryKey”与“@NonNull”两个注解,表示该字段是个非空的主键。下面是BookInfo类的定义代码例子:

(完整代码见chapter06\src\main\java\com\example\chapter06\entity\BookInfo.java)

//书籍信息
@Entity
public class BookInfo {

@PrimaryKey(autoGenerate = true)
private int id;

private String name; // 书籍名称
private String author; // 作者
private String press; // 出版社
private double price; // 价格

public int getId() {
    return id;
}

public void setId(int id) {
    this.id = id;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public String getAuthor() {
    return author;
}

public void setAuthor(String author) {
    this.author = author;
}

public String getPress() {
    return press;
}

public void setPress(String press) {
    this.press = press;
}

public double getPrice() {
    return price;
}

public void setPrice(double price) {
    this.price = price;
}

@Override
public String toString() {
    return "BookInfo{" +
            "id=" + id +
            ", name='" + name + '\'' +
            ", author='" + author + '\'' +
            ", press='" + press + '\'' +
            ", price=" + price +
            '}';
}

}
2 .编写图书信息表对应的持久化类所谓持久化,指的是将数据保存到磁盘而非内存,其实等同于增删改等SQL语句。假设图书信息表的持久化类名叫作BookDao,那么该类必须添加“@Dao”注解,内部的记录查询方法必须添加“@Query”注解,记录插入方法必须添加“@Insert”注解,记录更新方法必须添加“@Update”注解,记录删除方法必须添加“@Delete”注解(带条件的删除方法除外)。对于记录查询方法,允许在@Query之后补充具体的查询语句以及查询条件;对于记录插入方法与记录更新方法,需明确出现重复记录时要采取哪种处理策略。下面是BookDao类的定义代码例子:

@Dao
public interface BookDao {

    @Insert
    void insert(BookInfo... book);

    @Delete
    void delete(BookInfo... book);

    // 删除所有书籍信息
    @Query("DELETE FROM BookInfo")
    void deleteAll();

    @Update
    int update(BookInfo... book);

    // 加载所有书籍信息
    @Query("SELECT * FROM BookInfo")
    List queryAll();

    // 根据名字加载书籍
    @Query("SELECT * FROM BookInfo WHERE name = :name ORDER BY id DESC limit 1")
    BookInfo queryByName(String name);
}

3 .编写图书信息表对应的数据库类因为先有数据库然后才有表,所以图书信息表还得放到某个数据库里,这个默认的图书数据库要从RoomDatabase派生而来,并添加“@Database”注解。下面是数据库类BookDatabase的定义代码例子:

//entities表示该数据库有哪些表,version表示数据库的版本号
//exportSchema表示是否导出数据库信息的json串,建议设为false,若设为true还需指定json文件的保存路径
@Database(entities = {BookInfo.class}, version = 1, exportSchema = true)
public abstract class BookDatabase extends RoomDatabase {
    // 获取该数据库中某张表的持久化对象
    public abstract BookDao bookDao();
}

4 .在自定义的Application类中声明图书数据库的唯一实例为了避免重复打开数据库造成的内存泄漏问题,每个数据库在App运行过程中理应只有一个实例,此时要求开发者自定义新的Application类,在该类中声明并获取图书数据库的实例,并将自定义的Application类设为单例模式,保证App运行之时有且仅有一个应用实例。下面是自定义Application类的代码例子:

(完整代码见chapter06\src\main\java\com\example\chapter06\MainApplication.java)

public class MainApplication extends Application {
   private final static String TAG = "MainApplication";
   private static MainApplication mApp; // 声明一个当前应用的静态实例
   private BookDatabase bookDatabase; // 声明一个书籍数据库对象
   // 利用单例模式获取当前应用的唯一实例
   public static MainApplication getInstance() {
       return mApp;
 }
   @Override
   public void onCreate() {
       super.onCreate();
       Log.d(TAG, "onCreate");
       mApp = this; // 在打开应用时对静态的应用实例赋值
       // 构建书籍数据库的实例
       bookDatabase = Room.databaseBuilder(mApp, BookDatabase.class,"book")
              .addMigrations() // 允许迁移数据库(发生数据库变更时,Room默认删除原数据库再创建新数据库。如此一来原来的记录会丢失,故而要改为迁移方式以便保存原有记录)
              .allowMainThreadQueries() // 允许在主线程中操作数据库(Room默认不能在主线程中操作数据库)
              .build(); 
 }
   // 获取书籍数据库的实例
   public BookDatabase getBookDB(){ 
       return bookDatabase;
 } 
}

5 .在操作图书信息表的地方获取数据表的持久化对象持久化对象的获取代码很简单,只需下面一行代码就够了:

// 从App实例中获取唯一的图书持久化对象
BookDao bookDao = MainApplication.getInstance().getBookDB().bookDao();

完成以上 5 个编码步骤之后,接着调用持久化对象的queryXXX、insertXXX、updateXXX、deleteXXX等方法,就能实现图书信息的增删改查操作了。例程的图书信息演示页面有两个,分别是记录保存页面和记录读取页面,其中记录保存页面通过insertOneBook方法向数据库添加图书信息。

运行测试App,先打开记录保存页面,依次录入两本图书信息并保存至数据库,如图所示。再打开记录读取页面,从数据库读取图书信息并展示在页面上,如图所示。

2022 最新 Android 基础教程,从开发入门到项目实战【b站动脑学院】学习笔记——第六章:数据存储_第8张图片

6.5 实战项目:购物车

2022 最新 Android 基础教程,从开发入门到项目实战【b站动脑学院】学习笔记——实战三:购物车

6.6 小结

本章主要介绍了Android常用的几种数据存储方式,包括共享参数SharedPreferences的键值对存取、数据库SQLite的关系型数据存取、存储卡的文件读写操作(含文本文件读写和图片文件读写)、App全局内存的读写,以及为实现全局内存而学习的Application组件的生命周期及其用法。最后设计了一个实战项目“购物车”,通过该项目的编码进一步复习巩固本章几种存储方式的使用。

通过本章的学习,我们应该能够掌握以下 4 种开发技能:

( 1 )学会使用共享参数存取键值对数据。

( 2 )学会使用SQLite存取数据库记录。

( 3 )学会使用存储卡读写文本文件和图片文件。

( 4 )学会应用组件Application的用法。

6.7 课后练习题

一、填空题

1 .SharedPreferences采用的存储结构是 __ 的键值对方式。

2 .Android可以直接操作的数据库名为 __ 。

3 . __ 是Android提供的SQLite数据库管理器。

4 .数据库记录的修改动作由 __ 命令完成。

5 .为了确保在App运行期间只有唯一的Application实例,可以采取 __ __模式实现。

二、判断题(正确打√,错误打×)

1 .共享参数只能保存字符串类型的数据。(  )

2 .SQLite可以直接读写布尔类型的数据。(  )

3 .从Android 7.0开始,系统默认禁止App访问公共存储空间。(  )

4 .App在私有空间上读写文件无须任何授权。(  )

5 .App终止时会调用Application的onTerminate方法。(  )

三、选择题

1 .(  )不是持久化的存储方式。

A.共享参数

B.数据库

C.文件

D.全局变量

2 .DDL语言包含哪些数据库操作(  )。

A.创建表格

B.删除表格

C.清空表格

D.修改表结构

3 .调用(  )方法会返回结果集的Cursor对象。

A.update

B.insert

C.query

D.rawQuery

4 .位图工厂BitmapFactory的(  )方法支持获取图像数据。

A.decodeStream

B.decodeFile

C.decodeImage

D.decodeResource

5 .已知某个图片文件的存储卡路径,可以调用(  )方法将它显示到图像视图上。

A.setImageBitmap

B.setImageFile

C.setImageURI

D.setImageResource

四、简答题

请简要描述共享参数与数据库两种存储方式的主要区别。

五、动手练习

1 .请上机实验完善找回密码项目的记住密码功能,分别采用以下两种存储方式:

( 1 )使用共享参数记住上次登录成功时输入的用户名和密码。

( 2 )使用SQLite数据库记住用户名对应的密码,也就是根据用户名自动填写密码。

2 .请上机实验本章的购物车项目,要求实现下列功能:

( 1 )往购物车添加商品。

( 2 )自动计算购物车中所有商品的总金额。

( 3 )移除购物车里的某个商品。

( 4 )清空购物车。

你可能感兴趣的:(Android,android,学习,sqlite)