十二、后端和应用编程接口

在本章中,我们将把我们的应用连接到远程后端实例。我们创建的所有数据将同步到后端和从后端。对于应用编程接口调用,我们将使用改装。改装是安卓平台最常用的 HTTP 客户端。我们将一步一步地指导您完成常见的实践,以便您可以在将来开发的任何应用中轻松地连接并实现后端连接。

到目前为止,这一章是本书最长的一章,在这里,我们将涵盖许多重要的事情,例如以下主题:

  • 使用数据类
  • 加装
  • 葛兰素史克与科森图书馆
  • 内容提供商
  • 内容加载器
  • 安卓适配器
  • 数据绑定
  • 使用列表和网格

仔细阅读这一章,享受玩你的应用。

确定使用的实体

在我们同步任何东西之前,我们必须准确地确定我们将同步什么。这个问题的答案是显而易见的,但无论如何,我们将概述我们的实体列表。我们计划同步两个主要实体:

  • Note实体
  • Todo实体

它们具有以下属性:

  • 常见属性:
    • title : String
    • message : String
    • location : Location(将连载)

Note that, currently, we represent the location with latitude and longitude in our database. We will change this to Text type since we will introduce Gson and Kotson for serialization/deserialization purposes!

  • Todo 特定属性如下:
    • scheduledFor : Long

再一次,打开你的课,看一看。

使用数据类

在 Kotlin 中,建议使用data类作为实体的表示。在我们的例子中,我们没有使用data类,因为我们扩展了一个包含NoteTodo类之间共享的属性的公共类。

我们建议使用data类,因为它可以显著简化您的工作例程,尤其是当您在后端通信中使用这些实体时。

我们对只有一个目的——保存数据——的类的需求并不罕见。使用data类的好处是自动提供一些经常与其目的一起使用的功能。因为您可能已经知道如何定义data类,所以您必须这样做:

   data class Entity(val param1: String, val param2: String) 

对于data类,编译器会自动为您提供以下内容:

  • equals()hashCode()方法
  • 人类可读形式的toString()方法, Entity(param1=Something, param2=Something)
  • 克隆的方法

所有data类必须满足以下条件:

  • 主构造函数需要至少有一个参数
  • 所有主构造器参数需要标记为valvar
  • 数据类不能是abstractopensealedinner

来介绍几节data课吧!因为我们计划使用远程后端实例,所以它需要一些身份验证。我们将为身份验证过程中通过的数据以及身份验证的结果创建新的实体(data类)。创建一个名为api的新包。然后,新建一个名为UserLoginRequestdata类,如下所示:

     package com.journaler.api 

     data class UserLoginRequest( 
        val username: String, 
        val password: String 
     )  

UserLoginRequest类将包含我们的认证凭证。该应用编程接口调用将返回一个 JSON,该 JSON 将被反序列化为JournalerApiToken数据类,如下所示:

    package com.journaler.api 
    import com.google.gson.annotations.SerializedName 

    data class JournalerApiToken( 
        @SerializedName("id_token") val token: String, 
        val expires: Long 
    ) 

请注意,我们使用注释告诉 Gson,令牌字段将从 JSON 中的id_token字段获得。

To summarize--Always consider the use of data classes! Especially if the data they represent will be used for holding database and backend information.

将数据模型连接到数据库

如果您有一个像我们在 Journaler 应用中那样的场景来保存数据库中的数据,并且您计划将其与远程后端实例同步,那么首先创建一个持久层来存储您的数据可能是一个好主意。将数据持久保存到本地文件系统数据库中可以防止数据丢失,尤其是当数据量较大时!

那么,我们又做了什么?我们创建了一个持久性机制,将我们所有的数据存储到 SQLite 数据库中。然后,在本章中,我们将介绍后端通信机制。因为我们不知道我们的 API 调用是否会失败,也不知道后端实例是否可用,所以我们保留了数据。如果我们只将数据保存在设备内存中,并且如果用于同步的 API 调用失败并且我们的应用崩溃,我们可能会丢失这些数据。假设应用编程接口调用失败,应用崩溃,但是我们的数据保持不变,我们可以重试同步。数据还在!

改装介绍

正如我们已经提到的,改装是一个开源库。它是目前安卓最流行的 HTTP 客户端。因此,我们将向您介绍改装基础知识,并演示如何使用它。我们将介绍的版本是 2.3.0。我们将逐步指导您如何使用它。

首先,改装也依赖于一些库。我们将把它和 Okhttp 一起使用。Okhttp 是一个 HTTP/HTTP2 客户端,是由开发改装的人开发的。在开始之前,我们将把依赖项放入我们的build.gradle配置中,如下所示:

    apply plugin: "com.android.application" 
    apply plugin: "kotlin-android" 
    apply plugin: "kotlin-android-extensions" 
    ... 
    dependencies { 
      ... 
      compile 'com.squareup.retrofit2:retrofit:2.3.0' 
      compile 'com.squareup.retrofit2:converter-gson:2.0.2' 
      compile 'com.squareup.okhttp3:okhttp:3.9.0' 
      compile 'com.squareup.okhttp3:logging-interceptor:3.9.0' 
   } 

我们更新了我们的改装和 Okhttp 到最新版本。我们添加了以下依赖项:

  • 改造图书馆
  • 将用于反序列化 API 响应的 Gson 转换器
  • Okhttp 库
  • Okhttp 的日志拦截器,这样我们就可以记录我们的 API 调用发生了什么

同步完我们的 Gradle 配置后,我们就可以开始了!

定义改装服务

改装把你的 HTTP 应用编程接口变成了一个 Kotlin 接口。在 API 包内部创建一个名为JournalerBackendService的接口。让我们在里面放一些代码:

    package com.journaler.api 

    import com.journaler.model.Note 
    import com.journaler.model.Todo 
    import retrofit2.Call 
    import retrofit2.http.* 

    interface JournalerBackendService { 

      @POST("user/authenticate") 
      fun login( 
            @HeaderMap headers: Map<String, String>, 
            @Body payload: UserLoginRequest 
      ): Call<JournalerApiToken> 

      @GET("entity/note") 
      fun getNotes( 
            @HeaderMap headers: Map<String, String> 
      ): Call<List<Note>> 

      @GET("entity/todo") 
      fun getTodos( 
            @HeaderMap headers: Map<String, String> 
      ): Call<List<Todo>> 

      @PUT("entity/note") 
      fun publishNotes( 
            @HeaderMap headers: Map<String, String>, 
            @Body payload: List<Note> 
      ): Call<Unit> 

      @PUT("entity/todo") 
      fun publishTodos( 
            @HeaderMap headers: Map<String, String>, 
            @Body payload: List<Todo> 
      ): Call<Unit> 

      @DELETE("entity/note") 
      fun removeNotes( 
            @HeaderMap headers: Map<String, String>, 
            @Body payload: List<Note> 
      ): Call<Unit> 

      @DELETE("entity/todo") 
      fun removeTodos( 
            @HeaderMap headers: Map<String, String>, 
            @Body payload: List<Todo> 
      ): Call<Unit> 

    } 

这个界面有什么?我们定义了一个能够执行以下操作的调用列表:

  • 用户身份验证:这接受包含用户凭证的UserLoginRequest类的请求头和实例。它将被用作我们通话的有效载荷。执行调用将返回一个包装的JournalerApiToken实例。我们将需要一个用于所有其他呼叫的令牌,并且我们将把它的内容放入每个呼叫的报头中。
  • NotesTODOs获取:这也接受包含认证令牌的请求头。调用的结果是,我们得到了一个包装好的NoteTodo类实例列表。
  • NotesTODOs放置(当我们向服务器发送新内容时):这也接受包含身份验证令牌的请求头。呼叫的有效载荷将是一个NoteTodo类实例的列表。我们不会返回这些电话的任何重要数据。重要的是响应代码是肯定的。
  • NotesTODOs移除-这也接受包含身份验证令牌的请求头。该调用的负载将是一个从我们的远程后端服务器实例中删除的NoteTodo类实例的列表。我们不会返回这些电话的任何重要数据。重要的是响应代码是肯定的。

每个都有一个适当的注释,用路径表示 HTTP 方法。我们还使用注释来标记负载主体和头部映射。

构建改造服务实例

现在,在我们描述了我们的服务之后,我们需要一个真正的改装实例,我们将使用它来触发应用编程接口调用。首先,我们将介绍一些额外的类。我们将在TokenManager对象中保存最新的令牌实例:

    package com.journaler.api 
     object TokenManager { 
       var currentToken = JournalerApiToken("", -1) 
     } 

我们还将有一个对象来获取名为BackendServiceHeaderMap的 API 调用头映射,如下所示:

    package com.journaler.api 

    object BackendServiceHeaderMap { 

     fun obtain(authorization: Boolean = false): Map<String, String> { 
        val map = mutableMapOf( 
                Pair("Accept", "*/*"), 
                Pair("Content-Type", "application/json; charset=UTF-8") 
        ) 
        if (authorization) { 
            map["Authorization"] = "Bearer
             ${TokenManager.currentToken.token}" 
        } 
        return map 
    } 

   } 

