《第一行代碼》第三版之探究ContentProvider(九)

        本章我們介紹了跨程序共享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方法。
}
   

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章