Android 提供3种持久化方式:文件存储、SharedPreference存储、数据库存储
文件存储
写入文件
Context 类中提供了一个 openFileOutput() 方法,可以用于将数据存储到指定的文件夹中,这个方法接收两个参数:
第一个是文件名(文件名不可包含路径,以为所有的文件都默认存储到 /data/data/<packagename>/files/ 目录下)
第二个是文件的操作模式,主要有两种模式可选:MODE_PRIVATE 和 MODE_APPEND,MODE_PRIVATE 是默认的操作方式,表示当指定同样的文件名的时候,所写入的内容将会覆盖原文件中的内容,MODE_APPEND 表示如果该文件已经存在,就向这个文件追加内容
openFileOutput() 方法返回一个 FileOutputStream 对象,得到这个对象就可以用Java流的方式将数据写入到文件中了
public class FileSave {
private volatile static FileSave fileSave;
private FileSave(){}
public static FileSave getInstance(){
if (fileSave == null){
synchronized (FileSave.class){
if (fileSave == null){
fileSave = new FileSave();
}
}
}
return fileSave;
}
public void save(Context context, String data){
FileOutputStream out;
BufferedWriter writer = null;
try {
out = context.openFileOutput("data", Context.MODE_PRIVATE);
writer = new BufferedWriter(new OutputStreamWriter(out));
writer.write(data);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if (writer != null){
writer.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
注意 class 如果不是继承Context,要用上下文对象来调用 openFileOutput() 方法
edit = (EditText)findViewById(R.id.edittext);
Button button = (Button)findViewById(R.id.save);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String inputText = edit.getText().toString();
FileSave.getInstance().save(PersistenceActivity.this, inputText);
}
});
最后写个界面 EditText 用于输入要保存的内容,按钮用于保存
点击红框的 Device File Explorer 工具,在 /data/data/<packagename>/files/ 文件夹下可看到保存的文件,双击打开可以看到保存的内容
读取文件
Context 类中提供了一个 openFileInput() 方法,用于从文件中读取数据,接收一个要读取的文件名参数,系统会自动到 /data/data/<packagename>/files/ 目录下去找,并返回一个 FileInputStream 对象
public String load(Context context){
FileInputStream in = null;
BufferedReader reader = null;
StringBuilder content = new StringBuilder();
try {
in = context.openFileInput("data");
reader = new BufferedReader(new InputStreamReader(in));
String line = "";
while ((line = reader.readLine()) != null){
content.append(line);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return content.toString();
}
在 onCreate() 方法中调用 load() 方法来读取文件中存储的文本内容,如果读到的内容不为空就调用 EditText 的 setText() 方法将文本信息填充到 EditText 中,并调用 setSelection() 方法将输入光标移动到文本末尾
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_persistence);
edit = (EditText)findViewById(R.id.edittext);
Button button1 = (Button)findViewById(R.id.save);
String inputText = FileSave.getInstance().load(PersistenceActivity.this);
if (!TextUtils.isEmpty(inputText)){
edit.setText(inputText);
edit.setSelection(inputText.length());
Toast.makeText(this, "读取文件成功", Toast.LENGTH_SHORT).show();
}
}
SharedPreferences 存储
不同于文件存储的方式,SharedPreferences 是使用键值对的方式进行存储的
将数据存储到SharedPreferences
(1) 获取 SharedPreferences 对象
- Context 类中 getSharePreferences() 方法
此方法接收两个参数,第一个是用于指定 SharedPreferences 文件的名称,如果指定的文件不存在则会创建一个,SharedPreferences 文件都放在 /data/data/<packagename>/shared_prefs/ 目录下,第二个参数是指定操作模式,目前只有 MODE_PRIVATE(默认) 这一种模式,表示只有当前应用程序才可以对这个 SharedPreferences 文件进行读写。 - Activity 类中的 getPreferences() 方法
与 Context 中的 getSharePreferences() 方法类似,只接收一个操作模式的传参数,引文这个方法会自动将当前活动的类名作为 SharedPreferences 的文件名 - PreferenceManager 类中的 getDefaultSharedPreferences() 方法
这是一个静态方法,接收一个Context 参数,并自动使用当前应用程序的包名作为前缀来命名 SharedPreferences 文件,
(2)调用 SharedPreferences 对象的 edit() 方法来获取一个 SharedPreferences.Editor 对象
(3)向 SharedPreferences.Editor 对象中添加数据,添加字符串就使用 putString() 方法,添加布尔类型就使用 putBoolean() 方法,以此类推
(4)调用 apply() 方法将添加的数据提交
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
SharedPreferences.Editor editor = (SharedPreferences.Editor) getSharedPreferences("data", MODE_PRIVATE).edit();
String inputText = edit1.getText().toString();
editor.putString("name", inputText);
editor.apply();
}
});
在 /data/data/<packagename>/shared_prefs/ 目录下已经数据保存下来了,并且 SharedPreferences 文件是以 XML 格式来对数据进行管理的
从 SharedPreferences 将数据读出
对应存数据的 put 方法,读数据使用 get 方法,例如读取字符串使用 getString() 方法,读取布尔类型就使用 getBoolean() 方法以此类推,这些 get 方法接收两个参数,一个是取出数据的键值,第二个是当找不到对应键的值时返回的默认值
button3.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
SharedPreferences preferences = getSharedPreferences("data", MODE_PRIVATE);
String name = preferences.getString("name", "null");
edit1.setText(name);
edit1.setSelection(name.length());
}
});
核心就是
SharedPreferences preferences = getSharedPreferences("data", MODE_PRIVATE);
String name = preferences.getString("name", "null");
SQLite 数据库存储
SQLite 不仅支持标准的 SQL 语法,还遵循了 ACID 事务
创建数据库
Android 提供了一个 SQLiteOpenHelper 帮助类,SQLiteOpenHelper 是一个抽象类,也就是说要使用就需要创建一个新帮助类去继承它,SQLiteOpenHelper 中有两个抽象方法:onCreate() 和 onUpdate() 重写这两个方法就可以创建和更新升级数据库
SQLiteOpenHelper 有两个重要的实例方法: getReadableDatabase() 和 getWritableDatabase() 这两个方法都可以创建或打开一个现有的数据库(如果数据库存在直接打开,否则创建一个新的数据库),并返回一个可以对数据库读写的操作对象
注意:要是磁盘已满,getReadableDatabase() 以只读的方式打开数据库,getWritableDatabase() 则会抛出异常
数据库有三种构造方法,常用的构造方法有4个参数,第一个是 Context 第二个是数据库的名称,第三个是允许在查询数据的时候返回一个自定义的 Cursor 一般传入 null,第四个是当前数据库的版本号,课用于对数据库的升级操作,数据库的文件放在 /data/data/<packagename>/databases/ 目录下
SQLite 数据类型很简单:integer 表示整型,real 表示浮点型,text 表示文本类型,blob 表示二进制类型
public class MyDatabaseHelper extends SQLiteOpenHelper {
public static final String CREATE_BOOK = "create table Book("
+ "id integer primary key autoincrement,"
+ "name text,"
+ "author text,"
+ "price real)";
private Context context;
public MyDatabaseHelper(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
this.context = context;
}
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
sqLiteDatabase.execSQL(CREATE_BOOK);
Toast.makeText(context, "数据库创建成功", Toast.LENGTH_SHORT).show();
}
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
}
}
在 CREATE_BOOK 创建一个数据库表的语句,其中 primary key 表示将 id 设为主键,autoincrement 表示 id 列是自增长的,然后在 onCreate() 方法中调用了 SQLiteDatabase 的execSQL() 方法执行这条创建表的语句,并弹出一个 Toast 提示成功
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_persistence);
myDatabaseHelper = new MyDatabaseHelper(this, "BookStore.db", null, 1);
Button button = (Button)findViewById(R.id.create_database);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
myDatabaseHelper.getWritableDatabase();
}
});
}
创建了一个按钮,用于创造数据库,点击按钮后提示创建数据库成功,再次点击就不会提示
在 /data/data/<packagename>/databases/ 目录下会生成两个文件一个是创建的数据库 BookStore.db 一个是 BookStore.db-journal 是为了让数据库支持事务而产生的临时日志文件
打开数据库会发现有两张表,一张是建立的 Book 表,另一张 android_metadata 表是数据库自动生成的
升级数据库
onUpdate() 方法用于对数据库进行升级,当需要在数据库中添加新的表或删除表或更改表结构的时候会调用 onUpdate() 进行数据库的更新,因为 数据库已经存在,所以 onCreate() 方法不会执行
public static final String CREAT_CATEGORY = "create table Category("
+ "id integer primary key autoincrement,"
+ "category_name text,"
+ "category_code integer)";
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
sqLiteDatabase.execSQL("drop table if exists Category");
sqLiteDatabase.execSQL(CREAT_CATEGORY);
Toast.makeText(context, "数据库update成功", Toast.LENGTH_SHORT).show();
}
先执行了 drop 语句删除数据库中的 Category 表,在创建 Category 表,如果在创建时发现这张表已经存在了就会直接报错
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_persistence);
myDatabaseHelper = new MyDatabaseHelper(this, "BookStore.db", null, 2);
Button button = (Button)findViewById(R.id.create_database);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
myDatabaseHelper.getWritableDatabase();
}
});
}
myDatabaseHelper = new MyDatabaseHelper(this, "BookStore.db", null, 2)
只需要在构造方法中将版本号改成大于前一个版本的就行
可以看到 Category 表已经创建成功
添加数据
getReadableDatabase() 和 getWritableDatabase() 方法除了可以用于创建和升级数据库,还会返回一个 SQLiteDatabase 对象,通过这个对象就可以对数据库进行 CRUD 操作
SQLiteDatabase 中有 execSQL(),通过上面可以得知 execSQL() 中传入一个 sql 语句参数,数据库就会执行这天语句。
SQLiteDatabase 提供了一个 insert() 方法,接收3个参数,第一个是表名,第二个参数用于在为指定添加数据的情况下某些可以为空的列自动赋值 NULL,第三个参数是一个 ContentValues 对象,它提供了一系列的 put() 方法重载,用于向 ContentValues 中添加数据,只需要将表名中的每个列名以及相应待添加数据传入即可
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
SQLiteDatabase db = myDatabaseHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("name", "11111");
values.put("author", "aaa");
values.put("price", 11.90);
db.insert("Book", null, values);
values.clear();
values.put("name", "22222");
values.put("author", "bbb");
values.put("price", 29.98);
db.insert("Book", null, values);
Toast.makeText(PersistenceActivity.this, "添加数据成功", Toast.LENGTH_SHORT).show();
}
});
可以看到两条数据已经添加到数据库中的 Book 表中了
数据更新
SQLiteDatabase 提供了一个 update() 方法,这个方法接收4个参数,第一个是表名,第二个是 ContentValues 对象,将要跟新的数据组装进去,第三个参数用于约束(不指定默认所有),第四个是预处理数据
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
SQLiteDatabase db = myDatabaseHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("price", 100.98);
db.update("Book", values, "name=?", new String[] {"11111"});
Toast.makeText(PersistenceActivity.this, "数据更新成功", Toast.LENGTH_SHORT).show();
}
});
可以看到数据库中的 Book 表书名为 11111 的书的价格跟新成了 100.98
删除数据
SQLiteDatabase 提供了一个 delete() 方法,这个方法接收3个参数,第一个是表名,第二个参数用于约束(不指定默认所有),第三个是预处理数据
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
SQLiteDatabase db = myDatabaseHelper.getWritableDatabase();
db.delete("Book", "price > ?", new String[] {"100"});
Toast.makeText(PersistenceActivity.this, "数据更新成功", Toast.LENGTH_SHORT).show();
}
});
可以看到数据库中的 Book 价格大于100的书籍已经删除
查询数据
SQLiteDatabase 提供了一个 query() 方法进行查询,这个方法比较复杂,最短的一个方法重载也需要传入 7 个参数
query() 方法参数 | 描述 |
table | 指定查询的表名 |
columns | 指定查询的列名 |
selection | 指定查询的where约束条件 |
selectionArgs | 为where中的占位符提供具体数值 |
groupBy | 指定需要的分组的列 |
having | 对group by 后结果进一步约束 |
orderBy | 指定查询的结果的排序方式 |
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
SQLiteDatabase db = myDatabaseHelper.getWritableDatabase();
Cursor cursor = db.query("Book", null, "name=?", new String[] {"11111"}, null, null, null);
String name = null;
String author = null;
double price = 0;
if (cursor.moveToFirst()){
do {
name = cursor.getString(cursor.getColumnIndex("name"));
author = cursor.getString(cursor.getColumnIndex("author"));
price = cursor.getDouble(cursor.getColumnIndex("price"));
}while (cursor.moveToNext());
}
Toast.makeText(PersistenceActivity.this, "书名:"+naem+"作者:"+author+"价格:"+price, Toast.LENGTH_SHORT).show();
}
});
query() 方法返回一个 Cursor 对象,接着调用 moveToFirst() 方法将数据的指针移动到一个行的位置,就可以通过循环遍历从得到每一行的数据,通过 Cursor 的 getColumnIndex() 方法获取到某一列在表中对应位置的索引,然后通过这个索引传入到相应的取值方法中就可以得到数据库中的数据
第一条数据被删除了,重新点击Add按钮插入两条数据,按照书名查询,将查询结果的最后一个信息 Toast 打印
由于查询过于复杂,还是建议用 sql 语句进行查询,前面的增删改都是用 execSQL() 方法执行,而查询用 rawQuery("select * from Book Where name=?", new String[] {"11111"})
与上查询结果一样,返回一个 Cursor 对象。
LitePal
LitePal 是一款开源的 Android 数据库框架,采用对象关系映射(ORM)的模式
添加依赖
implementation 'org.litepal.android:core:1.6.1'
建立一个与java、res同级的文件夹assets,在assets下建立litepal.xml文件
<?xml version="1.0" encoding="utf-8"?>
<litepal>
<dbname value="BookStore"></dbname>
<version value="1"></version>
<list>
</list>
</litepal>
其中,<dbname>
标签用于指定数据库的名称,<version>
标签用于指定数据库的版本号,<list>
标签用于指定所有的映射模型
修改AndroidManifest.xml的代码,在文件中加:android:name="org.litepal.LitePalApplication"
<application
android:name="org.litepal.LitePalApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
......
</application>
LitePal创建数据库
之前创建数据库是通过自定义一个类继承 SQLiteOpenHelper,然后再 onCreate() 方法中编写创建表的sql语句来实现,首先删除上面的 BookStore 数据库创建一个 Book 类
public class Book {
private int id;
private String name;
private String author;
private double price;
// 省略 getter 和 setter 方法
}
将Book类添加到映射模型列表中,修改 litepal.xml 中的 <list> 标签
<list>
<mapping class="com.example.persistence.bean.Book"/>
</list>
mapping 标签用于圣母配置的映射模型类,注意一定要用完整的类名,只要完成任意一次数据库的操作,BookStore.db 就会自动创建出来
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Connector.getDatabase();
}
});
其中,调用 Connector.getDatabase() 方法就是一次最简单的数据库操作,数据库已然存放在 /data/data/<packagename>/databases/目录下,打开数据库可以看到3张表,android_metadata 表是数据库自动生成,table_schema 表是 LitePal 内部使用自动生成的,book表就是根据定义的book类自动生成的
LitePal更新数据库
使用 SQLiteOpenHelper 升级数据库时要先吧之前的数据库删除再重新创建,在删除数据库的时候就会把原有的数据一并删除 LitePal 升级数据库与 SQLiteOpenHelper 类似,至需要将 litepal.xml 文件中的版本号加1,完成任意一次数据库的操作,不同的是 LitePal 会保留之前表中的所有数据
LitePal添加数据
LitePal 进行表管理的操作时不需要有任何的继承结构,但 CRUD 操作需要继承 DataSupport 类
public class Book extends DataSupport {
......
}
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Book book1 = new Book();
book1.setName("11111");
book1.setAuthor("aaa");
book1.setPrice(19.56);
book1.setPress("中国");
book1.save();
Book book2 = new Book();
book2.setName("22222");
book2.setAuthor("bbb");
book2.setPrice(199.99);
book2.setPress("Unknow");
book2.save();
}
});
创建一个 Book 实例,对实例赋值,最后调用 Book.save() 方法就能向数据库添加数据
LitePal 查询数据
查询一个表中的所有数据
List<Book> books = DataSupport.findAll(Book.class);
for(Book book:books){
// ......
}
方法 | 说名 | 用例 |
findAll() | 查询指定表中的所有数据 | List books = DataSupport.findAll(Book.class) |
findFirst() | 查询指定表中的第一条数据 | Book book = DataSupport.findFirst(Book.class) |
findLast() | 查询指定表中的最后一条数据 | Book book = DataSupport.findLast(Book.class) |
select() | 指定查询那几列数据 | List books = DataSupport.select(“name”, “author”).find(Book.class) |
where() | 指定查询的约束条件 | List books = DataSupport.where(“price > ?”, “100”).find(Book.class) |
orfer() | 指定查询结果的排序方式 | List books = DataSupport.order(“price desc”).find(Book.class) |
limit() | 指定查询结果的数量 | List books = DataSupport.limit(3).find(Book.class) |
offset() | 指定查询结果的偏移量 | List books = DataSupport.limit(3).offset(1).find(Book.class) |
LitePal 数据更新
最简单的更新方式,对已存储的对象重新设值,然后重新调用 save() 进行保存,已存储的对象根据 model.isSaved() 方法判断,返回 true 表示已存储,只有两种情况才会返回 true,一种是已经调用过 model.save() 方法后的 model 对象,另一种是通过查询 API 查询出来的 model 对象
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
List<Book> books = DataSupport.where("price > ?", "100").find(Book.class);
for(Book book:books){
book.setPrice(200.98);
book.save();
}
}
});
可以使用 update() 方法,方法内传入一个 id 参数
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Book book = new Book();
book.setPrice(200.98);
book.updateAll("name = ? and author = ?", "22222", "bbb");
//book.update(1)
}
});
先 new 出了一个Book 对象,然后设置它的价格,再调用 updateAll() 方法进行更新,updateAll() 方法中可以指定一个约束条件,如果不指定就更新所有数据
注意:如果湘江一个字段更新成默认值的时候是不能用 setter() 方法的,Java 任何一种数据类型都有默认值,例如 int 默认值是0,boolean 是 false,String 是 null 等,如果直接调用book.setPrice(0),再调用 book.updateAll() 方法,LitePal 是不会对这个列进行更新的,LitePal 统一提供一个 setToDefault()
方法,传入一个列名参数
Book book = new Book();
book.setToDefault("price");
book.updateAll("name = ? and author = ?", "22222", "bbb");
price 是 double 类型,通过这样就可以将满足条件的列的 price 设置成 Java double 的默认值0
LitePal 删除数据
删除数据方式有两种,一种是调用已存储对象的 delete() 方法,另一种是调用 DataSupport对象方法 DataSupport.delete() 方法接收两个参数,删除数据的表和 id,DataSupport.deleteAll() 方法,接收两个参数,第一个是删除数据的表,第二个是删除数据的约束条件,不指定第二个参数将删除表中所有的数据
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
DataSupport.deleteAll(Book.class, "price > ?", "100");
}
});
将Book表中价格大于100的书籍删除