现在我们可以向您展示如何构建一个Retrofit实例。创建一个名为BackendServiceRetrofit的新对象,并确保它如下所示:

    package com.journaler.api 

    import okhttp3.OkHttpClient 
    import okhttp3.logging.HttpLoggingInterceptor 
    import retrofit2.Retrofit 
    import retrofit2.converter.gson.GsonConverterFactory 
    import java.util.concurrent.TimeUnit 

    object BackendServiceRetrofit { 

      fun obtain( 
            readTimeoutInSeconds: Long = 1, 
            connectTimeoutInSeconds: Long = 1 
      ): Retrofit { 
        val loggingInterceptor = HttpLoggingInterceptor() 
        loggingInterceptor.level 
      = HttpLoggingInterceptor.Level.BODY 
        return Retrofit.Builder() 
                .baseUrl("http://127.0.0.1") 
                .addConverterFactory(GsonConverterFactory.create()) 
                .client( 
                        OkHttpClient 
                                .Builder() 
                                .addInterceptor(loggingInterceptor) 
                                .readTimeout(readTimeoutInSeconds,
                                TimeUnit.SECONDS) 
                                  .connectTimeout
                                (connectTimeoutInSeconds,
                                TimeUnit.SECONDS) 
                                .build() 
                ) 
                .build() 
     } 

    } 

调用obtain()方法将为我们返回一个准备好触发 API 调用的Retrofit实例。我们创建了一个Retrofit实例,后端基本网址设置为本地主机。我们还传递了 Gson 转换器工厂,用作 JSON 反序列化的机制。最重要的是,我们传递了一个将要使用的客户端实例,并创建了一个新的 OkHttp 客户端。

用 Kotson 库介绍 Gson

JSON 序列化和反序列化对于每个 Android 应用都非常重要,并且经常使用。为此,我们将使用谷歌开发的 Gson 库。此外,我们将为 Gson 使用 Kotson 和 Kotlin 绑定。所以,让我们开始吧!

首先,我们需要为我们的build.gradle配置提供依赖关系,如下所示:

    apply plugin: "com.android.application" 
    apply plugin: "kotlin-android" 
    apply plugin: "kotlin-android-extensions" 
    ... 
    dependencies { 
      ... 
      compile 'com.google.code.gson:gson:2.8.0' 
      compile 'com.github.salomonbrys.kotson:kotson:2.3.0' 
      ... 
    } 

我们将更新代码,将 Gson 与 Kotson 绑定一起用于数据库管理中的位置序列化/反序列化。首先,我们需要对Db类进行一个小改动:

    class DbHelper(dbName: String, val version: Int) :
    SQLiteOpenHelper( 
      Journaler.ctx, dbName, null, version 
    ) { 

    companion object { 
        val ID: String = "_id" 
        val TABLE_TODOS = "todos" 
        val TABLE_NOTES = "notes" 
        val COLUMN_TITLE: String = "title" 
        val COLUMN_MESSAGE: String = "message" 
        val COLUMN_LOCATION: String = "location" 
        val COLUMN_SCHEDULED: String = "scheduled" 
    } 
    ... 
    private val createTableNotes =  """ 
                                    CREATE TABLE if not exists
                                     $TABLE_NOTES 
                                    ( 
                                        $ID integer PRIMARY KEY
                                        autoincrement, 
                                        $COLUMN_TITLE text, 
                                        $COLUMN_MESSAGE text, 
                                        $COLUMN_LOCATION text 
                                    ) 
                                    """ 

    private val createTableTodos =  """ 
                                    CREATE TABLE if not exists
                                     $TABLE_TODOS 
                                    ( 
                                        $ID integer PRIMARY KEY
                                         autoincrement, 
                                        $COLUMN_TITLE text, 
                                        $COLUMN_MESSAGE text, 
                                        $COLUMN_SCHEDULED integer, 
                                        $COLUMN_LOCATION text 
                                    ) 
                                    """ 
    ... 
   } 

如您所见,我们更改了位置信息处理。不再有位置纬度和经度列,我们现在只有一个数据库列- location。类型为Text。我们将保存由 Gson 库生成的序列化的Location类值。此外,当我们检索序列化的值时,我们将使用 Gson 将它们反序列化为Location类实例。

