持久化技术 持久化技术简介 数据持久化就是指将那些内存中的瞬时数据保存到存储设备中,保证即使在手机或计算机关机 的情况下,这些数据仍然不会丢失。保存在内存中的数据是处于瞬时状态的,而保存在存储设 备中的数据是处于持久状态的。持久化技术提供了一种机制,可以让数据在瞬时状态和持久状 态之间进行转换。
文件存储 文件存储是Android中最基本的数据存储方式,它不对存储的内容进行任何格式化处理,所有数 据都是原封不动地保存到文件当中的,因而它比较适合存储一些简单的文本数据或二进制数 据。如果你想使用文件存储的方式来保存一些较为复杂的结构化数据,就需要定义一套自己的 格式规范,方便之后将数据从文件中重新解析出来。
将数据存储到文件中 Context类提供了一个openFileOutput()方法,可以用于将数据存储到指定的文件中。接收两个参数:第一个参数是文件名,不能包含路径;第二个参数是文件的操作模式,主要有MODE_PRIVATE和MODE_APPEND两种模式可选,默认是MODE_PRIVATE,表示当指定相同文件名的时候,所写入的内容将会覆盖原文件中的内容,而MODE_APPEND则表示文件已存在,就往文件里面追加内容, 不存在就创建新文件。
openFileOutput()方法返回的是一个FileOutputStream对象,得到这个对象之后就可以使用Java流的方式将数据写入文件中了。以下是一段简单的代码示例,展示了如何将一段文本文件内容保存到文件中:
fun save (inputText:String ) { val output = openFileOutput("data" ,Context.MODE_PRIVATAE) val write = BuffererWrite(OutputStreamWriter(output)) writer.use{ it.write(inputText) } }
这里通过openFileOutput()方 法能够得到一个FileOutputStream对象,然后借助它构建出一个OutputStreamWriter对 象,接着再使用OutputStreamWriter构建出一个BufferedWriter对象,这样你就可以通 过BufferedWriter将文本内容写入文件中了。
注意,这里还使用了一个use函数,这是Kotlin提供的一个内置扩展函数。它会保证在Lambda 表达式中的代码全部执行完之后自动将外层的流关闭,这样就不需要我们再编写一个finally 语句,手动去关闭流了,是一个非常好用的扩展函数
另外,Kotlin是没有异常检查机制(checked exception)的。这意味着使用Kotlin编写的所 有代码都不会强制要求你进行异常捕获或异常抛出。上述代码中的try catch代码块是参照 Java的编程规范添加的,即使你不写try catch代码块,在Kotlin中依然可以编译通过。
下面我们就编写一个完整的例子,借此学习一下如何在Android项目中使用文件存储的技术。首 先创建一个FilePersistenceTest项目,并修改activity_main.xml中的代码,如下所示:
<LinearLayout xmlns:android ="http://shcemas.android.com/apk/res/android" android:orientation ="vertical" android:layout_width ="match_parent" android:layout_height ="match_parent" > <EditText android:id ="@+id/editText" android:layout_width ="match_parent" android:layot_height ="wrap_content" android:hint ="Type something here" /> </LinearLayout >
这里我们要做的,就是在数据被回收之前,将它存储到 文件当中。修改MainActivity中的代码,如下所示:
class MainActivity : AppCompatActivity (){ private lateinit var binding:ActivityMainBinding override fun onCreate (savedInstanceState:Bundle ?) { super .onCreate(savedInstanceState) setContentView(binding.root) } override fun onDestroy () { super .onDestroy() val inputText = 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() } } }
可以看到,首先我们重写了onDestroy()方法,这样就可以保证在Activity销毁之前一定会调 用这个方法。在onDestroy()方法中,我们获取了EditText中输入的内容,并调用save()方 法把输入的内容存储到文件中,文件命名为data。save()方法中的代码和之前的示例基本相 同,这里就不再做解释了。
然后按下Back键关闭程序,这时我们输入的内容就保存到文件中了。那么如何才能证实数据确 实已经保存成功了呢?我们可以借助Device File Explorer工具查看一下。这个工具在Android Studio的右侧边栏当中,通常是在右下角的位置,如果你的右侧边栏中没有这个工具的话,也 可以使用快捷键Ctrl + Shift + A(Mac系统是command + shift + A)打开搜索功能,在搜 索框中输入“Device File Explorer”即可找到这个工具。
从文件中读取数据 类似于将数据存储到文件中,Context类中还提供了一个openFileInput()方法,用于从文 件中读取数据。这个方法要比openFileOutput()简单一些,它只接收一个参数,即要读取的 文件名,然后系统会自动到/data/data//files/目录下加载这个文件,并返 回一个FileInputStream对象,得到这个对象之后,再通过流的方式就可以将数据读取出来 了。
以下是一段简单的代码示例,展示了如何从文件中读取文本数据:
fun load () :String{ val content = StringBuilder() try { val input = openFileInput("data" ) val reader = BufferedReader(InputStreamReader(input)) reader.use{ reader.forEachLine{ content.append(it) } } } }
了解了从文件中读取数据的方法,那么我们就来继续完善上一小节中的例子,使得重新启动程 序时EditText中能够保留我们上次输入的内容。修改MainActivity中的代码,如下所示
class MainActivity :AppCompatActivity (){ private lateinit var binding:ActivityMainBinding override fun onCreate (savedInstanceState:Bundle ?) { super .onCreate(savedInstaceState) binding = ActivityMianBinding.inflate(layoutInflater) val inputText = load() val editText = binding.editText if (inputText.isNotEmpty()){ editText.setText(inputText) editText.setSelection(inputTextt.length) Toast.makeText(this ,"Restoring succeeded" ,Toast.LENGTH_SHORT).show() } } private 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() } }
可以看到,这里的思路非常简单,在onCreate()方法中调用load()方法读取文件中存储的文 本内容,如果读到的内容不为空,就调用EditText的setText()方法将内容填充到EditText 里,并调用setSelection()方法将输入光标移动到文本的末尾位置以便继续输入,然后弹出 一句还原成功的提示。load()方法中的细节我们在前面已经讲过,这里就不再赘述了。
读写文件总结 写文件用到的几个类 openFileOutput() BufferedWritter() OutPutStreamWriter()
读文件用到的几个类 openFileInput() BufferdReader() InPutStreamReader()
SharedPreferences存储 将数据存储到SharedPreferences中 要想使用SharedPreferences存储数据,首先需要获取SharedPreferences对象。Android中主要提供了以下两种方法用于得到SharedPreference对象
Context类中的getSharedPreferences()方法
此方法接收两个参数:第一个参数用于指定SharedPreference文件的名称,如果指定的文件不存在则会创建一个,SharedPreferences文件都是存在/data/data//shared_prefs目录下的;第二个参数用于指定操作模式,目前只有默认的MODE_PRIVATE这一种模式可选,它和直接传入0的效果是相同的,表示只有当前的应用程序才可以对这个SharedPreferences文件进行读写。
Activity类中的getPreferences()方法
这个方法和Context中的getShared方法很相似,不过它直接收一个操作模式参数,因为使用这个方法时会自定将当前的Activity的类名作为SharedPreferences的文件名
向SharedPreferences文件中存储数据的步骤
调用SharedPreferences对象的edit()方法获取一个SharedPreferences.Editor对象
向SharedPreferences.Editor对象中添加数据,比如添加一个布尔型数据就使用putBoolean()方法,添加一个字符串则使用putString()方法,以此类推
调用apply()方法将添加的数据提交,从而完成数据存储操作
新建一个SharedPreferencesTest项目,然后修改activity_main.xml中的代码,如下所示
<LinearLayout xmlns:android ="http://schemas.android.com/apk/res/android" android:layout_width ="match_parent" android:layout_height ="match_parent" android:orientation ="vertical" > <Button android:id ="@id/saveButton" android:layout_width ="match_parent" android:layout_height ="wrap_content" android:text ="Save Data" /> </LinearLayout >
这里我们不做任何复杂的功能,只是简单地放置了一个按钮,用于将一些数据存储到 SharedPreferences文件当中。然后修改MainActivity中的代码,如下所示:
class MainActivity : AppCompatActivity (){ private lateinit binding:ActivityMainBinding override fun onCreate (savedInstanceState:Bundle ?) { super .onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) val saveButton = binding.saveButton saveButton.setOnClickListener{ val editor = getSharedPreferences("data" ,Context.MODE_PRIVATE).edit() editor.putString("name" ,"Tom" ) editor.putInt("age" ,28 ) editor.putBoolean("married" ,false ) editor.apply() } } }
从SharedPreferences中读取数据 通过例子来实际体验一下吧,仍然是在SharedPreferencesTest项目的基础上继续开 发,修改activity_main.xml中的代码,如下所示:
<LinearLayout xmlns:android ="http://schemas.android.com/apk/res/android" android:layout_width ="match_parent" android:layout_height ="match_parent" android:orientation ="vertical" > <Button android:id ="@+id/saveButton" android:layout_width ="match_parent" android:layout_height ="wrap_content" android:text ="Save Data" /> <Button android:id ="@id/restoreButton" android:layout_width ="match_parent" android:layout_height ="wrap_content" android:text ="Restore Data" /> </LinearLayout >
class MainActivity : AppCompatActivity () { override fun onCreate (savedInstanceState: Bundle ?) { super .onCreate(savedInstanceState) setContentView(R.layout.activity_main) ... 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 " ) } } }
实现记住密码的功能 activity_login.xml中的代 码,如下所示:
<?xml version="1.0" encoding="utf-8" ?> <LinearLayout xmlns:android ="http://schemas.android.com/apk/res/android" android:orientation ="vertical" android:layout_width ="match_parent" android:layout_height ="match_parent" > <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" android:hint ="Enter your account" /> </LinearLayout > <LinearLayout android:layout_width ="match_parent" android:layout_height ="60dp" android:orientation ="horizontal" > <TextView android:layout_width ="90dp" android:layout_height ="wrap_content" android:layout_gravity ="center_vertical" android:text ="Password:" android:textSize ="18sp" /> <EditText android:id ="@+id/passwordEdit" android:layout_width ="0dp" android:layout_height ="wrap_content" android:layout_gravity ="center_vertical" android:layout_weight ="1" android:hint ="Enter your password" android:inputType ="textPassword" /> </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 >
这里使用了一个新控件:CheckBox。这是一个复选框控件,用户可以通过点击的方式进行选中 和取消,我们就使用这个控件来表示用户是否需要记住密码。
然后修改LoginActivity中的代码,如下所示:
package com.example.broadcastbestpracticeimport android.content.Contextimport android.content.Intentimport android.os.Bundleimport android.widget.Toastimport androidx.activity.enableEdgeToEdgeimport androidx.appcompat.app.AppCompatActivityimport androidx.core.view.ViewCompatimport androidx.core.view.WindowInsetsCompatimport com.example.broadcastbestpractice.databinding.ActivityLoginBindingclass LoginActivity : BaseActivity () { override fun onCreate (savedInstanceState: Bundle ?) { super .onCreate(savedInstanceState) val binding = ActivityLoginBinding.inflate(layoutInflater) setContentView(binding.root) val login = binding.login val accountEdit = binding.accountEdit val passwordEdit = binding.passwordEdit val prefs = getPreferences(Context.MODE_PRIVATE) val isRemember = prefs.getBoolean("remember_password" ,false ) val rememberPass = binding.rememberPass if (isRemember){ val account = prefs.getString("account" ,"" ) val password = prefs.getString("password" ,"" ) accountEdit.setText(account) passwordEdit.setText(password) rememberPass.isChecked = true } login.setOnClickListener { val account = accountEdit.text.toString() val password = passwordEdit.text.toString() 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() } } } }
SQLite数据库存储 创建数据库 Android为我们提供了一个SQLiteOpenHelper帮助类,我们可以通过这个类来对数据库进行创建和升级
SQLiteOpenHelper是一个抽象类,如果要使用就需要自己创建一个类来继承它。SQLiteOpenHelper中有两个抽象方法:onCreate()和onUpgrade()。我们必须在自己的帮助类里重写这两个方法,然后分别在这两个方法中实现创建和升级数据库的逻辑
SQLiteOpenHelper中还有两个非常重要的实例方法:getReadableDatabase()和getWritableDatabase()。这两个方法都可以创建或打开一个现有的数据库(如果数据库已存在则直接打开,否则要创建一个新的数据库),并返回一个可对数据库进行读写操作的对象。不同的是,当数据库不可写入的时候(如磁盘空间)已满,getReadable方法返回的对象将以只读的方式打开数据库,而getWritableDatabase()方法则将出现异常。
SQLiteOpenHelper中有两个构造方法可供重写,一般使用参数少一点的构造方法。这个构造方法中接收4个参数:第一个参数是Context,第二个是数据库名,第三个参数是Cursor,一般传入null,第四个参数表示当前的数据库的版本号。构建出SQLiteOpenHelper实例之后,再调用它的getReadableDatabase()或getWritableDatabase()方法就能创建数据库了,数据库文件会放在/data/data//databases/目录下
接下来还是让我们通过具体的例子来更加直观地体会SQLiteOpenHelper的用法吧,首先新建 一个DatabaseTest项目
这里我们希望创建一个名为BookStore.db的数据库,然后在这个数据库中新建一张Book表, 表中有id(主键)、作者、价格、页数和书名等列。创建数据库表当然还是需要用建表语句的, 这里就要考验一下你的SQL基本功了,Book表的建表语句如下所示:
create table Book( id integer primary key autoincrement, author text, price real, pages integer, name text) )
integer:表示整型,real表式浮点型,text表示文本类型,blob表示二进制类型,使用primary key将id类设为主键,并用autoincrement关键字表示id列是自增长的
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 ) }
可以看到,我们把建表语句定义成了一个字符串变量,然后在onCreate()方法中又调用了 SQLiteDatabase的execSQL()方法去执行这条建表语句,并弹出一个Toast提示创建成功, 这样就可以保证在数据库创建完成的同时还能成功创建Book表。
现在修改activity_main.xml中的代码,如下所示
<LinearLayout xmlns:android ="http://schemas.android.com/apk/res/android" android:orientation ="vertical" android:layout_width ="match_parent" android:layout_height ="match_parent" > <Button android:id ="@id/createDatabase" android:layout_width ="match_parent" android:layout_height ="wrap_content" android:text ="Create Daabase" /> </LinearLayout >
布局文件很简单,就是加入了一个按钮,用于创建数据库。最后修改MainActivity中的代码, 如下所示:
class MainActivity : AppCompatActivity (){ private lateinit var binding:ActivityMainBinding override fun onCreate (saveInstanceState:Bundle ?) { super .onCreate(savedInstance) val dbHelper = MyDatabaseHelper(this ,"BookStore.db" ,1 ) val createDatabase = binding.createDatabase createDatabase.setOnClickListener{ dbHelper.writableDatabase } } }
这里我们在onCreate()方法中构建了一个MyDatabaseHelper对象,并且通过构造函数的参 数将数据库名指定为BookStore.db,版本号指定为1,然后在“Create Database”按钮的点击 事件里调用了getWritableDatabase()方法。这样当第一次点击“Create Database”按钮 时,就会检测到当前程序中并没有BookStore.db这个数据库,于是会创建该数据库并调用 MyDatabaseHelper中的onCreate()方法,这样Book表也就创建好了,然后会弹出一个 Toast提示创建成功。再次点击“Create Database”按钮时,会发现此时已经存在 BookStore.db数据库了,因此不会再创建一次。
创建数据库的步骤
注意 使用Database Navigation插件时候路径不要加双引号
升级数据库 目前,DatabaseTest项目中已经有一张Book表用于存放书的各种详细数据,如果我们想再添 加一张Category表用于记录图书的分类,该怎么做呢
比如Category表中有id(主键)、分类名和分类代码这几个列,那么建表语句就可以写成:
create table Category( id integer primary key autoincrement category_name text, category_code ingeger )
接下来我们将这条建表语句添加到MyDatabaseHelper中,代码如下所示:
class MyDatabaseHelper (context:Context,name:String,version:Int ): SQLiteopenHelper(context,name,version){ private val createBook = "create table Book(" + "id integer primary key autoincrement," + "author text," + "pages integer," + "name text)" private val createCategory = "create table category(" + "id integer primary key autoincrement," + "category_name text," + "category_code text)" override fun onCreate (db:SQLiteDatabase ) { db.execSQL(createBook) db.execSQL(createCategory) Toast.makeText(context,"Create succeed" ,Toast.LENGHT_SHORT).show() } override fun onUpgrade (db:SQLiteDatabase ,oldVerdion:Int ,newVersion:Int ) { db.exeSQL("drop table if exists Book" ) db.exeSQL("drop table if exists category" ) onCreate(db) } }
MainActivity的代码
class MainActivity :AppCompatActivity (){ private lateinit var binding : ActivityMainBinding override fun onCreate (savedInstanceState:Bundle ?) { super .onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInfalter) setContentView(binding.root) val dbHelper = MyDataBaseHelper(this ,"BookStore.db" ,1 ) val createDatabase = binding.createDatabase createDatabase.setOnClickListener{ dbHelper.writableDatabase } } }
添加数据 我们可以对数据进行的操作无非有4种,即CRUD,C代表添加(create),R代表查找(retrieve),U代表更新(update),D代表删除(delete)。
前面我们已经知道,调用SQLiteOpenHelper的getReadableDatabase()或 getWritableDatabase()方法是可以用于创建和升级数据库的,不仅如此,这两个方法还都 会返回一个SQLiteDatabase对象,借助这个对象就可以对数据进行CRUD操作了
那么下面我们首先学习一下如何向数据库的表中添加数据吧。SQLiteDatabase中提供了一个 insert()方法,专门用于添加数据。它接收3个参数:第一个参数是表名,我们希望向哪张表 里添加数据,这里就传入该表的名字;第二个参数用于在未指定添加数据的情况下给某些可为 空的列自动赋值NULL,一般我们用不到这个功能,直接传入null即可;第三个参数是一个 ContentValues对象,它提供了一系列的put()方法重载,用于向ContentValues中添加数 据,只需要将表中的每个列名以及相应的待添加数据传入即可
介绍完了基本用法,接下来还是让我们通过例子来亲身体验一下如何添加数据吧。修改 activity_main.xml中的代码,如下所示:
<LinearLayout xmlns:android ="http://schemas.android.com/apk/res/android" android:orientation ="vertical" android:layout_widht ="match_parent" android:Llayout_height ="match_parent" > <Button android:id ="@+id/createDatabase" android:layout_width ="match_parent" android:layout_height ="wrap_content" android:text ="createDatabase" /> <Button android:id ="@id/addData" android:layout_width ="match_parent" android:layout_height ="wrap_content" android:text ="Add Data" /> </LinearLayout >
可以看到,我们在布局文件中又新增了一个按钮,稍后就会在这个按钮的点击事件里编写添加 数据的逻辑。接着修改MainActivity中的代码,如下所示:
class MainActivity : AppCompatActivity (){ private lateinit var binding:ActivityMainBinding override fun Create (savedInstaceState:Bundle ?) { super .onCreate(savedInstanceState) setContentView(binding.root) val deHelper = MyDatabaseHelper(this ,"BookStore.db" ,2 ) val createDatabase = binding.createDatabase val addData = binding.addData createDatabase.setOnClickListener{ dbHelper.writableDatabase } addData.setOnClickListener{ val db = dbHelper.writableDatabase val values1 = ContentValues().apply{ put("name" ,"The Da Vinci Code" ) put("author" ,"Dan Brown" ) put("pages" ,454 ) put("price" ,16.96 ) } db.insert("Book" ,null ,values1) val value2 = ContentValues().applly{ put("name" ,"The Lost Symbol" ) put("author" ,"Dan Brown" ) put("pages" ,510 ) put("price" ,29.95 ) } db.insert("Book" ,null ,values2) } } }
更新数据 SQLiteDatabase中提供了一个非常好用的update()方法,用于对数据进行更新。这个方法 接收4个参数:第一个参数和insert()方法一样,也是表名,指定更新哪张表里的数据;第二 个参数是ContentValues对象,要把更新数据在这里组装进去;第三、第四个参数用于约束更 新某一行或某几行中的数据,不指定的话默认会更新所有行。 那么接下来,我们仍然是在DatabaseTest项目的基础上修改,看一下更新数据的具体用法。比 如刚才添加到数据库里的第一本书,由于过了畅销季,卖得不是很火了,现在需要通过降低价 格的方式来吸引更多的顾客,我们应该怎么操作呢?首先修改activity_main.xml中的代码,如 下所示:
<LinearLayout xmlns:android ="http://schemas.android.com/apk/res/android" android:orientation ="vertical" android:layout_widht ="match_parent" android:Llayout_height ="match_parent" > <Button android:id ="@+id/createDatabase" android:layout_width ="match_parent" android:layout_height ="wrap_content" android:text ="createDatabase" /> <Button android:id ="@id/addData" android:layout_width ="match_parent" android:layout_height ="wrap_content" android:text ="Add Data" /> <Button android:id ="@id/updateData" android:layout_width ="match_parent" android:layout_height ="wrap_content" android:text ="Update Data" /> </LinearLayout >
修改MainActivity的代码
class MainActivity : AppCompatActivity (){ private lateinit var binding : ActivityMainBinding override fun onCreate (savedInstanceState:Bundle ?) { super .onCreate(savedInstance) binding = ActivityMainBinding.inflate(layoutInflater) setContent(binding.root) val createDatabase = binding.createDatabase val dbHelper = MyDataBaseHelper(this ,"BookStore.db" ,1 ) createDatabase.setOnClickListener{ dbHelper.writableDatabase } val addData = binding.addData{ val db = dbHleper.writableDatabase val values1 = ContentValues().apply{ put("name" ,"The Da Vinci Code" ) put("author" ,"Dan Brown" ) put("pages" ,454 ) put("price" ,16.96 ) } 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) } val updateData = binding.updataData updateData.setOnClickListern{ val db = dbHelper.writableDatabase val values3 = ContentValues() valuse3.put("price" ,10.99 ) db.updata("Book" ,values3,"name=?" ,arrayOf("The Da Vinci Code" )) } } }
删除数据 修改activity_main.xml中的代码,如下 所示:
<LinearLayout xmlns:android ="http://schemas.android.com/apk/res/android" android:orientation ="vertical" android:layout_width ="match_parent" android:layout_height ="match_parent" > ... <Button android:id ="@+id/deleteData" android:layout_width ="match_parent" android:layout_height ="wrap_content" android:text ="Delete Data" /> </LinearLayout >
然后修改MainActivity中的代码,如下 所示:
class MainActivity : AppCompatActivity (){ private lateinit var binding:ActivityMainBinding override fun onCreate (savedInstanceState:Bundle ?) { super .onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) val dbHelper = MyDataBaseHelper(this ,"BookStore.db" ,1 ) val createDatabase = binding.createDatabase createDatabase.setOnClickListener{ dbHelper.writableDatabase } val addData = binding.addData addData.setOnClickListener{ val db = dbHelper.writableDatabase val values1 = ContentValues().apply{ put("name" ,"The Da Vinci Code" ) put("author" ,"Dan Brown" ) put("pages" ,454 ) put("price,16.96" ) } db.insert("Book" ,null ,values) 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) } val updataData = binding.updateData updataData.setOnClickListener{ val db = dbHelper.writableDatabase val values3 = ContentValue() values3.put("price" ,10.99 ) db.update("Book" ,values3,"name=?" ,arrayOf("The Da Vinci Code" )) } val deleteData = binding.deleteData deleteData.setOnClickListener{ val db = dbHelper.writableDatabase db.delete("Book" ,"pages>?" ,arrayOf("500" )) } } }
查询数据 SQLiteDatabase中还提供了一个query()方法用于对数据进行查询。这 个方法的参数非常复杂,最短的一个方法重载也需要传入7个参数。那我们就先来看一下这7个 参数各自的含义吧。第一个参数不用说,当然还是表名,表示我们希望从哪张表中查询数据。 第二个参数用于指定去查询哪几列,如果不指定则默认查询所有列。第三、第四个参数用于约 束查询某一行或某几行的数据,不指定则默认查询所有行的数据。第五个参数用于指定需要去 group by的列,不指定则表示不对查询结果进行group by操作。第六个参数用于对group by 之后的数据进行进一步的过滤,不指定则表示不进行过滤。第七个参数用于指定查询结果的排 序方式,不指定则表示使用默认的排序方式。
虽然query()方法的参数非常多,但是不要对它产生畏惧,因为我们不必为每条查询语句都指 定所有的参数,多数情况下只需要传入少数几个参数就可以完成查询操作了。调用query()方 法后会返回一个Cursor对象,查询到的所有数据都将从这个对象中取出。
下面还是让我们通过具体的例子来体验一下查询数据的用法,修改activity_main.xml中的代 码,如下所示:
<LinearLayout xmlns:android ="http://schemas.android.com/apk/res/android" android:orientation ="vertical" android:layout_width ="match_parent" android:layout_height ="match_parent" > ... <Button android:id ="@+id/queryData" android:layout_width ="match_parent" android:layout_height ="wrap_content" android:text ="Query Data" /> </LinearLayout >
这个已经没什么好说的了,添加了一个按钮用于查询数据。然后修改MainActivity中的代码, 如下所示:
class MainActivity : AppCompatActivity (){ override fun onCreate (savedInstanceState:Bundle ?) { super .onCreate(savedInstanceState) setContentView(R.layout.activity_main) val dbHelper = MyDatabaseHelper(this ,"BookStore.db" ,1 ) 、、、、 qerryData.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() } } }
最后别忘了调用close()方法来关闭 Cursor
SQLite数据库基本操作总结 创建 使用SQLiteOpenHelper
class MyDatabaseHelper (val context:Context, val name:String, val version:Int ): SQLiteOpenHelper(context,name,version){ private val createBook = "create table Book(" + "id integer primary key autoincrement," + "autor text," + "pages integer," + "name text)" private val createCategory = "create table Category(" + "id integer primary key autoincrement," + "category_name text," + "category_code integer)" override fun onCreate (db:SQLiteDatabase ) { db.execSQL(createBook) db.execSQL(createCategory) Toast.makeText(context,"Create succeed" ,Toast.LENGTH_SHORT).show() } override fun onUpgrade (db:SQLiteDatabase ,oldVersion:Int ,newVersion:Int ) { db.execSQL("drop table if exists Book" ) db.execSQL("drop table if exists Category" ) } }
接下来是完整操作
class MainActivity : AppCompatActivity (){ @SuppressLint("Range" ) private lateinit binding:ActivityMainBinding override fun onCreate (savedInstanceState:Bundle ?) { super .onCreate(savedInstaceState) binding.inflate(layoutInflater) setContentView(binding.root) val dbHelper = MyDatabaseHelper(this ,"BookStore.db" ,1 ) val createDatabase = binding.createDatabase createDatabase.setOnClickListener{ dbHelper.writableDatabase } val addData = binding.addData addData.setOnClickListener{ val db = dbHelper.writableDatabase val values1 = ContentValues().apply{ put("name" ,"The Da Vinci Code" ) put("author" ,"Dan Brown" ) put("pages" ,454 ) put("price" ,16.96 ) } 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 ,values) } val updateData = binding.updateData updateData.setOnClickListener{ val db = dbHelper.writableDatabase val values3 = ContentValues() values3.put("price" ,10.99 ) db.update("Book" ,value3,"name=?" ,arrayOf("The Da Vinci Code" )) } val deleteData = binding.deleteData deleteData.setOnClickListener{ val db = dbHelper.writableDatabase db.delete("Book" ,"pages>?" ,arrayOf("500" )) } val queryData = binding.queryData queryData.setOnClickListener{ val db = dbHelper.writableDatabase val cursor = db.query("Book" ,null ,null ,null ,null ,null ,null ) if (cursor.moveToFirst()){ do { val name = cursor.getStrng(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.movtToNext) cursor.close() } } } }
记得调用cursor.close()
使用SQL操作数据库 虽然Android已经给我们提供了很多非常方便的API用于操作数据库,不过总会有一些人不习惯 使用这些辅助性的方法,而是更加青睐于直接使用SQL来操作数据库。如果你也是其中之一的 话,那么恭喜,Android充分考虑到了你们的编程习惯,同样提供了一系列的方法,使得可以直 接通过SQL来操作数据库。
下面我就来简略演示一下,如何直接使用SQL来完成前面几个小节中学过的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" ))
查询数据 val cursor = db.rawQuery("select * from Book" ,null )
SQLite数据库的最佳实践 使用事务 class MainActivity :AppCompatActivity (){ override fun onCreate (savedInstanceState:Bundle ?) { super .onCreate(savedInstanceState) setContentView(R.layout.activity_main) val dbHelper = MyDatabaseHelper(this ,"BookStore.db" ,2 ) 、、、 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" ,"Georage Martin" ) put("pages" ,720 ) put("price" ,20.85 ) } db.insert("Book" ,null ,values) db.setTransactionSuccessful() }catch (e:Exception){ e.printStackTrace() }finally { db.endTransaction() } } } }
升级数据库的最佳写法 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," + "category_id integer)" private val createCategory = "create table Category(" + "category_name text," + "category_code integer)" override fun onCreate (db:SQLiteDatabase ) { db.execSQL(createBook) 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" ) } } }
Kotlin:高阶函数的应用 简化SharedPreference的用法 首先来看SharedPreferences,在开始对它进行简化之前,我们先回顾一下 SharedPreferences原来的用法。向SharedPreferences中存储数据的过程大致可以分为 以下3步:
(1) 调用SharedPreferences的edit()方法获取SharedPreferences.Editor对象; (2) 向SharedPreferences.Editor对象中添加数据; (3) 调用apply()方法将添加的数据提交,完成数据存储操作。
对应的代码示例如下:
val editor = getSharedPreferences("data" , Context.MODE_PRIVATE).edit() editor.putString("name" , "Tom" ) editor.putInt("age" , 28 ) editor.putBoolean("married" , false ) editor.apply()
当然,这段代码其实本身已经足够简单了,但是这种写法更多还是在用Java的编程思维来编写 代码,而在Kotlin当中我们明显可以做到更好。
fun SharedPreferences.open (block: SharedPreferences .Editor .() -> Unit ) { val editor = edit() editor.block() editor.apply() }
定义好了open函数之后,我们以后在项目中使用SharedPreferences存储数据就会更加方便 了,写法如下所示:
getSharedPreferences("data" , Context.MODE_PRIVATE).open { putString("name" , "Tom" ) putInt("age" , 28 ) putBoolean("married" , false ) }
简化ContentValues的用法 新建一个ContentValues.kt文件,然后在里面 定义一个cvOf()方法,如下所示:
fun cvOf (vararg pairs: Pair <String , Any?>) : ContentValues { }
这个方法的作用是构建一个ContentValues对象,有几点我需要解释一下。首先,cvOf()方 法接收了一个Pair参数,也就是使用A to B语法结构创建出来的参数类型,但是我们在参数 前面加上了一个vararg关键字,这是什么意思呢?其实vararg对应的就是Java中的可变参数 列表,我们允许向这个方法传入0个、1个、2个甚至任意多个Pair类型的参数,这些参数都会 被赋值到使用vararg声明的这一个变量上面,然后使用for-in循环可以将传入的所有参数遍 历出来。
Any是Kotlin中所有类的共同基类,相当于Java中的Object,而Any?则表示允许传入空值
接下来我们开始为cvOf()方法实现功能逻辑,核心思路就是先创建一个ContentValues对 象,然后遍历pairs参数列表,取出其中的数据并填入ContentValues中,最终将 ContentValues对象返回即可。思路并不复杂,但是存在一个问题:Pair参数的值是Any?类 型的,我们怎样让它和ContentValues所支持的数据类型对应起来呢?这个确实没有什么好的 办法,只能使用when语句一一进行条件判断,并覆盖ContentValues所支持的所有数据类 型。结合下面的代码来理解应该更加清楚一些:
fun cvOf (vararg pairs: Pair <String , Any?>) = 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) } } }
有了这个cvOf()方法之后,我们使用ContentValues时就会变得更加简单了,比如向数据库 中插入一条数据就可以这样写:
val values = cvOf("name" to "Game of Thrones" , "author" to "George Martin" , "pages" to 720 , "price" to 20.85 ) db.insert("Book" , null , values)