《第一行代码》第三版之探究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方法。
}
   

 

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