《Android》Chap.7 数据持久化

持久化技术简介

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

文件存储

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

将数据存储到文件

实现方法

Context类中提供了一个openFileOutput()方法,可以用于将数据存储到指定的文件中。这个方法接收两个参数:
第一个参数是文件名,在文件创建的时候使用,注意这里指定的文件名不可以包含路径,因为所有的文件都默认存储到/data/data//files/目录下。
第二个参数是文件的操作模式,主要有MODE_PRIVATEMODE_APPEND两种模式可选

名称 用途
MODE_PRIVATE 表示当指定相同文件名的时候,所写入的内容将会覆盖原文件中的内容
MODE_APPEND 表示如果该文件已存在, 就往文件里面追加内容,不存在就创建新文件

默认是 MODE_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()
    }
}
  • openFileOutput()方法返回的是一个FileOutputStream对象,得到这个对象之后就可以使用Java流的方式将数据写入文件。
  • use函数是kotlin内置扩展函数,它会保证在Lambda表达式中的代码全部执行完之后自动将外层的流关闭,这样就不需要再编写一个finally语句,手动去关闭流了,

完整使用

首先在activity_main.xml中添加一个ideditTextEditText,用于输入文本。代码略
然后修改MainActivity中的代码

class MainActivity : AppCompatActivity() {

    private lateinit var mainBinding : ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mainBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(mainBinding.root)
    }

    //保证在Activity销毁前一定会调用这个方法
    override fun onDestroy() {
        super.onDestroy()
        val inputText = mainBinding.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()
        }
    }

}

EditText中输入一串字符
《Android》Chap.7 数据持久化_第1张图片
按下返回键退出后,在文件中查找:
借助蓝色箭头指向的工具
参照红色方框标记的路径
data文件中的字符就是上一步在文本框中输入的字符
《Android》Chap.7 数据持久化_第2张图片

从文件中读取数据

代码示例

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()
}

完整使用

修改刚刚的主函数

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    mainBinding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(mainBinding.root)

    val inputText = load()
    if(inputText.isNotEmpty()){ //获取文件中的内容显示在框内
        mainBinding.editText.setText(inputText)
        mainBinding.editText.extendSelection(inputText.length)
        Toast.makeText(this,"Restoring succeeded",Toast.LENGTH_SHORT).show()
    }
}

《Android》Chap.7 数据持久化_第3张图片

SharedPreferences存储

SharedPreferences是使用键值对的方式来存储数据的。
当保存一条数据的时候,需要给这条数据提供一个对应的键,这样在读取数据的时候就可以通过这个键把相应的值取出来。而且SharedPreferences还支持多种不同的数据类型存储,如果存储的数据类型是整型,那么读取出来的数据也是整型的;如果存储的数据是一个字符串,那么读取出来的数据仍然是字符串。

将数据存储到SharedPreferences中

要想使用SharedPreferences存储数据,首先需要获取SharedPreferences对象。

Context类中的getSharedPreferences()方法

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

Activity类中的getPreferences()方法

此方法只接收一个操作模式参数,因为使用这个方法时会自动将当前Activity的类名作为SharedPreferences的文件名。
得到了SharedPreferences对象之后,就可以开始向SharedPreferences文件中存储数据了,主要可以分为3步实现。

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

使用实例

activity_main.xml中添加一个id为saveButton的按钮
然后在MainActivity中编写代码

class MainActivity : AppCompatActivity() {
    private lateinit var mainBinding: ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mainBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(mainBinding.root)
        mainBinding.saveButton.setOnClickListener {
            //获取SharedPreferences.Editor对象
            val editor = getSharedPreferences("data",Context.MODE_PRIVATE).edit()
            editor.putString("name","ZLS") //添加String类型数据
            editor.putInt("age",18)
            editor.putBoolean("married",false)
            editor.apply() //提交数据
        }
    }
}

点击按钮后,在/data/data/com.example.sharedPreferencestest/shared_prefs/目录下找到新生成的data.xml文件,其中保存了刚刚所有的数据。
《Android》Chap.7 数据持久化_第4张图片

从SharedPreferences中读取数据

