持久化技术
持久化技术就是将那些在内存中的瞬时数据存储到存储设备中,使其成为持久数据
- 文件存储
- SharedPregerences存储
- 数据库存储
文件存储
数据存储到文件中
Context类中提供了openFileOutput()方法将数据存储到指定的文件里面,默认会把所有文件存储到/data/data//file/目录下。
有两个参数:
- 文件名,
- 文件的操作模式(有两种操作模式):
MODE_PRIVATE
(默认就是):表示当指定相同文件夹的时候,所写入的内容会覆盖原文件的内容MODE_APPEND
:表示存在就在之后追加,不存在就创建
注意:其实还有两种操作模式,但是过于危险,很容易引起安全漏洞,所以在Android4.后被废除MODE_WORLD_READABLE
MODE_WORLD_WRITEABLE
这两种模式表示允许其他应用程序对我们程序中的文件进行读写操作
实践出真知
可以编写一个输入框,然后把输入框里的内容保存下来
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
···>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/editText"
android:hint="写点什么"/>
···
</LinearLayout>
class MainActivity : AppCompatActivity() {
···
override fun onDestroy() {
super.onDestroy()
/*确保在程序销毁前会把数据保存到储存设备里面*/
val inputText=editText.text.toString()
save(inputText)
}
private fun save(inputText: String) {
try {
/*通过openFileOutput打开一个文件输出流,得到一个FileOutStream对象*/
val output=openFileOutput("data",Context.MODE_PRIVATE)
/*接着使用FileOutStream对象构建出一个BufferedWriter对象*/
val write=BufferedWriter(OutputStreamWriter(output))
/*用BufferedWriter对象把文字写到文件里面*/
write.use {
//use函数是kotlin提供的一个内置扩展函数,他确保表达式中的代码全部执行完了之后自动将外层的流关闭
//这样就不需要自己手动去关闭流了
it.write(inputText)
}
}catch (e:IOException){//异常处理
e.printStackTrace()
}
}
}
读取文件中的数据
Context类中还提供了一个OpenFileInPut()方法读取数据只有一个参数
- 参数:要读取的文件的文件名字
- 系统会自动去默认保存位置加载文件,并且返回一个FileInputStream对象
- 得到对象之后就可以读取数据了
实践出真知
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val inputText=load()
if(inputText.isNotEmpty()){
editText.setText(inputText)
/* 不太明白为什么这里不能用editText.text=inputText*/
Log.d("MainActivity","inputText=${inputText}")
editText.setSelection(inputText.length)//这个是把输入光标移动到文本的末尾
Toast.makeText(this,"加载数据成功",Toast.LENGTH_LONG).show()
}
}
private fun load(): String {
val content=StringBuilder()
try {
val input=openFileInput("data")
val reader=BufferedReader(InputStreamReader(input))
reader.forEachLine { //输入流读取文件的每一行回调到lambda表达式
content.append(it)
}
}catch (e:IOException){
e.printStackTrace()
}
return content.toString()
}
···
}
SharedPreferences存储
- SharedPreferences存储使用键值对的方式来存储数据
- 也就是说,当存储一条数据的时候,需要给这个数据踢狗一个对应的键,这样就可以通过键把值取出来
- 而且存储的是什么数据类型,读出来的就是什么数据类型
将数据存储到SharedPreferences中
Context类中的getSharedPreferences()方法
两个参数
- 第一个用于指定SharedPreferences文件的名称,不存在就创建,SharedPreferences的文件都是存储在/data/data//shared_prefs/目录下
- 第二个用于指定操作模式
MODE_PRIVATE
他和直接传入0的效果一样,表示当前的应用程序才可以对这个SharedPreferences文件读写- 废弃的操作模式
MODE_WORLD_READABLE
在Android4.2被废弃MODE_WORLD_WRITEABLE
在Android6.0被废弃
Activity类中的getPreferences()方法
- 一个参数 :操作模式参数
- 因为使用这个方法会自动将当前的Activity的类名作为SharedReferences的文件名
- 得到SharedPreferences对象后就可以向SharedPreferences文件中存储数据了
- 调用SharedPreferences对象的edit()方法获取一个SharedPreferences.Editor对象
- 向SharedPreferences.Editor对象中添加数据,比如添加Boolean数据就用putBollean()等等
实践出真知
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
···>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/saveButton"
android:text="保存数据"/>
···
</LinearLayout>
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
saveButton.setOnClickListener {
var editor= getSharedPreferences("data", Context.MODE_PRIVATE).edit()
editor.putString("name","波")
editor.putInt("age",21)
editor.putBoolean("married",false)
editor.apply()//用apply()完成提交
}
···
}
}
从SharedPreference中读取数据
SharedPreferences对象中提供了一系列的get方法用来读取数据,每种都对应getSharedPreferences().edit中的put()方法
实践出真知
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
···>
···
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/loadButton"
android:text="读取数据"/>
</LinearLayout>
实操一下才能熟练
- 我们可以复用以下上一章的广播训练的代码
- 我们只需要改动两个地方就可以实现记住账户和密码
LoginLayout.xml中
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
···>
···
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<!--这是一个复选框,用户可以通过点击来选中和取消-->
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/rememberPass"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:text="记住密码"
android:id="@+id/rememberPassTextView"/>
</LinearLayout>
···
</LinearLayout>
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","bi")
val password=prefs.getString("password","12345")
accountEdit.setText(account)
if (account != null) {
accountEdit.setSelection(account.length)
}
passwordEdit.setText(password)
if (password != null) {
passwordEdit.setSelection(password.length)
}
rememberPass.isChecked=true
}
//登录逻辑,登陆成功就进入MainActivity
login.setOnClickListener {
val account=accountEdit.text.toString()
val password=passwordEdit.text.toString()
//账号:bo,密码:123456,就成功登录
if(account=="bo"&&password=="123456"){
val editor=prefs.edit()
if(rememberPass.isChecked){
editor.putBoolean("remember_password",rememberPass.isChecked)
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,"账号或密码错误", Toast.LENGTH_LONG).show()
}
}
}
}
重中之重-SQLite数据库存储
- 关系型数据库
- 轻量级,占用资源少,通常几百kB就成了
- 不需要账户和密码,已经嵌入到了Android系统中
创建数据库
SQLiteOpenHelper
帮助类可以简单的创建数据库和升级
SQLiteOpenHelper
是一个抽象类
我们需要创建一个帮助类来继承他,才能使用他,SQLiteOpenHelper
中有两个抽象方法
onCreate()
:创建数据库onUpgrade()
: 升级数据库
SQLiteOpenHelper
中有两个非常重要的实例方法
这两个方法都可以创建或者打开一个现有的数据库,斌且返回一个可对数据库进行读写操作的对象
getReadableDatabase()
: 当数据库不能写入(磁盘满了)时,会以只读的方式打开数据库getWritableDatabase()
: 当数据库不能写入(磁盘满了)时,会出现异常
SQLiteOpenHelper
有两个构造方法可以重写
SQLiteOpenHelper(context,databaseName,cursor,id)
comtext
:上下文databaseName
:数据库名称cursor
:查询数据时会返回一个自定义的Cursor,一般传入nullid
:当前数据库的版本号
创建的数据库文件存放在/data/data//databases/目录下,创建时onCreate()会调用
,处理一些创建表的逻辑
实践出真知
布局,点击按钮就创建数据库和表
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/createDatabase"
android:text="创建数据库"/>
</LinearLayout>
SQL语句不熟就快tm去练
class MyDatabaseHelper(private val context: Context, private val name:String, private val version:Int):
SQLiteOpenHelper(context,name,null,version) {
private val createBook=
"create table Book ("+//创建Book表
"id integer primary key autoincrement,"+//设置一个自增id作为主键
"author text,"+//文本类型
"price real,"+//浮点型
"pages integer,"+//整型
"name text)"//blob是二进制类型
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(createBook)//执行SQL语句
Toast.makeText(context,"表创建成功",Toast.LENGTH_LONG).show()
}
override fun onUpgrade(p0: SQLiteDatabase?, p1: Int, p2: Int) {
TODO("Not yet implemented")
}
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val dbHelper=MyDatabaseHelper(this,"BookStore.db",1)
createDatabase.setOnClickListener {
dbHelper.writableDatabase
}
}
}
会出现一个BookStore.db的数据库文件,里面有一个Book表
升级数据库
class MyDatabaseHelper(private val context: Context, private val name:String, private val version:Int):
SQLiteOpenHelper(context,name,null,version) {
···
private val createCategory=""+
"create table Category(" +
"id integer primary key autoincrement,"+
"category_name text,"+
"category_code integer)"
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(createBook)//执行SQL语句
db.execSQL(createCategory)
Toast.makeText(context,"表创建成功",Toast.LENGTH_LONG).show()
}
//如果数据库已经创建过了,那么直接走onCreate()是不会运行的,那么想加一张表也是做不到的,所以我们可以利用升级数据库,在onUpgrade()中对数据库进行后续的操作
override fun onUpgrade(db: SQLiteDatabase, p1: Int, p2: Int) {
db.execSQL("drop table if exists Book")
//如果不先删除的话,原来的库中室友这个表,再创建就会报错
db.execSQL("drop table if exists category")
onCreate(db)
}
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
···
val dbHelper=MyDatabaseHelper(this,"BookStore.db",2)
//只有版本号更高,才会调用onUpgrade(),升级数据库
···
}
}
数据库的本质就是CRUD
- C(create):添加数据
- R(retrieve):查询数据
- U(update):更新数据
- D(delete):删除数据
添加数据(Create)
SQLiteDatabase
中提供了insert()
方法用来添加数据
一共有3个参数
- 第一个:表名
- 第二个: 未指定添加数据的情况下给某些可为空的列自动赋值
null
,一般传入null
- 第三个: 是一个
ContentValues
对象,他提供了一系列的put()方法重载,用于向ContentValues
中添加数据,只需要将表中的每个列名以及对应的待添加数据传入即可
实践出真知
修改activity_main.xml中
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
···>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/addData"
android:text="添加数据"/>
</LinearLayout>
修改MainActivity.kt中
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
addData.setOnClickListener {
val db=dbHelper.writableDatabase
val values1=ContentValues().apply{
//组装第一条数据
put("name","剑来")
put("author","陈太监")
put("pages",454)
put("price",30.12)
}
db.insert("Book",null,values1)
val values2=ContentValues().apply{
//组装第一条数据
put("name","三体")
put("author","刘慈欣")
put("pages",600)
put("price",100)
}
db.insert("Book",null,values2)
Toast.makeText(this,"点击了添加数据",Toast.LENGTH_LONG).show()
}
}
}
更新数据
update()方法
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
···
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/upDateData"
android:text="更新数据"/>
···
</LinearLayout>
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
···
upDateData.setOnClickListener {
val db=dbHelper.writableDatabase
val values=ContentValues()
//构建一个ContentValues对象,里面存储的是键值对
values.put("price",199)
values.put("name","剑来")
values.put("author","陈太监")
//我们添加了一个键值对(price:199)
db.update("Book",values,"name = ? and author = ?", arrayOf("陈太监","pig"))
//这个update()方法相当于SQL语句:
// UPDATE `Book` SET `price` = 199 WHERE `name` = '剑来'
//arrayOf()是创建一个数组,where和数组中元素一样的就更改
}
···
}
}
删除数据
delete()
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
···
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/deleteData"
android:text="删除数据"/>
···
</LinearLayout>
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
···
deleteData.setOnClickListener {
val db=dbHelper.writableDatabase
db.delete("Book","name = ?", arrayOf("剑来"))
}
···
}
}
查询数据
其实查询是所有里面最复杂的.
query()
方法,最短也只有7个参数的重载,调用query()会返回一个对象,查询道德所有数据都从这个对象中取出
- 第一个: 表名
- 第二个:查询哪些列(筛选条件),不指定就查所有行
- 第三,四个: 约束查询某一行或者某几行的数据,不指定纠察所有行
- 第五个:指定需要去
group by
的列,不指定就不过滤 - 第六个:用于对
group by
过的数据继续过滤,不指定就不过滤 - 第七个:用于指定查询结果的排序方式,不指定就默认排序规则
实践出真知
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
···
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/queryData"
android:text="查询数据"/>
</LinearLayout>
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val dbHelper=MyDatabaseHelper(this,"BookStore.db",1)
···
queryData.setOnClickListener {
val db=dbHelper.writableDatabase
val cursor=db.query("Book",null,null,null,null,null,null,)
if(cursor.moveToFirst()){
do {
//遍历Cursor对象,读出数据打印
val name=cursor.getString(cursor.getColumnIndex("name"))
val author=cursor.getString(cursor.getColumnIndex("author"))
val pages=cursor.getString(cursor.getColumnIndex("pages"))
val price=cursor.getString(cursor.getColumnIndex("price"))
Log.d("MainActivity","${name}的作者是${author},共计${pages}页,${price}元")
}while (cursor.moveToNext())
}
cursor.close()
}
···
}
}
SQLite的实践
事务
事务的特性保证一些列操作要么全部完成要么全部不完成
比如Book中的数据已经很老了,我们准备把旧数据废弃,替换成新数据
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
···
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/replaceData"
android:text="替换数据"/>
</LinearLayout>
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val dbHelper=MyDatabaseHelper(this,"BookStore.db",1)
···
replaceData.setOnClickListener {
val db=dbHelper.writableDatabase
db.beginTransaction()//开启事务
try {
db.delete("Book",null,null)
//throw NullPointerException()
val values=ContentValues().apply{
put("name","龙族")
put("author","江南")
put("pages",543)
put("price",29.53)
}
db.insert("Book",null,values)
db.setTransactionSuccessful()//事务已经成功执行
}catch (e:Exception){
e.printStackTrace()
}finally {
db.endTransaction()//结束事务
}
}
···
}
}
升级数据库的最佳写法
之前对数据库的升级其实非常粗暴,删表重建,这样就带来了问题,删表后用户的数据就丢失了,重建的表中不会有原来的数据,其实这个是由解决办法的.
办法就是:这里需要为每个版本号赋予其所对应的数据库改动,然后再onUpgrade()
中对当前的数据库版本号进行判断,在执行相应的改变就可以了
纸上得来终觉浅,绝知此事要躬行
开始的时候客户的需求只需要有一个数据库和Book表
class MyDatabaseHelper(private val context: Context, private val name:String, private val version:Int):
SQLiteOpenHelper(context,name,null,version) {
private val createBook=
"create table Book ("+//创建Book表
"id integer primary key autoincrement,"+//设置一个自增id作为主键
"author text,"+//文本类型
"price real,"+//浮点型
"pages integer,"+//整型
"name text)"//blob是二进制类型
override fun onCreate(db: SQLiteDatabase) {//之前没安装过程序的就执行onCreate()就行了
db.execSQL(createBook)//执行SQL语句
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
}
}
过了几天客户又想再加一个Category表
class MyDatabaseHelper(private val context: Context, private val name:String, private val version:Int):
SQLiteOpenHelper(context,name,null,version) {
private val createBook=
"create table Book ("+//创建Book表
"id integer primary key autoincrement,"+//设置一个自增id作为主键
"author text,"+//文本类型
"price real,"+//浮点型
"pages integer,"+//整型
"name text)"//blob是二进制类型
private val createCategory=""+
"create table Category(" +
"id integer primary key autoincrement,"+
"category_name text,"+
"category_code integer)"
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(createBook)//执行SQL语句
db.execSQL(createCategory)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
if(oldVersion<=1)//现在版本是2了安装过这个程序,数据库版本还在1的,要升级
db.execSQL(createCategory)
}
}
又过了几天客户觉得要把Book和Category联系起来,所以要在Book表中加一个Category_id属性
class MyDatabaseHelper(private val context: Context, private val name:String, private val version:Int):
SQLiteOpenHelper(context,name,null,version) {
private val createBook=
"create table Book ("+//创建Book表
"id integer primary key autoincrement,"+//设置一个自增id作为主键
"author text,"+//文本类型
"price real,"+//浮点型
"pages integer,"+//整型
"name text,"+//blob是二进制类型
"category_id integer)"
private val createCategory=""+
"create table Category(" +
"id integer primary key autoincrement,"+
"category_name text,"+
"category_code integer)"
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(createBook)//执行SQL语句
db.execSQL(createCategory)
Toast.makeText(context,"表创建成功",Toast.LENGTH_LONG).show()
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
if(oldVersion<=1)//现在版本是2了安装过这个程序,数据库版本还在1的,要升级
db.execSQL(createCategory)
if(oldVersion<=2)//现在版本是3了安装过这个程序,数据库版本还在2以下的,要升级
db.execSQL("alter table Book add column category_id integer")
}
}