第6章 数据存储

第 6 章 数据存储

bilibili学习地址
github代码地址
本章介绍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文件例子:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="name">Mr Lee</string> 
<int nane="age" value="30"/>
<boolean name="married" value="true" /> 
<float name="weight" value="100.0"/>
</map>

基于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类,保存数据的代码示例如下:

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

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***方法

的第二个参数表示默认值,读取数据的代码示例如下:

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

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的浮点数

下面通过测试页面演示共享参数的存取过程,先在编辑页面录入用户注册信息,点击保存按钮把数据提

交至共享参数,如图6-1所示。再到查看页面浏览用户注册信息,App从共享参数中读取各项数据,并将

注册信息显示在页面上,如图6-2所示。

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

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

6.1.2 实现记住密码功能

上一章末尾的实战项目,登录页面下方有一个“记住密码”复选框,当时只是为了演示控件的用法,并未

真正记住密码。因为用户退出后重新进入登录页面,App没有回忆起上次的登录密码。现在利用共享参

数改造该项目,使之实现记住密码的功能。

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

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

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

loginSuccess方法中增加以下代码:

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

// 如果勾选了“记住密码”,就把手机号码和密码都保存到共享参数中 
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就会自动填写上次登

录的手机号码与密码。具体的效果如图6-3和图6-4所示。其中,图6-3为用户首次登录成功的界面,此时

勾选了“记住密码”;图6-4为用户再次进入登录的界面,因为上次登录成功时已经记住密码,所以这次页

面会自动填充保存的登录信息。

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

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

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方法会自动创建数据库,并返回该数据库的管理器实

例,创建结果如图6-7所示。

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

获得数据库实例之后,就能对该数据库开展各项操作了。数据库管理器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方法向数据库添加用户信息,完整代码见

    chapter06\src\main\java\com\example\chapter06\SQLiteHelperActivity.java;而记录读取页面通过

    query方法从数据库读取用户信息,完整代码见

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

    运行测试App,先打开记录保存页面,依次录入并将两个用户的注册信息保存至数据库,如图6-8和图6

    9 所示。再打开记录读取页面,从数据库读取用户注册信息并展示在页面上,如图6-10所示。

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

上述演示页面主要用到了数据库记录的添加、查询和删除操作,对应的数据库帮助器关键代码如下所

示,尤其关注里面的insert、delete、update和query方法:

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

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<User> queryAll() {
        List<User> 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<User> queryByName(String name) {
        List<User> 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

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

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

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<UserInfo> 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-11所示。接着点击密码框,光标随之跳到密码框,此时密

码框自动填入了该号码对应的密码串,如图6-12所示。由效果图可见,这次实现了真正意义上的记住密

码功能。

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

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

6.3 存储卡的文件操作

本节介绍Android的文件存储方式—在存储卡上读写文件,包括:公有存储空间与私有存储空间有什么

区别、如何利用存储卡读写文本文件、如何利用存储卡读写图片文件等。

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

为了更规范地管理手机存储空间,Android从7.0开始将存储卡划分为私有存储和公共存储两大部分,也

就是分区存储方式,系统给每个App都分配了默认的私有存储空间。App在私有空间上读写文件无须任

何授权,但是若想在公共空间读写文件,则要在AndroidManifest.xml里面添加下述的权限配置。

<!-- 存储卡读写 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> 
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAG" />

但是即使App声明了完整的存储卡操作权限,系统仍然默认禁止该App访问公共空间。打开手机的系统

设置界面,进入到具体应用的管理页面,会发现该应用的存储访问权限被禁止了。

当然禁止访问只是不让访问存储卡的公共空间,App自身的私有空间依旧可以正常读写。这缘于

Android把存储卡分成了两块区域,一块是所有应用均可访问的公共空间,另一块是只有应用自己才可

访问的专享空间。虽然Android给每个应用都分配了单独的安装目录,但是安装目录的空间很紧张,所

以Android在存储卡的“Android/data”目录下给每个应用又单独建了一个文件目录,用来保存应用自己

需要处理的临时文件。这个目录只有当前应用才能够读写文件,其他应用是不允许读写的。由于私有空

间本身已经加了访问权限控制,因此它不受系统禁止访问的影响,应用操作自己的文件目录自然不成问

题。因为私有的文件目录只有属主应用才能访问,所以一旦属主应用被卸载,那么对应的目录也会被删

掉。

既然存储卡分为公共空间和私有空间两部分,它们的空间路径获取也就有所不同。若想获取公共空间的

存储路径,调用的是Environment.getExternalStoragePublicDirectory方法;若想获取应用私有空间的

存储路径,调用的是getExternalFilesDir方法。下面是分别获取两个空间路径的代码例子:

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

// 获取系统的公共存储路径 
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);

该例子运行之后获得的路径信息如图6-14所示,可见应用的私有空间路径位于“存储卡根目

录/Android/data/应用包名/files/Download”这个目录中。

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

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

文本文件的读写借助于文件IO流FileOutputStream和FileInputStream。其中,FileOutputStream用于

写文件,FileInputStream用于读文件,它们读写文件的代码例子如下:

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

// 把字符串保存到指定路径的文本文件
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方法保存文本,完整代码见

chapter06\src\main\java\com\example\chapter06\FileWriteActivity.java;而读文件页面调用

readText方法从指定路径的文件中读取文本内容,完整代码见

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

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

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

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方法的入参,相应的图片读取代码如下:

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

// 从指定路径的图片文件中读取位图数据
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();
                }
            }
        }
    }

