Anko的设计之道

Anko 是一个完全基于 Kotlin 设计的 Android 三方库,名字来自于 Android Kotlin 这两个单词的前两个字母。Anko 试图建立一套新的 Android 开发范式, 虽然不会成为主流,但是它的设计思想值得我们借鉴。

新的 UI 体系

先看一下 Anko 用于构建 UI 的几个关键类:

+--------------+
| ViewManaager |
+-------.------+
       /|\
        |
+---------------+
|  AnkoContext  |<--------------+
+-------.-------+               |
       /|\                      |
        |                       |
+-----------------+   +-----------------------+
| AnkoContextImpl |   | DelegatingAnkoContext |
+-------.---------+   +-----------------------+
       /|\
        |
+---------------------+
| ReusableContextImpl |
+---------------------+

这里需要强调的是,AnkoContext 继承自 ViewManager,而不是 android.content.Context。刚开始读源码时,总会觉得 AnkoContext 的命名有点反直觉,但是,就像文章一开始所说的——“Anko 试图建立一套新的 Android 开发范式” ——其实 AnkoContextandroid.content.Context 之间是并列关系。

就像 Android 需要基于 Context 来创建一个 View 一样,Anko 创建 UI 组件也需要基于 AnkoContext

既然 AnkoContextandroid.content.Context 是并列关系,那么大部分为 android.content.Context 定义扩展的地方也定义了 AnkoContext 的扩展。例如 Dimensions.kt 文件:

1
2
3
4
fun Context.dip(value: Int): Int = (value * resources.displayMetrics.density).toInt()
inline fun AnkoContext<*>.dip(value: Int): Int = ctx.dip(value)
inline fun View.dip(value: Int): Int = context.dip(value)
inline fun Fragment.dip(value: Int): Int = activity.dip(value)

Anko 的 UI 组件用一个接口 AnkoComponent 来表示,接口内提供了一个模板方法 createView

1
2
3
interface AnkoComponent<in T> {
fun createView(ui: AnkoContext<T>): View
}

可以看出,AnkoComponent 的类型参数(type parameter)是一个逆变的声明处变型(declaration-site variance),模板方法 createView 的作用是基于 AnkoContext<T> 实例创建一个 View

AnkoContext 继承自 ViewManager,也是一个接口,它“内藏”了 Android 的 android.content.Context 实例,同时还定义了一个类型为 T 的属性 owner 用于表示 AnkoContext 的拥有者。AnkoContext 没有对类型参数 T 加以限定,任何类型都可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
interface AnkoContext<out T> : ViewManager {
val ctx: Context
val owner: T
val view: View

override fun updateViewLayout(view: View, params: ViewGroup.LayoutParams) {
throw UnsupportedOperationException()
}

override fun removeView(view: View) {
throw UnsupportedOperationException()
}

companion object {
fun create(ctx: Context, setContentView: Boolean = false): AnkoContext<Context>
= AnkoContextImpl(ctx, ctx, setContentView)

fun createReusable(ctx: Context, setContentView: Boolean = false): AnkoContext<Context>
= ReusableAnkoContext(ctx, ctx, setContentView)

fun <T> create(ctx: Context, owner: T, setContentView: Boolean = false): AnkoContext<T>
= AnkoContextImpl(ctx, owner, setContentView)

fun <T> createReusable(ctx: Context, owner: T, setContentView: Boolean = false): AnkoContext<T>
= ReusableAnkoContext(ctx, owner, setContentView)

fun <T: ViewGroup> createDelegate(owner: T): AnkoContext<T> = DelegatingAnkoContext(owner)
}
}

AnkoContext 还通过伴生对象(companion object)提供了五个工厂方法,它们生产出来的对象都是 AnkoContext 的子类,上面的类关系图已经展示出了它们之间的关系。

AnkoContextImplAnkoContext 接口的具体实现,覆写了 ViewManager#addView 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
override fun addView(view: View?, params: ViewGroup.LayoutParams?) {
if (view == null) return

if (myView != null) {
alreadyHasView()
}

this.myView = view

if (setContentView) {
doAddView(ctx, view)
}
}

我们来分析一下 addView 的具体实现:

参数 view: View? 是一个可空类型,但是 AnkoContext 的属性 view: View 是一个非可空类型,所以 AnkoContextImpl 重新定义了一个属性 var myView: View? 用于存储参数 view: View?

1
2
3
4
private var myView: View? = null

override val view: View
get() = myView ?: throw IllegalStateException("View was not set previously")

AnkoContextImpl 的构造方法多了一个属性参数 setContentView: Boolean,它表示是否要把 addView 的参数 view 设置为属性 ctx: Context 的 content view:

1
2
3
4
5
6
7
private fun doAddView(context: Context, view: View) {
when (context) {
is Activity -> context.setContentView(view)
is ContextWrapper -> doAddView(context.baseContext, view)
else -> throw IllegalStateException("Context is not an Activity, can't set content view")
}
}

ReusableAnkoContext 继承自 AnkoContextImpl,两者唯一的不同点在于 ReusableAnkoContextalreadHasView 是一个空实现,而 AnkoContextImpl 会抛出一个异常,它不支持 view 复用。

DelegatingAnkoContextAnkoContext 的另一个实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
internal class DelegatingAnkoContext<T: ViewGroup>(override val owner: T): AnkoContext<T> {
override val ctx: Context = owner.context
override val view: View = owner

override fun addView(view: View?, params: ViewGroup.LayoutParams?) {
if (view == null) return

if (params == null) {
owner.addView(view)
} else {
owner.addView(view, params)
}
}
}

DelegatingAnkoContext 的类型参数指定了 upper bound —— <T: ViewGroup>,也就是说 DelegatingAnkoContextowner 必须是 ViewGroup 或者它的子类。

关于 DelegatingAnkoContext 名字中的 Delegating 应该是为了表达属性 view: View 代理了(delegating)owner: T

1
override val view: View = owner

再来回看一下这几个类的继承关系:

  • AnkoContext 继承自 ViewManager
  • AnkoContextImplDelegatingAnkoContext 实现了 AnkoContext
  • ReusableContextImpl 继承自 AnkoContextImpl
+--------------+
| ViewManaager |
+-------.------+
       /|\
        |
+---------------+
|  AnkoContext  |<--------------+
+-------.-------+               |
       /|\                      |
        |                       |
+-----------------+   +-----------------------+
| AnkoContextImpl |   | DelegatingAnkoContext |
+-------.---------+   +-----------------------+
       /|\
        |
+---------------------+
| ReusableContextImpl |
+---------------------+

基于 DSL 实现 UI 布局

DSL 可以用于取代手写 XML 布局,其优势如下:

  • 类型安全:编译阶段就能够发现类型错误,而且不会出现 NPE
  • 代码复用:可封装 View 的创建逻辑
  • 性能提升:节省了 inflate 的运行开销

