Google 推出 Kotlin 作为 Android 的官方语言已经有一段时间,最近用工作上一些闲暇时间做了个项目,切身体验下。

一、需求描述

一直以来对各个网站的密码管理都比较头疼,因为担心“撞库”,所有网站密码都不相同。注册网站时都会随便写一个密码,却没有一个好的密码管理工具,下次登录时基本都需要找回密码,结果又忘记如此反复。对于普通的网站重新找回一次并不算复杂,但是对于像 QQ、微信、支付宝 这样有比较高安全验证的网站找回起来并不容易,处理起来很繁琐特别头疼(承认我记忆力不好,突然想到自己好几张银行卡密码也忘记了 ( ̄▽ ̄)~~~ ,不过也没存款。。。。。。)

之前找过管理密码的软件,但不是太放心。软件又没开源,也不确定有没有后门或漏洞,自己动手要踏实得多。

我的《密码本》正是基于这一需求产生的,不但让自己的密码相对有一个保障,同时练练手学习新的技术。最后该项目作为开源项目,希望也能帮助你解决同样的烦恼。

二、项目截图

GitHub: https://github.com/iOnesmile/PasswordNotebook

安装包: 百度云下载

三、待完善

  • 提升加密文件安全度,研究其它算法并检验安全性
  • 应用内安全验证,如数据存储、锁屏、页面超时、导出权限等
  • 优化交互体验,简化操作流程,和指纹解锁等验证机制
  • 其它平台开发(iOS、Windows、MacOS),信息同步
  • 语言国际化
  • 其它……

如果有什么好的想法和建议,或在使用中遇到什么问题,欢迎反馈,我们一起完善吧!!!

四、使用 Kotlin 的坑或技术总结

  1. 在设置监听时,提示错误 Expected a value of type Boolean
    原因:该监听有一个返回值,类型是 Boolean
    例如:

    textView.onLongClick {
        // TODO
        return@onLongClick true
    }
    
  2. EditText 设置值时提示 Type mismatch. Required: Editable! Found: String
    原因:要给 EditText 设置 String 类型的值时,需要使用 setText() 方法
    例如:

    editText.setText("XXX")
    
  3. 函数式编程
    • map
      映射函数也是一个高阶函数,将一个集合经过一个传入的变换函数映射成另外一种集合

    • filter
      筛选函数将用户给定的布尔逻辑作用于集合,返回由原集合中符合条件的元素组合的一个子集

    • reduce
      归纳函数将一个数据集合的所有元素通过传入的操作函数实现数据集合的积累叠加效果

五、使用技术/库

一、打包 aar

1、单个模块打包

  1. 打开 Gradle 工具窗口,找到 Android Library 模块. 在 build 任务中双击 assemble.

  2. 执行成功后,在 mylibrary/build/outputs/aar 目录下找到 aar 包.

默认 DebugReleaseAAR 包都会打出来,当然你也可以选择只打 Debug 的包,双击 assembleDebug 任务就可以了. 只打 Release 的包同理.

2、多个模块打包

当要打包的模块又依赖了其它几个模块时,常常需要把它们打包成一个 aar。多模块打包使用 fat-aar,打包关键步骤如下:

  1. 将下载好的 fat-aar.gradle 文件添加到对应的模块目录中,并在 build.gradle 中引入 apply from: 'fat-aar.gradle'。或直接引用 apply from: 'https://raw.githubusercontent.com/adwiv/android-fat-aar/master/fat-aar.gradle'

  2. 添加要打包的工程,使用 embedded 关键字。示例代码如下:

    apply from: 'fat-aar.gradle'
    dependencies {
        ...
       embedded project(':DynamicPageLibrary')
       embedded project(':VideoPlayerLib')
       embedded project(':AudioPlayLibrary')
       embedded project(':BaseCloudMusicResource')
    }
    
  3. 步骤同上《单个模块打包》一致。

二、引入 aar

方法一、通过 libs 引入到 app 中

  1. aar 文件放在 libs 目录下

  2. appbuild.gradle 中添加如下内容

    repositories {
        flatDir {
            dirs 'libs' 
        }
    }
    
  3. 之后通过如下方式引入
    dependencies {
        compile(name:'test', ext:'aar')
    }
    
  4. Rebuild project

  5. 如果发现引入后无法使用,重启 Android studio

方法二、把 aar 作为一个库工程的方式引入

当项目中库工程较多且依赖关系比较复杂时,最好采用这一种方式。如:某一个库工程也要引入这个 aar 时。

菜单栏 -> File -> New -> New Module

-> Import .Jar/.AAR Package

-> Next

-> 选择 File name 的文件 -> Subproject name 命名工程

-> Finish

创建完成后是一个工程,工程中包括 aar 文件和 build.gradle 文件。build.gradle 文件内容如下:

configurations.create("default")
artifacts.add("default", file('musiclibrary_20170622.aar'))

三,遇到的问题

  1. Non-constant Fields in Case Labels

    原因:在 Android Library 中不能使用 switch case

    解决:改成用 else if,如下图:

  2. java.lang.IllegalArgumentException: No view found for id 0x7f0d013d () for fragment TestFragment

    描述:在项目中引用了库里的 fragment,在运行后抛出了找不到 view 的异常。但是在 Demo 项目中运行是没有问题的。

    原因:库里 fragmentlayoutID 与项目中另外一个 FragmentlayoutID 名字相同,导致项目中的布局会覆盖库中的布局。

    解决:修改为不同的名称。在库中要注意资源名称可能与项目同名的问题,比如在库中的资源文件都添加前缀或后缀,或较长不容易重复的名字。同名的资源文件只会存在一个,根据库的嵌套关系,外层会覆盖内层的资源文件。

一、类与方法