现在,我们必须实际使用 Gson。打开Db.kt并更新为序列化,使用 Gson 反序列化Location类实例,如下所示:

    package com.journaler.database 
    ... 
    import com.google.gson.Gson 
    ... 
    import com.github.salomonbrys.kotson.* 

    object Db : Crud<DbModel> { 
      ... 
      private val gson = Gson() 
      ... 
      override fun insert(what: Collection<DbModel>): Boolean { 
        ... 
        what.forEach { 
            item -> 
            when (item) { 
                is Entry -> { 
                    ... 
                    values.put(DbHelper.COLUMN_LOCATION,
                     gson.toJson(item.location)) 
                    ... 
            } 
        } 
        ... 
        return success 
    } 
    ... 
    override fun update(what: Collection<DbModel>): Boolean { 
        ... 
        what.forEach { 
            item -> 
            when (item) { 
                is Entry -> { 
                    ... 
                    values.put(DbHelper.COLUMN_LOCATION,
                    gson.toJson(item.location)) 
                } 
       ... 
        return result 
    } 
    ... 
    override fun select(args: Pair<String, String>, clazz:  
    KClass<DbModel>): List<DbModel> { 
        return select(listOf(args), clazz) 
    } 

    override fun select( 
        args: Collection<Pair<String, String>>, clazz: Kclass<DbModel> 
    ): List<DbModel> { 
        ... 
        if (clazz.simpleName == Note::class.simpleName) { 
            val result = mutableListOf<DbModel>() 
            val cursor = db.query( 
                ... 
            ) 
            while (cursor.moveToNext()) { 
                ... 
                val locationIdx =
                cursor.getColumnIndexOrThrow(DbHelper.COLUMN_LOCATION) 
                val locationJson = cursor.getString(locationIdx) 
                val location = gson.fromJson<Location>(locationJson) 
                val note = Note(title, message, location) 
                note.id = id 
                result.add(note) 
            } 
            cursor.close() 
            return result 
        } 
        if (clazz.simpleName == Todo::class.simpleName) { 
                ... 
            ) 
            while (cursor.moveToNext()) { 
                ... 
                val locationIdx =
                cursor.getColumnIndexOrThrow(DbHelper.COLUMN_LOCATION) 
                val locationJson = cursor.getString(locationIdx) 
                val location = gson.fromJson<Location>(locationJson) 
                ... 
                val todo = Todo(title, message, location, scheduledFor) 
                todo.id = id 
                result.add(todo) 
            } 
            cursor.close() 
            return result 
        } 
        db.close() 
        throw IllegalArgumentException("Unsupported entry type: 
        $clazz") 
      } 
   }  

如您所见,在前面的代码中,使用 Gson 进行更新非常简单。我们依赖于从 Gson 类实例访问的以下两个 Gson 库方法:

  • fromJson<T>()
  • toJson()

多亏了 Kotson 和 Kotlin 绑定,我们可以使用fromJson<T>()方法为序列化的数据使用参数化类型。

还有什么可用的?

现在,我们将列出改装和 Gson 的一些替代方案。在外面,有一个大的开源社区,每天都在做大事。您没有义务使用我们提供的任何库。您可以选择任何一个替代方案,甚至创建自己的实现!

改装替代方案

正如其主页所说,凌空是一个 HTTP 库,它让安卓应用的联网变得更容易,最重要的是,更快。凌空提供的一些关键功能如下:

  • 网络请求的自动调度
  • 多个并发网络连接
  • 具有标准 HTTP 缓存一致性的透明磁盘和内存响应缓存
  • 支持请求优先级。
  • 取消请求应用编程接口
  • 易于定制
  • 强排序
  • 调试和跟踪工具

主页-https://github . com/Google/full。

Gson 替代方案

Jackson 是一个低级 JSON 解析器。它非常类似于用于 XML 的 Java StAX 解析器。杰克逊提供的一些关键功能如下:

  • 非常快捷方便
  • 广泛的注释支持
  • 流式读写
  • 树模型
  • 开箱即用的 JAX-RS 支持
  • 对二进制内容的集成支持

主页--https://github . com/faster XML/Jackson。

执行我们的第一个应用编程接口调用

我们用所有的应用编程接口调用定义了一个改装服务,但是我们还没有连接到它。是时候使用它了。我们将扩展我们的代码来使用改装。每个 API 调用都可以同步或异步执行。我们会向你展示两种方式。您还记得我们将我们的改装服务基础网址设置为本地主机吗?这意味着我们需要一个本地后端实例来响应我们的 HTTP 请求。由于后端实现不是本书的主题,我们将让您创建一个简单的服务来响应这个请求。您可以从任何您喜欢的编程语言来实现它,例如 Kotlin、Java、Python 和 PHP。

如果您不耐烦,不想实现自己的应用来处理 HTTP 请求,您可以覆盖基本 URL、Notes 和 TODOs 路径,如下例所示,并使用准备试用的后端实例:

        @POST("authenticate") 
        // @POST("user/authenticate") 
        fun login( 
            ... 
        ): Call<JournalerApiToken> 
  • Notes GET对标的:
        @GET("notes") 
        // @GET("entity/note") 
        fun getNotes( 
            ... 
        ): Call<List<Note>> 
  • TODOs GET对标的:
        @GET("todos") 
        // @GET("entity/todo") 
       fun getTodos( 
            ... 
       ): Call<List<Todo>> 

像这样,我们将针对返回我们存根NotesTODOs的远程后端实例。现在打开JournalerBackendService界面,扩展如下:

    interface JournalerBackendService { 
      companion object { 
        fun obtain(): JournalerBackendService { 
            return BackendServiceRetrofit 
                    .obtain() 
                    .create(JournalerBackendService::class.java) 
        } 
     } 
      ... 
    } 

我们刚刚添加的方法将为我们提供一个使用改装的JournalerBackendService实例。通过这个,我们会触发我们所有的通话。开启MainService课。找到synchronize()的方法。请记住,我们将睡眠放在那里,以模拟与后端的通信。现在,我们将用真正的后端调用替换它:

    /** 
    * Authenticates user synchronously, 
    * then executes async calls for notes and TODOs fetching. 
    * Pay attention on synchronously triggered call via execute() 
      method. 
    * Its asynchronous equivalent is: enqueue(). 
    */ 
    override fun synchronize() { 
        executor.execute { 
            Log.i(tag, "Synchronizing data [ START ]") 
            var headers = BackendServiceHeaderMap.obtain() 
            val service = JournalerBackendService.obtain() 
            val credentials = UserLoginRequest("username", "password") 
            val tokenResponse = service 
                    .login(headers, credentials) 
                    .execute() 
            if (tokenResponse.isSuccessful) { 
                val token = tokenResponse.body() 
                token?.let { 
                    TokenManager.currentToken = token 
                    headers = BackendServiceHeaderMap.obtain(true) 
                    fetchNotes(service, headers) 
                    fetchTodos(service, headers) 
                } 
            } 
            Log.i(tag, "Synchronizing data [ END ]") 
        } 
    } 

    /** 
    * Fetches notes asynchronously. 
    * Pay attention on enqueue() method 
    */ 
    private fun fetchNotes( 
            service: JournalerBackendService, headers: Map<String,  
    String> 
    ) { 
        service 
            .getNotes(headers) 
            .enqueue( 
            object : Callback<List<Note>> { 
              verride fun onResponse( 
               call: Call<List<Note>>?, response: Response<List<Note>>? 
                            ) { 
                                response?.let { 
                                    if (response.isSuccessful) { 
                                        val notes = response.body() 
                                        notes?.let { 
                                            Db.insert(notes) 
                                        } 
                                    } 
                                } 
                            } 

                            override fun onFailure(call: 
                            Call<List<Note>>?, t: Throwable?) { 
                                Log.e(tag, "We couldn't fetch notes.") 
                            } 
                        } 
                ) 
     } 

     /** 
     * Fetches TODOs asynchronously. 
     * Pay attention on enqueue() method 
     */ 
     private fun fetchTodos( 
            service: JournalerBackendService, headers: Map<String,  
      String> 
     ) { 
        service 
                .getTodos(headers) 
                .enqueue( 
                        object : Callback<List<Todo>> { 
                            override fun onResponse( 
                                    call: Call<List<Todo>>?, response:
         Response<List<Todo>>? 
                            ) { 
                                response?.let { 
                                    if (response.isSuccessful) { 
                                        val todos = response.body() 
                                        todos?.let { 
                                            Db.insert(todos) 
                                        } 
                                    } 
                                } 
                            } 

                            override fun onFailure(call:
                            Call<List<Todo>>?, t: Throwable?) { 
                                Log.e(tag, "We couldn't fetch notes.") 
                            } 
                        } 
                 ) 
     } 

慢慢分析代码,慢慢来!有很多事情正在发生!首先,我们将创建头和日志后端服务的实例。然后,我们通过触发execute()方法同步进行认证。我们收到了Response<JournalerApiToken>JournalerApiToken实例被包装在Response类实例中。在我们检查响应是否成功,并且我们实际上接收并反序列化了JournalerApiToken之后,我们将其设置为TokenManager。最后,我们触发NotesTODOs检索的异步调用。

enqueue()方法触发异步操作,并作为参数接受改装回调具体化。我们将执行与同步调用相同的操作。我们会检查是否成功,是否有数据。如果一切正常,我们会将所有实例传递给数据库管理器进行保存。

我们只实现了NotesTODOs检索。对于其余的 API 调用,我们让您来完成实现。这是学习改装的好方法!

让我们为您构建一个应用并运行它。当一个应用及其主服务启动时,API 调用被执行。通过 OkHttp 过滤 Logcat 输出。请注意以下几点。

身份验证日志行:

  • 请求:
 D/OkHttp: --> POST 
 http://static.milosvasic.net/jsons/journaler/authenticate 
        D/OkHttp: Content-Type: application/json; charset=UTF-8 
        D/OkHttp: Content-Length: 45 
        D/OkHttp: Accept: */* 
        D/OkHttp: {"password":"password","username":"username"} 
        D/OkHttp: --> END POST (45-byte body) 
  • 回应:
 D/OkHttp: <-- 200 OK 
 http://static.milosvasic.net/jsons/journaler/
 authenticate/ (302ms) 
       D/OkHttp: Date: Sat, 23 Sep 2017 15:46:27 GMT 
       D/OkHttp: Server: Apache 
       D/OkHttp: Keep-Alive: timeout=5, max=99 
       D/OkHttp: Connection: Keep-Alive 
       D/OkHttp: Transfer-Encoding: chunked 
       D/OkHttp: Content-Type: text/html 
       D/OkHttp: { 
       D/OkHttp:   "id_token": "stub_token_1234567", 
       D/OkHttp:   "expires": 10000 
       D/OkHttp: } 
       D/OkHttp: <-- END HTTP (58-byte body) 

Notes日志行:

  • 请求:
 D/OkHttp: --> GET 
 http://static.milosvasic.net/jsons/journaler/notes 
        D/OkHttp: Accept: */* 
        D/OkHttp: Authorization: Bearer stub_token_1234567 
        D/OkHttp: --> END GET 
  • 回应:
 D/OkHttp: <-- 200 OK 
 http://static.milosvasic.net/jsons/journaler/notes/ (95ms) 
        D/OkHttp: Date: Sat, 23 Sep 2017 15:46:28 GMT 
        D/OkHttp: Server: Apache 
        D/OkHttp: Keep-Alive: timeout=5, max=97 
        D/OkHttp: Connection: Keep-Alive 
        D/OkHttp: Transfer-Encoding: chunked 
        D/OkHttp: Content-Type: text/html 
        D/OkHttp: [ 
        D/OkHttp:   { 
        D/OkHttp:     "title": "Test note 1", 
        D/OkHttp:     "message": "Test message 1", 
        D/OkHttp:     "location": { 
        D/OkHttp:       "latitude": 10000, 
        D/OkHttp:       "longitude": 10000 
        D/OkHttp:     } 
        D/OkHttp:   }, 
        D/OkHttp:   { 
        D/OkHttp:     "title": "Test note 2", 
        D/OkHttp:     "message": "Test message 2", 
        D/OkHttp:     "location": { 
        D/OkHttp:       "latitude": 10000, 
        D/OkHttp:       "longitude": 10000 
        D/OkHttp:     } 
        D/OkHttp:   }, 
        D/OkHttp:   { 
        D/OkHttp:     "title": "Test note 3", 
        D/OkHttp:     "message": "Test message 3", 
        D/OkHttp:     "location": { 
        D/OkHttp:       "latitude": 10000, 
        D/OkHttp:       "longitude": 10000 
        D/OkHttp:     } 
        D/OkHttp:   } 
        D/OkHttp: ] 
        D/OkHttp: <-- END HTTP (434-byte body) 

TODOs日志行:

  • 请求:这是我们做的请求部分的一个例子:
 D/OkHttp: --> GET
 http://static.milosvasic.net/jsons/journaler/todos 
        D/OkHttp: Accept: */* 
        D/OkHttp: Authorization: Bearer stub_token_1234567 
        D/OkHttp: --> END GET 
  • 回应:这是我们收到的回应示例:
 D/OkHttp: <-- 200 OK
 http://static.milosvasic.net/jsons/journaler/todos/ (140ms) 
       D/OkHttp: Date: Sat, 23 Sep 2017 15:46:28 GMT 
       D/OkHttp: Server: Apache 
       D/OkHttp: Keep-Alive: timeout=5, max=99 
       D/OkHttp: Connection: Keep-Alive 
       D/OkHttp: Transfer-Encoding: chunked 
       D/OkHttp: Content-Type: text/html 
       D/OkHttp: [ 
       D/OkHttp:   { 
       D/OkHttp:     "title": "Test todo 1", 
       D/OkHttp:     "message": "Test message 1", 
       D/OkHttp:     "location": { 
       D/OkHttp:       "latitude": 10000, 
       D/OkHttp:       "longitude": 10000 
       D/OkHttp:     }, 
       D/OkHttp:     "scheduledFor": 10000 
       D/OkHttp:   }, 
       D/OkHttp:   { 
       D/OkHttp:     "title": "Test todo 2", 
       D/OkHttp:     "message": "Test message 2", 
       D/OkHttp:     "location": { 
       D/OkHttp:       "latitude": 10000, 
       D/OkHttp:       "longitude": 10000 
       D/OkHttp:     }, 
       D/OkHttp:     "scheduledFor": 10000 
       D/OkHttp:   }, 
       D/OkHttp:   { 
       D/OkHttp:     "title": "Test todo 3", 
       D/OkHttp:     "message": "Test message 3", 
       D/OkHttp:     "location": { 
       D/OkHttp:       "latitude": 10000, 
       D/OkHttp:       "longitude": 10000 
       D/OkHttp:     }, 
       D/OkHttp:     "scheduledFor": 10000 
       D/OkHttp:   } 
       D/OkHttp: ] 
       D/OkHttp: <-- END HTTP (515-byte body) 

恭喜你!您已经实施了第一项改装服务!现在是时候实现剩下的调用了。还有,做一些代码重构!这是给你的一个小作业。更新您的服务,以便它可以接受登录凭据。在我们当前的代码中,我们对用户名和密码进行了硬编码。您的任务将是重构代码并传递参数化的凭据。

或者,改进代码,以便不再可能在同一时刻多次执行同一个调用。我们把这个作为我们以前工作的遗产。

内容提供商

是时候进一步改进我们的应用,并向您介绍安卓内容提供商了。内容提供商是安卓框架必须提供的顶级功能之一。内容提供商的目的是什么?顾名思义,内容提供商的目的是管理对由我们的应用存储或由其他应用存储的数据的访问。它们提供了一种与其他应用共享数据的机制,并为数据访问提供了一种安全机制,这些数据可能来自也可能不来自同一个进程。

请看下图,显示内容提供商如何管理对共享存储的访问:

我们计划与其他应用共享NotesTODOs数据。得益于内容提供商提供的抽象层,在不影响上层的情况下,很容易在存储实现层进行更改。因此,即使您不打算与其他应用共享任何数据,也可以使用内容提供商。例如,我们可以将 SQLite 中的持久性机制完全替换为完全不同的东西。请看下图:

如果您不确定是否需要内容提供商,以下是您应该实现它的时间:

  • 如果您计划与其他应用共享应用的数据
  • 如果您计划将复杂的数据或文件从您的应用复制并粘贴到其他应用
  • 如果您计划支持自定义搜索建议

安卓框架附带了一个已经定义的内容提供商,您可以使用;例如,管理联系人、音频、视频或其他文件。内容提供商不仅限于 SQLite 访问,您还可以将其用于其他结构化数据。

让我们再次强调主要优势:

  • 访问数据的权限
  • 抽象数据层

因此,正如我们已经说过的,我们计划支持 Journaler 应用的数据暴露。在我们创建内容提供者之前,我们必须注意到这将需要重构当前的代码。别担心,我们会展示内容提供商,向您解释它以及我们所做的所有重构。在我们这样做之后——完成我们的实现和重构——我们将创建一个示例客户端应用,它将使用我们的内容提供者并触发所有的 CRUD 操作。

让我们创建一个ContentProvider类。创建一个名为provider的新包,其中JournalerProvider类扩展了ContentProvider类。

开始上课:

    package com.journaler.provider 

    import android.content.* 
    import android.database.Cursor 
    import android.net.Uri 
    import com.journaler.database.DbHelper 
    import android.content.ContentUris 
    import android.database.SQLException 
    import android.database.sqlite.SQLiteDatabase 
    import android.database.sqlite.SQLiteQueryBuilder 
    import android.text.TextUtils 

    class JournalerProvider : ContentProvider() { 

      private val version = 1 
      private val name = "journaler" 
      private val db: SQLiteDatabase by lazy { 
        DbHelper(name, version).writableDatabase 
    } 

定义companion对象:

     companion object { 
        private val dataTypeNote = "note" 
        private val dataTypeNotes = "notes" 
        private val dataTypeTodo = "todo" 
        private val dataTypeTodos = "todos" 
        val AUTHORITY = "com.journaler.provider" 
        val URL_NOTE = "content://$AUTHORITY/$dataTypeNote" 
        val URL_TODO = "content://$AUTHORITY/$dataTypeTodo" 
        val URL_NOTES = "content://$AUTHORITY/$dataTypeNotes" 
        val URL_TODOS = "content://$AUTHORITY/$dataTypeTodos" 
        private val matcher = UriMatcher(UriMatcher.NO_MATCH) 
        private val NOTE_ALL = 1 
        private val NOTE_ITEM = 2 
        private val TODO_ALL = 3 
        private val TODO_ITEM = 4 
    } 

类初始化:

    /** 
     * We register uri paths in the following format: 
     * 
     * <prefix>://<authority>/<data_type>/<id> 
     * <prefix> - This is always set to content:// 
     * <authority> - Name for the content provider 
     * <data_type> - The type of data we provide in this Uri 
     * <id> - Record ID. 
     */ 
    init { 
        /** 
         * The calls to addURI() go here, 
         * for all of the content URI patterns that the provider should
          recognize. 
         * 
         * First: 
         * 
         * Sets the integer value for multiple rows in notes (TODOs) to 
         1\. 
         * Notice that no wildcard is used in the path. 
         * 
         * Second: 
         * 
         * Sets the code for a single row to 2\. In this case, the "#"
         wildcard is 
         * used. "content://com.journaler.provider/note/3" matches, but 
         * "content://com.journaler.provider/note doesn't. 
         * 
         * The same applies for TODOs. 
         * 
         * addUri() params: 
         * 
         * authority    - String: the authority to match 
         * 
         * path         - String: the path to match. 
         *              * may be used as a wild card for any text, 
         *              and # may be used as a wild card for numbers. 
         * 
         * code              - int: the code that is returned when a
        URI 
         *              is matched against the given components. 
         */ 
        matcher.addURI(AUTHORITY, dataTypeNote, NOTE_ALL) 
        matcher.addURI(AUTHORITY, "$dataTypeNotes/#", NOTE_ITEM) 
        matcher.addURI(AUTHORITY, dataTypeTodo, TODO_ALL) 
        matcher.addURI(AUTHORITY, "$dataTypeTodos/#", TODO_ITEM) 
    } 

覆盖onCreate()方法:

     /** 
     * True - if the provider was successfully loaded 
     */ 
    override fun onCreate() = true 

插入操作如下:

     override fun insert(uri: Uri?, values: ContentValues?): Uri { 
        uri?.let { 
            values?.let { 
                db.beginTransaction() 
                val (url, table) = getParameters(uri) 
                if (!TextUtils.isEmpty(table)) { 
                    val inserted = db.insert(table, null, values) 
                    val success = inserted > 0 
                    if (success) { 
                        db.setTransactionSuccessful() 
                    } 
                    db.endTransaction() 
                    if (success) { 
                        val resultUrl = ContentUris.withAppendedId
                        (Uri.parse(url), inserted) 
                        context.contentResolver.notifyChange(resultUrl,
                        null) 
                        return resultUrl 
                    } 
                } else { 
                    throw SQLException("Insert failed, no table for
                    uri: " + uri) 
                } 
            } 
        } 
        throw SQLException("Insert failed: " + uri) 
    } 

按如下方式更新操作:

     override fun update( 
            uri: Uri?, 
            values: ContentValues?, 
            where: String?, 
            whereArgs: Array<out String>? 
    ): Int { 
        uri?.let { 
            values?.let { 
                db.beginTransaction() 
                val (_, table) = getParameters(uri) 
                if (!TextUtils.isEmpty(table)) { 
                    val updated = db.update(table, values, where,
                     whereArgs) 
                    val success = updated > 0 
                    if (success) { 
                        db.setTransactionSuccessful() 
                    } 
                    db.endTransaction() 
                    if (success) { 
                        context.contentResolver.notifyChange(uri, null) 
                        return updated 
                    } 
                } else { 
                    throw SQLException("Update failed, no table for
                     uri: " + uri) 
                } 
            } 
        } 
        throw SQLException("Update failed: " + uri) 
    } 

删除操作如下:

    override fun delete( 
            uri: Uri?, 
            selection: String?, 
            selectionArgs: Array<out String>? 
    ): Int { 
        uri?.let { 
            db.beginTransaction() 
            val (_, table) = getParameters(uri) 
            if (!TextUtils.isEmpty(table)) { 
                val count = db.delete(table, selection, selectionArgs) 
                val success = count > 0 
                if (success) { 
                    db.setTransactionSuccessful() 
                } 
                db.endTransaction() 
                if (success) { 
                    context.contentResolver.notifyChange(uri, null) 
                    return count 
                } 
            } else { 
                throw SQLException("Delete failed, no table for uri: "
               + uri) 
            } 
        } 
        throw SQLException("Delete failed: " + uri) 
    } 

正在执行查询:

     override fun query( 
            uri: Uri?, 
            projection: Array<out String>?, 
            selection: String?, 
            selectionArgs: Array<out String>?, 
            sortOrder: String? 
     ): Cursor { 
        uri?.let { 
            val stb = SQLiteQueryBuilder() 
            val (_, table) = getParameters(uri) 
            stb.tables = table 
            stb.setProjectionMap(mutableMapOf<String, String>()) 
            val cursor = stb.query(db, projection, selection,
             selectionArgs, null, null, null) 
            // register to watch a content URI for changes 
            cursor.setNotificationUri(context.contentResolver, uri) 
            return cursor 
        } 
        throw SQLException("Query failed: " + uri) 
    } 

    /** 
     * Return the MIME type corresponding to a content URI. 
     */ 
    override fun getType(p0: Uri?): String = when (matcher.match(p0)) { 
        NOTE_ALL -> { 
            "${ContentResolver.
            CURSOR_DIR_BASE_TYPE}/vnd.com.journaler.note.items" 
        } 
        NOTE_ITEM -> { 
            "${ContentResolver.
             CURSOR_ITEM_BASE_TYPE}/vnd.com.journaler.note.item" 
        } 
        TODO_ALL -> { 
            "${ContentResolver.
             CURSOR_DIR_BASE_TYPE}/vnd.com.journaler.todo.items" 
        } 
        TODO_ITEM -> { 
            "${ContentResolver.
            CURSOR_ITEM_BASE_TYPE}/vnd.com.journaler.todo.item" 
        } 
        else -> throw IllegalArgumentException
        ("Unsupported Uri [ $p0 ]") 
    } 

课程结束:

     private fun getParameters(uri: Uri): Pair<String, String> { 
        if (uri.toString().startsWith(URL_NOTE)) { 
            return Pair(URL_NOTE, DbHelper.TABLE_NOTES) 
        } 
        if (uri.toString().startsWith(URL_NOTES)) { 
            return Pair(URL_NOTES, DbHelper.TABLE_NOTES) 
        } 
        if (uri.toString().startsWith(URL_TODO)) { 
            return Pair(URL_TODO, DbHelper.TABLE_TODOS) 
        } 
        if (uri.toString().startsWith(URL_TODOS)) { 
            return Pair(URL_TODOS, DbHelper.TABLE_TODOS) 
        } 
        return Pair("", "") 
       } 

     }  

