Android持久化的实现有3中方式:
1、文件存储
2、SharedPreferences存储
3、数据库的存储
1、文件存储
文件存储是Android中最基本的持久化方式,它不对存储的内容进行任何格式化处理,会原封不动的存储到文件中,所以这种方式适合存储简单的文本数据或二进制数据。如果想存储比较复杂的结构化的数据需要自定义格式规范,方便进行后续的解析。
1.1、将数据存储到文件中
Context提供了openFileOutput(String name, int mode)方法,用于将数据存储到指定的文件中。该方法接收2个参数:
name:是文件名,不能包含路径,因为它是默认存储在data/data/packageName/files目下的。
mode:表示数据存储的方式,有2个可选值Context.MODE_PRIVATE和Context.MODE_APPEND,Context.MODE_PRIVATE表示相同文件名的情况下会进行数据的覆盖。Context.MODE_APPEND表示文件名相同的情况下会对内容进行追加。
返回值是一个FileOutputStream对象,得到这个对象后就能使用Java流的方式将数据写到文件中了,下面看下具体的使用。
fun writeContent(str: String) {
try {
val fileOutputStream = openFileOutput("content_string.txt", Context.MODE_APPEND)
val bufferWriter = BufferedWriter(OutputStreamWriter(fileOutputStream))
bufferWriter.use {
it.write(str)
}
} catch (e: Exception) {
}
}
这里使用了字符流的方式将数据写入了文件,注意这里我们使用了use函数,这是Kotlin提供的一个扩展内置函数,内部会对Lambda表示进行try catch处理,并在finally中进行关闭流的操作,使用函数use我们就不需要进行流的关闭了。
1.2、从文件中读取数据
Context提供了函数openFileInput它直接收一个文件名的参数,调用该方法会自动去data/data/packageName/files目录下加载指定文件名的文件并返回一个FileInputStream对象,使用这个对象我们就能使用Java流的方法读取数据了。
fun readContent() {
try {
val fileInputStream = openFileInput("content_string.txt")
val bufferedReader = BufferedReader(InputStreamReader(fileInputStream))
bufferedReader.use {
var temp: String?
do {
temp = it.readLine()
Log.e(tag, temp)
} while (temp != null)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
上面写法比较麻烦,这里我们使用Kotlin的另一个扩展函数forEachLine,它的作用是将读取到的每行数据都返回到Lambda表达式中。
fun readContent() {
try {
val fileInputStream = openFileInput("content_string.txt")
val bufferedReader = BufferedReader(InputStreamReader(fileInputStream))
bufferedReader.use {
bufferedReader.forEachLine {
Log.e(tag, it)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
2、SharedPreferences存储方式
不同于文件存储的方式,SharedPreferences存储是采用键值对的方式进行数据的存储。SharedPreferences支持多种不同类型的数据存储,支持的类型有int,float,long,boolea,String,存入的数据是什么类型取出的数据就是什么类型。
2.1、使用SharedPreferences存储数据
使用SharedPreferences存储数据需要获取SharedPreferences对象,获取对象的方式有以下两种:
1、Context类中的getSharedPreferences
public SharedPreferences getSharedPreferences(File file, int mode)
参数file:指定文件名称,如果不存在就会创建一个,不需要指定路径,sp文件默认是存储在/data/data/packagename/shared_prefs目录下的。
参数mode:可选值目前只有MODE_PRIVATE可用,表示只能当前应用程序可以对sp文件进行读写,模式MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE和MODE_MULTI_PROCESS均被废弃。
2、Activity中的getPreferences()
public SharedPreferences getPreferences(Context.PreferencesMode int mode) {
return getSharedPreferences(getLocalClassName(), mode);
}
它直接收一个mode参数,文件名是默认指定为当前Activity的类名。
得到SP的对象后,我们就能使用SP进行数据的存储了,使用步骤如下:
1、调用SP的对象的edit()得到SharedPreferences.Editor对象
2、调用Editor对象的putXX()相关方法存储数据
3、调用Editor对象的commit()或apply()进行数据的提交,commit()返回Boolean类型,表示提交数据成功与否,apply()无返回值。
下面看下使用sp进行数据的存储
fun saveDataBySP(name: String = "阿三", age: Int) {
val sp = getPreferences(Context.MODE_PRIVATE)
val editor = sp.edit()
editor.apply {
putString("name", name)
putInt("age", age)
apply()
}
}
sp文件最终会保存在/data/data/packagename/shared_prefs/目录下,并且文件的xml格式的,内容如下:
大圣
2.2、SharedPreferences读取数据
SharedPreferences的读取数据很简单,SharedPreferences内提供了一系列的getXX()方法,下面我们取出刚才存入的数据。
fun getData() {
val sp = getPreferences(Context.MODE_PRIVATE)
val name = sp.getString("name", "")
val age = sp.getInt("age", 0)
Log.e(tag, "name is $name,age is $age")
}
3、SQLite数据库存储
使用文件存储和SharedPreferences存储方式只能处理一些简单的数据,如果对于数据结构化比较复杂的数据就需要使用数据库存储方式来实现了。
3.1、SQLiteOpenHelper
Android为了更方便的管理数据库,专门提供了一个SQLiteOpenHelper抽象类,使用这个类可以很方便的完成数据库的创建和升级。
SQLiteOpenHelper是一个抽象类,在使用时候我们需要创建一个类来继承该类并实现onCreate()和onUpgrade()这两个抽象方法完成创建和升级的逻辑。
SQLiteOpenHelper提供了两个很重要的实例方法:
public SQLiteDatabase getWritableDatabase() {
synchronized (this) {
return getDatabaseLocked(true);
}
}
public SQLiteDatabase getReadableDatabase() {
synchronized (this) {
return getDatabaseLocked(false);
}
}
getWritableDatabase()和getReadableDatabase()都可以创建或打开一个数据库,如果数据库不存在就创建,存在就打开。它们均返回一个SQLiteDatabase对象,该对象可以对数据库进行读写操作。不同的是当数据库不可写入的时候(如磁盘已满),getReadableDatabase()返回的对象将以只读的方式打开数据库,getWritableDatabase()将抛出异常。
SQLiteOpenHelper类有多个构造函数,我们选用参数较少的即可。
public SQLiteOpenHelper(@Nullable Context context, @Nullable String name,
@Nullable CursorFactory factory, int version) {
this(context, name, factory, version, null);
}
name:数据库名称
factory:允许我们查询数据时返回一个自定义的Cursor,一般传入null即可
version:数据库的版本号,可以用于数据库的升级
SQLiteOpenHelper对象创建完成后,就能调用getWritableDatabase()或getReadableDatabase()去创建数据库了,数据库的文件存储在data/data/packagename/databases目录下,onCreate()方法只有第一次创建数据库时才会调用,我们可以在其中创建数据库中的表。
3.2、创建数据库
下面我们通过实例来学习下SQLiteOpenHelper的使用。
先来创建一个表BookStore,创建表的sql语句如下:
"create table Book(" +
"id integer primary key autoincrement," +
"autor text," +
"price real," +
"pages integer," +
"name text)"
SQLite不像其他数据库拥有繁杂的数据类型,它的数据类型很简单:
integer :表示整形。
real:表示浮点型
text:表示文本类型
blob:表示二进制类型
在上面语句中我们还是用了primary key 将id设为主键,并使用autoincrement表示自增
然后使用SQLiteDatabase.execSQL执行创建表的SQL语句。
class MySQLiteOPenHelper(context: Context, name: String, factory: SQLiteDatabase.CursorFactory? = null, version: Int) :
SQLiteOpenHelper(context, name, factory, version) {
private val tag = javaClass.simpleName
override fun onCreate(db: SQLiteDatabase?) {
Log.e(tag, "onCreate")
db?.execSQL(
"create table Book(" +
"id integer primary key autoincrement," +
"autor text," +
"price real," +
"pages integer," +
"name text)"
)
}
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
}
}
执行下面的代码进行数据的创建以及数据库中表的创建
create_database.setOnClickListener {
val mySQLiteOPenHelper = MySQLiteOPenHelper(this, "BookStore.db", version = 1)
val sqLiteDataBase = mySQLiteOPenHelper.readableDatabase
}
通过上面代码我们就创建了名为BookStore版本为1的数据库,mySQLiteOPenHelper.readableDatabase调用时才真正开始创建数据库,在数据库首次创建时才会会调用MySQLiteOPenHelper中的onCreate()方法,在数据库创建完成后,无论调用mySQLiteOPenHelper.readableDatabase多少次也不会再执行onCreate()方法。数据库会存储在data/data/packagename/databases目录下
image.png
可以看到我们创建的BookStore.db数据库文件,除此之外还有一个文件BookStore.db-journal,它是用于数据库支持事务而产生的临时文件。
将BookStore.db文件导出到指定位置,使用AndroidStudio的插件Database Navigation来查看数据库中的表,可能由于我的AS版本太低了,插件无法正常使用。所以我就使用了如下方法去查看。
1、debugImplementation 'com.amitshekhar.android:debug-db:1.0.4'引入到module的gradle文件中,然后运行app
2、从Log中找到D/DebugDB: Open http://192.168.50.150:8080 in your browser
3、将连接复制到浏览器即可查看
image.png
可以看到BookStore.db中确实有一个Book表,并且表中的列和我们创建语句完全匹配。
3.3、升级数据库
在BookStore.db中已经有Book表,如果我们想在数据库中增加一张Category表,此时就需要升级数据库了。
还记得MySQLiteOPenHelper中我们实现的方法onUpgrade是空实现,在数据库升级时该方法就会被回调。
1、首先我们需要在onCreate()中增加一条创建表的语句,这个很好理解,当我重新安装app时数据库首次创建时BookStore.db中肯定要有Book和Category两张表的。
2、在onUpgrade()中进行版本判断,比如我们数据库版本为1时数据库中只有一个表Book,然后升级到版本2时,我们就需要新增一个Category表。
具体代码如下:
class MySQLiteOPenHelper(context: Context, name: String, factory: SQLiteDatabase.CursorFactory? = null, version: Int) :
SQLiteOpenHelper(context, name, factory, version) {
private val tag = javaClass.simpleName
private val create_book = "create table Book(" +
"id integer primary key autoincrement," +
"autor text," +
"price real," +
"pages integer," +
"name text)"
private val create_category = "create table Category(" +
"id integer primary key autoincrement," +
"category_name text," +
"category_code integer)"
override fun onCreate(db: SQLiteDatabase?) {
Log.e(tag, "onCreate")
db?.let {
it.execSQL(create_book)
it.execSQL(create_category)
}
}
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
when (oldVersion) {
1 -> db?.execSQL(create_category)
}
}
}
然后将数据库版本指定为2,表示对数据库的升级
create_database.setOnClickListener {
val mySQLiteOPenHelper = MySQLiteOPenHelper(this, "BookStore.db", version = 2)
val sqLiteDataBase = mySQLiteOPenHelper.readableDatabase
}
此时再用工具查看下BookStore.db
image.png
可以看到数据库中已经有2张表了,说明我们的升级是有效的。
3.4、数据的增删改查
调用mySQLiteOPenHelper.readableDatabase返回一个SQLiteDatabase对象,我们使用这个对象就可以完成数据的增删改查。
3.4.1、数据的添加
SQLiteDatabase中提供了insert方法,用于数据的添加。
public long insert(String table, String nullColumnHack, ContentValues values) {}
table:指定向那个表中添加数据
nullColumnHack:用于未指定添加数据的情况,给某些可为空的列自动赋值为NULL,一般用不到这个功能,直接传null即可。
values:ContentValues对象提供了一系列put方法用于向ContentValues中添加数据,只需要将表中的每个列名以及待添加的数据添加进去即可。
fun insertData() {
//插入第一条数据
val contentValues1 = ContentValues()
contentValues1.apply {
put("autor", "施耐庵")
put("price", 200)
put("pages", 3000)
}
sqLiteDataBase.insert(TABLE_BOOK, null, contentValues1)
//插入第二条数据
val contentValues2 = ContentValues()
contentValues2.apply {
put("autor", "金庸")
put("price", 120)
put("pages", 1000)
}
sqLiteDataBase.insert(TABLE_BOOK, null, contentValues2)
}
上面我们插入了两条数据,现在查看下Book表中的数据是不是我们添加的
image.png
3.4.2、数据的更新
SQLiteDatabase提供了update方法,去更新数据库中的数据
public int update(String table, ContentValues values, String whereClause, String[] whereArgs) {}
table:要更新的表名
values:把更新的数据组装在ContentValues中
whereClause和whereArgs:用于约束更新某一行或某几行的数据,如果不指定则更新所有行的数据。
具体代码如下:
fun updateData() {
val contentValues = ContentValues()
contentValues.apply {
put("autor", "曹雪芹")
put("price", 400)
put("pages", 4000)
}
sqLiteDataBase.update(TABLE_BOOK, contentValues, "autor=? and id=?", arrayOf("施耐庵", "3"))
}
可以看到这里使用第三个参数和第四个参数指定要更新那几行,第三个参数相当于SQL语句的where部分,表示更新autor=?并且id=?的行,?是占位符和第四个参数提供的字符串数组中的对应的数据进行拼接。arrayOf()方法是Kotlin提供的便捷创建数组的内置方法。可以看到我们这里更新autor=施耐庵 and id=3对应的行。更新结果如下:
image.png
3.4.3、数据的删除
SQLiteDatabase提供了delete()方法对数据库数据进行删除。
public int delete(String table, String whereClause, String[] whereArgs) {}
table:要操作的表名
第二个参数和第三个参数是用于约束删除某一行或某几行的数据
代码很简单:
fun deleteData() {
sqLiteDataBase.delete(TABLE_BOOK, "id=?", arrayOf("3"))
}
3.4.4、数据的查询
SQLiteDatabase提供了query()方法查询数据,这个方法比较复杂,最短的参数也有7个,先来看下每个参数的具体意义。
public Cursor query(String table, String[] columns, String selection,
String[] selectionArgs, String groupBy, String having,
String orderBy) {
table:表名
columns:指定查询那几列,如果不指定则查询所有列
selection和selectionArgs:用于约束查询某一行或某几行的数据,如果不指定则查询所有行。
groupBy:用于指定需要groupBy的列,不指定则不对查询结果进行groupBy操作。
having:对groupBy之后的数据进行过滤,不指定则不过滤。
orderBy:用于指定查询结果的排序,不指定则按默认排序。
更多详细的内容看下表
query()方法参数
对应SQL部分
描述
columns
select column1,column2
指定查询的列名
table
from table
指定从哪个表中查询
selection
where column=value
指定where约束条件
selectionArgs
-
为where约束条件占位符指定具体的值
groupBy
groupBy column
指定groupBy的列
having
having column=value
对groupBy后的结果进一步约束
orderBy
order by column1,column2
指定查询结果的排序
query()方法返回一个Cursor对象,查询到的所有数据都从这个对象取出。
下面举个实例来看下query方法的使用。
为了讲解groupBy和having的用法,我们再创建一个表Order并向其中添加数据,上面已经学习了表的创建和数据的添加,这里就不再展示了,直接上添加数据后的图:
image.png
groupBy用于对结果集进行分组,一般是和函数搭配使用,例如查询Order表中每个用户的总计金额:
fun queryOrderData() {
val cursor = sqLiteDataBase.query(
TABLE_ORDERS,
arrayOf("Customer", "OrderDate", "SUM(OrderPrice)"),
null,
null,
"Customer",
null,
null
)
if (cursor.moveToFirst()) {
do {
val orderData = cursor.getString(cursor.getColumnIndex("OrderDate"))
val orderPrice = cursor.getInt(cursor.getColumnIndex("SUM(OrderPrice)"))
val customer = cursor.getString(cursor.getColumnIndex("Customer"))
Log.e(tag, "orderData=${orderData},orderPrice=$orderPrice,customer=$customer")
} while (cursor.moveToNext())
}
cursor.close()
}
//打印结果如下:
orderData=2008/08/06,orderPrice=2000,customer=Adams
orderData=2008/09/28,orderPrice=2000,customer=Bush
orderData=2008/07/21,orderPrice=1700,customer=Carter
这里我们通过函数SUM结合groupBy Customer查询每个用户的订单总金额。从打印结果中可以看出和我们预期的结果一致。
从查询返回的结果中,通过列名SUM(OrderPrice)来查询订单总金额的列的数据,我们还可以通过SUM(OrderPrice) as Total来修改列名,修改后从Cursor中获取数据时就可以通过Total来查询了。
having必须配合groupBy一起使用,单独使用having是会报错滴,having是对groupBy后的数据进行再次过滤,使用如下:
val cursor = sqLiteDataBase.query(
TABLE_ORDERS,
arrayOf("Customer", "OrderDate", "SUM(OrderPrice)"),
null,
null,
"Customer",
"SUM(OrderPrice)>1700",
null
)
//打印结果如下:
orderData=2008/08/06,orderPrice=2000,customer=Adams
orderData=2008/09/28,orderPrice=2000,customer=Bush
从打印结果可以看出,在groupBy分组后数据的基础上过滤掉总金额小于1700的数据。
3.4.4、数据库事务
事务特性就是保证一系列的操作要么全部成功,要么全部失败。最典型的就是银行的转账业务,转账业务包括从转账方扣除转账金额和向收款方增加相同的金额,如果在扣除成功后系统出现异常,那么收款方增加相同金额的操作就无法执行,那么此时就需要使用事务来解决这个问题了。
比如我们想删除Book表中的数据并且向表中添加新的数据,将删除和添加的操作添加到事务中。
private fun replaceData() {
sqLiteDataBase.beginTransaction()
try {
sqLiteDataBase.delete(TABLE_BOOK, null, null)
val contentValues = ContentValues()
contentValues.apply {
put("autor", "孙悟空")
put("price", 800)
put("pages", 8000)
put("name", "西游记")
}
if (true)
throw NullPointerException()
sqLiteDataBase.insert(TABLE_BOOK, null, contentValues)
sqLiteDataBase.setTransactionSuccessful()
} catch (e: Exception) {
e.printStackTrace()
} finally {
sqLiteDataBase.endTransaction()
}
}
上述代码是数据库事务的标准使用,步骤如下:
1、使用sqLiteDataBase.beginTransaction()开启事务
2、执行相应的操作
3、在所有操作执行完成后,调用sqLiteDataBase.setTransactionSuccessful()表示事务执行成功了。
4、最后在finally中调用sqLiteDataBase.endTransaction()结束事务。
上面代码中我们手动抛出一个异常,所以执行不到事务成功的方法,所以删除Book表的操作是没有完成的。