SharedPreferences对象中提供了一系列的get方法,用于读取存储的数据,每种get方法都对应了SharedPreferences.Editor中的一种put方法,比如读取一个布尔型数据就使用getBoolean()方法,读取一个字符串就使用getString()方法。
这些get方法都接收两个参数:第一个参数是键,传入存储数据时使用的键就可以得到相应的值了;第二个参数是默认值,即表当传入的键找不到对应的值时会以什么样的默认值进行返回。

使用实例

activity_main.xml中再添加一个id为printButton的按钮
然后在MainActivity中添加代码

mainBinding.printButton.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")
}

点击第二个button就会得到:
在这里插入图片描述

实现记住密码功能

编写activity_login.xml登录界面


<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"
    android:orientation="vertical"
    tools:context=".LoginActivity">

    <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" />

    LinearLayout>

    <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="Password:" />

        <EditText
            android:id="@+id/passwordEdit"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_gravity="center_vertical" />

    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>

LoginActivity中编写代码

class LoginActivity : BaseActivity() {

    private lateinit var loginBinding: ActivityLoginBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        loginBinding = ActivityLoginBinding.inflate(layoutInflater)
        setContentView(loginBinding.root)

        //获取SharedPreferences对象
        val prefs = getPreferences(MODE_PRIVATE)
        val isRemember = prefs.getBoolean("remember_password",false)
        if(isRemember){
            //将账号和密码都设置在文本框中
            val account = prefs.getString("account","")
            val password = prefs.getString("password","")
            loginBinding.accountEdit.setText(account)
            loginBinding.passwordEdit.setText(password)
            loginBinding.rememberPass.isChecked = true
        }

        loginBinding.login.setOnClickListener {
            val account = loginBinding.accountEdit.text.toString()
            val password = loginBinding.passwordEdit.text.toString()
            if (account == "admin" && password == "123456"){
                val editor = prefs.edit()
                if (loginBinding.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()
            }
        }
    }
}

《Android》Chap.7 数据持久化_第5张图片
当输入正确的账号密码并选择保存后,关闭应用,再次打开时,账号和密码已经自动填充。
《Android》Chap.7 数据持久化_第6张图片

SQLite数据库存储

SQLite是一款轻量级的关系型数据库,它的运算速度非常快,占用资源很少,通常只需要几百KB的内存就足够了,因而特别适合在移动设备上使用。SQLite不仅支持标准的SQL语法,还遵循了数据库的ACID事务,所以只要以前使⽤过其他的关系型数据库,就可以很快地上手SQLite。而SQLite又比一般的数据库要简单得多,它甚至不用设置用户名和密码就可以使用。

创建数据库

Android为了让用户能够更加方便地管理数据库,专门提供了一个SQLiteOpenHelper帮助类。
SQLiteOpenHelper是一个抽象类,这意味着如果想要使用它,就需要创建一个自己的帮助类去继承它。SQLiteOpenHelper中有两个抽象方法:onCreate()onUpgrade()。必须在自己的帮助类里重写这两个方法,然后分别在这两个方法中实现创建和升级数据库的逻辑。
SQLiteOpenHelper中还有两个非常重要的实例方法:getReadableDatabase()getWritableDatabase()。这两个方法都可以创建或打开一个现有的数据库(如果数据库已存在则直接打开,否则要创建一个新的数据库),并返回一个可对数据库进行读写操作的对象。 不同的是,当数据库不可写入的时候(如磁盘空间已满),getReadableDatabase()方法返回的对象将以只读的方式打开数据库, 而getWritableDatabase()方法则将出现异常。
SQLiteOpenHelper中有两个构造方法可供重写,一般使用参数少一点的那个构造方法即可。这个构造方法中接收4个参数:第一个参数是Context,这个没什么好说的,必须有它才能对数据库进行操作;第二个参数是数据库名,创建数据库时使用的就是这里指定的名称;第三个参数允许我们在查询数据的时候返回一个自定义的Cursor,一般传入null即可;第四个参数表示当前数据库的版本号,可用于对数据库进行升级操作。构建出SQLiteOpenHelper的实例之后,再调用它的getReadableDatabase()getWritableDatabase()方法就能够创建数据库了,数据库文件会存放在/data/data//databases/目录下。此时,重写的onCreate()方法也会得到执行,所以通常会在这里处理一些创建表的逻辑。

