数据存储 文件存储 存储路径:/data/data/com.example.filepersistencetest/files/目录
将数据存储到文件中
1 2 3 4 5 6 7 8 9 10 11 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() } }
读取文件中的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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() }
SharedPreferences存储 存储路径:/data/data/<package name>/shared_prefs/目录下,xml文件形式
SharedPreferences
是 Android 中用于存储简单键值对数据的轻量级存储机制。它提供了一种简单的方式,用于在应用程序中存储少量的基本数据类型的数据,例如布尔值、整数、浮点数、字符串等。SharedPreferences
的数据存储在应用的私有文件系统中,其他应用无法直接访问。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 SharedPreferences preferences = getSharedPreferences("my_prefs" , Context.MODE_PRIVATE); SharedPreferences.Editor editor = preferences.edit(); editor.putString("username" , "JohnDoe" ); editor.putInt("score" , 1000 ); editor.putBoolean("isPremium" , true ); editor.apply();SharedPreferences preferences = getSharedPreferences("my_prefs" , Context.MODE_PRIVATE);String username = preferences.getString("username" , "" );int score = preferences.getInt("score" , 0 );boolean isPremium = preferences.getBoolean("isPremium" , false );
可以使用kotlin中的高阶函数来简化实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 fun SharedPreferences .open (block: SharedPreferences.Editor.() -> Unit ) { val editor = edit () editor.block () editor.apply () }getSharedPreferences ("data" , Context .MODE_PRIVATE ).open { putString ("name" , "Tom" ) putInt ("age" , 28 ) putBoolean ("married" , false ) }
其实Google提供的KTX扩展库中已经包含了上述SharedPreferences的简化用法,这个扩展库会在Android Studio创建项目的时候自动引入build.gradle的dependencies中
1 2 3 4 5 getSharedPreferences("data", Context.MODE_PRIVATE).edit { putString("name", "Tom") putInt("age", 28 ) putBoolean("married", false ) }
SQLite数据库存储 前面我们所学的文件存储和SharedPreferences存储毕竟只适用于保存一些简单的数据和键值对,当需要存储大量复杂的关系型数据的时候,你就会发现以上两种存储方式很难应付得了。
数据库文件会存放在/data/data/<package name>/databases/目录下
Android为了让我们能够更加方便地管理数据库,专门提供了一个SQLiteOpenHelper
帮助类,是抽象类,我们想要使用,就要创建一个自己的帮助类去继承它。并重写两个方法onCreate()
和onUpgrade()
,实现创建和升级数据库的逻辑。
有两个构造方法可供重写,一般使用参数少一点的那个构造方法即可
1 2 3 4 5 SQLiteOpenHelper(context, name, null , DB_VERSION); 第一个参数是Context,这个没什么好说的,必须有它才能对数据库进行操作; 第二个参数是数据库名,创建数据库时使用的就是这里指定的名称; 第三个参数允许我们在查询数据的时候返回一个自定义的Cursor,一般传入null 即可; 第四个参数表示当前数据库的版本号,可用于对数据库进行升级操作。
使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <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 Database" /> </LinearLayout>
MainActivity
1 2 3 4 5 6 7 8 9 10 11 12 class MainActivity : AppCompatActivity () { override fun onCreate (savedInstanceState: Bundle? ) { super .onCreate (savedInstanceState) setContentView (R.layout .activity_main ) val dbHelper = MyDatabaseHelper (this , "BookStore.db" , 1 ) createDatabase.setOnClickListener { dbHelper.writableDatabase } } }
这样当第一次点击“Create Database”按钮时,就会检测到当前程序中并没有BookStore.db这个数据库,于是会创建该数据库并调用MyDatabaseHelper中的onCreate()方法,再次点击“Create Database”按钮时,会发现此时已经存在BookStore.db数据库了,因此不会再创建一次。
升级数据库方法:
当在上面的例子中再添加一个数据库表的时候,并不会被创建,原因在上面黄色背景文字。解决方法就是用到了升级数据库的那个方法onUpgrade
,
1 2 3 4 5 6 7 8 9 10 11 12 class MyDatabaseHelper (val context : Context , name : String , version : Int ): SQLiteOpenHelper (context, name, null , version) { ... 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 ()`中的版本号改为2:` val dbHelper = MyDatabaseHelper (this , "BookStore.db" , 2 )
表示我们对数据库进行升级了。
上面这种方法是非常粗暴的,不建议在开发中这么编写, 因为:比如你编写的某个应用已经成功上线了,并且还拥有了不错的下载量。现在由于添加了新功能,数据库需要一起升级,结果用户更新了这个版本之后却发现以前程序中存储的本地数据全部丢失了!那么很遗憾,你的用户群体可能已经流失一大半了。
升级数据库的最佳写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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)" 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) } override fun onUpgrade (db: SQLiteDatabase, oldVersion: Int, newVersion: Int ) { if (oldVersion <= 1 ) { db.execSQL (createCategory) } } }
这样当用户直接安装第2版的程序时,就会进入onCreate()方法,将两张表一起创建。而当用户使用第2版的程序覆盖安装第1版的程序时,就会进入升级数据库的操作中,此时由于Book表已经存在了,因此只需要创建一张Category表即可。
但是没过多久,新的需求又来了,这次要给Book表和Category表之间建立关联,需要在Book表中添加一个category_id字段。再次修改MyDatabaseHelper中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 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 (" + "id integer primary key autoincrement," + "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" ) } } }
每当升级一个数据库版本的时候,onUpgrade()方法里都一定要写一个相应的if判断语句。为什么要这么做呢?这是为了保证App在跨版本升级的时候,每一次的数据库修改都能被全部执行。比如用户当前是从第2版升级到第3版,那么只有第二条判断语句会执行,而如果用户是直接从第1版升级到第3版,那么两条判断语句都会执行。使用这种方式来维护数据库的升级,不管版本怎样更新,都可以保证数据库的表结构是最新的,而且表中的数据完全不会丢失。
添加数据、修改数据、删除数据、查询数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 class MainActivity : AppCompatActivity() { override fun onCreate (savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) setContentView(R.layout.activity_main) val dbHelper = MyDatabaseHelper(this , "BookStore.db" , 2 ) ... 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 , values2) } updateData.setOnClickListener { val db = dbHelper.writableDatabase val values = ContentValues() values.put("price" , 10.99 ) db.update("Book" , values, "name = ?" , arrayOf("The Da Vinci Code" )) } deleteData.setOnClickListener { val db = dbHelper.writableDatabase db.delete("Book" , "pages > ?" , arrayOf("500" )) } 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() } } }
query ()方法参数的详细解释:
ContentValues用法也可以被简化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 fun cvOf (vararg pairs: Pair <String , Any?>) : ContentValues { val cv = 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 <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) } } }val values = cvOf("name" to "Game of Thrones" , "author" to "George Martin" , "pages" to 720 , "price" to 20.85 ) db.insert("Book" , null , values)
当然,虽然我们编写了一个非常好用的cvOf()方法,KTX库中也提供了一个具有同样功能的contentValuesOf()方法
1 2 3 val values = contentValuesOf("name" to "Game of Thrones", "author" to "George Martin", "pages" to 720 , "price" to 20.85 ) db.insert("Book", null , values )
使用事务 SQLite数据库是支持事务的,事务的特性可以保证让一系列的操作要么全部完成,要么一个都不会完成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 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" , "George Martin" ) put("pages" , 720 ) put("price" , 20.85 ) } db.insert("Book" , null , values) db.setTransactionSuccessful() } catch (e: Exception) { e.printStackTrace() } finally { db.endTransaction() } } } }
我们在删除旧数据的操作完成后手动抛出了一个NullPointerException,这样添加新数据的代码就执行不到了。不过由于事务的存在,中途出现异常会导致事务的失败,此时旧数据应该是删除不掉的。
将手动抛出异常的那行代码删除并重新运行程序,此时点击一下“Replace Data”按钮,就会将Book表中的数据替换成新数据了,你可以再使用“Query Data”按钮来验证一次。
使用SQL操作数据库 直接使用SQL来操作数据库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 )