持久化技术简介
数据持久化就是指将那些内存中的瞬时数据保存到存储设备中,保证即使在手机或计算机关机的情况下,这些数据仍然不会丢失。
保存在内存中的数据是处于瞬时状态的,而保存在存储设备中的数据是处于持久状态的。
持久化技术提供了一种机制,可以让数据在瞬时状态和持久状态之间进行转换。
文件存储
文件存储是Android中最基本的数据存储方式,它不对存储的内容进行任何格式化处理, 所有数据都是原封不动地保存到文件当中的,因而它比较适合存储一些简单的文本数据或二进制数据。如果你想使用文件存储的方式来保存一些较为复杂的结构化数据,就需要定义一套自己的格式规范,方便之后将数据从文件中重新解析出来。
将数据存储到文件
实现方法
Context
类中提供了一个 openFileOutput()
方法,可以用于将数据存储到指定的文件中。这个方法接收两个参数:
第一个参数是文件名,在文件创建的时候使用,注意这里指定的文件名不可以包含路径,因为所有的文件都默认存储到 /data/data/<package name>/files/</package>
目录下。
第二个参数是文件的操作模式,主要有 MODE_PRIVATE
和 MODE_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
中添加一个 id
为 editText
的 EditText
,用于输入文本。 代码略
然后修改 MainActivity
中的代码
class MainActivity : AppCompatActivity() {
private lateinit var mainBinding : ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mainBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(mainBinding.root)
}
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
中输入一串字符
按下返回键退出后,在文件中查找:
借助 蓝色箭头指向的工具
参照 红色方框标记的路径
data
文件中的字符就是上一步在文本框中输入的字符从文件中读取数据
代码示例
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()
}
}
SharedPreferences是使用键值对的方式来存储数据的。
当保存一条数据的时候,需要给这条数据提供一个对应的键,这样在读取数据的时候就可以通过这个键把相应的值取出来。而且SharedPreferences还支持多种不同的数据类型存储,如果存储的数据类型是整型,那么读取出来的数据也是整型的;如果存储的数据是一个字符串,那么读取出来的数据仍然是字符串。
要想使用SharedPreferences存储数据,首先需要获取SharedPreferences对象。
此方法接收两个参数:
第一个参数用于指定SharedPreferences文件的名称,如果指定的文件不存在则会创建一个,SharedPreferences文件都是存放在 /data/data/<package name>/shared_prefs/</package>
目录下的;
第二个参数用于指定操作模式,目前只有默认的 MODE_PRIVATE
这⼀种模式可选,它和直接传入0的效果是相同的,表示只有当前的应用程序才可以对这个SharedPreferences文件进行读写。
Activity类中的getPreferences()方法
此方法只接收一个操作模式参数,因为使用这个方法时会自动将当前Activity的类名作为SharedPreferences的文件名。
得到了SharedPreferences对象之后,就可以开始向SharedPreferences文件中存储数据了,主要可以分为3步实现。
- 调用SharedPreferences对象的
edit()
方法获取一个SharedPreferences.Editor
对象。 - 向
SharedPreferences.Editor
对象中添加数据,比如添加一个布尔型数据就使用putBoolean()
方法,添加一个字符串则使用putString()
方法,以此类推。 - 调用
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 {
val editor = getSharedPreferences("data",Context.MODE_PRIVATE).edit()
editor.putString("name","ZLS")
editor.putInt("age",18)
editor.putBoolean("married",false)
editor.apply()
}
}
}
点击按钮后,在 /data/data/com.example.sharedPreferencestest/shared_prefs/
目录下找到新生成的 data.xml
文件,其中保存了刚刚所有的数据。
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)
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()
}
}
}
}
当输入正确的账号密码并选择保存后,关闭应用,再次打开时,账号和密码已经自动填充。
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/<packagename>/databases/</packagename>
目录下。此时,重写的 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)
val dbHelper = MyDatabaseHelper(this,"BookStore.db",1)
mainActivity.createDatabase.setOnClickListener {
dbHelper.writableDatabase
}
}
}
第一次点击button后,弹出toast,表明数据库文件已经新建完成。
查看数据表
借助一个叫作Database Navigator的插件工具可以查看数据表。
从Android Studio导航栏中打开Preferences→Plugins,就可以进入插件管理界面了:
点击
Install
后重启Android Studio。
打开 Device File Explorer
,然后进入 /data/data/com.example.databasetest/databases/
目录下,可以看到已经存在了一个 BookStore.db
文件,右击后选择 Save As
,将它放置在任意你方便查找的位置。
然后在Android Studio的左侧边栏找到
DB Browser
工具。为了打开刚刚导出的数据库文件,需要点击这个工具左上角的加号按钮,并选择
SQLite
选项然后在弹出窗口的
Database
配置中选择我们刚才导出的 BookStore.db
文件可以看到
BookStore.db
数据库中确实存在了一张 Book
表,并且 Book
表中的列也和前面使用的建表语句完全匹配,由此证明 BookStore.db
数据库和 Book
表确实已经创建成功了。; 升级数据库
onUpgrade()
方法是用于对数据库进行升级的。
在 val dbHelper = MyDatabaseHelper(this,"BookStore.db",1)
语句中,最后一个参数代表版本号,当版本号为 比1大的数
时,就执行 onUpgrade()
方法。
添加数据
调用 SQLiteOpenHelper
的 getReadableDatabase()
或 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
双击
Book
表格在弹出设置查询条件的窗口中点击 No Filter
,表示不需要设置任何查询条件,然后就能看到表中的数据了。
更新数据
SQLiteDatabase
中提供了一个非常好用的 update()
方法,用于对数据进行更新。
它接收4个参数:第一个参数和 insert()
方法一样,也是表名,指定更新哪张表里的数据;第⼆个参数是 ContentValues
对象,要把更新数据在这里组装进去;第三、第四个参数用于约束更新某一行或某几行中的数据,不指定的话默认会更新所有行。
完整使用
在 activity_main.xml
中再添加一个id为 updataData
的按钮
然后在 MainActivity
中的添加代码
mainActivity.updateData.setOnClickListener {
val db = dbHelper.writableDatabase
val values = ContentValues()
values.put("price",10.99)
db.update("Book",values,"name = ?", arrayOf("The Da Vinci Code"))
}
再次查看表格数据已经更改完成。
删除数据
SQLiteDatabase
中提供了一个 delete()
方法,专门用于删除数据。
它接收3个参数:第一个参数仍然是表名;第二、第三个参数用于约束删除某一行或某几行的数据,不指定的话默认会删除所有行。
完整使用
在 activity_main.xml
中再添加一个id为 deleteData
的按钮
然后在 MainActivity
中的添加代码
mainActivity.deleteData.setOnClickListener {
val db = dbHelper.writableDatabase
db.delete("Book","pages > ?", arrayOf("500"))
}
再次查看表格。
查询数据
SQLiteDatabase
中还提供了一个 query()
方法用于对数据进行查询。
它至少接收7个参数:第一个参数还是表名,表示从哪张表中查询数据;第二个参数用于指定去查询哪几列,如果不指定则默认查询所有列;第三、第四个参数用于约束查询某一行或某几行的数据,不指定则默认查询所有行的数据;第五个参数用于指定需要去 group by
的列,不指定则表示不对查询结果进行 group by
操作;第六个参数用于对 group by
之后的数据进行进一步的过滤,不指定则表示不进行过滤;第七个参数用于指定查询结果的排序方式,不指定则表示使用默认的排序方式。 query()
方法参数对应SQL部分描述tablefrom table_name指定查询的表名columnsselect column1, column2指定查询的列名selectionwhere column = value指定 where
的约束条件selectionArgs-为 where
中的占位符提供具体的值groupBygroup by column指定需要 group by
的列havinghaving column = value对 group by
后的结果进一步约束orderByorder by column1, column2指定查询结果的排序方式
调用 query()
方法后会返回一个 Cursor
对象,查询到的所有数据都将从这个对象中取出。
完整使用
在 activity_main.xml
中再添加一个id为 queryData
的按钮
然后在 MainActivity
中的添加代码
mainActivity.queryData.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()
}
运行后点击 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版,那么两条判断语句都会执行。使用这种方式来维护数据库的升级,不管版本怎样更新,都可以保证数据库的表结构是最新的,而且表中的数据完全不会丢失。
Original: https://blog.csdn.net/m0_50754064/article/details/122703581
Author: 我还能码嘛。
Title: 《Android》Chap.7 数据持久化
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/815768/
转载文章受原作者版权保护。转载请注明原作者出处!