从上到下,我们执行了以下操作:

  • 定义了数据库名称和版本
  • 定义了数据库实例惰性初始化
  • 定义了我们将用于访问数据的 URI
  • 实现了所有的 CRUD 操作
  • 为数据定义了 MIME 类型

现在,当您有一个内容提供商实现时,需要在您的manifest中注册它,如下所示:

    <manifest xmlns:android=
    "http://schemas.android.com/apk/res/android" 
    package="com.journaler"> 
    ... 
      <application 
        ... 
      > 
        ... 
        <provider 
            android:exported="true" 
            android:name="com.journaler.provider.JournalerProvider" 
            android:authorities="com.journaler.provider" /> 
        ... 
     </application> 
    ... 
    </manifest> 

观察。我们将导出的属性设置为True。这是什么意思?这意味着,如果True,日志提供程序可用于其他应用。任何应用都可以使用提供商的内容 URI 来访问数据。一个更重要的属性是multiprocess。如果应用在多个进程中运行,则此属性决定是否创建了 Journaler 提供程序的多个实例。如果True,每个应用的进程都有自己的内容提供商实例。

让我们继续。在Crud界面,如果您还没有这个对象,请将其添加到companion对象中:

    companion object { 
        val BROADCAST_ACTION = "com.journaler.broadcast.crud" 
        val BROADCAST_EXTRAS_KEY_CRUD_OPERATION_RESULT = "crud_result" 
   }  

