持久化技术

持久化技术简介

数据持久化就是指将那些内存中的瞬时数据保存到存储设备中,保证即使在手机或计算机关机 的情况下,这些数据仍然不会丢失。保存在内存中的数据是处于瞬时状态的,而保存在存储设 备中的数据是处于持久状态的。持久化技术提供了一种机制,可以让数据在瞬时状态和持久状 态之间进行转换。

文件存储

文件存储是Android中最基本的数据存储方式,它不对存储的内容进行任何格式化处理,所有数 据都是原封不动地保存到文件当中的,因而它比较适合存储一些简单的文本数据或二进制数 据。如果你想使用文件存储的方式来保存一些较为复杂的结构化数据,就需要定义一套自己的 格式规范,方便之后将数据从文件中重新解析出来。

将数据存储到文件中

Context类提供了一个openFileOutput()方法,可以用于将数据存储到指定的文件中。接收两个参数:第一个参数是文件名,不能包含路径;第二个参数是文件的操作模式,主要有MODE_PRIVATE和MODE_APPEND两种模式可选,默认是MODE_PRIVATE,表示当指定相同文件名的时候,所写入的内容将会覆盖原文件中的内容,而MODE_APPEND则表示文件已存在,就往文件里面追加内容, 不存在就创建新文件。

openFileOutput()方法返回的是一个FileOutputStream对象,得到这个对象之后就可以使用Java流的方式将数据写入文件中了。以下是一段简单的代码示例,展示了如何将一段文本文件内容保存到文件中:

fun save(inputText:String){
val output = openFileOutput("data",Context.MODE_PRIVATAE)
val write = BuffererWrite(OutputStreamWriter(output))
writer.use{
it.write(inputText)
}
}

这里通过openFileOutput()方 法能够得到一个FileOutputStream对象,然后借助它构建出一个OutputStreamWriter对 象,接着再使用OutputStreamWriter构建出一个BufferedWriter对象,这样你就可以通 过BufferedWriter将文本内容写入文件中了。

注意,这里还使用了一个use函数,这是Kotlin提供的一个内置扩展函数。它会保证在Lambda 表达式中的代码全部执行完之后自动将外层的流关闭,这样就不需要我们再编写一个finally 语句,手动去关闭流了,是一个非常好用的扩展函数

另外,Kotlin是没有异常检查机制(checked exception)的。这意味着使用Kotlin编写的所 有代码都不会强制要求你进行异常捕获或异常抛出。上述代码中的try catch代码块是参照 Java的编程规范添加的,即使你不写try catch代码块,在Kotlin中依然可以编译通过。

下面我们就编写一个完整的例子,借此学习一下如何在Android项目中使用文件存储的技术。首 先创建一个FilePersistenceTest项目,并修改activity_main.xml中的代码,如下所示:

<LinearLayout xmlns:android="http://shcemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layot_height="wrap_content"
android:hint="Type something here"
/>
</LinearLayout>

这里我们要做的,就是在数据被回收之前,将它存储到 文件当中。修改MainActivity中的代码,如下所示:

class MainActivity : AppCompatActivity(){
private lateinit var binding:ActivityMainBinding
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
setContentView(binding.root)
}
override fun onDestroy(){
super.onDestroy()
val inputText = editText.text.toString()
save(inputText)
}
private 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()
}
}
}

可以看到,首先我们重写了onDestroy()方法,这样就可以保证在Activity销毁之前一定会调 用这个方法。在onDestroy()方法中,我们获取了EditText中输入的内容,并调用save()方 法把输入的内容存储到文件中,文件命名为data。save()方法中的代码和之前的示例基本相 同,这里就不再做解释了。

然后按下Back键关闭程序,这时我们输入的内容就保存到文件中了。那么如何才能证实数据确 实已经保存成功了呢?我们可以借助Device File Explorer工具查看一下。这个工具在Android Studio的右侧边栏当中,通常是在右下角的位置,如果你的右侧边栏中没有这个工具的话,也 可以使用快捷键Ctrl + Shift + A(Mac系统是command + shift + A)打开搜索功能,在搜 索框中输入“Device File Explorer”即可找到这个工具。

从文件中读取数据