1,类

  • 类的声明
    class Bar(var b: Int): Foo() {
        var c = 1
        init {
            println("class initializer")
        }
    
        constructor(): this(1) {
            println("secondary constructor")
        }
    }
    

    Bar类在这里继承了Foo类,Bar类有两个构造函数,直接在Bar类头的是primary constructor,另外一个构造函数使用constructor关键字定义,注意必须要先调用primary constructor,另外,init标明的是class initializer,每个构造函数都会首先调用class initializer里面的代码,再调用构造函数

  • 创建类的实例,不需要 new

    var bar = Bar()
    
  • 继承

    内定义默认是 final 的,要想能被继承,基类头必须有 open 注解

  • Inner class

    class Outer {
        class Inner {      
        }
    }
    

    与 Java 不同,Kotlin 中所有的内部类默认就是静态的,这样可以减少很多内存泄露的问题。如果需要在内部类中引用外部类对象,可以在Inner类的声明前加上inner关键字,然后在Inner类中使用标记的this:this@Outer来指向外部类对象

  • Singleton

    object Single {
        var c = 1
    
        fun foo() = println("foo")
    }
    

    单利对象用 object 关键字表示,可以直接使用 Single.foo() 来调用了

2,接口

interface Interface {
    fun foo() {
        println(1)
    }
    fun bar()
}

可以带有默认的实现方法,并且不允许通过属性来维护状态。

3,函数

  • 函数声明
    fun foo(va: Int): Int {
        return 1
    }
    
  • 也可以使用单行声明
    fun foo(va: Int): Int = 1
    
  • 重载

    与类的派生一样,允许重载的方法要有open注解,而在派生类中重载时要使用override注解

    override fun foo(va: Int): Int {
        return 2
    }
    

4,修饰符

5,成员变量的 Get 与 Set

注:
1,类和方法默认定义都是 final,以此来提高效率。类想要被继承用 open 关键字
2,类 和 成员变量 默认是 public 修饰

二、语法

1,语法糖,对类的扩充

在不修改类的原始定义的情况下实现对类的扩展,如下面的代码为Person类增加了一个名为isTeenager的扩展:

fun Person.isTeenager(): Boolean {
    return age in 13..19
}

2,排除空指针

  • 定义一个为空的变量是需要加上 ? 符号
    var text: String? = null
    
  • 操作一个可能为空的对象时,同样要加上 ? 符号
    var length = text?.length
    
  • 如果将该变量传递给函数,在参数后面需要加 !! 符号
    if (text != null) {
        customPrint(text!!)
    }
    
  • 如何去掉 !! 符号呢,当代码充满该符号时显然很不优雅,这时可以使用 let 函数
    text?.let { customPrint(it) }
    
  • 如果遇到多个参数的情况,你可以选择嵌套多个 let,但这样可读性并不好。比如:
    if (mUserName != null && mPhotoUrl != null) {
       uploadPhoto(mUserName!!, mPhotoUrl!!)
    }
    

    这时你可以构建一个全局函数:

    fun <T1, T2> ifNotNull(value1: T1?, value2: T2?, bothNotNull: (T1, T2) -> (Unit)) {
       if (value1 != null && value2 != null) {
           bothNotNull(value1, value2)
       }
    }
    

    调用方式

    ifNotNull(mUserName, mPhotoUrl, {name, url ->
            uploadPhoto(name, url)
    })
    

3,高阶函数和Lambda表达式

  • 例如给一个变量赋 lambda 表达式 {x,y->x+y}
    val sumLambda: (Int, Int) -> Int = {x,y -> x+y}
    
  • 定义一个可以传表达式的高阶函数

    kotlin
    fun doubleTheResult(x:Int, y:Int, f:(Int, Int)->Int): Int {
    return f(x,y) * 2
    }
    kotlin

  • 调用方法如下

    val result1 = doubleTheResult(3, 4, sumLambda)
    或
    val result2 = doubleTheResult(3, 4, {x,y -> x+y})
    

4,范围表达式

  • 范围创建只需要 .. 操作符,为升序,例如:
    // 该范围包含数值1,2,3,4,5
    val r1 = 1..5
    
  • 如果要表示降序,用 downTo 函数
    // 该范围包含数值5,4,3,2,1
    val r2 = 5 downTo 1
    
  • 如果步长不是1,则需要使用step函数
    // 该范围包含数值5,3,1
    val r3 = 5 downTo 1 step 2
    // 同理,升序的序列
    val r4 = 1..10 step 2
    

5,条件结构

  • if 表达式(类似于 Java 的 ?: 运算符)
    var age = 20
    val isEligibleToVote = if(age > 18) "Yes" else "No"
    
  • when表达式(类似于 Java 的 switch,但功能更强大)
    val age = 17
    
    val typeOfPerson = when(age){
        0 -> "New born"
        in 1..12 -> "Child"
        in 13..19 -> "Teenager"
        else -> "Adult"
    }
    

6,循环结构

使用 for..in 遍历数组、集合及其它提供了迭代器的数据结构,语法同Java几乎完全相同,只是用 in 操作符取代了 : 操作符

val names = arrayOf("Jake", "Jill", "Ashley", "Bill")

for (name in names) {
    println(name)
}

while 和 do..while 循环的语法与Java完全相同。

7,字符串模板

可以在字符串中嵌入变量表达式,例如:

val name = "Bob"
println("My name is ${name}") //打印"My name is Bob"

val a = 10
val b = 20
println("The sum is ${a+b}") //打印"The sum is 30"

三、XML 布局 + kotlin-android-extensions

1, 通常在 xml 中查找控件的写法

val name = find<TextView>(R.id.tv_name)
// 等同于 findViewById()
val name = findViewById(R.id.tv_name) as TextView
name.text="张三"

2,如果使用扩展后,可以直接调用并赋值

tv_name.text="张三"

环境配置

1,项目下面的 build.gradle 加入如下代码:

buildscript {

    ext.kotlin_version ="1.0.4"

    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

2,app 下面的 build.gradle 加入如下代码:

apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    sourceSets{
        main.java.srcDirs+='src/main/kotlin'
    }
}

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    compile 'org.jetbrains.anko:anko-sdk25:0.10.0-beta-1'// sdk15, sdk19, sdk21, sdk23 are also available
    compile 'org.jetbrains.anko:anko-appcompat-v7:0.10.0-beta-1'
}

3,Gradle Sync

4,菜单栏 —> Code —> Convert Java File to Kotlin File

Anko Layout

一、优点

1,运行速度快。XML布局是在运行时解析的,也就是说XML需要从资源文件中获取,然后 XmlPullParser 需要解析所有的元素并一个一个的创建它们。还要解析元素的属性,然后设置,这个过程非常繁重。

2,类型安全,不再需要那么多的 findById() 之后的类型转换。

3,null 安全,Kotlin 里,如果一个变量用?表示为可空,并且使用?之后再调用的时候,即使变量为空也不会引发异常。

4,代码复用,可以通过继承AnkoComponent的方式实现代码复用。XML布局是每一个Activity,每一个View各自专属一个,代码复用比较少。

二、缺点

1,Anko DSL 布局不能预览。虽然有一个叫 Anko Preview Plugin 的预览插件,但是每次修改后都需要 make 下才能预览,关键是在新版本 Android Studio2.2 以上都不支持。

  • 笔者在 Android studio2.3 上安装该插件,导致重启后无法进入项目界面。
  • 幸好在启动页面的左下角有一个 Config 选项,点击其中的 Plugin,卸载 Anko Preview 插件才可以正常启动。

2,定义 id 比较繁琐,需要定义一个变量,或者在 values 资源文件下定义 ids。不用 id 行不行呢?你去问问 RelativeLayout 答应不答应吧。

3,如果定义在 xml 的话,可以直接通过 id 使用对应的 View(XML 布局 + kotlin-android-extensions 的方式),但是在 Anko DSL 布局的话,只能通过定义变量的方式来实现。

4,动态替换外部资源以达到换肤的效果,那么 XML 显然比 Kotlin 代码要来得容易:前者可以编译成一个只有资源的 apk 供应用加载,后者的话就得搞一下动态类加载了。

三、引用方式

// 继承 AnkoComponent 创建布局
class LoginLayout<T> : AnkoComponent<T> {

    override fun createView(ui: AnkoContext<T>): View {
        return with(ui){
            ...
        }
    }
}

// Activity 中引用
override fun onCreate(savedInstanceState: Bundle?) {
    LoginLayout<MainActivity>().setContentView(this)
}

// Fragment 中引用
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    var view = LoginLayout<LoginFragment>().createView(AnkoContext.create(context, LoginFragment()))
    return view
}

四、常用语法

1,定义 TextView

textView("Hello") {
    textSize = 16f
    textColor = Color.RED
    backgroundResource = R.drawable.shape_et_bg
    gravity = Gravity.CENTER
}.lparams(matchParent, wrapContent){
    margin = dip(12)
    padding = dip(2)
}

2,提取样式

// 给 EditText 扩展样式方法
fun EditText.commonStyle(){
    textSize = 16f
    backgroundResource = R.drawable.shape_et_bg
}

// 直接在布局的后面添加上
.style {
    view ->
    when (view) {
        is Button -> {
            view.gravity = Gravity.CENTER
        }
        is TextView -> {
            view.gravity = Gravity.LEFT
            view.textSize = 20f
            view.textColor = Color.DKGRAY
        }
    }
}

3,设置点击事件

var etInput = editText {
    hint = "请输入文字"
    commonStyle()
}

button("点我"){
    // 在按钮属性内部设置点击事件
    onClick {
        toast("输入的内容:${etInput.text}")
    }
}

// 通过变量 + . 的方式设置
etInput.onClick { 

}

4,布局方式

val ID_USERNAME = 1

// 垂直布局,== LinearLayout + orientation="vertical"
verticalLayout {  }
// 相对布局,需要使用到 ID
relativeLayout {
    textView("姓名") {
        id = ID_USERNAME
    }
    textView("描述") {

    }.lparams {
        below(ID_USERNAME)
        alignParentLeft()
    }
}
// 线性布局
linearLayout {
    orientation = LinearLayout.HORIZONTAL
}
frameLayout { }
tableLayout { }

5,ui: AnkoContext

// 包含的变量
val ctx: Context
val owner: T
val view: View

// 例如,可以通过 owner 直接调用外部 Activity 的方法
override fun createView(ui: AnkoContext<T>): View {
    if (ui.owner is Activity) {
        (owner as Activity).onBackPressed()
    }
}

参考链接

Kotlin Primer·第二章·基本语法 https://kymjs.com/code/2017/02/04/01/

http://blog.csdn.net/io_field/article/details/53365834

只需五分钟,开始使用Kotlin开发Android
https://barryhappy.github.io/2016/10/20/start-kotlin-in-5-mins/

登陆注册 Demo
http://blog.csdn.net/xiehuimx/article/details/72354371

Kotlin 系统入门到进阶 视频教程 https://github.com/enbandari/Kotlin-Tutorials

官方文档:https://www.kancloud.cn/pholance/kotlin/125094

Gradle 核心是基于 Groovy 脚本语言,Groovy 脚本基于 Java 且拓展了 Java。因此 Gradle 需要依赖 JDK 和 Groovy 库。

快速安装 Groovy 可以通过 Bash,命令如下:

$ curl -s get.sdkman.io | bash
$ source "$HOME/.sdkman/bin/sdkman-init.sh"
$ sdk install groovy
// 查看版本,判断是否成功
$ groovy -version

关键字:

as、assert、break、case、catch、class、const、continue、def、default、do、else、enum、extends、false、finally、for、goto、if、implements、import、in、instanceof、interface、new、null、package、return、super、switch、this、throw、throws、trait、true、try、while

