《第一行代码》第三版之数据存储方案(八)

       本章我们介绍了数据存储方案:文件存储、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操作。

                                        《第一行代码》第三版之数据存储方案(八)_第1张图片
      布局文件activity_main.xml。




   

      修改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)

 

你可能感兴趣的:(第一行代码第三版,第一行代码第三版,安卓,Kotlin,数据存储)