【译】创建一个基于 Kotlin 的 Android 项目(下集)

3,415 阅读6分钟
原文链接: github.com
第 2 部分

在先前的文章中,我们从零开始新建了一个项目,并且为小猫咪应用调整了 build.gradle

接下来就是针对应用的基础部分编写代码了。

数据模型

此应用的一个主要特征是通过网络从 http://thecatapi.com/ 中解析数据。

完整的 API 如此调用:http://thecatapi.com/api/images/get?format=xml&results_per_page=10

API 返回一个 XML 文件,如下:

查看图片

它需要反序列化数据来获取包含小猫咪图片位置的 url 属性。

Kotlin 有一个非常有用的数据类(data class)可以完美实现此目的。

右击 model.cats 包 (package) 开始新建一个类文件并且选择 New -> Kotlin File/Class 然后将其命名为 Cats 并选择 Class 作为文件类型。

为像接收到的 XML 文件那样构造类,Cats.kt 文件将如下所示:

data class Cats(var data: Data? = null)

data class Data(var images: ArrayList? = null)

data class Image(var url: String? = "", var id: String? = "", var source_url: String? = "")

目前还非常简单……

但同样的类在 Java 中长多了!

Kotlin 中的数据类有几个好处,例如由编译器生成 getter()setter() 以及 toString() 方法,还有更多的像 equals()hashCode() 以及 copy() 这些。所以使用它反序列化数据甚是完美。

API 调用

通过网络解析数据有很多种方法,也有各种第三方库可以应付。其中就有 Square 的 Retrofit2

这是一个非常强大的 HTTPClient 并且安装简单。

我们从 interface 开始,先在 network 包下创建之。

称其为 CatAPI,如下所示:

interface CatAPI {
    @GET("/api/images/get?format=xml&results_per_page=" + BuildConfig.MAX_IMAGES_PER_REQUEST)
    fun getCatImageURLs(): Observable
}

interface 会完成对 API 端 /api/images/get?format=xml&results_per_page=Get 请求。

本例中 results_per_page 参数从 build.gradle 中定义的 MAX_IMAGES_PER_REQUEST 常量获取数值,该常量的不同取值取决于使用的 buildTypes

