本章我們介紹了跨程序共享CotentProvider。從權限機制分類(普通權限、危險權限)到6.0(API>=23)之後的運行時權限;從訪問系統聯繫人程序的數據到創建自己的ContentProvider供外部程序進行CRUD;從泛型、委託到實現自己的lazy函數,比較充實。
8.1.ContentProvider簡介
上一章談到的持久化存儲技術只能在當前程序中訪問,如何進行跨程序數據共享,考慮使用ContentProvider。譬如:系統通訊錄共享、短信、媒體庫等。ContentProvider可以選擇哪一部分數據進行共享,保證隱私不會被泄漏。ContentProvider會使用到運行時權限。
8.2.運行時權限
8.2.1.Android權限機制詳解
之前在BroadcastTest項目的AndroidManifest.xml中有一個這樣的權限聲明:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapplication">
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
....
</manifest>
檢測開機廣播涉及用戶安全,若不聲明會崩潰掉。加入此聲明的作用有兩個:1.低於6.0的系統上安裝程序,會清楚地知道申請了哪些權限,2.用於隨時在應用程序管理界面查看任意一個程序的權限申請情況。
理想很豐滿,現實很骨感,現在的app存在濫用權限的問題。在Android6.0之後加入了運行時權限功能,用戶無需一次性授權所有權限,可在使用過程中再對其進行授權。並不是所有權限都需要運行時申請,權限分爲普通和危險權限兩種,前者不會直接威脅隱私和安全,系統自動授權,譬如RECEIVE_BOOT_COMPLETED;後者可能會觸及用戶隱私或對設備安全性進行影響,譬如讀取設備聯繫人和定位GPS,手動授權。
Android共有100多種權限,危險權限有11組30個權限。如果是下表中的危險權限,需要運行時權限處理。
一旦用戶同意某個權限申請,那麼同組其他權限也會被自動授權。但Android系統可能隨時調整權限分組。
8.2.2.在程序運行時申請權限
新建RuntimePermissionTest項目,使用Call_PHONE權限來作爲示例,在6.0系統出現之前,實現很簡單。
<?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:orientation="vertical">
<Button
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:id="@+id/make_call"
android:text="Make Call"/>
</LinearLayout>
package com.example.myapplication
import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
make_call.setOnClickListener {
try {
//構建隱式Intent,Intent的action指定爲系統內置打電話工作,data部分指定了協議爲tel,號碼是10086.
//實現撥打電話的功能。ACTION_CALL是直接撥打電話,需要權限,ACTION_DIAL的是到撥號界面,無需權限。
val intent = Intent(Intent.ACTION_CALL)
intent.data = Uri.parse("tel:10086")
startActivity(intent)
} catch (e: SecurityException) {
e.printStackTrace()
}
}
}
}
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapplication">
<uses-permission android:name="android.permission.CALL_PHONE" />
.....
</manifest>
Android6.0之前可以正常運行,Android6.0(API大於等於23)之後會報錯Permission Denied。
嘗試修復這個問題,在MainActivity中進行修改:
package com.example.myapplication
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import kotlinx.android.synthetic.main.activity_main.*
import java.util.jar.Manifest
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
make_call.setOnClickListener {
//1.首先判斷用戶是不是已經授權過了,藉助checkSelfPermission函數,第一個參數是context,第二個是權限名,然後該返回值與PERMISSION_GRANTED比較。
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
//未曾授權
//ActivityCompat.requestPermissions申請授權,第一個參數是Activity實例,第二個String數組,申請的權限名放入數組即可;第三個參數是請求碼,確保唯一即可。
ActivityCompat.requestPermissions(this, arrayOf(android.Manifest.permission.CALL_PHONE), 1)
} else {
//已經授權
call()
}
}
}
//調用完requestPermissions自動彈出對話框申請權限,無論同意與否都會調用onRequestPermissionsResult方法
//grantResults是授權結果
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
1 -> {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
call()
} else {
Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show()
}
}
}
}
private fun call() {
try {
//構建隱式Intent,Intent的action指定爲系統內置打電話工作,data部分指定了協議爲tel,號碼是10086.
//實現撥打電話的功能。ACTION_CALL是直接撥打電話,需要權限,ACTION_DIAL的是到撥號界面,無需權限。
val intent = Intent(Intent.ACTION_CALL)
intent.data = Uri.parse("tel:10086")
startActivity(intent)
} catch (e: SecurityException) {
e.printStackTrace()
}
}
}
8.3.訪問其他程序中的數據
ContentProvider有兩種寫法:1.使用現有的ContentProvider讀取和操作相應程序的數據;2.創建自己的ContentProvider供外部訪問。Android系統的通訊錄、短信和媒體庫都有類似的訪問接口。
8.3.1.ContentResolver的基本寫法
藉助ContentProvider類來訪問共享實例,通過Context的getContentResolver方法來獲取該類實例,ContentProvider類提供了類似SQLiteBase的CRUD方法。不同於SQLiteBase,前者不接收表名參數,而藉助於Uri參數,內容URI是唯一標識符,分爲authority和path。前者是包名.provider,後者對不同的表進行區分,譬如/table1和/table2。最後還需要加上頭部協議聲明。因此完整的URI如下所示:
content://com.example.app.provider/table1
在得到URI字符串後,需要使用URi.parse方法將其解析爲Uri對象後作爲參數傳入,現在我們可以使用Uri對象來查詢,query的參數列表並不複雜,依次爲:uri、查詢列名projection、約束條件selection、約束條件佔位符具體值selectionArgs和排序方式sortOrder。查詢完後返回的是cursor對象。
8.3.2.讀取系統聯繫人
實現讀取系統聯繫人CotentProvier的功能,新建ContactsTest項目,修改activity_main.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:orientation="vertical">
<ListView
android:id="@+id/contacts_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
修改MainActivity.java:
package com.example.myapplication
import android.content.pm.PackageManager
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.provider.ContactsContract
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
private val contactsList = ArrayList<String>()
private lateinit var adapter: ArrayAdapter<String>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//ListView標準寫法
adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, contactsList)
contacts_view.adapter = adapter
//1.首先判斷用戶是不是已經授權過了,藉助checkSelfPermission函數,第一個參數是context,第二個是權限名,然後該返回值與PERMISSION_GRANTED比較。
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
//未曾授權
//ActivityCompat.requestPermissions申請授權,第一個參數是Activity實例,第二個String數組,申請的權限名放入數組即可;第三個參數是請求碼,確保唯一即可。
ActivityCompat.requestPermissions(this, arrayOf(android.Manifest.permission.READ_CONTACTS), 1)
} else {
//已經授權
readContacts()
}
}
//調用完requestPermissions自動彈出對話框申請權限,無論同意與否都會調用onRequestPermissionsResult方法
//grantResults是授權結果
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
1 -> {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
readContacts()
} else {
Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show()
}
}
}
}
private fun readContacts() {
//首先通過contentResolver.query查詢聯繫人數據,返回的是一個cursor對象。
//uri對象並沒有通過uri.parse去解析內容URI,這是因爲Phone類已經做好封裝,拿到的是Uri.parse解析後的結果
contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null)?.apply {
//通過移動遊標來遍歷Cursor的所有行,取出每一列中相應行的數據
while (moveToNext()) {
//獲取聯繫人姓名
val displayname = getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))
//獲取聯繫人號碼
val number = getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
contactsList.add("$displayname\n$number")
}
//刷新一下ListView
adapter.notifyDataSetChanged()
//將Cursor對象關閉
close()
}
}
}
最後添加權限:
<uses-permission android:name="android.permission.READ_CONTACTS" />
8.4.創建自己的ContentProvider
上一節講了獲得程序的內容URI後,使用ContentResolver來進行CRUD。本節我們將介紹給外界提供此接口?
8.4.1.創建ContentProvider的步驟
新建MyProvider類,代碼示例如下:
package com.example.myapplication
import android.content.ContentProvider
import android.content.ContentValues
import android.content.UriMatcher
import android.database.Cursor
import android.net.Uri
/**
* 一個標準的URI寫法爲:content://com.example.app.provider/table/1,期望訪問的是com.example.app這個應用中tale表的id的1的數據
*/
class MyProvider : ContentProvider() {
private val table1Dir = 0
private val table1Item = 1
private val table2Dir = 2
private val table2Item = 3
//藉助URIMatch可以輕鬆實現匹配內容URI的功能
private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
init {
//URIMatch提供了addURI方法,接受三個參數:可以分別把authority(應用程序包名.provider)、path和一個自定義代碼傳進去
//這樣調用uriMath.match方法可以將URI對象解析爲自定義代碼,這樣可以得到用戶期望調到那張表
//將期望匹配的內容URI格式傳遞進去,這裏是傳入的路徑參數是可以使用通配符的。
uriMatcher.addURI("com.example.app.provider", "table1", table1Dir)
uriMatcher.addURI("com.example.app.provider", "table1/#", table1Item)
uriMatcher.addURI("com.example.app.provider", "table2", table2Dir)
uriMatcher.addURI("com.example.app.provider", "table2/#", table2Item)
}
//初始化ContentProvider。完成數據庫的初始化和升級等操作,返回true表示成功,否則失敗
override fun onCreate(): Boolean {
TODO("Not yet implemented")
}
//向ContentProvider添加數據。uri用於確定要添加到的表,待添加的數據保存values參數中,最後返回一個用於表示新記錄的URI
override fun insert(uri: Uri, values: ContentValues?): Uri? {
TODO("Not yet implemented")
}
//從ContentProvider查詢數據,uri確定要添加那張表,projectoin確定查詢那些列,selcetion和selectionArgs用於約束查詢那些行,返回結果以cursor對象返回
override fun query(
uri: Uri,
projectoin: Array<String>?,
selcetion: String?,
selectionArgs: Array<String>?,
sortorder: String?
): Cursor? {
//判斷調用方期望訪問的是那張表
when (uriMatcher.match(uri)) {
//訪問table1表的所有數據
table1Dir -> {
}
//訪問table1表的任意一行的數據內容
table1Item -> {
}
table2Dir -> {
}
table2Item -> {
}
}
return null
}
//更新ContentProvider中已有的數據。uri表示哪一張表,新數據保存在values中,selcetion和selcetionArgs用於約束那些行。
override fun update(
uri: Uri,
values: ContentValues?,
selcetion: String?,
selcetionArgs: Array<String>?
): Int {
TODO("Not yet implemented")
}
//刪除ContentProvider的數據
override fun delete(p0: Uri, p1: String?, p2: Array<String>?): Int {
TODO("Not yet implemented")
}
//根據傳入的內容URI來返回相應的MIME類型,MIME類型主要由三部分組成:
//1.必須以vnd開頭;2.如果內容URI以路徑結尾,則後接android.cursor.dir/;如果內容URI以ID結尾,則後接android.cursor.item/;3.最後街上vnd.<authority>.<path>
override fun getType(uri: Uri): String? {
when(uriMatcher.match(uri)){
//內容URI爲content://com.example.app.provider.table1
table1Dir ->{return "vnd.android.cursor.dir/vnd.com.example.app.provider.table1"}
//內容URI爲content://com.example.app.provider.table1/1
table1Item ->{return "vnd.android.cursor.item/vnd.com.example.app.provider.table1"}
table2Dir ->{return "vnd.android.cursor.dir/vnd.com.example.app.provider.table2"}
table2Item ->{return "vnd.android.cursor.item/vnd.com.example.app.provider.table2"}
else ->{return null}
}
}
}
8.4.2.實現跨程序數據共享
打開DatabaseTest項目,新建類名爲DatabaseProvider、URI爲com.eample.myapplication.provider的ContentProvider。修改其中的代碼提供外部程序訪問DatabaseTest項目的ContentProvideer的入口:
package com.example.myapplication
import android.content.ContentProvider
import android.content.ContentValues
import android.content.UriMatcher
import android.database.Cursor
import android.net.Uri
/**
* 需要在XML中進行註冊,已經自動註冊過了,註冊代碼如下:
* <provider
android:name=".DatabaseProvider"
android:authorities="com.example.myapplication.provider"
android:enabled="true"
android:exported="true"></provider>
*/
class DatabaseProvider : ContentProvider() {
//訪問Book表的所有數據、單條數據;訪問Category表的所有數據、單條數據
private val bookDir = 0
private val bookItem = 1
private val categoryDir = 2
private val categoryItem = 3
private val authority = "com.example.myapplication.provider"
private var dbhelper: MyDatabaseHelper? = null
//by lazy代碼塊中完成UriMatcher的初始化操作,將希望匹配的幾種URI添加進去,by lazy是Kotlin提供的懶加載技術。
//即代碼一開始並不會執行,只有當uriMatcher變量首次被調用時候纔會執行
private val uriMatcher by lazy {
val matcher = UriMatcher(UriMatcher.NO_MATCH)
matcher.addURI(authority, "book", bookDir)
matcher.addURI(authority, "book/#", bookItem)
matcher.addURI(authority, "category", categoryDir)
matcher.addURI(authority, "category/#", categoryItem)
//將返回值賦給uriMatcher
matcher
}
//使用了語法糖、?.操作符、let函數以及?:操作符,以及單行函數語法糖
//首先調用getContext方法並藉助?.操作符判斷返回值爲空,若爲空則用?:返回false,若爲false表明失敗。若爲true執行let函數。
override fun onCreate(): Boolean = context?.let {
//創建MyDataBaseHelper實例
dbhelper = MyDatabaseHelper(it, "BookStore.db", 2)
//ContentProvider初始化成功
true
} ?: false
override fun query(
uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?
): Cursor? = dbhelper?.let {
//獲取SQLiteDataBase的實例
val db = it.readableDatabase
//根據傳入的uri選擇訪問哪張表,再調用query進行查詢,最後將cursor對象返回即可
val cursor = when (uriMatcher.match(uri)) {
bookDir -> db.query("Book", projection, selection, selectionArgs, null, null, sortOrder)
bookItem -> {
val bookId = uri.pathSegments[1]
db.query("Book", projection, "id=?", arrayOf(bookId), null, null, sortOrder)
}
categoryDir -> db.query(
"Category",
projection,
selection,
selectionArgs,
null,
null,
sortOrder
)
categoryItem -> {
//訪問單條數據,將URI權限的部分以/進行分割,將分割後的結果放入一個字符串列表中,第0個位置是路徑,第1個位置是id。
val categoryId = uri.pathSegments[1]
db.query("Category", projection, "id=?", arrayOf(categoryId), null, null, sortOrder)
}
else -> null
}
cursor
}
override fun insert(uri: Uri, values: ContentValues?): Uri? = dbhelper?.let {
val db = it.writableDatabase
val uriReturn = when (uriMatcher.match(uri)) {
bookDir, bookItem -> {
val newBookId = db.insert("Book", null, values)
Uri.parse("content://$authority/book/$newBookId")
}
categoryDir, categoryItem -> {
val newCategoryId = db.insert("Category", null, values)
Uri.parse("content://$authority/book/$newCategoryId")
}
else -> null
}
uriReturn
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int =
dbhelper?.let {
val db = it.writableDatabase
val deleteRows = when (uriMatcher.match(uri)) {
bookDir -> db.delete("Book", selection, selectionArgs)
bookItem -> {
val bookId = uri.pathSegments[1]
db.delete("Book", "id=?", arrayOf(bookId))
}
categoryDir -> db.delete("Category", selection, selectionArgs)
categoryItem -> {
val categoryId = uri.pathSegments[1]
db.delete("Category", "id=?", arrayOf(categoryId))
}
else -> 0
}
deleteRows
} ?: 0
override fun getType(uri: Uri): String? = when (uriMatcher.match(uri)) {
bookDir -> "vnd.android.cursor.dir/vnd.com.example.myapplication.provider.book"
bookItem -> "vnd.android.cursor.item/vnd.com.example.myapplication.provider.book"
categoryDir -> "vnd.android.cursor.dir/vnd.com.example.myapplication.provider.category"
categoryItem -> "vnd.android.cursor.item/vnd.com.example.myapplication.provider.category"
else -> null
}
override fun update(
uri: Uri, values: ContentValues?, selection: String?,
selectionArgs: Array<String>?
): Int = dbhelper?.let {
val db = it.writableDatabase
val updateRows = when (uriMatcher.match(uri)) {
bookDir -> db.update("Book", values, selection, selectionArgs)
bookItem -> {
val bookId = uri.pathSegments[1]
db.update("Book", values, "id=?", arrayOf(bookId))
}
categoryDir -> db.update("Category", values, selection, selectionArgs)
categoryItem -> {
val categotyId = uri.pathSegments[1]
db.update("Category", values, "id=?", arrayOf(categotyId))
}
else -> 0
}
updateRows
} ?: 0
}
新建項目ProviderTest,修改佈局文件:
<?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:orientation="vertical">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/addData"
android:text="Add To Book"
/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/queryData"
android:text="Query from Book"
/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/updateData"
android:text="Update Book"
/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/deleteData"
android:text="Delete from Book"
/>
</LinearLayout>
修改MainActivity.java,這樣可以獲取DatabaseTest向外界提供的ContentProvider接口,ContentProvider本質上也是基於Database的。
package com.example.myapplication1
import android.content.UriMatcher
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.core.content.contentValuesOf
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
var bookId: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
addData.setOnClickListener {
//將內容URI解析爲Uri對象,將要添加的數據存放至ContentValues中
val uri = Uri.parse("content://com.example.myapplication.provider/book")
val values = contentValuesOf(
"name" to "A Clash of Kings",
"author" to "George",
"pages" to 1040,
"prices" to 22.85
)
//執行insert方法,返回一個uri對象,通過getpathSegments將這個id取出。
val newuri = contentResolver.insert(uri, values)
bookId = newuri?.pathSegments?.get(1)
}
queryData.setOnClickListener {
val uri = Uri.parse("content://com.example.myapplication.provider/book")
contentResolver.query(uri, null, null, null, null)?.apply {
//遍歷cursor裏面的所有內容
while (moveToNext()) {
val name = getString(getColumnIndex("name"))
val author = getString(getColumnIndex("author"))
val pages = getInt(getColumnIndex("pages"))
val prices = getDouble(getColumnIndex("prices"))
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 prices is $prices")
}
close()
}
}
updateData.setOnClickListener {
bookId?.let {
//隻影響添加數據返回的id。
val uri = Uri.parse("content://com.example.myapplication.provider/book/$it")
val values = contentValuesOf(
"name" to "A Storm of Swords",
"pages" to 1216,
"prices" to 24.05
)
contentResolver.update(uri, values, null, null)
}
}
deleteData.setOnClickListener {
bookId?.let {
val uri = Uri.parse("content://com.example.myapplication.provider/book/$it")
contentResolver.delete(uri, null, null)
}
}
}
}
8.5.泛型與委託
8.5.1.泛型的基本用法
Kotlin泛型與Java泛型有同有異,泛型允許我們在不指定具體類型的情況下進行編程。譬如:List是可存放數據的列表,但並未限制只能存整形或字符型數組。那麼如何定義自己的泛型實現呢?
泛型有兩種定義方式,定義泛型類和定義泛型方法,使用的語法結構都是<T>,事實上任何字母或單詞都行。如何定義一個泛型類和泛型方法,那麼:
package com.company
/**
* 定義一個泛型類Myclass,Myclass方法允許使用T類型的參數和返回值
*/
//class Myclass<T> {
// fun method(param: T): T {
// return param
// }
//}
/**
* 定義一個泛型方法,將定義泛型的語法結構放在方法前
*/
//class Myclass {
// fun <T> method(param: T): T {
// return param
// }
//}
/**
* 我們設置可以將泛型指定爲任意類型,指定爲Number類型可以爲Int、Double和Float。泛型的的上界默認爲Any?。
*/
class Myclass {
fun <T:Number> method(param: T): T {
return param
}
}
調用該泛型方法的代碼如下:
package com.company
fun main() {
//在這裏Myclass類的泛型可以指定爲Int類型,於是method可以接受一個Int類型的參數,而且返回值也爲Int類型
// val myclass = Myclass<Int>()
// val result = myclass.method(123)
// println("result is $result")
val myclss = Myclass()
val result = myclss.method<Int>(123)
//因爲有着類型推導機制,你可以省略int型
val result1 = myclss.method(123)
}
學廢了麼?看看6.5.1小節所述的高階函數。
//爲StringBuilder類定義一個build擴展函數,此函數接收函數類型參數,返回值類型爲StringBuilder。
//函數聲明與之前不一樣的是加上了一個StringBuilder.語法結構,這是高階函數完整語法規則,在函數類型前加上ClassName
//表明這是在哪一個類當中的。這樣做的好處是調用該方法時自動擁有StringBuilder的上下文。
fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder {
block()
return this
}
可以利用泛型函數簡化爲如下所示,隨後可以將contentResolver的apply函數改爲build函數:
fun <T> T.build(block: T.() -> Unit): T {
block()
return this
}
8.5.2.類委託和委託屬性
委託是設計模式,即操作對象自己不會處理某段邏輯,而是會把工作爲委託給另外一個扶助對象去處理,Java語言級別沒有委託。Kotlin支持委託:類委託和委託屬性。
類委託的核心思想是將一個類的具體實現委託給另一個類去完成。譬如Set接口,要使用它,必須藉助具體實現類譬如HashSet。藉助委託模式,可以輕鬆實現Set接口:
package com.company
//這是一種委託模式,MySet的構造函數中接受一個HashSet參數,相當於一個輔助對象,然後再Set接口的方法實現中,我們都調用了輔助對象中相應的方法實現
//好處是:大部分調用輔助對象方法,少部分自己重寫,MySet會成爲全新數據結構類。
class MySet<T>(val helperSet: HashSet<T>) : Set<T> {
override val size: Int
get() = helperSet.size
override fun contains(element: T): Boolean = helperSet.contains(element)
override fun containsAll(elements: Collection<T>): Boolean = helperSet.containsAll(elements)
override fun isEmpty(): Boolean = helperSet.isEmpty()
override fun iterator(): Iterator<T> = helperSet.iterator()
}
假設接口的實現方法有成百上千,那一個一個寫不累死,在Kotlin中委託使用的關鍵字是by,只需在接口聲明的後面使用by關鍵字,再加上委託的輔助對象對象,可以免去一大堆模板代碼:
class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet{
//如果需要對某個方法重新實現,單獨重寫某個方法即可,其他set接口裏的功能,與HashSet保持一致。
fun helloworld() = println("hello,world")
override fun isEmpty(): Boolean = false
}
委託屬性的本質是將一個屬性(字段)的具體實現委託給另一個類去完成:
/**
* 委託屬性的句法結構如下,by關鍵字鏈接了左邊的p屬性和右邊的Delegate實例,將p屬性的具體實現委託給了Delegate類去完成
* 當調用p屬性時回信自動調用Delegate的getValue方法,賦值會調用setValue方法
*/
class Myclass {
//如果聲明爲val則不用在Delegate中創建setValue方法。
var p by Delegate()
}
//標準代碼實現模板,必須實現getValue和setValue方法,並且使用operator關鍵字進行聲明
class Delegate {
var propValue: Any? = null
//第一個參數聲明該Delegate類的委託功能可在什麼類中使用,第二個參數是Kotlin的屬性操作類,用於獲取屬性相關的值,目前用不到
//<*>爲你不知道或者不關注泛型的具體類型
operator fun getValue(myclass: Myclass, prop: KProperty<*>): Any? {
return propValue
}
operator fun setValue(myclass: Myclass, prop: KProperty<*>, value: Any?) {
propValue = value
}
}
8.5.3.實現一個自己的lazy函數
懶加載技術是將延遲執行的代碼放到by lazy代碼塊中,一開始不會執行,當調用到相應變量時,代碼塊中的代碼纔會執行。基本語法結構如下val p by lazy{...},by是關鍵字,lazy是高階函數。實現一個自己的lazy函數代碼如下:
package com.company
import kotlin.reflect.KProperty
//頂層函數:創建Later類的實例,並將節後首的函數類型參數傳給Later類的構造函數。這樣可以替代之前的lazy函數
fun <T> later(block: () -> T) = Later(block)
//定義Later類將其指定爲泛型類,接受一個函數類型參數,此函數類型參數不接受任何參數。
class Later<T>(val block: () -> T) {
var value: Any? = null
//第一個參數爲任意類型,希望Later的委託功能在所有類中都能夠使用,使用一個value值進行緩存,若空,就調用構造函數中傳入的函數類型參數去獲取值,否則返回。
operator fun getValue(any: Any?, prop: KProperty<*>): T {
if (value == null) {
value = block()
}
return value as T
}
//懶加載技術不會對屬性進行賦值,不用實現setValue方法。
}