赞
踩
学习目标:
关于Kotlin与Android的数据持久化操作,我们需要
对于Android中的4种主要存储方式的用法,包括共享参数SharedPreference、数据库SQLite、文件I/O操作、App的全局变量。
共享参数是Android系统最简单的数据持久化存储方式。不是因为它存储结构简单,而是因为开发编码简单。即使通过Java编写共享参数读写的代码,也不过寥寥数行。那么Kotlin究竟采用了什么技术手段,优化了java的不足呢?
共享参数SharedPreferences是Android最简单的数据存储方式,常用于存储“key-value”键值对数据。在使用共享参数之前,首先要调用getSharedPreferences方法声明文件名与操作模式。
SharedPreferences sps = getSharedPreferences("share",Context.MODE_PRIVATE);
// 该方法的第一个参数是文件名,参数share表示当前的共享参数文件是share.xml
//第2个参数是操作模式,一般填MODE_PRIVATE表示私有模式
使用共享参数要存储数据,要借助Editor类。
SharedPreferences.Editor editor=sps.edit();
editor.putString("name","尹磊");
editor.putInt("age",20);
editor.putBoolean("married",false);
editor.commit();
使用共享参数读取数据直接调用其对象的get()方法即可获取数据,
注意get()方法的第二个参数表示默认值。
String name=sps.getString("name","");
int age=sps.getInt("age",0);
boolean married=sps.getBoolean("married",false);
可以看出,共享参数的存取操作有些繁琐。因此实际开发中常将共享参数的相关操作提取到一个工具类,在新的工具类里面封装SharedPreferences的常用操作,下面便是一个共享参数工具类的Java代码例子:
public class SharedUtil{ private static SharedUtil mUtil; private static SharedPreferences mShared; public static SharedUtil getInstance(Context context){ if(mUtil == null){ mUtil=new SharedUtil(); } mShared = context.getSharedPreferences("share",Context.MODE_PRIVATE); return mUtil; } public void writeShared(String key,String value){ SharedPreferfences.Editor editor =mShared.edit(); editor.putString(key,value); editor.commit(); } public String readShared(String key,String defaultValue){ return mShared.getString(key,defaultVlaue); } } //使用工具类: //调用工具类写入共享参数 SharedUtil.getInstance(this).writeShared("name",“尹磊”); //调用工具类读取共享参数 String name =SharedUtil.getInstance(this).readShared("name","");
从上面的代码可以看出,其他数据类型的数据读写还没有写,如果外部需要先读取某个字段的数值,等处理完了再写回共享参数,那么使用该工具类也要2行代码。
以下是Kotlin封装共享参数的工具代码:
class Preference<T>(val context: Context,val name: String,val default: T):ReadWriteProperty<Any?,T>{ //通过属性代理初始化共享参数对象 val prefs: SharedPreferences by lazy{ context.getSharedPreferences("default",Context.MODE_PRIVATE)} //接管属性值的获取行为 override fun getValue(thisRef: Any?, property: KProperty<*>): T { return findPrefence(name,default) } //利用with函数定义临时的命名空间 private fun findPrefence(name: String, default: T): T = with(prefs){ val res: Any =when(default){ is Long -> getLong(name,default); is String -> getString(name,default); is Int -> getInt(name,default) is Boolean -> getBoolean(name,default) else -> throw IllegalArgumentException("This type can be saved into Preferences") } return res as T } //接管属性值的修改行为 override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { putPreference(name,value) } private fun putPreference(name: String, value: T)= with(prefs.edit()) { //putInt、putString方法返回Editor对象 when(value){ is Long -> putLong(name,value) is String -> putString(name,value) is Int -> putInt(name,value) is Boolean -> putBoolean(name,value) else -> throw IllegalArgumentException("This type can be saved into Preference") }.apply()//commit()方法和apply()方法都表示提交修改 } } //声明字符串的委托属性 private var name: String by Preference(this,"name","") //声明整型数类型的委托属性 private var age: Int by Preference(this,"age",0)
上述代码的运行结果和Java的运行结果是一致的。
上述涉及到的知识点补充说明:
这个Preference运用了:
模板类
因为共享参数允许保存的数据类型包括整形、浮点型、字符串等,所有将Preference定义成模板类,具体的参数类型在调用的时候再指定。
除了代表模板类泛型的T,该类还有与之相似的Any和*。
T、Any、* 三者之间的区别:
1):T是抽象的泛型,再模板类中用来占位子,外部再调用模板类时才能确定T的具体类型。
2):Any是Kotlin的基本类型,所有kotlin类都从Any派生而来,相当于Java里面的Object。
3):星号* 表示一个不确定的类型,同样也是在外部调用时才能确定。但T出现在模板类的定义中,而与模板类无关,它出现在单个函数帝国一的参数列表中,因此相当于Java里面的问号?
委托属性/属性代理
注意到外部利用Preference声明参数字段的时候,后面跟着表达式“by Preference(…)”,这个by表示代理的动作。所谓的属性代理,就是说该属性的类型不变,但是属性的读写行为被后面的类接管了。
接管属性的读写行为的必要性:
举例,交电费,市民需要每个月都交电费,每个月自己跑去营业厅交钱很麻烦,后来支持在网上自主缴费,但是需要用户主动上网缴费,可能出现用户忘记缴费。所以银行推行“委托代扣”的业务,用户只要跟银行签约并指定委托扣费的电力账户,那么每个月指定时间,银行会自动从用户银行卡扣费并缴纳给指定的电力账户,免去了用户的人工操作。
委托缴费场景对应到共享参数这里,开发者的人工操作是:手工的编码从SharedPreference类读取数据和保存数据。而自动操作指的是给出一个月约定:代理的属性自动通过模板类“Preference<T>”完成数据的读取和保存,也就是说,Preference<T>接管了这希望属性的读写行为,接管后的操作即为模板类的getValue和setValue方法,因此,属性被接管的行为叫做属性代理,而被代理的属性称作委托属性。
例如下方的with(){}函数使用示例:
private fun findPrefence(name: String, default: T): T = with(prefs){
val res: Any =when(default){
is Long -> getLong(name,default);
is String -> getString(name,default);
is Int -> getInt(name,default)
is Boolean -> getBoolean(name,default)
else -> throw IllegalArgumentException("This type can be saved into Preferences")
}
return res as T
}
可以看出,with方法的函数语句分为2部分:
SQLite是手机上的轻量级数据库,但与Oracle一样存在数据库的创建、变更、删除、连接等DDL操作,以及数据表的增、删、改、查等DML操作。
Android为SQLite提供了2个管理类:SQLiteDatabase、SQLiteOpenHelper类。
//创建数据库,如果已经存在,就打开
SQLiteDatabase db =getApplicationContext().openOrCreateDatabase("test.db",Context.MODE_PRIVATE,null);
//删除数据库
getApplicationContext().deleteDatabase("test.db");
SQLiteDatabase的常见方法:
游标Cursor类的常用方法:
·1.游标控制类方法,用于指定游标的状态。
1):close:关闭游标。
2):isClosed:判断游标是否关闭。
3):isFirst:判断游标是否在开头。
4):isLast:判断游标是否在末尾。
2.游标移动类方法,把游标移动到指定位置。
1):moveToFirst:移动游标到开头。
2):moveToLast:移动游标到末尾。
3):moveToNext:移动游标到下一个。
4):moveToPrevious:移动游标到上一个。
5):move:往后移动游标若干偏移量。
3.获取记录类方法,可获取记录的数量、类型以及取值。
1):getCount:获取记录数。
2):getInt:获取指定字段的整型值。
3):getFloat:获取指定字段的浮点数值。
4):getString:获取指定字段的字符串值。
5):getType:获取指定字段的字段类型。
对于Kotlin,运用更加安全的ManagedSQLiteOpenHelper:
系统自带的SQLiteOpenHelper它并未封装数据库管理类SQLiteDataabse,造成一个后果:开发者需要操作表之前中手工打开数据库连接,然后再操作结束后手工关闭数据库连接。可是手工开关数据库连接存在着诸多问题。比如数据库连接是否造成业务异常。都制约SQLiteOpenHelper的安全性。
于是,Kotin结合Anko库推出了改良版的SQLite管理工具————ManagedSQLiteOpenHelper.它封装了数据库连接的开关操作,使得开发者完全无须关心SQLiteDatabase在何处、何时调用,避免了手工开关数据库连接可能导致的各种异常。ManagedSQLiteOpenHelper的用法与SQLiteOpenHelper几乎一模一样,唯一区别:数据表的增删改查语句需要放在use语句块之中,具体格式:
use{
//1.插入记录
//insert(...)
//2.更新记录
//update(...)
//3.删除记录
//delete(...)
//4.查询记录
//query(...)/rawQuery(...)
}
注:新的管理类ManagedSQLiteOpenHelper来自于Anko库,所有在文件头部,需要导入import org.jetbrains.anko.db.ManagedSQLiteOpenHelper。另外,有别于常见的anko-common包,Anko库把和数据库有关的部分放到了anko-sqlit包中,所有,需要修改模块下的build.gradle文件,在dependencies节点中补充anko-sqlite包编译配置:compile "org.jetbrains.anko:anko-sqlite:$anko_version"
数据库不是万能的,更多的其他格式的数据仍然要以文件形式保存。Java的文件I/O功能很强大,但是很啰嗦。
手机上的存储空间:内部存储、外部存储。内部存储放的是手机系统以及各应用的安装目录。外部存储放的是公共文件,如图片、文档、音视频文件等。
早期的外部存储被作为可拔插的SD卡,然而SD卡质量不统一,经常影响APP的正常运行。现如今,我们的手机把SD卡固化到手机内部,但Android仍然称之为外部存储。由于内部存储空间优先,因此为了不影响系统的流畅运行,APP运行过程中需要处理的文件都保存在外部存储空间。
为了保证App正常的读写外部存储,需要在清单文件增加SD卡权限配置:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
完成上面的权限配置后,代码里面就可以正常读写SD卡的文件。Android从7.0开始加强了SD卡的权限管理,即使声明了完整的SD卡操作权限,系统仍然默认禁止该APP访问外部存储。不过系统默认关闭存储只是关闭外部存储的公共空间,外部存储的私有空间仍然可以正常读写。因为Android把外部存储分为了2块区域:所有应用均可访问的公共空间、只有应用自己才可以访问的专享空间。
内部存储保存着每个应用的安装目录,但是安装目录的空间很紧张,所有Android在SD卡的“Android/data”目录下给每个应用又单独建了一个文件目录,用于给应用保存自己需要处理的临时文件。这个给每个应用单独建立的文件目录只有当前应用才能够读写文件,其他应用是不允许进行读写的,所以"Android/data"目录算是外部存储上的私有空间。这个私有空间本身已经做了访问权限的控制,因此它不受系统禁止访问的影响,应用操作自己的文件目录就不成问题。私有文件的目录只有属主应用才能访问,所以一旦属主应用被用户卸载,那么对应的文件目录也会被一起清理掉。
关于外部存储中的2个空间的路径获取方法:
1.获取公共空间的存储路径调用的是Environment.getExternalStoragePublicDirectory()
2.获取应用私有空间的存储路径调用的是:getExternalFilesDir()
Android 7.0之后默认禁止访问公共存储目录。
Java中常常封装一个文件工具类:
public class FileUtil{ //保存文本文件 public static void saveText(String path,String txt){ try{ FileOutPutStream fos=new FileOutPutStream(path); fos.write(txt.getBytes()); fos.close(); }catch(Exception e){ e.printStackTrace(); } } //读取文本文件 public static String openText(String path){ String readStr=""; try{ FileInputStream fis=new FileInputStream(path); byte[] b=new byte[fis.available()]; fis.read(b); readStr=new String(b); fis.close(); }catch(Exception e){ e.printStackTrace(); } return readStr; } }
上述Java代码,“长!”那么我们来看看Kotlin怎么处理的:
文件的写入操作:
//把文本写入文件
File(file_path).writeText(content)
如要往源文件追加文本,则可调用appendText()。
文件的读取操作:
//从文件中读取全部的文本:
val content=File(file_path).readText()
像图片等2进制格式的文件,可通过字节数组的形式写入文件,kotlin提供了writeBytes()方法用于覆盖写入字节数组,也提供了appendBytes()方法用于追加数组。
但是由于图像存储比较特殊,涉及到压缩格式与压缩质量,因此还得通过输出流来处理(Bitmap的compress()方法要求的)
//图片文件的写入代码 fun saveImage(path: String,bitmap: Bitmap){ try{ val file= File(path) //outputStream获取文件的输出流对象 //writer获取文件的writer对象 //printWriter获取文件的PrintWriter对象 val fos:OutputStream =file.outputStream() //压缩格式未JPG图像,压缩质量为80% bitmap.compress(Bitmap.CompressFormat.JPEG,80,fos) fos.flush() fos.close() }catch (e: Exception){ e.printStackTrace() } }
要想从图片文件中读取位图信息,按上面的writeBytes使用说明,应调用readBytes()方法。该办法可行,Android的BitmapFactory刚好提供了decodeByteArray()函数,用于从字节数组中解析位图:
//方式1:利用字节数组读取位图
//readBytes读取字节数组形式的文件内容
val bytes=File(file_path).readBytes()
//decodeByteArray从字节数组中解析图片
val bitmap=BitmapFactory.decodeByteArray(bytes,0,bytes.size)
将位图保存为图片文件时,通过输出流进行处理;反过来,从文件读取位图数据也可以用输入流来实现。BitmapFactory的decodeStream()方法使得输入流解析位图变成现实:
//方式2:利用输入流读取位图
//inputStream获取文件的输入流对象
val fis=File(file_path).inputStream()
//decodeStream从输入流解析图片
val bitmap=BitmapFactory.decodeStream(fis)
fis.close()
上述的2种读取图片文件的方式都包含了:先从File对象获得文本内容,再利用BitmapFactory解码成位图。
大招:decodeFile。只要给出图片文件的完整路径,文件读取和 位图解析的操作都一起搞定。
//方式3:直接从文件路径获取位图
//decodeFile从指定路径解析图片
val bitmap=BitmapFactory.decodeFile(file_path)
Kotlin把目录遍历重新梳理了以下,归纳为FileTreeWalk文件树,通过给文件树设置各式各样的参数与条件即可化繁为简,轻松获取文件的搜索结果。
文件树首先调用File对象的walk方法得到FileTreeWalk实例,接着依次为该实例设置具体的条件,包括遍历深度、是否匹配文件夹、文件扩展名以及最后的文件队列循环处理。
var fileNames:MutableList<String> =mutableListOf()
//在该目录下走一圈,得到文件目录树结构
val fileTree:FileTreeWalk = File(mPath).walk()
fileTree.maxDepth(1)//需遍历的目录层级为1,即无须检查子目录
.filter{it.isFile}//只挑选文件,不处理文件夹
//.filter{it.extension == "text"}//选择扩展名为txt的文本文件
.filter{it.extension in listOf("png","jpg")}//选择扩展名为png和jpg的图片文件
.forEach{fileNames.add(it.name)}//循环处理符合条件的文件
Application是android的又一组件,它的生命周期接连着app的整个运行过程,因此,开发者常常给自定义的Application运用单例模式,使之具备全部变量的管理功能。
在App运行过程中,有且仅有一个Application对象贯穿应用的整个生命周期,所以适合在Application中保存应用运行时的全局变量,而开展该工作的基础是必须获得Application对象的唯一实例,也就是Application单例化。获取一个类的单例对象需要运用程序设计中常见的单例模式,通过Java编码实现单例化:
public class MainApplication extends Application{
private static MainApplication mApp;
public static MainApplication getInstance(){
return mApp;
}
@Override
public void onCreate(){
super.onCreate();
mApp=this;
}
}
上述代码中,单例模式的实现过程主要有3个步骤:
上述代码同样的单例化过程通过Kotlin编码实现的话,静态属性和静态方法可利用伴生对象(理解为“人的影子”)来实现,这就形成了Kotlin单例化的第一种方式:手工声明属性单例化:
class MainApplication : Application(){ override fun onCreate() { super.onCreate() instance=this } //单例化的第一种方式:声明一个简单的application属性 companion object { //情况1:声明可空的属性 // private var instance: MainApplication? =null // fun instance()=instance!! //情况2:声明延迟初始化属性 private lateinit var instance: MainApplication fun instance()= instance } }
//利用系统代理行为实现单例化的kotlin代码:
class MainApplication : Application(){
override fun onCreate() {
super.onCreate()
instance=this
}
//单例化的第二种方式:利用系统字带的Delegates生成委托属性。
companion object {
private var instance: MainApplication by Delegates.notNull()
fun instance() = instance
}
}
上述的2种方式获取Application实例是一样的,带哦有"MainApplication.instance()"这个函数获得Application的自身实例。
//自定义代理行为的单例化代码 class MainApplication : Application(){ override fun onCreate() { super.onCreate() instance=this } //单例化的第三种方式:自定义一个非空且只能一次性赋值的委托属性 companion object { private var instance:MainApplication by NotNullSingleValueVar() fun instance() = instance } //定义一个属性管理类,进行非空和重复赋值的判断 private class NotNullSingleValueVar<T>() : ReadWriteProperty<Any?,T>{ private var value: T? =null //非空的校验 override fun getValue(thisRef: Any?, property: KProperty<*>): T { return value ?: throw IllegalArgumentException("application not initialized") } //重复赋值的校验 override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { this.value=if (this.value==null) value else throw IllegalArgumentException("application already initialized") } }
一旦有了单例的Application对象,就意味着App在运行程序过程中获取的Application实例是唯一的。因此可在该实例内部声明几个静态成员变量,从而形成所谓的全局变量。
全局的意思就是其他代码都可以引用该变量,因此,全局变量是共享数据和传递信息的好方法。
适合在Application中保存的全局变量主要由以下数据:
通过Applicaiton 实现全局变量的读写,需要:
//在清单文件中的配置:
<application
android:name=".MainApplication"(替换成自己继承Application的类的类名)
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name">
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。