Kotlin学习笔记(8)
系列文章全部为本人的学习笔记,若有任何不妥之处,随时欢迎拍砖指正。如果你觉得我的文章对你有用,欢迎关注我,我们一起学习进步!Kotlin学习笔记(1)- 环境配置Kotlin学习笔记(2)- 空安全Kotlin学习笔记(3)- 语法Kotlin学习笔记(4)- 流程控制Kotlin学习笔记(5)- 类Kotlin学习笔记(6)- 属性Kotlin学习笔记(7)- 接口Kotlin学习笔记(8)- 扩展Kotlin学习笔记(8)- 扩展(续)Kotlin学习笔记(9)- 数据类Kotlin学习笔记(10)- 泛型Kotlin学习笔记(11)- 内部类和嵌套类Kotlin学习笔记(12)- 委托Kotlin学习笔记(13)- 函数式编程Kotlin学习笔记(14)- lambda
今天要开始学习扩展(Extension)了,扩展是kotlin中非常重要的一个特性,它能让我们对一些已有的类进行功能增加、简化,使他们更好的应对我们的需求。这么说可能有点枯燥,我们先来看一个小栗子。
// 对Context的扩展,增加了toast方法。为了更好的看到效果,我还加了一段log日志fun Context.toast(msg : String){ Toast.makeText(this, msg, Toast.LENGTH_SHORT).show() Log.d("text", "Toast msg : $msg")}// Activity类,由于所有Activity都是Context的子类,所以可以直接使用扩展的toast方法class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { ...... toast("hello, Extension") }}// 输出Toast msg : hello, Extension
怎么样,有没有一点小激动,反正我是有的。按照通常的做法,我们会写一个ToastUtils
工具类,或者在BaseActivity
中实现toast
方法,但是现在这些统统不需要了,直接给Context
增加扩展,这样不论是Activity
或者Service
都可以直接调用,更简洁、更优雅。接下来是我的学习进程,希望也能对你有所帮助。
扩展方法,顾名思义,就是对类的方法进行扩展,写法和定义方法类似,但是要声明目标类,也就是对哪个类进行扩展,kotlin中称之为Top Level
。就比如上面的栗子。
fun Context.toast(msg : String){ Toast.makeText(this, msg, Toast.LENGTH_SHORT).show() Log.d("text", "Toast msg : $msg")}
其中,Context
就是目标类(Top Level),我们把它放到方法名前,用点.
表示从属关系。在方法体中,用关键字this
对本体进行调用。和普通方法一样,如果有返回值,在方法后面跟上返回类型,我这里没有返回值,所以直接省略了。
还有一点很神奇的是,我们可以允许目标类接受null。不是说对null进行扩展,而是可以设置目标类为一个可以为空的对象。再举一个栗子,也是我们很常用的功能:
fun Any?.string() : String=if(this==null) "null obj" else toString()var a=1toast("hello, ${a.string()}")var b : View?=nulltoast("hello, ${b.string()}")// 输出Toast msg : hello, 1Toast msg : hello, null obj
在这里我们对Any
进行扩展,它在konlin中类似于java中的Object
,所有的类都继承与它,我们给它扩展了一个string()
方法。还记得吗,类型后面跟问号?
表示可为空。在这里当目标为null
时,我们返回了一个字符串,看一下调用,再看一下输出,完全和预想的一样。
二、扩展属性(extension property)kotlin中其实已经默认对toString方法进行了处理,如果是空,则会返回字符串null。这其实很多时候不符合我们的需求,我们可以自己扩展一下,将返回值改为空字符串就好。
fun Any?.string() : String=if(this==null) "" else toString()
扩展属性和扩展方法类似,是对目标类的属性进行扩展。扩展属性也会有set
和get
方法,并且要求实现这两个方法,不然会提示编译错误。因为扩展并不是在目标类上增加了这个属性,所以目标类其实是不持有这个属性的,我们通过get
和set
对这个属性进行读写操作的时候也不能使用field
指代属性本体。可以使用this
,依然表示的目标类。
// 扩展了一个属性paddingHvar View.panddingH : Int get()=(paddingLeft + paddingRight) / 2 set(value) { setPadding(value, paddingTop, value, paddingBottom) }// 设置值text.panddingH=100
在栗子中,我们首先给View扩展了一个属性paddingH
,并给属性增加了set
和get
方法,然后再activity中通过textview调用。
kotlin中的静态用关键字companion
表示,而且它不是修饰属性或方法,而是定义一个方法块,在方法块中的所有方法和属性都是静态的,这样就将静态部分统一包装了起来。静态部分的访问和java一致,直接使用类名+静态属性/方法名就可以
// 定义静态部分class Extension { companion object part{ var name="Extension" }}// 通过类名+属性名直接调用toast("hello, ${Extension.name}")// 输出Toast msg : hello, Extension
上面栗子中,companion object
一起是修饰关键字,part
是方法块的名称。其中,方法块名称part
可以省略,如果省略的话,默认缺省名为Companion
静态的扩展和普通的扩展类似,但是在目标类要加上静态方法块的名称,所以如果我们要对一个静态部分扩展,就要先知道静态方法块的名称才行。
class Extension { companion object part{ var name="Extension" }}// part为静态方法块名称fun Extension.part.upCase() : String{ return name.toUpperCase()}// 调用一下toast("hello, ${Extension.name}")toast("hello, ${Extension.upCase()}")//输出Toast msg : hello, ExtensionToast msg : hello, EXTENSION
四、引用在AS中,如果我们使用扩展方法和扩展属性,大部分情况下会自动提示导入import
,但有时候如果没有提示,就需要手动import
一下了。扩展的import
路径为:包名+方法名/属性名,比如我最先创建的扩展方法toast
,其实文件全部是这样的
package com.study.jcking.weatherkotlin.execimport android.content.Contextimport android.util.Logimport android.widget.Toast/** * Created by Jcking on 2017/6/3. */fun Context.toast(msg : String){ Toast.makeText(this, msg, Toast.LENGTH_SHORT).show() Log.d("text", "Toast msg : $msg")}
可见,toast方法
没有在类内部,而是与包名同级,当然这不是绝对,只是习惯这样写,那我们引用的时候就这样写import
import com.study.jcking.weatherkotlin.exec.toast
五、最后以上就是个人学习扩展的一些总结,东西不多,但是感觉是kotlin中很重要的一部分。他让我们的代码更简洁,更优雅,而且功能定位更准确、更清晰。再有,就是感觉一大波util要下岗了~
最后抛出个问题,暂时还没有搞明白,希望小伙伴们帮我解惑:如果我将扩展没有放在包名同级,而是放在一个类的内部,在语法上是完全没有问题的,但是类外部调用却好像不行了。请问是它失去了对外部的可见性,还是我的调用方式有问题?欢迎讨论指教,先行谢过!!
更新:2017年6月4日问题已经找到资料了,确实是不行的,请看我下一篇笔记: Kotlin学习笔记(8)- 扩展(续)
Extension类,包package com.study.jcking.weatherkotlin.exec
package com.study.jcking.weatherkotlin.execclass Extension { companion object part{ var name="Extension" } fun Context.toast(msg : String){ Toast.makeText(this, msg, Toast.LENGTH_SHORT).show() Log.d("text", "Toast msg : $msg") }}
调用类,包package com.study.jcking.weatherkotlin
package com.study.jcking.weatherkotlinclass MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) toast("hello, ${Extension.upCase()}") // 这里toast报错,编译错误,无法调用 }}
一、什么是扩展函数?
扩展函数数是指在一个类上增加一种新的行为,甚至我们没有这个类代码的访问权限。这是一个在缺少有用函数的类上扩展的方法,Kotlin能够为我们做到那些令人关注的事情,而这些Java做不到。
在Java中,通常会实现很多带有static方法的工具类,而Kotlin中扩展函数的一个优势是我们不需要在调用方法的时候把整个对象当作参数传入,它表现得就像是属于这个类的一样,而且我们可以使用this关键字和调用所有public方法。
二、扩展函数的使用
(1)函数的扩展
简单来说,Kotlin扩展函数允许我们在不改变已有类的情况下,为类添加新的函数。例如,我们能够为Activity中添加新的方法,让我们以更简单术语显示toast,并且这个函数不需要传入任何context,它可以被任何Context或者它的子类调用,比如Activity或者Service:
fun Context.toast(message: CharSequence, duration: Int=Toast.LENGTH_SHORT) {
当然你也可以这样写:
fun Activity.toast(message: CharSequence, duration: Int=Toast.LENGTH_SHORT){
对参数的解释:
Activity:表示函数的接收者,也就是函数扩展的对象
. :扩展函数修饰符
toast:扩展函数的名称
我们可以在任何地方(例如在一个工具类文件中)声明这个函数,然后在我们的Activity中将它作为普通方法来直接使用(这里的两种使用方式后文会做解释):
override fun onCreate(savedInstanceState: Bundle?) {
当然了,Anko已经包括了自己的toast扩展函数,跟上面这个很相似(关于Anko是什么,你可以看我的这篇文章。。。)。Anko提供了一些针对CharSequence和resource的函数,还有两个不同的toast和longToast方法:
toast("Hello world!")
有一点值得注意:扩展函数并不是真正地修改了原来的类,它的这些作用效果是以静态导入的方式来实现的。扩展函数可以被声明在任何文件中,因此有个通用的方式是把一系列有关的函数放在一个新建的文件里,就像我们刚才所说的工具类当中。
我觉得还是再举几个几个例子来说明一下吧,因为它们完全显示扩展函数的力量。
我们首先看看这个例子:
fun Context.niceToast(message: String,
我们增加了一个默认值是类名的参数,如果这是在Java中的话,那么总数开销会以几何形式增长,而现在我们可以通过以下方式调用:
toast("Hello")
我们甚至还有其它选择,因为我们可以使用参数名字来调用,也就是说我们可以通过在值前写明参数名来传入我们希望的参数:
toast(message="Hello", length=Toast.LENGTH_SHORT)
这样我们就使用了第二个参数的默认形式,而使用了第一第三个参数的传入值形式。但是你可能觉得这样的函数调用比较难以理解“[$className] $message”,这个是 Kotlin 中的String模版,我们接下来讲解一下它。
String模版:
我们可以在String中直接使用模版表达式,它可以帮助我们很简单地在静态值和变量的基础上编写复杂的String。在上面的例子中,我们使用了"[$className] $message"。
这就意味着,在任何时候我们使用一个$符号就可以插入一个表达式。如果这个表达式有一点复杂,我们就需要使用一对大括号括起来,比如:"My name is ${user.name}"。字符串可以包含模板表达式,即可求值的代码片段,并将其结果连接到字符串中。下面我们举几个例子:
一个模板表达式由一个 $ 和简单名称组成
val i=10
一个模板表达式由一个$ 和大括号括起来的表达式组成
val s="abc"
**如果想输出字符,比如“$” **
${'$'}
● 在 onCreateViewHolder 中的使用
第一个是我们在 RecyclerView 中的适配器中用到的例子,正常情况下我们这样使用:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
但是实际上加载布局的逻辑实在是太麻烦了,并且绝大多数情况下我们都在重复编写同样的适配器代码,那么我们为什么不给 ViewGroups 赋予 inflate 的能力呢:
fun ViewGroup.inflate(layoutRes: Int): View {
然后我们可以想现在这样使用它:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
(2)属性的扩展
扩展函数也可以是一个属性,所以我们可以通过相似的方法来扩展属性。我们知道Kotlin由于互操作性的特性已经提供了getter、setter这个属性,但是我们任然通过下面的例子来展示一下使用自己的getter/setter生成一个属性的方式,因为这很有助于理解扩展属性背后的思想:
public var TextView.text: CharSequence
再比如,我们可以用此方法来设置View的padLeft属性:
// 使用扩展属性(extension property)
(3)扩展函数中的操作符
通常来讲,我们不需要去扩展我们自己的类,但是我需要去使用扩展函数扩展我们已经存在的类来让第三方的库能提供更多的操作,比如我们可以去像访问List的方式去访问 ViewGroup 的 view(关于 View 和 ViewGroup 请参看这篇。。。):
operator fun ViewGroup.get(position: Int): View=getChildAt(position)
于是我们就可以像这样非常简单地从一个 ViewGroup 中通过 position 得到一个 view:
val container: ViewGroup=find(R.id.container)
三、可选参数和默认值
聪明的你不知道有没有注意到刚才的那段代码中,Toast 的第二个形参为什么在一开始就赋予了一个默认值呢?其实这就涉及到了 Kotlin 中可选参数的概念。
什么是可选参数呢?简而言之,就是我们在调用该函数的时候对于该参数既可以传参也可以不传参,比如上文代码中的第二个参数。那么在不传参时,默认的参数自然就成了我们上文代码中指明的“Toast.LENGTH_SHORT”。
所以说这样做的好处也是显而易见的,那就是借助于参数和构造函数的默认值,我们将不再需要进行函数重载了。只需要我们做一个函数的声明就可以满足我们几乎所有的需求。还是拿刚才的 Toast 来说事:
fun Activity.toast(message: CharSequence, duration: Int=Toast.LENGTH_SHORT){
第二个参数表示 toast 的显示持续时间,这就是一个我们刚刚说的可选参数,但没有显式指定时,它将使用默认的Toast.LENGTH_SHORT这个值。因此,我们可以采用前面的两种方式来调用这个函数:
override fun onCreate(savedInstanceState: Bundle?) {
再比如,我们可以采用这样的方式在 Activity 中支持 lollipop 动画(我们暂时不必给出具体实现):
inline public fun <reified T : Activity> Activity.navigate(
所以我们就有了三种方式去调用这个函数,先看一下我们刚介绍过的前两种:
navigate<DetailActivity>("2")
而对于第三种方式,虽说在这种情况下意义不大,但却让我们知道了我们也是可以通过使用参数名字来决定哪个参数会被调用的:
navigate<DetailActivity>(id="2", transitionName=TRANSITION_NAME)
所以,参数的默认值可以让我们只声明一个构造函数,但却会得到很多重载。
除此之外,我们呢还可以扩展View的dp转换函数:
// 使用扩展函数
可以看到,扩展函数让我们在编写代码时省去了很多功夫。
我们总结下 Kotlin 的三个特点:
Kotlin的扩展函数功能使得我们可以为现有的类添加新的函数,实现某一具体功能 。
扩展函数是静态解析的,并未对原类添加函数或属性,对类本身没有任何影响。
扩展属性允许定义在类或者kotlin文件中,不允许定义在函数中。
四、Kotlin 标准库扩展函数集合
Kotlin 标准库提供了一些扩展 Javahttp库的函数,我们接下来一一介绍下。
apply
apply 是 Any 的扩展函数,因而所有类型都能调用。
apply 接受一个lambda表达式作为参数,并在apply调用时立即执行,apply返回原来的对象。
apply 主要作用是将多个初始化代码链式操作,提高代码可读性。
比如:
val task=Runnable { println("Running") }
上面这段代码可以简化为:
val task=Runnable { println("Running") }
五、用 Kotlin 的扩展函数 findViewOften 丢掉 ViewHolder
(1)ViewHolder 介绍
作为一名 Android 开发者,对 ViewHolder 应该再熟悉不过了。ViewHolder 一开始并不是 Android 原生提供的(现在已经是 RecycleView 的默认实现了),而是 Google 为了提高 ListView 的使用性能,为开发者提供的一种最佳实践。
Google 提供的 ViewHolder 的标准实现如下,熟悉者可以直接跳到下个部分 ViewHolder的变种 继续阅读:
staticclassViewHolder{
在 Item 第一次创建视图的时候,填充 ViewHolder 并且将其保存在视图中:
ViewHolder holder=newViewHolder();
这样在填充 Item 数据的时候,直接使用 Viewholder 对象的属性,这样可以减少在滚动 ListView 频繁调用 findViewById() 而导致的性能问题。当然关于 ListView 性能优化的问题还有一些内容可以介绍,不过我们这里暂不作展开。
(2)ViewHolder 的变种
Google 提供的 ViewHolder 的确能够提升 ListView 的使用效率,但是 ViewHolder 的实现相对繁琐,需要为每一种 Item 定义一个 ViewHolder,对代码书写和维护都是额外的开销。于是有人针对 ViewHolder 的实现做了一些优化,让 ViewHolder 写起来更方便。网上有很多种写法,下面提供一种最为简单优雅又高效的方式:
public class ViewHolder {
这里我们使用 SparseArray 映射每个视图 id 和对应的视图,并将其保存在视图中,这样既保证在滚动过程中频繁获取视图的效率,使用起来也极其方便:
ImageView bananaView=ViewHolder.get(convertView, R.id.banana);
(3)Kotlin 扩展函数 findViewOften()
这里Kotlin 实现 ViewHolder 的扩展函数和上面的变种使用的同一种思路,但得益于 Kotlin 语言提供的特性,实现和使用起来更加方便流畅,甚至都感觉不到 ViewHolder 这种特殊机制的存在:
fun<T : View> View.findViewOften(viewId:Int): T {
这里实现了一个 View 的扩展函数 findViewOften(viewId: Int) 意味着在需要频繁寻找一个视图的子视图的情况下使用,这样我们在 Item 中就可以这样写了:
val subTitle: TextView=convertView.findViewOften(R.id.list_item_subtitle)
由于 Kotlin 提供类型推断功能,所以 findViewOften 的返回值不用手动转换或者手动指定泛型类型。所以利用 Kotlin 的语言特性,为 View 扩展一个方法,从此再也不用繁琐的定义 Viewholder 了,使用的时候也是如此的顺畅。
(4)RecycleView 中的 ViewHolder
最后,不得不提一下在 RecycleView 应该怎么办,因为在 RecycleView 的机制里面,在创建 Item 的 View 的时候,必须创建一个 RecyclerView.ViewHolder 并且返回。对于我们上面那么完美的封装, Google 这明显是在帮倒忙,还好这忙虽然帮倒了,不过还不至于无法挽回。
如果大家在使用 RecycleView 还想使用本文提供的方法的话,可以参考下面的方式实现:提供一个 RecyclerView.ViewHolder 默认实现类,该类提供一个通过 id 获取视图的方法,在创建 Item 的 View 的时候默认都返回这个类的实例。
class MyViewHolder(val convertView: View) : RecyclerView.ViewHolder(convertView) {
如果不想 MyViewHolder 的外部有不需要的依赖,可以将 findViewOften 直接实现在 MyViewHolder 里面。
有想学习android开发的可以进群的哦306978835
下一篇:11.21心理学
发表评论