类似于将数据存储到文件中,Context类中还提供了一个openFileInput()方法,用于从文 件中读取数据。这个方法要比openFileOutput()简单一些,它只接收一个参数,即要读取的 文件名,然后系统会自动到/data/data//files/目录下加载这个文件,并返 回一个FileInputStream对象,得到这个对象之后,再通过流的方式就可以将数据读取出来 了。

以下是一段简单的代码示例,展示了如何从文件中读取文本数据:

fun load():String{
val content = StringBuilder()
try{
val input = openFileInput("data")
val reader = BufferedReader(InputStreamReader(input))
reader.use{
reader.forEachLine{
content.append(it)
}
}
}
}

了解了从文件中读取数据的方法,那么我们就来继续完善上一小节中的例子,使得重新启动程 序时EditText中能够保留我们上次输入的内容。修改MainActivity中的代码,如下所示

class MainActivity:AppCompatActivity(){
private lateinit var binding:ActivityMainBinding
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstaceState)
binding = ActivityMianBinding.inflate(layoutInflater)
val inputText = load()
val editText = binding.editText
if(inputText.isNotEmpty()){
editText.setText(inputText)
editText.setSelection(inputTextt.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()
}
}

可以看到,这里的思路非常简单,在onCreate()方法中调用load()方法读取文件中存储的文 本内容,如果读到的内容不为空,就调用EditText的setText()方法将内容填充到EditText 里,并调用setSelection()方法将输入光标移动到文本的末尾位置以便继续输入,然后弹出 一句还原成功的提示。load()方法中的细节我们在前面已经讲过,这里就不再赘述了。

读写文件总结

写文件用到的几个类

openFileOutput()
BufferedWritter()
OutPutStreamWriter()

读文件用到的几个类

openFileInput()
BufferdReader()
InPutStreamReader()

SharedPreferences存储

将数据存储到SharedPreferences中

要想使用SharedPreferences存储数据,首先需要获取SharedPreferences对象。Android中主要提供了以下两种方法用于得到SharedPreference对象

  • Context类中的getSharedPreferences()方法

    此方法接收两个参数:第一个参数用于指定SharedPreference文件的名称,如果指定的文件不存在则会创建一个,SharedPreferences文件都是存在/data/data//shared_prefs目录下的;第二个参数用于指定操作模式,目前只有默认的MODE_PRIVATE这一种模式可选,它和直接传入0的效果是相同的,表示只有当前的应用程序才可以对这个SharedPreferences文件进行读写。

  • Activity类中的getPreferences()方法

    这个方法和Context中的getShared方法很相似,不过它直接收一个操作模式参数,因为使用这个方法时会自定将当前的Activity的类名作为SharedPreferences的文件名

向SharedPreferences文件中存储数据的步骤

  • 调用SharedPreferences对象的edit()方法获取一个SharedPreferences.Editor对象
  • 向SharedPreferences.Editor对象中添加数据,比如添加一个布尔型数据就使用putBoolean()方法,添加一个字符串则使用putString()方法,以此类推
  • 调用apply()方法将添加的数据提交,从而完成数据存储操作

新建一个SharedPreferencesTest项目,然后修改activity_main.xml中的代码,如下所示

<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:id="@id/saveButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Save Data"
/>

</LinearLayout>

这里我们不做任何复杂的功能,只是简单地放置了一个按钮,用于将一些数据存储到 SharedPreferences文件当中。然后修改MainActivity中的代码,如下所示:

class MainActivity : AppCompatActivity(){
private lateinit binding:ActivityMainBinding
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
val saveButton = binding.saveButton
saveButton.setOnClickListener{
val editor = getSharedPreferences("data",Context.MODE_PRIVATE).edit()
editor.putString("name","Tom")
editor.putInt("age",28)
editor.putBoolean("married",false)
editor.apply()
}
}
}

从SharedPreferences中读取数据

通过例子来实际体验一下吧,仍然是在SharedPreferencesTest项目的基础上继续开 发,修改activity_main.xml中的代码,如下所示:

<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:id="@+id/saveButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Save Data"
/>
<Button
android:id="@id/restoreButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Restore Data"
/>


</LinearLayout>
class MainActivity : AppCompatActivity() { 

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
...
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)
Log.d("MainActivity", "name is $name")
Log.d("MainActivity", "age is $age")
Log.d("MainActivity", "married is $married")
}
}

}

实现记住密码的功能

activity_login.xml中的代 码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">