Hello Groovy

#!/usr/bin/env groovy
println "Hello Groovy."

一、类型定义

1,标识符

  • 普通标识符:只能以字母、美元符、下划线开始,不能以数字开头。
  • 引用标识符:引用标识符出现在点后的表达式中。
    // 定义一个空的 map 集合
    def map = [:]
    // 引用标示符中可以出现空格、横杆等
    map."a b-c" = "ALLOWED"
    // 断言,map 中标识符的值与右边的字符串相等
    assert map."a b-c" == "ALLOWED"
    

注:Groovy 中所有的字符串都可以当引用标识符。

2,字符串

Groovy 有 java.lang.Stringgroovy.lang.GString 两中字符串对象类型。

  • 单引号字符串

    java.lang.String 类型,不支持站位符插值操作。

  • 双引号字符串

    groovy.lang.GString 类型,支持站位符插值操作。其中插值占位符我们可以用 ${} 或者 $ 来标示,${} 用于一般替代字串或者表达式,$ 主要用于A.B的形式中。

  • 三重单引号字符串

    java.lang.String 类型,不支持站位符插值操作,可以标示多行字符串。

  • 多重双引号字符串

    支持站位插值操作,可以标示多行字符串。

  • 斜线字符串

    和双引号字符串很类似,通常用在正则表达式中。

    def name = 'Test Groovy!'
    def body = 'Test $name'
    // 单引号字符串中,占位符不会被替换
    assert body == 'Test $name'
    
    // 双引号字符串,*${}* 标识,括号内面的表达式会被计算,变量会被替换
    def sum = "The sum of 2 and 3 equals ${2 + 3}"
    assert sum.toString() == 'The sum of 2 and 3 equals 5'
    
    // 双引号字符串,*$* 标识,只对 A.B 有效,对括号、闭包等无效,会抛出 groovy.lang.MissingPropertyException 异常
    def person = [name: 'Guillaume', age: 36]
    assert "$person.name is $person.age years old" == 'Guillaume is 36 years old'
    
    // 三重单引号字符串,不支持站位符插值操作
    def aMultilineString = '''line one
    line two
    line three'''
    
    // 多重双引号字符串,支持站位符插值操作
    def name = 'Groovy'
    def template = """
        Hello, ${name}
        Welcome.
    """
    
    // 斜线字符串
    def fooPattern = /.*foo.*/
    assert fooPattern == '.*foo.*'
    // 多行支持
    def multilineSlashy = /one
        two
        three/
    // 含站位符使用支持
    def color = 'blue'
    def interpolatedSlashy = /a ${color} car/
    

3,字符 Characters

Groovy没有明确的Characters。但是我们可以有如下三种不同的方式来将字符串作为字符处理,譬如:

char c1 = 'A' 
assert c1 instanceof Character

def c2 = 'B' as char 
assert c2 instanceof Character

def c3 = (char)'C' 
assert c3 instanceof Character

4,数字 Numbers

  • 整型

    和 Java 一样,支持 byte、char、short、int、long、java.lang.BigInteger

  • 浮点型

    和 Java 一样,支持 float、double、java.lang.BigDecimal

    // 整型,‘0’开头,八进制表示
    int xInt = 077
    
    // 整型,‘0x’开头,十六进制表示
    int xInt = 0x77
    
    // 整型,‘0b’开头,二进制表示
    int xInt = 0b10101111
    
    // 浮点型,科学计数表示法
    assert 1e3  ==  1_000.0
    assert 2E4  == 20_000.0
    assert 3e+1 ==     30.0
    assert 4E-2 ==      0.04
    

5,Booleans 类型

def myBooleanVariable = true
boolean untypedBooleanVar = false
booleanField = true

6,Lists 集合

支持 java.util.List, 可以增删改对象,列表中类型不受限制,可以用超出列表范围的数来索引列表。

//使用动态List
def numbers = [1, 2, 3]         
assert numbers instanceof List  
assert numbers.size() == 3

//List中存储任意类型
def heterogeneous = [1, "a", true]

//判断List默认类型
def arrayList = [1, 2, 3]
assert arrayList instanceof java.util.ArrayList

//使用as强转类型
def linkedList = [2, 3, 4] as LinkedList    
assert linkedList instanceof java.util.LinkedList

//定义指定类型List
LinkedList otherLinked = [3, 4, 5]          
assert otherLinked instanceof java.util.LinkedList

//定义List使用
def letters = ['a', 'b', 'c', 'd']
//判断item值
assert letters[0] == 'a'     
assert letters[1] == 'b'
//负数下标则从右向左index
assert letters[-1] == 'd'    
assert letters[-2] == 'c'
//指定item赋值判断
letters[2] = 'C'             
assert letters[2] == 'C'
//给List追加item
letters << 'e'               
assert letters[ 4] == 'e'
assert letters[-1] == 'e'
//获取一段List子集
assert letters[1, 3] == ['b', 'd']         
assert letters[2..4] == ['C', 'd', 'e'] 

//多维List支持
def multi = [[0, 1], [2, 3]]     
assert multi[1][0] == 2 

7,Arrays 数组

和 Java 数组类似。

//定义初始化String数组
String[] arrStr = ['Ananas', 'Banana', 'Kiwi']  
assert arrStr instanceof String[]    
assert !(arrStr instanceof List)

//使用def定义初始化int数组
def numArr = [1, 2, 3] as int[]      
assert numArr instanceof int[]       
assert numArr.size() == 3

//声明定义多维数组指明宽度
def matrix3 = new Integer[3][3]         
assert matrix3.size() == 3

//声明多维数组不指定宽度
Integer[][] matrix2                     
matrix2 = [[1, 2], [3, 4]]
assert matrix2 instanceof Integer[][]

