Kotlin 开发Android 实战(三) - 扩展变量与其它技巧

ColemanHoll 8年前
   <p>在前面的两篇文章中,我们探索了如何使用Kotlin来进行部分实用Android开发工作。如果你还没有看前面两篇,请进入传送门<a href="http://www.open-open.com/lib/view/open1461719932326.html">第一部分</a>/<a href="http://www.open-open.com/lib/view/open1461720482382.html">第二部分</a>。</p>    <p>到现在,我们已经可以使用比XML更少的代码完成View的构建,更别说Java了。Kotlin的语法为声明式,View之间的嵌套也十分清晰,而且我们还可以给类很方便地添加实用方法。</p>    <p>但在上一篇的结尾我们提到要给View设置左内边距并不容易实现。如果硬是要用Kotlin做这件事,就需要如下编码,注意其中我们需要调用setPadding()并传入四个参数,而不是给一个由JavaBean风格的getter/setter生成的模拟属性赋值。</p>    <pre>  <code class="language-kotlin">v<TextView> {      layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)      setPadding(dp_i(16), 0, 0, 0)  // 风格一点也不一致      text = "Hello"  }  </code></pre>    <p>为了使构建View的代码风格一致,我们更愿意直接给左内边距赋值,而不是调用有四个参数的方法。你可能想到给View类添加一个扩展方法setleftPadding(int)。这样做OK,但是你无法通过lambda with receiver这样写:leftPadding = dp_i(16),原因是由Kotlin生成的长相类似JavaBean的方法不会生成模拟变量,只有Java类中的成员方法可以。</p>    <p>但是,Kotlin可以定义扩展属性,长得和Java类与方法中的变量差不多。一个扩展变量会像Kotlin扩展方法一样被移植到存在的类中,所以可以直接通过该类实例访问,只需你在代码中导入扩展变量。</p>    <p>我们可以定义一个扩展变量使得对于左内边距的设置代码能和其他属性设置代码风格一致。下面的代码设置了左内边距,其他类似:</p>    <pre>  <code class="language-kotlin">var View.padLeft: Int      // 指定setter方法,值是int类型      set(value) {          // 通过View.setPadding方法设置左边距          // 对于其他的参数使用Kotlin的模拟变量          setPadding(value, paddingTop, paddingRight, paddingBottom)      }      // 指定getter方法      get() {          return paddingLeft      }  </code></pre>    <p>所以TextView就可以这样创建了:</p>    <pre>  <code class="language-kotlin">v<TextView> {      layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)      padLeft = dp_i(16)  // 是不是强多了      text = "Hello"  }  </code></pre>    <p>所有属性的设置代码风格变一致了,棒!</p>    <p>对于padLeft扩展属性有如下几点需要注意:</p>    <ul>     <li> <p>扩展属性的注解是class dot property,注意与扩展方法的class dot function注解区别开。</p> </li>     <li> <p>扩展属性的类型在冒号后指定。</p> </li>     <li> <p>通过var关键字声明,这也是Kotlin中声明可变变量的关键字,也就是可以直接赋值。如果要声明不可变的只读变量则使用val关键字。</p> </li>     <li> <p>一个可变的扩展属性需要我们同时提供getter和setter的实现。</p> </li>    </ul>    <p>padLeft的setter方法的实现基于View的setPadding()方法,它接收赋值语句右边的值作为第一个参数传入,并传入TextView的其他内边距属性的值。getter方法的实现只不过将View内部的左内边距属性返回。</p>    <p>小吐槽:Android的View类给内边距属性提供了JavaBean风格的getter方法,却没有提供setter方法,现在通过Kotlin我们化解了这个尴尬。</p>    <p>如果你觉得现有的哪个不可修改的API所提供的功能还不够,就可以通过Kotlin的扩展方法和扩展属性完善它。</p>    <h2>中场</h2>    <p>以上说的种种风格与语法都有助于以声明的方式动态构建View层级,使Kotlin成为一个“域确切”(domain specific)的语言。比如,综合使用上述所有功能,我们可以写一个设置了左内边距属性的两个TextView:</p>    <pre>  <code class="language-kotlin">v<LinearLayout> {      layoutParams = LayoutParams(MATCH_PARENT, WRAP_CONTENT)      orientation = VERTICAL      padLeft = dp_i(16f)        v<TextView> {          layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)          text = "Hello"      }      v<TextView> {          layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)          text = "World"      }  }  </code></pre>    <p>就这一段代码而言还是不错的,而且肯定比传统方式强一些。不过,语法还能更加紧凑,还有提升的空间。</p>    <h3>如何给特定的layout参数属性赋值?</h3>    <p>从上面的代码中我们不容易发现,想给一个LinearLayout设置某个特定的layout参数是很难的。到现在为止,我们都在通过把给定LinearLayout.LayoutParams对象赋值给View对象的layoutParams虚拟属性来给View设置宽高。但当我们需要设置gravity属性呢?可能你想这么写:</p>    <pre>  <code class="language-kotlin">layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)  layoutParams.gravity = Gravity.CENTER  // 很遗憾编译器并不同意  </code></pre>    <p>编译器不同意的原因是,View的所有layoutParams属性都是ViewGroup.LayoutParams类型,也就是所有其他LayoutParams的超类,为了设置LinearLayout的属性,就需要向下转型。</p>    <p>在Java中我们需要将LayoutParams转型或是重新赋值,但在Kotlin我们可以利用一个名叫with的标准库函数来处理LayoutParams。with方法接收一个对象和一个用于处理该对象的lambda with receiver。描述起来可能没什么厉害的,但它的语法是这样的:</p>    <pre>  <code class="language-kotlin">v<TextView> {      with(layoutParams as LinearLayout.LayoutParams) {          width = WRAP_CONTENT          height = WRAP_CONTENT          gravity = CENTER      }      text = "Hello"  }  </code></pre>    <p>请注意这里我们可以同时将layoutParams转型为LinearLayout.LayoutParams并将其作为receiver通过lambda快速访问其属性。</p>    <p>还有一点需要知道的就是Android View容器会在子View生成时自动生成一个layout参数对象并赋值给子View。在这里,当把一个TextView添加进LinearLayout时,会自动生成一个LinearLayout.LayoutParams并赋值给TextView的layoutParams属性。也就是说,我们并不需要自己创建一个LayoutParams对象,直接用父LinearLayout提供的就可以。记住这种情况只在将一个子View添加进一个父ViewGroup时才会发生,所以最外层的ViewGroup并不会获得这类对象,因为其没有父ViewGroup。</p>    <p>对于layout参数来讲,这种语法可能还有改善的控件,但已经很直白了。</p>    <h3>现在,我要让“v”方法消失</h3>    <p>伴随我们这么久的v方法已经很方便了,但其语法还有简化的控件。如果能像下面这样构建一个View层级岂不是更好?</p>    <pre>  <code class="language-kotlin">linearLayout {      textView {          // 各属性...      }      textView {          // 各属性...      }  }  </code></pre>    <p>这样写显得更加自然,且可读性有很大的提升。(长得挺像Gradle)实现这种语法的技巧就是给每一种View类型定义一个缩写形式。所以为了实现上述代码,我们需要一个叫linearLayout的方法和v等价,还需要一个textView方法和v等价。在Kotlin中这不难实现:</p>    <pre>  <code class="language-kotlin">fun ViewGroup.linearLayout(init: LinearLayout.() -> Unit) = v(init)  fun Context.linearLayout(init: LinearLayout.() -> Unit) = v(init)    fun ViewGroup.textView(init: TextView.() -> Unit) = v(init)  fun Context.textView(init: TextView.() -> Unit) = v(init)  </code></pre>    <p>在这里我为每一种View类型写了两个方法来适配各种被调用的情况,因为在前面v方法可以被Context或ViewGroup调用。Kotlin中支持这种声明的语言特点叫做单一表述方法single expression function。这是一种针对方法的特殊语法,可以让你:</p>    <ol>     <li>省略方法体的大括号。</li>     <li>通过表述的返回类型猜测方法的返回类型。</li>     <li>省略return关键字。</li>    </ol>    <p>现在我们通过这些方法来重新构建上面的View层级:</p>    <pre>  <code class="language-kotlin">linearLayout {      layoutParams = LayoutParams(MATCH_PARENT, WRAP_CONTENT)      orientation = VERTICAL      padLeft = dp_i(16f)        textView {          layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)          text = "Hello"      }      textView {          layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)          text = "World"      }  }  </code></pre>    <p>所以只要你愿意给每一种要用到的View类型定义一个缩写函数,这还是挺有用的。</p>    <h2>Kotlin实用吗?</h2>    <p>到现在为止,我们通过type-safe builder模式、扩展方法与扩展属性、lambda with receiver写了一个快速构建View层级的方法。</p>    <p>所以你现在可能要问:我到底要不要以这种方式在我自己的APP中构建View?到现在为止,我一直在说这样做可以很简单很方便,但只与在Java中进行同样工作进行了比较,结果不言自明。不过在Android中我们一般通过XML资源来描述View,所以我们来对比一下通过XML与通过Kotlin的type-safe builder来构建View哪种更强一些。</p>    <h3>当Activity配置改变</h3>    <p><img alt="实战Kotlin@Android(三)" src="https://simg.open-open.com/show/132b0fb592d86b1b06af9bd64d534e4f.png"></p>    <p>Android设备中配置随时会改变,最简单的一种改变就是屏幕方向。当然还有其他各种改变,具体参见<a href="/misc/goto?guid=4959554152653088073">官方文档</a>。方向的改变对Android View的影响极大,因为我们经常会针对竖向和横向写两套布局。</p>    <p>对于XML布局而言,无需自己更新UI来应对配置改变,只需要写两套布局,一套放在res/layout-land下,另一套放在res/layout-port下就好。当在这两个目录中给文件相同的命名时,Android就会针对各种情况自己找到正确的文件来填充。总而言之,无需自己写代码来处理方向改变。</p>    <p>但当处理动态生成的layout时,就需要自己处理配置信息的改变了。如果你想要对各种配置有不同的布局,就需要自己写逻辑来判断要生成何种View。如果你总是需要这么做,代码就会变得笨重。</p>    <p>所以,如果对于每种配置信息要填充不同的UI,XML布局更加方便。</p>    <h3>当需要处理RelativeLayout等复杂布局</h3>    <p><img alt="实战Kotlin@Android(三)" src="https://simg.open-open.com/show/428300e02be158644cd8d217a9b16ff7.png"></p>    <p>RelativeLayout让我们以一种非常灵活的方式进行相对布局。你可以很简单地声明一个View要放在另一个锚View的上面下面左边右边,做这件事时需要将锚View的ID传入前者。View ID还用于在代码中对特定View进行配置修改。</p>    <p>在Android中,最好的方式就是让编译器自己给View ID赋值,你不应自己写ID,也就是说,你需要使用Android工具来指定这些ID以备后面使用。</p>    <p>在XML布局中,创建一个View ID很简单,只需要这样:</p>    <pre>  <code class="language-kotlin"><TextView android:id="@+id/tv" ... />  </code></pre>    <p>@+id注解会告诉Android定义一个叫做tv的新ID,或是重用有同样名称的已存在ID。代码不能更简单。</p>    <p>当处理动态生成的布局时,无法通过代码动态创建ID,为了创建新ID,你需要在XML中定义一个新ID,然后再在代码中通过编译后的R类来获取ID的引用。所以,动态操作View就要牵扯到另一个文件,而且在Android中,即使一个ID不再被引用,也不会被删除。</p>    <p>总的来说,在XML不居中更容易操作View的ID,因为有工具来支持ID的动态管理。</p>    <h3>如果需要在给View属性赋值时进行计算</h3>    <p>计算View的属性很常见,比如设置其内部文本时,或是背景色、尺寸时。</p>    <p>Android XML布局语言完全是声明性的,你不能在赋值字符串中进行计算。也就是说这些属性必须在View被填充后动态修改。这样一来布局文件中的定义与代码中的修改会被分开。Android开发者对这一点可并不陌生。</p>    <p>但当你动态构建View时,你可以直接将计算结果赋值给View属性,也可以将某个属性设置的与另外一个属性一样,毕竟我们经常需要给marginLeft和marginRight设置同样的值。</p>    <p>既然当动态构建View时可以享受代码带来的所有灵活性,在赋值方面肯定是动态创建View更加方便,尤其是配合Kotlin的type-safe builder。</p>    <h3>如何进行布局微调</h3>    <p><img alt="实战Kotlin@Android(三)" src="https://simg.open-open.com/show/7b0760887e162e1a90fd560ba1fb0f8c.png"></p>    <p>除非你能看到代码就能想象出图形界面,你肯定需要使用Android Studio的布局预览来对XML布局进行微调,而不需要构建项目并在终端上跑起来。</p>    <p>当你动态编写布局时,你不能快速预览。不过随着Android Studio2.0中Instant Run功能的推出,这一点不再那么困扰人了。但是就现在而言,修改布局的最简单的方式还是Android Studio预览功能。</p>    <h3>两种方式的性能如何?</h3>    <p>性能是考量填充View与动态构造View优劣的重要指标。在我一开始测试的时候,我发现构建一个简单的布局比填充几乎快一倍!但当我开始使用更多的Kotlin语法特色,并构建更复杂的View时,差别就不那么大了。在我的Nexus 6P上,我发现动态构建所花的时间约是填充的四分之三。我猜测这是因为填充时需要先解析布局资源然后构建View。</p>    <p>顺带说一句,在两种情况中,构建一个有5个View的View层级所花的时间均小于1秒,所以除非你要构建很多View,性能上不需要担心太多。</p>    <p>如果你想自己测试一下,源码可以在我的<a href="/misc/goto?guid=4959671728231427950">Github</a>上找到。</p>    <h3>使用Kotlin的大小上限是多少?</h3>    <p>在第一部分中我们提及了有关在Android中使用Kotlin的大小需求问题。你需要声明一个运行时和一个标准库的依赖。每当你想给一个Android app添加一个依赖,都应该考虑一下大小,尤其当你不能使用multidex时。</p>    <p>在Kotlin1.0.0中,运行时+标准库的大小总共是210k+636k=846k,这对一个库来说真的不小了。使用dex-method-counts统计出的方法数为6584,这只包含kotlin包下的,没有算里面引用的Java runtime方法。这一下就占了一个dex的10%的方法数。但当在我的测试工程上使用了ProGuard后,数目一下减到42个方法。最终的数目自然会增加,因为还需要用到Kotlin中的其他方法。在包含Kotlin的应用中使用ProGuard可以有效控制其大小。</p>    <h2>总结</h2>    <p>Kotlin用起来还是很愉快的,它可以直接应用在Android开发中。对于构建View来讲,它不是特别的厉害,因为使用XML布局有诸多优势,就现在而言是最佳的方式。但在某些情况下动态的构建View更符合需求,此时Kotlin就能很大程度上简化代码、优化风格。</p>    <p>你可能在思考使用Kotlin有没有其他的坑。这个问题问得好,在<a href="/misc/goto?guid=4959671728325568712">Reddit</a>上有讨论。</p>    <p>如果你想看一看我的测试项目,并将XML布局与Kotlin进行比较,可以clone我的<a href="/misc/goto?guid=4959671728231427950">这个repo</a></p>    <p>希望你一路读下来能有所收获!</p>    <p>来源:http://blog.chengdazhi.com/index.php/172</p>