room
title: 数据存储
共享参数SharedPreferences
实际开发中,共享参数经常存储的数据包括:App的个性化配置信息、用户使用App的行为信息、临时需要保存的片段信息等。(如记住密码)
本节介绍Android的键值对存储方式——共享参数SharedPreferences的使用方法,包括:如何将数据保
存到共享参数,如何从共享参数读取数据,如何使用共享参数实现登录页面的记住密码功能,如何利用
设备浏览器找到共享参数文件。
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退出后再次启动时,之前保存的数据仍然有效。
共享参数对数据的存储和读取操作类似于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的浮点数
实现记住密码功能
(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); // 往密码编辑框填写上次保存的密码
Application
本节介绍Android重要组件Application的基本概念和常见用法。首先说明Application的生命周期贯穿了
App的整个运行过程,接着利用Application实现App全局变量的读写,然后阐述了如何借助App实例来
操作Room数据库框架。
生命周期
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节点示例如下:
<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:在配置改变时调用,例如从竖屏变为横屏。
需要注意的是onTerminate永远不会执行,不能用它回收资源
Application读写全局变量
C/C++有全局变量的概念,因为全局变量保存在内存中,所以操作全局变量就是操作内存,显然内存的读写速度远比读写数据库或读写文件快得多。所谓全局,指的是其他代码都可以引用该变量,因此全局变量是共享数据和消息传递的好帮手。
不过Java没有全局变量的概念,与之比较接近的是类里面的静态成员变量,该变量不但能被外部直接引用,而且它在不同地方引用的值是一样的(前提是在引用期间不能改动变量值),所以借助静态成员变量也能实现类似全局变量的功能。
Application的生命周期覆盖了App运行的全过程。不像短暂的Activity生命周期,一旦退出该页面,Activity实例就被销毁。因此,利用Application的全生命特性,能够在Application实例中保存全局变量。适合在Application中保存的全局变量主要有下面3类数据:
(1)会频繁读取的信息,例如用户名、手机号码等。
(2)不方便由意图传递的数据,例如位图对象、非字符串类型的集合对象等。
(3)容易因频繁分配内存而导致内存泄漏的对象,例如Handler处理器实例等。比如room
要想通过Application实现全局内存的读写,得完成以下3项工作:
(1)编写一个继承自Application的新类MyApplication。该类采用单例模式,内部先声明自身类的一
个静态成员对象,在创建App时把自身赋值给这个静态对象,然后提供该对象的获取方法getInstance。
public class MyApplication 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)在活动页面代码中调用MyApplication的getInstance方法,获得它的一个静态对象,再通过该对
象访问MyApplication的公共变量和公共方法。
(3)不要忘了在AndroidManifest.xml中注册新定义的Application类名,也就是给application节点增
加android:name属性,其值为.MainApplication。
接下来演示如何读写内存中的全局变量,首先分别创建写内存页面和读内存页面,其中写内存页面把用户的注册信息保存到全局变量infoMap,而读内存页面从全局变量infoMap读取用户的注册信息
public class AppWriteActivity extends AppCompatActivity implements View.OnClickListener {
private EditText et_name;
private EditText et_age;
private EditText et_height;
private EditText et_weight;
private CheckBox ck_married;
private MyApplication app;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_app_write);
et_name = findViewById(R.id.et_name);
et_age = findViewById(R.id.et_age);
et_height = findViewById(R.id.et_height);
et_weight = findViewById(R.id.et_weight);
ck_married = findViewById(R.id.ck_married);
// 此处注意执行顺序,先进行了保存
findViewById(R.id.btn_save).setOnClickListener(this);
app = MyApplication.getInstance();
reload();
}
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);
}
}
@Override
public void onClick(View v) {
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.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() ? "是" : "否");
}
Room
由于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类的定义代码例子:
//书籍信息
@Entity
@Data //生成get set
public class BookInfo {
@PrimaryKey // 该字段是主键,不能重复
@NonNull // 主键必须是非空字段
private String name; // 书籍名称
private String author; // 作者
private String press; // 出版社
private double price; // 价格
}
2.编写图书信息表对应的持久化类
所谓持久化,指的是将数据保存到磁盘而非内存,其实等同于增删改等SQL语句。假设图书信息表的持
久化类名叫作BookDao,那么该类必须添加“@Dao”注解,内部的记录查询方法必须添加“@Query”注
解,记录插入方法必须添加“@Insert”注解,记录更新方法必须添加“@Update”注解,记录删除方法必须
添加“@Delete”注解(带条件的删除方法除外)。对于记录查询方法,允许在@Query之后补充具体的查询语句以及查询条件;对于记录插入方法与记录更新方法,需明确出现重复记录时要采取哪种处理策略。下面是BookDao类的定义代码例子:
@Dao
public interface BookDao {
@Query("SELECT * FROM BookInfo") // 设置查询语句
List<BookInfo> queryAllBook(); // 加载所有书籍信息
@Query("SELECT * FROM BookInfo WHERE name = :name") // 设置带条件的查询语句
BookInfo queryBookByName(String name); // 根据名字加载书籍
@Insert(onConflict = OnConflictStrategy.REPLACE) // 记录重复时替换原记录
void insertOneBook(BookInfo book); // 插入一条书籍信息
@Insert
void insertBookList(List<BookInfo> bookList); // 插入多条书籍信息
@Update(onConflict = OnConflictStrategy.REPLACE)// 出现重复记录时替换原记录
int updateBook(BookInfo book); // 更新书籍信息
@Delete
void deleteBook(BookInfo book); // 删除书籍信息
@Query("DELETE FROM BookInfo WHERE 1=1") // 设置删除语句
void deleteAllBook(); // 删除所有书籍信息
}
3.编写图书信息表对应的数据库类
因为先有数据库然后才有表,所以图书信息表还得放到某个数据库里,这个默认的图书数据库要 RoomDatabase派生而来,并添加“@Database”注解。下面是数据库类BookDatabase的定义代码例子:
//entities表示该数据库有哪些表,version表示数据库的版本号
//exportSchema表示是否导出数据库信息的json串,建议设为false,若设为true还需指定json文件的保
存路径
@Database(entities = {BookInfo.class},version = 1, exportSchema = false)
public abstract class BookDatabase extends RoomDatabase {
// 获取该数据库中某张表的持久化对象
public abstract BookDao bookDao();
}
4.在自定义的Application类中声明图书数据库的唯一实例
为了避免重复打开数据库造成的内存泄漏问题,每个数据库在App运行过程中理应只有一个实例.
此时要求开发者自定义新的Application类,在该类中声明并获取图书数据库的实例,并将自定义的Application类设为单例模式,保证App运行之时有且仅有一个应用实例。下面是自定义Application类的代码例子:
public class MainApplication extends Application {
private final static String TAG = "MainApplication";
private static MainApplication mApp; // 声明一个当前应用的静态实例
// 声明一个公共的信息映射对象,可当作全局变量使用
public HashMap<String, String> infoMap = new HashMap<String, String>();
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,"BookInfo")
.addMigrations() // 允许迁移数据库(发生数据库变更时,Room默认删除原数据库再创建新数据库。如此一来原来的记录会丢失,故而要改为迁移方式以便保存原有记录)
.allowMainThreadQueries() // 允许在主线程中操作数据库(Room默认不能在主线程中操作数据库)
.build();
}
// 获取书籍数据库的实例
public BookDatabase getBookDB(){return bookDatabase;}
}
5.在操作图书信息表的地方获取数据表的持久化对象
// 从App实例中获取唯一的图书持久化对象
BookDao bookDao = MainApplication.getInstance().getBookDB().bookDao();
完成以上5个编码步骤之后,接着调用持久化对象的queryXXX、insertXXX、updateXXX、deleteXXX等
方法,就能实现图书信息的增删改查操作了。例程的图书信息演示页面有两个,分别是记录保存页面和
记录读取页面,其中记录保存页面通过insertOneBook方法向数据库添加图书信息。