<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="60dp">
<TextView
android:layout_width="90dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textSize="18sp"
android:text="Account:" />

<EditText
android:id="@+id/accountEdit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:hint="Enter your account"/>
</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:text="Password:"
android:textSize="18sp" />

<EditText
android:id="@+id/passwordEdit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:hint="Enter your password"
android:inputType="textPassword" />
</LinearLayout>

<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<CheckBox
android:id="@+id/rememberPass"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:text="Remember password"
/>

</LinearLayout>

<Button
android:id="@+id/login"
android:layout_width="200dp"
android:layout_height="60dp"
android:layout_gravity="center_horizontal"
android:text="Login" />

</LinearLayout>

这里使用了一个新控件:CheckBox。这是一个复选框控件,用户可以通过点击的方式进行选中 和取消,我们就使用这个控件来表示用户是否需要记住密码。

然后修改LoginActivity中的代码,如下所示:

package com.example.broadcastbestpractice

import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.example.broadcastbestpractice.databinding.ActivityLoginBinding

class LoginActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
val login = binding.login
val accountEdit = binding.accountEdit
val passwordEdit = binding.passwordEdit
val prefs = getPreferences(Context.MODE_PRIVATE)
val isRemember = prefs.getBoolean("remember_password",false)
val rememberPass = binding.rememberPass
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为我们提供了一个SQLiteOpenHelper帮助类,我们可以通过这个类来对数据库进行创建和升级

SQLiteOpenHelper是一个抽象类,如果要使用就需要自己创建一个类来继承它。SQLiteOpenHelper中有两个抽象方法:onCreate()和onUpgrade()。我们必须在自己的帮助类里重写这两个方法,然后分别在这两个方法中实现创建和升级数据库的逻辑

SQLiteOpenHelper中还有两个非常重要的实例方法:getReadableDatabase()和getWritableDatabase()。这两个方法都可以创建或打开一个现有的数据库(如果数据库已存在则直接打开,否则要创建一个新的数据库),并返回一个可对数据库进行读写操作的对象。不同的是,当数据库不可写入的时候(如磁盘空间)已满,getReadable方法返回的对象将以只读的方式打开数据库,而getWritableDatabase()方法则将出现异常。

SQLiteOpenHelper中有两个构造方法可供重写,一般使用参数少一点的构造方法。这个构造方法中接收4个参数:第一个参数是Context,第二个是数据库名,第三个参数是Cursor,一般传入null,第四个参数表示当前的数据库的版本号。构建出SQLiteOpenHelper实例之后,再调用它的getReadableDatabase()或getWritableDatabase()方法就能创建数据库了,数据库文件会放在/data/data//databases/目录下

接下来还是让我们通过具体的例子来更加直观地体会SQLiteOpenHelper的用法吧,首先新建 一个DatabaseTest项目

这里我们希望创建一个名为BookStore.db的数据库,然后在这个数据库中新建一张Book表, 表中有id(主键)、作者、价格、页数和书名等列。创建数据库表当然还是需要用建表语句的, 这里就要考验一下你的SQL基本功了,Book表的建表语句如下所示:

create table Book(
id integer primary key autoincrement,
author text,
price real,
pages integer,
name text)
)

integer:表示整型,real表式浮点型,text表示文本类型,blob表示二进制类型,使用primary key将id类设为主键,并用autoincrement关键字表示id列是自增长的

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)"
override fun onCreate(db:SQLiteDatabase){
db.execSQL(createBook)
Toast.makeText(context,"Create succeeded",Toast.LENGTH_SHORT).show()
}
override fun onUpgrade(db:SQLiteDatabase,oldVersion:Int,newVersion:Int)
}

可以看到,我们把建表语句定义成了一个字符串变量,然后在onCreate()方法中又调用了 SQLiteDatabase的execSQL()方法去执行这条建表语句,并弹出一个Toast提示创建成功, 这样就可以保证在数据库创建完成的同时还能成功创建Book表。

现在修改activity_main.xml中的代码,如下所示

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<Button
android:id="@id/createDatabase"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Create Daabase"
/>

</LinearLayout>

布局文件很简单,就是加入了一个按钮,用于创建数据库。最后修改MainActivity中的代码, 如下所示:

