本章我们介绍了数据存储方案:文件存储、SharedPreference和SQLite。文件存储的读写,SharedPreference的读写和实现记住密码功能;SQLite的创建升级数据库以及CRUD;SQLite数据库的最佳实践:事务以及升级数据库;最后我们讲了利用高阶函数和KTX库简化SharedPreference和ContentValues的写法。
7.1.持久化技术简介
保存于内存的数据是瞬时状态,瞬时数据无法持久化保存。保存在存储设备的数据是持久状态的,有3种方式可以实现:文件存储、SharedPreference存储和数据库存储。
7.2.文件存储
作用数据原封不动的保存到文件中,适用于存储简单地文本数据或二进制数据。
新建项目FilrPersistenceTest项目,使用Context类的openFileoutput方法可以将数据存储至指定文件夹,openFileInput读取指定的文件。MainActivity中的onDestroy方法中调用save方法,onCreate方法中调用load方法,代码示例如下:
package com.example.myapplication
import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_main.*
import java.io.*
import java.lang.StringBuilder
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val inputtext = load()
//如果读到内容不为空,setText填充后使用setSelection将光标移动到末尾位置
if (inputtext.isNotEmpty()) {
edit_text.setText(inputtext)
edit_text.setSelection(inputtext.length)
Toast.makeText(this, "Restore succeed", Toast.LENGTH_SHORT).show()
}
}
//销毁之前调用了save方法用于存储文本框内的数据
override fun onDestroy() {
super.onDestroy()
val inputText = edit_text.text.toString()
save(inputText)
}
fun save(inputText: String) {
try {
//Context提供openFileOutput方法,将数据存储到指定文件夹,第一个参数是文件名,默认存储至data/data/packagename/file目录下
// 第二个参数是文件的操作模式,MODE_PRIVATE和MODE——APPEND,前者表示相同文件名,写入内容覆盖之前内容;后者该文件已存在,追加内容,不存在创建文件
//第一步:openFileOutput构建FileOutputStream对象
val output = openFileOutput("data", Context.MODE_PRIVATE)
//第二步:构建一个OutputStreamWriter对象后构造一个BufferWriter对象
val writer = BufferedWriter(OutputStreamWriter(output))
//第三步:通过bufferwriter开始将文本内容写入文件当中
//Kotlin内置扩展函数use,他保证了代码执行完成之后自动将外层流关闭,无需finally方法手动关闭流
writer.use {
it.write(inputText)
}
} catch (e: IOException) {
e.printStackTrace()
}
}
fun load(): String {
val content = StringBuilder()
try {
//第一步:openFileInput获取FileInputStream对象
val input = openFileInput("data")
//第二步:借助它构建InputStreamReader后构建BufferedReader对象
val reader = BufferedReader(InputStreamReader(input))
//第三步:forEachLine内置扩展函数,每行内容都会调到Lambda表达式并拼接
reader.use {
reader.forEachLine {
content.append(it)
}
}
} catch (e: IOException) {
e.printStackTrace()
}
return content.toString()
}
}
相应的xml文件:
7.3.SharedPreference存储
文件存储不适合存储复杂结构型数据,SharedPreference使用键值对来存储数据,读取数据时根据键将值读取出来,支持多种不同数据类型存储。
7.3.1.将数据存储到SharedPreference当中
主要通过Context的getSharedPreference方法获取SharedPreference对象,调用edit()后获取对象,putString等方法存储键值对,最后使用apply方法进行提交。
7.3.2.从SharedPreference中读取数据
很简单,要通过Context的getSharedPreference方法获取SharedPreference对象,调用getString等方法根据键获取相应的值。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
save_button.setOnClickListener {
//第一步:获取SharedPreference对象,有两种方式:context类的getSharedPreferences和Activity类的getPreference方法,前者可以指定文件名,后者将Activity类名作为文件名
//第二步:调用SharedPreference对象的editor方法获取相应的对象
val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
//第三步:Editor对象添加数据,添加布尔类型数据、String类型等数据
editor.putString("name", "Tom")
editor.putInt("age", 20)
editor.putBoolean("married", false)
//第四步:apply方法将数据进行提交,完成数据库存储操作
editor.apply()
}
restore_button.setOnClickListener{
val prefs = getSharedPreferences("data",Context.MODE_PRIVATE)
//两个参数:第一个是键,根据键得到相应的值;第二个是默认值,如果键找不到对应的值回忆默认值返回。
val name = prefs.getString("name","")
val age = prefs.getInt("age",0)
val married = prefs.getBoolean("married",false)
Log.d("MainActivity","name is $name")
Log.d("MainActivity","age is $age")
Log.d("MainActivity","married is $married")
}
}
}
相对应的xml文件:
7.3.3.基于SharedPreference实现记住密码的功能
打开BroadcastBestPractice项目,修改布局文件Activity_login.xml项目,复选框控件来表示用户是否需要记住密码:
....
....
修改LoginActivity,利用SharedPreference实现记住密码的功能:
/**
* 功能:非常简单的登录功能,若成功,跳转至MainActivity,否则提示账号或者密码错误
*/
class LoginActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
val prefs = getPreferences(Context.MODE_PRIVATE)
val isRemember = prefs.getBoolean("remember_password", false)
if (isRemember) {
//将账号和密码都输入到文本框中
val account = prefs.getString("account", "")
val password = prefs.getString("password", "")
account_edit.setText(account)
password_edit.setText(password)
rememberPass.isChecked = true
}
login.setOnClickListener {
val account = account_edit.text.toString()
val password = password_edit.text.toString()
if (account == "admin" && password == "123") {
val editor = prefs.edit()
if (rememberPass.isChecked) {//检查复选框是否被选中
editor.putBoolean("remember_password", true)
editor.putString("account", account)
editor.putString("password", password)
} else {
editor.clear()
}
editor.apply()
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
} else {
Toast.makeText(this, "account or pass is invalid", Toast.LENGTH_SHORT).show()
}
}
}
}
7.4.SQLite数据库存储
SQLite是轻量级关系型数据库,SharedPreference适用于存储简单数据和键值对,数据量大、结构性复杂的数据应当考虑使用SQLite数据库。
7.4.1.创建数据库
SQLiteOpenHelper是一个抽象类,用于简单对数据库进行创建和升级。创建DatabaseTest项目。新建MyDatabaseHelper类。
package com.example.myapplication
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.widget.Toast
//SQLiteHelper是一个抽象类,构造方法接收四个参数:context、数据库名、返回自定义cursor一般为null、数据库版本号
//创建数据库文件一般会放在data/data/packagename/databases/目录下,一般需要复写onCreate和onUpGrade方法
class MyDatabaseHelper(val context: Context, name: String, version: Int) :
SQLiteOpenHelper(context, name, null, version) {
//创建Book表,integer是正整型,real是浮点型,text是文本类型, blob是二进制类型。
//primary key将id列为主键,autoincrement表示id列是自增长的。
private val createbooks = "create table Book(" +
"id integer primary key autoincrement," +
"author text," +
"prices real," +
"pages integer," +
"name text)"
override fun onCreate(db: SQLiteDatabase?) {
//执行建表语句
db?.execSQL(createbooks)
Toast.makeText(context, "Create succeed", Toast.LENGTH_SHORT).show()
}
override fun onUpgrade(db: SQLiteDatabase, oldversoin: Int, newversion: Int) {
TODO("Not yet implemented")
}
}
其次,修改布局文件为一个按钮:
最后修改MainActivity的方法,调用创建数据库的getWritablebase方法。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//构造一个SQLiteOpenHelper对象,通过构造函数将数据库指定为book.db,版本号为1.
val dbHelper = MyDatabaseHelper(this,"BookStore.db",1)
create_db.setOnClickListener{
//点击后调用getWritableDatabase或者getReadableDatabase创建或打开一个现有的数据库,若存在直接打开,否则创建新的。
//并返回一个可读对象。
//若满时,前者出现异常,后者返回可读对象。
dbHelper.writableDatabase
}
}
}
7.4.2.升级数据库
MyDatabaseHelper类复写的onUpgrade方法用于对数据库进行升级,已经有Book表,再添加category表用于记录图书分类。
三步走:1.建立SQL语句以创建category表,在onCreate方法中执行;2.在onUpgrade方法中添加删表语句以及调用onCreate方法;3.为使onUpgrade方法执行,SQLiteOpenHelper的数据库版本号要比之前的大。
//SQLiteHelper是一个抽象类,构造方法接收四个参数:context、数据库名、返回自定义cursor一般为null、数据库版本号
//创建数据库文件一般会放在data/data/packagename/databases/目录下,一般需要复写onCreate和onUpGrade方法
class MyDatabaseHelper(val context: Context, name: String, version: Int) :
SQLiteOpenHelper(context, name, null, version) {
//创建Book表,integer是正整型,real是浮点型,text是文本类型, blob是二进制类型。
//primary key将id列为主键,autoincrement表示id列是自增长的。
private val createbooks = "create table Book(" +
"id integer primary key autoincrement," +
"author text," +
"prices real," +
"pages integer," +
"name text)"
///创建记录图书分类的Castegory的SQL语句
private val createcategory = "create table Category(" +
"id integer primary key autoincrement," +
"category_name text," +
"category_code integer)"
override fun onCreate(db: SQLiteDatabase?) {
//执行建表语句
db?.execSQL(createbooks)
db?.execSQL(createcategory)
Toast.makeText(context, "Create succeed", Toast.LENGTH_SHORT).show()
}
//数据库中已经存在Book表或者Category表,就将这两条表删除,重新执行onCreate方法创建
// 如果发现创建时已经存在此表,那么会直接报错、
override fun onUpgrade(db: SQLiteDatabase, oldversoin: Int, newversion: Int) {
db.execSQL("drop table if exists Book")
db.execSQL("drop table if exists Category")
onCreate(db)
}
}
//只要传入比1大的数,就可以让onUpgrade方法得到执行
val dbHelper = MyDatabaseHelper(this,"BookStore.db",2)
7.4.3.CRUD
CRUD分别指添加insert(create)、查询select(retrive)、更新update(update)和删除delete(delete)。前面提到getWritableDatabase或者getReadableDatabase返回一个SQLiteDataBase对象,借助此可以进行CRUD操作。
修改MainActivity.java增加CRUD的操作:可以使用辅助性方法来完成操作数据库的操作,也可以使用SQL来直接操作数据库。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//构造一个SQLiteOpenHelper对象,通过构造函数将数据库指定为book.db,版本号为1.
//只要传入比1大的数,就可以让onUpgrade方法得到执行
val dbHelper = MyDatabaseHelper(this, "BookStore.db", 2)
//实现创建数据库的功能
create_db.setOnClickListener {
//点击后调用getWritableDatabase或者getReadableDatabase创建或打开一个现有的数据库,若存在直接打开,否则创建新的。
//并返回一个可读对象。
//若满时,前者出现异常,后者返回可读对象。
dbHelper.writableDatabase
}
//实现添加数据的功能
add_data.setOnClickListener {
//获取并返回DataBase对象
val db = dbHelper.writableDatabase
val values1 = ContentValues().apply {
put("name", "The Da Vinci Code")
put("author", "Dan Brown")
put("pages", 454)
put("prices", 16.96)
}
//insert方法用于向表中添加数据
//第一个参数为表名,往那张表中添加;第二个参数用于指定哪列为null,一般不使用;
// 第三个参数是ContentValue对象,提供一系列put方法添加数据。
db.insert("Book", null, values1)
val values2 = ContentValues().apply {
put("name", "The Lost Symbol")
put("author", "Dan Brown")
put("pages", 510)
put("prices", 19.95)
}
db.insert("Book", null, values2)
//直接使用SQL来操作数据库,无需Android的SQLIte辅助性语言
db.execSQL(
"insert into Book(name ,author,pages,prices) values(?,?,?,?)",
arrayOf("The Da Vinci Code", "Dan Brown", "454", "16.96")
)
}
//实现更新数据的功能,将The Da Vinci Code书的价格调低一些
update_data.setOnClickListener {
val db = dbHelper.writableDatabase
val values = ContentValues()
values.put("prices", 10.99)
//update共接收四个参数,第一个是那个表,第二个是要更新的ContentValues数据;
// 第三、第四个参数是更新某一行或者某几行的数据,不指定的话会默认更新所有行
//第三个对应的是SQL语句的where部分,表示更新所有name=?的部分,?是占位符,第四个参数为其提供内容
//arrayof是Kotlin提供的用于便捷创建数据的内置方法
db.update("Book", values, "name=?", arrayOf("The Da Vinci Code"))
db.execSQL(
"update Book set prices =? where name = ?",
arrayOf("10.99", "The Da Vinci Code")
)
}
//删除Book表中超过500页的书
delete_data.setOnClickListener {
val db = dbHelper.writableDatabase
//delete方法第一个参数是表明,第二、三个是用于约束删除某一行或几行的参数,不指定的话会删除所有
db.delete("Book", "pages>?", arrayOf("500"))
db.execSQL("delete from Book where pages >?", arrayOf("500"))
}
//查询Book表中的所有数据,并将其Log出来
query_data.setOnClickListener {
val db = dbHelper.writableDatabase
//query共七个参数,调用后可以返回一个cursor对象
val cursor = db.query("Book", null, null, null, null, null, null)
//将指针移动到第一行的位置后进入循环
if (cursor.moveToFirst()) {
//便利查询到的每一行数据,通过Cursor的getColumnIndex去获取某一列在表中对应位置的索引,然后通过索引来获取数值。最后Log出来。
do {
//遍历cursor对象
val name = cursor.getString(cursor.getColumnIndex("name"))
val author = cursor.getString(cursor.getColumnIndex("author"))
val pages = cursor.getString(cursor.getColumnIndex("pages"))
val prices = cursor.getString(cursor.getColumnIndex("prices"))
Log.d("MainActivity", "book name is $name")
Log.d("MainActivity", "book author is $author")
Log.d("MainActivity", "book pages is $pages")
Log.d("MainActivity", "book prices is $prices")
} while (cursor.moveToNext())
}
cursor.close()
val cursor1 = db.rawQuery("select * from Book", null)
cursor1.close()
}
}
}
7.5.SQLite数据库的最佳实践
7.5.1.使用事务
SQLite支持事务,事务特性是ACID:原子性(Atomicity)是操作这些指令时,要么全部执行成功,要么全部不执行;一致性(Consistency)是事务的执行使数据从一个状态转换为另一个状态,但是对于整个数据的完整性保持稳定;隔离性(Isolation)是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。持久性(Durability)是当事务正确完成后,它对于数据的改变是永久性的。
在DatabaseTest项目基础上修改,譬如Book表全部废除替换成新数据,delete之后insert,要保证删除旧数据和添加新数据的操作必须一起完成,否则继续保留原来的旧数据。
replace_data.setOnClickListener {
val db = dbHelper.writableDatabase
db.beginTransaction()//beginTransaction开启事务
try {
db.delete("Book", null, null)
//手动抛出异常,让事务失败,代码回滚,事务的原子性。注释掉可以成功删除该表
// if (true) {
// //手动抛出异常,让事务失败
// throw NullPointerException()
// }
val values = ContentValues().apply {
put("name", "Game of Thrones")
put("author", "George Martin")
put("pages", 720)
put("prices", 20.85)
}
db.insert("Book", null, values)
//表示事务已经执行成功了
db.setTransactionSuccessful()
} catch (e: Exception) {
e.printStackTrace()
} finally {
//结束事务
db.endTransaction()
}
}
7.5.2.升级数据库的最佳写法
之前onupgrade有点粗暴,有没有更好地数据库升级方式?答案是有的。第二版是在只有Book表的基础上再添加Category表;第三版是给Book表添加category_id列。充分考虑到跨版本升级与新版本安装时的情况,代码示例如下:
package com.example.myapplication
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.widget.Toast
//SQLiteHelper是一个抽象类,构造方法接收四个参数:context、数据库名、返回自定义cursor一般为null、数据库版本号
//创建数据库文件一般会放在data/data/packagename/databases/目录下,一般需要复写onCreate和onUpGrade方法
class MyDatabaseHelper(val context: Context, name: String, version: Int) :
SQLiteOpenHelper(context, name, null, version) {
//创建Book表,integer是正整型,real是浮点型,text是文本类型, blob是二进制类型。
//primary key将id列为主键,autoincrement表示id列是自增长的。
private val createbooks = "create table Book(" +
"id integer primary key autoincrement," +
"author text," +
"prices real," +
"pages integer," +
"name text," +
"category_id integer)"
///创建记录图书分类的Castegory的SQL语句
private val createcategory = "create table Category(" +
"id integer primary key autoincrement," +
"category_name text," +
"category_code integer)"
override fun onCreate(db: SQLiteDatabase?) {
//执行建表语句
db?.execSQL(createbooks)
db?.execSQL(createcategory)
Toast.makeText(context, "Create succeed", Toast.LENGTH_SHORT).show()
}
//数据库中已经存在Book表或者Category表,就将这两条表删除,重新执行onCreate方法创建
// 如果发现创建时已经存在此表,那么会直接报错、
override fun onUpgrade(db: SQLiteDatabase, oldversoin: Int, newversion: Int) {
// db.execSQL("drop table if exists Book")
// db.execSQL("drop table if exists Category")
// onCreate(db)
//第二版加入了添加category表的功能,无论版本是1、2都可以执行到。要充分考虑到第一版升到第二版或直接升到第三版的情况。
if (oldversoin <= 1) {
db.execSQL(createcategory)
}
//第三版加入了category_id列,无论版本号是1、2、3,都可以照顾到
if (oldversoin <= 2) {
db.execSQL("alter table Book add column category_id integer")
}
}
}
7.6.Kotlin课堂:高阶函数的应用
7.6.1.简化SharedPreference的用法
利用高阶函数简化SharedPreference的用法,之前的代码如下:
//第1步:调用SharedPreference对象的editor方法获取相应的对象
val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
//第2步:Editor对象添加数据,添加布尔类型数据、String类型等数据
editor.putString("name", "Tom")
editor.putInt("age", 20)
editor.putBoolean("married", false)
//第3步:apply方法将数据进行提交,完成数据库存储操作
editor.apply()
创建SharedPreference.kt文件:
package com.example.myapplication
import android.content.SharedPreferences
//SharedPreferences类中添加一个open函数,接收函数类型的参数。open函数内拥有SharedPre的上下文
fun SharedPreferences.open(block:SharedPreferences.Editor.() -> Unit){
//直接掉用edit来获取SharedPreference.Editor对象。
val editor = edit()
//open函数接受的是SharedPreferences.Editor的函数类型参数,调用editor.block()对函数类型参数进行调用
editor.block()
//最后提交数据
editor.apply()
}
接着使用SharedPreference调用高阶函数存储数据:
//直接在SharedPreference对象上调用open函数,接着在Lambda表达式中完成数据的添加操作
getSharedPreferences("data", Context.MODE_PRIVATE).open {
//此时拥有了SharedPreference.Editor的上下文环境,只需要调用相应的put方法即可。
putString("name", "Tom")
putInt("age", 28)
putBoolean("married", false)
}
7.6.2.简化ContentValues的用法
ContentValues的普通写法:
val values2 = ContentValues()
values2.put("name", "The Lost Symbol")
values2.put("author", "Dan Brown")
values2.put("pages", 510)
values2.put("prices", 19.95)
db.insert("Book", null, values2)
//ContentValues写法利用apply简化:
val values2 = ContentValues().apply {
put("name", "The Lost Symbol")
put("author", "Dan Brown")
put("pages", 510)
put("prices", 19.95)
}
这样还不够,利用mapOf函数的写法,使用”Apple” to 1语法结构快速创建一个键值对。在Kotlin中可以使用A to B的语法结构创建一个Pair对象。创建ContentValues.kt文件。
package com.example.myapplication
import android.content.ContentValues
//cvOf及接受一个Pair参数,在参数前面使用了vararg关键字,即允许0个、1个或者多个Pair类型的参数。这些参数会被传入然后通过for-in循环
//fun cvOf(vararg pairs: Pair): ContentValues {
// //Pair类型实践支队,ContentValues的键是String类型,将Pair键的泛型指定为String,值可以为任意类型,Any?允许传入空值、字符串类等等类型
// val cv = ContentValues()
// //遍历Pairs参数列表,取出其中的值放在ContentValues中。利用when判断并覆盖ContentValues支持的所有类型
// for (pair in pairs) {
// val key = pair.first
// val value = pair.second
// when (value) {
// is Int -> cv.put(key, value)
// is Long -> cv.put(key, value)
// is Short -> cv.put(key, value)
// is Float -> cv.put(key, value)
// is Double -> cv.put(key, value)
// is Boolean -> cv.put(key, value)
// is String -> cv.put(key, value)
// is Byte -> cv.put(key, value)
// is ByteArray -> cv.put(key, value)
// null -> cv.putNull(key)
// }
// }
// return cv
//}
//利用高阶函数再行简化
fun cvOf(vararg pairs: Pair) = ContentValues().apply {
for (pair in pairs) {
val key = pair.first
val value = pair.second
when (value) {
is Int -> put(key, value)
is Long -> put(key, value)
is Short -> put(key, value)
is Float -> put(key, value)
is Double -> put(key, value)
is Boolean -> put(key, value)
is String -> put(key, value)
is Byte -> put(key, value)
is ByteArray -> put(key, value)
null -> putNull(key)
}
}
}
调用的函数如下:
val values2 = cvOf("name" to "The Lost Symbol","author" to "Dan Brown", "pages" to 510,"prices" to 19.95)
db.insert("Book", null, values2)
另外,更简化的代码如下,我们在build.gradle中导入KTX包,然后简单调用即可:
implementation 'androidx.core:core-ktx:1.1.0'
val values2 = contentValuesOf(
"name" to "The Lost Symbol",
"author" to "Dan Brown",
"pages" to 510,
"prices" to 19.95
)
db.insert("Book", null, values2)