先来看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
verticalLayout {
padding = dip(32)

imageView(android.R.drawable.ic_menu_manage).lparams {
margin = dip(16)
gravity = Gravity.CENTER
}

val name = editText {
hintResource = R.string.name
}
val password = editText {
hintResource = R.string.password
inputType = TYPE_CLASS_TEXT or TYPE_TEXT_VARIATION_PASSWORD
}

button("Log in") {
onClick {
ui.owner.tryLogin(ui, name.text, password.text)
}
}

myRichView()
}.applyRecursively(customStyle)

在 Anko 新的 UI 体系中,AnkoComponent 表示一个 UI 组件,模板方法 createView 返回 View 实例,上面的代码即可作为 createView 的函数内容,返回值是一个 vertical 的 LinearLayout

加上 AnkoComponent 把上面的代码补全:

1
2
3
4
5
6
7
8
9
10
11
12
13
class MainActivityUi : AnkoComponent<MainActivity> {
private val customStyle = { v: Any ->
when (v) {
is Button -> v.textSize = 26f
is EditText -> v.textSize = 24f
}
}
override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {
relativeLayout {
// ...
}.applyRecursively(customStyle)
}
}

我们来分析一下 relativeLayout 如何成为 ui: AnkoContext<MainActivity> 的 content view。

首先,relativeLayoutViewManager 的扩展函数,返回值是一个 LinearLayout

1
2
3
inline fun ViewManager.verticalLayout(theme: Int = 0, init: (@AnkoViewDslMarker _LinearLayout).() -> Unit): LinearLayout {
return ankoView(`$$Anko$Factories$CustomViews`.VERTICAL_LAYOUT_FACTORY, theme, init)
}

然后,它会调用 ViewManager 的另一个扩展函数——ankoView

1
2
3
4
5
6
7
inline fun <T : View> ViewManager.ankoView(factory: (ctx: Context) -> T, theme: Int, init: T.() -> Unit): T {
val ctx = AnkoInternals.wrapContextIfNeeded(AnkoInternals.getContext(this), theme)
val view = factory(ctx)
view.init()
AnkoInternals.addView(this, view)
return view
}

ankoView 会调用 AnkoInternals#addView

1
2
3
4
5
fun <T : View> addView(manager: ViewManager, view: T) = when (manager) {
is ViewGroup -> manager.addView(view)
is AnkoContext<*> -> manager.addView(view, null)
else -> throw AnkoException("$manager is the wrong parent")
}

也就是说,verticalLayout 函数会把它所创建的 LinearLayout 作为 child view 添加到接收者(receiver)中,这里是 ViewManager

再回到 AnkoComponentcreateView

1
2
3
4
5
override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {
verticalLayout {
// ...
}
}

with(ui) 会把 ui: AnkoContext<MainActivity> 作为第二个参数(用大括号表示的高阶函数)的 receiver,也就是说 高阶函数内的 this 会指向变量 ui。这样的话,AnkoInternals.addView(this, view) 会调用到 AnkoContextaddView 方法,具体如何处理取决于接口 AnkoContext 的具体实现。

下面举例来展示它们的用法。

自定义 View

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class RichView : LinearLayout {
private lateinit var image: ImageView
private lateinit var text: TextView

private fun init() = AnkoContext.createDelegate(this).apply {
gravity = CENTER
padding = dip(24)

image = imageView(imageResource = R.drawable.kotlin) {
onClick { startAnimation() }

padding = dip(8)
layoutParams = LinearLayout.LayoutParams(dip(48), dip(48))
}

text = textView("Anko Rich view") {
textSize = 19f
}

startAnimation()
}

// ...
}

AnkoContext.createDelegate 创建了一个 DelegatingAnkoContext,它的 owner 和 view 都是 this,也就说扩展函数 imagetext 所创建的 view 都会作为 child view 添加给 this

Activity 的 content view

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val adapter = ProverbAdapter(this, proverbs)
MainActivityUI(adapter).setContentView(this)
}
}

class MainActivityUI(private val adapter: ProverbAdapter) : AnkoComponent<MainActivity> {
override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {
relativeLayout {
recyclerView {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false).apply {
adapter = this@MainActivityUI.adapter
}
}.lparams(width = matchParent, height = matchParent)
}.apply {
layoutParams = FrameLayout.LayoutParams(matchParent, matchParent).apply {
padding = dip(16)
}
}
}
}

MainActivityUIAnkoComponent<MainActivity> 的子类,类型实参 <MainActivity> 表示 AnkoContextowner 的类型。

创建完 MainActivityUI 的实例之后,用法就变得非常简单,直接调用它的扩展函数 setContentView

1
MainActivityUI(adapter).setContentView(this)

setContentView 会创建一个 AnkoContextImpl 类型的实例,然后把这个实例作为参数调用 createView

1
2
fun <T : Activity> AnkoComponent<T>.setContentView(activity: T): View =
createView(AnkoContextImpl(activity, activity, true))

RecyclerView.ViewHolder

首先用 AnkoComponent 定义 item 的 UI 组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ProverbComponent(val ui: AnkoContext<ProverbAdapter>) : AnkoComponent<ProverbAdapter> {
lateinit var category: TextView
lateinit var title: TextView

fun createView() = createView(ui)

override fun createView(ui: AnkoContext<ProverbAdapter>) = with (ui) {
linearLayout {
category = textView {
textColor = Color.RED
}

textView {
text = ": "
}

title = textView {
textColor = Color.BLUE
}
}
}
}

然后用 ViewHolder 来 hold 这个组件。

1
class ViewHolder(val ankoComponent: ProverbComponent) : RecyclerView.ViewHolder(ankoComponent.createView())

创建组件所使用的 AnkoContextReusableAnkoContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ProverbAdapter(context: Context, private val items: List<Proverb>) : RecyclerView.Adapter<ViewHolder>() {
private val ankoContext: AnkoContext<ProverbAdapter> = AnkoContext.createReusable(context, this)

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.ankoComponent.category.text = items[position].category
holder.ankoComponent.title.text = items[position].phrase
}

override fun getItemCount(): Int {
return items.size
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(ProverbComponent(ankoContext))
}
}

总结

Anko 这种基于 DSL 的布局方式可以算得上是一股清流,虽然在执行效率上面有所提升,但是从开发效率和分工协作的角度来看,DSL 算不上是一种高效的方式。

我们都知道,用 Sketch 画 UI 肯定比手写代码的方式效率高,无论是 Android 还是 iOS,UI 的布局方式都在朝着“拖拽”的方向演进,比如 ConstraintLayoutAutoLayout,设计稿直接转换为 XML 布局文件也已经指日可待。除此之外,逻辑代码和布局代码混在一起的方式也不便于代码维护。

UI 布局只是 Anko 的功能之一,有时间了再分析一下 Anko Coroutines 的实现。

Google I/O 2018 看点

Google I/0 2018 马上就要开始了,美国加利福尼亚州山景城时间 5 月 8 日上午 10 点,对应北京时间是 5 月 9 日凌晨 1 点。

