第一行代码Android 阅读笔记 第七章(仅自用)

数据存储-持久化技术

瞬时数据:存储在内存中,可能因为程序关闭或其他原因导致内存被回收而丢失的数据。
为了保存关键性数据,要用到数据持久化技术。
数据持久化:将内存中的瞬时数据保存到存储设备中,保证即使在手机或计算机关机的情况下,数据仍不会丢失。
持久化技术提供了一种机制,可让数据在瞬时状态和持久状态间进行转换。
实现数据持久化功能:文件存储、SharedPreferences存储、数据库存储

文件存储

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

Context类中提供了一个openFileOutput方法,可用于将数据存储到指定文件中,接收的参数有两个,参数1为文件名,不可包含文件名,参数2为文件的操作模式,分别为MODE_PRIVATE、MODE_APEND,前者为当指定文件已存在时,覆写内容,后者为指定文件存在时,追加内容,但两个都是指定文件不存在时直接创建文件。其实还有MODE_WORLD_READBLE、MODE_WORLD_WRITEABLE,这两个都允许其他应用程序对我们程序中的文件进行读写,在4.2中被废弃。

//此处读取edit框内容
fun save(inputText: String) {
//kotlin没有异常检查机制
  try {
  //openFileOutput方法返回的是一个FileOutputStream对象,
  //通过java流的方式将数据写入文件中
    val output = openFileOutput("data", Context.MODE_PRIVATE)
    val writer = BufferedWriter(OutputStreamWriter(output))
    writer.use {
    //此处的use为kotlin的内置拓展函数,
    //保证lambda表达式中的代码全部执行完后自动将外层的流关闭。
    //无需自行通过finally手动关闭流
    it.write(inputText)
     }
  } catch (e: IOException) {
    e.printStackTrace()
  }
}
//此处将文件内容写入edit框
edit.setText(load())

fun load(): String {
   val content = StringBuilder()
   try {
     //openFileInput()获取了一个FileInputStream对象,原理同上
     val input = openFileInput("data")
     val reader = BufferedReader(InputStreamReader(input))
     reader.use {
     //将文件中的内容一行一行地进行读取
       reader.forEachLine {
       content.append(it)
     }
   }
 } catch (e: IOException) {
 e.printStackTrace()
 }
 return content.toString()
}

数据存储于/data/data/包名/files

Shared Preferences存储

不同于文件存储,SP是使用键值对进行存储数据的,且支持多种不同的数据类型存储。
使用SP前需要获取SP对象,android提供以下两种方法:

  • context类中的getSharedPreferences(),接收两个参数,参数1为文件名称,参数2为操作模式,仅有MODE_PRIVATE,表示只有当前程序可对这个SP文件进行读写,其余模已被废弃。
  • activity类中的getPreferences(),仅接收一个参数,操作模式。自动将当前activity的类名作为文件名。

获取SP对象后,通过三步操作完成储存

  • 调用SharedPreferences对象的edit方法获取一个SharedPreferences.Editor对象。
  • 向SharedPreferences.Editor对象中添加数据,添加布尔类型则使用putBoolean,添加字符串则使用putString。
  • 调用apply方法将添加的数据提交,完成数据存储
saveButton.setOnClickListener {
  val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
  editor.putString("name", "Tom")
  editor.putInt("age", 28)
  editor.putBoolean("married", false)
  editor.apply()
 }

实际效果很像bundle
在这里插入图片描述
取出SP文件中的数据
SP对象中提供了一系列的get方法,对应SharedPreferences.Editor方法的每一种put方法,这些get方法都接收两个参数,参数1为键,参数2为默认值,即当传入的键找不到值时以怎样的默认值返回。

//取出数据
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")
 }

实现记住密码功能

class LoginActivity : BaseActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_login)
    //使用SP,打开流对象
    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)
      //此处增添了checkbox,即记住密码的复选框
      rememberPass.isChecked = true
 }
  login.setOnClickListener {
  val account = accountEdit.text.toString()
  val password = passwordEdit.text.toString()
  // 如果账号是admin且密码是123456,就认为登录成功
  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()
 }
 }
 }
}