完整使用

新建MyDatabaseHelper类继承自SQLiteOpenHelper,在代码中执行SQL语句,完成创建表的操作

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) {
    }

}

activity_main.xml中再添加一个id为createDatabase的按钮
然后在MainActivity中添加代码

class MainActivity : AppCompatActivity() {
    private lateinit var mainActivity: ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mainActivity = ActivityMainBinding.inflate(layoutInflater)
        setContentView(mainActivity.root)

		//数据库名为BookStore.db,版本号为1
        val dbHelper = MyDatabaseHelper(this,"BookStore.db",1)
        mainActivity.createDatabase.setOnClickListener {
            dbHelper.writableDatabase
        }

    }
}

第一次点击button后,弹出toast,表明数据库文件已经新建完成。
《Android》Chap.7 数据持久化_第7张图片

查看数据表

借助一个叫作Database Navigator的插件工具可以查看数据表。
从Android Studio导航栏中打开Preferences→Plugins,就可以进入插件管理界面了:
《Android》Chap.7 数据持久化_第8张图片
点击Install后重启Android Studio。

打开Device File Explorer,然后进入/data/data/com.example.databasetest/databases/目录下,可以看到已经存在了一个BookStore.db文件,右击后选择Save As,将它放置在任意你方便查找的位置。
《Android》Chap.7 数据持久化_第9张图片
然后在Android Studio的左侧边栏找到DB Browser工具。
为了打开刚刚导出的数据库文件,需要点击这个工具左上角的加号按钮,并选择SQLite选项
《Android》Chap.7 数据持久化_第10张图片
然后在弹出窗口的Database配置中选择我们刚才导出的BookStore.db文件
《Android》Chap.7 数据持久化_第11张图片
可以看到BookStore.db数据库中确实存在了一张Book表,并且Book表中的列也和前面使用的建表语句完全匹配,由此证明BookStore.db数据库和Book表确实已经创建成功了。
《Android》Chap.7 数据持久化_第12张图片

升级数据库

onUpgrade()方法是用于对数据库进行升级的。
val dbHelper = MyDatabaseHelper(this,"BookStore.db",1)语句中,最后一个参数代表版本号,当版本号为比1大的数时,就执行onUpgrade()方法。

添加数据

调用SQLiteOpenHelpergetReadableDatabase()getWritableDatabase()方法是可以用于创建和升级数据库的,不仅如此,这两个方法还都会返回⼀个SQLiteDatabase对象,借助这个对象就可以对数据进行添加、查询、更新、删除操作了。
SQLiteDatabase中提供了一个insert()方法,专门用于添加数据。
它接收3个参数:第一个参数是表名,希望向哪张表里添加数据,这里就传入该表的名字;第二个参数用于在未指定添加数据的情况下给某些可为空的列自动赋值NULL,一般用不到这个功能,直接传入null即可;第三个参数是一个ContentValues对象,它提供了一系列的put()方法重载,用于向ContentValues中添加数据,只需要将表中的每个列名以及相应的待添加数据传入即可。

完整使用

activity_main.xml中再添加一个id为addData的按钮
然后在MainActivity中的添加代码

mainActivity.addData.setOnClickListener {
    val db = dbHelper.writableDatabase
    val value1 = ContentValues().apply {
        //组装数据
        put("name","The Da Vinci Code")
        put("author","Dan Brown")
        put("pages", 454)
        put("price", 16.96)
    }
    db.insert("Book",null,value1) //插入数据

    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)
}

再次运行时,点击ADD DATA按钮后,再次使用DB Browser打开BookStore.db
《Android》Chap.7 数据持久化_第13张图片
双击Book表格
《Android》Chap.7 数据持久化_第14张图片

在弹出设置查询条件的窗口中点击No Filter,表示不需要设置任何查询条件,然后就能看到表中的数据了。
《Android》Chap.7 数据持久化_第15张图片

更新数据

SQLiteDatabase中提供了一个非常好用的update()方法,用于对数据进行更新。
它接收4个参数:第一个参数和insert()方法一样,也是表名,指定更新哪张表里的数据;第⼆个参数是ContentValues对象,要把更新数据在这里组装进去;第三、第四个参数用于约束更新某一行或某几行中的数据,不指定的话默认会更新所有行。