我们将把我们的Db类重命名为内容。更新Content实现,如下,使用JournalerProvider:

    package com.journaler.database 

    import android.content.ContentValues 
    import android.location.Location 
    import android.net.Uri 
    import android.util.Log 
    import com.github.salomonbrys.kotson.fromJson 
    import com.google.gson.Gson 
    import com.journaler.Journaler 
    import com.journaler.model.* 
    import com.journaler.provider.JournalerProvider 

    object Content { 

      private val gson = Gson() 
      private val tag = "Content" 

      val NOTE = object : Crud<Note> { ... 

Note插入操作:

     ... 
     override fun insert(what: Note): Long { 
       val inserted = insert(listOf(what)) 
       if (!inserted.isEmpty()) return inserted[0] 
         return 0 
     } 

     override fun insert(what: Collection<Note>): List<Long> { 
        val ids = mutableListOf<Long>() 
        what.forEach { item -> 
           val values = ContentValues() 
           values.put(DbHelper.COLUMN_TITLE, item.title) 
           values.put(DbHelper.COLUMN_MESSAGE, item.message) 
           values.put(DbHelper.COLUMN_LOCATION,
           gson.toJson(item.location)) 
           val uri = Uri.parse(JournalerProvider.URL_NOTE) 
           val ctx = Journaler.ctx 
           ctx?.let { 
             val result = ctx.contentResolver.insert(uri, values) 
             result?.let { 
                 try { 
                      ids.add(result.lastPathSegment.toLong()) 
                  } catch (e: Exception) { 
                  Log.e(tag, "Error: $e") 
                } 
             } 
           } 
         } 
         return ids 
        } ... 

Note更新操作:

    .. 
    override fun update(what: Note) = update(listOf(what)) 

    override fun update(what: Collection<Note>): Int { 
      var count = 0 
      what.forEach { item -> 
          val values = ContentValues() 
          values.put(DbHelper.COLUMN_TITLE, item.title) 
          values.put(DbHelper.COLUMN_MESSAGE, item.message) 
          values.put(DbHelper.COLUMN_LOCATION,
          gson.toJson(item.location)) 
          val uri = Uri.parse(JournalerProvider.URL_NOTE) 
          val ctx = Journaler.ctx 
          ctx?.let { 
            count += ctx.contentResolver.update( 
              uri, values, "_id = ?", arrayOf(item.id.toString()) 
            ) 
          } 
         } 
         return count 
        } ... 

Note删除操作:

   ... 
   override fun delete(what: Note): Int = delete(listOf(what)) 

   override fun delete(what: Collection<Note>): Int { 
     var count = 0 
     what.forEach { item -> 
       val uri = Uri.parse(JournalerProvider.URL_NOTE) 
       val ctx = Journaler.ctx 
       ctx?.let { 
         count += ctx.contentResolver.delete( 
         uri, "_id = ?", arrayOf(item.id.toString()) 
       ) 
     } 
   } 
   return count 
  } ...  

Note选择操作:

     ...  
     override fun select(args: Pair<String, String> 
      ): List<Note> = select(listOf(args)) 

     override fun select(args: Collection<Pair<String, String>>):  
     List<Note> { 
            val items = mutableListOf<Note>() 
            val selection = StringBuilder() 
            val selectionArgs = mutableListOf<String>() 
            args.forEach { arg -> 
                selection.append("${arg.first} == ?") 
                selectionArgs.add(arg.second) 
            } 
            val ctx = Journaler.ctx 
            ctx?.let { 
                val uri = Uri.parse(JournalerProvider.URL_NOTES) 
                val cursor = ctx.contentResolver.query( 
                        uri, null, selection.toString(),
                  selectionArgs.toTypedArray(), null 
                ) 
                while (cursor.moveToNext()) { 
                    val id = cursor.getLong
                    (cursor.getColumnIndexOrThrow(DbHelper.ID)) 
                    val titleIdx = cursor.getColumnIndexOrThrow
                    (DbHelper.COLUMN_TITLE) 
                    val title = cursor.getString(titleIdx) 
                    val messageIdx = cursor.getColumnIndexOrThrow
                   (DbHelper.COLUMN_MESSAGE) 
                    val message = cursor.getString(messageIdx) 
                    val locationIdx = cursor.getColumnIndexOrThrow
                   (DbHelper.COLUMN_LOCATION) 
                    val locationJson = cursor.getString(locationIdx) 
                    val location = gson.fromJson<Location>
                    (locationJson) 
                    val note = Note(title, message, location) 
                    note.id = id 
                    items.add(note) 
                } 
                cursor.close() 
                return items 
            } 
            return items 
        } 

        override fun selectAll(): List<Note> { 
            val items = mutableListOf<Note>() 
            val ctx = Journaler.ctx 
            ctx?.let { 
                val uri = Uri.parse(JournalerProvider.URL_NOTES) 
                val cursor = ctx.contentResolver.query( 
                        uri, null, null, null, null 
                ) 
                while (cursor.moveToNext()) { 
                    val id = cursor.getLong
                    (cursor.getColumnIndexOrThrow(DbHelper.ID)) 
                    val titleIdx = cursor.getColumnIndexOrThrow
                   (DbHelper.COLUMN_TITLE) 
                    val title = cursor.getString(titleIdx) 
                    val messageIdx = cursor.getColumnIndexOrThrow
                    (DbHelper.COLUMN_MESSAGE) 
                    val message = cursor.getString(messageIdx) 
                    val locationIdx = cursor.getColumnIndexOrThrow
                   (DbHelper.COLUMN_LOCATION) 
                    val locationJson = cursor.getString(locationIdx) 
                    val location = gson.fromJson<Location>
                  (locationJson) 
                    val note = Note(title, message, location) 
                    note.id = id 
                    items.add(note) 
                } 
                cursor.close() 
            } 
            return items 
        } 
    }  

Todo对象定义及其插入操作:

     ... 
     val TODO = object : Crud<Todo> { 
        override fun insert(what: Todo): Long { 
            val inserted = insert(listOf(what)) 
            if (!inserted.isEmpty()) return inserted[0] 
            return 0 
        } 

        override fun insert(what: Collection<Todo>): List<Long> { 
            val ids = mutableListOf<Long>() 
            what.forEach { item -> 
                val values = ContentValues() 
                values.put(DbHelper.COLUMN_TITLE, item.title) 
                values.put(DbHelper.COLUMN_MESSAGE, item.message) 
                values.put(DbHelper.COLUMN_LOCATION,
                gson.toJson(item.location)) 
                val uri = Uri.parse(JournalerProvider.URL_TODO) 
                values.put(DbHelper.COLUMN_SCHEDULED,   
                item.scheduledFor) 
                val ctx = Journaler.ctx 
                ctx?.let { 
                    val result = ctx.contentResolver.insert(uri, 
                    values) 
                    result?.let { 
                        try { 
                            ids.add(result.lastPathSegment.toLong()) 
                        } catch (e: Exception) { 
                            Log.e(tag, "Error: $e") 
                        } 
                    } 
                } 
            } 
            return ids 
        } ... 

Todo更新操作:

     ... 
     override fun update(what: Todo) = update(listOf(what)) 

     override fun update(what: Collection<Todo>): Int { 
        var count = 0 
        what.forEach { item -> 
                val values = ContentValues() 
                values.put(DbHelper.COLUMN_TITLE, item.title) 
                values.put(DbHelper.COLUMN_MESSAGE, item.message) 
                values.put(DbHelper.COLUMN_LOCATION,
                gson.toJson(item.location)) 
                val uri = Uri.parse(JournalerProvider.URL_TODO) 
                values.put(DbHelper.COLUMN_SCHEDULED, 
                item.scheduledFor) 
                val ctx = Journaler.ctx 
                ctx?.let { 
                    count += ctx.contentResolver.update( 
                            uri, values, "_id = ?",
                           arrayOf(item.id.toString()) 
                    ) 
                } 
            } 
            return count 
        } ... 

Todo删除操作:

     ... 
     override fun delete(what: Todo): Int = delete(listOf(what)) 

     override fun delete(what: Collection<Todo>): Int { 
            var count = 0 
            what.forEach { item -> 
                val uri = Uri.parse(JournalerProvider.URL_TODO) 
                val ctx = Journaler.ctx 
                ctx?.let { 
                    count += ctx.contentResolver.delete( 
                            uri, "_id = ?", arrayOf(item.id.toString()) 
                    ) 
                } 
            } 
            return count 
        } 

Todo选择操作:

         ... 
        override fun select(args: Pair<String, String>): List<Todo> =  
        select(listOf(args)) 

        override fun select(args: Collection<Pair<String, String>>):
         List<Todo> { 
            val items = mutableListOf<Todo>() 
            val selection = StringBuilder() 
            val selectionArgs = mutableListOf<String>() 
            args.forEach { arg -> 
                selection.append("${arg.first} == ?") 
                selectionArgs.add(arg.second) 
            } 
            val ctx = Journaler.ctx 
            ctx?.let { 
                val uri = Uri.parse(JournalerProvider.URL_TODOS) 
                val cursor = ctx.contentResolver.query( 
                        uri, null, selection.toString(),
                        selectionArgs.toTypedArray(), null 
                ) 
                while (cursor.moveToNext()) { 
                    val id = cursor.getLong
                   (cursor.getColumnIndexOrThrow(DbHelper.ID)) 
                    val titleIdx = cursor.getColumnIndexOrThrow
                   (DbHelper.COLUMN_TITLE) 
                    val 
                    title = 
                    cursor.getString(titleIdx) 
                    val messageIdx = cursor.getColumnIndexOrThrow
                    (DbHelper.COLUMN_MESSAGE) 
                    val message = cursor.getString(messageIdx) 
                    val locationIdx = cursor.getColumnIndexOrThrow
                   (DbHelper.COLUMN_LOCATION) 
                    val locationJson = cursor.getString(locationIdx) 
                    val location = gson.fromJson<Location>
                    (locationJson) 
                    val scheduledForIdx = cursor.getColumnIndexOrThrow( 
                        DbHelper.COLUMN_SCHEDULED 
                    ) 
                    val scheduledFor = cursor.getLong(scheduledForIdx) 
                    val todo = Todo(title, message, location,
                    scheduledFor) 
                    todo.id = id 
                    items.add(todo) 
                } 
                cursor.close() 
            } 
            return items 
        } 

        override fun selectAll(): List<Todo> { 
            val items = mutableListOf<Todo>() 
            val ctx = Journaler.ctx 
            ctx?.let { 
                val uri = Uri.parse(JournalerProvider.URL_TODOS) 
                val cursor = ctx.contentResolver.query( 
                        uri, null, null, null, null 
                ) 
                while (cursor.moveToNext()) { 
                    val id = cursor.getLong
                   (cursor.getColumnIndexOrThrow(DbHelper.ID)) 
                    val titleIdx = cursor.getColumnIndexOrThrow
                   (DbHelper.COLUMN_TITLE) 
                    val title = cursor.getString(titleIdx) 
                    val messageIdx = cursor.getColumnIndexOrThrow
                   (DbHelper.COLUMN_MESSAGE) 
                    val message = cursor.getString(messageIdx) 
                    val locationIdx = cursor.getColumnIndexOrThrow
                   (DbHelper.COLUMN_LOCATION) 
                    val locationJson = cursor.getString(locationIdx) 
                    val location = gson.fromJson<Location>
                    (locationJson) 
                    val scheduledForIdx = cursor.getColumnIndexOrThrow( 
                        DbHelper.COLUMN_SCHEDULED 
                    ) 
                    val scheduledFor = cursor.getLong(scheduledForIdx) 
                    val todo = Todo
                    (title, message, location, scheduledFor) 
                    todo.id = id 
                    items.add(todo) 
                } 
                cursor.close() 
            } 
            return items 
         } 
      } 
   }  