数据存储在/data/data/包名/shared_prefs

SQLite数据库存储

SQLite是轻量级的关系型数据库,运算速度快,占用资源少。不仅支持标准的SQL语法,还遵循了数据库的ACID事务。

SQLiteHelper

SQLiteHelper是抽象类,需要自己创建帮助类来继承,并重写onCreate()和onUpgrade(),SQLiteHelper内还有两个重要的实例方法:getReadableDatabase()和getWritableDatabase()这两个方法都可创建或打开一个现有数据库,并返回一个可对数据库进行读写操作的对象。而两者不同在于,当数据库不可写入时,前者返回的对象以只读的方式打开数据库,而后者则出现异常。
SQLiteHelper中有两个构造方法可供重写,使用参数较少那个即可。
该构造方法中接收4个参数,第一个参数为context。第二个参数为数据库名,第三个参数允许我们在查询数据时返回一个自定义的Cursor,一般传入null即可,第四个参数为数据库版本号。
构建出SQLiteHelper实例后,再调用它的getReadableDatabase()或getWritableDatabase()创建数据库,此时也会执行onCreate()。
数据库文件会存放在/data/data/包名/database。

//创建数据库
class bookDBHelper(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(p0: SQLiteDatabase) {
        //执行语句
        p0.execSQL(createBook)
        Toast.makeText(context,"Create Succeeded",Toast.LENGTH_SHORT).show()
    }

    override fun onUpgrade(p0: SQLiteDatabase?, p1: Int, p2: Int) {
        TODO("Not yet implemented")
    }
}
    //创建helper实例
    private val dbHelper = bookDBHelper(this,"BookStore.db",1)
    dbHelper.writableDatabase

在这里插入图片描述
添加插件:Database Navigator
由查询数据库文件得知(如上图),目录下存在另一个BookStore.db-journal文件,为了让数据库能够支持事务而产生的临时日志文件。
右键BookStore.db ,点击Save AS,导出电脑的任意位置。
添加插件后重启as,左侧会有一栏:DB Browser
添加 SQLite connection,在database file中间三个点处选定文件位置,点击ok即可,此时看到我们的数据库确实创建成功
在这里插入图片描述

升级数据库

onUpgrade()方法可用于对数据库进行升级的。
当我们想增添一张表时,仅仅让helper执行建表语句是不够的,因为此时BookStore.db已经被创建,如果想要创建新表,只能卸载应用重新安装,但这样是不可接受的。
因此我们利用onUpgrade,在upgrade中删除已经存在的表,并重新调用onCreate

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)//onCreate是创建Book和category两张表
 }
//修改版本号为大于1的数,使得upgrade被执行
val dbHelper = MyDatabaseHelper(this, "BookStore.db", 2)
    createDatabase.setOnClickListener {
        dbHelper.writableDatabase
 }

在这里插入图片描述

对数据库的操作CRUD

插入数据

//插入数据
 addData.setOnClickListener {
  val db = dbHelper.writableDatabase
  //通过put向contentvalues中添加数据
  val values1 = ContentValues().apply {
 // 开始组装第一条数据
  put("name", "The Da Vinci Code")
  put("author", "Dan Brown")
  put("pages", 454)
  put("price", 16.96)
  }
  //参数1为表名、参数2为给未指定添加数据的列自动赋值,参数3为ContentValues对象
  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) // 插入第二条数据
 }

更新数据

updateData.setOnClickListener {
  val db = dbHelper.writableDatabase
  val values = ContentValues()
  //说明要更新的列和值
  values.put("price", 10.99)
  //参数1为表名,
  //参数2为contentvalue对象,
  //参数3为条件,即where部分,而?是一个占位符,通过参数4给出对应内容
  //参数4为字符数组,通过arrayof提供编辑创建数组,对应给出占位符指定的内容
  //此处是在Book表中,更新书名为“The Da Vinci Code”的书本价格为10.99
  db.update("Book", values, "name = ?", arrayOf("The Da Vinci Code"))
 }

删除数据

deleteData.setOnClickListener {
 val db = dbHelper.writableDatabase
 db.delete("Book", "pages > ?", arrayOf("500"))
 }