接下来完整演示一遍图片文件的读写操作,首先创建图片写入页面,从某个资源图片读取位图数据,再

把位图数据保存为私有目录的图片文件,相关代码示例如下:

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

// 获取当前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);

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

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

// 获取当前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,先打开图片写入页面,点击保存按钮把资源图片保存到存储卡,此时写入界面如图6-17

所示。再打开图片读取页面,App自动在私有目录下找到图片文件列表,并展示其中一张图片,此时读

取界面如图6-18所示。

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

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

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节点示例如下:

(完整代码见chapter06\src\main\AndroidManifest.xml)

<application
       android:name=".MainApplication" 
       android:icon="@mipmap/ic_launcher" 
       android:label="@string/app_name" 
       android:theme="@style/AppTheme">

( 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 will

never 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。

具体实现代码示例如下:

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

public class MainApplication extends Application {
   private final static String TAG = "MainApplication";
   private static MainApplication mApp; // 声明一个当前应用的静态实例
   // 声明一个公共的信息映射对象,可当作全局变量使用
   public HashMap<String, String> infoMap = new HashMap<String, String>();
   // 利用单例模式获取当前应用的唯一实例
   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,先打开内存写入页面,录入注册信息后保存至全局变量,此时写入界面如图6-19所

示。再打开内存读取页面,App自动从全局变量获取注册信息,并展示拼接后的信息文本,此时读取界

面如图6-20所示。

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类的定义代码例子:

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

@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<BookInfo> queryAll();

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

3 .编写图书信息表对应的数据库类

因为先有数据库然后才有表,所以图书信息表还得放到某个数据库里,这个默认的图书数据库要从

RoomDatabase派生而来,并添加“@Database”注解。下面是数据库类BookDatabase的定义代码例

子: (完整代码见chapter06\src\main\java\com\example\chapter06\database\BookDatabase.java)

//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方法向数据库添加图书信息,完整代码见

chapter06\src\main\java\com\example\chapter06\RoomWriteActivity.java;而记录读取页面通过

queryAllBook方法从数据库读取图书信息,完整代码见

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

运行测试App,先打开记录保存页面,依次录入两本图书信息并保存至数据库,如图6-21和图6-22所

示。再打开记录读取页面,从数据库读取图书信息并展示在页面上,如图6-23所示。

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

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

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

6.5 实战项目:购物车

购物车的应用面很广,凡是电商App都可以看到它的身影,之所以选择购物车作为本章的实战项目,除

了它使用广泛的特点,更因为它用到了多种存储方式。现在就让我们开启电商购物车的体验之旅吧。

6.5.1 需求描述

电商App的购物车可谓是司空见惯了,以京东商城的购物车为例,一开始没有添加任何商品,此时空购

物车如图6-24所示,而且提示去逛秒杀商场;加入几件商品之后,购物车页面如图6-25所示。

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

图6-24 京东App购物车的初始页面

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

图6-25 京东App购物车加了几件商品

可见购物车除了底部有个结算行,其余部分主要是已加入购物车的商品列表,然后每个商品行左边是商

品小图,右边是商品名称及其价格。

据此仿照本项目的购物车功能,第一次进入购物车页面,购物车里面是空的,同时提示去逛手机商场,

如图6-26所示。接着去商场页面选购手机,随便挑了几部手机加入购物车,再返回购物车页面,即可看

到购物车的商品列表,如图6-27所示,有商品图片、名称、数量、单价、总价等等信息。当然购物车并

不仅仅只是展示待购买的商品,还要支持最终购买的结算操作、支持清空购物车等功能。

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

图6-26 首次打开购物车页面

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

图6-27 选购商品后的购物车

购物车的存在感很强,不仅仅在购物车页面才能看到购物车。往往在商场页面,甚至商品详情页面,都

会看到某个角落冒出购物车图标。一旦有新商品加入购物车,购物车图标上的商品数量立马加一。当

然,用户也能点击购物车图标直接跳到购物车页面。商场页面除了商品列表之外,页面右上角还有一个

购物车图标,如图6-28所示,有时这个图标会在页面右下角。商品详情页面通常也有购物车图标,如图

6-29所示,倘使用户在详情页面把商品加入购物车,那么图标上的数字也会加一。

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

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

图6-29 手机详情页面

至此大概过了一遍购物车需要实现的基本功能,提需求总是很简单的,真正落到实处还得开发者发挥想象力,把购物车做成一个功能完备的模块。

6.5.2 界面设计

首先找找看,购物车使用了哪些Android控件:

  • 线性布局LinearLayout:购物车界面从上往下排列,用到了垂直方向的线性布局。

  • 网格布局GridLayout:商场页面的陈列橱柜,允许分行分列展示商品。

  • 相对布局RelativeLayout:页面右上角的购物车图标,图标右上角又有数字标记,按照指定方位排列控件正是相对布局的拿手好戏。

  • 其他常见控件尚有文本视图TextView、图像视图ImageView,按钮控件Button等。

    然后考虑一下购物车的存储功能,到底采取了哪些存储方式:

  • 数据库SQLite:最直观的肯定是数据库了,购物车里的商品列表一定是放在SQLite中,增删改查都少不了它。

  • 全局内存:购物车图标右上角的数字表示购物车中的商品数量,该数值建议保存在全局内存中,这样不必每次都到数据库中执行count操作。

  • 存储卡文件:通常商品图片来自于电商平台的服务器,此时往往引入图片缓存机制,也就是首次访问先将网络图片保存到存储卡,下次访问时直接从存储卡获取缓存图片,从而提高图片的加载速度。

  • 共享参数SharedPreferences:是否首次访问网络图片,这个标志位推荐放在共享参数中,因为它需要持久化存储,并且只有一个参数信息。

    真是想不到,一个小小的购物车,竟然用到了好几种存储方式。

6.5.3 关键代码

为了读者更好更快地完成购物车项目,下面列举几个重要功能的代码片段。

1 .关于页面跳转

因为购物车页面允许直接跳到商场页面,并且商场页面也允许跳到购物车页面,所以如果用户在这两个

页面之间来回跳转,然后再按返回键,结果发现返回的时候也是在两个页面间往返跳转。出现问题的缘

由在于:每次启动活动页面都往活动栈加入一个新活动,那么返回出栈之时,也只好一个一个活动依次

退出了。

解决该问题的办法参见第 4 章的“4.1.3 Activity的启动模式”,对于购物车的活动跳转需要指定启动标志

FLAG_ACTIVITY_CLEAR_TOP,表示活动栈有且仅有该页面的唯一实例,如此即可避免多次返回同一页

面的情况。比如从购物车页面跳到商场页面,此时活动跳转的代码示例如下:

// 从购物车页面跳到商场页面
Intent intent = new Intent(this, ShoppingChannelActivity.class); 
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);  // 设置启动标志 
startActivity(intent);  // 跳转到手机商场页面

又如从商场页面跳到购物车页面,此时活动跳转的代码示例如下:

// 从商场页面跳到购物车页面
Intent intent = new Intent(this, ShoppingCartActivity.class); 
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);  // 设置启动标志 
startActivity(intent);  // 跳转到购物车页面