仔细阅读代码。我们用内容提供商取代了直接数据库访问。更新您的用户界面类以使用新的重构代码。如果您在这方面有困难,您可以看看包含这些更改的 GitHub 分支:

https://github . com/PacktPublishing/-Mastering-Android-Development-with-Kotlin/tree/examples/chapter _ 12

该分支还包含一个日志内容提供者客户端应用的示例。我们将在客户端应用的主屏幕上突出显示一个包含四个按钮的使用示例。每个按钮都会触发一个 CRUD 操作示例,如下所示:

    package com.journaler.content_provider_client 

    import android.content.ContentValues 
    import android.location.Location 
    import android.net.Uri 
    import android.os.AsyncTask 
    import android.os.Bundle 
    import android.support.v7.app.AppCompatActivity 
    import android.util.Log 
    import com.github.salomonbrys.kotson.fromJson 
    import com.google.gson.Gson 
    import kotlinx.android.synthetic.main.activity_main.* 

   class MainActivity : AppCompatActivity() { 

     private val gson = Gson() 
     private val tag = "Main activity" 

     override fun onCreate(savedInstanceState: Bundle?) { 
        super.onCreate(savedInstanceState) 
        setContentView(R.layout.activity_main) 

        select.setOnClickListener { 
            val task = object : AsyncTask<Unit, Unit, Unit>() { 
                override fun doInBackground(vararg p0: Unit?) { 
                    val selection = StringBuilder() 
                    val selectionArgs = mutableListOf<String>() 
                    val uri = Uri.parse
                    ("content://com.journaler.provider/notes") 
                    val cursor = contentResolver.query( 
                            uri, null, selection.toString(),
                            selectionArgs.toTypedArray(), null 
                    ) 
                    while (cursor.moveToNext()) { 
                        val id = cursor.getLong
                        (cursor.getColumnIndexOrThrow("_id")) 
                        val titleIdx =  cursor.
                        getColumnIndexOrThrow("title") 
                        val title = cursor.getString(titleIdx) 
                        val messageIdx = cursor.
                        getColumnIndexOrThrow("message") 
                        val message = cursor.getString(messageIdx) 
                        val locationIdx = cursor.
                        getColumnIndexOrThrow("location") 
                        val locationJson = cursor.
                        getString(locationIdx) 
                        val location = 
                        gson.fromJson<Location>(locationJson) 
                        Log.v( 
                                tag, 
                                "Note retrieved via content provider [
                                 $id, $title, $message, $location ]" 
                        ) 
                    } 
                    cursor.close() 
                } 
            } 
            task.execute() 
        } 

        insert.setOnClickListener { 
            val task = object : AsyncTask<Unit, Unit, Unit>() { 
                override fun doInBackground(vararg p0: Unit?) { 
                    for (x in 0..5) { 
                        val uri = Uri.parse
                       ("content://com.journaler.provider/note") 
                        val values = ContentValues() 
                        values.put("title", "Title $x") 
                        values.put("message", "Message $x") 
                        val location = Location("stub location $x") 
                        location.latitude = x.toDouble() 
                        location.longitude = x.toDouble() 
                        values.put("location", gson.toJson(location)) 
                        if (contentResolver.insert(uri, values) !=
                        null) { 
                            Log.v( 
                                    tag, 
                                    "Note inserted [ $x ]" 
                            ) 
                        } else { 
                            Log.e( 
                                    tag, 
                                    "Note not inserted [ $x ]" 
                            ) 
                        } 
                    } 
                } 
            } 
            task.execute() 
        } 

        update.setOnClickListener { 
            val task = object : AsyncTask<Unit, Unit, Unit>() { 
                override fun doInBackground(vararg p0: Unit?) { 
                    val selection = StringBuilder() 
                    val selectionArgs = mutableListOf<String>() 
                    val uri =
                    Uri.parse("content://com.journaler.provider/notes") 
                    val cursor = contentResolver.query( 
                            uri, null, selection.toString(),
                           selectionArgs.toTypedArray(), null 
                    ) 
                    while (cursor.moveToNext()) { 
                        val values = ContentValues() 
                        val id = cursor.getLong
                        (cursor.getColumnIndexOrThrow("_id")) 
                        val titleIdx =
                        cursor.getColumnIndexOrThrow("title") 
                        val title = "${cursor.getString(titleIdx)} upd:
                        ${System.currentTimeMillis()}" 
                        val messageIdx =
                       cursor.getColumnIndexOrThrow("message") 
                        val message = 
                       "${cursor.getString(messageIdx)} upd:
                       ${System.currentTimeMillis()}" 
                        val locationIdx = 
                       cursor.getColumnIndexOrThrow("location") 
                        val locationJson =
                       cursor.getString(locationIdx) 
                        values.put("_id", id) 
                        values.put("title", title) 
                        values.put("message", message) 
                        values.put("location", locationJson) 

                        val updated = contentResolver.update( 
                                uri, values, "_id = ?",
                                arrayOf(id.toString()) 
                        ) 
                        if (updated > 0) { 
                            Log.v( 
                                    tag, 
                                    "Notes updated [ $updated ]" 
                            ) 
                        } else { 
                            Log.e( 
                                    tag, 
                                    "Notes not updated" 
                            ) 
                        } 
                    } 
                    cursor.close() 
                } 
            } 
            task.execute() 
        } 

        delete.setOnClickListener { 
            val task = object : AsyncTask<Unit, Unit, Unit>() { 
                override fun doInBackground(vararg p0: Unit?) { 
                    val selection = StringBuilder() 
                    val selectionArgs = mutableListOf<String>() 
                    val uri = Uri.parse
                   ("content://com.journaler.provider/notes") 
                    val cursor = contentResolver.query( 
                            uri, null, selection.toString(),
                            selectionArgs.toTypedArray(), null 
                    ) 
                    while (cursor.moveToNext()) { 
                        val id = cursor.getLong
                        (cursor.getColumnIndexOrThrow("_id")) 
                        val deleted = contentResolver.delete( 
                                uri, "_id = ?", arrayOf(id.toString()) 
                        ) 
                        if (deleted > 0) { 
                            Log.v( 
                                    tag, 
                                    "Notes deleted [ $deleted ]" 
                            ) 
                        } else { 
                            Log.e( 
                                    tag, 
                                    "Notes not deleted" 
                            ) 
                        } 
                    } 
                    cursor.close() 
                } 

           } 
            task.execute() 
        } 
      } 
   } 