查询数据

SQLiteDatabase提供了query方法,参数最少的也有7个
参数1为表名,参数2为自动
不指定columns则默认查询所有列 select *,
不指定selection和selectionArgs则默认查询所有行 ,
不指定groupBy则表明不对查询结果进行group by操作,
不指定having则表明不进行进一步地过滤,
不指定orderby则表明使用默认的排序方式。

//通过按键触发
queryData.setOnClickListener {
  val db = dbHelper.writableDatabase
 // 查询Book表中所有的数据,此处等价 select * from Book,返回了一个Cursor对象
  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
  cursor.close()
}

不使用API完成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"))
//查询数据:除了查询使用的rawQuery,其他都是execSQL
 val cursor = db.rawQuery("select * from Book", null)

SQLite数据库是支持事务的,事务的特性可以保证一系列的操作要么全部完成,要么一个都不会完成。此特性很有用,就像转账,需要一方扣除金额,一方收到同等金额才能算成功。

//以下是事务的标准用法
//通过beginTransaction开启事务,然后在一个异常捕获的代码块中执行具体的数据库操作
//完成操作后调用setTransactionSuccessful()表示事务已经执行成功,
//最终在finally块中调用endTransaction结束事务
//此处手动抛出异常,检查数据库会发现数据未发生改变
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() // 结束事务
 }
}

升级数据库的最佳写法

//此处提供两次更新的范例,因为每次更新版本号,才会调用onUpgrade,因此我们对每次的版本号进行判定
//这样的判定能保证用户从任意低的版本可升级到最高版本。
//而新安装的用户在onCreate阶段就已经是最新版本了
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
   if (oldVersion <= 1) {
     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")
   }
 }

优化SharePreferences的写法

//这样的写法跟java很像,在kotlin里面应该写得更简洁
 val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
 editor.putString("name", "Tom")
 editor.putInt("age", 28)
 editor.putBoolean("married", false)
 editor.apply()
 //简洁写法
 //此处使用了高阶函数和拓展函数的写法,在SharedPreferences拓展了open函数,且接收一个函数类型的参数
 //
 fun SharedPreferences.open(block: SharedPreferences.Editor.() -> Unit) {
   //由于拓展函数拥有类的上下文,因此此处直接调用edit()
   val editor = edit()
   //调用函数参数
   editor.block()
   //提交数据
   editor.apply()
}
 //存储数据,无需自己调用apply,因为open最后会自动调用apply
 getSharedPreferences("data", Context.MODE_PRIVATE).open {
 putString("name", "Tom")
 putInt("age", 28)
 putBoolean("married", false)
}

谷歌的ktx库其实已经提供了简便写法,原理就是上面这样,具体写法无异

//androidx.core:core-ktx:1.0.2
  getSharedPreferences("data", Context.MODE_PRIVATE).edit {
  putString("name", "Tom")
  putInt("age", 28)
  putBoolean("married", false)
}

简化ContentValues写法
mapof()函数,允许使用 A to B这样的语法创建键值对,这在kotlin中为Pair对象。

//vararg指的是可变参数列表,可向此方法传入n个pair类型参数(n=0,1,2...)
//这些参数会被赋值到pairs里面,使用for-in即可遍历
//Pair是键值对类型数据,因此需要指定键和值的数据类型
//而ContentValues的键都为字符串类型,因此指定string类型,
//而值可以有多重类型,指定为Any?,any为kotlin所有类的基类,
//?则允许其传入空值,但由于any指定类型不明,需要通过when或if特判定
fun cvOf(vararg pairs: Pair<String, Any?>) = ContentValues().apply {
 for (pair in pairs) {
 val key = pair.first
 val value = pair.second
 when (value) {
    //此处带有kotlin的smart cast功能,自动转换类型,
   //的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)
 }
 }
}

同样kotlin也提供了 通过ktx库实现的简便写法

val values = contentValuesOf("name" to "Game of Thrones", "author" to "George Martin",
 "pages" to 720, "price" to 20.85)
db.insert("Book", null, values)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值