2 .关于商品图片的缓存

通常商品图片由后端服务器提供,App打开页面时再从服务器下载所需的商品图。可是购物车模块的多

个页面都会展示商品图片,如果每次都到服务器请求图片,显然既耗时间又耗流量非常不经济。因此

App都会缓存常用的图片,一旦从服务器成功下载图片,便在手机存储卡上保存图片文件。然后下次界

面需要加载商品图片时,就先从存储卡寻找该图片,如果找到就读取图片的位图信息,如果没找到就再

到服务器下载图片。

以上的缓存逻辑是最简单的二级图片缓存,实际开发往往使用更高级的三级缓存机制,即“运行内存→存

储卡→网络下载”。当然就初学者而言,先从掌握最简单的二级缓存开始,也就是“存储卡→网络下载”。

按照二级缓存机制,可以设计以下的缓存处理逻辑:

( 1 )先判断是否为首次访问网络图片。

( 2 )如果是首次访问网络图片,就先从网络服务器下载图片。

( 3 )把下载完的图片数据保存到手机的存储卡。

( 4 )往数据库中写入商品记录,以及商品图片的本地存储路径。

( 5 )更新共享参数中的首次访问标志。

按照上述的处理逻辑,编写的图片加载代码示例如下:

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

private String mFirst = "true"; // 是否首次打开 
// 模拟网络数据,初始化数据库中的商品信息
private void downloadGoods() {
   // 获取共享参数保存的是否首次打开参数
   mFirst = SharedUtil.getIntance(this).readString("first", "true");
     // 获取当前App的私有下载路径 
   String path =
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() + "/"; 
   if (mFirst.equals("true")) { // 如果是首次打开
       ArrayList<GoodsInfo> goodsList = GoodsInfo.getDefaultList(); // 模拟网络图 
片下载
       for (int i = 0; i < goodsList.size(); i++) { 
           GoodsInfo info = goodsList.get(i);
           long rowid = mGoodsHelper.insert(info); // 往商品数据库插入一条该商品的记 
录
           info.rowid = rowid;
           Bitmap pic = BitmapFactory.decodeResource(getResources(), info.pic);
           String pic_path = path + rowid + ".jpg";
           FileUtil.saveImage(pic_path, pic); // 往存储卡保存商品图片
           pic.recycle(); // 回收位图对象
           info.pic_path = pic_path;
           mGoodsHelper.update(info); // 更新商品数据库中该商品记录的图片路径
    }
 }
   // 把是否首次打开写入共享参数
   SharedUtil.getIntance(this).writeString("first", "false"); 
}