class MainActivity : AppCompatActivity(){
private lateinit var binding:ActivityMainBinding
override fun onCreate(saveInstanceState:Bundle?){
super.onCreate(savedInstance)
val dbHelper = MyDatabaseHelper(this,"BookStore.db",1)
val createDatabase = binding.createDatabase
createDatabase.setOnClickListener{
dbHelper.writableDatabase
}
}
}

这里我们在onCreate()方法中构建了一个MyDatabaseHelper对象,并且通过构造函数的参 数将数据库名指定为BookStore.db,版本号指定为1,然后在“Create Database”按钮的点击 事件里调用了getWritableDatabase()方法。这样当第一次点击“Create Database”按钮 时,就会检测到当前程序中并没有BookStore.db这个数据库,于是会创建该数据库并调用 MyDatabaseHelper中的onCreate()方法,这样Book表也就创建好了,然后会弹出一个 Toast提示创建成功。再次点击“Create Database”按钮时,会发现此时已经存在 BookStore.db数据库了,因此不会再创建一次。

创建数据库的步骤

  • 继承SQLiteOpenHelper,重写它onCreate()和onUpgrade()方法

  • 使用SQLiteDababase

  • MyDatabaseHelper

    class MyDatabaseHelp(context:Context,name:String,version:Int):
    SQLiteOpenHleper(context,name,null,version){
    private val crateBook = "create table Book(",
    "id integer primary key autoincrement,"+
    "author text,"+
    "pages integer,"+
    "name text)"
    override onCreate(db:SQLiteDatabase){
    db.exeSQL(createBook)
    }
    override onUpgrade(db :QLiteDatabase,oldVersion:Int,newVersion:Int){}
    }

注意使用Database Navigation插件时候路径不要加双引号

升级数据库

目前,DatabaseTest项目中已经有一张Book表用于存放书的各种详细数据,如果我们想再添 加一张Category表用于记录图书的分类,该怎么做呢

比如Category表中有id(主键)、分类名和分类代码这几个列,那么建表语句就可以写成:

create table Category(
id integer primary key autoincrement
category_name text,
category_code ingeger
)

接下来我们将这条建表语句添加到MyDatabaseHelper中,代码如下所示:

class MyDatabaseHelper(context:Context,name:String,version:Int):
SQLiteopenHelper(context,name,version){
private val createBook = "create table Book("+
"id integer primary key autoincrement,"+
"author text,"+
"pages integer,"+
"name text)"
private val createCategory = "create table category("+
"id integer primary key autoincrement,"+
"category_name text,"+
"category_code text)"
override fun onCreate(db:SQLiteDatabase){
db.execSQL(createBook)
db.execSQL(createCategory)
Toast.makeText(context,"Create succeed",Toast.LENGHT_SHORT).show()
}
override fun onUpgrade(db:SQLiteDatabase,oldVerdion:Int,newVersion:Int){
db.exeSQL("drop table if exists Book")
db.exeSQL("drop table if exists category")
onCreate(db)
}
}

MainActivity的代码

class MainActivity:AppCompatActivity(){
private lateinit var binding : ActivityMainBinding
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInfalter)
setContentView(binding.root)
val dbHelper = MyDataBaseHelper(this,"BookStore.db",1)
val createDatabase = binding.createDatabase
createDatabase.setOnClickListener{
dbHelper.writableDatabase
}
}
}

添加数据

我们可以对数据进行的操作无非有4种,即CRUD,C代表添加(create),R代表查找(retrieve),U代表更新(update),D代表删除(delete)。

前面我们已经知道,调用SQLiteOpenHelper的getReadableDatabase()或 getWritableDatabase()方法是可以用于创建和升级数据库的,不仅如此,这两个方法还都 会返回一个SQLiteDatabase对象,借助这个对象就可以对数据进行CRUD操作了

那么下面我们首先学习一下如何向数据库的表中添加数据吧。SQLiteDatabase中提供了一个 insert()方法,专门用于添加数据。它接收3个参数:第一个参数是表名,我们希望向哪张表 里添加数据,这里就传入该表的名字;第二个参数用于在未指定添加数据的情况下给某些可为 空的列自动赋值NULL,一般我们用不到这个功能,直接传入null即可;第三个参数是一个 ContentValues对象,它提供了一系列的put()方法重载,用于向ContentValues中添加数 据,只需要将表中的每个列名以及相应的待添加数据传入即可