//数组的元素使用及赋值操作
String[] names = ['Cédric', 'Guillaume', 'Jochen', 'Paul']
assert names[0] == 'Cédric'     
names[2] = 'Blackdrag'          
assert names[2] == 'Blackdrag'

8,Maps 键值对

在Groovy中键key不一定是String,可以是任何对象(实际上 Groovy 中的 Map 就是java.util.LinkedHashMap)。

//定义一个Map
def colors = [red: '#FF0000', green: '#00FF00', blue: '#0000FF']   
//获取一些指定key的value进行判断操作
assert colors['red'] == '#FF0000'    
assert colors.green  == '#00FF00'
//给指定key的对赋值value操作与判断    
colors['pink'] = '#FF00FF'           
colors.yellow  = '#FFFF00'           
assert colors.pink == '#FF00FF'
assert colors['yellow'] == '#FFFF00'
//判断Map的类型
assert colors instanceof java.util.LinkedHashMap
//访问Map中不存在的key为null
assert colors.unknown == null

//定义key类型为数字的Map
def numbers = [1: 'one', 2: 'two']
assert numbers[1] == 'one'

对于Map需要特别注意一种情况,如下:

//把一个定义的变量作为Map的key,访问Map的该key是失败的
def key = 'name'
def person = [key: 'Guillaume']      
assert !person.containsKey('name')   
assert person.containsKey('key') 

//把一个定义的变量作为Map的key的正确写法---添加括弧,访问Map的该key是成功的
person = [(key): 'Guillaume']        
assert person.containsKey('name')    
assert !person.containsKey('key')   

二、运算符

下面介绍与 Java 不同的运算符,其它请参照 Java。

1,次方运算符(**)

assert  2 ** 3 == 8

def f = 3
f **= 2
assert f == 9

2,非运算符(!)

assert (!true)    == false    
// 支持字符串的判断,为空时返回 false,不为空时返回 true                  
assert (!'foo')   == false                      
assert (!'')      == true 

3,安全占位符(?.)

这个运算符主要用于避免空指针异常。

def person = Person.find { it.id == 123 }    
def name = person?.name                      
assert name == null  

4,直接域访问操作符(.@)

因为Groovy自动支持属性getter方法,但有时候我们有一个自己写的特殊getter方法,当不想调用这个特殊的getter方法则可以用直接域访问操作符。

class User {
    public final String name                 
    User(String name) { this.name = name}
    String getName() { "Name: $name" }       
}
def user = new User('Bob')

assert user.name == 'Name: Bob'
assert user.@name == 'Bob' 

5,方法指针操作符(.&)

因为闭包可以被作为一个方法的参数,如果想让一个方法作为另一个方法的参数则可以将一个方法当成一个闭包作为另一个方法的参数。

def list = ['a','b','c']  
//常规写法 
list.each{  
    println it  
}  

String printName(name){  
    println name  
}  

// 方法指针操作符写法,将迭代出的每一个值作为方法的参数
list.each(this.&printName)  

6,三目运算符(?:)

displayName = user.name ? user.name : 'Anonymous'   
// 简化为二目运算符,逻辑同上一样
displayName = user.name ?: 'Anonymous' 

7,展开运算符(*.)

一个集合使用展开运算符可以得到一个元素为原集合各个元素执行后面指定方法所得值的集合。

cars = [
   new Car(make: 'Peugeot', model: '508'),
   null,                                              
   new Car(make: 'Renault', model: 'Clio')]
assert cars*.make == ['Peugeot', null, 'Renault']     
assert null*.make == null 

三、程序结构

1,包名

和 Java 一致。

// defining a package named com.yoursite
package com.yoursite

2,Imports 引入

常规的导包和 Java 一致,有一个特殊。

//例1:
import groovy.xml.MarkupBuilder

// using the imported class to create an object
def xml = new MarkupBuilder()
assert xml != null

//例2:
import groovy.xml.*

def markupBuilder = new MarkupBuilder()
assert markupBuilder != null
assert new StreamingMarkupBuilder() != null

//例3:
import static Boolean.FALSE

assert !FALSE

//例4:特殊的,相当于用as取别名
import static Calendar.getInstance as now

assert now().class == Calendar.getInstance().class

注意:Groovy与Java类似,已经帮我们默认导入了一些常用的包,所以在我们使用这些包的类时就不用再像上面那样导入了,如下是自动导入的包列表:

import java.lang.*
import java.util.*
import java.io.*
import java.net.*
import groovy.lang.*
import groovy.util.*
import java.math.BigInteger
import java.math.BigDecimal

3,脚本与类

相对于传统的Java类,一个包含main方法的Groovy类可以如下书写:

class Main {                                    
    static void main(String... args) {          
        println 'Groovy world!'                 
    }
}

和Java一样,程序会从这个类的main方法开始执行,这是Groovy代码的一种写法,实际上执行Groovy代码完全可以不需要类或main方法,所以更简单的写法如下:

println 'Groovy world!'

上面这两中写法其实是一样的,具体我们可以通过如下命令进行编译为class文件:

groovyc demo.groovy //编译Groovy源码为class

我们使用反编译工具可以查看到这个demo.groovy类源码如下:

import org.codehaus.groovy.runtime.InvokerHelper
class Main extends Script {                     
    def run() {                                 
        println 'Groovy world!'                 
    }
    static void main(String[] args) {           
        InvokerHelper.runScript(Main, args)     
    }
}

可以看见,上面我们写的groovy文件编译后的class其实是Java类,该类从Script类派生而来(查阅API);可以发现,每个脚本都会生成一个static main方法,我们执行groovy脚本的实质其实是执行的这个Java类的main方法,脚本源码里所有代码都被放到了run方法中,脚本中定义的方法(该例暂无)都会被定义在Main类中。

通过上面可以发现,Groovy的实质就是Java的class,也就是说他一定会和Java一样存在变量作用域!对哦,前面我们解释变量时竟然没说到这个东东,这里说下吧。看下面例子:

//单个Groovy源码文件,运行会报错找不到num变量
def num = 1 
def printNum(){  
    println num  
}

//单个Groovy源码文件,运行会报错找不到num变量
int num = 1 
def printNum(){  
    println num  
}  

//单个Groovy源码文件,运行OK成功
num = 1 
def printNum(){  
    println num  
} 

上面的例子可以发现,我们如果想要在Groovy的方法中使用Groovy的变量则不能有修饰符。然而,如果我们想在B.groovy文件访问A.groovy文件的num变量咋办呢,我们可以使用Field注解,具体操作如下:

import groovy.transform.Field;
@Field num = 1

哈哈,这就是Groovy的变量作用域了,如果你想知道上面这些写法为啥出错,很简单,自己动手整成Java源码相信你一定可以看懂为啥鸟。

四、闭包

1,语法

定义一个闭包:

// [closureparameters -> ]是可选的逗号分隔的参数列表
{ [closureParameters -> ] statements }

参数可以定义,也可以不定义,如果不定义默认有一个 it 的参数。

//使用显示的名为参数
{ name -> println name }                            
//接受两个参数的闭包
{ String x, int y ->                                
    println "hey ${x} the value is ${y}"
}
//包含一个参数多个语句的闭包
{ reader ->                                         
    def line = reader.readLine()
    line.trim()
}

一个闭包其实就是一个groovy.lang.Closure类型的实例,因此可以如下定义:

//定义一个Closure类型的闭包
def listener = { e -> println "Clicked on $e.source" }      
assert listener instanceof Closure
//定义直接指定为Closure类型的闭包
Closure callback = { println 'Done!' }                      
Closure<Boolean> isTextFile = {
    File it -> it.name.endsWith('.txt')                     
}

调用闭包,可以调用 call,也可以不

def isOdd = { int i-> i%2 == 1 }                            
assert isOdd(3) == true                                     
assert isOdd.call(2) == false

2,参数

参数有如下规则:参数类型可选,参数默认值可选,多个参数必须用逗号隔开。

def closureWithOneArg = { str -> str.toUpperCase() }
assert closureWithOneArg('groovy') == 'GROOVY'

def closureWithOneArgAndExplicitType = { String str -> str.toUpperCase() }
assert closureWithOneArgAndExplicitType('groovy') == 'GROOVY'

def closureWithTwoArgs = { a,b -> a+b }
assert closureWithTwoArgs(1,2) == 3

def closureWithTwoArgsAndExplicitTypes = { int a, int b -> a+b }
assert closureWithTwoArgsAndExplicitTypes(1,2) == 3

def closureWithTwoArgsAndOptionalTypes = { a, int b -> a+b }
assert closureWithTwoArgsAndOptionalTypes(1,2) == 3

def closureWithTwoArgAndDefaultValue = { int a, int b=2 -> a+b }
assert closureWithTwoArgAndDefaultValue(1) == 3

当一个闭包没有显式定义一个参数列表时,闭包总是有一个隐式的it参数。

def greeting = { "Hello, $it!" }
assert greeting('Patrick') == 'Hello, Patrick!'

当然,如果你想声明一个不接受任何参数的闭包,且必须限定为没有参数的调用,那么你必须将它声明为一个空的参数列表,如下:

def magicNumber = { -> 42 }
// this call will fail because the closure doesn't accept any argument
magicNumber(11)

Groovy的闭包支持最后一个参数为不定长可变长度的参数,具体用法如下:

def concat1 = { String... args -> args.join('') }           
assert concat1('abc','def') == 'abcdef'                     
def concat2 = { String[] args -> args.join('') }            
assert concat2('abc', 'def') == 'abcdef'

def multiConcat = { int n, String... args ->                
    args.join('')*n
}
assert multiConcat(2, 'abc','def') == 'abcdefabcdef'

3,闭包省略调用

很多方法的最后一个参数都是一个闭包,我们可以在这样的方法调运时进行略写括弧。比如:

def debugClosure(int num, String str, Closure closure){  
      //dosomething  
}  

debugClosure(1, "groovy", {  
   println"hello groovy!"  
})

可以看见,当闭包作为闭包或方法的最后一个参数时我们可以将闭包从参数圆括号中提取出来接在最后,如果闭包是唯一的一个参数,则闭包或方法参数所在的圆括号也可以省略;对于有多个闭包参数的,只要是在参数声明最后的,均可以按上述方式省略。

四、GDK(Groovy Development Kit)

Groovy除了可以直接使用Java的JDK以外还有自己的一套GDK,其实也就是对JDK的一些类的二次封装罢了;一样,这是GDK官方API文档,写代码中请自行查阅。

1,I/O 操作

Groovy提供了很多IO操作的方法,你可以使用Java的那写IO方法,但是没有Groovy的GDK提供的简单牛逼。

//读文件打印脚本
new File('/home/temp', 'haiku.txt').eachLine { line ->
    println line
}

//读文件打印及打印行号脚本
new File(baseDir, 'haiku.txt').eachLine { line, nb ->
    println "Line $nb: $line"
}

可以看见,这是一个读文件打印每行的脚本,eachLine方法是GDK中File的方法,eachLine的参数是一个闭包,这里采用了简写省略括弧。

当然了,有时候你可能更加喜欢用Reader来操作,使用Reader时即使抛出异常也会自动关闭IO。如下:

def count = 0, MAXSIZE = 3
new File(baseDir,"haiku.txt").withReader { reader ->
    while (reader.readLine()) {
        if (++count > MAXSIZE) {
            throw new RuntimeException('Haiku should only have 3 verses')
        }
    }
}

接着我们再看几个关于读文件的操作使用,如下:

//把读到的文件行内容全部存入List列表中
def list = new File(baseDir, 'haiku.txt').collect {it}
//把读到的文件行内容全部存入String数组列表中
def array = new File(baseDir, 'haiku.txt') as String[]
//把读到的文件内容全部转存为byte数组
byte[] contents = file.bytes