3 .关于各页面共同的标题栏

注意到购物车、手机商场、手机详情三个页面顶部都有标题栏,而且这三个标题栏风格统一,既然如

此,能否把它做成公共的标题栏呢?当然App界面支持局部的公共布局,以购物车的标题栏为例,公共

布局的实现过程包括以下两个步骤:

步骤一,首先定义标题栏专用的布局文件,包含返回箭头、文字标题、购物车图标、商品数量表等,具

体内容如下所示:

(完整代码见chapter06\src\main\res\layout\title_shopping.xml)

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="50dp"
   android:background="#aaaaff" >
   <ImageView
       android:id="@+id/iv_back"
       android:layout_width="50dp"
       android:layout_height="match_parent"
       android:layout_alignParentLeft="true"
       android:padding="10dp"
       android:scaleType="fitCenter"
       android:src="@drawable/ic_back" />
   <TextView
       android:id="@+id/tv_title"
       android:layout_width="wrap_content"
       android:layout_height="match_parent"
       android:layout_centerInParent="true"
       android:gravity="center"
       android:textColor="@color/black"
       android:textSize="20sp" />
   <ImageView 
       android:id="@+id/iv_cart"
       android:layout_width="50dp"
       android:layout_height="match_parent"
       android:layout_alignParentRight="true"
       android:scaleType="fitCenter"
       android:src="@drawable/cart" />
   <TextView
       android:id="@+id/tv_count"
       android:layout_width="20dp"
       android:layout_height="20dp"
       android:layout_alignParentTop="true"
       android:layout_toRightOf="@+id/iv_cart"
       android:layout_marginLeft="-20dp"
       android:gravity="center"
       android:background="@drawable/shape_oval_red"
       android:text="0"
       android:textColor="@color/white"
       android:textSize="15sp" />