本来今年打算报名现场的,因其它事情花了一笔钱,经费紧张,遂放弃,只能看直播了。

Google I/O Extended 2018 Hangzhou 报名链接: https://www.meetup.com/Hangzhou-GDG/events/249858297/

我根据 topics 整理了一个 session 的脑图,格式是 svg 格式,可以无限放大,新 Tab 中打开也可以查看大图。

内容脑图

兴趣主题

Machine Learning & AI

毫无疑问,今年的主题依然是 AI。RISC 发明人、图领奖得主 John Hennsessy 会做主题为《The future of computing》的 Keynote。Fei-Fei Li 也会露面,她的 Keynote 是《Building the future of artificial intelligence for everyone》,值得期待。

TensorFlow 大大降低了 Machine Learning 的入门门槛,基于它,没有读过 PhD 的人也能够做一些 AI 相关的工作。

物联网 IoT

IoT 方面,除了一如至往的 Android Things,又多了一个技术栈—— OpenThread,它为低功耗(low-power)的 IoT 设备带来了 Internet。

IoT 产品也可以基于 Google Cloud Platform 来开发,叫 Cloud IoT。

终端设备用于收集数据,再加上机器学习的运用,一定会创造出意想不到的产品体验,这是我们的机会。

Machine learning models + IoT data = a smarted world

突然想起了以前在 fujitsu 工作时墙上的 slogon:Shaping tomorrow with you。

Firebase

Firebase 为 Mobile、Web 提供了全套解决方案,基于它我们可以快速高效地部署应用,它是 Google 云计算的产品之一,背后依托于强大的 Google Cloud Platform。

Google Cloud Platform

云产品的基石是 Google Cloud Platform ,Firebase、Cloud TPU、Cloud IoT 等都是基于 GCP 为开发者提供服务。

Android

Android 内容不少,Googler 会传授很多开发姿势给我们,还有一些 OS 层的原理,比如 ART、Rendering。当然也少不了 Architecture Components、RecycerView、ConstraintLayout、Support Library 这些能够着实提高开发效率的 SDK。

Kotlin 的内容不多,只有两个 session,一个是 Kotlin 语言的设计者教你怎么写代码,另一个是 Jake 大神加盟 Google 之后开源的 Android KTX

Web

相比于 Android 来说,Web 出现了很多新技术,旨在打造一个摩登时代,叫做 Modern Web。

例如,注册登录相关的 WebAuthnOne-tap Sign-upreCAPCHA V3,第一个 secure-only 的顶级域名 .app,无 UI 的 Headless Chrome,PWA、AMP 等等,很有意思,值得学习。

Flutter

Flutter 可以快速构建一个跨平台、响应式的 App。本次 IO 大会上,与 Material Design 结合得比较紧密。

总结

对于一般人来说,知易行难,仅仅了解这些技术是远远不够的,只有大量的工程实践才能慢慢转化为自己的内功,这种内功才能为未来的技术决策提供强大的能量。

Kotlin网络库Fuel的设计之道

使用场景

一个“朴素”的 url 完全可以用一个字符串来表示(例如 "https://www.youzan.com"),我们可以利用 Kotlin 语言本身的特性为 String 类型添加一个扩展函数 httpGet(),然后借此发起 http 请求:

1
"https://www.youzan.com".httpGet()

但是,对于不是朴素字符串的对象来说,我们可以让其实现一个接口:

1
2
3
interface PathStringConvertible {
val path: String
}

然后,将“计算”过后的 path 通过一个 String 类型提供出来,例如:

1
2
3
4
5
6
7
8
9
enum class HttpsBin(relativePath: String) : Fuel.PathStringConvertible {
USER_AGENT("user-agent"),
POST("post"),
PUT("put"),
PATCH("patch"),
DELETE("delete");

override val path = "https://httpbin.org/$relativePath"
}

但是,也会存在一种情况,所有的 url 可能会共享一个 base url,或者是其他公用参数,那么还需有一个地方来存储这些通用配置,这个地方的幕后老大就叫 FuelManager

StringPathStringConvertible 最终也会调用到 FuelManager

+----------+
|  String  |------------->----+
+----------+                  |    +------+    +-------------+
                              |--->| Fuel |--->| FuelManager |
+-------------------------+   |    +------+    +-------------+
|  PathStringConvertible  |->-+
+-------------------------+

除了通过 String 或者 PathStringConvertiable 来发起请求,我们还可以直接用一个 Request,因此 Fuel 还提供了转换 Request 的接口:

1
2
3
interface RequestConvertible {
val request: Request
}

综上来看,发起一个 http 请求可以有如下四种方式:
0. 一个字符串
0. PathStringConvertible 变量
0. RequestConvertible 变量
0. 直接使用 Fuel 伴生对象提供的方法

代码实现

对外提供服务的 Fuel