//把读到的文件转为InputStream,切记此方式需要手动关闭流
def is = new File(baseDir,'haiku.txt').newInputStream()
// do something ...
is.close()

//把读到的文件以InputStream闭包操作,此方式不需要手动关闭流
new File(baseDir,'haiku.txt').withInputStream { stream ->
    // do something ...
}

上面介绍了一些常用的文件读操作,其它的具体参见API和GDK吧。

写文件操作:

有了上面的读操作,接下来直接看几个写操作的例子得了,如下:

//向一个文件以utf-8编码写三行文字
new File(baseDir,'haiku.txt').withWriter('utf-8') { writer ->
    writer.writeLine 'Into the ancient pond'
    writer.writeLine 'A frog jumps'
    writer.writeLine 'Water’s sound!'
}
//上面的写法可以直接替换为此写法
new File(baseDir,'haiku.txt') << '''Into the ancient pond
A frog jumps
Water’s sound!'''
//直接以byte数组形式写入文件
file.bytes = [66,22,11]
//类似上面读操作,可以使用OutputStream进行输出流操作,记得手动关闭
def os = new File(baseDir,'data.bin').newOutputStream()
// do something ...
os.close()
//类似上面读操作,可以使用OutputStream闭包进行输出流操作,不用手动关闭
new File(baseDir,'data.bin').withOutputStream { stream ->
    // do something ...
}

上面介绍了一些常用的文件写操作,其它的具体参见API和GDK吧。

文件树操作:

在脚本环境中,遍历一个文件树是很常见的需求,Groovy提供了多种方法来满足这个需求。如下:

//遍历所有指定路径下文件名打印
dir.eachFile { file ->                      
    println file.name
}
//遍历所有指定路径下符合正则匹配的文件名打印
dir.eachFileMatch(~/.*\.txt/) { file ->     
    println file.name
}
//深度遍历打印名字
dir.eachFileRecurse { file ->                      
    println file.name
}
//深度遍历打印名字,只包含文件类型
dir.eachFileRecurse(FileType.FILES) { file ->      
    println file.name
}
//允许设置特殊标记规则的遍历操作
dir.traverse { file ->
    if (file.directory && file.name=='bin') {
        FileVisitResult.TERMINATE                   
    } else {
        println file.name
        FileVisitResult.CONTINUE                    
    }
}

执行外部程序:

Groovy提供一种简单方式来处理执行外部命令行后的输出流操作。如下:

def process = "ls -l".execute()             
println "Found text ${process.text}"

execute方法返回一个java.lang.Process对象,支持in、out、err的信息反馈。在看一个例子,如下:

def process = "ls -l".execute()             
process.in.eachLine { line ->               
    println line                            
}

上面使用闭包操作打印出执行命令行的输入流信息。

二、有用的工具类操作

ConfigSlurper配置:

ConfigSlurper是一个配置管理文件读取工具类,类似于Java的*.properties文件,如下:

def config = new ConfigSlurper().parse('''
    app.date = new Date()  
    app.age  = 42
    app {                  
        name = "Test${42}"
    }
''')

assert config.app.date instanceof Date
assert config.app.age == 42
assert config.app.name == 'Test42'

上面介绍了一些常用的属性配置操作,其它的具体参见API和GDK吧。

Expando扩展:

def expando = new Expando()
expando.toString = { -> 'John' }
expando.say = { String s -> "John says: ${s}" }

assert expando as String == 'John'
assert expando.say('Hi') == 'John says: Hi'

上面介绍了一些常用的拓展操作,其它的具体参见API和GDK吧。

还有很多其他操作,这里就不一一列举,详情参考官方文档即可,譬如JSON处理、XML解析啥玩意的,自行需求摸索吧。

五,DSL(Domain Specific Languages)领域相关语言

这个就不特殊说明了,只在这里提一下,因为我们前边很多地方已经用过它了,加上我们只是干货基础掌握,所以不做深入探讨。

DSL是一种特定领域的语言(功能领域、业务领域),Groovy是通用的编程语言,所以不是DSL,但是Groovy却对编写全新的DSL提供了很好的支持,这些支持来自于Groovy自身语法的特性,如下:

  • Groovy不需用定义CLASS类就可以直接执行脚本;
  • Groovy语法省略括弧和语句结尾分号等操作;

所以说这个基础入门没必要特别深入理解,简单的前面都用过了,理解DSL作用即可,点到为止,详情参考官方文档。

参考链接:
Groovy脚本基础全攻略: http://blog.csdn.net/yanbober/article/details/49047515
Gradle脚本基础全攻略: http://blog.csdn.net/yanbober/article/details/49314255

Gradle 插件解析:

通过自定义 Gradle 插件修改编译后的 class 文件 https://juejin.im/entry/577b03438ac2470061afb130

在AndroidStudio中自定义Gradle插件 http://blog.csdn.net/huachao1001/article/details/51810328