这个例子演示了如何使用内容提供者从其他应用触发 CRUD 操作。

安卓适配器

为了在我们的主屏幕上呈现内容,我们将使用安卓适配器类。安卓框架提供适配器作为一种机制,提供项目以列表或网格的形式查看组。为了展示适配器使用的例子,我们将定义我们自己的适配器实现。创建一个名为adapter的新包和一个扩展BaseAdapter类的EntryAdapter成员类:

    package com.journaler.adapter 

    import android.annotation.SuppressLint 
    import android.content.Context 
    import android.view.LayoutInflater 
    import android.view.View 
    import android.view.ViewGroup 
    import android.widget.BaseAdapter 
    import android.widget.TextView 
    import com.journaler.R 
    import com.journaler.model.Entry 

    class EntryAdapter( 
        private val ctx: Context, 
        private val items: List<Entry> 
    ) : BaseAdapter() { 

    @SuppressLint("InflateParams", "ViewHolder") 
    override fun getView(p0: Int, p1: View?, p2: ViewGroup?): View { 
        p1?.let { 
            return p1 
        } 
        val inflater = LayoutInflater.from(ctx) 
        val view = inflater.inflate(R.layout.adapter_entry, null) 
        val label = view.findViewById<TextView>(R.id.title) 
        label.text = items[p0].title 
        return view 
    } 

    override fun getItem(p0: Int): Entry = items[p0] 
    override fun getItemId(p0: Int): Long = items[p0].id 
    override fun getCount(): Int = items.size 
   } 

我们覆盖了以下方法:

  • getView():这将根据容器中的当前位置返回填充视图的实例
  • getItem():返回我们用来创建视图的项目的实例;在我们的例子中,这是Entry类实例(NoteTodo)
  • getItemId():返回当前项目实例的标识
  • getCount():返回项目总数

我们将连接适配器和我们的用户界面。打开ItemsFragment并更新其onResume()方法来实例化适配器并将其分配给一个ListView,如下所示:

    override fun onResume() { 
        super.onResume() 
        ... 
        executor.execute { 
            val notes = Content.NOTE.selectAll() 
            val adapter = EntryAdapter(activity, notes) 
            activity.runOnUiThread { 
                view?.findViewById<ListView>(R.id.items)?.adapter =
             adapter 
            } 
        } 
    } 

当您构建和运行您的应用时,您应该会看到ViewPager的每个页面都填充了加载的项目,如下图所示:

内容加载器

内容加载器为您提供了一种从内容提供者或其他数据源加载数据的机制,以便在用户界面组件(如活动或片段)中显示。这些是装载机提供的优势:

  • 在单独的线程上运行
  • 通过提供回调方法简化线程管理
  • 加载程序跨配置更改保存和缓存结果,从而防止重复查询
  • 我们可以实现并观察数据的变化

我们将创建我们的内容加载器实现。首先,我们需要更新Adapter类。由于我们将处理光标,我们将使用CursorAdapte r 代替BaseAdapterCursorAdapter接受一个Cursor实例作为主构造函数中的参数。CursorAdapter的实现比我们现在拥有的要简单得多。打开EntryAdapter并更新如下:

    class EntryAdapter(ctx: Context, crsr: Cursor) : CursorAdapter(ctx,
    crsr) { 

    override fun newView(p0: Context?, p1: Cursor?, p2: ViewGroup?):
    View { 
        val inflater = LayoutInflater.from(p0) 
        return inflater.inflate(R.layout.adapter_entry, null) 
    } 

    override fun bindView(p0: View?, p1: Context?, p2: Cursor?) { 
        p0?.let { 
            val label = p0.findViewById<TextView>(R.id.title) 
            label.text = cursor.getString( 
                cursor.getColumnIndexOrThrow(DbHelper.COLUMN_TITLE) 
            ) 
        } 
    } 

   } 