介绍完了基本用法,接下来还是让我们通过例子来亲身体验一下如何添加数据吧。修改 activity_main.xml中的代码,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_widht="match_parent"
android:Llayout_height="match_parent">
<Button
android:id="@+id/createDatabase"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="createDatabase"
/>
<Button
android:id="@id/addData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Add Data"
/>
</LinearLayout>

可以看到,我们在布局文件中又新增了一个按钮,稍后就会在这个按钮的点击事件里编写添加 数据的逻辑。接着修改MainActivity中的代码,如下所示:

class MainActivity : AppCompatActivity(){
private lateinit var binding:ActivityMainBinding
override fun Create(savedInstaceState:Bundle?){
super.onCreate(savedInstanceState)
setContentView(binding.root)
val deHelper = MyDatabaseHelper(this,"BookStore.db",2)
val createDatabase = binding.createDatabase
val addData = binding.addData
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",16.96)
}
db.insert("Book",null,values1)
val value2 = ContentValues().applly{
put("name","The Lost Symbol")
put("author","Dan Brown")
put("pages",510)
put("price",29.95)
}
db.insert("Book",null,values2)
}
}

}

更新数据

SQLiteDatabase中提供了一个非常好用的update()方法,用于对数据进行更新。这个方法 接收4个参数:第一个参数和insert()方法一样,也是表名,指定更新哪张表里的数据;第二 个参数是ContentValues对象,要把更新数据在这里组装进去;第三、第四个参数用于约束更 新某一行或某几行中的数据,不指定的话默认会更新所有行。 那么接下来,我们仍然是在DatabaseTest项目的基础上修改,看一下更新数据的具体用法。比 如刚才添加到数据库里的第一本书,由于过了畅销季,卖得不是很火了,现在需要通过降低价 格的方式来吸引更多的顾客,我们应该怎么操作呢?首先修改activity_main.xml中的代码,如 下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_widht="match_parent"
android:Llayout_height="match_parent">
<Button
android:id="@+id/createDatabase"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="createDatabase"
/>
<Button
android:id="@id/addData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Add Data"
/>
<Button
android:id="@id/updateData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Update Data"
/>
</LinearLayout>

修改MainActivity的代码

class MainActivity: AppCompatActivity(){
private lateinit var binding : ActivityMainBinding
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstance)
binding = ActivityMainBinding.inflate(layoutInflater)
setContent(binding.root)
val createDatabase = binding.createDatabase
val dbHelper = MyDataBaseHelper(this,"BookStore.db",1)
createDatabase.setOnClickListener{
dbHelper.writableDatabase
}
val addData = binding.addData{
val db = dbHleper.writableDatabase
val values1 = ContentValues().apply{
put("name","The Da Vinci Code")
put("author","Dan Brown")
put("pages",454)
put("price",16.96)
}
db.insert("Book",null,values1)
val values2 = ContentValues().apply{
put("name","The Lost Symbol")
put("author","Dan Brown")
put("pages",510)
put("price",19.95)
}
db.insert("Book",null,values2)
}
val updateData = binding.updataData
updateData.setOnClickListern{
val db = dbHelper.writableDatabase
val values3 = ContentValues()
valuse3.put("price",10.99)
db.updata("Book",values3,"name=?",arrayOf("The Da Vinci Code"))
}
}
}

删除数据

修改activity_main.xml中的代码,如下 所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
...
<Button
android:id="@+id/deleteData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Delete Data"
/>
</LinearLayout>

然后修改MainActivity中的代码,如下 所示:

class MainActivity : AppCompatActivity(){
private lateinit var binding:ActivityMainBinding
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val dbHelper = MyDataBaseHelper(this,"BookStore.db",1)
val createDatabase = binding.createDatabase
createDatabase.setOnClickListener{
dbHelper.writableDatabase
}
val addData = binding.addData
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,16.96")
}
db.insert("Book",null,values)
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)
}
val updataData = binding.updateData
updataData.setOnClickListener{
val db = dbHelper.writableDatabase
val values3 = ContentValue()
values3.put("price",10.99)
db.update("Book",values3,"name=?",arrayOf("The Da Vinci Code"))
}
val deleteData = binding.deleteData
deleteData.setOnClickListener{
val db = dbHelper.writableDatabase
db.delete("Book","pages>?",arrayOf("500"))
}
}

}

查询数据