</RelativeLayout>

步骤二,然后在购物车页面的布局文件中添加如下一行include标签,表示引入title_shopping.xml的布

局内容:

(完整代码见chapter06\src\main\res\layout\activity_shopping_cart.xml)

<include layout="@layout/title_shopping" />

之后重新运行测试App,即可发现购物车页面的顶部果然出现了公共标题栏,商场页面、详情页面的公

共标题栏可参考购物车页面的include标签。

4 .关于商品网格的单元布局

商场页面的商品列表,呈现三行二列的表格布局,每个表格单元的界面布局雷同,都是商品名称在上、

商品图片居中、商品价格与添加按钮在下,看起来跟公共标题栏的处理有些类似。但后者为多个页面引

用同一个标题栏,是多对一的关系;而前者为一个商场页面引用了多个商品网格,是一对多的关系。因

此二者的实现过程不尽相同,就商场网格而言,它的单元复用分为下列 3 个步骤:

步骤一,在商场页面的布局文件中添加GridLayout节点,如下所示:

(完整代码见chapter06\src\main\res\layout\activity_shopping_channel.xml)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:background="@color/orange"
   android:orientation="vertical" >
   <include layout="@layout/title_shopping" />
   <ScrollView
       android:layout_width="match_parent"
       android:layout_height="wrap_content" >
       <GridLayout
           android:id="@+id/gl_channel"
           android:layout_width="match_parent"
           android:layout_height="wrap_content" 
           android:columnCount="2" />
   </ScrollView> 
</LinearLayout>

步骤二,为商场网格编写统一的商品信息布局,XML文件内容示例如下:

(完整代码见chapter06\src\main\res\layout\item_goods.xml)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:id="@+id/ll_item"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:layout_gravity="center"
   android:gravity="center"
   android:background="@color/white"
   android:orientation="vertical">
   <TextView
       android:id="@+id/tv_name"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:gravity="center"
       android:textColor="@color/black"
       android:textSize="17sp" />
   <ImageView
       android:id="@+id/iv_thumb"
       android:layout_width="180dp"
       android:layout_height="150dp"
       android:scaleType="fitCenter" />
   <LinearLayout
       android:layout_width="match_parent"
       android:layout_height="45dp"
       android:orientation="horizontal">
       <TextView
           android:id="@+id/tv_price"
           android:layout_width="0dp"
           android:layout_height="match_parent"
           android:layout_weight="2"
           android:gravity="center"
           android:textColor="@color/red"
           android:textSize="15sp" />
       <Button
           android:id="@+id/btn_add"
           android:layout_width="0dp"
           android:layout_height="match_parent"
           android:layout_weight="3"
           android:gravity="center"
           android:text="加入购物车"
           android:textColor="@color/black"
           android:textSize="15sp" />
   </LinearLayout>
  </LinearLayout>