一、MVC/MVP/MVVP 框架的理解

  • MVC 全名是 Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。

    Android 中的 MVC:

    • 视图层(View):一般采用 XML 文件进行界面的描述,这些 XML 可以理解为 Android 应用的 View。
    • 控制层(Controller):Android的控制层的重任通常落在了众多的 Activity/Fragment 的肩上。
    • 模型层(Model):针对业务模型,建立的数据结构和相关的类,就可以理解为 Android 应用的Model,Model 是与 View 无关,而与业务相关。对数据库的操作、对网络和对业务计算等操作都应该在该层处理。
      MVC 模式的缺点
      在Android开发中,Activity并不是一个标准的MVC模式中的Controller,同时它还负责加载布局、初始化界面和接受并处理用户的操作请求。随着界面及其逻辑的复杂度不断提升以致 Activity 变得庞大臃肿。
  • MVP 从更早的MVC框架演变过来,与MVC有一定的相似性:Controller/Presenter负责逻辑的处理,Model提供数据,View负责显示。在MVP模式里通常包含3个要素(加上View interface是4个):

    • View:负责绘制 UI 元素、与用户进行交互(在 Android 中体现为 Activity)
    • Model:负责存储、检索、操纵数据(有时也实现一个 Model interface 用来降低耦合)
    • Presenter:作为 View 与 Model 交互的中间纽带,处理与用户交互的负责逻辑
    • View interface:需要 View 实现的接口,View 通过 View interface 与 Presenter 进行交互,降低耦合,方便进行单元测试
  • MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。唯一的区别是,它采用双向绑定(data-binding):View的变动,自动反映在 ViewModel,反之亦然。这就使得视图和控制层之间的耦合程度进一步降低,关注点分离更为彻底,同时减轻了Activity的压力。

二、如何构建一个 MVP 的框架

1,TodoMVP 框架结构

有如下几个步骤:
1. 在 Activity 中初始化 Fragment 和 Presenter 的实例,并给 Fragment 设置 Presenter
2. Fragment 实现了 View 的接口,并拥有 Presenter 的引用,Fragment 的非 UI 操作都通过调用 Presenter 来实现,同时 Presenter 把处理好的结果通过回调 View 回传给 Fragment 显示
3. Presenter 拥有 View 的引用,具体的处理事情并回调 View 返回接口

正因为 UI 与具体实现相分离,使 Activity 只需要根据回传状态渲染 UI,Presenter 中也只用考虑处理逻辑,将处理好的状态告知 View 即可。在应用 UI 经常变更时会有非常大的优势。

2,写一个简单的搜索 Demo,输入关键字,在点击搜索按钮时开始搜索,并将返回结果显示出来。

SearchContract 的实现,View 中包含搜索成功和搜索失败的回调,Presenter 中主要是搜索方法和 Activity 生命周期的方法。

public interface SearchContract {

    interface View extends BaseView<SearchContract.Presenter> {

        void onSearchSuccess(String result);

        void onSearchFailure(String message);
    }

    interface Presenter extends BasePresenter {

        void stop();

        void startSearch(String searchKey);
    }
}

在 MainActivity 中实现了 SearchContract.View,并创建 SearchPresenter 实例和初始化 UI。

public class MainActivity extends AppCompatActivity implements SearchContract.View {

    private SearchContract.Presenter mPresenter;

    private EditText etSearchKey;
    private TextView tvResult;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        new SearchPresenter(this, new DataManager());

        etSearchKey = (EditText) findViewById(R.id.et_search_key);
        tvResult = (TextView) findViewById(R.id.tv_result);

        findViewById(R.id.btn_search).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String searchKey = etSearchKey.getText().toString();
                if (TextUtils.isEmpty(searchKey)) {
                    showToast("搜索关键字不能为空!");
                    return;
                }
                mPresenter.startSearch(searchKey);
            }
        });
    }

    @Override
    public void onResume() {
        super.onResume();
        mPresenter.start();
    }

    @Override
    protected void onStop() {
        super.onStop();
        mPresenter.stop();
    }

    @Override
    public void onSearchSuccess(String result) {
        tvResult.setText(result);
    }

    @Override
    public void onSearchFailure(String message) {
        tvResult.setText(message);
    }

    @Override
    public void setPresenter(SearchContract.Presenter presenter) {
        mPresenter = presenter;
    }

    public void showToast(String message) {
        Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
    }
}

创建 SearchPresenter 实现 SearchContract.Presenter 接口,其中的搜索方法通过调用 Model 层的接口获取数据,并处理完结果后将状态回调给 View。

public class SearchPresenter implements SearchContract.Presenter {

    private SearchContract.View searchView;
    private CompositeSubscription mCompositeSubscription;
    private DataManager dataManager;
    private String mResult;

    public SearchPresenter(SearchContract.View searchView, DataManager dataManager) {
        this.searchView = searchView;
        this.dataManager = dataManager;

        this.searchView.setPresenter(this);
    }

    @Override
    public void start() {
        mCompositeSubscription = new CompositeSubscription();
    }

    @Override
    public void stop() {
        if (mCompositeSubscription.hasSubscriptions()) {
            mCompositeSubscription.unsubscribe();
        }
    }

    @Override
    public void startSearch(String searchKey) {
        mCompositeSubscription.add(dataManager.getSearchResult(searchKey)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<ResponseBody>() {
                    @Override
                    public void onCompleted() {
                        if (mResult != null) {
                            searchView.onSearchSuccess(mResult);
                        }
                    }

                    @Override
                    public void onError(Throwable e) {
                        e.printStackTrace();
                        searchView.onSearchFailure("请求失败!!!");
                    }

                    @Override
                    public void onNext(ResponseBody responseBody) {
                        if (responseBody != null) {
                            try {
                                mResult = responseBody.source().readUtf8();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                })
        );
    }
}

三、一些好的框架设计流程

  1. AOP 在 Android 框架中的应用

AOP(Aspect-Oriented Programming, 面向切面编程),诞生于上个世纪90年代,是对OOP(Object-Oriented Programming, 面向对象编程)的补充和完善。OOP引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。对于其他类型的代码,如安全性、异常处理和透明的持续性也是如此。这种散布在各处的无关的代码被称为横切(Cross-Cutting)代码,在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。而AOP技术则恰恰相反,它利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即方面。所谓“方面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。

AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处都基本相似。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

示例:Android AOP 框架(SAF-AOP)

四、Google 推荐的框架结构蓝图

GoogleSamples 的中有推荐了一些好的架构图,如下:

Demo 下载地址:https://github.com/iOnesmile/ModularizationDemo

参考网站:
Android App的设计架构:MVC,MVP,MVVM与架构经验谈