Kotlin for Android 实践

最先进的语言Kotlin

Posted by Mio4kon on 2016-08-17

准备

  1. 创建一个新工程
  2. Android Studio需要安装Kotlin插件(IDEA默认已经安装)
  3. command+shift+A在弹出框中输入Convert Java File to Kotlin File

做完这三步,你会发现原来生成的java代码转成了Kotlin代码:

1
2
3
4
5
6
7
8
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}

现在就可以使用Kotlin作为Android开发的语言了.

配置Gradle

准备工作的第三步,除了帮你将java转为Kotlin,还帮你配置了Gradle,如果不想转换原有的java代码,那么需要自己配置Gradle.除了kotlin-stdlib这个库,Android开发建议加上anko-common这个库.

关于Anko

Anko是一个用来简化一些Android任务的很强大的Kotlin库,如果不使用Anko的DSL功能,只需要使用anko-common库就行了,它是Anko的精简版.

Project-gradle:

1
2
3
4
5
6
7
8
9
10
11
12
buildscript {
ext.kotlin_version = '1.0.3'
ext.anko_version = '0.8.2'
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

app-gradle:

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
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
...
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:24.1.1'
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
compile "org.jetbrains.anko:anko-common:$anko_version"
}
```
## Hello Kotlin
项目准备完成后,其实就可以运行了.我们先给布局文件中的TextView设置一个id
```xml
<TextView
android:id="@+id/tv"
android:textSize="20dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!" />

在代码中对这个TextView操作有下面几种方法:

第一种普通方法:

1
2
var tv =findViewById(R.id.tv) as TextView
tv.text = "Hollo Kotlin"

第二种:

1
2
val tv = find<TextView>(R.id.tv)
tv.text = "Hello kotlin"

第三种:

甚至你可以直接使用tv.text = "Hello Kotlin"
当你在打出tv的时候插件会自动导入import kotlinx.android.synthetic.main.activity_main.*
如果没有,可以自行导入,它可以让你直接使用activity_main布局文件下的所以带id的控件.

编写一个Toast的工具类

在java中编写ToastUtil一般是下面写法:

1
2
3
4
5
6
7
8
9
10
11
public class ToastUtils {
public static void show(Context ctx, String msg) {
Toast.makeText(ctx, msg, Toast.LENGTH_SHORT).show();
}
public static void show(Context ctx, String msg, int duration) {
Toast.makeText(ctx, msg, duration).show();
}
}

用Kotlin可以用默认参数的写法:

1
2
3
fun showToast(ctx: Context, msg: String, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(ctx, msg, duration)
}

这样就不需要在写重载函数了.你以为只有这么多?如果你用了扩展函数,其实还可以更简单!

扩展函数

1.扩展函数数能够扩展一个类的新功能,而无需继承类或使用任何类型的设计模式.
2.扩展不能真正的修改他们继承的类.它是以静态导入的方式来实现的.

声明一个扩展函数,需要被扩展的类型来作为他的前缀,通过this关键字在扩展方法内接受对应的对象

重写Toast工具类

1
2
3
fun Context.showToast(msg: String, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(this, msg, duration).show()
}

这样只要在继承了Context的类下就可以直接使用 showToast()方法,方便,快捷,你值得拥有.
顺便说一下, anko已经实现好了,可以直接使用toast()方法.点源码发现其实和我们写的没太大区别,只是语法点区别罢了.

1
2
fun Context.toast(text: CharSequence) =
Toast.makeText(this, text, Toast.LENGTH_SHORT).show()

页面跳转

以前页面跳转的写法是这么写的:

1
2
Intent intent = new Intent(this,RecyclerViewActivity.class);
this.startActivity(intent);

虽然代码只有两行,但是这么写还是不够优雅,为什么一定要传一个Class?我只给个泛型就就打开这个泛型的页面不行吗?

我们知道在java中的泛型函数是没有办法获得该泛型类型的Class.只能通过参数传递这个Class.
而在Kotlin中,使用内联函数(inline)是可以被具体化(reified)的(后面会说).这就可以在函数中得到泛型的Class.

先定义一个方法:

1
2
3
4
inline fun <reified T : Activity> Activity.gotoActivity() {
var intent = Intent(this, T::class.java)
this.startActivity(intent)
}

这是一个扩展内联函数,通过T::class.java可以拿到泛型的Class
这时候跳转只需要这么写:

gotoActivity<RecyclerViewActivity>()

是不是非常的简单,而且可读性特别强.

一般常用方法,Anko都帮我们实现了,页面跳转我们也可以直接使用AnkostartActivity<T>()
方法.具体实现其实和上面写的类似.只是加了一下扩展.

PS:Anko的源码是非常有参考意义的.

内联函数

上面例子提到了内联函数(inline),它主要服务于高阶函数的,高阶函数就是可以将函数当做参数和返回值.
但是高阶函数,每一个函数是一个对象,包括函数内的对象都会捕获.这会导致内存开销和虚拟调用的时间开销.
而内联函数正是解决这个缺点的.它在编译的时候将方法体插入到每一个调用出.它的缺点是:有时会引起生成的代码数量增加,但只要不内联大的函数,是可以提高性能的.

Reified类型参数

如果是Reified类型的,它支持运行的时候将类型传入到方法(仅限内联函数)

示例:

1
2
3
4
5
inline fun <reified T> getClass() = T::class
fun main(s: Array<String>) {
println(getClass<Int>()) //class kotlin.Int
println(getClass<String>()) //class kotlin.String
}

实现一个Recyclerview

添加Recyclerview的依赖

1
compile "com.android.support:recyclerview-v7:$supportVersion"

设置LayoutManager

recycler.layoutManager = LinearLayoutManager(this)

