构建domain层
我们现在创建一个新的包作为domain
层。这一层中会包含一些Commands
的实现来为app执行任务。
首先,必须要定义一个Command
:
public interface Command<T> {
fun execute(): T
}
这个command会执行一个操作并且返回某种类型的对象,这个类型可以通过范型被指定。你需要知道一个有趣的概念,一切kotlin函数都会返回一个值。如果没有指定,它就默认返回一个Unit
类。所以如果我们想让Command不返回数据,我们可以指定它的类型为Unit。
Kotlin中的接口比Java(Java 8以前)中的强大多了,因为它们可以包含代码。但是我们现在不需要更多的代码,以后的章节中会仔细讲这个话题。
第一个command需要去请求天气预报结构然后转换结果为domain类。下面这些类会在domain类中被定义:
data class ForecastList(val city: String, val country: String,
val dailyForecast:List<Forecast>)
data class Forecast(val date: String, val description: String, val high: Int,
val low: Int)
当更多的功能被增加,这些类可能会需要在以后被审查。但是现在这些类对我们来说已经足够了。
这些类必须从数据映射到我们的domain类,所以我接下来需要创建一个DataMapper
:
public class ForecastDataMapper {
fun convertFromDataModel(forecast: ForecastResult): ForecastList {
return ForecastList(forecast.city.name, forecast.city.country,
convertForecastListToDomain(forecast.list))
private fun convertForecastListToDomain(list: List<Forecast>):
List<ModelForecast> {
return list.map { convertForecastItemToDomain(it) }
}
private fun convertForecastItemToDomain(forecast: Forecast): ModelForecast {
return ModelForecast(convertDate(forecast.dt),
forecast.weather[0].description, forecast.temp.max.toInt(),
forecast.temp.min.toInt())
}
private fun convertDate(date: Long): String {
val df = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.getDefault())
return df.format(date * 1000)
}
}
当我们使用了两个相同名字的类,我们可以给其中一个指定一个别名,这样我们就不需要写完整的包名了:
import com.antonioleiva.weatherapp.domain.model.Forecast as ModelForecast
这些代码中另一个有趣的是我们从一个forecast list中转换为domain model的方法:
return list.map { convertForecastItemToDomain(it) }
这一条语句,我们就可以循环这个集合并且返回一个转换后的新的List。Kotlin在List中提供了很多不错的函数操作符,它们可以在这个List的每个item中应用这个操作并且任何方式转换它们。对比Java 7,这是Kotlin其中一个强大的功能。我们很快就会查看所有不同的操作符。知道它们的存在是很重要的,因为它们要方便得多,并可以节省很多时间和模版。
现在,编写命令前的准备就绪:
class RequestForecastCommand(val zipCode: String) :
Command<ForecastList> {
override fun execute(): ForecastList {
val forecastRequest = ForecastRequest(zipCode)
return ForecastDataMapper().convertFromDataModel(
forecastRequest.execute())
}
}
在UI中绘制数据
MainActivity
中的代码有些小的改动,因为现在有真实的数据需要填充到adapter中。异步调用需要被重写成:
async() {
val result = RequestForecastCommand("94043").execute()
uiThread{
forecastList.adapter = ForecastListAdapter(result)
}
}
Adapter也需要被修改:
class ForecastListAdapter(val weekForecast: ForecastList) :
RecyclerView.Adapter<ForecastListAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
ViewHolder? {
return ViewHolder(TextView(parent.getContext()))
}
override fun onBindViewHolder(holder: ViewHolder,
position: Int) {
with(weekForecast.dailyForecast[position]) {
holder.textView.text = "$date - $description - $high/$low"
}
}
override fun getItemCount(): Int = weekForecast.dailyForecast.size
class ViewHolder(val textView: TextView) : RecyclerView.ViewHolder(textView)
}
with函数
with是一个非常有用的函数,它包含在Kotlin的标准库中。它接收一个对象和一个扩展函数作为它的参数,然后使这个对象扩展这个函数。这表示所有我们在括号中编写的代码都是作为对象(第一个参数)的一个扩展函数,我们可以就像作为this一样使用所有它的public方法和属性。当我们针对同一个对象做很多操作的时候这个非常有利于简化代码。
在这一章中有很多新的代码加入,所以检出库中的代码吧。
操作符重载
Kotin有一些固定数量象征性的操作符,我们可以在任何类中很容易地使用它们。方法是创建一个方法,方法名为保留的操作符关键字,这样就可以让这个操作符的行为映射到这个方法。重载这些操作符可以增加代码可读性和简洁性。
操作符表
这里你可以看见一系列包括操作符
和对应方法
的表。对应方法必须在指定的类中通过各种可能性被实现。
一元操作符
操作符 | 函数 |
---|---|
+a | a.unaryPlus() |
-a | a.unaryMinus() |
!a | a.not() |
a++ | a.inc() |
a-- | a.dec() |
二元操作符
操作符 | 函数 |
---|---|
a + b | a.plus(b) |
a - b | a.minus(b) |
a * b | a.times(b) |
a / b | a.div(b) |
a % b | a.mod(b) |
a..b | a.rangeTo(b) |
a in b | b.contains(a) |
a !in b | !b.contains(a) |
a += b | a.plusAssign(b) |
a -= b | a.minusAssign(b) |
a *= b | a.timesAssign(b) |
a /= b | a.divAssign(b) |
a %= b | a.modAssign(b) |
数组操作符
操作符 | 函数 |
---|---|
a[i] | a.get(i) |
a[i, j] | a.get(i, j) |
a[i_1, ..., i_n] | a.get(i_1, ..., i_n) |
a[i] = b | a.set(i, b) |
a[i, j] = b | a.set(i, j, b) |
a[i_1, ..., i_n] = b | a.set(i_1, ..., i_n, b) |
等于操作符
操作符 | 函数 |
---|---|
a == b | a?.equals(b) ?: b === null |
a != b | !(a?.equals(b) ?: b === null) |
相等操作符有一点不同,为了达到正确合适的相等检查做了更复杂的转换,因为要得到一个确切的函数结构比较,不仅仅是指定的名称。方法必须要如下准确地被实现:
operator fun equals(other: Any?): Boolean
操作符===
和!==
用来做身份检查(它们分别是Java中的==
和!=
),并且它们不能被重载。
函数调用
方法 | 调用 |
---|---|
a(i) | a.invoke(i) |
a(i, j) | a.invoke(i, j) |
a(i_1, ..., i_n) | a.invoke(i_1, ..., i_n) |
例子
你可以想象,Kotlin List是实现了数组操作符的,所以我们可以像Java中的数组一样访问List的每一项。除此之外:在可修改的List中,每一项也可以用一个简单的方式被直接设置:
val x = myList[2]
myList[2] = 4
如果你还记得,我们有一个叫ForecastList的数据类,它是由很多其他额外的信息组成的。有趣的是可以直接访问它的每一项而不是请求内部的list得到某一项。做一个完全不相关的事情,我要去实现一个size()
方法,它能稍微能简化一点当前的Adapter:
data class ForecastList(val city: String, val country: String,
val dailyForecast: List<Forecast>) {
operator fun get(position: Int): Forecast = dailyForecast[position]
fun size(): Int = dailyForecast.size
}
它会使我们的onBindViewHolder
更简单一点:
override fun onBindViewHolder(holder: ViewHolder,
position: Int) {
with(weekForecast[position]) {
holder.textView.text = "$date - $description - $high/$low"
}
}
当然还有getItemCount()
方法:
override fun getItemCount(): Int = weekForecast.size()
扩展函数中的操作符
我们不需要去扩展我们自己的类,但是我需要去使用扩展函数扩展我们已经存在的类来让第三方的库能提供更多的操作。几个例子,我们可以去像访问List的方式去访问ViewGroup
的view:
operator fun ViewGroup.get(position: Int): View = getChildAt(position)
现在真的可以非常简单地从一个ViewGroup
中通过position得到一个view:
val container: ViewGroup = find(R.id.container)
val view = container[2]
别忘了去Kotlin for Android developers repository去查看这些代码。
使Forecast list可点击
作为一个真正的app,当前列表的每一个item布局应该做一些工作。第一件事就是创建一个合适的XML,能符合我们的需要就行。我们希望显示一个图标,日期,描述以及最高和最低温度。所以让我们创建一个名为item_forecast.xml
的layout:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/spacing_xlarge"
android:background="?attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/icon"
android:layout_width="48dp"
android:layout_height="48dp"
tools:src="@mipmap/ic_launcher"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginLeft="@dimen/spacing_xlarge"
android:layout_marginRight="@dimen/spacing_xlarge"
android:orientation="vertical">
<TextView
android:id="@+id/date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
tools:text="May 14, 2015"/>
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
tools:text="Light Rain"/>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:id="@+id/maxTemperature"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
tools:text="30"/>
<TextView
android:id="@+id/minTemperature"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
tools:text="15"/>
</LinearLayout>
</LinearLayout>
Domain model和数据映射时必须生成完整的图标uil,所以我们可以这样去加载它:
data class Forecast(val date: String, val description: String,
val high: Int, val low: Int, val iconUrl: String)
在ForecastDataMapper
中:
private fun convertForecastItemToDomain(forecast: Forecast): ModelForecast {
return ModelForecast(convertDate(forecast.dt),
forecast.weather[0].description, forecast.temp.max.toInt(),
forecast.temp.min.toInt(), generateIconUrl(forecast.weather[0].icon))
}
private fun generateIconUrl(iconCode: String): String
= "http://openweathermap.org/img/w/$iconCode.png"
我们从第一个请求中得到图标的code,用来组成完成的图标url。加载图片最简单的方式是使用图片加载库。Picasso
是一个不错的选择。它需要加到build.gradle
的依赖中:
compile "com.squareup.picasso:picasso:<version>"
如此,Adapter也需要一个大的改动了。还需要一个click listener,我们来定义它:
public interface OnItemClickListener {
operator fun invoke(forecast: Forecast)
}
如果你还记得上一课程,当被调用时invoke
方法可以被省略。所以我们来使用它来简化。listener可以被以下两种方式调用:
itemClick.invoke(forecast)
itemClick(forecast)
ViewHolder
将负责去绑定数据到新的View:
class ViewHolder(view: View, val itemClick: OnItemClickListener) :
RecyclerView.ViewHolder(view) {
private val iconView: ImageView
private val dateView: TextView
private val descriptionView: TextView
private val maxTemperatureView: TextView
private val minTemperatureView: TextView
init {
iconView = view.find(R.id.icon)
dateView = view.find(R.id.date)
descriptionView = view.find(R.id.description)
maxTemperatureView = view.find(R.id.maxTemperature)
minTemperatureView = view.find(R.id.minTemperature)
}
fun bindForecast(forecast: Forecast) {
with(forecast) {
Picasso.with(itemView.ctx).load(iconUrl).into(iconView)
dateView.text = date
descriptionView.text = description
maxTemperatureView.text = "${high.toString()}"
minTemperatureView.text = "${low.toString()}"
itemView.setOnClickListener { itemClick(forecast) }
}
}
}
现在Adapter的构造方法接收一个itemClick
。创建和绑定数据也是更简单:
public class ForecastListAdapter(val weekForecast: ForecastList,
val itemClick: ForecastListAdapter.OnItemClickListener) :
RecyclerView.Adapter<ForecastListAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
ViewHolder {
val view = LayoutInflater.from(parent.ctx)
.inflate(R.layout.item_forecast, parent, false)
return ViewHolder(view, itemClick)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bindForecast(weekForecast[position])
}
...
}
如果你使用了上面这些代码,parent.ctx
不会被编译成功。Anko提供了大量的扩展函数来让Android编程更简单。举个例子,activitys、fragments以及其它包含了ctx
这个属性,通过ctx
这个属性来返回context,但是在View中缺少这个属性。所以我们要创建一个新的名叫ViewExtensions.kt
文件来代替ui.utils
,然后增加这个扩展属性:
val View.ctx: Context
get() = context
从现在开始,任何View都可以使用这个属性了。这个不是必须的,因为你可以使用扩展的context属性,但是我觉得如果我们使用ctx
的话在其它类中也会更有连贯性。而且,这是一个很好的怎么去使用扩展属性的例子。
最后,MainActivity调用setAdapter,最后结果是这样的:
forecastList.adapter = ForecastListAdapter(result,
object : ForecastListAdapter.OnItemClickListener{
override fun invoke(forecast: Forecast) {
toast(forecast.date)
}
})
如你所见,创建一个匿名内部类,我们去创建了一个实现了刚刚创建的接口的对象。看起来不是很好,对吧?这是因为我们还没开始试使用另一个强大的函数式编程的特性,但是你将会在下一章中学习到怎么去把这些代码转换得更简单。
去代码库中更新新的代码。UI开始看起来更好了。