首先 Fuel 作为对外的接口提供方(类似 Facade 模式),通过一个伴生对象(companion object)提供服务(以 get 方法为例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
companion object {
@JvmStatic @JvmOverloads
fun get(path: String, parameters: List<Pair<String, Any?>>? = null): Request =
request(Method.GET, path, parameters)

@JvmStatic @JvmOverloads
fun get(convertible: PathStringConvertible, parameters: List<Pair<String, Any?>>? = null): Request =
request(Method.GET, convertible, parameters)

private fun request(method: Method, path: String, parameters: List<Pair<String, Any?>>? = null): Request =
FuelManager.instance.request(method, path, parameters)

private fun request(method: Method, convertible: PathStringConvertible, parameters: List<Pair<String, Any?>>? = null): Request =
request(method, convertible.path, parameters)
}

Fuel 类通过伴生对象提供的 http 方法有 get/post/put/patch/delete/download/upload/head,这些方法最终会路由到 FuleManager 的实例(instance)。

同时,Fule.kt 源文件为 StringPathStringConvertible 定义了扩展,以支持这些 http 方法(以 get 方法为例):

1
2
3
4
5
@JvmOverloads
fun String.httpGet(parameters: List<Pair<String, Any?>>? = null): Request = Fuel.get(this, parameters)

@JvmOverloads
fun Fuel.PathStringConvertible.httpGet(parameter: List<Pair<String, Any?>>? = null): Request = Fuel.get(this, parameter)

幕后老大 FuleManager

FuleManager 利用伴生对象实现了单例模式:

1
2
3
4
companion object {
//manager
var instance by readWriteLazy { FuelManager() }
}

同时利用代理属性实现了单例的懒加载。

readWriteLazy 是一个函数,它的返回值是一个 ReadWriteProperty,代码比较容易,具体可见 Delegates.kt

也就是说,当我们第一次访问 FuelManager 时,一个具体的实例会被创建出来,这个实例担负了存储公用配置和发起请求的重任,首先来看它的属性:

1
2
3
4
5
6
7
8
9
10
11
var client: Client
var proxy: Proxy?
var basePath: String?

var baseHeaders: Map<String, String>?
var baseParams: List<Pair<String, Any?>>

var keystore: KeyStore?
var socketFactory: SSLSocketFactory

var hostnameVerifier: HostnameVerifier

Client 是一个接口,通过它我们可以自定义 http 引擎。

1
2
3
interface Client {
fun executeRequest(request: Request): Response
}
+---------+     +--------+     +----------+
| Request | ==> | Client | ==> | Response |
+---------+     +--------+     +----------+
                     |
                    \|/                   +--------------------+
              +------------+              | HttpURLConnection  |
              | HttpClient | --based on-- +--------------------+
              +------------+              | HttpsURLConnection |
                                          +--------------------+

Fuel 默认提供的 Http 引擎是 HttpClient,它是基于 HttpURLConnection 的实现。

basePathbaseHeadersbaseParams 存储了请求的公用配置,我们可以通过 FuleManager.instance 为其赋值:

1
2
3
4
5
FuelManager.instance.apply {
basePath = "http://httpbin.org"
baseHeaders = mapOf("Device" to "Android")
baseParams = listOf("key" to "value")
}

keystore 用于构建 socketFactory,再加上 hostnameVerifier,它们用于 https 请求,在 HttpClient 中有用到:

1
2
3
4
5
6
7
8
9
10
11
12
private fun establishConnection(request: Request): URLConnection {
val urlConnection = if (proxy != null) request.url.openConnection(proxy) else request.url.openConnection()
return if (request.url.protocol == "https") {
val conn = urlConnection as HttpsURLConnection
conn.apply {
sslSocketFactory = request.socketFactory // socketFactory
hostnameVerifier = request.hostnameVerifier // hostnameVerifier
}
} else {
urlConnection as HttpURLConnection
}
}

如果要深入了解 HTTPS 证书,可参考 「HTTPS 精读之 TLS 证书校验」。

FuelManager 在发起请求时会用这些参数构建一个 Request

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fun request(method: Method, path: String, param: List<Pair<String, Any?>>? = null): Request {
val request = request(Encoding(
httpMethod = method,
urlString = path,
baseUrlString = basePath,
parameters = if (param == null) baseParams else baseParams + param
).request)

request.client = client
request.headers += baseHeaders.orEmpty()
request.socketFactory = socketFactory
request.hostnameVerifier = hostnameVerifier
request.executor = createExecutor()
request.callbackExecutor = callbackExecutor
request.requestInterceptor = requestInterceptors.foldRight({ r: Request -> r }) { f, acc -> f(acc) }
request.responseInterceptor = responseInterceptors.foldRight({ _: Request, res: Response -> res }) { f, acc -> f(acc) }
return request
}

关于 requestInterceptorresponseInterceptor,原理与 OkHttp 实现的拦截器一致,只不过这里利用了 Kotlin 的高阶函数,代码实现非常简单,具体细节可参考 「Kotlin实战之Fuel的高阶函数」。

跟其他网络库一样,一次完整的请求,必然包含两个实体—— Request & Response,先来看 Request

请求实体 Request

1
2
3
4
5
6
7
8
9
10
11
12
class Request(
val method: Method,
val path: String,
val url: URL,
var type: Type = Type.REQUEST,
val headers: MutableMap<String, String> = mutableMapOf(),
val parameters: List<Pair<String, Any?>> = listOf(),
var name: String = "",
val names: MutableList<String> = mutableListOf(),
val mediaTypes: MutableList<String> = mutableListOf(),
var timeoutInMillisecond: Int = 15000,
var timeoutReadInMillisecond: Int = timeoutInMillisecond) : Fuel.RequestConvertible

它支持三种类型的请求:

1
2
3
4
5
enum class Type {
REQUEST,
DOWNLOAD,
UPLOAD
}

针对每个类型都有对应的任务(task):

1
2
3
4
5
6
7
8
//underlying task request
internal val taskRequest: TaskRequest by lazy {
when (type) {
Type.DOWNLOAD -> DownloadTaskRequest(this)
Type.UPLOAD -> UploadTaskRequest(this)
else -> TaskRequest(this)
}
}

涉及到上传下载的 DownloadTaskRequestUploadTaskRequest 都继承自 TaskRequest,它们会处理文件和流相关的东西,关于此可参考 IO 哥写的 一些「流与管道」的小事 以及 OK, IO

FuelManager 在构造 Request 时用到了一个类——Encoding

1
2
3
4
5
6
class Encoding(
val httpMethod: Method,
val urlString: String,
val requestType: Request.Type = Request.Type.REQUEST,
val baseUrlString: String? = null,
val parameters: List<Pair<String, Any?>>? = null) : Fuel.RequestConvertible

Encoding 也是继承自 Fuel.RequestConvertible,它完成了对 Request 参数的组装编码,并产生了一个 Request

Encoding 组装 query parameter 的方式可以说赏心悦目,贴出来欣赏一下:

1
2
3
4
private fun queryFromParameters(params: List<Pair<String, Any?>>?): String = params.orEmpty()
.filterNot { it.second == null }
.map { (key, value) -> URLEncoder.encode(key, "UTF-8") to URLEncoder.encode("$value", "UTF-8") }
.joinToString("&") { (key, value) -> "$key=$value" }

请求返回结果 Response

1
2
3
4
5
6
7
class Response(
val url: URL,
val statusCode: Int = -1,
val responseMessage: String = "",
val headers: Map<String, List<String>> = emptyMap(),
val contentLength: Long = 0L,
val dataStream: InputStream = ByteArrayInputStream(ByteArray(0))

Response 的属性可以看出,它所携带的仍然是一个流(Stream),我们先看 Response 是如何与 Request 串联起来的。

Deserializable.kt 文件为 Request 定了名称为 response 的扩展函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private fun <T : Any, U : Deserializable<T>> Request.response(
deserializable: U,
success: (Request, Response, T) -> Unit,
failure: (Request, Response, FuelError) -> Unit): Request {

val asyncRequest = AsyncTaskRequest(taskRequest)

asyncRequest.successCallback = { response ->
val deliverable = Result.of { deserializable.deserialize(response) }
callback {
deliverable.fold({
success(this, response, it)
}, {
failure(this, response, FuelError(it))
})
}
}

asyncRequest.failureCallback = { error, response ->
callback {
failure(this, response, error)
}
}

submit(asyncRequest)
return this
}

扩展函数 response 的参数中,deserializable 负责反序列化操作,successfailure 用于处理请求结果。

Fuel 提供了两个 Deserializable 的实现:StringDeserializer 以及 ByteArrayDeserializer,它们用于反序列化 response 的 stream。

异步请求

Deserializable.ktRequest 定义的扩展函数 response 在执行异步操作时用到了一个 AsnycTaskRequest,其实它本身并不提供异步实现,而是交由一个 ExecutorService 去执行,而这个 ExecutorService 恰由 FuelManager 定义,并在构造 Request 时传入给它。

FuleManager.kt

1
2
3
4
5
6
7
8
9
//background executor
var executor: ExecutorService by readWriteLazy {
Executors.newCachedThreadPool { command ->
Thread(command).also { thread ->
thread.priority = Thread.NORM_PRIORITY
thread.isDaemon = true
}
}
}

AsyncTaskRequestUploadTaskRequestDownloadTaskRequest 一样,都是继承自 TaskRequest,只不过它多了两个异步调用的回调:

1
2
var successCallback: ((Response) -> Unit)? = null
var failureCallback: ((FuelError, Response) -> Unit)? = null

请求图例

至此,请求、回复,异步调用,对外接口都了解过了,一个基本的网络库框架已经成型。

         +------------------------+
         | https://www.youzan.com |
         +------------------------+
                     |
                     |
                    \|/
                  +------+
                  | Fuel |
                  +------+
                     |
                     |
                    \|/
              +-------------+
              | FuelManager |
              +-------------+
                     |
                     |
                    \|/
+---------+      +--------+      +----------+
| Request | ===> | Client | ===> | Response |
+---------+      +--------+      +----------+

虽然Fuel 的复杂度不可与 OkHttp 相提并论,但是依赖 Kotlin 语言本身的灵活性,它的代码却比 OkHttp 要简洁的多,特别是关于高阶函数和扩展函数的运用,极大地提升了代码的可读性。

参考资料

Kotlin实战之Fuel的高阶函数

Fuel 是一个用 Kotlin 写的网络库,与 OkHttp 相比较,它的代码结构比较简单,但是它的巧妙之处在于充分利用了 Kotlin 的语言特性,所以代码看上去干净利落。

OkHttp 使用了一个 interceptor chain 来实现拦截器的串联调用,由于 Java 语言( JDK ≤ 7)本身的局限性,所以实现代码比较臃肿,可读性也不友好。当然,RxJava 再加上 retrolambda 这种 backport 的出现,一定程度上了缓解了这种尴尬,但是 Kotlin 天生具备的声明式写法又使得 Java 逊色了很多。

我们知道,拦截器本质上是一个责任链模式(chain of responsibility)的实现,我们通过具体代码来学习一下 Kotlin 究竟是如何利用高阶函数实现了拦截器功能。

首先定义一个 MutableList 用于存储拦截器实例:

1
2
3
val requestInterceptors: 
MutableList<((Request) -> Request) -> ((Request) -> Request)>
= mutableListOf()

注意,Kotlin 的类型系统明确区分了 mutable 和 immutable,默认的 List 类型是 immutable。

requestInterceptors 的元素类型是一个高阶函数

1
((Request) -> Request) -> ((Request) -> Request)

作为元素类型的高阶函数,其参数也是一个高阶函数 (Request) -> Request, 同时,返回值也是高阶函数 (Request) -> Request

然后,我们给 requestInterceptors 定义一个增加元素的方法:

1
2
3
4
fun addRequestInterceptor(
interceptor: ((Request) -> Request) -> ((Request) -> Request)) {
requestInterceptors += interceptor
}

addRequestInterceptor 的参数类型

1
(Request) -> Request) -> ((Request) -> Request)

requestInterceptors 的元素类型一致。

注意,这里又出现了一个 Kotlin 有而 Java 没有的语言特性:操作符重载。

我们没有调用 requestInterceptors.add(interceptor),而是用了一个 plusAssign 的操作符 +=(MutableCollections.kt 中定义的操作符重载):

1
2
3
4
5
6
7
/**
* Adds the specified [element] to this mutable collection.
*/
@kotlin.internal.InlineOnly
public inline operator fun <T> MutableCollection<in T>.plusAssign(element: T) {
this.add(element)
}

那么,此时应该定义一个拦截器的函数实例了:

1
2
3
4
5
6
7
fun <T> loggingRequestInterceptor() =
{ next: (T) -> T ->
{ t: T ->
println(t.toString())
next(t)
}
}

loggingRequestInterceptor 是一个函数,它的返回值是一个 lambda 表达式(即高阶函数):

1
2
3
4
5
6
{ next: (T) -> T ->
{ t: T ->
println(t.toString())
next(t)
}
}
  1. 这个 lambda 的参数next: (T) -> T(参数名是 next,参数类型是 (T) -> T),返回值是另一个 lambda 表达式:
1
2
3
4
{ t: T ->
println(t.toString())
next(t)
}
  1. 因为 lambda 本身是一个函数字面量(function literal),它的类型通过函数本身可以推到得出,如果我们用一个变量来引用这个 lambda 的话,变量的类型是 (T) -> T

由1、2两点可知,loggingRequestInterceptor() 的返回值是一个 lambda 表达式,它的参数是 (T) -> T,返回值也是 (T) -> T

这里的泛型函数略抽象,我们来看一个具体化的函数:

1
2
3
4
5
6
7
fun cUrlLoggingRequestInterceptor() =
{ next: (Request) -> Request ->
{ r: Request ->
println(r.cUrlString())
next(r)
}
}

同理,cUrlLoggingRequestInterceptor() 函数的参数为 (Request) -> Request、返回值为 (Request) -> Request

拦截器都定义好了,那么应该如何调用呢?Kotlin 一行代码搞定🤟::

1
requestInterceptors.foldRight({ r: Request -> r }) { f, acc -> f(acc) }

foldRightList 的一个扩展函数,先来看声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Accumulates value starting with [initial] value and applying [operation] from right to left to each element and current accumulator value.
*/
public inline fun <T, R> List<T>.foldRight(initial: R, operation: (T, acc: R) -> R): R {
var accumulator = initial
if (!isEmpty()) {
val iterator = listIterator(size) // 让迭代器指向最后一个元素的末尾
while (iterator.hasPrevious()) {
accumulator = operation(iterator.previous(), accumulator)
}
}
return accumulator
}

函数功能总结为一句话:从右往左,对列表中的每一个元素执行 operation 操作,每个操作的结果是下一次操作的入参,第一次 operation 的初始值是 initial

回头来看拦截器列表 requestInterceptors 如何执行了 foldRight

1
requestInterceptors.foldRight({ r: Request -> r }) { f, acc -> f(acc) }

参数 inital: R 的实参是 { r: Request -> r },一个函数字面量,没有执行任何操作,接收 r 返回 r

参数 operation: (T, acc: R) -> R 可接收一个 lambda,所以它的实参 {f, acc -> f(acc)} 可以位于圆括号之外。f 的泛型是 T,具体类型是

1
((Request) -> Request) -> ((Request) -> Request)

acc 的类型通过 initial: R 的实参 { r: Request -> r } 可以推到得出——(Request) -> Request

OK,语法完全没毛病,再来看语义。

+---------------------+
| { r: Request -> r } | ---> 初始值,命名为 *fun0*
+---------------------+
           |
           |
          \|/    fun0 作为参数传递给 requestInterceptors 最右的 f(最后一个元素)
+----------------------------------|------------------------f---------------------|-+
| cUrlLoggingRequestInterceptor(): ((Request) -> Request) -> ((Request) -> Request) |
+----------------------------------|----------------------------------------------|-+
           |
           |                  f 返回结果:
           |                  +-----------------------------+
           |                  | { r: Request ->             |
           |                  |     println(r.cUrlString()) |
           |                  |     fun0(r)                 |
           |                  | }                           |
           |                  +-----------------------------+
           |                                    命名为 *fun1*
           |  
          \|/   fun1 作为参数,传递给倒数第二个 f
+----------------------------------|-----------------------f--------------------|-+
| loggingRequestInterceptor(): ((Request) -> Request) -> ((Request) -> Request)   |
+----------------------------------|--------------------------------------------|-+
           |
           |                  f 返回结果:
           |                  +-----------------------------+
           |                  | { r: Request ->             |
           |                  |     println(1.toString())   |
           |                  |     fun1(r)                 |
           |                  | }                           |
           |                  +-----------------------------+
           |                                    命名为 *fun2*
          \|/   将 fun2 解体:
+------------------------------+
| { r: Request ->              |
|     println(r.toString())    |
|     println(r.cUrlString())  | 类型为:(Request) -> request
|     r                        |
| }                            |
+------------------------------+

至此,一个简单的拦截器功能就实现了,代码竟然如此简洁,感动!

参考

「译」精通Kotlin标准函数:run、with、let、also和apply

原文地址:https://medium.com/@elye.project/mastering-kotlin-standard-functions-run-with-let-also-and-apply-9cd334b0ef84

一些 Kotlin 的标准函数非常相似,以至于我们都无法确定要使用哪一个。这里我会介绍一种简单的方式来区分他们的不同点以及如何选择使用。

作用域函数

接下来聚焦的函数有:runwithT.runT.letT.also 以及 T.apply。我称他们为作用域函数(scoping functions),因为它们为调用方函数提供了一个内部作用域。

最能够体现作用域的是 run 函数:

1
2
3
4
5
6
7
8
9
10
fun test() {
var mode = "I am sad"

run {
val mood = "I am happy"
println(mood) // I am happy
}

println(mood) // I am sad
}

基于此,在 test 函数内部,你可以拥有一个单独的区域,在这个作用域内,mood 在打印之前被重新定义成了 I am happy,并且它完全被包裹(enclosed)在 run 的区域内。

这个作用域函数本身看起来并不会非常有用。但是除了拥有单独的区域之外,它还有另一个优势:它有返回值,即区域内的最后一个对象。

因此,下面的代码会变得整洁,我们把 show() 函数应用到两个 view 之上,但是并不需要调用两次。

1
2
3
run {
if (firstTimeView) introView else normalView
}.show()

这里演示所用,其实还可以简化为 (if (firstTimeView) introView else normalView).show()

作用域函数三大特性

为了让作用域函数更有意思,可将其行为分类为三大特性。我会使用这些特性来区分彼此。

一、正常 vs. 扩展函数

如果我们看一下 withT.run,会发现它们的确非常相似。下面的代码做了同样的事情。

1
2
3
4
5
6
7
8
9
10
11
with(webview.settings) {
javaScriptEnabled = true
databaseEnabled = true
}

// similarly

webview.settings.run {
javaScriptEnabled = true
databaseEnabled = true
}

但是,它们的不同点在于,一个是正常函数(即 with),另一个是扩展函数(即 T.run)。

假设 webview.settings 可能为空,那么代码就会变成下面的样子:

1
2
3
4
5
6
7
8
9
10
11
// Yack!
with(webview.settings) {
this?.javaScriptEnabled = true
this?.databaseEnabled = true
}

// Nice
webview.settings?.run {
javaScriptEnabled = true
databaseEnabled = true
}

在这个案例中,T.run 的扩展函数明显要好一些,因为我们可以在使用前就做好了空检查。

二、this vs. it 参数

如果我们看一下 T.runT.let,会发现两个函数是相似的,只有一点不同:它们接收参数的方式。下面代码展示了用两个函数实现同样的逻辑:

1
2
3
4
5
6
7
8
9
stringVariable?.run {
println("The length of this String is $length")
}

// Similarly

stringVariable?.let {
println("The length of this String is ${it.length}")
}

如果检查一下 T.run 的函数签名就会发现 T.run 只是一个调用 block: T.() 的扩展函数。因此在它的作用域内,T 可以被引用为 this。实际编程中,this 大部分情况下都可以被省略。因此,在上面的例子中,我们可以在 println 的声明语句中使用 $length 而不是 ${this.length}。我把它称之为:this 作为参数进行传递。

但是,对于 T.let 函数,你会发现 T.let 把它自己传入了函数 block: (T)。因此它被当做一个 lambda 参数来传递。在作用域函数内它可以被引用为 it。所以我称之为:it 作为参数进行传递。

从上面可以看出,T.run 好像比 T.let 高级,因为它更隐式一些,但是 T.let 函数会有些一些微妙的优势:

  • T.let 可以更清楚地区分所得变量和外部类的函数/成员。

  • this 不能被省略的情况下,例如用作一个函数参数,itthis 更短更清晰。

  • T.let 允许用更好的命名来表示转换过的所用变量(the converted used variable),也就是说,你可以把 it 转换为其他名字:

    1
    2
    3
    4
    stringVariable?.let {
    nonNullString ->
    println("The non null string is $nonNullString")
    }

三、返回 this vs. 其他类型

现在,我们看一下 T.letT.also,如果我们看一下函数作用域内部的话,会发现两者是一样的:

1
2
3
4
5
6
7
8
9
stringVariable?.let {
println("The length of this String is ${it.length}")
}

// Exactly the same as below

stringVariable?.also {
println("The length of this String is ${it.length}")
}

但是,它们微妙的区别之处在于返回了什么。T.let 返回了一个不同类型的值,但是 T.also 返回了 T 自身,也就是 this

简单的示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
val original = "abc"

// Evolve the value and send to the next chain
original.let {
println("The original String is $it") // "abc"
it.reversed() // evolve it as parameter to send to next let
}.let {
println("The reverse String is $it") // "cba"
it.length // can be evolve to other type
}.let {
println("The length of the String is $it") // 3
}

// Wrong
// Same value is sent in the chain (printed answer is wrong)
original.also {
println("The original String is $it") // "abc"
it.reversed() // even if we evolve it, it is useless
}.also {
println("The reverse String is ${it}") // "abc"
it.length // even if we evolve it, it is useless
}.also {
println("The length of the String is ${it}") // "abc"
}

// Corrected for also (i.e. manipulate as original string
// Same value is sent in the chain
original.also {
println("The original String is $it") // "abc"
}.also {
println("The reverse String is ${it.reversed()}") // "cba"
}.also {
println("The length of the String is ${it.length}") // 3
}

上面的 T.also 貌似没什么意义,因为我们可以轻松把它们组合进一个单一的函数块内。仔细想一下,它们会有如下优势:

  • 它可以为相同的对象提供清晰的处理流程,可以使用粒度更小的函数式部分。
  • 它可以在被使用之前做灵活的自处理(self manipulation),可以创建一个链式构造器操作。

如果两者结合链式来使用,一个进化自己,一个持有自己,就会变得非常强大,例如:

1
2
3
4
5
6
7
8
9
// Normal approach
fun makeDir(path: String): File {
val result = File(path)
result.mkdirs()
return result
}

// Improved approach
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }

回顾一下所有的特性

通过这三个特性,我们可以清楚地知道每个函数的行为。让我们举例说明一下上面没有提到的 T.apply 函数,它的 3 个特性如下所述:

  • 它是一个扩展函数
  • 它把 this 作为参数
  • 它返回了 this(它自己)
1
2
3
4
5
6
7
8
9
10
11
12
// Normal approach
fun createIntent(intentData: String, intentAction: String): Intent {
val intent = Intent()
intent.action = intentAction
intent.data = Uri.parse(intentData)
return intent
}

// Improved approach, chaining
fun createIntent(intentData: String, intentAction: String) =
Intent().apply { action = intentAction }
.apply { data = Uri.parse(intentData) }

或者我们也可以把一个非链式的对象创建过程变得可链式(chain-able):

1
2
3
4
5
6
7
8
9
10
11
12
// Normal approach
fun createIntent(intentData: String, intentAction: String): Intent {
val intent = Intent()
intent.action = intentAction
intent.data = Uri.parse(intentData)
return intent
}

// Improved approach, chaining
fun createIntent(intentData: String, intentAction: String) =
Intent().apply { action = intentAction }
.apply { data = Uri.parse(intentData) }

函数选择

现在思路变清晰了,根据这三大特性,我们可以对函数进行分类。基于此可以构建一个决策树来帮助我们根据需要来选择使用哪一个函数。