buildTypes {
    debug {
        buildConfigField("int", "MAX_IMAGES_PER_REQUEST", "10")
        ...

此方式对常量在像 debugrelease 情景下的不同取值极其有用,
尤其是在需要从线上 API 切换到测试 API 的时候

关于 interface CatAPI 有一个关键点,那就是用来实现从 API 回调的函数 fun getCatImageURLs(): Observable

所以下一步便是其实现。

同在 network 包下,新建一个类并将其命名为 CatAPINetwork,如下:

class CatAPINetwork {
    fun getExec(): Observable {
        val retrofit = Retrofit.Builder()
            .baseUrl("http://thecatapi.com")
            .addConverterFactory(SimpleXmlConverterFactory.create())
            .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
            .build()

        val catAPI: CatAPI = retrofit.create(CatAPI::class.java)

        return catAPI.getCatImageURLs().
            subscribeOn(Schedulers.io()).
            observeOn(AndroidSchedulers.mainThread())
    }
}

fun getExec(): Observable 为隐式 public,这意味着它可以在此类以外被调用。

.addConverterFactory(SimpleXmlConverterFactory.create()) 这一行表明使用 XML 转换器来反序列化调用 API 的结果。

接着 .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) 是用于 API 回调的调用适配器。

return 行返回 RxJavaObservable 对象:

return catAPI.getCatImageURLs().
            subscribeOn(Schedulers.io()).
            observeOn(AndroidSchedulers.mainThread())

Presenter

Presenter 模块负责完成应用的逻辑部分并在 ViewModel 之间实现数据绑定。

本例会实现 View 调用以解析 API 数据的方法并将其送至负责展示的 Adapter

为与 View 通信,我们先在 presenter 包中创建其 interface 然后将其命名为 MasterPresenter,如下所示:

interface MasterPresenter {
    fun connect(imagesAdapter: ImagesAdapter)
    fun getMasterRequest()
}

第一个函数 fun connect(imagesAdapter: ImagesAdapter) 用来连接 Adapter interface 以显示数据,并且由 fun getMasterRequest() 启动 API 请求。

我们将这些实现置于 presenter 包的一个新类中并将其命名为 MasterPresenterImpl

class MasterPresenterImpl : MasterPresenter {
    lateinit private var imagesAdapter: ImagesAdapter

    override fun connect(imagesAdapter: ImagesAdapter) {
        this.imagesAdapter = imagesAdapter
    }

    override fun getMasterRequest() {
        imagesAdapter.setObservable(getObservableMasterRequest(CatAPINetwork()))
    }

    private fun getObservableMasterRequest(catAPINetwork: CatAPINetwork): Observable {
        return catAPINetwork.getExec()
    }
}

值得注意的是,在 lateinit private var imagesAdapter: ImagesAdapter 一行中,Kotlin 允许我们使用 lateinit 关键字在未初始化的情况下声明一个非空可变的对象。它将会在运行时第一次使用它的时候被初始化,比如在本例中会调用 fun connect(imagesAdapter: ImagesAdapter)

fun getMasterRequest() 函数负责启用 API 调用,只设置 Observable 以便 Adapter (例如 imagesAdapter)在启用执行 API 调用的 catAPINetwork.getExec() 函数后“订阅”之。

View 部分

实现 UI 的类均集中于 view 包中。

基本上都是 ViewAdapter 这些;本例中是 MainActivityImagesAdapter

Layouts

开始实现之前,我们先来研究一下布局 ( Layout ) 设计。

查看图片

为实现此设计我们大体上需要主容器 item 容器这两个基本组件。

主容器包含 item 列表,且我们会将其置于项目 res -> layout 文件夹的 activity_main.xml 中;此文件已在创建项目的初始价段自动生成。

我们需要将应用装进一个RecyclerView 组件中(一个非常强大并且改良过的列表视图组件)。

activity_main.xml 如下所示:




    

containerRecyclerView 组件代表 item 列表主容器

row_card_view.xml 是列表的 item 容器,大体上像这样:




    

        
    

如你所见,item 容器正是主要由一个包含 ImageView (imgVw_cat) 的 RelativeLayout 组成的 card_view

Adapter

现在已经有了 Layout 的基本部分,那么接下来我们继续实现 MainActivityAdapter

Adapter 开始首先要创建其 interface 以被前面的 MasterPresenterImpl 调用,所以我们在 view 包中新建一个文件并将其命名为 ImagesAdapter,然后内容如下:

interface ImagesAdapter {
    fun setObservable(observableCats: Observable)
    fun unsubscribe()
}

setObservable(observableCats: Observable) 函数被 MasterPresenterImpl 调用来设置 Observable 以及让 Adapter “订阅”。

unsubscribe() 函数会被 MainActivity 调用以在 activity 被销毁的时候“退订” Adapter

现在我们在同一个包下一个新建的类中实现它们,称其为 ImagesAdapterImpl,如下:

class ImagesAdapterImpl : RecyclerView.Adapter(), ImagesAdapter {
    private val TAG = ImagesAdapterImpl::class.java.simpleName

    private var cats: Cats? = null
    private val subscriber: Subscriber by lazy { getSubscribe() }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImagesURLsDataHolder {
        return ImagesURLsDataHolder(
                LayoutInflater.from(parent.context).inflate(R.layout.row_card_view, parent, false))
    }

    override fun getItemCount(): Int {
        return cats?.data?.images?.size ?: 0
    }

    override fun onBindViewHolder(holder: ImagesURLsDataHolder, position: Int) {
        holder.bindImages(cats?.data?.images?.get(position)?.url ?: "")
    }

    private fun setData(cats: Cats?) {
        this.cats = cats
    }

    override fun setObservable(observableCats: Observable) {
        observableCats.subscribe(subscriber)
    }

    override fun unsubscribe() {
        if (!subscriber.isUnsubscribed) {
            subscriber.unsubscribe()
        }
    }

    private fun getSubscribe(): Subscriber {
        return object : Subscriber() {
            override fun onCompleted() {
                Log.d(TAG, "onCompleted")
                notifyDataSetChanged()
            }

            override fun onNext(cats: Cats) {
                Log.d(TAG, "onNextNew")
                setData(cats)
            }

            override fun onError(e: Throwable) {
                //TODO : Handle error here
                Log.d(TAG, "" + e.message)
            }
        }
    }

    class ImagesURLsDataHolder(view: View) : RecyclerView.ViewHolder(view) {

        fun bindImages(imgURL: String) {
            Glide.with(itemView.context).
                    load(imgURL).
                    placeholder(R.mipmap.document_image_cancel).
                    diskCacheStrategy(DiskCacheStrategy.ALL).
                    centerCrop().
                    into(itemView.imgVw_cat)
        }
    }
}

这是填充 row_card_view.xml 的类,基本上就是 onCreateViewHolder 函数的 item 容器

private val subscriber: Subscriber by lazy { getSubscribe() } 一行中,getSubscribe() 函数为 Adapter “订阅”用到的 Observable,这里你会看到 lazy 初始化,这是一种声明一个不可变对象的方法(比如 subscriber)并且会在运行时首次调用时创建于函数体内(例如 getSubscribe())。

Subscriber 和 Observable 概念来源于 RxJava;我们今后会深入讨论。

最后值得注意的还有使用 Glide 库来填充 imgVw_cat 的名为 ImagesURLsDataHolder 的内部类 (inner class) ,这有助于从调用 API 取得的传递 URL 获取图片。这部分包含在 bindImages(imgURL: String) 函数中并且由统一文件中的 onBindViewHolder 方法调用。

Activity

最后同样重要的便是 Activity(例如 MainActivity):

class MainActivity : AppCompatActivity() {
    private val imagesAdapterImpl: ImagesAdapterImpl by lazy { ImagesAdapterImpl() }

    private val masterPresenterImpl: MasterPresenterImpl
            by lazy {
                MasterPresenterImpl()
            }

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

        initRecyclerView()
        connectingToMasterPresenter()
        getURLs()
    }

    override fun onDestroy() {
        imagesAdapterImpl.unsubscribe()
        super.onDestroy()
    }

    private fun initRecyclerView() {
        containerRecyclerView.layoutManager = GridLayoutManager(this, 1)
        containerRecyclerView.adapter = imagesAdapterImpl
    }

    private fun connectingToMasterPresenter() {
        masterPresenterImpl.connect(imagesAdapterImpl)
    }

    private fun getURLs() {
        masterPresenterImpl.getMasterRequest()
    }
}

注意到以下函数:

  • initRecyclerView()
  • connectingToMasterPresenter()
  • getURLs()

分别用于:

  • 初始化主容器(例如 RecyclerView
  • MasterPresenterImpl 连接至 MainActivity 并传至 ImagesAdapterImpl(又称 Adapter) 的 interface
  • getURLs() 启动 API 请求以获取 XML 数据,然后执行任务(反序列化数据,通过 Adapter 获取图片)。

至此小猫咪应用已经准备就绪。

你可以在我 Github 仓库中找到 KShow 完整的项目。

该项目也有 Java 的实现:JShows,以便对比。