数据存储全方案
2021.2.22
Gary哥哥的哥哥的哥哥
详解持久化操作
持久化技术简介
数据持久化就是指那些内存中的瞬时数据保存到存储设备上,保证即使设备开机关机,这些数据仍然不会丢失
Android主要提供了三种方式用于简单实现数据持久化功能:
- 文件存储
- SharedPreference存储
- 数据库存储
下面对上面的三种方式一一展开讲解
文件存储
写入数据
Android中最基本的数据存储方式,他不对存储内容进行任何格式化的处理
- 比较适合存储一些简单的文本数据或二进制数据
- 如果你想保存一些较为复杂的结构化数据,就需要定义一套自己的格式规范了,方便之后数据从文件中重新解析出来
Context类中提供一个openFileOutput()方法,可以用于将数据存储到指定的文件中,两个参数
- p1为文件名
- 文件创建时候使用,注意,不带路径的,有一个默认存储的位置,/data/data/com.workaholiclab.savefile/files/data
- 这里要打开Android studio右下角的Device File Explorer这个工具(如果没找到就ctrl+Shift+A 输入进去)
- p2为文件的操作模式
- MODE_PRIVATE,覆盖原文件内容
- MODE_APPEND,加载源文件后面
- (默认是第一个)
- 返回的是一个FileOutputStream对象,得到这个对象后,可以使用Java流的方式将数据写入文件中
- 注意两种模式,如果文件不存在的话,都会自动创建!!!
下面是简单的代码示例:
fun save(inputText:String){
try {
val output=openFileOutput("data", Context.MODE_PRIVATE)
val writer=BufferedWriter(OutputStreamWriter(output))
writer.use {
it.write(inputText)
}
}catch (e:IOException){
e.printStackTrace()
}
}
前面的操作和Java很类似
这里还用到了一个use函数,是一个Kotlin提供的一个内置扩展函数
它会保证Lambda表达式中的代码全部执行完之后自动将外层的流关闭,这样就不惜要手动写一个finnally语句手动关闭流
下面修改一下activity_main.xml的代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Type something here"
android:id="@+id/editText"/>
</LinearLayout>
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun onDestroy() {
super.onDestroy()
val inputText=editText.text.toString()
save(inputText)
}
fun save(inputText:String){
try {
val output=openFileOutput("data", Context.MODE_PRIVATE)
val writer=BufferedWriter(OutputStreamWriter(output))
writer.use {
it.write(inputText)
}
}catch (e:IOException){
e.printStackTrace()
}
}
}
从文件中读取数据
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.setSelection(inputText.length)
Toast.makeText(this,"Restoring succeeded",Toast.LENGTH_SHORT).show()
}
}
private fun load():String{
val content=StringBuilder()
try {
val input=openFileInput("data")
val reader=BufferedReader(InputStreamReader(input))
reader.use {
reader.forEachLine {
content.append(it)
}
}
}catch (e:IOException){
e.printStackTrace()
}
return content.toString()
}
}
注意下面这条语句:
editText.setSelection(inputText.length)
将输入光标移动到文本的末尾位置便于继续输入
SharedPreferences 存储
保存数据
- 是一种键值对的方式来存储数据
- 支持多种不同的数据结构
- 存储数据和读取的数据是同一类型
- 数据持久化比使用文件方便很多
想要存储数据,就必须先获得SharedPreferences对象,下面提供两种方法获得对象
- Context类中的getSharedPreferences方法
- p1文件的名称,指定目录在:/data/data/com.workaholiclab.savefile/shared_prefs目录下
- p2指定模式,其实目前只有一种模式科学:MODE_PRIVATE,他和直接传入0的效果是一致的
- 表示只有当前应用程序可以对这个SharedPreferences文件进行读写
- Activity类中的getSharedPreferences方法
- 只接受一个操作模式的参数
- 自动将类名作为文件名
得到对象过后,开始想SharedPreferences文件进行存储数据,主要分三步实现:
- 调用SharedPreferences对象的edit方法获取SharedPreferences.Editor对象
- 向上面获得的对象添加数据,比如一个整型的数据就使用putInt()
- 调用apply的方法将添加的数据提交
下面用代码演示一下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical">
<Button
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="Save Data"
android:textAllCaps="false"
android:id="@+id/saveButton"/>
</LinearLayout>
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
saveButton.setOnClickListener {
val editor=getSharedPreferences("data", Context.MODE_PRIVATE).edit()
editor.putString("name","Wendy")
editor.putInt("age",20)
editor.putBoolean("married",false)
editor.apply()
}
}
}
看到数据文件是以xml方式保存的
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="name">Wendy</string>
<boolean name="married" value="false" />
<int name="age" value="20" />
</map>
读取数据
操作很类似
<?xml version="1.0" encoding="utf-8"?>
<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:text="Save Data"
android:textAllCaps="false"
android:id="@+id/saveButton"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Restore Data"
android:id="@+id/restoreButton"/>
</LinearLayout>
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
saveButton.setOnClickListener {
val editor=getSharedPreferences("data", Context.MODE_PRIVATE).edit()
editor.putString("name","Wendy")
editor.putInt("age",20)
editor.putBoolean("married",false)
editor.apply()
}
restoreButton.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)
println("$name $age $married")
}
}
}
get***的第二个参数数默认值,找不到对应的key就当成默认值
记住密码功能实现
利用SharedPreferences存储可以实现简单的记住密码的功能:
- 下面通过上一章节的登录页面修改来实现功能:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".LoginActivity"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:orientation="horizontal">
<TextView
android:layout_width="90dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textSize="18sp"
android:text="Account:"/>
<EditText
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:id="@+id/accountEdit"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:orientation="horizontal">
<TextView
android:layout_width="90dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textSize="18sp"
android:text="Password:"/>
<EditText
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:id="@+id/passwordEdit"
android:inputType="textPassword"/>
</LinearLayout>
<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="Login"
android:textAllCaps="false"/>
</LinearLayout>
<Button
android:layout_width="200dp"
android:layout_height="60dp"
android:layout_gravity="center_horizontal"
android:text="Login"
android:id="@+id/login"
android:textAllCaps="false"/>
</LinearLayout>
class LoginActivity : AppCompatActivity() {
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","")
accountEdit.setText(account)
passwordEdit.setText(password)
rememberPass.isChecked=true
}
login.setOnClickListener {
val account=accountEdit.text.toString()
val password=passwordEdit.text.toString()
if(account=="admin"&&password=="123456")
{
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 password is invalid",Toast.LENGTH_SHORT).show()
}
}
}
}
SQLite 数据库存储
Android是内置了数据库的
SQLite是一款轻量级的关系型数据库,迅速按书读非常快,占用资源很少
- 不仅支持SQL语法
- 遵循数据库的ACID事物
应用:
- 存储大量关系复杂的数据型数据
这里以后有空再把全部的示例讲解代码补上,现在先看看书吧
一系列的增删改查(CRUD)操作
我这里知识简单演示一下代码
- 具体的函数用法见书本p288开始
class MyDatabaseHelper(val context: Context,name:String,version:Int):SQLiteOpenHelper(context,name,null,version) {
private val createBook="create table Book ("+
"id integer primary key autoincrement,"+
"author text,"+
"price real,"+
"pages integer,"+
"name text)"
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)
db?.execSQL(createCategory)
Toast.makeText(context,"Create succeeded",Toast.LENGTH_SHORT).show()
}
//升级数据库,比如像加多一张表
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: 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?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val dbHelper=MyDatabaseHelper(this,"BookStore.db",1)
createDatabase.setOnClickListener {
dbHelper.writableDatabase
}
//添加数据
addData.setOnClickListener {
val db=dbHelper.writableDatabase
val values1=ContentValues().apply {
//开始组装第一条数据
put("name","The Da Vinci Code")
put("author","Dan Brown")
put("pages",454)
put("price",19.95)
}
db.insert("Book",null,values1)//插入第一条数据
val values2=ContentValues().apply {
//开始组装第一条数据
put("name","First Code")
put("author","guolin")
put("pages",692)
put("price",99.00)
}
db.insert("Book",null,values2)//插入第二条数据
Toast.makeText(this,"成功添加数据",Toast.LENGTH_SHORT).show()
}
//更新数据
updateData.setOnClickListener {
val db=dbHelper.writableDatabase
val values=ContentValues()
values.put("price",10.99)
db.update("Book",values,"name = ?", arrayOf("The Da Vinvi Code"))
Toast.makeText(this,"更新数据成功",Toast.LENGTH_SHORT).show()
}
//删除数据
deleteData.setOnClickListener {
val db=dbHelper.writableDatabase
db.delete("Book","pages > ?", arrayOf("500"))
Toast.makeText(this,"删除数据成功",Toast.LENGTH_SHORT).show()
}
//查询数据
queryData.setOnClickListener {
val db=dbHelper.writableDatabase
//查询Book表中所有的标准数据
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.getInt(cursor.getColumnIndex("pages"))
val price=cursor.getDouble(cursor.getColumnIndex("price"))
println("$name $author $pages $price")
}while (cursor.moveToNext())
}
cursor.close()
Toast.makeText(this,"查询数据成功",Toast.LENGTH_SHORT).show()
}
//使用事务
replaceData.setOnClickListener {
val db=dbHelper.writableDatabase
db.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("price",20.85)
}
db.insert("Book",null,values)
db.setTransactionSuccessful()//事务已经执行成功
}catch (e:IOException){
e.printStackTrace()
}finally {
db.endTransaction()//结束事务
}
}
}
}
SQLite的最佳实现
SQLite数据库是支持事务的,事务的特性可以保证让一些列操作要么全部完成,要么一个都不会完成。
- 如,银行转账双方
//使用事务
replaceData.setOnClickListener {
val db=dbHelper.writableDatabase
db.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("price",20.85)
}
db.insert("Book",null,values)
db.setTransactionSuccessful()//事务已经执行成功
}catch (e:IOException){
e.printStackTrace()
}finally {
db.endTransaction()//结束事务
}
}
升级数据库的最佳写法
在升级数据库中,在onUpgrade方法调用onCreate方法是非常粗暴的,在开发时候可以使用,但产品上线之后千万不要使用
class MyDatabaseHelper(val context: Context,name:String,version:Int):SQLiteOpenHelper(context,name,null,version) {
private val createBook="create table Book ("+
"id integer primary key autoincrement,"+
"author text,"+
"price real,"+
"pages integer,"+
"name text,"+
"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)
db?.execSQL(createCategory)
Toast.makeText(context,"Create succeeded",Toast.LENGTH_SHORT).show()
}
//升级数据库,比如像加多一张表
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
if(oldVersion<=1)
db?.execSQL(createCategory)
if (oldVersion<=2){
db?.execSQL("alter table Book add column category_id integer")
}
}
}
MainActivity中版本记得也修改为3哦
加多一个属性:
db?.execSQL("alter table Book add column category_id integer")
数据库博客完更