SQLiteDatabase中还提供了一个query()方法用于对数据进行查询。这 个方法的参数非常复杂,最短的一个方法重载也需要传入7个参数。那我们就先来看一下这7个 参数各自的含义吧。第一个参数不用说,当然还是表名,表示我们希望从哪张表中查询数据。 第二个参数用于指定去查询哪几列,如果不指定则默认查询所有列。第三、第四个参数用于约 束查询某一行或某几行的数据,不指定则默认查询所有行的数据。第五个参数用于指定需要去 group by的列,不指定则表示不对查询结果进行group by操作。第六个参数用于对group by 之后的数据进行进一步的过滤,不指定则表示不进行过滤。第七个参数用于指定查询结果的排 序方式,不指定则表示使用默认的排序方式。

虽然query()方法的参数非常多,但是不要对它产生畏惧,因为我们不必为每条查询语句都指 定所有的参数,多数情况下只需要传入少数几个参数就可以完成查询操作了。调用query()方 法后会返回一个Cursor对象,查询到的所有数据都将从这个对象中取出。

下面还是让我们通过具体的例子来体验一下查询数据的用法,修改activity_main.xml中的代 码,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
>

...

<Button
android:id="@+id/queryData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Query Data"
/>
</LinearLayout>

这个已经没什么好说的了,添加了一个按钮用于查询数据。然后修改MainActivity中的代码, 如下所示:

class MainActivity : AppCompatActivity(){
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val dbHelper = MyDatabaseHelper(this,"BookStore.db",1)
、、、、
qerryData.setOnClickListener{
val db = dbHelper.writableDatabase
val cursor = db.query("Book", null, null, null, null, null, null)
if(cursor.moveToFirst()){
do{
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"))
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 price is $price")
}while(cursor.moveToNext())
}
cursor.close()
}
}
}

最后别忘了调用close()方法来关闭 Cursor

SQLite数据库基本操作总结

创建

使用SQLiteOpenHelper

class MyDatabaseHelper(val context:Context, val name:String, val version:Int):
SQLiteOpenHelper(context,name,version){
private val createBook = "create table Book("+
"id integer primary key autoincrement,"+
"autor text,"+
"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 succeed",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")
}
}

接下来是完整操作

class MainActivity : AppCompatActivity(){
@SuppressLint("Range")
private lateinit binding:ActivityMainBinding
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstaceState)
binding.inflate(layoutInflater)
setContentView(binding.root)
val dbHelper = MyDatabaseHelper(this,"BookStore.db",1)
val createDatabase = binding.createDatabase
createDatabase.setOnClickListener{
dbHelper.writableDatabase
}
val addData = binding.addData
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",16.96)
}
db.insert("Book",null,values1)
val values2 = ContentValues().apply{
put("name","The Lost Symbol")
put("author","Dan Brown")
put("pages",510)
put("price",19.95)
}
db.insert("Book",null,values)
}
val updateData = binding.updateData
updateData.setOnClickListener{
val db = dbHelper.writableDatabase
val values3 = ContentValues()
values3.put("price",10.99)
db.update("Book",value3,"name=?",arrayOf("The Da Vinci Code"))
}
val deleteData = binding.deleteData
deleteData.setOnClickListener{
val db = dbHelper.writableDatabase
db.delete("Book","pages>?",arrayOf("500"))
}
val queryData = binding.queryData
queryData.setOnClickListener{
val db = dbHelper.writableDatabase
val cursor = db.query("Book",null,null,null,null,null,null)
if(cursor.moveToFirst()){
do{
val name = cursor.getStrng(cursor.getColumnIndex("name"))
val author = cursor.getString(cursor.getColumnIndex("author"))
val pages = cursor.getInt(cursor.getColumnIndex("pages"))
val price = cursor.getDouble(cursor.getColumnIndex("price"))
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 price is $price")
}while(cursor.movtToNext)
cursor.close()
}
}
}
}

记得调用cursor.close()

使用SQL操作数据库

虽然Android已经给我们提供了很多非常方便的API用于操作数据库,不过总会有一些人不习惯 使用这些辅助性的方法,而是更加青睐于直接使用SQL来操作数据库。如果你也是其中之一的 话,那么恭喜,Android充分考虑到了你们的编程习惯,同样提供了一系列的方法,使得可以直 接通过SQL来操作数据库。

