完整代码Gitee地址:kotlin-demo: 15天Kotlin学习计划
第六天学习内容代码:Chapter6
前言
简介
知识点1:文件存储
知识点2:sharedPreferences存储
知识点3:SQLite数据库存储
创建数据库
添加数据
更新数据
删除数据
读取数据
知识点4:BuildConfig分包
知识点5:实战封装高性能存储
总结
前言
????????任何一个应用程序,其实说白了就是在不停地和数据打交道,没有数据的应用程序就变成了一个空壳子,对用户来说没有任何实际用途。那么这些数据是从哪儿来的呢?现在多数的数据基本是由用户产生的,比如你发微博、评论新闻,其实都是在产生数据。
简介
????????数据持久化就是指将那些内存中的瞬时数据保存到存储设备中,保证即使在手机或计算机关机的情况下,这些数据仍然不会丢失。保存在内存中的数据是处于瞬时状态的,而保存在存储设备中的数据是处于持久状态的。持久化技术提供了一种机制,可以让数据在瞬时状态和持久状态之间进行转换。持久化技术被广泛应用于各种程序设计领域,而本节要探讨的自然是Android中的数据持久化技术。
????????Android系统中主要提供了3种方式用于简单地实现数据持久化功能:文件存储、SharedPreferences存储以及数据库存储。下面来看一看这三种方式,以及封装的高性能缓存工具是如何实现的。
知识点1:文件存储
????????文件存储是Android中最基本的数据存储方式,它不对存储的内容进行任何格式化处理,所有数据都是原封不动地保存到文件当中,下面来写一例子存储到内部路径,不需要额外申请读写权限,封装一个FileUtils工具类,实现了保存、读取文件:
class FileUtils {
companion object {
/*保存文件*/
@JvmStatic
fun saveFile(context: Context, fileName: String, cont: String) {
try {
//第一个参数是文件名
//第二个参数是文件的操作模式(相同文件MODE_PRIVATE覆盖,MODE_APPEND追加内容)
val output = context.openFileOutput(fileName, Context.MODE_PRIVATE)
val writer = BufferedWriter(OutputStreamWriter(output))
writer.use {
it.write(cont)
}
} catch (e: Exception) {
}
}
/*读取文件*/
@JvmStatic
fun readerFile(context: Context, fileName: String): String {
val content = StringBuffer()
try {
val input = context.openFileInput(fileName)
val reader = BufferedReader(InputStreamReader(input))
reader.use {
reader.forEachLine {
content.append(it)
}
}
} catch (e: Exception) {
}
return content.toString()
}
}
}
????????下面我们就编写一个完整的例子,并修改activity_learn6.xml中的代码,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#e8e8e8"
android:orientation="vertical">
<com.example.kotlin_demo.TitleLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<EditText
android:id="@+id/edit_cont"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:hint="输入储存内容,退出保存"
android:text=""
android:textColor="@color/teal_700"
android:textSize="16sp" />
</LinearLayout>
????????在文本输入框中随意输入点什么内容,再按下Back键,这时输入的内容肯定就已经丢失了,因为它只是瞬时数据,在Activity被销毁后就会被回收。而这里我们要做的,就是在数据被回收之前,将它存储到文件当中。修改Learn6Activity中的代码,如下所示:
private val fileName = "strData"//文件名
override fun onDestroy() {
super.onDestroy()
//页面关闭储存文件
FileUtils.saveFile(this, fileName, editCont.text.toString())
}
????????现在重新运行一下程序,并在EditText中输入一些内容,这时我们输入的内容就保存到文件中了。那么如何才能证实数据确实已经保存成功了呢?我们可以借助Device File Explorer工具查看一下。这个工具在Android Studio的右侧边栏当中,我们来打开看一看,路径data - data - 下。
? ? ? ? 从文件中读取数据,代码如下所示:
//1、文件储存,onDestroy方法中调用存储,存储到应用内不需要申请权限;
editCont = findViewById(R.id.edit_cont)
//读取文件
val fileCont = FileUtils.readerFile(this, fileName)
if (fileCont.isNotEmpty()) {
editCont.setText(fileCont)
editCont.setSelection(fileCont.length)
}
????????现在重新运行一下程序,刚才保存的Something important字符串肯定会被填充到EditText中,然后编写一点其他的内容,比如在EditText中输入“Hello world”,接着按下Back键退出程序,再重新启动程序,这时刚才输入的内容并不会丢失,而是还原到了EditText中,如下图所示:
知识点2:sharedPreferences存储
????????不同于文件的存储方式,SharedPreferences是使用键值对的方式来存储数据的,如下所示:
//2、sharedPreferences存储
butSave = findViewById(R.id.but_save)
butSave.setOnClickListener {
//第一个参数是文件名
//第二个参数默认的MODE_PRIVATE这一种模式可选,可为0
val editor = getSharedPreferences(fileName, Context.MODE_PRIVATE).edit()
editor.putString("account", "PeaceJay")
editor.putInt("passWord", 123456)
editor.putBoolean("remember", false)
editor.apply()
}
//读取文件
butRead = findViewById(R.id.but_read)
butRead.setOnClickListener {
val editor = getSharedPreferences(fileName, Context.MODE_PRIVATE)
val userName = editor.getString("account", "")
val userAge = editor.getInt("passWord", 0)
val toast = "account= $userName passWord= $userAge"
Toast.makeText(this, toast, Toast.LENGTH_SHORT).show()
}
? 可以看到生成了一个data.xml文件,一般用来轻量存储,比如登录账号信息,效果如下所示:
知识点3:SQLite数据库存储
????????SQLite是一款轻量级的关系型数据库,它的运算速度非常快,占用资源很少,通常只需要几百KB的内存就足够了,特别适合在移动设备上使用。文件存储和SharedPreferences存储毕竟只适用于保存一些简单的数据和键值对,当需要存储大量复杂的关系型数据的时候,你就会发现以上两种存储方式很难应付得了。
创建数据库
????????Android为了让我们能够更加方便地管理数据库,专门提供了一个SQLiteOpenHelper帮助类,借助这个类可以非常简单地对数据库进行创建和升级,新建一个SqliteHelper帮助类:
//第二个参数是数据库名,创建数据库时使用的就是这里指定的名称;
//第三个参数允许我们在查询数据的时候返回一个自定义的Cursor,一般传入null即可;
//第四个参数表示当前数据库的版本号
class SqliteHelper(val context: Context, name: String, version: Int) :
SQLiteOpenHelper(context, name, null, version) {
//建表语句 primary key将id列设为主键,并用autoincrement关键字表示id列是自增长
//integer表示整型,real表示浮点型,text表示文本类型,blob表示二进制类型
private val createBook =
"create table Book(id integer primary key autoincrement,author text,price real,pages integer,name text)"
override fun onCreate(db: SQLiteDatabase) {
//execSQL()方法去执行这条建表语句
db.execSQL(createBook)//Book表存放书的各种详细数据
Toast.makeText(context, "create succeeded", Toast.LENGTH_SHORT).show()
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
}
}
????????可以看到,我们把建表语句定义成了一个字符串变量,然后在onCreate()方法中又调用了SQLiteDatabase的execSQL()方法去执行这条建表语句,并弹出一个Toast提示创建成功,这样就可以保证在数据库创建完成的同时还能成功创建Book表。
在Learn6Activity新增创建表方法:
//3、SQLite数据库存储 SQLiteOpenHelper帮助类,不能降版本
val dbHelper = SqliteHelper(this, "bookInfo.db", 1)
butSqliteAdd = findViewById(R.id.but_Sqlite_add)
butSqliteAdd.setOnClickListener {
//当前程序中并没有BookStore.db这个数据库,于是会创建该数据库并调用MyDatabaseHelper中的onCreate()方法,
//再次点击“Create Database”按钮时,会发现此时已经存在BookStore.db数据库了,因此不会再创建一次。
dbHelper.writableDatabase
}
????????这里我们在onCreate()方法中构建了一个SqliteHelper对象,并且通过构造函数的参数将数据库名指定为bookInfo.db,版本号指定为1,点击事件里调用了getWritableDatabase()方法。这样当第一次点击按钮时,就会检测到当前程序中并没有BookStore.db这个数据库,于是会创建该数据库并调用MyDatabaseHelper中的onCreate()方法,这样Book表也就创建好了,然后会弹出一个Toast提示创建成功。当你再次点击按钮时,不会再有Toast弹出。版本改为2,会进入onUpgrade更新方法。来验证一下是否创建成功:
????????这个目录下还存在另外一个bookKinfo.db-journal文件,这是一个为了让数据库能够支持事务而产生的临时日志文件,通常情况下这个文件的大小是0字节,我们可以暂时不用管它。
添加数据
????????SQLiteDatabase中提供了一个insert()方法,专门用于添加数据。它接收3个参数:第一个参数是表名,我们希望向哪张表里添加数据,这里就传入该表的名字;第二个参数用于在未指定添加数据的情况下给某些可为空的列自动赋值NULL,一般我们用不到这个功能,直接传入null即可;第三个参数是一个ContentValues对象,它提供了一系列的put()方法重载,用于向ContentValues中添加数据,只需要将表中的每个列名以及相应的待添加数据传入即可。
//添加数据
butSqliteInsert = findViewById(R.id.but_Sqlite_insert)
butSqliteInsert.setOnClickListener {
val db = dbHelper.writableDatabase
val value1 = ContentValues().apply {
put("author", "汉斯·克里斯汀·安徒生")
put("name", "安徒生童话")
put("pages", 230)
put("price", 16.08)
}
db.insert("Book", null, value1)
val value2 = ContentValues().apply {
put("author", "格林")
put("name", "格林童话")
put("pages", 430)
put("price", 26.28)
}
db.insert("Book", null, value2)
Toast.makeText(this, "插入完成", Toast.LENGTH_SHORT).show()
}
????????点击一下数据添加按钮,此时两条数据应该都已经添加成功了。要怎么验证数据是否添加成功呢,这时间就要用到插件了,Database Navigator,Preferences→Plugins,就可以进入插件管理界面了,安装成功后重启工具。
????????现在对着bookInfo.db文件右击→Save As,将它从模拟器导出到你的计算机的任意位置。然后观察Android Studio的左侧边栏何顶部菜单栏,现在应该多出了一个DB Browser工具,如图所示:
为了打开刚刚导出的数据库文件,我们需要点击这个工具左上角的加号按钮,并选择SQLite选项?
?
然后在弹出窗口的Database配置中选择我们刚才导出的bookInfo.db文件位置,如图所示。?
????????点击“OK”完成配置,这个时候DB Browser中就会显示出bookInfo.db数据库里所有的内容了,如图所示。
????????想要查询哪张表的内容,只需要双击这张表就可以了,这里我们双击Book表,直接点击窗口下方的“NoFilter”按钮即可:
更新数据
????????调用了SQLiteDatabase的update()方法执行具体的更新操作,可以看到,这里使用了第三、第四个参数来指定具体更新哪几行。第三个参数对应的是SQL语句的where部分,表示更新所有name等于?的行,而?是一个占位符,可以通过第四个参数提供的一个字符串数组为第三个参数中的每个占位符指定相应的内容,arrayOf()方法是Kotlin提供的一种用于便捷创建数组的内置方法。
butSqliteUpdate = findViewById(R.id.but_Sqlite_updata)
butSqliteUpdate.setOnClickListener {
//构建了一个ContentValues对象,并且只给它指定了一组数据
val db = dbHelper.writableDatabase
val values = ContentValues()
values.put("author", "古天乐")
values.put("name", "是兄弟就来砍我")
values.put("pages", 430)
values.put("price", 33.28)
//arrayOf()方法是Kotlin提供的一种用于便捷创建数组的内置方法
db.update("Book", values, "name = ?", arrayOf("安徒生童话"))
Toast.makeText(this, "更新完成", Toast.LENGTH_SHORT).show()
}
删除数据
????????SQLiteDatabase中提供了一个delete()方法,专门用于删除数据。这个方法接收3个参数:第一个参数仍然是表名,这个没什么好说的;第二、第三个参数用于约束删除某一行或某几行的数据,不指定的话默认会删除所有行。
//删除数据
butSqliteDelete = findViewById(R.id.but_Sqlite_delete)
butSqliteDelete.setOnClickListener {
val db = dbHelper.writableDatabase
db.delete("Book", "pages > ?", arrayOf("400"))
Toast.makeText(this, "删除完成", Toast.LENGTH_SHORT).show()
}
读取数据
? ? ? ? 查询调用了SQLiteDatabase的query()方法查询数据。这里的query()方法非常简单,只使用了第一个参数指明查询Book表,后面的参数全部为null。这就表示希望查询这张表中的所有数据,虽然这张表中目前只剩下一条数据了。
????????查询完之后就得到了一个Cursor对象,接着我们调用它的moveToFirst()方法,将数据的指针移动到第一行的位置,然后进入一个循环当中,去遍历查询到的每一行数据。在这个循环中可以通过Cursor的getColumnIndex()方法获取某一列在表中对应的位置索引,然后将这个索引传入相应的取值方法中,就可以得到从数据库中读取到的数据了。接着我们使用Log将取出的数据打印出来,借此检查读取工作有没有成功完成。最后调用close()方法来关闭Cursor。
butSqliteRead = findViewById(R.id.but_Sqlite_read)
butSqliteRead.setOnClickListener {
val db = dbHelper.writableDatabase
//语法展示、与直接使用SQL
//val queryValue = db.query("Book",null,null,null,null,null,null)
val queryValue = db.rawQuery("select * from Book", null)
if (queryValue.moveToFirst()) {
do {
val name = queryValue.getString(queryValue.getColumnIndex("name"))
Log.i("TAG", "name: $name")
} while (queryValue.moveToNext())
}
queryValue.close()
}
在真正的项目中,可能会遇到比这要复杂得多的查询功能?,这需要自己慢慢摸索。
知识点4:BuildConfig分包
????????使用BuildConfig实现多渠道打包,为下一个知识点做储备;
plugins {
id 'com.android.application'
id 'kotlin-android'
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.example.kotlin_demo"
minSdkVersion 16
targetSdkVersion 30
versionCode 1
versionName "1.0.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true //分包
flavorDimensions "versionCode"//使用了productFlavors多渠道必须添加
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
//各个环境的配置
productFlavors {
dev {
applicationId "peaceJay.Kotlin"
resValue('string', 'app_name', 'Kotlin-开发版')
buildConfigField "String", "SERVER_BASE", '"http://xxxx"'
}
beta {
applicationId "peaceJay.Kotlin"
resValue('string', 'app_name', 'Kotlin-测试版')
buildConfigField "String", "SERVER_BASE", '"http://xxxx"'
}
prod {
applicationId "peaceJay.Kotlin"
resValue('string', 'app_name', 'Kotlin')
buildConfigField "String", "SERVER_BASE", '"http://xxxx"'
}
}
android.applicationVariants.all { variant ->
variant.outputs.all { output ->
if (variant.productFlavors[0].name.endsWith("dev")) {
outputFileName = "app_dev_v${defaultConfig.versionName}_${defaultConfig.versionCode}_${buildTime()}.apk"
} else if (variant.productFlavors[0].name.endsWith("beta")) {
outputFileName = "app_beta_v${defaultConfig.versionName}_${defaultConfig.versionCode}_${buildTime()}.apk"
} else if (variant.productFlavors[0].name.endsWith("prod")) {
outputFileName = "app_release_v${defaultConfig.versionName}_${defaultConfig.versionCode}_${buildTime()}.apk"
}
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
}
static def buildTime() {
return new Date().format("yyyy-MM-dd-HH-mm-ss", TimeZone.getTimeZone("GMT+08:00"))
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.1'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
/* 灵活的RecyclerView框架 */
implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.4'
/* JSON解析 */
implementation 'com.google.code.gson:gson:2.8.6'
}
applicationId 会改变日志目录,如图:
resValue是应用包名,如图:
outputFileName是打包自动生成的文件名,如图:
知识点5:实战封装高性能存储
????????磨刀不误砍柴工,我们在开始一个完整的项目之前,首先需要的是一个趁手的工具和框架。新建java class文件命名Prefs,实现了磁盘、内存缓存,多运用与存储多种类型数据,与JSON数据。
public class Prefs {
// 数据缓存器
private static Map<Object, Object> dataMap;
// 储存对象
private static SharedPreferences prefer;
/**
* 初始化
*
* @param context {@link Application}
*/
public static void init(Application context) {
Prefs.dataMap = new HashMap<>();
Prefs.prefer = context.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE);
}
/**
* 对象数据缓存
*
* @param key 键
* @param data 数据对象
*/
public static void put(String key, Object data) {
Prefs.put(key, data, true);
}
/**
* 对象数据缓存
*
* @param key 键
* @param data 数据对象
* @param diskCache 是否缓存到磁盘
*/
public static void put(String key, Object data, boolean diskCache) {
// 内存缓存
Prefs.dataMap.put(key, data);
if (diskCache && data != null) {
// 磁盘缓存
if (data instanceof Integer) {
// int
Prefs.prefer.edit().putInt(key, (Integer) data).apply();
} else if (data instanceof Long) {
// long
Prefs.prefer.edit().putLong(key, (Long) data).apply();
} else if (data instanceof Float) {
// float
Prefs.prefer.edit().putFloat(key, (Float) data).apply();
} else if (data instanceof Boolean) {
// boolean
Prefs.prefer.edit().putBoolean(key, (Boolean) data).apply();
} else if (data instanceof String) {
// String
Prefs.prefer.edit().putString(key, (String) data).apply();
} else {
// Object
Prefs.prefer.edit().putString(key, JSON.toJson(data)).apply();
}
}
}
/**
* 获取对象数据缓存
*
* @param key 键
* @param cls 数据类型
* @param <T> 泛型
* @return 返回指定类型的数据对象或null
*/
@SuppressWarnings("unchecked")
public static <T> T object(String key, Class<T> cls) {
if (Prefs.dataMap.containsKey(key)) {
return (T) Prefs.dataMap.get(key);
}
String result = Prefs.prefer.getString(key, null);
if (result != null) {
T data = JSON.toObject(result, cls);
Prefs.dataMap.put(key, data);
return data;
}
return null;
}
/**
* 获取String类型的数据
*
* @param key 键
* @param def 默认
* @return 返回获取到的数据或默认值
*/
public static String get(String key, String def) {
if (Prefs.dataMap.containsKey(key)) {
return (String) Prefs.dataMap.get(key);
}
String result = Prefs.prefer.getString(key, def);
Prefs.dataMap.put(key, result);
return result;
}
/**
* 获取Boolean类型的数据
*
* @param key 键
* @param def 默认
* @return 返回获取到的数据或默认值
*/
public static boolean get(String key, boolean def) {
if (Prefs.dataMap.containsKey(key)) {
return (boolean) Prefs.dataMap.get(key);
}
boolean result = Prefs.prefer.getBoolean(key, def);
Prefs.dataMap.put(key, result);
return result;
}
/**
* 获取Float类型的数据
*
* @param key 键
* @param def 默认
* @return 返回获取到的数据或默认值
*/
public static float get(String key, float def) {
if (Prefs.dataMap.containsKey(key)) {
return (float) Prefs.dataMap.get(key);
}
float result = Prefs.prefer.getFloat(key, def);
Prefs.dataMap.put(key, result);
return result;
}
/**
* 获取Int类型的数据
*
* @param key 键
* @param def 默认
* @return 返回获取到的数据或默认值
*/
public static int get(String key, int def) {
if (Prefs.dataMap.containsKey(key)) {
return (int) Prefs.dataMap.get(key);
}
int result = Prefs.prefer.getInt(key, def);
Prefs.dataMap.put(key, result);
return result;
}
/**
* 获取Long类型的数据
*
* @param key 键
* @param def 默认
* @return 返回获取到的数据或默认值
*/
public static long get(String key, long def) {
if (Prefs.dataMap.containsKey(key)) {
return (long) Prefs.dataMap.get(key);
}
long result = Prefs.prefer.getLong(key, def);
Prefs.dataMap.put(key, result);
return result;
}
/**
* 清理内存缓存
*
* @param key 键
*/
public static void remove(String key) {
Prefs.dataMap.remove(key);
}
/**
* 清理内存缓存并删除磁盘缓存
*
* @param key 键
*/
public static void delete(String key) {
Prefs.remove(key);
Prefs.prefer.edit().remove(key).apply();
}
/**
* 清空内存中的数据
*/
public static void clean() {
Prefs.dataMap.clear();
}
}
使用前需要初始化,最好是在<application ? ? android:name=".App">中
Prefs.init(this);? 初始化缓存
这里演示了储存单个类型数据并读取,与实体数据保存与读取:
//①储存单个类型数据
Prefs.put("key.one", "String数据")
Prefs.put("key.two", 200, false)
//获取单个数据类型
val one = Prefs.get("key.one", "")
Log.i(tag, "key.one: $one")
val two = Prefs.get("key.two", 0)
Log.i(tag, "key.two: $two")
//②储存JSON数据
val student = PeopleBean("刘德华", 17320002222)
Prefs.put("key.Json", student)
//获取JSON数据
val bean = Prefs.`object`("key.Json",PeopleBean::class.java)
Log.i(tag, "key.Json: " + JSON.toJson(bean))
class PeopleBean(val name: String, val phone: Long)
效果展示:
总结
????????今天主要对Android常用的数据持久化方式进行了详细的讲解,虽然目前已经掌握了这几种数据持久化方式的用法,但需根据项目的实际需求来选择使用。
|