希望上面的决策树能够更清晰地阐述这些函数,同时也能简化你的决策,使你能够得当地使用这些函数。

Kotlin 中 var、val、const 关键字解析

昨天公众号后台收到一位小伙伴的留言询问,他对于 Kotlin 为何没有 Java 的 final 关键字感到困惑,这应该是很多初学者都会遇到的问题,所以我就写了这篇博文从更底层的角度来解析 Kotlin 声明变量时用到的三个关键字:varvalconst

其实,Java 的 final 就等价于 Kotlin 的 val, 虽然通过 javap 反编译可以看到两者的底层实现不一样,但是从语义上讲,它们两者的确是等价的。具体原因,我们来逐一分析。

什么是属性

我们知道,在 Kotlin 的世界中,class 已经不再是唯一的一等公民,我们可以直接在代码文件的最顶层(top-level)声明类、函数和变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Address {
// class properties
var province = "zhejiang"
val city = "hangzhou"
}

fun prettify(address: Address): String {
// local variable
val district = "xihu"
return district + ',' + address.city + ',' + address.province
}

// top-level property
val author = "liangfei"

上例中的 Address 是一个类,prettify 是一个函数,author 是一个变量,它们都是一等公民,也就是说,函数和变量可以单独存在,不会像 Java 那样依附于类。