我们有以下两种方法可以覆盖:

  • newView():这将返回要用数据填充的视图实例
  • bindView():这将填充来自Cursor实例的数据

最后,让我们更新我们的ItemsFragment类,因此它使用内容加载器实现:

    class ItemsFragment : BaseFragment() { 
      ... 
      private var adapter: EntryAdapter? = null 
      ... 
      private val loaderCallback = object :
      LoaderManager.LoaderCallbacks<Cursor> { 
        override fun onLoadFinished(loader: Loader<Cursor>?, cursor:
        Cursor?) { 
            cursor?.let { 
                if (adapter == null) { 
                    adapter = EntryAdapter(activity, cursor) 
                    items.adapter = adapter 
                } else { 
                    adapter?.swapCursor(cursor) 
                } 
            } 
        } 

        override fun onLoaderReset(loader: Loader<Cursor>?) { 
            adapter?.swapCursor(null) 
        } 

        override fun onCreateLoader(id: Int, args: Bundle?):
        Loader<Cursor> { 
            return CursorLoader( 
                    activity, 
                    Uri.parse(JournalerProvider.URL_NOTES), 
                    null, 
                    null, 
                    null, 
                    null 
            ) 
        } 
    } 

    override fun onCreate(savedInstanceState: Bundle?) { 
        super.onCreate(savedInstanceState) 
        loaderManager.initLoader( 
                0, null, loaderCallback 
        ) 
    } 

    override fun onResume() { 
        super.onResume() 
        loaderManager.restartLoader(0, null, loaderCallback) 

        val btn = view?.findViewById
       <FloatingActionButton>(R.id.new_item) 
        btn?.let { 
            animate(btn, false) 
        } 
    } 
   }  

我们通过调用片段的LoaderManager成员来初始化LoaderManager。我们执行的两个关键方法如下:

  • initLoader():这确保加载器被初始化并激活
  • restartLoader():这将启动一个新的或重新启动一个现有的loader实例

这两种方法都接受加载器标识和绑定数据作为参数,并且LoaderCallbacks<Cursor>实现提供了以下三种方法来覆盖:

  • onCreateLoader():这将为我们提供的 ID 实例化并返回一个新的加载器实例
  • onLoadFinished():当之前创建的加载器完成加载时,调用这个函数
  • onLoaderReset():当一个先前创建的加载程序正在被重置,并且因此使其数据不可用时,调用这个函数

数据绑定

安卓支持数据绑定机制,这样数据就可以与视图绑定,并且最小化粘合代码。通过更新构建梯度配置启用数据绑定,如下所示:

     android { 
       .... 
       dataBinding { 
        enabled = true 
       } 
     } 
     ... 
     dependencies { 
      ... 
      kapt 'com.android.databinding:compiler:2.3.1' 
    } 
    ...  

现在,您可以定义绑定表达式了。看看下面的例子:

    <?xml version="1.0" encoding="utf-8"?> 
    <layout xmlns:android="http://schemas.android.com/apk/res/android"> 

    <data> 
        <variable 
            name="note" 
            type="com.journaler.model.Note" /> 
    </data> 

    <LinearLayout 
        android:layout_width="match_parent" 
        android:layout_height="match_parent" 
        android:orientation="vertical"> 

        <TextView 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" 
            android:text="@{note.title}" /> 

    </LinearLayout> 
  </layout>  

让我们将数据绑定如下:

    package com.journaler.activity 

    import android.databinding.DataBindingUtil 
    import android.location.Location 
    import android.os.Bundle 
    import com.journaler.R 
    import com.journaler.databinding.ActivityBindingBinding 
    import com.journaler.model.Note 

    abstract class BindingActivity : BaseActivity() { 

    override fun onCreate(savedInstanceState: Bundle?) { 
        super.onCreate(savedInstanceState) 
        /** 
         * ActivityBindingBinding is auto generated class 
         * which name is derived from activity_binding.xml filename. 
         */ 
        val binding : ActivityBindingBinding =
        DataBindingUtil.setContentView( 
            this, R.layout.activity_binding 
        ) 
        val location = Location("dummy") 
        val note = Note("my note", "bla", location) 
        binding.note = note 
      } 

    }  

就这样!看看将数据绑定到布局视图有多简单!我们强烈建议您尽可能多地使用数据绑定。创造你自己的例子!随意实验!

使用列表

我们向您展示了如何处理数据。正如您所注意到的,在主视图数据容器中,我们使用了ListView。我们为什么选择它?首先,它是保存数据最常用的容器。在大多数情况下,您将使用ListView保存来自适配器的数据。永远不要把大量的视图放在像LinearLayout这样的可滚动容器里!尽可能使用ListView。当不再需要视图时,它将回收视图,并在需要时重新保存它们。

使用列表会影响应用的性能,因为它是显示数据的优化容器。显示列表几乎是任何应用的基本功能!任何通过某种操作产生一组数据的应用都需要一个列表。几乎不可能不在应用中使用它。

使用网格

我们注意到列表有多重要。然而,如果我们计划将我们的数据呈现为一个网格呢?我们真幸运!!安卓框架为我们提供了一个与ListView非常相似的GridView。您可以在布局中定义您的GridView,并将适配器实例分配给GridView的适配器属性。GridView将为您回收所有视图,并在需要时执行实例化。列表和网格的主要区别是你必须为你的GridView定义列数。以下示例将向您展示GridView的使用示例:

    <?xml version="1.0" encoding="utf-8"?> 
   <GridView xmlns:android="http://schemas.android.com/apk/res/android" 
      android:id="@+id/my_grid" 
      android:layout_width="match_parent" 
      android:layout_height="match_parent" 
      android:columnWidth="100dp" 
      android:numColumns="3" 
      android:verticalSpacing="20dp" 
      android:horizontalSpacing="20dp" 

      android:stretchMode="columnWidth" 
      android:gravity="center" 
    /> 

我们将强调我们在此示例中使用的重要属性:

  • columnWidth:指定每列的宽度
  • numColumns:指定列数
  • verticalSpacing:指定行与行之间的垂直间距
  • horizontalSpacing:指定网格中项目之间的水平间距

尝试更新当前应用的主ListView,将数据显示为GridView。调整它,让它看起来让最终用户感到愉悦。再一次,随意实验!

实现拖放

在本章的最后一节,我们将向您展示如何实现拖放功能。在大多数包含列表数据的应用中,这可能是您需要的功能。使用列表并不是执行拖放的必要条件,因为您可以拖动任何东西(视图)并将其释放到任何定义了适当监听器的地方。为了更好地理解我们正在讨论的内容,我们将向您展示一个如何实现它的示例。

让我们定义一个视图。在该视图中,我们将设置一个长按监听器来触发拖放操作:

    view.setOnLongClickListener { 
            val data = ClipData.newPlainText("", "") 
            val shadowBuilder = View.DragShadowBuilder(view) 
            view.startDrag(data, shadowBuilder, view, 0) 
            true 
   } 

我们使用ClipData类传递数据来丢弃目标。我们这样定义dragListener,并将其分配给一个我们预计它会下降的视图:

    private val dragListener = View.OnDragListener { 
        view, event -> 
        val tag = "Drag and drop" 
        event?.let { 
            when (event.action) { 
                DragEvent.ACTION_DRAG_STARTED -> { 
                    Log.d(tag, "ACTION_DRAG_STARTED") 
                } 
                DragEvent.ACTION_DRAG_ENDED -> { 
                    Log.d(tag, "ACTION_DRAG_ENDED") 
                } 
                DragEvent.ACTION_DRAG_ENTERED -> { 
                    Log.d(tag, "ACTION_DRAG_ENDED") 
                } 
                DragEvent.ACTION_DRAG_EXITED -> { 
                    Log.d(tag, "ACTION_DRAG_ENDED") 
                } 
                else -> { 
                    Log.d(tag, "ACTION_DRAG_ ELSE ...") 
                } 
            } 
        } 
        true 
     } 

    target?.setOnDragListener(dragListener) 

当我们开始拖动一个视图,并最终在分配了侦听器的target视图上释放它时,拖动侦听器将激发代码。

摘要

在这一章中,我们涵盖了许多主题。我们了解了后端通信,如何使用改装与后端远程实例建立通信,以及如何处理我们获得的数据。本章的目的是与内容提供者和内容加载者合作。我们希望你意识到它们的重要性和好处。最后,我们演示了数据绑定;注意到我们的数据视图容器的重要性,如ListViewGridView;并向您展示了如何执行拖放操作。在下一章,我们将开始测试我们的代码。做好性能优化的准备,因为这是我们下一章要做的!