步骤三,在商场页面的Java代码中,先利用下面代码获取布局文件item_goods.xml的根视图:

View view = LayoutInflater.from(this).inflate(R.layout.item_goods, null);

再从根视图中依据控件ID分别取出网格单元的各控件对象:

ImageView iv_thumb = view.findViewById(R.id.iv_thumb); 
TextView tv_name = view.findViewById(R.id.tv_name); 
TextView tv_price = view.findViewById(R.id.tv_price); 
Button btn_add = view.findViewById(R.id.btn_add);

然后就能按照寻常方式操纵这些控件对象了,下面便是给网格布局加载商品的代码例子:

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

private void showGoods() {
       int screenWidth = Utils.getScreenWidth(this);
       LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
               screenWidth/2, LinearLayout.LayoutParams.WRAP_CONTENT);
       gl_channel.removeAllViews(); // 移除下面的所有子视图
       // 查询商品数据库中的所有商品记录
       List<GoodsInfo> goodsArray = mGoodsHelper.query("1=1");
       for (final GoodsInfo info : goodsArray) {
           // 获取布局文件item_goods.xml的根视图
           View view = LayoutInflater.from(this).inflate(R.layout.item_goods, 
null);
           ImageView iv_thumb = view.findViewById(R.id.iv_thumb);
           TextView tv_name = view.findViewById(R.id.tv_name);
           TextView tv_price = view.findViewById(R.id.tv_price);
           Button btn_add = view.findViewById(R.id.btn_add);
           tv_name.setText(info.name); // 设置商品名称
           iv_thumb.setImageURI(Uri.parse(info.pic_path)); // 设置商品图片
           iv_thumb.setOnClickListener(new View.OnClickListener() {
               @Override
               public void onClick(View v) {
                   Intent intent = new Intent(ShoppingChannelActivity.this, 
ShoppingDetailActivity.class);
                   intent.putExtra("goods_id", info.rowid);
                   startActivity(intent); // 跳到商品详情页面
        }
          });
           tv_price.setText("" + (int)info.price); // 设置商品价格
           btn_add.setOnClickListener(new View.OnClickListener() {
               @Override
               public void onClick(View v) {
                   addToCart(info.rowid, info.name); // 添加到购物车
        }
          });
           gl_channel.addView(view, params); // 把商品视图添加到网格布局
    }
 }

弄好了商场页面的网格单元,购物车页面的商品行也可照此办理,不同之处在于购物车页面的商品行使

用线性布局而非网格布局,其余实现过程依然分成上述 3 个步骤。


6.6 小结

本章主要介绍了Android常用的几种数据存储方式,包括共享参数SharedPreferences的键值对存取、数

据库SQLite的关系型数据存取、存储卡的文件读写操作(含文本文件读写和图片文件读写)、App全局

内存的读写,以及为实现全局内存而学习的Application组件的生命周期及其用法。最后设计了一个实战

项目“购物车”,通过该项目的编码进一步复习巩固本章几种存储方式的使用。

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

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

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

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

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

6.7 课后练习题

一、填空题

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

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

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

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

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

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

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

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

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

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

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

三、选择题

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

A.共享参数

B.数据库

C.文件

D.全局变量

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

A.创建表格

B.删除表格

C.清空表格

D.修改表结构

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

A.update

B.insert

C.query

D.rawQuery

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

A.decodeStream

B.decodeFile

C.decodeImage

D.decodeResource

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


A.setImageBitmap

B.setImageFile

C.setImageURI

D.setImageResource

四、简答题

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

五、动手练习

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

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

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

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

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

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

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

       btn_add.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               addToCart(info.rowid, info.name); // 添加到购物车
    }
      });
       gl_channel.addView(view, params); // 把商品视图添加到网格布局
}

}


  弄好了商场页面的网格单元,购物车页面的商品行也可照此办理,不同之处在于购物车页面的商品行使 

  用线性布局而非网格布局,其余实现过程依然分成上述 3 个步骤。 

---

## 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 )清空购物车。 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

L念安dd

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值