首先,varval 可分为三种类型:

  • 类的属性(class property),例如上例中的 var province = "zhejiang",它是 Address 类的一个属性;
  • 顶层属性(top-level property),例如上例中的 val author = "liangfei",它是文件(module)的一个属性;
  • 局部变量(local variable),例如上例中的 val district = "xihu",它是函数 prettify 的一个局部变量。

类的属性和顶层属性都是属性,所以可以统一来看待,属性本身不会存储值,也就是说它不是一个字段(field),那它的值是哪里来的呢?我们先来看一下声明一个属性的完整语法:

1
2
3
var <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]

可以看出,一个属性的声明可以分解为五个部分:属性名、属性类型、initializer、getter、setter。

  • 属性的名就是就是我们用来引用属性的方式;
  • 属性的类型可以显示声明,因为 Kotlin 支持类型推导,如果类型能够从上下文推导得出,那么它也可以省略;
  • initializer 是类型推导的线索之一,例如 val author = "liangfei",根据 = "liangfei" 可以得出它是一个 String 类型;
  • getter 也是类型推导的线索之一,所有使用属性名获取值的操作,都是通过 getter 来完成的;
  • setter 用于给属性赋值。

以上只是声明了一个属性,如果我们要赋值,它的值会存储在哪里呢?其实,编译器还会自动为属性生成一个用于存储值的字段(field),因为写代码时感知不到到它的存在,所以称为幕后字段(backing field)。具体可以参考幕后字段,因为与本文关系不大,所以此处不做介绍。

varval 所声明的属性,其最本质的区别就是:**val 不能有 setter**,这就达到了 Java 中 final 的效果。

例如,上面 Kotlin 代码中的 Address 类:

1
2
3
4
class Address {
var province = "zhejiang"
val city = "hangzhou"
}

它在 JVM 平台上的实现是下面这样的(通过 javap 命令查看):

1
2
3
4
5
6
public final class Address {
public final java.lang.String getProvince();
public final void setProvince(java.lang.String);
public final java.lang.String getCity();
public Address();
}

可以看出,针对 var province 属性,生成了 getProvince()setProvince(java.lang.String) 两个函数。但是 val city 只生成了一个 getCity() 函数。

对于局部变量来说,var 或者 val 都无法生成 getter 或 setter,所以只会在编译阶段做检查。

看一下它的官方定义(中文版可参考属性和字段):

Classes in Kotlin can have properties. These can be declared as mutable, using the var keyword or read-only using the val keyword.

对于类的属性来说:var 表示可变(mutable),val 表示只读(read-only)。对于顶层属性来说也是一样的。

可变和只读

var 表示可变,val 表示只读,而不是不可变(immutable)。我们已经知道了 val 属性只有 getter,但是这并不能保证它的值是不可变的。例如,下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
var name = "liangfei"
var age = 30

val nickname: String
get() {
return if (age > 30) "laoliang" else "xiaoliang"
}

fun grow() {
age += 1
}
}

属性 nickname 的值并非不可变,当调用 grow() 方法时,它的值会从 "laoliang" 变为 "xiaoliang",但是无法直接给 nickname 赋值,也就是说,它不能位于赋值运算的左侧,只能位于右侧,这就说明了为什么它是只读(read-only),而不是不可变(immutable)。

其实,Kotlin 有专门的语法来定义可变和不可变的变量,后面会专门写一篇博问来分析,这里不再深入。

我们知道,Java 中可以使用 static final 来定义常量,这个常量会存放于全局常量区,这样编译器会针对这些变量做一些优化,例如,有三个字符串常量,他们的值是一样的,那么就可以让这个三个变量指向同一块空间。我们还知道,局部变量无法声明为 static final,因为局部变量会存放在栈区,它会随着调用的结束而销毁。

