1.开发环境,2.数据类型,3.控制语句,4.函数运用,5.类和对象

目录

  • 第一章 搭建Kotlin开发环境
    • 1.5.1 Kotlin代码和Java代码PK
    • 1.5.2 Anko库
    • 1.5.3 Lambda表达式
  • 第二章 数据类型
    • 2.1 简单变量之间的转换
    • 2.2 数组变量的声明
    • 2.3 字符串
    • 2.4 容器
      • Set/MutableSet(集合)
      • List/MutableList(队列)
      • Map/MutableMap(映射)
  • 第三章 控制语句
    • 3.1条件分支
      • 简单分支
      • 多路分支
      • 类型判断
    • 3.2循环处理
      • 遍历循环
      • 条件循环
      • 跳出多重循环
    • 3.3空安全
      • 字符串的有效性判断
      • 声明可空变量
      • 校验空值的运算符
      • 总结
    • 3.4等式判断
      • 结构相等
      • 引用相等
      • s和in
    • 3.5小结
    • 第四章 函数运用
    • 4.1 函数的基本用法
    • 4.2 输入参数的变化
      • 入参的默认值
      • 命名参数
      • 可变参数
    • 4.3几种特殊函数
      • 泛型函数
      • 内联函数
      • 简化函数
      • 尾递归函数tailrec
      • 高阶函数
    • 4.4 增强系统函数
      • 扩展函数
      • 拓展高阶函数
      • 日期时间函数
      • 单例对象
    • 4.5小结
  • 第五章 类和对象
    • 5.1类的构造
      • 类的简单定义
      • 类的构造函数
      • 带默认参数的构造参数
    • 5.2类的成员
      • 成员属性
      • 成员方法
      • 伴生对象
      • 静态属性
    • 5.3类的继承
      • 开放性修饰符
      • 普通类的继承
      • 抽象类
      • 接口
      • 接口代理
    • 5.4几种特殊类
      • 嵌套类
      • 内部类
      • 枚举类
      • 密封类
      • 数据类
      • 模板类(泛型类)
    • 5.5小结
  • 第六章 Kotlin使用简单控件
    • 6.1使用按钮控件
      • 按钮button
      • 复选框CheckBox
      • 单选按钮RadioButton
    • 6.2使用页面布局
      • 线性布局LinearLayout
      • 相对布局 RelativeLayout
      • 约束布局 ConstraintLayout
    • 6.3使用图文控件
      • 图像视图 ImageView
      • 文本编辑框EditText
    • 6.4Activity活动跳转
      • 传送配对字段数据
      • 传送序列化数据
      • 跳转时指定启动模式
        • 清单文件设置
        • 代码设置
      • 处理返回数据 startActivityForResult
    • 6.5实战项目
    • 6.6总结
  • 第七章 Kotlin操纵复杂控件
    • 7.1使用视图排列
      • 下拉框spinner
      • 网格视图
      • 循环视图RecyclerView
    • 7.2使用材质设计
      • 协调布局CoordinatorLayout
        • Toolbar的常用方法
      • 应用栏布局AppBarLayout
      • 可折叠工具栏布局CollapsingToolbarLayout
    • 7.3实现页面切换
      • 翻页视图ViewPager
      • Fragment
      • Tablayout
    • 广播Broadcast
      • 系统广播
    • 总结
  • 第八章 Kotlin进行数据存储
    • 8.1使用共享参数SharedPreferences
      • 共享参数读写模板Preference
      • 属性代理等黑科技
        • 模板类:
        • 委托属性/属性代理
        • lazy修饰符
        • with函数
      • 实现记住密码的功能
    • 8.2使用数据库SQLite
      • 数据库帮助器SQLiteOpenHelper
      • 更安全的ManagedSQLiteOpenHelper
      • 优化记住密码功能
    • 8.3 文件I/O操作
      • 读写文本文件
      • 读写图片文件
      • 遍历文件目录
      • 利用Application实现全局变量
      • 总结
  • 第九章 Kotlin自定义控件
    • 9.1自定义普通视图
      • 构造对象
      • 测量尺寸
      • 绘制
    • 9.3自定义通知栏
      • Notification
      • 远程视图RemoteView
    • 9.4 Service服务
      • 普通启动服务
      • 绑定方式启动服务
      • 推送服务到前台
      • 总结
  • 第十章 Kotlin实现网络通信
    • 10.1多线程
    • 10.2访问HTTP接口
      • 移动数据格式JSON
      • HTTP接口调用
    • 10.3文件下载操作
    • 10.4 ContentProvider
      • 内容提供者 ContentProvider
      • 内容解析器:ContentResolver
      • 内容观察器:ContentObserver
      • SpannableString

第一章 搭建Kotlin开发环境

1.5.1 Kotlin代码和Java代码PK

可以把Kotlin看做是Java的升级版,不但完全兼容Java,而且极大的精简了语法,让开发者专注于业务逻辑代码,无须关心代码框架,若想充分运用Kotlin的优异特性,除了导入Kotlin的核心库,还得导入Kotlin的扩展库和anko库
Kotlin的优势
1.控件

tv_test.setText("张飞");可以简化为tv_test.text="张飞"

2.监听

btn_toast.setOnClickListener { toast("点击事件") }
btn_toast_long.setOnLongClickListener { longToast(" 长按事件并longtoast"); true }

1.5.2 Anko库

1.5.3 Lambda表达式

Lambda表达式其实是一个匿名函数
Java是从1.8开始支持Lambda表达式,所以Java代码只能从1.8开始使用。但是Kotlin一诞生就支持Lambda表达式,所以不在乎JDK版本是1.7还是1.8

第二章 数据类型

2.1 简单变量之间的转换

toInt,toLong toString

2.2 数组变量的声明

分配一个常量数组:

var long_array:LongArray = longArrayOf(1, 2, 3)
var float_array:FloatArray = floatArrayOf(1.0f, 2.0f, 3.0f)
var double_array:DoubleArray = doubleArrayOf(1.0, 2.0, 3.0)
var boolean_array:BooleanArray = booleanArrayOf(true, false, true)
var char_array:CharArray = charArrayOf('a', 'b', 'c')var ins:IntArray = intArrayof(1,2,3)

String数组的话比较特殊,如下:

var string_array:Array<String> = arrayOf("How", "Are", "You")

其他的数组类型也可以这样写

var int_array:Array<Int> = arrayOf(1, 2, 3)
var long_array:Array<Long> = arrayOf(1, 2, 3)
var float_array:Array<Float> = arrayOf(1.0f, 2.0f, 3.0f)
var double_array:Array<Double> = arrayOf(1.0, 2.0, 3.0)
var boolean_array:Array<Boolean> = arrayOf(true, false, true)
var char_array:Array<Char> = arrayOf('a', 'b', 'c')

数组元素的操作
string_array[i]和 string_array.get(i) 都可以获取元素

2.3 字符串

字符串转换为其他类型:toInt,toDouble
获取字符串指定位置的元素:

tv_convert.text = origin[number].toString()
tv_convert.text = origin.get(number).toString()

字符串模板:${origin.length}

2.4 容器

容器默认是只读容器,如果需要允许修改容器,需要加上Mutable前缀,如MutableSet,MutableList,MutableMap,只有可变容器才能进行增删改

Kotlin中的3个循环方式:for in,迭代器,forEach
无论是for in还是迭代器,写法都不够简洁,所以Kotlin创造了forEach采用匿名函数的形式,it代表每个元素

Set/MutableSet(集合)

集合是一种最简单的容器
1.容器内部的元素按顺序排列,因此无法按照下标访问
2.容器内部元素不重复,通过hash值校验是否存在相同的元素,若存在则覆盖它

Set/MutableSet有以下不足:
1.集合不允许修改内部元素的值
2.集合无法删除指定位置的元素
3.不能通过下标获取指定位置的元素
所以实际开发基本用不到集合,而是用队列(List)和映射(Map)

List/MutableList(队列)

有序排列的容器,
1.可以通过下标和get方法获取元素
2.add方法默认在队列末尾,也可以指定位置
3.set方法替换或修改指定位置元素
4.removeAt删除指定位置元素
5.除3种遍历方式外多了一种元素下标遍历的方式
sortBy和sortByDescending可以升序和降序排列

Map/MutableMap(映射)

1.constainsKey和constainsValue判断是否存在相同的key和value
2.put方法添加或修改元素
3.remove通过key删除元素

//to方式初始化
var goodsMap: Map<String, String> = mapOf(" key " to "value")
//Pair方式初始化
var goodsMutMap: MutableMap<String, String> = mutableMapOf(Pair(" key", "value"))

4.3种遍历方式都支持

第三章 控制语句

3.1条件分支

简单分支

tv_answer.text = if (is_odd==true) “打他” else “不打他”
仿佛Java中的三元运算符:变量名= 条件语句?A:B,可是Kotlin并不提供这个三元运算符,使用上面的if else语句就实现了相同的功能,所以三元就取消了。

多路分支

when/else

when/else和switch/case的区别:
1.switch被when替代
2.case 常量值被 常量值->替代
3.break取消了,因为kotlin默认一个分支处理完了自动跳出语句
4.default被else替代

简化写法:

btn_when_value.setOnClickListener {tv_answer.text = when (count) {0 -> "张飞"
1 -> "刘备"
else -> "默认"
}

Java中的switch/case有个限制,case后面只能是常量,不能是变量,Kotlin的when/case则可以使用变量。
拓展多个相同条件

btn_when_region.setOnClickListener {tv_answer.text = when (count) {1,3,5,7,9 -> "张飞"
in 13..19 -> "刘备"
!in 6..10 -> "许褚"
else -> "默认"
}

类型判断

Java中判断变量是否是String类型
if(a instanceof String)
Kotlin 中则使用
if(a is String)代替
when/else也支持类型判断

tv_answer.text = when (countType) {is Long -> "是Long类型"
is Double -> "是浮点类型"
else -> "默认"
}

3.2循环处理

遍历循环

for (int i=0; i<array.length; i++) 方式被替代为:for (i in array.indices) indices表示下标数组

条件循环

// 左闭右开区间,合法值包括11不包括66
for (i in 11 until 66) { … }
// 默认递增1这里默认递增4
for (i in 23…89 step 4) { … }
// for循环默认递增downTo代表递减
for (i in 50 downTo 7) { … }

可是这些方法并不完美,业务需求是千变万化的,并非几种固定模式可以解决,所以while循环和Java处理是一致的

btn_repeat_begin.setOnClickListener {var poem:String=""
var i:Int = 0
while (i < poemArray.size) {if (i%2 ==0) {poem = "$poem${poemArray[i]}?\n"
} else {poem = "$poem${poemArray[i]}?\n"
}
i++
}
poem = "${poem}??????${i}??"
tv_poem_content.text = poem
}

跳出多重循环

while+continue+break
通过@标记直接跳出多重循环

btn_repeat_break.setOnClickListener {var i:Int = 0
var is_found = false
//给外层循环加个outside的标记
outside@ while (i < poemArray.size) {var j:Int = 0
var item = poemArray[i];
while ( j < item.length) {if (item[j] == '?') {is_found = true
//直接跳出outside循环
break@outside
}
j++
}
// 如果内层循环直接跳出两层循环,那么下面的判断就不需要了
// if (is_found)
// break
i++
}
tv_poem_content.text = if (is_found) "找到了" else "没找到"}

总结:
对于循环,Kotlin仍然保留for和while两种循环,主要区别是Kotlin取消了for(int i =0;i++;i<10)的规则,同时新增了跳出多重循环的支持:break@标记位

3.3空安全

字符串的有效性判断

Kotlin校验字符串空值的几个方法:
isNullOrEmpty : 为空指针或者字串长度为0时返回true,非空串与可空串均可调用。

isNullOrBlank : 为空指针或者字串长度为0或者全为空格时返回true,非空串与可空串均可调用。

isEmpty : 字串长度为0时返回true,只有非空串可调用。

isBlank : 字串长度为0或者全为空格时返回true,只有非空串可调用。

isNotEmpty : 字串长度大于0时返回true,只有非空串可调用。

isNotBlank : 字串长度大于0且不是全空格串时返回true,只有非空串可调用。

声明可空变量

Kotlin中默认声明的变量默认都是非空,可空则需要加?
非空和可空字符串
var strNotNull:String = “”
var strCanNull:String?

strNotNull允许调用全部6个方法
strCanNull只允许调用isNullOrEmpty和isNullOrBlank两个方法
所以Kotlin对可空串进行了编译检查,一旦发现可空串调用了非空方法,会提示语法错误

下面获取字符串的长度的判断

val strA:String = “非空”
val strB:String? = null
val strC:String? = “可空串”
strA可以直接调用:strA.length,对于strB和strC,必须进行非空判断,否则编译不通过

校验空值的运算符

?: 类似Java中的三元运算符
!!表示强行把该变量从可空类型转为非空类型,从而避免变量是否非空的校验,!!强行放弃了非空判断,开发者就得自己判断了。//!!表示 不做非空判断,如果变量为空,就是抛出异常,只有确保为非空时才能使用!!

btn_exclamation_two.setOnClickListener {try {length = strB!!.length
tv_check_result.text = "$length"
} catch(e:Exception) {tv_check_result.text = "空指针异常"
}
}

总结

Kotlin引入了空安全的概念,并在编译时开展对象是否为空的校验。相关的操作符说明概括如下:

1、声明对象实例时,在类型名称后面加问号,表示该对象可以为空;

2、调用对象方法时,在实例名称后面加问号,表示一旦实例为空就返回null;

3、新引入运算符“?:”,一旦实例为空就返回该运算符右边的表达式;

4、新引入运算符“!!”,通知编译器不做非空校验,运行时一旦发现实例为空就扔出异常;

3.4等式判断

结构相等

Kotlin把字符串当做跟整型一样的基本数据类型,统一运算符

这种不比较存储地址,只比较变量结构内部值的行为,Kotlin称之为结构相等

引用相等

有时候结构相等并不是真的相等,判断相等需要一种由内而外的全部相等判断,该准则叫引用相等,意思是除了值相等,引用的地址也必须相等,=和!
引用相等校验的是变量的唯一性,结构相等校验的是变量的等值性

s和in

除了判断是否相等,还可以判断是否为某种类型,数组是否存在某个元素
运算符is和!is代替instansof
运算符in和!in ,变量名in数组名,判断该数组中是否存在此变量

3.5小结

本章可以学到
1.Kotlin的简单分支和多路分支
2.Kotlin的遍历循环和条件循环
3.可空变量,可空变量的处理
4.结构相等和引用相等,类型判断,数组存在判断

第四章 函数运用

4.1 函数的基本用法

kotlin默认函数就是public,省略了public
空安全机制,如果变量允许为空,需要在变量类型后面加?

Kotlin中的函数和变量的声明写法类似

var i:Int
fun main():Int

功能定义var对fun,参数类型Int对Int,唯一的区别就是函数定义多了()以及()内部的入参,Kotlin设计师的初衷正是想把函数作为一个特殊的变量

Java使用void表示不存在返回参数,Kotlin的返回参数一定存在,即使不返回,也会默认返回一个Unit对象
//Unit类型表示没有返回参数,也可以直接省略Unit声明

fun getDinnerUnit():Unit {tv_process.text = "张飞"
tv_result.text = ""
}

4.2 输入参数的变化

入参的默认值

fun getFourBigDefault(general:String, first:String=" 第一", second:String=" 第二 "):String {var answer:String = "$general:$first,$second"
return answer
}

命名参数

修改默认值

getFourBigDefault("第一个") 只修改第一个参数的值
getFourBigDefault(second="第二个")只修改第二个参数的值

使用了命名参数表达式:second=“第二个”,该表达式实现了给指定参数赋值的功能。

可变参数

Java中的可变参数是String… strs,在Kotlin中新增了vararg关键字代替

fun getFourBigVararg(general:String, first:String=" 第一", second:String=" 第二", vararg otherArray: String?):String {var answer:String = "$general:$first,$second"
//循环取出可变参数数组
for (item in otherArray) {answer = "$answer,$item"
}
return answer
}

可变数组也是可以的 :vararg otherArray: Array

4.3几种特殊函数

泛型函数

定义泛型函数时,要在函数名称前面添加 <T>,入参也肯定包含<T>
fun <T> appendString(tag:String, vararg otherInfo: T?):String {var str:String = "$tag:"for (item in otherInfo) {str = "$str${item.toString()},"
}
return str
}
调用
appendString<Int>("小于10的素数",2,3,5,7)
appendString<Double>("花钱的日子",5.20,6.18,11.11,12.12)

内联函数

//该函数既不接收Array<Int>也不接收Array<Double>
fun setArrayNumber(array:Array<Number>) {var str:String = "数组排列"
for (item in array) {str = str + item.toString() + ", "
}
tv_function_result.text = str
}
//只有内联函数才可以被具体化
inline fun <reified T : Number> setArrayStr(array:Array<T>) {var str:String = "数组排列"
for (item in array) {str = str + item.toString() + ", "
}
tv_function_result.text = str
}var int_array:Array<Int> = arrayOf(1, 2, 3)
var float_array:Array<Float> = arrayOf(1.0f, 2.0f, 3.0f)
var double_array:Array<Double> = arrayOf(11.11, 22.22, 33.33)
//Kotlin要求参数类型完全匹配,所以Int继承Number也不能调用 setArrayNumber传送Int类型
//btn_generic_number.setOnClickListener { setArrayNumber(int_array) }
btn_generic_number.setOnClickListener {when (count%3) {0 -> setArrayStr<Int>(int_array)
1 -> setArrayStr<Float>(float_array)
else -> setArrayStr<Double>(double_array)
}
count++
}

简化函数

函数其实是一种特殊的变量,既然是变量那么就可以用=赋值

fun factorial(n:Int):Int {if (n <= 1) n
else n*factorial(n-1)
}
可以简写为:
fun factorial(n:Int):Int = if (n <= 1) n else n*factorial(n-1)

尾递归函数tailrec

如果函数尾部递归调用自身,加上tailrec关键字表示是一个尾递归函数,此时编译器会优化递归,用循环代替递归,从而避免栈溢出的情况。

tailrec fun findFixPoint(x: Double = 1.0): Double
= if (x == Math.cos(x)) x else findFixPoint(Math.cos(x))

高阶函数

A函数作为B函数的入参,那么B函数称为高阶函数,A函数称为函数变量

fun <T> maxCustom(array: Array<T>, greater: (T, T) -> Boolean): T? {var max: T? = null
for (item in array)
if (max == null || greater(item, max))
max = item
return max
}greater就是函数变量

调用

因为高阶函数maxCustom是泛型函数,所以要在函数名称后面加<String>:var string_array:Array<String> = arrayOf("How", "do", "you", "do")maxCustom<String>(string_array, { a, b -> a.length > b.length })

第二个参数被大括号包了起来,这是Lambda表达式的匿名函数写法,中间的->把匿名函数分为2部分,前半部分表示输入函数,后半部分表示函数体。{ a, b -> a.length > b.length }按照规范的写法是下面这样的,

fun anonymous(a:String, b:String):Boolean {var result:Boolean = a.length > b.length
return result
}

4.4 增强系统函数

扩展函数

开发者给系统类补写新的方法,比如扩展系统的Array类,可以在你定义的函数名称前加上Array,Array

拓展函数结合泛型函数
fun <T> Array<T>.swap(pos1: Int, pos2: Int) {val tmp = this[pos1] //this表示数组变量自身
this[pos1] = this[pos2]
this[pos2] = tmp
}

有了拓展函数,数组变量可以直接调用,像是系统自带的方法一样

拓展高阶函数

maxCustom是求数组元素的最大值,可以把该方法拓展到Array类中,数组变量就可以直接调用了

fun <T> Array<T>.maxCustomize(greater: (T, T) -> Boolean): T? {var max: T? = null
for (item in this)
if (max == null || greater(item, max))
max = item
return max
}

日期时间函数

除了数组,日期和时间的函数还是很常见的,一般我们都封装一个工具类,在Kotlin中我们拓展一下Date类

//只返回日期字符串
fun Date.getNowDate(): String {val sdf = SimpleDateFormat("yyyy-MM-dd")
return sdf.format(this)
}
//只返回时间字符串
fun Date.getNowTime(): String {val sdf = SimpleDateFormat("HH:mm:ss")
return sdf.format(this)
}
//返回详细时间字符串,精确到毫秒
fun Date.getNowTimeDetail(): String {val sdf = SimpleDateFormat("HH:mm:ss.SSS")
return sdf.format(this)
}

调用 :

Date().getNowDate()

单例对象

虽然拓展函数已经实现日期的获取,但是稍显繁琐,比如:Date().getNowDate(),一共占了4个括号。而且这些方法是依赖系统类来拓展的,如果没有系统类,那么也就无法拓展了。所以这个时候,Java中的Uitls工具类反而更灵活,更广泛,那么Kotlin有没有工具类的写法呢?

Java中一般采用static方法作为工具类,表示静态,无需对类进行构造即可直接调用。
既然是工具,那么一旦制定了规格就不会改变了,不能构造也不能修改,所以Kotlin使用object修饰,称为单例对象,相当于Java中的工具类。

我们用单例对象改造后

object DateUtil {//返回日期
val nowDate: String
get() {val sdf = SimpleDateFormat("yyyy-MM-dd")
return sdf.format(Date())
}
//返回时间
val nowTime: String
get() {val sdf = SimpleDateFormat("HH:mm:ss")
return sdf.format(Date())
}
//返回具体时间
val nowTimeDetail: String
get() {val sdf = SimpleDateFormat("HH:mm:ss.SSS")
return sdf.format(Date())
}
}

调用:

DateUtil.nowDateTime

4.5小结

1.定义输入和输出的完整函数
2.输入参数的几种特殊定义,包括默认参数,命名参数,可变参数,以及调用
3.特殊函数的使用,包括泛型函数,单例函数,简化函数,尾递归函数,高阶函数
4.合理利用拓展函数,单例对象对系统函数进行功能增强

第五章 类和对象

5.1类的构造

类的简单定义

Kotlin省略了pubic,默认就是public
:代替extends
Kotlin进行继承时,后面多了()
Kotlin对类初始化的方法是init,Java则为构造函数
Kotlin创建实例省略new

类的构造函数

Kotlin把函数看成特殊变量,那么类就能看成特殊函数,所以构造方法可以直接加在类名后面,而init方法是实例化该类的初始化动作。
Kotlin有主构造函数和二级构造函数的概念,来满足多个构造函数的情况

class AnimalMain constructor(context:Context, name:String) {init {context.toast("$name")
}
constructor(context:Context, name:String, sex:Int) : this(context,
name) {var sexName:String = if(sex==0) "?" else "?"
context.toast("??${name}?${sexName}?")
}
}

二级构造函数没有函数名称,只用关键字constructor表示
二级构造函数需要调用主构造函数

但是测试中,发现会调用2次toast,能否不强制调用主构造函数呢?
Kotlin设定了主构造函数不是必须的,可以去掉主构造函数,这样两个构造函数是互相独立的,就不会去调用主构造函数了

class AnimalSeparate {
constructor(context:Context, name:String) {
context.toast("这是$name")
}
constructor(context: Context, name:String, sex:Int) {
var sexName:String = if(sex==0) "公" else "母"
context.toast("${name}?${sexName}")
}
}

带默认参数的构造参数

如此折腾一番,貌似跟Java一样了,没有简化,别急,Kotlin还有大招
可以把它们合并成一个带默认参数的主构造函数,既能输入2个参数,也能输入3个参数

//类的主构造函数使用了默认参数
class AnimalDefault (context: Context, name:String, sex:Int = 0) {
init {
var sexName:String = if(sex==0) "?" else "?"
context.toast("??${name}?${sexName}?")
}
}

但是这个默认参数只支持Kotlin代码调用,若想让Java也识别默认参数,得往该类的构造函数添加注解@JvmOverloads,告知编译器这个类是给Java重载用的

//加上@JvmOverloads的目的是让Java代码也能识别默认参数
//因为添加了注解,所以必须加上constructor
class AnimalDefault @JvmOverloads constructor(context: Context, name:St
ring, sex:Int = 0) {
init {
var sexName:String = if(sex==0) "?" else "?"
context.toast("??${name}?${sexName}?")
}
}

现在Java也能像Kotlin一样调用多构造方法了。
总结:
主构造函数的入参在类名后面声明,函数体则位于init方法中,二级构造函数必定先调用主构造函数,Kotlin构造函数也支持默认参数,避免了冗余的构造函数定义

5.2类的成员

成员属性

Java方式保存入参的值,太啰嗦了

class WildAnimal (name:String, sex:Int = 0) {var name:String
val sex:Int
init {this.name = name
this.sex = sex
}
}

可以简化为:

class WildAnimal (var name:String, val sex:Int = 0) {}

只有2个改动之处:
增加关键字var,表示声明与该参数同名的可变属性并自动赋值
增加关键字val,表示声明与该参数同名的不可变属性并自动赋值
外部调用,就可以直接调用name:

var animal = WildAnimal("小狗", "公")
${animal.name}

所以Kotlin精简了太多:
1.冗余的同名属性声明语句
2.冗余的同名属性赋值语句
3.冗余的属性的set和get方法
如果某个字段并非入参的同名属性,就需要在内部显式的声明

class WildAnimalMember (var name:String, val sex:Int = 0) {//非空的属性必须在声明是赋值或者在构造函数中赋值(本例是在构造函数中)
//否则编译器会报“Property must be initialized or be abstract”
var sexName:String
init {sexName = if(sex==0) "?" else "?"
}
}

外部当然可以这样访问了:${animal.sexName}

成员方法

class WildAnimalFunction (var name:String, val sex:Int = 0) {var sexName:String
init {sexName = if(sex==0) "公" else "母"
}
//成员方法和普通方法一样
fun getDesc(tag:String):String {return "${sexName}"
}
}

调用:

var animal = WildAnimalFunction(animalName, animalSex)
animal.getDesc("动物园")

伴生对象

Java中有个static静态方法,Kotlin去掉了static,无法直接声明静态成员,用伴生对象代替

class WildAnimalCompanion (var name:String, val sex:Int = 0) {var sexName:String
init {sexName = if(sex==0) "公" else "母"
}
fun getDesc(tag:String):String {return "{sexName}"
}
//在类加载时就运行伴生对象的代码块,其作用相当于Java里面的static { ... }静态代码块
?
//关键字companion表示伴随,object表示对象,WildAnimal表示伴生对象的名称
companion object WildAnimal{fun judgeSex(sexName:String):Int {var sex:Int = when (sexName) {"公","雄" -> 0
"母","雌" -> 1
else -> -1
}
return sex
}
}
}

外部调用:

WildAnimalCompanion.WildAnimal.judgeSex("公")
和简化的
WildAnimalCompanion.judgeSex("公") (更像Java中的static调用)

静态属性

class WildAnimalConstant(var name:String, val sex:Int = MALE) {var sexName:String
init {sexName = if(sex==MALE) "?" else "?"
}
fun getDesc(tag:String):String {return "????$tag???${name}?${sexName}??"
}
companion object WildAnimal{//静态常量的值是不可变的,所以要使用关键字val修饰
val MALE = 0
val FEMALE = 1
val UNKNOWN = -1
fun judgeSex(sexName:String):Int {var sex:Int = when (sexName) {"公","雄" -> MALE
"母","雌" -> FEMALE
else -> UNKNOWN
}
return sex
}
}
}

Kotlin 的类成员分为实例成员和静态成员两种
实例成员包括成员属性和成员方法,入参同名的成员属性可以在构造函数中直接声明,外部调用必须通过类的实例才能访问成员属性和成员方法。
静态成员包括静态属性和静态方法,在类的伴生对象中定义,外部可以通过类名直接访问静态成员。

5.3类的继承

开放性修饰符

Kotlin默认每个类都不能被继承,如果要让某个类成为基类,就需把该类开放出来,也就是添加关键字open作为修饰符

普通类的继承

open class Bird (var name:String, val sex:Int = MALE) {//变量,方法,类默认都是public,一般把public省略了
//public var sexName:String
var sexName:String
init {sexName = getSexName(sex)
}//open和private不能共存
//open private fun getSexName(sex:Int):String {open protected fun getSexName(sex:Int):String {return if(sex==MALE) "?" else "?"
}fun getDesc(tag:String):String {return "${name},${sexName}"
}companion object BirdStatic{val MALE = 0
val FEMALE = 1
val UNKNOWN = -1
fun judgeSex(sexName:String):Int {var sex:Int = when (sexName) {"?","?" -> MALE
"?","?" -> FEMALE
else -> UNKNOWN
}
return sex
}
}
}

下面我们来继承看看

//父类Bird已经在构造函数中声明了属性,所以Duck不用重复声明
//即子类的构造函数在入参签名无需增加val和var
class Duck(name:String="??", sex:Int = Bird.MALE) : Bird(name, sex) {}

子类也可以定义新的成员属性和成员方法,或者重写父类的open方法

抽象类

//子类的构造函数,原来的入参不用加var和val,新增的入参必须加var或val
//因为抽象类不能直接使用,所以构造函数不必给默认参数赋值
abstract class Chicken(name:String, sex:Int, var voice:String) : Bird(n
ame, sex) {val numberArray:Array<String> = arrayOf("?","?","?","?","?","?"
,"?","?","?","?");
//抽象方法必须在子类进行重写,所以可以省略关键字open,因为abstract方法默认就是open类型的
open??
//open abstract fun callOut(times:Int):String
abstract fun callOut(times:Int):String
}

下面创建个实体类继承 抽象方法:

class Cock(name:String=" 鸡 ", sex:Int = Bird.MALE, voice:String=" 嘎嘎嘎") : Chicken(name, sex, voice) {override fun callOut(times: Int): String {var count = when {//when判断大于小于时,要把完整的判断条件写到每个分支中
times<=0 -> 0
times>=10 -> 9
else -> times
}
return "$sexName$name${voice},${numberArray[count]}"
}
}

接口

Kotlin和Java一样也只能继承一个类,所以只能通过接口定义几个抽象方法,然后在实现类中重写,从而间接实现C++中的多重继承功能。
注意点:
1.接口不能定义构造函数
2.接口的内部方法要被实现类重写,这些方法默认为抽象方法
3.于Java不同的是,Kotlin允许接口内实现非抽象方法,而Java接口中必须全部是抽象方法

interface Behavior {//接口内部默认是抽象open方法,所以可以省略abstract和open
open abstract fun fly():String
//比如这个方法,就省略了
fun swim():String//kotlin接口内部允许实现方法,该方法不是抽象方法,不过依然是open类型,接口内部默认open
fun run():String {return "跑步"
}
//Kotlin接口允许声明抽象属性,实现该接口的方法必须重载该属性
//和方法一样,open和abstract也可以省略
//open abstract var skilledSports:String
var skilledSports:String
}

我们创建一个实现类来实现接口:

class Goose(name:String="鹅", sex:Int = Bird.MALE) : Bird(name, sex), B
ehavior {override fun fly():String {return "起飞"
}
override fun swim():String {return "额鹅鹅鹅"
}
//这个run方法可实现,可不实现
override fun run():String {//super用来调用父类的属性和方法,由于Kotlin的接口支持实现方法,所以super所指的对象也就是父类的interface
return super.run()
}
//重载了来之接口的抽象属性
override var skilledSports:String = "??"
}

接口代理

class BehaviorFly : Behavior {override fun fly():String {return "飞1"
}
override fun swim():String {return "游泳1"
}
override fun run():String {return "跑1"
}
override var skilledSports:String = "飞翔"
}class BehaviorSwim : Behavior {override fun fly():String {return "飞2"
}
override fun swim():String {return "游泳2"
}
override fun run():String {return "跑2"
}
override var skilledSports:String = "游泳"
}
class BehaviorRun : Behavior {override fun fly():String {return "飞3"
}
override fun swim():String {return "游泳3"
}
override fun run():String {return "跑3"
}
override var skilledSports:String = "奔跑"
}

下面定义一个代理类:

//只有接口才能使用关键字by进行代理操作
class WildFowl(name:String, sex:Int=MALE, behavior:Behavior) : Bird(nam
e, sex), Behavior by behavior {}

外部调用:代理类的入参要传入具体的行为对象

WildFowl("老鹰", Bird.MALE, BehaviorFly())
WildFowl("企鹅", behavior=BehaviorSwim())
WildFowl("鸵鸟", Bird.MALE, BehaviorRun())

总结:
1.Kotlin的类默认不可被继承,必须添加open才可以,而Java默认允许被继承,除了final类
2.Kotlin新增了修饰符internal,表示只对本模块(app自身)开放
3.:代替Java中的extends和implement
4.Kotlin接口中可以实现方法,而Java接口中全部必须是抽象方法
5.Kotlin引入接口代理的概念,而Java中不存在代理的写法

5.4几种特殊类

嵌套类

Java中的嵌套类允许访问外部类的成员,而Kotlin的嵌套类不允许访问外部类的成员

class Tree(var treeName:String) {// 内部类即嵌套类
class Flower (var flowerName:String) {fun getName():String {return "$flowerName"
//普通嵌套类不能访问外部成员
//return "${treeName},${flowerName}"}}
}

调用嵌套类:

//调用嵌套类时,只能引用外部类的类名,不能调用外部类的构造函数
val peachBlossom = Tree.Flower("桃花");
peachBlossom.getName()

内部类

普通嵌套类不能访问外部类的成员,那么如果想访问怎么办呢?
inner关键字,加载嵌套类class的前面即可,就可以访问外部类的成员了

class Tree(var treeName:String) {// 加上inner 关键字就变成了内部类
inner class Flower (var flowerName:String) {fun getName():String {return "${treeName},${flowerName}"}}
}

调用内部类:

//调用内部类时,必须调用外部类的构造函数
val peach = Tree("桃树").Flower("桃花");
tv_class_secret.text = peach.getName()

总结:
内部类比嵌套类多了inner关键字
内部类可以访问外部类的成员
调用内部类必须调用外部类的构造函数,调用嵌套类只能调用外部类的类名

枚举类

Java中的枚举类:

enum Season {
SPRING, SUMMER, AUTUMN, WINTER}

Kotlin中的枚举类:

enum class SeasonType {SPRING, SUMMER, AUTUMN, WINTER
}
enum class SeasonName (val seasonName:String) {SPRING("春天"),
SUMMER("夏天"),
AUTUMN("秋天"),
WINTER("冬天")
}

调用:

btn_class_enum.setOnClickListener {if (count%2 == 0) {//ordinal表示枚举类型的序号,name表示枚举类型的名称
tv_class_secret.text = when (count++%4) {SeasonType.SPRING.ordinal -> SeasonType.SPRING.name
SeasonType.SUMMER.ordinal -> SeasonType.SUMMER.name
SeasonType.AUTUMN.ordinal -> SeasonType.AUTUMN.name
SeasonType.WINTER.ordinal -> SeasonType.WINTER.name
else -> "未知"
}
} else {tv_class_secret.text = when (count++%4) {//使用自定义属性seasonName表示
SeasonName.SPRING.ordinal -
> SeasonName.SPRING.seasonName
SeasonName.SUMMER.ordinal -
> SeasonName.SUMMER.seasonName
SeasonName.AUTUMN.ordinal -
> SeasonName.AUTUMN.seasonName
SeasonName.WINTER.ordinal -
> SeasonName.WINTER.seasonName
else -> "未知"
枚举类的构造函数是给枚举类型使用的,外部不能直接调用枚举类的构造函数
//else -> SeasonName("??").name
}
}
}

密封类

当when语句判断枚举类的时候,末尾例行公事加了else分支,因为when语句不知道有4种枚举,因此以防万一,必须要有else分支。为了解决判断多余分支的问题,Kotlin提出了"密封类"的概念,密封类像是一种更严格的枚举类,它内部有且仅有自身的实例对象,密封类采用了嵌套类的手段,它的嵌套类全部由自身派生而来,仿佛一个家谱。

sealed class SeasonSealed {//密封类内部的每个嵌套类都必须继承该类
class Spring (var name:String) : SeasonSealed()
class Summer (var name:String) : SeasonSealed()
class Autumn (var name:String) : SeasonSealed()
class Winter (var name:String) : SeasonSealed()
}

有了密封类,外部使用when就不需要else分支了
调用:

btn_class_sealed.setOnClickListener {var season = when (count++%4) {0 -> SeasonSealed.Spring("春天")
1 -> SeasonSealed.Summer("夏天")
2 -> SeasonSealed.Autumn("秋天")
else -> SeasonSealed.Winter("冬天")
}
//密封类是一种严格的枚举类,它的值是一个有限的集合
//密封类确保条件分支覆盖了所有的枚举类型,因此不再需要else分支
tv_class_secret.text = when (season) {is SeasonSealed.Spring -> season.name
is SeasonSealed.Summer -> season.name
is SeasonSealed.Autumn -> season.name
is SeasonSealed.Winter -> season.name
}
}

数据类

即Java中的Bean实体类,Java中的做法:
1.定义字段,构造函数
2.定义get /set方法
3.只想修改对象的某几个字段的值,其他字段也必须修改
4.调试时,得把每个字段的值都打印出来
这些任务都毫无技术含量可言,所以Kotlin有了数据类

只需要在class前面增加data关键字,并声明完整参数的构造函数,即可实现以下功能:
1.自动声明于构造函数入参同名的属性字段
2.自动实现每个属性字段的get/set方法
3.自动提供equals方法,用于比较两个数据对象是否相等
4.自动提供copy方法,允许完整负责数据对象,也可在复制后单独修改某几个字段的值
5.自动提供toString方法,便于打印数据对象中保存的值

下面我们马上定义一个:

//数据类必须有主构造函数,且至少有一个输入参数
//并且要声明与输入参数同名的属性,即输入参数前面添加var或val
//数据类不能是基类,也不能是子类,不能是抽象类,也不能是内部类,更不能是密封类
data class Man(var name:String, var age:Int, var sex:String) {}

精简的前提是要有规范:
1.数据类必须有主构造函数,且至少有一个输入参数,因为它的属性字段要和输入参数一一对应,如果没有属性字段,那么也就不是数据类了
2.输入参数前面添加var或val,这保证每个入参都会自动声明同名的属性字段
3.只能是独立的类,不能是其他类型的类,否则不同规则之间会产生冲突

var man = Man("张飞", "25", "男")
//数据类的copy方法不带参数,表示复制一模一样的对象
var man2 = man.copy()
//数据类的copy方法带参数,表示指定参数另外赋值
var man3 = man.copy(name="诸葛亮")
//数据类自带equals方法和toString方法
man.equals(man2)
man.toString()

copy,equals,toString方法都是数据类自带的,提高了开发者的编码效率

模板类(泛型类)

常见的ArrayList,HashMap,AsyncTask都是模板类
举个例子:
计算小河的长度,如果输入数字就以m为单位,如果输入汉字就以米为单位

//在类名后面添加<T>,表示这个是一个模板类
class River<T> (var name:String, var length:T) {fun getInfo():String {var unit:String = when (length) {is String -> "米"
//Int,Long,Float,Double都是数字类型的Number
is Number -> "m"
else -> ""
}
return "${name}的长度是$length$unit"
}
}

调用的时候,要在类名后面补充<参数类型>,从而动态指定实际的参数类型。
正如声明变量那样,编译器根据初始值判断变量类型,就不用显式指定类型
模板类也有这种偷懒写法,编译器根据入参的值就能知晓参数类型,那么调用的时候,就不用显式指定<参数类型>了

btn_class_generic.setOnClickListener {var river = when (count++%4) {//模板类声明对象时,要在模板类的类名后面加上<参数类型>
0 -> River<Int>("小溪", 100)
//如果编译器根据入参能判断参数类型,那么可以省略
1 -> River("小溪", 99.9f)
//当然保守起见,还是按照规矩添加<参数类型>
2 -> River<Double>("??", 50.5)
//如果你已经是老手了,怎么方便怎么来,Kotlin的涉及初衷就是偷懒
else -> River("大河", "一千")
}tv_class_secret.text = river.getInfo()
}

5.5小结

1.类的定义和主构造函数和二级构造函数
2.类内部定义的成员属性和成员方法,伴生对象的静态属性和静态方法
3.修饰符,继承抽象类,接口,接口代理
4.特殊类:嵌套类,内部类,枚举类,密封类,数据类,模板类

附加:
Kotlin系列之let、with、run、apply、also函数的使用

第六章 Kotlin使用简单控件

6.1使用按钮控件

按钮button

按钮事件三种Kotlin编码方式:匿名函数,内部类,接口实现

1.匿名函数方式

btn_click_anonymos.setOnClickListener { v ->
//Kotlin变量类型转换使用as
toast("${(v as Button).text}")
}btn_click_anonymos.setOnLongClickListener { v ->
//Kotlin变量类型转换使用as
longToast("${(v as Button).text}")
true
}

2.内部类方式

private inner class MyClickListener : View.OnClickListener {override fun onClick(v: View) {toast("${(v as Button).text}")
}
}
private inner class MyLongClickListener : View.OnLongClickListener {override fun onLongClick(v: View): Boolean {longToast("${(v as Button).text}")
return true
}
}

调用的时候:

btn_click_inner.setOnClickListener(MyClickListener())
btn_click_inner.setOnLongClickListener(MyLongClickListener())

3.接口实现方式
内部类的话,每个事件都要定义一个内部类,多了也不好,试试接口实现

class ButtonClickActivity : AppCompatActivity(), OnClickListener, OnLon
gClickListener {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)
setContentView(R.layout.activity_button_click)btn_click_interface.setOnClickListener(this)
btn_click_interface.setOnLongClickListener(this)
}override fun onClick(v: View) {if (v.id == R.id.btn_click_interface) {toast("${(v as Button).text}")
}
}
override fun onLongClick(v: View): Boolean {if (v.id == R.id.btn_click_interface) {longToast("${(v as Button).text}")
}
return true
}
}

复选框CheckBox

单选按钮RadioButton

同Java

6.2使用页面布局

线性布局LinearLayout

  1. Kotlin允许对属性orientation直接赋值,从而取代了setOrientation方法;类似的还有属性gravity取代了setGravity方法;
  2. Kotlin使用关键字as进行变量的类型转换;
  3. Kolin支持调用dip方法将dip数值转换为px数值,倘若由Java编码则需开发者自己实现一个像素转换的工具类;
    因为dip方法来自于Kotlin扩展的Anko库,所以需要在Activity代码头部加上下面一行导入语句:
    import org.jetbrains.anko.dip

相对布局 RelativeLayout

rl_params.addRule和rl_params.above

约束布局 ConstraintLayout

layout_constraintTop_toTopOf : 该控件的顶部与另一个控件的顶部对齐
layout_constraintTop_toBottompOf : 该控件的顶部与另一个控件的底部对齐
layout_constraintBottom_toTopOf : 该控件的底部与另一个控件的顶部对齐
layout_constraintBottom_toBottomOf : 该控件的底部与另一个控件的底部对齐
layout_constraintLeft_toLeftOf : 该控件的左侧与另一个控件的左侧对齐
layout_constraintLeft_toRightOf : 该控件的左侧与另一个控件的右侧对齐
layout_constraintRight_toLeftOf : 该控件的右侧与另一个控件的左侧对齐
layout_constraintRight_toRightOf : 该控件的右侧与另一个控件的右侧对齐

若要利用代码给约束布局动态添加控件,则可照常调用addView方法,不同之处在于,新控件的布局参数必须使用约束布局的布局参数,即ConstraintLayout.LayoutParams,该参数通过setMargins/setMarginStart/setMarginEnd方法设置新控件与周围控件的间距,至于新控件与周围控件的位置约束关系,则可参照ConstraintLayout.LayoutParams的下列属性说明:
topToTop : 当前控件的顶部与指定ID的控件顶部对齐
topToBottom : 当前控件的顶部与指定ID的控件底部对齐
bottomToTop : 当前控件的底部与指定ID的控件顶部对齐
bottomToBottom : 当前控件的底部与指定ID的控件底部对齐
startToStart : 当前控件的左侧与指定ID的控件左侧对齐
startToEnd : 当前控件的左侧与指定ID的控件右侧对齐
endToStart : 当前控件的右侧与指定ID的控件左侧对齐
endToEnd : 当前控件的右侧与指定ID的控件右侧对齐

6.3使用图文控件

###文本视图TextView
ellipsize+TextUtils.TruncateAt.MARQUEE 跑马灯效果

Java
tv_marquee.setGravity(Gravity.LEFT | Gravity.CENTER);
Kotlin
tv_marquee.gravity = Gravity.LEFT or Gravity.CENTER

图像视图 ImageView

文本编辑框EditText

6.4Activity活动跳转

传送配对字段数据

Java:
Intent intent = new Intent(MainActivity.this, LinearLayoutActivity.class);
startActivity(intent);Kotlin anko
startActivity<LinearLayoutActivity>()带参数的
Java:
Intent intent = new Intent(this, ActSecondActivity.class);
intent.putExtra("request_time", DateUtil.getNowTime());
intent.putExtra("request_content", et_request.getText().toString());
startActivity(intent);Kotlin
方式一:用to关键字
startActivity<ActSecondActivity>(
"request_time" to DateUtil.nowTime,
"request_content" to et_request.text.toString())方式二:Pair
startActivity<ActSecondActivity>(
Pair("request_time", DateUtil.nowTime),
Pair("request_content", et_request.text.toString()))

接收方:

val bundle = intent.extras
val request_time = bundle.getString("request_time")
val request_content = bundle.getString("request_content")

传送序列化数据

//@Parcelize注解表示自动实现Parcelize接口的相关方法
@Parcelize
data class MessageInfo(val content: String, val send_time: String) : Pa
rcelable {}//@Parcelize标记需要在build.gradle设置experimental = true
androidExtensions {experimental = true
}
//传送序列化数据
val request = MessageInfo(et_request.text.toString(), DateUtil.nowTime)
startActivity<ParcelableSecondActivity>("message" to request)

接收方:

//获得Parcelable的参数
val request = intent.extras.getParcelable<MessageInfo>("message")
${request.content}

跳转时指定启动模式

anko库取消了intent方法有利有弊,弊端是intent对象的setAction,setData,addCategory,setFlags怎么设置
那么下面这种方式就可以拿到intent对象设置了

val intent = intentFor<ActSecondActivity>(
"request_time" to DateUtil.nowTime,
"request_content" to et_request.text.toString())

Android有两种方式设置启动模式:清单文件和代码设置

清单文件设置

代码设置

anko库仍然简化了代码:startActivity(intent.newTask())

处理返回数据 startActivityForResult

跳转

val info = MessageInfo(et_request.text.toString(), DateUtil.nowTime)
//ForResult表示需要返回参数
startActivityForResult<ActResponseActivity>(0, "message" to info)

下个页面返回数据

btn_act_response.setOnClickListener {val response = MessageInfo(et_response.text.toString(), DateUtil.no
wTime)
val intent = Intent()
intent.putExtra("message", response)
//调用setResult表示参数返回到上个页面
setResult(Activity.RESULT_OK, intent)
finish()
}

收到数据后:

//返回本页面时回调onActivityResult
override fun onActivityResult(requestCode: Int, resultCode: Int, data:
Intent?) {if (data != null) {//获得应答参数
val response = data.extras.getParcelable<MessageInfo>
("message")
tv_request.text = " ${response.send_time},${response.content}"
}
}

6.5实战项目

6.6总结

1.按钮控件
2.布局视图
3.图文控件
4.activity跳转
5.anko库对话框

第七章 Kotlin操纵复杂控件

7.1使用视图排列

下拉框spinner

private fun initSpinner() {val starAdapter = ArrayAdapter(this, R.layout.item_select, starArra
y)
starAdapter.setDropDownViewResource(R.layout.item_dropdown)
//Android 8.0之后的findViewById要求在后面添加"<View>"才能进行类型转换
val sp = findViewById<View>(R.id.sp_dialog) as Spinner
sp.prompt = "请选择行星"
sp.adapter = starAdapter
sp.setSelection(0)
sp.onItemSelectedListener = MySelectedListener()
}private val starArray = arrayOf("水星", "水星", "火星", "木星")internal inner class MySelectedListener : OnItemSelectedListener {override fun onItemSelected(arg0: AdapterView<*>, arg1: View, arg2:
Int, arg3: Long) {toast("${starArray[arg2]}")
}
override fun onNothingSelected(arg0: AdapterView<*>) {}
}

anko库的简化:

<TextView
android:id="@+id/tv_spinner"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_toRightOf="@+id/tv_dialog"
android:gravity="center"
android:drawableRight="@drawable/arrow_down"
android:textColor="@color/black"
android:textSize="17sp" />val satellites = listOf("水星", "水星", "火星", "木星")
tv_spinner.text = satellites[0]
tv_spinner.setOnClickListener {selector("请选择行星", satellites) { i ->
tv_spinner.text = satellites[i]
toast("${tv_spinner.text}")
}
}

主要借助了:import org.jetbrains.anko.selector
anko库里面的selector源码是利用了AlertDialog的setItem方法
###列表视图listview
Kotlin的扩展视图selector
Kotlin要求每个变量都要初始化
lateinit延迟初始化属性,那么修饰的变量无需赋空值,使用的时候也不用加!!

class PlanetListAdapter(private val context: Context, private val planetList: MutableList<Planet>, private val background: Int) : BaseAdapter() {override fun getCount(): Int = planetList.sizeoverride fun getItem(position: Int): Any = planetList[position]override fun getItemId(position: Int): Long = position.toLong()override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {var view = convertViewval holder: ViewHolderif (convertView == null) {view = LayoutInflater.from(context).inflate(R.layout.item_list_view, null)holder = ViewHolder()//先声明视图持有者的实例,再依次获取内部的各个控件对象holder.ll_item = view.findViewById(R.id.ll_item) as LinearLayoutholder.iv_icon = view.findViewById(R.id.iv_icon) as ImageViewholder.tv_name = view.findViewById(R.id.tv_name) as TextViewholder.tv_desc = view.findViewById(R.id.tv_desc) as TextViewview.tag = holder} else {holder = view.tag as ViewHolder}val planet = planetList[position]holder.ll_item.setBackgroundColor(background)holder.iv_icon.setImageResource(planet.image)holder.tv_name.text = planet.nameholder.tv_desc.text = planet.descreturn view!!}//ViewHolder中的属性使用关键字lateinit延迟初始化inner class ViewHolder {lateinit var ll_item: LinearLayoutlateinit var iv_icon: ImageViewlateinit var tv_name: TextViewlateinit var tv_desc: TextView}
}

以上的Kotlin代码总算有点模样了,虽然总体代码还不够精简,但是至少清晰明了,其中主要运用了Kotlin的以下三项技术:

1、构造函数和初始化参数放在类定义的首行,无需单独构造,也无需手工初始化;
2、像getCount、getItem、getItemId这三个函数,仅仅返回简单运算的数值,可以直接用等号取代大括号;
3、对于视图持有者的内部控件,在变量名称前面添加lateinit,表示该属性为延迟初始化属性;

网格视图

在前面的列表视图一小节中,给出了Kotlin改写后的适配器类,通过关键字lateinit固然避免了麻烦的空校验,可是控件对象迟早要初始化的呀,晚赋值不如早赋值。翻到前面PlanetListAdapter的实现代码,认真观察发现控件对象的获取其实依赖于布局文件的视图对象view,既然如此,不妨把该视图对象作为ViewHolder的构造参数传过去,使得视图持有者在构造之时便能一块初始化内部控件。据此改写后的Kotlin适配器代码如下所示:

class PlanetGridAdapter(private val context: Context, private val planetList: MutableList<Planet>, private val background: Int) : BaseAdapter() {override fun getCount(): Int = planetList.sizeoverride fun getItem(position: Int): Any = planetList[position]override fun getItemId(position: Int): Long = position.toLong()override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {var view = convertViewval holder: ViewHolderif (view == null) {view = LayoutInflater.from(context).inflate(R.layout.item_grid_view, null)holder = ViewHolder(view)//视图持有者的内部控件对象已经在构造时一并初始化了,故这里无需再做赋值view.tag = holder} else {holder = view.tag as ViewHolder}val planet = planetList[position]holder.ll_item.setBackgroundColor(background)holder.iv_icon.setImageResource(planet.image)holder.tv_name.text = planet.nameholder.tv_desc.text = planet.descreturn view!!}//ViewHolder中的属性在构造时初始化inner class ViewHolder(val view: View) {val ll_item: LinearLayout = view.findViewById(R.id.ll_item) as LinearLayoutval iv_icon: ImageView = view.findViewById(R.id.iv_icon) as ImageViewval tv_name: TextView = view.findViewById(R.id.tv_name) as TextViewval tv_desc: TextView = view.findViewById(R.id.tv_desc) as TextView}
}

外部调用

gv_planet.adapter = PlanetGridAdapter(this, Planet.defaultList, Color.W
HITE)

循环视图RecyclerView

RecyclerView可以实现线性列表,网格列表,瀑布流网格

布局管理器
LinearLayoutManager,GridLayoutManager,StaggeredGridLayoutManager
循环适配器
循环适配器和其他适配器的区别
1.自带视图持有者ViewHolder及其重用功能,无需开发者手工重用ViewHolder
2.未带点击和长按功能,需要开发者自己实现
3.增加区分不同列表项的视图类型
4.可单独对个别项进行增删改操作,无须刷新整个列表
Kotlin实现:

//ViewHolder在构造时初始化布局中的控件对象
class RecyclerLinearAdapter(private val context: Context, private val infos: MutableList<RecyclerInfo>) : RecyclerView.Adapter<ViewHolder>(), OnItemClickListener, OnItemLongClickListener {val inflater: LayoutInflater = LayoutInflater.from(context)//获得列表项的数目override fun getItemCount(): Int = infos.size//创建整个布局的视图持有者override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {val view: View = inflater.inflate(R.layout.item_recycler_linear, parent, false)return ItemHolder(view)}//绑定每项的视图持有者override fun onBindViewHolder(holder: ViewHolder, position: Int) {val vh: ItemHolder = holder as ItemHoldervh.iv_pic.setImageResource(infos[position].pic_id)vh.tv_title.text = infos[position].titlevh.tv_desc.text = infos[position].desc// 列表项的点击事件需要自己实现vh.ll_item.setOnClickListener { v ->itemClickListener?.onItemClick(v, position)}vh.ll_item.setOnLongClickListener { v ->itemLongClickListener?.onItemLongClick(v, position)true}}//ItemHolder中的属性在构造时初始化inner class ItemHolder(view: View) : RecyclerView.ViewHolder(view) {var ll_item = view.findViewById(R.id.ll_item) as LinearLayoutvar iv_pic = view.findViewById(R.id.iv_pic) as ImageViewvar tv_title = view.findViewById(R.id.tv_title) as TextViewvar tv_desc = view.findViewById(R.id.tv_desc) as TextView}private var itemClickListener: OnItemClickListener? = nullfun setOnItemClickListener(listener: OnItemClickListener) {this.itemClickListener = listener}private var itemLongClickListener: OnItemLongClickListener? = nullfun setOnItemLongClickListener(listener: OnItemLongClickListener) {this.itemLongClickListener = listener}override fun onItemClick(view: View, position: Int) {val desc = "您点击了第${position+1}项,标题是${infos[position].title}"context.toast(desc)}override fun onItemLongClick(view: View, position: Int) {val desc = "您长按了第${position+1}项,标题是${infos[position].title}"context.toast(desc)}
}

可是这个循环适配器RecyclerLinearAdapter仍然体量庞大,细细观察发现其实它有着数个与具体业务无关的属性与方法,譬如上下文对象context、布局载入对象inflater、点击监听器itemClickListener、长按监听器itemLongClickListener等等,故而完全可以把这些通用部分提取到一个基类,然后具体业务再从该基类派生出特定的业务适配器类。根据这种设计思路,提取出了循环视图基础适配器,它的Kotlin代码如下所示:

//循环视图基础适配器
abstract class RecyclerBaseAdapter<VH : RecyclerView.ViewHolder>(val context: Context) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), OnItemClickListener, OnItemLongClickListener {val inflater: LayoutInflater = LayoutInflater.from(context)//获得列表项的个数,需要子类重写override abstract fun getItemCount(): Int//根据布局文件创建视图持有者,需要子类重写override abstract fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder//绑定视图持有者中的各个控件对象,需要子类重写override abstract fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)override fun getItemViewType(position: Int): Int = 0override fun getItemId(position: Int): Long = position.toLong()var itemClickListener: OnItemClickListener? = nullfun setOnItemClickListener(listener: OnItemClickListener) {this.itemClickListener = listener}var itemLongClickListener: OnItemLongClickListener? = nullfun setOnItemLongClickListener(listener: OnItemLongClickListener) {this.itemLongClickListener = listener}override fun onItemClick(view: View, position: Int) {}override fun onItemLongClick(view: View, position: Int) {}
}

一旦有了这个基础适配器,实际业务的适配器即可由此派生而来,真正需要开发者编写的代码一下精简了不少。下面便是个循环视图的网格适配器,它实现了类似淘宝主页的网格频道栏目,具体的Kotlin代码如下所示:

//把公共属性和公共方法剥离到基类RecyclerBaseAdapter,
//此处仅需实现getItemCount、onCreateViewHolder、onBindViewHolder三个方法,以及视图持有者的类定义
class RecyclerGridAdapter(context: Context, private val infos: MutableList<RecyclerInfo>) : RecyclerBaseAdapter<RecyclerView.ViewHolder>(context) {override fun getItemCount(): Int = infos.sizeoverride fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {val view: View = inflater.inflate(R.layout.item_recycler_grid, parent, false)return ItemHolder(view)}override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {val vh = holder as ItemHoldervh.iv_pic.setImageResource(infos[position].pic_id)vh.tv_title.text = infos[position].title}inner class ItemHolder(view: View) : RecyclerView.ViewHolder(view) {var ll_item = view.findViewById(R.id.ll_item) as LinearLayoutvar iv_pic = view.findViewById(R.id.iv_pic) as ImageViewvar tv_title = view.findViewById(R.id.tv_title) as TextView}
}

然而基类不过是雕虫小技,Java也照样能够运用,所以这根本不入Kotlin的法眼,要想超越Java,还得拥有独门秘笈才行。注意到适配器代码仍然通过findViewById方法获得控件对象,可是号称在Anko库的支持之下,Kotlin早就无需该方法就能直接访问控件对象了呀,为啥这里依旧靠老牛拉破车呢?其中的缘由是Anko库仅仅实现了Activity活动页面的控件自动获取,并未实现适配器内部的自动获取。不过Kotlin早就料到了这一手,为此专门提供了一个插件名叫LayoutContainer,只要开发者让自定义的ViewHolder继承该接口,即可在视图持有者内部无需获取就能使用控件对象了。这下不管是在Activity代码,还是在适配器代码中,均可将控件名称拿来直接调用了。这么神奇的魔法,快来看看Kotlin的适配器代码是如何书写的:

//利用Kotlin的插件LayoutContainer,在适配器中直接使用控件对象,而无需对其进行显式声明
class RecyclerStaggeredAdapter(context: Context, private val infos: MutableList<RecyclerInfo>) : RecyclerBaseAdapter<RecyclerView.ViewHolder>(context) {override fun getItemCount(): Int = infos.sizeoverride fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {val view: View = inflater.inflate(R.layout.item_recycler_staggered, parent, false)return ItemHolder(view)}override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {(holder as ItemHolder).bind(infos[position])}//注意这里要去掉inner,否则运行报错“java.lang.NoSuchMethodError: No virtual method _$_findCachedViewById”class ItemHolder(override val containerView: View?) : RecyclerView.ViewHolder(containerView), LayoutContainer {fun bind(item: RecyclerInfo) {iv_pic.setImageResource(item.pic_id)tv_title.text = item.title}}
}

还需要在模块的build.gradle增加:

androidExtensions {experimental = true
}

上面采用了新的适配器插件,似乎已经大功告成,可是依然要书写单独的适配器代码,仔细研究发现这个RecyclerStaggeredAdapter还有三个要素是随着具体业务而变化的,包括:

1、列表项的布局文件资源编码,如R.layout.item_recycler_staggered;
2、列表项信息的数据结构名称,如RecyclerInfo;
3、对各种控件对象的设置操作,如ItemHolder类的bind方法;

除了以上三个要素,RecyclerStaggeredAdapter内部的其余代码都是允许复用的,因此,接下来的工作就是想办法把这三个要素抽象为公共类的某种变量。对于第一个的布局编码,可以考虑将其作为一个整型的输入参数;对于第二个的数据结构,可以考虑定义一个模板类,在外部调用时再指定具体的数据类;对于第三个的bind方法,若是Java编码早已束手无策,现用Kotlin编码正好将该方法作为一个函数参数传入。依照三个要素的三种处理对策,进而提炼出来了循环适配器的通用类RecyclerCommonAdapter,详细的Kotlin代码示例如下:

//循环视图通用适配器
//将具体业务中会变化的三类要素抽取出来,作为外部传进来的变量。这三类要素包括:
//布局文件对应的资源编号、列表项的数据结构、各个控件对象的初始化操作
class RecyclerCommonAdapter<T>(context: Context, private val layoutId: Int, private val items: List<T>, val init: (View, T) -> Unit): RecyclerBaseAdapter<RecyclerCommonAdapter.ItemHolder<T>>(context) {override fun getItemCount(): Int = items.sizeoverride fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {val view: View = inflater.inflate(layoutId, parent, false)return ItemHolder<T>(view, init)}override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {val vh: ItemHolder<T> = holder as ItemHolder<T>vh.bind(items.get(position))}//注意init是个函数形式的输入参数class ItemHolder<in T>(val view: View, val init: (View, T) -> Unit) : RecyclerView.ViewHolder(view) {fun bind(item: T) {init(view, item)}}
}

有了这个通用适配器,外部使用适配器只需像函数调用那样传入这三种变量就好了,具体调用的Kotlin代码如下所示:

//第二种方式:使用把三类可变要素抽象出来的通用适配器
val adapter = RecyclerCommonAdapter(this, R.layout.item_recycler_staggered, RecyclerInfo.defaultStag,{view, item ->val iv_pic = view.findViewById(R.id.iv_pic) as ImageViewval tv_title = view.findViewById(R.id.tv_title) as TextViewiv_pic.setImageResource(item.pic_id)tv_title.text = item.title})
rv_staggered.adapter = adapter

最终出炉的适配器仅有十行代码不到,其中的关键技术——函数参数真是不鸣则已、一鸣惊人。至此本节的适配器实现过程终于落下帷幕,一路上可谓是过五关斩六将,硬生生把数十行的Java代码压缩到不到十行的Kotlin代码,经过不断迭代优化方取得如此彪炳战绩。尤其是最后的两种实现方式,分别运用了Kotlin的多项综合技术,才能集Kotlin精妙语法之大成。

7.2使用材质设计

MaterialDesign库提供了协调布局CoordinatorLayout,AppBarLayout,CollapsingToolbarLayout

协调布局CoordinatorLayout

继承自ViewGroup
对齐方式:layout_gravity,
子视图位置:app:layout_anchor?app:layout_anchorGravity
行为:app:layout_behavior

FloatingActionButton 悬浮按钮
悬浮按钮会悬浮在其他视图之上
隐藏和显示悬浮按钮时会播放切换动画,hide和show方法
悬浮按钮默认会随着便签条Snackbar的出现或消失动态调整位置
###工具栏Toolbar
Android 5.0之后使用Toolbar代替ActionBar
不过为了兼容老版本,ActionBar仍然保留,可是ActionBar和Toolbar都占着顶部导航栏的位置,所以想引入Toolbar就得关闭ActionBar,具体步骤如下:
1.定义一个style

<style name="AppCompatTheme" parent= "Theme.AppCompat.Light.NoActionBar" />

2.清单文件中的application标签的

android:theme="@style/AppCompatTheme"

3.创建一个布局文件

<android.support.v7.widget.Toolbar
android:id="@+id/tl_head"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
extends AppCompatActivity

5.onCreate中setSupportActionBar

Toolbar的常用方法

Toolbar比ActionBar灵活,主要便是它提供了多个方法来修改控件风格,下面是Toolbar的常用方法:
setLogo : 设置工具栏图标。
setTitle : 设置标题文字。
setTitleTextAppearance : 设置标题的文字风格。
setTitleTextColor : 设置标题的文字颜色。
setSubtitle : 设置副标题文字。副标题在标题下方。
setSubtitleTextAppearance : 设置副标题的文字风格。
setSubtitleTextColor : 设置副标题的文字颜色。
setNavigationIcon : 设置导航图标。导航图标在工具栏图标左边。
setNavigationOnClickListener : 设置导航图标的点击监听器。
setOverflowIcon : 设置溢出菜单的按钮图标。
showOverflowMenu : 显示溢出菜单图标。
hideOverflowMenu : 隐藏溢出菜单图标。
dismissPopupMenus : 关闭已弹出的菜单。

应用栏布局AppBarLayout

AppBarLayout 继承自LinearLayout,所以它具备LinearLayout的所有属性方法

可折叠工具栏布局CollapsingToolbarLayout

7.3实现页面切换

翻页视图ViewPager

Fragment

Tablayout

广播Broadcast

###收发临时广播
1.一人发送广播,多人接收处理
2.对于发送者来说,不需要考虑接收者
3.对于接收者来说,要自行过滤符合条件的广播
sendBroadcast,registerReceiver,unregisterReceiver

静态属性如果是个常量,就还要添加修饰符const

companion object {//静态属性如果是个常量,就还要添加修饰符const
const val EVENT = "com.example.complex.fragment.BroadcastFragment"
}

编译时常量(const):真正意义的常量
运行时常量(val):可以在声明的时候赋值,运行后被赋值

系统广播

静态注册和动态注册

总结

视图排列:下拉框,列表视图,网格视图,循环视图,适配器的延迟初始化属性
材质设计:状态栏,工具栏
页面切换:ViewPager,fragment,tablayout
广播,两种常量

第八章 Kotlin进行数据存储

8.1使用共享参数SharedPreferences

共享参数读写模板Preference

class Perference<T>(val context: Context, val name: String, val default: T): ReadWriteProperty<Any?, T> {/**** 通过属性代理初始化共享参数对象* lazy:第一次使用时执行初始化*/val prefs: SharedPreferences by lazy { context.getSharedPreferences("default", Context.MODE_PRIVATE) }/**** 接管属性值的获取行为* *:表示一个不确定的类型*/override fun getValue(thisRef: Any?, property: KProperty<*>): T {return findPreference(name, default)}/**** 接管属性值的修改行为*/override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {putPreference(name, value)}//利用with函数定义临时的命名空间private fun <T> findPreference(name: String, default: T): T = with(prefs) {val res: Any = when (default) {is Long -> getLong(name, default)is String -> getString(name, default)is Int -> getInt(name, default)is Boolean -> getBoolean(name, default)is Float -> getFloat(name, default)else -> throw IllegalArgumentException("This type can be saved into Preferences")}return res as T}private fun <T> putPreference(name: String, value: T) = with(prefs.edit()) {//putInt、putString等方法返回Editor对象when (value) {is Long -> putLong(name, value)is String -> putString(name, value)is Int -> putInt(name, value)is Boolean -> putBoolean(name, value)is Float -> putFloat(name, value)else -> throw IllegalArgumentException("This type can be saved into Preferences")}.apply() //commit方法和apply方法都表示提交修改}}

调用:

private var name: String by Perference(this, "name", "")
private var age: String by Perference(this, "age", "")

后续代码若给委托属性赋值,则立即触发写入动作

属性代理等黑科技

这个高大上的Preference用了哪些黑科技?
用到了模板类,委托属性,lazy修饰符,with函数

模板类:

因为共享参数允许保存的类型包括,整型,浮点型,字符串等,所以要定义成模板类,参数类型在调用时再指定
除了T,还有Any和*

1、T是抽象的泛型,在模板类中用来占位子,外部调用模板类时才能确定T的具体类型;
2、Any是Kotlin的基本类型,所有Kotlin类都从Any派生而来,故而它相当于Java里面的Object;
3、星号表示一个不确定的类型,同样也是在外部调用时才能确定,这点跟T比较像,但T出现在模板类的定义中,而与模板类无关,它出现在单个函数定义的参数列表中,因此星号相当于Java里面的问号?;

委托属性/属性代理

by表示代理动作,第五章的例子是接口代理或称类代理,而这里则为属性代理,所谓属性代理,是说该属性的类型不变,但是属性的读写行为被后面的类接管了。类似银行推出了“委托代扣”的业务,只要用户跟银行签约并指定委托扣费的电力账户,那么在每个月指定时间,银行会自动从用户银行卡中扣费并缴纳给指定的电力账户,如此省却了用户的人工操作。
现实生活中的委托扣费场景,对应到共享参数这里,开发者的人工操作指的是手工编码从SharedPreferences类读取数据和保存数据,而自动操作指的是约定代理的属性自动通过模板类Preference完成数据的读取和保存,也就是说,Preference接管了这些属性的读写行为,接管后的操作则是模板类的getValue和setValue方法。属性被接管的行为叫做属性代理,而被代理的属性称作委托属性。

lazy修饰符

模板类Preference声明了一个共享参数的prefs对象,其中用到了关键字lazy,lazy的意思是懒惰,表示只在该属性第一次使用时执行初始化。联想到Kotlin还有类似的关键字名叫lateinit,意思是延迟初始化,加上lazy可以归纳出Kotlin变量的三种初始化操作,具体说明如下:
1、声明时赋值:这是最常见的变量初始化,在声明某个变量时,立即在后面通过等号“=”给它赋予具体的值。
2、lateinit延迟初始化:变量声明时没有马上赋值,但该变量仍是个非空变量,何时初始化由开发者编码决定。
3、lazy首次使用时初始化:声明变量时指定初始化动作,但该动作要等到变量第一次使用时才进行初始化。
此处的prefs对象使用lazy规定了属性值在首次使用时初始化,且初始化动作通过by后面的表达式来指定,即“{ context.getSharedPreferences(“default”, Context.MODE_PRIVATE) }”。连同大括号在内的这个表达式,其实是个匿名实例,它内部定义了prefs对象的初始化语句,并返回SharedPreferences类型的变量值。

with函数

with函数的书写格式形如“with(函数头语句) { 函数体语句 }”,看这架势,with方法的函数语句分为两部分,详述如下:
1、函数头语句:头部语句位于紧跟with的圆括号内部。它先于函数体语句执行,并且头部语句返回一个对象,函数体语句在该对象的命名空间中运行;即体语句可以直接调用该对象的方法,而无需显式指定该对象的实例名称。
2、函数体语句:体语句位于常规的大括号内部。它要等头部语句执行完毕才会执行,同时体语句在头部语句返回对象的命名空间中运行;即体语句允许直接调用头部对象的方法,而无需显式指定该对象的实例名称。
综上所述,在模板类Preference的编码过程中,联合运用了Kotlin的多项黑科技,方才实现了优于Java的共享参数操作方式

实现记住密码的功能

private var phone: String by Preference(this, "phone", "")
private var password: String by Preference(this, "password", "")

由于上面的语句已经自动从共享参数获取属性值,接下来若要往共享参数保存新的属性值,只需修改委托属性的变量值即可。

8.2使用数据库SQLite

数据库帮助器SQLiteOpenHelper

SQLiteDatabase(数据库管理类):获取数据库实例
SQLiteOpenHelper:操作数据表的API

更安全的ManagedSQLiteOpenHelper

SQLiteOpenHelper开发者需要在操作表之前打开数据库连接,结束后关闭数据库连接
于是Kotlin结合anko库推出了改良版的SQLite管理工具:ManagedSQLiteOpenHelper

use {//1、插入记录//insert(...)//2、更新记录//update(...)//3、删除记录//delete(...)//4、查询记录//query(...)或者rawQuery(...)}
class UserDBHelper(var context: Context, private var DB_VERSION: Int=CURRENT_VERSION) : ManagedSQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {companion object {private val TAG = "UserDBHelper"var DB_NAME = "user.db" //数据库名称var TABLE_NAME = "user_info" //表名称var CURRENT_VERSION = 1 //当前的最新版本,如有表结构变更,该版本号要加一private var instance: UserDBHelper? = null@Synchronizedfun getInstance(ctx: Context, version: Int=0): UserDBHelper {if (instance == null) {//如果调用时没传版本号,就使用默认的最新版本号instance = if (version>0) UserDBHelper(ctx.applicationContext, version)else UserDBHelper(ctx.applicationContext)}return instance!!}}override fun onCreate(db: SQLiteDatabase) {Log.d(TAG, "onCreate")val drop_sql = "DROP TABLE IF EXISTS $TABLE_NAME;"Log.d(TAG, "drop_sql:" + drop_sql)db.execSQL(drop_sql)val create_sql = "CREATE TABLE IF NOT EXISTS $TABLE_NAME (" +"_id INTEGER PRIMARY KEY  AUTOINCREMENT NOT NULL," +"name VARCHAR NOT NULL," + "age INTEGER NOT NULL," +"height LONG NOT NULL," + "weight FLOAT NOT NULL," +"married INTEGER NOT NULL," + "update_time VARCHAR NOT NULL" +//演示数据库升级时要先把下面这行注释",phone VARCHAR" + ",password VARCHAR" + ");"Log.d(TAG, "create_sql:" + create_sql)db.execSQL(create_sql)}override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {Log.d(TAG, "onUpgrade oldVersion=$oldVersion, newVersion=$newVersion")if (newVersion > 1) {//Android的ALTER命令不支持一次添加多列,只能分多次添加var alter_sql = "ALTER TABLE $TABLE_NAME ADD COLUMN phone VARCHAR;"Log.d(TAG, "alter_sql:" + alter_sql)db.execSQL(alter_sql)alter_sql = "ALTER TABLE $TABLE_NAME ADD COLUMN password VARCHAR;"Log.d(TAG, "alter_sql:" + alter_sql)db.execSQL(alter_sql)}}fun delete(condition: String): Int {var count = 0use {count = delete(TABLE_NAME, condition, null)}return count}fun insert(info: UserInfo): Long {val infoArray = mutableListOf(info)return insert(infoArray)}fun insert(infoArray: MutableList<UserInfo>): Long {var result: Long = -1for (i in infoArray.indices) {val info = infoArray[i]var tempArray: List<UserInfo>// 如果存在同名记录,则更新记录// 注意条件语句的等号后面要用单引号括起来if (info.name.isNotEmpty()) {val condition = "name='${info.name}'"tempArray = query(condition)if (tempArray.size > 0) {update(info, condition)result = tempArray[0].rowidcontinue}}// 如果存在同样的手机号码,则更新记录if (info.phone.isNotEmpty()) {val condition = "phone='${info.phone}'"tempArray = query(condition)if (tempArray.size > 0) {update(info, condition)result = tempArray[0].rowidcontinue}}// 不存在唯一性重复的记录,则插入新记录val cv = ContentValues()cv.put("name", info.name)cv.put("age", info.age)cv.put("height", info.height)cv.put("weight", info.weight)cv.put("married", info.married)cv.put("update_time", info.update_time)cv.put("phone", info.phone)cv.put("password", info.password)use {result = insert(TABLE_NAME, "", cv)}// 添加成功后返回行号,失败后返回-1if (result == -1L) {return result}}return result}@JvmOverloadsfun update(info: UserInfo, condition: String = "rowid=${info.rowid}"): Int {val cv = ContentValues()cv.put("name", info.name)cv.put("age", info.age)cv.put("height", info.height)cv.put("weight", info.weight)cv.put("married", info.married)cv.put("update_time", info.update_time)cv.put("phone", info.phone)cv.put("password", info.password)var count = 0use {count = update(TABLE_NAME, cv, condition, null)}return count}fun query(condition: String): List<UserInfo> {val sql = "select rowid,_id,name,age,height,weight,married,update_time,phone,password from $TABLE_NAME where $condition;"Log.d(TAG, "query sql: " + sql)var infoArray = mutableListOf<UserInfo>()use {val cursor = rawQuery(sql, null)if (cursor.moveToFirst()) {while (true) {val info = UserInfo()info.rowid = cursor.getLong(0)info.xuhao = cursor.getInt(1)info.name = cursor.getString(2)info.age = cursor.getInt(3)info.height = cursor.getLong(4)info.weight = cursor.getFloat(5)//SQLite没有布尔型,用0表示false,用1表示trueinfo.married = if (cursor.getInt(6) == 0) false else trueinfo.update_time = cursor.getString(7)info.phone = cursor.getString(8)info.password = cursor.getString(9)infoArray.add(info)if (cursor.isLast) {break}cursor.moveToNext()}}cursor.close()}return infoArray}fun queryByPhone(phone: String): UserInfo {val infoArray = query("phone='$phone'")val info: UserInfo = if (infoArray.size>0) infoArray[0] else UserInfo()return info}fun deleteAll(): Int = delete("1=1")fun queryAll(): List<UserInfo> = query("1=1")}

记得加:

compile "org.jetbrains.anko:anko-sqlite:$anko_version"

调用:

var helper: UserDBHelper = UserDBHelper.getInstance(this)
val userArray = helper.queryAll()

优化记住密码功能

共享参数实现了记住密码的功能,但是只能记住一个账号的。可以采用数据库

8.3 文件I/O操作

###文件保存空间
手机的存储空间分为内部存储和外部存储
内部存储放的是手机系统以及各应用的安装目录
外部存储放的是公共文件,如图片,视频,文档等
为了不影响系统的流畅运行,App运行过程中要处理的文件都保存在外部存储空间
为保证App正常读写外部存储,需要在AndroidManifest中增加权限

<!-- SD读写权限 -->
<uses-
permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-
permission android:name="android.permission.READ_EXTERNAL_STORAG" />
<uses-
permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"
/>

Android7.0加强了SD卡的权限管理,系统默认关闭了外部存储的公共空间,外部存储的私有空间依然可以正常读写
因为Android把外部存储分成了2块区域,一块是所有应用都能访问的公共空间,一块是只有应用自己才可以访问的专享空间。当然,您的应用被卸载,那么这块专享空间文件目录也就没有了
获取公共空间的存储路径:Environment.getExternalStoragePublicDirectory
获取应用私有空间的存储路径:getExternalFilesDir

//公共路径
val publicPath = Environment.getExternalStoragePublicDirectory
(Environment.DIRECTORY_DOWNLOADS).toString();
//私有路径
val privatePath = getExternalFilesDir(Environment. DIRECTORY_DO
WNLOADS).toString();
tv_file_path.text = "{publicPath}" ,${privatePath}"
"Android7.0之后默认禁止访问公共存储目录

读写文本文件

Kotlin利用拓展函数功能添加了一些常用的文件内容读写方法,一行代码搞定问题

//比如文本写入文件
File(file_path).writeText(content)

若要往源文件追加文本,则可调用appendText方法

readText:读取文本形式的文件内容
readLines:按行读取文件内容
val content = File(file_path).readText()

读写图片文件

写入

fun saveImage(path: String, bitmap: Bitmap) {try {val file = File(path)
val fos: OutputStream = file.outputStream()
//压缩格式为JPEG,压缩质量为80%
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, fos)
fos.flush()
fos.close()
} catch (e: Exception) {e.printStackTrace()
}
}

读取:

//方式一
val bytes = File(file_path).readBytes()
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)//方式二
val fis = File(file_path).inputStream()
val bitmap = BitmapFactory.decodeStream(fis)
fis.close()//方式三
val bitmap = BitmapFactory.decodeFile(file_path)

遍历文件目录

Kotlin新概念:文件树

var fileNames: MutableList<String> = mutableListOf()
//在该目录下走一圈,得到文件目录树结构
val fileTree: FileTreeWalk = File(mPath).walk()
fileTree.maxDepth(1) //需遍历的目录层级为1,即无需检查子目录
.filter { it.isFile } //只挑选文件,不处理文件夹
.filter { it.extension == "txt" } //选择拓展名为txt的文本文件
.forEach { fileNames.add(it.name) } //循环处理符合条件的文件

改进后:

var fileNames: MutableList<String> = mutableListOf()
//在该目录下走一圈,得到文件目录树结构
val fileTree: FileTreeWalk = File(mPath).walk()
fileTree.maxDepth(1) //需遍历的目录层级为1,即无需检查子目录
.filter { it.isFile } //只挑选文件,不处理文件夹
.filter { it.extension in listOf("png","jpg") } //选择拓展名为png和jpg的图片文件
.forEach { fileNames.add(it.name) } //循环处理符合条件的文件

8.4 Application全局变量
方式一:

class MainApplication : Application() {override fun onCreate() {super.onCreate()
instance = this
}
//单例化的第一种方式:声明一个简单的Application属性
companion object {//声明可空属性
private var instance: MainApplication? = null
fun instance() = instance!!
//声明延迟初始化属性
//private lateinit var instance: MainApplication
//fun instance() = instance
}
}

方式二

class MainApplication : Application() {override fun onCreate() {super.onCreate()
instance = this
}
//单例化的第二种方式:利用系统自带的Delegates生成委托属性
companion object {private var instance: MainApplication by Delegates.notNull()
fun instance() = instance
}
}

前两种单例都只完成了非空校验,还不是严格意义上的单例化,真正的单例化有且仅有一次的赋值操作,尽管前两种单例化并未实现唯一赋值功能,不多大多数场合已经够用了。
那么怎实现唯一赋值的的单例化?需要开发者自己写一个校验赋值次数的行为类

class MainApplication : Application() {override fun onCreate() {super.onCreate()
instance = this
}
//单例化的第三种方式:自定义一个非空且只能一次性赋值的委托属性
companion object {private var instance: MainApplication by NotNullSingleValueVar(
)
fun instance() = instance
}
//定义一个属性管理类,进行非空和重复赋值的判断
private class NotNullSingleValueVar<T>
() : ReadWriteProperty<Any?, T> {private var value: T? = null
override fun getValue(thisRef: Any?, property: KProperty<*>): T
{return value ?: throw IllegalStateException("application no
t initialized")
}
override fun setValue(thisRef: Any?, property: KProperty<*>, va
lue: T) {this.value = if (this.value == null) value
else throw IllegalStateException("application already initi
alized")
}
}
}

上述代码,自定义的代理行为在getValue方法中进行非空校验,在setValue方法中进行重复赋值的校验,按照要求接管了委托属性的读写行为。

利用Application实现全局变量

适合在Application中保持的全局变量主要有一下几类数据
1.频繁读取的信息,如用户名,手机号等
2.网络上获取的临时数据,节约流量也为了减少用户的等待时间,暂时放在内存中供下次使用,例如logo,图片
3.容易因频繁分配内存而导致内存泄漏的对象,例如Handle线程池ThreadPool等

总结

1.利用工具类Preference进行共享参数的键值对管理工作,并掌握委托属性,lazy修饰符,with函数的基本用法
2.使用Kotlin的ManagedSQLiteOpenHelper管理数据库
3.使用Kotlin的文件I/O函数进行文件处理,包括文本文件读写,图片文件读写,文件目录遍历等
4.Kotlin实现Application单例化,通过Application操作全局变量

第九章 Kotlin自定义控件

9.1自定义普通视图

构造对象

自定义属性的步骤:

  1. 在res\values目录下创建attrs.xml,文件内容如下所示,其中declare-styleable的name属性值表示新视图的名称,两个attr节点表示新增的两个属性分别是textColor和textSize:
<resources><declare-styleable name="CustomPagerTab"><attr name="textColor" format="color" /><attr name="textSize" format="dimension" /></declare-styleable>
</resources>
  1. 在模块的widget目录下创建CustomPagerTab.java,填入以下自定义视图的代码:
public class CustomPagerTab extends PagerTabStrip {private int textColor = Color.BLACK;private int textSize = 15;public CustomPagerTab(Context context) {super(context);}public CustomPagerTab(Context context, AttributeSet attrs) {super(context, attrs);//构造函数从attrs.xml读取CustomPagerTab的自定义属性if (attrs != null) {TypedArray attrArray=getContext().obtainStyledAttributes(attrs, R.styleable.CustomPagerTab);textColor = attrArray.getColor(R.styleable.CustomPagerTab_textColor, textColor);textSize = attrArray.getDimensionPixelSize(R.styleable.CustomPagerTab_textSize, textSize);attrArray.recycle();}setTextColor(textColor);setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize);}//    //PagerTabStrip没有三个参数的构造函数
//    public PagerTab(Context context, AttributeSet attrs, int defStyleAttr) {//    }
}
  1. 布局文件的根节点增加自定义的命名空间声明,如“xmlns:app=“http://schemas.android.com/apk/res-auto””;并把android.support.v4.view.PagerTabStrip的节点名称改为自定义视图的全路径名称如“com.example.custom.widget.PagerTab”,同时在该节点下指定新增的两个属性即app:textColor与app:textSize。修改之后的布局文件代码如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"android:padding="10dp" ><android.support.v4.view.ViewPagerandroid:id="@+id/vp_content"android:layout_width="match_parent"android:layout_height="400dp" ><com.example.custom.widget.CustomPagerTabandroid:id="@+id/pts_tab"android:layout_width="wrap_content"android:layout_height="wrap_content"app:textColor="@color/red"app:textSize="17sp" /></android.support.v4.view.ViewPager>
</LinearLayout>

上述自定义属性的三个步骤,其中第二步骤涉及到Java代码,接下来用Kotlin改写CustomPagerTab类的代码,主要改动有以下两点:

1、原来的两个构造函数,合并为带默认参数的一个主构造函数,并且直接跟在类名后面;
2、类名后面要加上注解“@JvmOverloads constructor”,表示该类支持被Java代码调用。因为布局文件中引用了自定义视图的节点,系统是通过SDK里的Java代码找到自定义视图类,所以凡是自定义视图都要加上该注解,否则App运行时会抛出异常。
下面是CustomPagerTab类改写之后的Kotlin代码:

//自定义视图务必要在类名后面增加“@JvmOverloads constructor”,因为布局文件中的自定义视图必须兼容Java
class CustomPagerTab @JvmOverloads constructor(context: Context, attrs: AttributeSet?=null) : PagerTabStrip(context, attrs) {private var txtColor = Color.BLACKprivate var textSize = 15init {txtColor = Color.BLACKtextSize = 15//初始化时从attrs.xml读取CustomPagerTab的自定义属性if (attrs != null) {val attrArray = getContext().obtainStyledAttributes(attrs, R.styleable.CustomPagerTab)txtColor = attrArray.getColor(R.styleable.CustomPagerTab_textColor, txtColor)textSize = attrArray.getDimensionPixelSize(R.styleable.CustomPagerTab_textSize, textSize)attrArray.recycle()}setTextColor(txtColor)setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize.toFloat())}
}

测量尺寸

绘制

完整的自定义视图有3部分组成:
1.定义构造函数,读取自定义属性值并初始化
2.重写测量函数onMesure,计算该视图的宽高尺寸
3.重写绘图函数onDraw(控件+布局)或者dispatchDraw(布局),在当前视图内部绘制指定形状

public class RoundTextView extends TextView {public RoundTextView(Context context) {super(context);
}
public RoundTextView(Context context, AttributeSet attrs) {super(context, attrs);
}
public RoundTextView(Context context, AttributeSet attrs, int defSt
yle) {super(context, attrs, defStyle);
}
//控件只能重写onDraw方法
@Override
protected void onDraw(Canvas canvas) {super.onDraw(canvas);
Paint paint = new Paint();
paint.setColor(Color.RED);
paint.setStrokeWidth(2);
paint.setStyle(Style.STROKE);
paint.setAntiAlias(true);
RectF rectF = new RectF(1, 1, this.getWidth()-1, this.getHeight()-1);canvas.drawRoundRect(rectF, 10, 10, paint);
}
}//“@JvmOverloads constructor”
class RoundTextView @JvmOverloads constructor(context: Context, attrs:
AttributeSet?
=null, defStyle: Int=0) : TextView(context, attrs, defStyle) {override fun onDraw(canvas: Canvas) {super.onDraw(canvas)val paint = Paint()
paint.color = Color.RED
paint.strokeWidth = 2f
paint.style = Style.STROKE
paint.isAntiAlias = true
val rectF = RectF(1f, 1f, (this.width - 1).toFloat(), (this.hei
ght - 1). toFloat())
canvas.drawRoundRect(rectF, 10f, 10f, paint)
}
}
public class RoundLayout extends LinearLayout {public RoundLayout(Context context) {super(context);
}
public RoundLayout(Context context, AttributeSet attrs) {super(context, attrs);
}
public RoundLayout(Context context, AttributeSet attrs, int defStyl
e) {super(context, attrs, defStyle);
}
//布局一般重写dispatchDraw方法,防止绘图效果 被上面的控件覆盖
@Override
protected void dispatchDraw(Canvas canvas) {super.dispatchDraw(canvas);
Paint paint = new Paint();
paint.setColor(Color.BLUE);
paint.setStrokeWidth(2);
paint.setStyle(Style.STROKE);
paint.setAntiAlias(true);
RectF rectF = new RectF(1, 1, this.getWidth()-1, this.getHeight
()-1);
canvas.drawRoundRect(rectF, 10, 10, paint);
}
}

用Kotlin改写后:

//自定义视图要在类名后增加“@JvmOverloads constructor”
class RoundLayout @JvmOverloads constructor(context: Context, attrs: At
tributeSet?
=null, defStyle: Int=0) : LinearLayout(context, attrs, defStyle) {override fun dispatchDraw(canvas: Canvas) {super.dispatchDraw(canvas)
val paint = Paint()
paint.color = Color.BLUE
paint.strokeWidth = 2f
paint.style = Style.STROKE
paint.isAntiAlias = true
val rectF = RectF(1f, 1f, (this.width - 1).toFloat(), (this.height - 1).toFloat())
canvas.drawRoundRect(rectF, 10f, 10f, paint)
}
}

9.3自定义通知栏

Notification



三种特殊的通知类型:
1.进度通知 setProgress
2.浮动通知 setFullScreenIntent
3.锁屏通知setVisibility

远程视图RemoteView

消息通知自定义布局,需要借助RemoteView实现
Notification.Builder的setContent

9.4 Service服务

普通启动服务

startService<NormalService>()
//带参数
startService<NormalService>
("request_content" to et_request.text.toString())val intent = intentFor<NormalService>
("request_content" to et_request.text.toString())
startService(intent)val intent = intentFor<NormalService>
(Pair("request_content", et_request.text.toString()))
startService(intent)

绑定方式启动服务

val bindFlag = bindService(intentBind, mFirstConn, Context.BIND_AUTO_CREATE)
//解除绑定
if (mBindService != null) {unbindService(mFirstConn)
mBindService = null
}

推送服务到前台

不要让服务依附于任何页面,而Android允许服务以某种形式出现在屏幕上,那就是通知栏。
startForeground:服务切到前台
stopForeground:停止前台运行(true表示清除通知,false不清除)

<!-- 震动权限-->
<uses-permission android:name="android.permission.VIBRATE" />
val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
vibrator.vibrate(3000)

其他服务管理器:

//下载管理器
val Context.downloader: DownloadManager
get() = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
//定位管理器
val Context.locator: LocationManager
get() = getSystemService(Context.LOCATION_SERVICE) as LocationManager
//连接管理器
val Context.connector: ConnectivityManager
get() = getSystemService(Context.CONNECTIVITY_SERVICE) as Connectiv
ityManager
//电话管理器
val Context.telephone: TelephonyManager
get() = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyMan
ager
//无线管理器
val Context.wifi: WifiManager
get() = getSystemService(Context.WIFI_SERVICE) as WifiManager
//闹钟管理器
val Context.alarm: AlarmManager
get() = getSystemService(Context.ALARM_SERVICE) as AlarmManager
//音频管理器
val Context.audio: AudioManager
get() = getSystemService(Context.AUDIO_SERVICE) as AudioManager

总结

1.自定义视图
2.消息通知
3.Service
4.各种系统管理

第十章 Kotlin实现网络通信

10.1多线程

Thread {//具体业务
}.start()

水平进度对话框

val dialog = progressDialog("正在加载中", "请稍后")
dialog.show()

圆圈进度对话框

val dialog = indeterminateProgressDialog("正在加载中", "请稍后")
dialog.show()

异步任务

doAsync {//业务代码
uiThread {
//回到主线程}
}

有时候,APP会启动多个分线程,然后对这些线程对象进行调度,动态控制 线程的运行状态,那么就用上doAsyncResult了

//doAsyncResult返回一个异步对象
val future : Future<String> = doAsyncResult(null, longTask)

10.2访问HTTP接口

移动数据格式JSON

普通解析:Kotlin和Java代码差不多

可以利用data数据类+Gson处理

HTTP接口调用

doAsync{//网络请求
uiThread{//更新数据
}
}

10.3文件下载操作

DownloadManager



Android7.0增强了文件 访问权限,造成字段DownloadManager.COLUMN_LOCAL_FILENAME被废弃,所以7.0以上版本访问该字段会抛出java.lang.SecurityException异常,此时获取下载任务对应的文件路径可以通过DownloadManager.COLUMN_LOCAL_URI获得。

10.4 ContentProvider

内容提供者 ContentProvider

ContentProvider为app存取内部数据提供了统一的外部接口,让不同的应用之间共享数据

内容解析器:ContentResolver

内容观察器:ContentObserver

SpannableString

SpannableString可以对文本样式分段处理

val str: Spanned = buildSpanned {append("为", Bold) //文字字体使用粗体
append("人民", RelativeSizeSpan(1.5f)) //文字大小
append("服务", foregroundColor(Color.RED)) //文字颜色}

常见的anko简写:

完。

《Kotlin从零到精通Android开发》欧阳燊相关推荐

  1. Android:《Kotlin 从零到精通Android开发》读书笔记

    原文发布在我的公众号:CnPeng 所有文章将优先发布于公众号,随后才会更新简书. 前前后后整整四十天,终于利用非工作时间读完了 欧阳燊(shen)写的 <Kotlin 从零到精通Android ...

  2. 《Kotlin从零到精通Android开发》欧阳燊(二)

    第六章 Kotlin使用简单控件 6.1使用按钮控件 按钮button 按钮事件三种Kotlin编码方式:匿名函数,内部类,接口实现 1.匿名函数方式 btn_click_anonymos.setOn ...

  3. 《Kotlin从零到精通Android开发》资源下载和内容勘误

    资源下载 下面是<Android Studio开发实战 从零基础到App上线>一书用到的工具和代码资源: 1.本书使用的Android Studio版本为3.0.1,Kotlin版本为1. ...

  4. Kotlin从零到精通Android开发

    作者 博客地址 https://blog.csdn.net/aqi00 最新源代码 https://github.com/aqi00/kotlin 资源下载和内容勘误  https://blog.cs ...

  5. 列表怎么有限的初始化为零_《零基础学习Android开发》第五课 类与面向对象编程1-1...

    视频:<零基础学习Android开发>第五课 类与面向对象编程1-1 类的定义.成员变量.构造方法.成员方法 一.从数据与逻辑相互关系审视代码 通过前面的课程,我们不断接触Java语言的知 ...

  6. 精通Android开发 0

    准备写一个Android的教程,顺便复习巩固下自己的知识,主要参考的书是<精通Android 3 > 1 环境搭建. 以前准备入门一门语言的时候,环境的搭建会是件很头疼的事情.知道的人懒的 ...

  7. 零基础学Android开发系列

    目标:沉浸了这么久,准备写点文章,在写文章的同时,巩固Android开发的基础知识,每天一个Android小案例,从零带领大家开发简单的Android应用.

  8. 精通Android开发 1

    intent 概括 Android引入了一个intent的概念来调用组件. Android的活动组件包括活动(UI组件),服务,广播,和contentProvider 简单层面上将,intent就是告 ...

  9. Kotlin - 100%兼容java和android开发

    2019独角兽企业重金招聘Python工程师标准>>> https://kotlinlang.org/ 还可以输出js 转载于:https://my.oschina.net/swin ...

最新文章

  1. php字符串数组访问,php – 按字符串键x访问数组,其中x是“123”
  2. 网络命令大全(13)--ftp
  3. 深度学习笔记二:PAC,PAC白化,ZCA白化
  4. K8S资源限定(CPU、内存)及pod数量修改
  5. c语言变凉存储性,C语言数据的表示和存储(IEEE 754标准)
  6. 小米手机60帧录屏_手机录屏怎样只录手机内部声音不录入外部声音?教你三种方法,一定能帮到你...
  7. 如何为编程爱好者设计一款好玩的智能硬件(三)——该选什么样的MCU呢?
  8. PHP远程文件管理,可以给表格排序,遍历目录,时间排序
  9. Nginx学习总结(7)——Nginx配置HTTPS 服务器
  10. 通过char与varchar的区别,学习可变长的字符类型
  11. python批量删除txt文本前面几行和最后几行
  12. Xcode里的-ObjC,-all_laod和-force_load的作用
  13. 华为m6更新鸿蒙吗,华为手机升级鸿蒙的方法,有哪位升级成功的吗?
  14. Android自定义View-简约风歌词控件
  15. 国内数藏造富只是个例,散户见好就收
  16. QQ圈子:你的亲密敌人
  17. Spring Boot教程十六:SpringBoot注入类实现多线程
  18. 数据结构系列-初识数据结构
  19. 关于css命名规范一些小技巧或经验
  20. 搭建Windows 家庭用无线局域网WLAN (Wireless Local Area Network)

热门文章

  1. 华硕adolbook14计算机专业能用吗,华硕a豆adolbook14s怎么样 华硕a豆adolbook14s值得买吗...
  2. 360公司为什么有名?商业模式不同而已
  3. Verilog门级实现二选一多路选择器
  4. Git---当本地分支和远程分支都被删除时,如何处理?
  5. SkeyeRTSPLive高效转码之SkeyeVideoEncoder高效硬件编码解决方案(附源码)
  6. 正点原子IMX6UL I2C驱动AT24C512
  7. Python 作业一
  8. 空间掩模matlab,Matlab数字图像 空间滤波
  9. linux游戏星际公民,众筹金额超3亿美元 《星际公民》开启促销庆祝活动
  10. Kubernetes家族容器小管家Pod在线答疑?