数据持久化就是指将那些内存中的瞬时数据保存到存储设备中,保证即使在手机或计算机关机的情况下,这些数据仍然不会丢失。
保存在内存中的数据是处于瞬时状态的,而保存在存储设备中的数据是处于持久状态的。
持久化技术提供了一种机制,可以让数据在瞬时状态和持久状态之间进行转换。
文件存储是Android中最基本的数据存储方式,它不对存储的内容进行任何格式化处理,所有数据都是原封不动地保存到文件当中的,因而它比较适合存储一些简单的文本数据或二进制数据。如果你想使用文件存储的方式来保存一些较为复杂的结构化数据,就需要定义一套自己的格式规范,方便之后将数据从文件中重新解析出来。
Context
类中提供了一个openFileOutput()
方法,可以用于将数据存储到指定的文件中。这个方法接收两个参数:
第一个参数是文件名,在文件创建的时候使用,注意这里指定的文件名不可以包含路径,因为所有的文件都默认存储到/data/data/
目录下。
第二个参数是文件的操作模式,主要有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)
}
//保证在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
中输入一串字符
按下返回键退出后,在文件中查找:
借助蓝色箭头指向的工具
参照红色方框标记的路径
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/
目录下的;
第二个参数用于指定操作模式,目前只有默认的MODE_PRIVATE
这⼀种模式可选,它和直接传入0的效果是相同的,表示只有当前的应用程序才可以对这个SharedPreferences文件进行读写。
此方法只接收一个操作模式参数,因为使用这个方法时会自动将当前Activity的类名作为SharedPreferences的文件名。
得到了SharedPreferences对象之后,就可以开始向SharedPreferences文件中存储数据了,主要可以分为3步实现。
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 {
//获取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
文件,其中保存了刚刚所有的数据。
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")
}
编写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()
}
}
}
}
当输入正确的账号密码并选择保存后,关闭应用,再次打开时,账号和密码已经自动填充。
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/
目录下。此时,重写的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,表明数据库文件已经新建完成。
借助一个叫作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() //构建ContentValues对象
values.put("price",10.99) //将价格这一列的数据改为10.99
db.update("Book",values,"name = ?", arrayOf("The Da Vinci Code"))
//第三个参数表示更新所有name=?的行,?表示占位符,对应的是第四个参数的内容
}
SQLiteDatabase
中提供了一个delete()
方法,专门用于删除数据。
它接收3个参数:第一个参数仍然是表名;第二、第三个参数用于约束删除某一行或某几行的数据,不指定的话默认会删除所有行。
在activity_main.xml
中再添加一个id为deleteData
的按钮
然后在MainActivity
中的添加代码
mainActivity.deleteData.setOnClickListener {
val db = dbHelper.writableDatabase
db.delete("Book","pages > ?", arrayOf("500")) //删除页数大于500的
}
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() //查询完要关闭
}
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版,那么两条判断语句都会执行。使用这种方式来维护数据库的升级,不管版本怎样更新,都可以保证数据库的表结构是最新的,而且表中的数据完全不会丢失。