创建Adapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class SimpleTextAdapter(val items: List<String>) : Adapter<SimpleTextAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SimpleTextAdapter.ViewHolder {
return ViewHolder(TextView(parent.context))
}
override fun onBindViewHolder(holder: SimpleTextAdapter.ViewHolder, position: Int) {
holder.textView.text = items[position]
}
override fun getItemCount() = items.count()
class ViewHolder(textView: TextView) : RecyclerView.ViewHolder(textView)
}

创建数据

1
2
3
4
5
6
var items = listOf<String>(
"11111111111111111111",
"11111111111111111111",
"11111111111111111111",
....
)

最后在设置adapter: recycler.adapter = SimpleTextAdapter(items)

访问网络数据

这里我们使用Retrofit访问网路数据,数据源为:GankIo的图片,图片框架为Glide,并且使用了RxKotlin作为配合Retrofit的利器.

首先先加入这些三方库的依赖:

1
2
3
4
5
6
7
8
compile "com.squareup.retrofit2:retrofit:$retrofitVersion"
compile "com.squareup.retrofit2:converter-gson:$retrofitVersion"
compile "com.squareup.retrofit2:adapter-rxjava:$retrofitVersion"
compile "com.github.bumptech.glide:glide:$glideVersion"
compile "io.reactivex:rxkotlin:$rxKotlinVersion"
compile "io.reactivex:rxandroid:$rxAndroidVersion"

创建GankService类和Model类:

GankService:

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 GankService {
companion object {
val API_HOST_URL = "http://gank.io/api/data/%E7%A6%8F%E5%88%A9/"
val api :Apis
init {
val restAdapter = Retrofit.Builder()
.baseUrl(API_HOST_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build()
api = restAdapter.create(Apis::class.java)
}
}
data class ResponseWrapper<T>(val error: Boolean, val results: List<T>)
interface Apis {
@GET("{count}/{pageNum}")
fun getMeizi(@Path("count") count: Int, @Path("pageNum") pageNum: Int): Observable<ResponseWrapper<Meizi>>
}
}

Meizi:

1
2
3
data class Meizi(
val url: String
)

这里用到了companion object,initdata语法.

companion object和init

init是初始化代码块,可以使用主构造的参数.如下:

1
2
3
4
5
class Customer(name: String) {
init {
logger.info("name = ${name}")
}
}

object关键字可以声明一个对象,从而通过它的名字来引用它.

1
2
3
4
5
6
7
object Manager {
fun do() {
// ...
}
}
Manager.do()

一个对象声明在一个类里可以标志上companion这个关键字–伴生对象,这样直接通过类名就可以调用伴随对象的方法和引用.

1
2
3
4
5
6
7
8
9
10
class MyClass {
companion object Factory {
fun do(){
//...
}
}
}
val instance = MyClass.do()

使用companion关键字时候,伴生对象的名称可以省略:

1
2
3
4
5
class MyClass {
companion object {
}
}

关于companion objectinit调用顺序的可参考->例子

数据类

类前用data关键字标记的为数据类.
特点是编译器会生成:
–equals()/hashCode()
–toString() 格式是 “User(name=John, age=42)”
–componentN() functions 对应按声明顺序出现的所有属性
–copy() 函数

更多请参考->文档

GankService类和Model类创建完成后,最后需要在Activity里使用Service(这里只是简单的模拟访问网络,不去对项目进行过多的设计)

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
class GankIoActivity : AppCompatActivity() {
val meiziList =ArrayList<Meizi>()
val adapter = GankioAdapter(meiziList)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_gank_io)
GankService.api.getMeizi(50, 1)
.subscribeOn(Schedulers.io())
.map { it.results }
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
meiziList.clear()
meiziList.addAll(it)
adapter.notifyDataSetChanged()
})
recycler.layoutManager = LinearLayoutManager(this)
recycler.adapter = adapter
}
}

对了,忘记附上Adapter的代码了,和之前的实现RecylerView中的Adapter区别不大:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class GankioAdapter(val items: List<Meizi>) : Adapter<GankioAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GankioAdapter.ViewHolder {
val root = LayoutInflater.from(parent.context).inflate(R.layout.item_big_img, parent, false)
return ViewHolder(root)
}
override fun onBindViewHolder(holder: GankioAdapter.ViewHolder, position: Int) {
holder.setImage(items[position].url)
}
override fun getItemCount() = items.count()
class ViewHolder(root: View) : RecyclerView.ViewHolder(root) {
private var imageView: ImageView = root.find<ImageView>(R.id.iv_mio)
fun setImage(url: String) {
Glide.with(imageView.context).load(url).fitCenter().into(imageView)
}
}
}

效果图:

总结

至此,Kotlin的Android实践算是完篇了.其中有很多地方是可以改进的.比如:
val root = LayoutInflater.from(parent.context).inflate(R.layout.item_big_img, parent, false)

其实可以变成:
val root = parent!!.context.layoutInflater.inflate(R.layout.item_big_img,parent,false)
等等.

最近也在是在研究kotlin的语法,所以写了这篇文章.总体来说kotlin的语法还是非常优雅的.
很多语法糖使用起来非常的爽,尤其是工具类的使用.有一点不习惯的是Kotlin所有变量都默认的必须不为null,除非显式的在后面加?.而java却不是这样.但所带来的成本可能就是在java中需要经常做非空判断.尤其是上一层api的不透明性,导致这层判空必须要做.所以很多java框架也都用了注释@Nullable来解决这个问题.

源文件

https://github.com/mio4kon/Kotlin-Android-Practice

参考

https://kotlinlang.org/docs/reference/

http://tanfujun.com/kotlin-web-site-cn/

https://wangjiegulu.gitbooks.io/kotlin-for-android-developers-zh/content/