Kotlin 实现配置化网络请求 | Ohmer's Blog
Kotlin官方提供一个DSL的典型应用场景,Anko致力直接用Kotlin配置页面布局和视图的属性。将布局文件代码化能够带来许多如类型安全、解析效率、代码重用等好处,而Anko让代码布局和XML一样简洁清晰。
受到Anko的启发,让我萌生了把Android中网络请求纷繁复杂配置信息也封装成配置化方式,实现如下方式的网络请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | Http.get { url = "http://api.openweathermap.org/data/2.5/weather" headers { "Content-Type" - 'application/json' "pragma-token" - '33162acxxxxxx5032ad21e0e79ff70d' } params { "q" - "shanghai" "appid" - "d7a98cf22463b1c0c3df4adfe5abbc77" } onSuccess { bytes -> // handle data } onFail { error -> // handle error } } |
目前该框架已经完成,后面还会继续完善,项目地址Kolley
奔着这个目标,我把之前自己简单封装的Volley库翻出来,用Kotlin重新封装一下。经过分析总体过程大概如下:
- 基础代码转Kotlin
- 重定义原子Request
- Request构造配置化
- 提供RESTful方法
基础代码转Kotlin
之前的框架是参考android-async-http做的封装,用okhttp作为网络请求引擎,图片请求缓存模块使用的jakewharton提供的disklrucache,这两块都可以复用,先将这部分代码直接转成Kotlin实现。
这不需要花太多的功夫,将java代码复制过来以后,直接使用Android Studio的快速转换功能,转换后可能会有一些语法上的错误,稍微处理一下就可以了,得到类似的内容。
1 2 3 4 5 6 7 8 9 10 | class OkHttpStack constructor(client: OkHttpClient = OkHttpClient()) : HurlStack() { private val mFactory: OkUrlFactory init { mFactory = OkUrlFactory(client) } override fun createConnection(url: URL): HttpURLConnection { return mFactory.open(url) } } |
重定义原子Request
需要在Volley提供的Request基础上继承一个BaseRequest预处理一些信息,如params。
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 | class ByteRequest(method: Int, url: String, errorListener: Response.ErrorListener? = Response.ErrorListener {}) : BaseRequest<ByteArray>(method, url, errorListener) { override fun parseNetworkResponse(response: NetworkResponse?): Response<ByteArray>? { return Response.success(response?.data, HttpHeaderParser.parseCacheHeaders(response)) } } abstract class BaseRequest<D>(method: Int, url: String, errorListener: Response.ErrorListener? = Response.ErrorListener {}) : Request<D>(method, url, errorListener) { protected val DEFAULT_CHARSET = "UTF-8" internal var _listener: Response.Listener<D>? = null protected val _params: MutableMap<String, String> = HashMap() // used for a POST or PUT request. /** * Returns a Map of parameters to be used for a POST or PUT request. * @return */ public override fun getParams(): MutableMap<String, String> { return _params } override fun deliverResponse(response: D?) { _listener?.onResponse(response) } protected fun log(msg: String) { if (BuildConfig.DEBUG) { Log.d(this.javaClass.simpleName, msg) } } } |
Request构造配置化
上一步封装的Request必须在构造器中提供一些参数,并且像Listener这样的参数不能直接传递表达式,为配置化调用的封装提供了一定的困难。需要重新封装一个Request构造器,再在最后交给执行队列的时候创建真正的Request传递给它,这样让所有网络请求需要的配置信息都可以很方便的构造。
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | open class BaseRequestWapper() { internal lateinit var _request: ByteRequest var url: String = "" var method: Int = Request.Method.GET private var _start: (() -> Unit) = {} private var _success: (ByteArray) -> Unit = {} private var _fail: (VolleyError) -> Unit = {} private var _finish: (() -> Unit) = {} protected val _params: MutableMap<String, String> = HashMap() // used for a POST or PUT request. protected val _headers: MutableMap<String, String> = HashMap() var tag: Any? = null fun onStart(onStart: () -> Unit) { _start = onStart } fun onFail(onError: (VolleyError) -> Unit) { _fail = onError } fun onSuccess(onSuccess: (ByteArray) -> Unit) { _success = onSuccess } fun onFinish(onFinish: () -> Unit) { _finish = onFinish } fun params(makeParam: RequestPairs.() -> Unit) { val requestPair = RequestPairs() requestPair.makeParam() _params.putAll(requestPair.pairs) } fun headers(makeHeader: RequestPairs.() -> Unit) { val requestPair = RequestPairs() requestPair.makeHeader() _headers.putAll(requestPair.pairs) } fun excute() { var url = url if (Request.Method.GET == method) { url = getGetUrl(url, _params) { it.toQueryString() } } _request = ByteRequest(method, url, Response.ErrorListener { _fail(it) _finish() }) _request._listener = Response.Listener { _success(it) _finish() } if (tag != null) { _request.tag = tag } Http.getRequestQueue().add(_request) _start() } private fun getGetUrl(url: String, params: MutableMap<String, String>, toQueryString: (map: Map<String, String>) -> String): String { return if (params == null || params.isEmpty()) url else "$url?${toQueryString(params)}" } private fun <K, V> Map<K, V>.toQueryString(): String = this.map { "${it.key}=${it.value}" }.joinToString("&") } |
代码中将网络请求需要的所有信息全部包装了一层,这样在调用的时候就可以很方便的逐个设置每个参数(当然会有一些默认值),最后在excute()方法中全部设置给真正的Request。这个封装保证了下面的调用方式:
1 2 3 4 5 6 7 8 9 | url = "http://api.openweathermap.org/data/2.5/weather" params { "q" - "shanghai" "appid" - "d7a98cf22463b1c0c3df4adfe5abbc77" } onSuccess { bytes -> // handle data } ... |
PS:上面params是的书写方式,使用了Kotlin的操作符重载功能,具体实现可以下载源码看下。
提供RESTful方法
实现到上一步,已经准备的差不多了,接下来还需要最后一步,提供RESTful请求方法。
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 | object Http { private var mRequestQueue: RequestQueue? = null fun init(context: Context) { // Set up the network to use OKHttpURLConnection as the HTTP client. // getApplicationContext() is key, it keeps you from leaking the // Activity or BroadcastReceiver if someone passes one in. mRequestQueue = Volley.newRequestQueue(context.applicationContext, OkHttpStack(OkHttpClient())) } fun getRequestQueue(): RequestQueue { return mRequestQueue!! } val request: (Int, BaseRequestWapper.() -> Unit) -> Request<ByteArray> = { method, request -> val baseRequest = BaseRequestWapper() baseRequest.method = method baseRequest.request() baseRequest.excute() baseRequest._request } val post = request.partially1(Request.Method.POST) val put = request.partially1(Request.Method.PUT) val delete = request.partially1(Request.Method.DELETE) val head = request.partially1(Request.Method.HEAD) val options = request.partially1(Request.Method.OPTIONS) val trace = request.partially1(Request.Method.TRACE) val patch = request.partially1(Request.Method.PATCH) } |
上面的request: (Int, BaseRequestWapper.() -> Unit) -> Request<ByteArray>方法为网络请求提供了入口、保证了配置化代码都可以在{}中调用、完成了真正网络请求添加到执行队列。用户可以通过http.requset(method){}方式发起各种请求。
val get = request.partially1(Request.Method.GET)等提供了RESTful方法的封装,实现Http.get{}的方便调用。
后续
关于图片请求模块的实现,其实也是异曲同工,虽然更加复杂一点,但是具体思路是一样的。有兴趣的可以下载源码查看实现,也欢迎提交代码。
图片请求的方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | Image.display { url = "http://7xpox6.com1.z0.glb.clouddn.com/android_bg.jpg" imageView = mImageView options { // these values are all default value , you do not need specific them if you do not want to custom imageResOnLoading = R.drawable.default_image imageResOnLoading = R.drawable.default_image imageResOnFail = R.drawable.default_image decodeConfig = Bitmap.Config.RGB_565 scaleType = ImageView.ScaleType.CENTER_CROP maxWidth = ImageDisplayOption.DETAULT_IMAGE_WIDTH_MAX maxHeight = ImageDisplayOption.DETAULT_IMAGE_HEIGHT_MAX } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | Image.load { url = "http://7xpox6.com1.z0.glb.clouddn.com/android_bg.jpg" options { scaleType = ImageView.ScaleType.CENTER_CROP maxWidth = ImageDisplayOption.DETAULT_IMAGE_WIDTH_MAX maxHeight = ImageDisplayOption.DETAULT_IMAGE_HEIGHT_MAX } onSuccess { bitmap -> _imageView2?.setImageBitmap(bitmap) } onFail { error -> log(error.toString()) } } |