完整使用

activity_main.xml中再添加一个id为updataData的按钮
然后在MainActivity中的添加代码

mainActivity.updateData.setOnClickListener {
    val db = dbHelper.writableDatabase
    val values = ContentValues() //构建ContentValues对象
    values.put("price",10.99) //将价格这一列的数据改为10.99
    db.update("Book",values,"name = ?", arrayOf("The Da Vinci Code")) 
    //第三个参数表示更新所有name=?的行,?表示占位符,对应的是第四个参数的内容
}

再次查看表格数据已经更改完成。
《Android》Chap.7 数据持久化_第16张图片

删除数据

SQLiteDatabase中提供了一个delete()方法,专门用于删除数据。
它接收3个参数:第一个参数仍然是表名;第二、第三个参数用于约束删除某一行或某几行的数据,不指定的话默认会删除所有行。

完整使用

activity_main.xml中再添加一个id为deleteData的按钮
然后在MainActivity中的添加代码

mainActivity.deleteData.setOnClickListener {
    val db = dbHelper.writableDatabase
    db.delete("Book","pages > ?", arrayOf("500")) //删除页数大于500的
}

再次查看表格。
《Android》Chap.7 数据持久化_第17张图片

查询数据

SQLiteDatabase中还提供了一个query()方法用于对数据进行查询。
它至少接收7个参数:第一个参数还是表名,表示从哪张表中查询数据;第二个参数用于指定去查询哪几列,如果不指定则默认查询所有列;第三、第四个参数用于约束查询某一行或某几行的数据,不指定则默认查询所有行的数据;第五个参数用于指定需要去group by的列,不指定则表示不对查询结果进行group by操作;第六个参数用于对group by之后的数据进行进一步的过滤,不指定则表示不进行过滤;第七个参数用于指定查询结果的排序方式,不指定则表示使用默认的排序方式。

query()方法参数 对应SQL部分 描述
table from table_name 指定查询的表名
columns select column1, column2 指定查询的列名
selection where column = value 指定where的约束条件
selectionArgs - where中的占位符提供具体的值
groupBy group by column 指定需要group by的列
having having column = value group by后的结果进一步约束
orderBy order by column1, column2 指定查询结果的排序方式

调用query()方法后会返回一个Cursor对象,查询到的所有数据都将从这个对象中取出。

完整使用

activity_main.xml中再添加一个id为queryData的按钮
然后在MainActivity中的添加代码

mainActivity.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"))
            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() //查询完要关闭
}

运行后点击queryData按钮,就可以看到表中的数据。
在这里插入图片描述

最佳实践

使用事务

SQLite数据库是支持事务的,事务的特性可以保证让一系列的操作要么全部完成,要么一个都不完成。

完整使用

activity_main.xml中再添加一个id为replaceData的按钮
然后在MainActivity中的添加代码

mainActivity.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: Exception){
        e.printStackTrace()
    }finally {
        db.endTransaction() //结束事务
    }
}

运行后点击replaceData按钮再点击queryData按钮,就可以看到表中还是之前的数据。表明即使在删除旧数据的操作完成后抛出异常,但添加新数据的代码没有执行到,事务失败,则删除旧数据的操作也失败
在这里插入图片描述
但如果删除掉其中的抛出异常的代码再执行,数据就替换成功
在这里插入图片描述

升级数据库的最佳写法

在实际的产品发布使用过程中,数据库的每次升级都要保证用户之前的数据不丢失。

override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
    if(oldVersion <= 1){
        //...  
    }
    if (oldVersion <= 2){
        //...
    }
}

每当升级一个数据库版本的时候,onUpgrade()方法里都一定要写一个相应的if判断语句。这是为了保证App在跨版本升级的时候,每一次的数据库修改都能被全部执⾏。
比如用户当前是从第2版升级到第3版,那么只有第二条判断语句会执行,而如果用户是直接从第1版升级到第3版,那么两条判断语句都会执行。使用这种方式来维护数据库的升级,不管版本怎样更新,都可以保证数据库的表结构是最新的,而且表中的数据完全不会丢失。

你可能感兴趣的:(#,android,kotlin,sqlite)