下面我就来简略演示一下,如何直接使用SQL来完成前面几个小节中学过的CRUD操作

添加数据:

db.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)", 
arrayOf("The Da Vinci Code", "Dan Brown", "454", "16.96")
)
db.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)",
arrayOf("The Lost Symbol", "Dan Brown", "510", "19.95")
)

更新数据:

db.execSQL("update Book set price = ? where name =?",arrayOf("10.99","The Da Vinci Code"))

删除数据:

db.execSQL("delete from Book where pages>?",arrayOf("500"))

查询数据

val cursor = db.rawQuery("select * from Book",null)

SQLite数据库的最佳实践

使用事务

class MainActivity:AppCompatActivity(){
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val dbHelper = MyDatabaseHelper(this,"BookStore.db",2)
、、、
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","Georage Martin")
put("pages",720)
put("price",20.85)
}
db.insert("Book",null,values)
db.setTransactionSuccessful()
}catch(e:Exception){
e.printStackTrace()
}finally{
db.endTransaction()
}
}
}
}

升级数据库的最佳写法

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("+
"category_name text,"+
"category_code integer)"

override fun onCreate(db:SQLiteDatabase){
db.execSQL(createBook)
db.execSQL(createCategory)
}
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")
}
}
}

Kotlin:高阶函数的应用

简化SharedPreference的用法

首先来看SharedPreferences,在开始对它进行简化之前,我们先回顾一下 SharedPreferences原来的用法。向SharedPreferences中存储数据的过程大致可以分为 以下3步:

(1) 调用SharedPreferences的edit()方法获取SharedPreferences.Editor对象; (2) 向SharedPreferences.Editor对象中添加数据; (3) 调用apply()方法将添加的数据提交,完成数据存储操作。

对应的代码示例如下:

val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit() 
editor.putString("name", "Tom")
editor.putInt("age", 28)
editor.putBoolean("married", false)
editor.apply()

当然,这段代码其实本身已经足够简单了,但是这种写法更多还是在用Java的编程思维来编写 代码,而在Kotlin当中我们明显可以做到更好。

fun SharedPreferences.open(block: SharedPreferences.Editor.() -> Unit) { 
val editor = edit()
editor.block()
editor.apply()
}

定义好了open函数之后,我们以后在项目中使用SharedPreferences存储数据就会更加方便 了,写法如下所示:

getSharedPreferences("data", Context.MODE_PRIVATE).open { 
putString("name", "Tom")
putInt("age", 28)
putBoolean("married", false)
}

简化ContentValues的用法

新建一个ContentValues.kt文件,然后在里面 定义一个cvOf()方法,如下所示:

fun cvOf(vararg pairs: Pair<String, Any?>): ContentValues { 
}

这个方法的作用是构建一个ContentValues对象,有几点我需要解释一下。首先,cvOf()方 法接收了一个Pair参数,也就是使用A to B语法结构创建出来的参数类型,但是我们在参数 前面加上了一个vararg关键字,这是什么意思呢?其实vararg对应的就是Java中的可变参数 列表,我们允许向这个方法传入0个、1个、2个甚至任意多个Pair类型的参数,这些参数都会 被赋值到使用vararg声明的这一个变量上面,然后使用for-in循环可以将传入的所有参数遍 历出来。

Any是Kotlin中所有类的共同基类,相当于Java中的Object,而Any?则表示允许传入空值

接下来我们开始为cvOf()方法实现功能逻辑,核心思路就是先创建一个ContentValues对 象,然后遍历pairs参数列表,取出其中的数据并填入ContentValues中,最终将 ContentValues对象返回即可。思路并不复杂,但是存在一个问题:Pair参数的值是Any?类 型的,我们怎样让它和ContentValues所支持的数据类型对应起来呢?这个确实没有什么好的 办法,只能使用when语句一一进行条件判断,并覆盖ContentValues所支持的所有数据类 型。结合下面的代码来理解应该更加清楚一些:

fun cvOf(vararg pairs: Pair<String, Any?>) = 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)
}
}
}

有了这个cvOf()方法之后,我们使用ContentValues时就会变得更加简单了,比如向数据库 中插入一条数据就可以这样写:

val values = cvOf("name" to "Game of Thrones", "author" to "George Martin", 
"pages" to 720, "price" to 20.85)
db.insert("Book", null, values)