Kotlin 引入一个新的关键字 const 来定义常量,但是这个常量跟 Java 的 static final 是有所区别的,如果它的值无法在编译时确定,则编译不过,因此 const 所定义的常量叫编译时常量

编译时常量

首先,const 无法定义局部变量,除了局部变量位于栈区这个原因之外,还因为局部变量的值无法在编译期间确定,因此,const 只能修饰属性(类属性、顶层属性)。

因为 const 变量的值必须在编译期间确定下来,所以它的类型只能是 String 或基本类型,并且不能有自定义的 getter。

所以,编译时常量需要满足如下条件:

  • 顶层或者 object 的成员(object 也是 Kotlin 的一个新特性,具体可参考对象声明)。
  • 初始化为一个 String 或者基本类型的值
  • 没有自定义 getter

总结

最后,总结一下:

  • varval 声明的变量分为三种类型:顶层属性、类属性和局部变量;
  • var 属性可以生成 getter 和 setter,是可变的(mutable),val 属性只有 getter,是只读的(read-only,注意不是 immutable);
  • 局部变量只是一个普通变量,不会有 getter 和 setter,它的 val 等价于 Java 的 final,在编译时做检查。
  • const 只能修饰没有自定义 getter 的 val 属性,而且它的值必须在编译时确定。

参考资料

Sass 和 Less 的用法和比较

SassLess 是两种 CSS 预处理器,扩展了 CSS 语法,目的都是为了让 CSS 更容易维护。

Sass

Sass 有两种语法,最常用是的 SCSS(Sassy CSS),是 CSS3 的超集。另一个语法是 SASS(老的,缩进语法,类 Python)。

Sass 的预处理器工具是 sass:

1
2
3
4
5
6
7
8
# 单个文件
sass input.sass output.css

# 监控
sass --watch input.sass output.css

# 监控目录
sass --watch app/sass:public/stylesheets

Sass 扩展了 css 的特性:

  • 变量
  • 嵌套
  • 混合(mixin)
  • 继承

变量

1
2
3
4
5
6
7
$font-stack:        Helvetica, sans-serif;
$primary-color: #333;

body {
font: 100% $font-stack;
color: $primary-color;
}

嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
nav {
ul {
margin: 0;
padding: 0;
list-style: none;
}

li { display: inline-block; }

a {
display: block;
padding: 6px 12px;
text-decoration: none;
}
}

局部(Partials)

局部 Sass 文件不会被翻译成 css 文件,命名规范是下划线开头——_partial.scss,可以用 @import 导入。

css 也可以模块化

导入

CSS 也有 import,但是会带来 HTTP 请求开销。Sass 的 @import 只是合并文件,不会发请求。

1
2
3
4
5
6
7
8
// _reset.cscc
html,
body,
ul,
ol {
margin: 0;
padding: 0;
}
1
2
3
4
5
6
7
8
// base.scss

@import 'reset';

body {
font: 100% Helvetica, sans-serif;
background-color: #efefef;
}

混合(Mixin)

Mixins are a way of including (“mixing in”) a bunch of properties from one rule-set into another rule-set.

Mixin 特别适用于处理 vendor prefiex。

1
2
3
4
5
6
7
8
@mixin border-radius($radius) {
-webkit-border-radius: $radius;
-moz-border-radius: $radius;
-ms-border-radius: $radius;
border-radius: $radius;
}

.box { @include border-radius(10px); }

扩展/继承

@extend 可以为 CSS 的属性 “提取公因式”,这个“公因式”叫占位类(placeholder class)。

如果占位类没有并用到就不会输出到 CSS 文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// This CSS won't print because %equal-height is never extended.
$equal-heights {
display: flex;
flex-wrap: wrap;
}

// This CSS will print because $message-shared is extended
$message-shared {
border: 1px solid #ccc;
padding: 10px;
color: #333;
}

.message {
@extend $message-shared;
}

.success {
@extend $message-shared;
border-color: green;
}

.error {
@extend $message-shared;
border-color: red;
}

.warning {
@extend $message-shared;
border-color: yellow;
}

处理后的 CSS 文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.message, .success, .error, .warning {
border: 1px solid #cccccc;
padding: 10px;
color: #333;
}

.success {
border-color: green;
}

.error {
border-color: red;
}

.warning {
border-color: yellow;
}

操作符

支持数学运算。

1
2
3
4
5
6
7
8
9
10
11
.container { width: 100%; }

article[role="main"] {
float: left;
width: 600px / 960px * 100%;
}

aside[role="complementary"] {
float: right;
width: 300px / 960px * 100%;
}

处理过后的 CSS 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
.container {
width: 100%;
}

article[role="main"] {
float: left;
width: 62.5%;
}

aside[role="complementary"] {
float: right;
width: 31.25%;
}

Less

相比于 Sass,Less 的预处理支持压缩。

1
lessc --clean-css styles.less styles.min.css

Less 还可以直接用在 Node 环境:

1
2
3
4
5
var less = require('less');

less.render('.class { width: (1 + 1) }'), function (e, output) {
console.log(output.css);
});

打印结果:

1
2
3
.class {
width: 2;
}

Less 还支持传入配置:

1
2
3
4
5
6
7
8
9
var less = require('less');
less.render('.class { width: (1 + 1) }',
{
paths: ['.', './lib'], // Specify search paths for @import directives
filename: 'style.less', // Specify a filename, for better error messages
},
function(e, output) {
console.log(output.css);
});

还可以直接用于端侧,不推荐用于生产环境,有性能开销。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- set options before less.js script -->
<link rel="stylesheet/less" type="text/css" href="styles.less" />

<script>
less = {
env: "development",
async: false,
fileAsync: false,
poll: 1000,
functions: {},
dumpLineNumbers: "comments",
relativeUrls: false,
rootpath: ":/a.com/"
};
</script>

<script src="less.js"></script>

tips

下面代码中的 & 表示父选择器:

1
2
3
4
5
6
7
8
9
10
11
12
13
.clearfix {
display: block;
zoom: 1;

$:after {
content: " ";
display: block;
font-size: 0;
height: 0;
clear: both;
visiblity: hidden;
}
}

vim

Command-Line Mode

Tip 27 Meet Vim’s Command Line

  • :[range]delete [x]
  • :[range]yank [x]
  • :[line]put [x]
  • :[range]copy {address}
  • :[range]move {address}
  • :[range]join
  • :[range]normal {commands}
  • :[range]substitue/{pattern}/{string}/[flags]
  • :[range]global/{pattern}/[cmd]
  • :edit/:write
  • :tabnew
  • :split
  • :prev/:next
  • :bprev/:bnext
  • :h ex-cmd-index

Tip 28 Execute a Command on One or More Consecutive Lines

  • :print
  • :delete
  • :join
  • :substitute
  • :normal
  • :3 <=> 3G
  • :3d <=> 3G dd
  • :{start},{end}
  • . = the current line
  • $ = the end line
  • % = all the lines