Skip to content

Kotlin

官方手册:Android 的 Kotlin 优先方法 | Android 开发者 | Android Developers (google.cn)

中文手册 : Kotlin/Native - Kotlin 语言中文站 (kotlincn.net)

参考文档 - Kotlin Programming Language (liying-cn.net)

Android精华教程_郭霖的专栏-CSDN博客

变量

```kotlin // 基本类型有:数字、字符、布尔值、数组与字符串 // 变量默认为 publish // 变量声明时必须初始化,而且不能为 null val str: String = null // error val str: String? = null // 指定一个可 null 的值 val len = str?.length // 自动兼容 null val len = str!!.length // 强制关闭 null 检测
val str: String // 变量声明后不可修改 var str: String // 变量声明后要修改 lateinit var str: String // 声明一个变量可以在以后初始化 ,不需要先赋值, 使用 ::str.isInitialized 判断有没有初始化 val name: Int by lazy { 1 } // 延迟初始化

```

函数

fun main(): Unit {} // 表示返回 void
fun main() {} // 可省略


// 函数中的函数
fun login(user: String, password: String, illegalStr: String) {

    fun validate(value: String) {
      if (value.isEmpty()) {
          throw IllegalArgumentException(illegalStr)
      }    
    }

    validate(user, illegalStr)
    validate(password, illegalStr)
}

可见性修饰符

public:公开,可见性最大,哪里都可以引用。
private:私有,可见性最小,根据声明位置不同可分为类中可见和文件中可见。
protected:保护,相当于 private + 子类可见。
internal:内部,仅对 module 内可见。

// 类默认为 public
// 类默认为 final 不可继承,需要手工指定 open, 但是子类默认也是 final
// 重载函数 override, 如果不想子类再重载,在函数前+ final
// abstract 虚拟类,与 c# 类似 

// 实例化不需要使用 now
var activity: Activity = NewActivity()

// is 类型判断 
if (activity is NewActivity) {        
    activity.action() // 的强转由于类型推断被省略了
}

// as 类型转换
(activity as NewActivity).action() // 如果转换失败,会抛出异常
(activity as? NewActivity)?.action() // 使用 as? , 转型失败返回 null,不调用,不会有异常


构造

// class Person (private val name: String) 可如下简写, 需要注明 val 或是 var ,次构造函数不能指定 val/var ,并且必须使用 this 调用  
// 主构造函数可以设置为 private , class Person private constructor(private val name: String)
// 主构造函数参数默认在类中创建对应的属性
class Person constructor(private val name: String) : Eater { 

    var age: Int = 0

    init {
        // 主构造函数
    }

    constructor(name: String, age: Int) : this(name) {
        this.age = age
    }
// 或是指定类为 文件名Kt, 默认为 public

class Program {

    companion object {

        // 入口函数, 静态函数
        @JvmStatic
        fun main(args: Array<String>) {

            Rect.ShowDemo()
        }
    }
}

示例

package Gnlab.X // 定义一个包名

// 自动生成一个 只读属性 Height , 一个私有的读写属性 width
class Rect(val Height: Int, private var width: Int) {



    companion object {

        // 演示类属性, 相当于静态函数
        fun ShowDemo() {

            val rect = Rect(220, 220)
            println("$rect")
            println("${rect.name}")
            println(rect.Height)
            rect.isChanged = false
            println("IsChanged: ${rect.isChanged} ")
            println("IsSquare: ${rect.isSquare}")

            println("demo")
        }
    }
}

字段、属性

open class Person {

    val lastName = "mike" // 字段,其实后台会编译为属性(加入 get set)

    // 一个只读属性, 后台会编译为属性(加入 get set)
    val name: String = "rect name"

    // 一个读写属性
    var isChanged: Boolean = true

    // 自定义一个访问器
    val isSquare: Boolean
        get() {

            return Height == width
        }

    var age = "demo" // 属性,并可进行赋值或计算返回
        get() {
            return field + " nb"
        }
        set(value) {
            field = "Cute " + value
        }

    val name = "Mike" // 只读属性,也可以进行计算返回 
        get() {
            return field + " nb"
        }

    // 简写,带有参数默认值
    fun area(width: Int, height: Int = 32): Int = width * height

}

静态函数、属性、静态类

// object 关键词,直接创建一个对象 , 就是单例对象, 而且线程安全 

//////////////////////////////////////////////////////////////////////////////////////////
// 相当于静态类
object Oper {

    fun GetName(): String {
        return "test"
    }
}

// 调用 
Oper.GetName()

//////////////////////////////////////////////////////////////////////////////////////////
// 匿名对象实现接口
interface IOper {
    fun getName(): String
}

val oper = object : IOper {
    override fun getName():String {
        return "test"
    }
}

val name = oper.getName()
Log.i("info", name)


//////////////////////////////////////////////////////////////////////////////////////////
// 静态属性、函数
class A {
    // 相当于实例化一个对象,所有静态属性或函数都放在这里。 B 可以省略。
    // 一个类中只允许一个内部的伴生对象,所以只能定义一个 companion object 
    companion object B {
        var c: Int = 12
        fun getName(): String {
            return "name"
        }

        // 定义一个静态对象,应该可以被 java 调用 
         @JvmStatic fun doAction(){
            println("test")
        }   
    }

}

Log.i("test", "${A.B.c}")
Log.i("test", "${A.c}") // B 可以省略
Log.i("test", "${A.B.getName()}")
Log.i("test", "${A.getName()}") // B 可以省略



顶层函数

// 使用静态函数的话,可以直接顶层函数来替代 

package com.hencoder.plus
fun topLevelFuncion() { } // 属于 package,不在 class/object 内

// 使用
import com.hencoder.plus.topLevelFunction
topLevelFunction()

// java 要调用的话,使用 文件名kt.函数名

常量

// const 只能常规数据类型或字符串,因为其它对象的话,可以修改对象属性值,不能认为是常量 

class Sample {
    companion object {
        const val CONST_NUMBER = 1 // 对象中
    }
}

const val CONST_SECOND_NUMBER = 2 // 项层类中

data class

// 使用 data class 自动完成 equals hashCode toString 这些重载函数
// 以简化常用类
data class Cellphone(val brand: String, val price: Double )

密封类

sealed class Result
class Success(val msg: String) : Result
class Failure(val error: Exception):Result

// 密封类的继承必须在同一个 kt 文件中,when 可以不用写 else, 因为继承类得到控制

数组等

val strs: Array<String> = arrayOf("a", "b", "c") // 数组 相当于 c# Array / []
val intArray = intArrayOf(1, 2, 3) // 特定类型的 array

val strList = listOf("a", "b", "c") // 相当于 list, mutableListOf 可变 或 toMutableList
val strSet = setOf("a", "b", "c") // mutableSetOf 可变 或 toMutableSet
val map = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 3) // 使用 to 将 key 与 value 创建连接, mutableMapOf 可变 可 toMutableMap
sequenceOf("a", "b", "c") // 不确定为什么多一个这个类型

常用操作 (lambda)

intArray.forEach { i -> print(i + " ") } // 遍历每一个元素
val newList: List = intArray.filter { i -> i != 1 } // 过滤掉数组中等于 1 的元素
val newList: List = intArray.map { i -> i + 1 } // 每个元素加 1, 得到一个新数组 
intArray.flatMap { i -> listOf("${i + 1}", "a") } // 每个元素生成新集合,并合并到一个新的数组中, linq 中的 manayselect
intArray.maxBy {  it.length }  // 取出最大值 
intArray.any { } // 某一项为 true
intArray.all { } // 所有为 true

Range

val range: IntRange = 0..1000 // [1, 1000]
val range: IntRange = 0 until 1000 // [0, 1000) 
var range = 0..1000 step 2 //
var range = 1000 downTo 1 // [1000, 1]                               

Sequence

相当于 c# linq

        // 类似于 linq 惰性计算,先编译计算公式,调用 first 之类才进行计算,并且计算出指定的值后即停止。下面的例子只计算 3 次
        val it1 = (1..1000).asSequence()
            .map { it -> Log.i("info", "map $it"); it * 2 }
            .filter { it -> Log.i("info", "filter $it"); it % 3 == 0 }
            .first()


        // 类比于 sequence map/filter 调用一次,所有的值都会计算一次
        val it2 = (1..1000).asIterable()
            .map { it -> Log.i("info", "map $it"); it * 2 }
            .filter { it -> Log.i("info", "filter $it"); it % 3 == 0 }
            .first()

字符串

// 拼接字符串
String.format("Hi %s", name)
"Hi $name"
"Hi ${name.length}"

// 原生字符串,相当于 C# @""
val text = """
      Hi $name!
    My name is $myName.\n
"""

// 逐行格式化, trimMargin, 默认去掉每一行前的 |及空格 
val text = """
      |Hi world!
    |My name is kotlin.
""".trimMargin()

lambda 函数简写

// 传入一个接口实例,相当于匿名接口实现类, 比如 Runnable 接口
Thread(object: Runnable { override fun run(){ Log.i('test') } }).start() // 使用 object 标志此为一个实例 

// 简写I, 因为该接口只要实现一个实现,所以自动推断实现函数
Thread( Runnable {Log.i('test')}).start() // 增掉函数声明部分

// 因为是传入接口,会自动推断为何接口,简写II
Thread( { Log.i('test') }).start() // 只要实现部分

// 如果函数参数就是一个 lambda ,则函数括号可以省略,简写III
Thread { Log.i('test') }.start() // 去掉括号

比如
button.setOnClickListener { 鼠标事件对应的函数体 }

逻辑控制

条件控制

if/else


// 类似于三元计算 
val it = if (a < b) 12 else 15

枚举/when

替代 switch

enum class Color {
    RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}

////////////////////////////////////////////////

// 也可以带有属性的类作为枚举 
enum class Color(val r: Int, val g: Int, val b: Int) {

    // 初始化枚举, ',' 进行分隔
    RED(255, 0, 0), ORANGE(0, 255, 0), BLUE(0, 0, 255);

    fun rgb() = ( r * 256 + g ) * 256 + b

    companion object{

        fun getName(color: Color) : String {

            return when(color){
                RED -> "RED"
                ORANGE -> "ORANGE"
                BLUE -> "BLUE"
                else -> throw Exception("unknown error")
            }
        }
    }
}

        when (x) {
            in 1..10 -> print("x 在区间 1..10 中")
            in listOf(1, 2) -> print("x 在集合中")
            !in 10..20 -> print("x 不在区间 10..20 中")
            is String -> print("x 是不是 string")
            ((x * 3) == 32) -> print("进行计算")
            else -> print("不在任何区间上")
        }

for-in

for (i in 0..10) {
    println(i)
}

try-catch

try {
    ...
}
catch (e: Exception) {
    ...
}
finally {
    ...
}

// 直接返回值
val a: Int? = try { parseInt(input) } catch (e: NumberFormatException) { null }

?. 和 ?:

val str: String? = "Hello" // 可为空对象S=
var length: Int = str?.length // error , 因为此为 Int?
val length: Int = str?.length ?: -1 // 正确, 相当于 c# ??

// let, 相当于做一次  if ( val != null) , 但是 if 线程不安全,let 线程安全
val str: String? = "test"
str?.let {
    it.substring(0, 2)
    val len = it.length
}

== 和 ===

== :可以对基本数据类型以及 String 等类型进行内容比较
=== :对引用的内存地址进行比较

val str1 = "123"
val str2 = "123"
println(str1 == str2) // 👈 内容相等,输出:true

val str1= "字符串"
val str2 = str1
val str3 = str1
print(str2 === str3) // 👈 引用地址相等,输出:true

标准函数

use

val bufferedWriter = BufferedWriter(streamWriter)
// use 调用完成后,自动调用关闭函数
bufferedWriter.use{
    it.write("测试内容")
}

let

// let, 相当于做一次  if ( val != null) {  }, 但是 if 线程不安全,let 线程安全
val str: String? = "test"
str?.let {
    it.substring(0, 2)
    val len = it.length
}

with

// with , 传入一个对象,with 中进行该对象的上下文,可以精简一些写法 
val range: IntRange = 0..1000 // [1, 1000]
        val result = with(StringBuilder()) { // 传入 with 的对象
            append("title") // 进入 with 对象的上下文
            range.forEach { append(it) } // 进入 with 对象的上下文
            append("end") // 进入 with 对象的上下文

            toString() // 作为 with 的返回值
        }

run

// 与 with 一样,写法不同。对一个对象调用 run ,进入该对象的上下文进行处理。最后一行返回结果
        val range: IntRange = 0..1000 // [1, 1000]
        StringBuilder().run { 
            append("title")  // 进入 with 对象的上下文
            range.forEach { append(it) }  // 进入 with 对象的上下文
            append("end")  // 进入 with 对象的上下文

            toString() // 作为 with 的返回值
        }

apply

// 与 run 一样,只是没有返回值,进行对象的上下文进行处理
        val range: IntRange = 0..1000 // [1, 1000]
        var result = StringBuilder().apply { // 但是没有返回值,返回值即对象本身
            append("title") // 进入 with 对象的上下文
            range.forEach { append(it) } // 进入 with 对象的上下文
            append("end") // 进入 with 对象的上下文
        }

        Log.i("log", result.toString())

扩展函数

// 1 最好作为顶层函数
// 2 类名.函数名, 函数中使用 this 来访问该对象
fun String.lettersCount(): Int {
    var count = 0
    for (char in this) {
        if (char.isLetter()) {
            count++
        }
    }

    return count
}

// 调用 
"abc".lettersCount()

运算符重载

// + plus; - minus; * times; / div; % rem; ++ inc; -- dec; ! not; == equals; >|<|>=|<=| compareTo; a..b rangeTo; [] get|set;  in contains
class Obj {

    operator fun plus(obj: Obj) { /* 处理加法 */
        this.count += obj.count
    }

}

高阶函数

参数或是返回值为函数的函数为高阶函数

# (函数参数类型, ) -> 返回值类型,如果无返回值,为 Unit
(String, Int) -> Unit 

# 例子
fun num1AndNum2(num1: Int, num2: Int, oper: (Int, Int) -> Int) :Int{
    return oper(num1, num2)
}

# 调用, ::plush 为一个顶层函数
num1AndNum2(1, 2, ::plus)

# lambda 写法
num1AndNum2(1, 3) { num1, num2 -> num1 + num2 }


# 辅助构造函数, StringBuilder.() 表示在哪个上下文中,类似于 apply
# 1. build 前的 StringBuilder 表示,这是 StringBuilder 的扩展函数, 
# 2. block 后的 StringBuilder 表示 block 的调用上下文是必须为 StringBuilder, 如果不加,则 block 为普通函数
# 3. 第三个 StringBuilder 表示,返回值类型
fun StringBuilder.build(block:StringBuilder.() -> Unit): StringBuilder {
    ... 进行一些自定义的初始化操作, 
    this.block()
    return this
}
# 调用
val result = StringBuild().build { /* 自定义初始化内容 */  }

内联函数

高阶函数的原理是使用 java 的一个 Function 接口,在函数参数中传入该接口的匿名类实现,但是每调用一次就会实例化一次接口实例效率会比较差

而内联就是将对函数的调用,在调用的地方直接展开,这样可以减少开稍

############################################################################
### 那么每个调用的地方就会将函数展开
inline fun num1AndNum2(num1: Int, num2: Int, oper: (Int, Int) -> Int) :Int{
    return oper(num1, num2)
}

############################################################################
### noinline 指定传入函数不进行展开
inline fun num1AndNum2(
    num1: Int,
    num2: Int,
    oper1: (Int, Int) -> Int,
    noinline oper2: (Int, Int) -> Int // 该函数不进行展开
): Int {
    return oper1(num1, num2) + oper2(num1, num2)
}

############################################################################
### lambda 中的 return , lambda 中不允许使用 return ,需要使用 return@函数名 的形式进行局部返回
### 但是如果函数进行了展开,则可以使用 return ,因为 return 会被在调用函数中展开的,但是这样就是将上层函数 return 了

############################################################################
### crossinline 
### 像下面这样调用会发生错误,
### 1. inline 函数中 block 中是可以使用 return 的。
### 2. 匿名函数实现 Runnable 中使用了 lambda 调用,是不允许使用 return 的。
### 3. 1、2 造成冲突。所以需要定义 block 为 crossinline , 表示 block 中确保不会调用 return。像这样 inline fun runRunnable( crossinline block: () -> Unit )  { ... }
inline fun runRunnable( block: () -> Unit ) :Runnable {

    // 内部使用了匿名接口实现,实现 Runnable 接口
    return  Runnable{
        block()
    }
}


高阶函数应用


/************************************************************************************/
/** 1. SharedPreferences 的 open 扩展函数, this 为  SharedPreferences */
/** 2. block 为 SharedPreferences.Editor 上下文的回调函数, 回调函数中为 Editor */
fun SharedPreferences.open(block: SharedPreferences.Editor.() -> Unit ){
    val editor = this.edit()
    /** todo: 进行一些通用化操作 */
    editor.block()
    editor.apply()
}

调用 
this.getSharedPreferences("tmp", MODE_PRIVATE).open{}

/************************************************************************************/
/** 一个创建数据库操作字典的快捷函数 */
/** vararg 指定这是一个可变函数 */
/** Pair<> 使用 a to b 的语法创建键值对 */
fun cvOf(vararg pairs: Pair<String, Any?>): ContentValues {

    val values = ContentValues()

    // 枚举出所有参数
     for (it in pairs){
         val key  = it.first
         // 因为 values 没有泛型的 put, 所以下面使用转型后调用 put
         when(val value = it.second){
             is Int -> values.put(key, value)
             is Long -> values.put(key, value)
             is Short -> values.put(key, value)
             is Float -> values.put(key, value)
             // ... 剩下其它类型的赋值
         }
     }

    return values
}

# 调用
cvOf("name" to 2, "author" to "x", "price" to 32.4 )



infix

定义一个函数的语法糖


// 定义一个函数, 前面加上 infix 并指定一个参数,则为 xxxx 函数名 参数
infix fun String.beginsWith(prefix:String) = startsWith(prefix)
if( "this is test " beginsWith "test" ){ } // 调用事例

// 构建 map 的 to 语法, 定义如下
infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

泛型与委托

使用上与 C# 很像

// 泛型类
class MyClass<T> {
    fun method(param: T): T {
        return param
    }
}

// 泛型函数
fun <T> func(param: T) :T{ return param }

// 泛型类型限制
fun <T:Number> func(param: T):T { return param }

使用例子

比如原来的 build 函数,使用了指定类型,这里可以转为泛型


// 这里指定了类型必须为 StringBuilder
fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder {
    // todo: 进行一些通用初始化
    block()
    return this
}

// 这里使用了泛型,使用范围可能更广
fun <T> T.build(block: T.() -> Unit):T{
    // todo: 进行一些通用初始化
    this.block()
    return this
}

高级使用

java/kotlin 的泛型只有编译时有效。运行时没有泛型的概念(称为类型擦除机制)。kotlin 更进一步,引用内联机制进行泛型的实化。这样可以使用 T::class.java 类型


# 1. 使用 inline 声明内联
# 2. 泛型使用 reified
inline fun <reified T> getGenericType() = T::class.java

# 使用
val strType = getGenericType<String>()
val intType = getGenericType<Int>()


# 例子, startActivity 的辅助函数
inline fun <reified T> startActivity(context: Context, block: Intent.() -> Unit) {

    val intent = Intent(context, T::class.java) # 1. 生成 Intent
    intent.block() # 2. 调用 block 进行初始化
    context.startActivity(intent) # 3. startActivity 启动
}

# 使用
startActivity<MyService>(this){
    this.putExtra("item1", 1)
    this.putExtra("item2", 2)
}

泛型的协变与逆变

不常用

协变

# 1. 现象
    泛型列表对象不能传至父类参数的函数中, 比如存在 B:A, list<B> 不能传到 fun(list<A>) 中
# 2. 原因
    因为如果存在 C:A, 在函数 fun(list<A>) 中可以调用 list<A>.add(C), 但是传入的实际上是 list<B>, 这是不允许的 
# 3. 解决
    声明泛型数据在类中只能通过函数返回,不能设置 (相当于只读),这叫做协变,就能安全的传入函数中
    使用 out 声明
    class SimpleData<out T>(val data: T?) // val data 或是 private var data 这样不能外部设置
    {
        fun get(): T? = data
        fun set(data: T?){ } # 不能这样用,因为会有设置
    }
# 4. 特例
    class SimpleData<out T>(val data: T) {
        fun set(data: @UnsafeVariance T?){ } # 使用 @UnsafeVariance 声明不会破坏内部数据,比如传入的 data 仅用来进行数据比较之类的, 编译器对放行
    }
    比如 list<T> 是只读的,它被声明为协变的。但是 contains 会传入 T ,所以加了 @UnsafeVariance

逆变

# 1. 现象
    泛型对象不能传至子类参数的函数中, 比如存在 B:A SimpleData<A> 不能会传入 fun(SimpleData<B>) 中
# 2. 原因
    因为如果存在 C:A 在函数 fun(SimpleData<B>) 中可能调用 SimpleData<B>[0] 返回一个 C     
    比如 
    class SimpleData<A>() {
        fun get() : A{ 
            return C() 
        }
    }

    func test(data: SimpleData<B>){
        data.get() # 因为参数声明为 B, get 应该返回 B, 但是可能会传入  SimpleData<A> ,这时返回 C ,在运行时会报错
    }

# 3. 解决
     声明泛型类只能传入参数不能传出参数, 使用 in 进行声明 
    class SimpleData<in A>() {
        fun get() : A{ 
            return C() 
        }
    }

# 4. 特例
    其实我有点晕
    class SimpleData<in A>() {
        fun get() : @UnsafeVariance A { 
            return C() 
        }
    }

委托

当我们实现一个接口的时候,如果接口比较多,有一种方式,可以内置一个实现该接口的对象,然后将所有接口实现都调用该对象对应的接口。但是如果接口太多的话,代码量太大了一些。

kotlin 可以在语法层面实现该功能,使用 by 语法,可以自动指定接口函数由某个对象实现

** 我的想法,直接继承内置类就不就可以了吗?

// 1. 自定义一个实现 Set 接口的类 MySet, 并为一个泛型类,
// 2. 内置一个 helperSet 字段,
// 3. 使用 by 指定自定义类的 Set 接口实现都由 helperSet 实现
class MySet<T>(private val helperSet: HashSet<T>) : Set<T> by helperSet {

    // 重载某个函数
    override fun isEmpty(): Boolean {
        // 进行一些自定义操作
        return this.helperSet.isEmpty()
    }

    // 实现一个自定义函数
    fun hello(): String {
        return "hello"
    }
}

委托属性

将一个属性(字段)的实现交给另一个类实现


class MyClass {
    // 调用 Delegate 的 getValue 与 setValue,
    // 读取 p 时调用 Delegate.getValue
    // 设置 p 时调用 Delegate.setValue
    // 如果是 val,则 Delegate 可以不用实现 Delegate.setValue
    var p by Delegate()
}

class Delegate {

    var propValue: Any? = null

    // myClass: MyClass 表示可以在哪个类中使用
    // prop: KProperty<*>:应该相当于 C# 中的 Type
    operator fun getValue(myClass: MyClass, prop: KProperty<*>): Any? {
        return this.propValue
    }

    operator fun setValue(myClass: MyClass, prop: KProperty<*>, value: Any?) {
        this.propValue = value
    }
}

委托属性应用

lazy 就使用了 by 关键字,下面自行实现一个


// 1. 初始化类时传入 T 的构建函数
class Later<T>(val block: () -> T) {

    var value: Any? = null

    operator fun getValue(any: Any?, prop: KProperty<*>): T {
        // 2. 被调用时如果第一次被调用,则
        if (value == null) {
            value = block()
        }
        // 返回生成的对象
        return value as T
    }
}

// 1.1 直接调用的方式
class MyClass {

    val p: StringBuilder by Later<StringBuilder> {
        val builder = StringBuilder()
        builder.appendLine("test")
        builder
    }
}

// 2. 将委托属性的类包装为一个函数
fun <T> later(block: () -> T) = Later<T>(block)

// 2.1 调用方式, 感觉与直接生成类对象(1.1)比较,没有啥区别
class MyXlass {
    val P: StringBuilder by later<StringBuilder> {
        val builder = StringBuilder()
        builder.appendLine("test")
        builder
    }
}

kotlin 高级应用

vararg

不定数量参数

    /* 返回不定长度数值的最大值 */
    fun max(vararg nums: Int) : Int{
        var current = nums.first()
        for (num in nums){
            current = max(current, num)            
        }

        return current
    }



    /* 返回不定长度数值的最大值, 不指定类型, 因为 java 数值实现接口  Comparable*/
    fun <T: Comparable<T>> max(vararg nums: Int) : Int{
        var current = nums.first()
        for (num in nums){

            if(num > current ){
                current = num
            }
        }

        return current
    }

简化函数

使用扩展函数,将一些调用函数简写

    /** 字符串扩展,提示提示 */
    fun String.showToast(context: Context, duration: Int = Toast.LENGTH_SHORT) {
        Toast.makeText(context, this, duration).show()
    }

    /** 资源文件扩展,提示提示 */
    fun Int.showToast(context: Context, duration: Int = Toast.LENGTH_SHORT) {
        Toast.makeText(context, this, duration).show()
    }

    /** 显示提示窗口 */
    fun View.showSnackbar(text:String, duration: Int = Snackbar.LENGTH_SHORT, actionText: String, block: (() -> Unit) ? = null){
        val snackbar = Snackbar.make(this, text, duration)
        if(!actionText.isNullOrEmpty()){
            snackbar.setAction(actionText){
                block?.invoke()
            }    
        }

        snackbar.show()
    }

DSL

DSL 领域特定语言(Domain Specific Language)。可以脱离原有的语法结构,构建出一套专有的语法结构。kotlin 有这种能力生成 DSL

// 实现一个如下语法 

dependency {
    implementation("x.com.lib.test1")
    implementation("x.com.lib.test2")
}

1. 实现一个类,作为基础。可以输入一些字符串作为类库保存于一个列表中
class Dependency {
    val libaries = ArrayList<String>()
    fun implementation(lib: String){
        libaries.add(lib)
    }
}

2. 实现语法 
    2.1 实现一个顶层函数 dependency
    2.2 传入一个 lambda 函数,该函数是 Dependency 的扩展函数
    2.3 函数内部生成 Dependency 对象,并调用该对象的扩展 labmbda 函数
    2.4 进行返回,有没有返回,或是返回什么无所无所谓
fun dependency(block: Dependency.() -> Unit):List<String>{
    val dependencies = Dependency()
    dependencies.block()
    return dependencies.libaries
}


构建 html DSL

演示

# 调用函数如下,生成一个 table html 
val html = table {        
            tr {                
                td { "Apple" } 
                td { "Grape" } 
                td { "Orange" } 
            }
        }

实现



// 1. td 层
class Td {
    var content = ""
    fun html() = "\n\t\t<td>$content</td>"
}

// 2. tr 层
class Tr {
    private val children = ArrayList<Td>()

    fun td(block: Td.() -> String) {
        val td = Td()
        td.content = td.block()
        children.add(td)
    }

    fun html(): String {
        val builder = StringBuilder()
        builder.append("\n\t<tr>")

        for (it in children) {
            builder.append(it.html())
        }

        builder.append("\n\t</tr>")
        return builder.toString()
    }
}

// 3. table 层
class Table {
    private val children = ArrayList<Tr>()

    fun tr(block: Tr.() -> Unit) {
        val tr = Tr()
        tr.block()
        children.add(tr)
    }

    fun html(): String {
        val builder = StringBuilder()
        builder.append("<table>")

        for (it in children) {
            builder.append(it.html())
        }

        builder.append("</table>")
        return builder.toString()
    }
}

// 4. table 顶层函数
fun table(block: Table.() -> Unit): String {
    val table = Table()
    table.block()
    return table.html()
}



---- 说明 ------
        /** 调用 table 顶层函数,并传入 table 的扩展 lambda 函数 */
        val html = table {
            /** 当前 this 为 table, 调用 table.tr 函数传入 lambda 函数 */
            tr {
                /** 当前 this 为 tr 对象, 调用 tr.td 函数传入 lambda 函数 */
                td { "Apple" } /** 调用 Tr.td */
                td { "Grape" } /** 调用 Tr.td */
                td { "Orange" } /** 调用 Tr.td */
            }
        }

android

Activity

AndroidManifest.xml 中添加 Activity 定义

        <activity
            android:name=".MainActivityX"
            android:label="@string/title_activity_xmain">
            <intent-filter>
                <!-- 定义启动 activity -->
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" /> 
            </intent-filter>
            <intent-filter>
                <!-- 定义一个自定义名称,可以使用该名称启动 -->
                <action android:name="xsoft.demo.xapp.action_start" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
            <intent-filter tools:ignore="AppLinkUrlError">
                <!-- 定义可以响应 view 请求。符合 https 的就支持, 也可以指定 host,port,path,mineType -->
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:scheme="https" />
            </intent-filter>
        </activity>

启动

// 显式调用,指定类定义
val intent = Intent(this, MainActivity_BBS::class.java) // 初始化时,指定启动目标
intent.setClass(this, MainActivity_BBS::class.java) // 指定启动目标, 据说内部调用 setComponent
intent.component = ComponentName(this, MainActivity_BBS::class.java); // setComponent, new 出 ComponentName 进行设置 
intent.setClassName(this, "xsoft.demo.myapplication.MainActivity_BBS") // 使用 string ,但是我没有设置成功

// 隐式调用,指定名称,在 AndroidManifest.xml 中的 intent-filter | action 中定义
val intent = Intent("xsoft.demo.xapp.action_start")

// 打开一个 url 
val intent = Intent(Intent.ACTION_VIEW)
intent.data = android.net.Uri.parse(url) // 动作对应的数据
intent.putExtra() // 传递附加数据
intent.putxxxx() // 传递附加数据

// 进行拨号 
val intent = Intent(Intent.ACTION_DIAL)
intent.data = Uri.parse("tel:{$phone}")

// 开始调用 
this.startActivity(intent)

新启动方式

// 原来的方式看起来过期了,

val result  = ActivityResultContracts.StartActivityForResult()
var resultLauncher = registerForActivityResult(result) { it ->
if (it.resultCode == Activity.RESULT_OK) {
    // There are no request codes
    val data: Intent? = it.data
    }
}

val intent = Intent(this, AppBar::class.java)
resultLauncher.launch(intent)

关闭

this.finish() // 关闭前调用 this.setResult  
this.finishActivity()  // 给 startActivityForResult 返回一个结果 

传递数据

// 使用 intent.putxxxx 附加参数 
val intent = Intent(this, MainActivityX2::class.java)
intent.putExtra("data", "1234567890") 

// 使用 intent.putExtras 批量附加参数 
val bundle = Bundle()
bundle.putString("key1", "test")
bundle.putInt("key2", 32)
intent.putExtras(bundle)

// 传递序列化对象
/// 存入
val bundle = Bundle()
bundle.putSerializable("person_json", Person())
intent.putExtras(bundle)
/// 取出
intent.getSerializableExtra("person_json") as Person

this.startActivity(intent) // 不需要等待返回值
this.startActivityForResult(intent, 123) // 需要等待返回值

// 等待打开窗口的回调返回值
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?){
    val data = data?.getStringExtra("data_return")
    Log.i("activity 返回", "回调 Id: {$requestCode}, activityId: {$resultCode}, data: {$data}")
}

// 打开的窗口中获取传入值
val value = this.intent.getStringExtra("data")

// 关闭打开的窗口,并返回值, 前一个窗口的 onActivityResult 会被回调
val intent = Intent()
intent.putExtra("data_return", "0987654321")
this.setResult(RESULT_OK, intent) // 定义要向前一页返回数据
this.finish()

菜单

<!-- res.menu 目录中定义 xml文件 -->
<menu ...>
    <item android:id="@+id/action_Add" android:title="Add" />
    <item android:id="@+id/action_Delete" android:title="Delete" />
    <item android:id="@+id/action_GotoBaidu" android:title="baidu" />
    <item android:id="@+id/action_Dial" android:title="dial" />
    <item android:id="@+id/action_Close" android:title="Close" />
</menu>
    // 初始化按钮 
    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        this.menuInflater.inflate(R.menu.menu_main, menu)
        return true
    }

    // 定义菜单处理事件 
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when (item.itemId) {
            R.id.action_Add -> this.showMessage("add message")
            R.id.action_GotoBaidu -> this.gotoUrl("https://www.baidu.com")
            R.id.action_Dial -> this.dial("12345678901")
        }

        return true
    }

上下文菜单

    override fun onResume() {
        super.onResume()
         // 在控件上注册菜单 
        this.registerForContextMenu(this.phoneText)
    }

    override fun onPause() {
        super.onPause()
         // 在控件上注销菜单 
        this.unregisterForContextMenu(this.phoneText) 
    }

    // 选择上下文菜单时
    override fun onContextItemSelected(item: MenuItem): Boolean {
        return super.onContextItemSelected(item)
        return true
    }

生命周期

stateDiagram-v2
[*] --> onCreate
onCreate --> onStart
onStart --> onResume
onResume --> onPause
onRestart --> onStart
onPause --> onStop
onPause --> onRestart
onStop --> onDestroy
onDestroy --> onCreate
onDestroy --> [*]
    // 回调保存一些数据
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putString("dataKey", "value")
    }

    // 启动时读回数据,并设置入界面中
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val value = savedInstanceState?.getString("dataKey")
    }

启动模式

AndroidManifest.xml 的使用 android:launchMode 指定

standard: 默认,每启动一次就创建一个实例,并保存至栈顶

singleTop: 如果栈顶有实例,则不创建,否则创建实例,并保存至栈顶

singleTask: 如果栈中有实例,直接将其顶部 activity 出栈,使用现有实例

singleInstance:标记为此的 activity 会单独使用一个调用栈。此 activity 一般与其它应用共用,所以要单独使用一个栈

常用启动及关闭方式


// 写一个单例,收集所有的 activity ,并进行管理
object ActivityCollector{
    private val activities = ArrayList<Activity>()
    fun addActivity(activity: Activity){
        this.activities.add(activity)
    }
    fun removeActivity(activity:Activity){
        this.activities.remove(activity)
    }
    fun finishAll(){
        this.activities.filter { !it.isFinishing }.forEach { it.finish() }
        android.os.Process.killProcess(android.os.Process.myPid())
    }
}

// 写一个 activity 父类,在启动时记录自身
class BaseActivity : AppCompatActivity(){
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Log.i("log", javaClass.simpleName)
        ActivityCollector.addActivity(this)
    }

    override fun onDestroy() {
        super.onDestroy()
        ActivityCollector.removeActivity(this)
    }

    // 关闭所有 activity ,
    fun Close(){
        ActivityCollector.finishAll()
    }

    companion object{
        //  一种常用的启动方式, 约定一些参数,以方式直接调用 
        fun actionStart(context: Context, param1: String, param2: String){
            val intent = Intent(context, BaseActivity::class.java)
            intent.putExtra("param1", param1)
            intent.putExtra("param2", param2)
            context.startActivity(intent)
        }

    }
}

Application

全局唯一实例。在 AndroidManifest.xml 中 application 节 name 属性 可以指定启动 application

<application android:name=".XApplication" />

// 根据需要设置一个全局对象供其它类中访问
class XApplication : Application() {

    // 应用启动时
    override fun onCreate(){
        super.onCreate()
    }

    // 低内存时
    override fun onLowMemory() {
        super.onLowMemory()
    }

    // 配置变更时,比如竖屏、横屏
    override fun onConfigurationChanged(newConfig: Configuration) {
        super.onConfigurationChanged(newConfig)
    }
}

// 访问,使用下面两个方法进行访问。是同一个对象,但在不同的场景下。比如 Activity、Service、BroadcastReceiver 中使用不同方式获取
val application = this.application
val applicationContext = this.applicationContext

控件

布局

LinearLayout

最常用布局之一


<LinearLayout android:orientation="vertical|horizontal">

</LinearLayout>

android:gravity="center" # 控件内的文字排版
android:layout_gravity="center_horizontal" # 该控件在父控件中的排版
android:layout_weight="3" # 该控件在父控件中分配长(宽)的权重。 一般设置 android:layout_width/layout_height="0dp" 
RelativeLayout
<!-- 相对布局 -->
<RelativeLayout
       android:layout_width="match_parent"
       android:layout_height="match_parent" >

       <TextView
           android:id="@+id/CenterButton"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:layout_centerInParent="true"  // 相对于父控件
           android:text="TextView" />

       <TextView
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:layout_above="@+id/CenterButton" // 相对于指定控件的位置
           android:layout_toLeftOf="@+id/CenterButton" // 相对于指定控件的位置
           android:text="TextView" />

   </RelativeLayout> 
FrameLayout

很简单的布局,仅使用 android:layout_gravity 指定位置 。如果不是居中的话,可以指定一下 layout_marginXXX 来定义一下边距

   <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center" // 定义布局
            android:text="文本" />

    </FrameLayout>
ConstraintLayout

现在主推布局

Android新特性介绍,ConstraintLayout完全解析_郭霖的专栏-CSDN博客_constraintlayout

ConstraintLayout学起来! - 简书 (jianshu.com)

Android之约束布局ConstraintLayout详解 - 华为云 (huaweicloud.com)

使用 ConstraintLayout 构建自适应界面 | Android 开发者 | Android Developers (google.cn)

// 指定靠近约束方向: layout_constraintA_toBOf 必须指定约束,不然其它属性不起作用
app:layout_constraintTop_toTopOf // 指定 top 对应于另外一个控件的 top, 比如对应于父框架,则使用 parent

// 居中: 如果指定 top_toxx 与 bottom_toxx 则为居中
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"

// 充满: 如果设置高度或是宽度为 0dp,
// 官方不推荐在ConstraintLayout中使用match_parent,可以设置 0dp (MATCH_CONSTRAINT) 配合约束代替match_parent,举个例子:
// 高充满至 top_toxxx 与 bottom_toxxx 之间的距离。宽充满至 left_toxxx 与 right_toxxx 之间的距离
比如 layout_height=0dp, 则宽度充满 top_toxx 与 bottom_toxx

// 宽高按比例显示, 将高或是宽设置为**父容器**高或宽的百分比, 注意是父容器
app:layout_constraintWidth_percent="0.1"
app:layout_constraintHeight_percent="0.5"

// 边距: layout_marginxxx
需要设置 对应方向 constraintxxx 则对应的 layout_marginxxx 才生效
比如 layout_constraintTop_toxxx layout_marginTop 生效

// layout_goneMarginX
控件不显示也会计算边距

// 左右边距比例
layout_constraintVertical_bias="0.2" 上下边距的比例
layout_constraintHorizontal_bias="0.8" 左右边距的比例

// 宽高比例
app:layout_constraintDimensionRatio="3:4" // 宽高比较, 宽或是高有一个必须为 0dp

// 位置位置均分,第一个控件设置 xxx_chainStyle, 后面的所有 xx_toxx 控件按该值进行分配位置
app:layout_constraintHorizontal_chainStyle="spread_inside" // 平均分配横向或是纵向空间,两侧控件贴边
app:layout_constraintVertical_chainStyle="spread" // 平均分配横向或是纵向空间,两侧控件不贴边 
app:layout_constraintVertical_chainStyle="packed" // 所有控件配横向或是纵向空间居中

// Guideline: 辅助线,不显示,但是其它控件可以设置与该值的相对关系

// 按文字对齐 
app:layout_constraintBaseline_toBaselineOf="@id/textView3"

// 根据角度值进行计算位置 
app:layout_constraintCircle="@+id/textView3" // 相对于哪个控件进行计算
app:layout_constraintCircleRadius="100dp" // 与相应控件的距离
app:layout_constraintCircleAngle="165"  // 与相应控件对应的角度值

// Guideline 辅助线,不显示,使用锚定元素
<androidx.constraintlayout.widget.Guideline
    android:id="@+id/guideline2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical|horizontal" // 设置方向 
    app:layout_constraintGuide_percent="0.3" // 设置左右分隔比例以定位 Guideline
    app:layout_constraintGuide_begin="100dp" //与parent左边界或上边界(根据GuideLine的方向)的距离
    app:layout_constraintGuide_end="100dp" ////与parent右边界或下边界(根据GuideLine的方向)的距离
/>

// Barriers 辅助定位
// 与 Guideline 相似,但不是一根线,而是针对一组控件的最突出的位置指定一条线。
// 比如 Setting 中 label 部分有长有短,而且不同语言下 label 也长短不统一。
// 将 label 都加入到 Barriers 中,并指定方向为 end ,这样 label 最长的那一头为Barriers的基准线
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2017/1017/8601.html
<androidx.constraintlayout.widget.Barrier
        android:id="@+id/barrier7"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:barrierDirection="right" // 以子控件的最右侧为基准线
        app:constraint_referenced_ids="labelText1,labelText2" // 对应的控件 
/>

// 点位符 PlaceHolder
<android.support.constraint.Placeholder
    app:content="@id/btn1" // 指定由哪个控件占用该位置 
    android:id="@+id/pl" 
    android:layout_width="50dp" // 与普通控件一样设置宽
    android:layout_height="50dp" // 与普通控件一样设置高
    app:layout_constraintStart_toStartOf="parent" // 与普通控件一样设置位置
    app:layout_constraintTop_toTopOf="parent" // 与普通控件一样设置位置
/>

// 代码设置
placeHolder.setContentId(R.id.btn1)

// 分组控件, 将一批控件设置一组,统一设置可见性
<androidx.constraintlayout.widget.Group
              android:id="@+id/group"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:visibility="visible"
              app:constraint_referenced_ids="button1,button2" />

// 使用ConstrainsSet 运行动态修改布局
https://www.jianshu.com/p/a95daeaa02d7

自定义布局

classDiagram
View <|-- TextView
View <|-- ViewGroup
View <|-- ImageView
TextView <|-- EditText
TextView <|-- Button
ViewGroup <|-- LinearLayout
ViewGroup <|-- RelativeLayout
ViewGroup <|-- 其它布局

创建布局控件
<!-- res.layout 目录中中创建布局文件类型为 LinearLayout -->

<!-- 引用, 如果要自定义事件,请看下一节 -->
<include layout="@layout/title" />

需要自定义处理函数
// 定义继承对象, 
class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
    init {
        // 与布局文件绑定
        LayoutInflater.from(context).inflate(R.layout.title, this)

        // 定义事件
        this.findViewById<Button>(R.id.title_back_button)?.setOnClickListener {
            val activity = context as Activity
            activity?.finish()
        }
    }
}

// 引用布局文件,需要使用完整路径 
<xsoft.demo.xapp.TitleLayout android:layout_width="match_parent" android:layout_height="wrap_content" />

列表

列表基类为 AdapterView 继承于 ViewGroup, 下面扩展出了 ListView, Spinner 之类的, 为视图提示渲染。

Adapter 为 AdapterView 提供了数据及数据项呈现方式 。一般常用基类为 BaseAdapter

​ ArrayAdapter: 最常用的 List 集合, 参数中指定数组,布局文件,布局文件中的控件编号,自动将数组中的每一项创建布局并填充对应的控件内容,只有填充布局中的一个控件

​ SimpleAdapter: 可以将多组 List 组合成多个列表项,参数中指定n 个等长的数组及布局文件,数组对应内容及内容对应的控件编号,相比于 ArrayAdapter 可以自动填充布局中的多个控件

​ SimpleCursorAdapter: 与 SimpleAdapter 类似,应该是提供了数据库操作相关的功能

​ BaseAdapter: 用来扩展定制,从中他创建布局并填充布局,新版本中扩展出带 ViewHolder 的类以方便使用

简单 ListView

使用字符串数组及ArrayAdapter

        // 生成一些模拟数据
        val names = (0..30).map { faker.animal.unique.name() } // 生成唯一值,但是可能会因为为了生成唯一值重复次数太多而失败

        // 使用 ArrayAdapter, 指定每一项定义 layout 文件, layout 文件中填写文本的控件编号 , 指定每一项的布局及对应数组
        val arrayAdapter = ArrayAdapter(this, R.layout.listview_item, R.id.TextView, names)

        // 设置 listview 的 adapter
        val listView = this.findViewById<ListView>(R.id.ListView)
        listView.adapter = arrayAdapter
简单 ListView 另一个例子
1. 定义界面 
    <ListView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/ListView" />

2. 生成一批模拟数据
    private val mockData: Array<String> by lazy { CreateMockData() }

    fun CreateMockData(): Array<String> {

        val faker = Faker()
        val ls = mutableListOf<String>()
        (1..100).asSequence().forEach { _ -> ls.add(faker.food.fruits()) }

        return ls.toTypedArray()
    }


3. listview 绑定数据的 ArrayAdapter
// android.R.layout.simple_list_item_1 填充方式 

        val adapter = ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, this.mockData)
        val view = this.findViewById<ListView>(R.id.ListView)
        view?.adapter = adapter

自定义 ListView 项
1.0 创建每一项的页面资源文件, 比如 fruit_item.xml
1.1 在 activity 中引用  ListView 

2. 创建每一项对应的数据在 
class Fruit(val name: String, val title: String)

3. 创建 listview 绑定适配器 
class FruitAdapter(private val activity: Activity, private val resourceId: Int, data: List<Fruit>) :
    ArrayAdapter<Fruit>(activity, resourceId, data) {

    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {

        // 返回对应数据项
        val item = this.getItem(position)

        // 返回每一项对应的 view ,并设置其中的控件, convertView 为每一项的视图。在上下滚动的时候,可以重用
        // 可以缓存 findViewById 的结果至 view.tag 中
        val view = convertView ?: LayoutInflater.from(this.activity).inflate(resourceId, parent, false)
        val nameTextView = view.findViewById<TextView>(R.id.Fruit_Item_Name)
        nameTextView?.text = item?.name

        val titleTextView = view.findViewById<TextView>(R.id.Fruit_Item_Title)
        titleTextView?.text = item?.title

        return view
    }
}

4. 将 listview 页面资源文件与适配器进行绑定 
val adapter = FruitAdapter(this, R.layout.fruit_item, this.mockData) // 将每一项的资源与适配器进行绑定 
val view = this.findViewById<ListView>(R.id.ListView) // 返回 listview, 并进行绑定 
view?.adapter = adapter // 绑定
view?.setOnItemClickListener {  parent, view, position, id -> Log.d("--> listviewclick", "我被点击了 ${position}") } // 点击

SimpleAdapter

使用 list 准备一个复杂数据进行填充

        val maxLen = 100 // 准备生成多少数据
        val nameArray = this.getNameArray(maxLen) // 生成一个名称数组 Array<String>
        val imgIdArray = this.getImgIdArray(maxLen) // 生成一个图像id 数组 Array<Int> resources.getIdentifier
        val listItems = (1..maxLen).map { it -> // 生成一个 HashMap 的数组
            val dict = HashMap<String, Any>()
            dict["name"] = nameArray[it]
            dict["img"] = imgIdArray[it]
        }

        // 生成一个 SimpleAdapter, 指定数据数组,数组必须为 HashMap 格式,每个 item 对应一条数据
        // 指定数据显示的布局
        // 指定数据数组中 hashmap 中需要显示的 key 数组
        // 与 key 顺序相同的顺序指定布局中的控件的编号
        // 程序会自动将每个数组中的值填写入控件
        val adapter = SimpleAdapter(
            this,
            listItems,
            R.layout.simple_adapter_item,
            arrayOf("name", "img"),
            intArrayOf(R.id.TextView, R.id.ImageView)
        )

        // 取出 list 视图,并设置入  adapter
        val listView = this.findViewById<ListView>(R.id.ListView)
        listView.adapter = adapter
BaseAdapter

可以从这里继承实现接口,然后设置为 list , 可以见 下面 RecyclerView 的例子,RecyclerView.Adapter 应该实现了 BaseAdapter 而且还加入了 ViewHolder 缓存以实现更高级内容

RecyclerView

应该为 GridView 的升级版本

1.0 准备每一项的资源页面文件 ,比如 fruit_item.xml
1.1 在 activity 中引用 android.support.v7.widget.RecyclerView

2. 准备每一项的数据类
class Fruit(val name: String, val title: String)

3. 创建一个适配器项
class FruitAdapter(val fruitList: List<Fruit>) : RecyclerView.Adapter<FruitAdapter.ViewHolder>() {

    // 每一项的 view 缓存项的定义 
    inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {

        val nameTextView: TextView? = view.findViewById<TextView>(R.id.Fruit_Item_Name)
        val titleTextView: TextView? = view.findViewById<TextView>(R.id.Fruit_Item_Title)
    }

    // 创建每一项的缓存
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {

        val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item, parent, false)
        val holder = ViewHolder(view)

        holder.nameTextView?.setOnClickListener { /* 点击事件处理 */ }

        return holder
    }

    // 填充每一项的页面
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {

        val fruit = fruitList[position]
        holder.nameTextView?.text = fruit.name
        holder.titleTextView?.text = fruit.title
    }

    override fun getItemCount(): Int {
        return this.fruitList.size
    }

}

4. 创建并设置 RecyclerView 的布局对象及数据适配器 
val view = this.findViewById<RecyclerView>(R.id.Fruit_ListView)
view.adapter = FruitAdapter(this.mockData) // 绑定数据项
view.layoutManager = LinearLayoutManager(this) // 布局管理器,根据需要可以换成不同的对象

5. 可以换用其它布局管理器

// 横向滚动
val manager = LinearLayoutManager(this)
manager.orientation = LinearLayoutManager.HORIZONTAL
view.layoutManager = manager

// 瀑布流滚动
view.layoutManager = StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL)




ExpandableListAdapter

收缩控件

// 本例从 BaseExpandableListAdapter 直接继承 , 实际操作时应该有更高级类使用

// 创建 adapter, 并设置视图的 adapter
val adapter = this.createExpandableListViewAdapter()
val view = this.findViewById<ExpandableListView>(R.id.ExpandableListView)
view.setAdapter(adapter)


    // 父项
    class Item(val name: String, val items: Array<SubItem>)

    // 子项
    class SubItem(val name: String, val imgId: Int)

    /* 创建收缩控件 ExpandableListAdapter*/
    private fun createExpandableListViewAdapter(): ExpandableListAdapter {

        // 保存数据项
        val items: Array<Item> = this.LoadItems()

        // 从 BaseExpandableListAdapter 直接继承
        val adapter = object : BaseExpandableListAdapter() {

            // 返回项目总数
            override fun getGroupCount(): Int {
                return items.size
            }

            // 指定父类的项目总数
            override fun getChildrenCount(groupPosition: Int): Int {
                return getGroup(groupPosition).items.size
            }

            // 返回父项
            override fun getGroup(groupPosition: Int): Item {
                return items[groupPosition]
            }

            // 返回父项中指定的子项
            override fun getChild(groupPosition: Int, childPosition: Int): SubItem {
                return getGroup(groupPosition).items[childPosition]
            }

            // 返回父编号
            override fun getGroupId(groupPosition: Int): Long {
                return groupPosition.toLong()
            }

            // 返回子项编号
            override fun getChildId(groupPosition: Int, childPosition: Int): Long {
                return childPosition.toLong()
            }

            // 不清楚
            override fun hasStableIds(): Boolean {
                return true
            }

            // 返回父项对应的视图
            override fun getGroupView(
                groupPosition: Int,
                isExpanded: Boolean,
                convertView: View?,
                parent: ViewGroup?
            ): View? {

                var itemView = convertView

                if (itemView == null) {
                    // 创建出视图
                    itemView = LayoutInflater.from(parent?.context)
                        .inflate(R.layout.activity_main_expandable_list_item_view, parent, false)
                }

                // 编辑视图, 显示文字
                val item = this.getGroup(groupPosition)
                val textView = itemView?.findViewById<TextView>(R.id.TextView)
                textView?.textSize = 24f
                textView?.text = item.name

                // 这里显示 +- 符,收缩展开
                val flag = itemView?.findViewById<TextView>(R.id.TextView_Flag)
                flag?.text = if (isExpanded) "+" else "-"
                flag?.visibility = View.VISIBLE

                return itemView
            }

            // 返回子项对应的视图
            override fun getChildView(
                groupPosition: Int,
                childPosition: Int,
                isLastChild: Boolean,
                convertView: View?,
                parent: ViewGroup?
            ): View? {

                var itemView = convertView

                if (itemView == null) {
                    itemView = LayoutInflater.from(parent?.context)
                        .inflate(R.layout.activity_main_expandable_list_item_view, parent, false)
                }

                val item = this.getChild(groupPosition, childPosition)
                val textView = itemView?.findViewById<TextView>(R.id.TextView)
                textView?.text = item.name

                return itemView
            }

            override fun isChildSelectable(p0: Int, p1: Int): Boolean {
                return true
            }
        }

        return adapter
    }
AdapterViewFlipper

图片轮换显示

    <AdapterViewFlipper
        android:id="@+id/ViewFlipper"
        android:animateFirstView="true"
        android:loopViews="true"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    </AdapterViewFlipper>

        // 設置 flipper
        val flipper = this.findViewById<AdapterViewFlipper>(R.id.ViewFlipper)
        val adapter = this.createAdapter()
        flipper.adapter = adapter // 显示

    /** 顯示下一張圖片 */
    fun showNextImage(){
        val flipper = this.findViewById<AdapterViewFlipper>(R.id.ViewFlipper)
        flipper.showNext()
    }

    private fun createAdapter(): BaseAdapter {

        return object : BaseAdapter() {

            // 载入图片资源列表
            val imgIds: Array<Int> = this@MainActivity_Flipper.loadImageIds()

            override fun getCount(): Int = this.imgIds.size
            override fun getItem(idx: Int): Int  = this.imgIds[idx]
            override fun getItemId(idx: Int): Long = idx.toLong()

            override fun getView(idx: Int, view: View?, parent: ViewGroup?): View {

                // 如果 view 已经存在了,直接重用,设置一下内容即可
                var imgView = view as? ImageView
                if(imgView == null ){
                    imgView = ImageView(this@MainActivity_Flipper)
                    imgView.scaleType = ImageView.ScaleType.CENTER_INSIDE
                    imgView.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)

                    // 点击图片时显示下一张图片
                    imgView.setOnClickListener { this@MainActivity_Flipper.showNextImage() }
                }

                // 设置图片内容
                imgView.setImageResource(getItem(idx))

                return imgView
            }

        }

    }
StackView

图片列表堆叠显示,与 AdapterViewFlipper 一样使用

微调框 (spinner)

其实就是下拉框

使用
<Spinner android:id="@+id/spinner_cal" />

val adapter = null; // 生成一个适配器

val spinner = this.findViewById<Spinner>(R.id.spinner_cal)
spinner.prompt = "请选择" // 文字提示
spinner.adapter = adapter // 设置入适配器
spinner.setSelection(0) // 设置默认选择项

/** 设置选择后的动作 */
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
    override fun onNothingSelected(parent: AdapterView<*>?) { }
    override fun onItemSelected( parent: AdapterView<*>?, view: View?, position: Int, id: Long ) {
                showNote("当前选择第 $position 项")
            }
    }
文本格式

纯文字格式的微调框

// 数据源。可以使用资源文件或数据库:https://developer.android.com/guide/topics/ui/controls/spinner
val strs = arrayOf("水星", "金星", "地球", "火星", "木星", "土星")

// 这里使用字符串 adapter
val adapter = android.widget.ArrayAdapter(this, R.layout.support_simple_spinner_dropdown_item, strs)
        adapter.setDropDownViewResource(R.layout.support_simple_spinner_dropdown_item)

return adapter
带图标

自定义视图, 假设定义一个带 imageview 与 textview 的视图名为 spinner_item.xml

1. 定义一个 ArrayAdapter
class IconArrayAdapter(
        private val activity: Activity,
        private val resourceId: Int,
        objects: Array<IconItem>
    ) :
        android.widget.ArrayAdapter<IconItem>(activity, resourceId, objects) {

        // 定义下拉时的视图, 与默认视图一样处理
        override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
            return getView(position, convertView, parent);
        }

        // 定义默认视图
        override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {

            val view = // 可以尝试缓存 View
                convertView ?: LayoutInflater.from(this.activity).inflate(resourceId, parent, false)

            val item = this.getItem(position) // 返回对应数据项
            item?.let {
                val imgView = view.findViewById<ImageView>(R.id.spinner_item_image_view)
                imgView?.setImageResource(item.icon) // 设置图标

                val txtView = view.findViewById<TextView>(R.id.spinner_item_Text_view)
                txtView?.text = item.text // 设置文字 
            }

            return view
        }
    }



2. 定义 IconArrayAdapter 并设置 spinner
    private fun setupIconArrayAdapterSpinner(spinner: Spinner): SpinnerAdapter {

        val strs = arrayOf(
            IconItem("星球1", R.drawable.star_1_24px),
            IconItem("星球2", R.drawable.star_2_24px),
            IconItem("星球3", R.drawable.star_3_24px),
            IconItem("星球4", R.drawable.star_4_24px),
            IconItem("星球5", R.drawable.star_5_24px),
            IconItem("星球6", R.drawable.star_5_24px),
            IconItem("星球7", R.drawable.star_7_24px)
        )

        // 这里使用字符串 adapter
        val adapter = IconArrayAdapter(this, R.layout.spinner_item, strs)
        return adapter
    }


进度条

ProgressBar
    <ProgressBar
        android:id="@+id/ProgressBar"
        android:layout_width="match_parent"
        android:layout_height="25dp"
        android:progress="50"
        android:max="100"
        style="@android:style/Widget.ProgressBar.Large"
                android:indeterminate="true"/>
SeekBar

滑动条

<SeekBar
    android:layout_width="match_parent"
    android:layout_height="42dp"></SeekBar>

RatingBar

评星条

<RatingBar
    android:layout_width="match_parent"
    android:layout_height="wrap_content"></RatingBar>

ViewSwitcher

多个 view 进行切换,可以设置切换动画

  1. 调用 ViewSwitcher的 setFactory 返回内嵌的 view
  2. 翻页前设置 ViewSwitcher.nextView ( setFactory 中返回 ) 的相关参数 , 比如这里的 view 是 GridView, 那么可以设置 adapter
  3. ViewSwitcher.showPrevious 或 ViewSwitcher.showNext 进行翻页
例子

创建一个类似于 launcher 的程序,设置一批应用图标,分配在多个 GridView 中并支持翻页

    /** 表示一项应用,一页中有多项  */
    class CellItem(val imgId: Int, val text: String)

    // 一些翻页数据
    private var pageIdx: Int = -1 // 当前对应的页码
    lateinit var items: Array<CellItem> = this.createItems() // 用来模拟的项数据, 分配至多页的 viewSwitcher 中

    // 设置 ViewSwitcher 的 setFactory 以返回多页中使用的 view, view 可以动态 new 出并设置,也可以从布局文件中载入
    val switcher = this.findViewById<ViewSwitcher>(R.id.ViewSwitcher)
    switcher.setFactory { //  ViewSwitcher 有多页,所以要使用 setFactory 返回多页布局

            // 这里 new 一个 GridView, 并设置为 3 列
            with(GridView(this)){
                numColumns = 3
                this
            } 

            /*
            * 也可以从布局文件载入一个视图
            val inflater = LayoutInflater.from(this@MainActivity_ViewSwitcer)
            inflater.inflate(R.layout.activity_main_bbs, null) */
        }

        // 设置切换下一页或是下一页
        this.findViewById<Button>(R.id.nextPageButton).setOnClickListener { this.next(switcher) } // 切换下一页
        this.findViewById<Button>(R.id.prevPageButton).setOnClickListener { this.prev(switcher) } // 切换上一页
        this.next(switcher) // 进入第一页

    /** 下一页, 为了方便,统一在 gotoPageView 中调用核心代码  */
    private fun next(switcher: ViewSwitcher) =this.gotoPageView(pageIdx + 1, switcher) { it.showNext() }

    /** 上一页, 为了方便,统一在 gotoPageView 中调用核心代码 */
    private fun prev(switcher: ViewSwitcher) =this.gotoPageView(pageIdx - 1, switcher) { it.showPrevious() }

    /** 跳转至指定的页码, next 为下一部操作 */
    private fun gotoPageView(page: Int,switcher: ViewSwitcher,next: (switcher: ViewSwitcher) -> Unit ) {

         val numEachPage: Int = 18 // 每一页几项

        // 调整下一页正确的页码
        pageIdx = if (page * numEachPage <= items.size) page else page - 1

        // 设置单页 view 中的相关参数,比如设置 adapter
        val gridView = switcher.nextView as GridView
        gridView.adapter = createAdapter(pageIdx, numEachPage, items) // 这里创建一个 BaseAdapter, 传入当前的页码及每页几项  

        next(switcher) // 通过回调调用  showNext 或 showPrevious
    }


    /** 为每一页 GridView 对应的 adapter, 并指定该页页码, 其它与标准操作一样 */
    private fun createAdapter(baseIdx: Int, numEachPage: Int, items: Array<CellItem>): BaseAdapter {
        return object : BaseAdapter() {

            override fun getCount(): Int {
                val remain = items.size - baseIdx * numEachPage
                return if (remain > numEachPage) numEachPage else remain
            }

            override fun getItem(idx: Int): CellItem = items[baseIdx * numEachPage + idx]

            override fun getItemId(idx: Int): Long = idx.toLong()

            override fun getView(idx: Int, view: View?, group: ViewGroup?): View {

                val item = this.getItem(idx)
                val outView = view ?: LayoutInflater.from(this@MainActivity_ViewSwitcer)
                    .inflate(R.layout.activity_main_view_switcer_item, group, false)

                outView.findViewById<TextView>(R.id.TextView).text = item.text // "我在第 $baseIdx 页"
                outView.findViewById<ImageView>(R.id.ImageView).setImageResource(item.imgId)

                return outView
            }
        }
    }

ImageSwitcher

图片切换,继承自 ViewSwitcher, setFactory 必须返回 ImageView

val switcher = this.findViewById<ImageSwitcher>(R.id.ImageSwitcher)
switcher.setFactory { ImageView(this) } // 设置 setFactory 返回 ImageView

var imgIdx = 0
val imgIds: Array<Int> = this.loadImageIds() // 载入要显示的图片资源 
this.findViewById<Button>(R.id.nextPageButton)
    .setOnClickListener { switcher.setImageResource(this.loadImageId(imgIds, imgIdx++)) } // 通过 loadImageId 载入想要显示的图片

/** 设置需要显示的图片 */
private fun loadImageId(imgIds: Array<Int>, imgIdx: Int): Int {
        var idx = abs(imgIdx % imgIds.size)
        return imgIds[idx]
    }

// 设置图片切换时的动画效果
android:inAnimation="@android:anim/fade_in"
android:outAnimation="@android:anim/fade_out"
TextSwitcher

文件切换显示,与 ImageSwitcher 一样处理

ViewFlipper

相当于 tab ,加入一批 view, 在 ViewFlipper 中调用 showNext 或是 showPrevious 进行切换,或是 startFlipping 进行自动切换

  <ViewFlipper
        android:outAnimation="@android:anim/slide_out_right"
        android:inAnimation="@android:anim/slide_in_left">
        <ImageView />
        <TextView />
        <ImageView />
        <TextView />
        <ImageView />
    </ViewFlipper>
val flipper = this.findViewById<ViewFlipper>(R.id.ViewFlipper)
flipper.setOnClickListener { flipper.showNext() } // 进行切换 

EditText

输入法
 /* 在 AndroidManifest.xml 中设置, stateVisible 为显示,默认为隐藏
            * <activity android:windowSoftInputMode="stateVisible"/>  */

            // 使用输入法管理器
            val manager = this.getSystemService(Context.INPUT_METHOD_SERVICE)
            if(manager is InputMethodManager){

                if(manager.isActive(editText)){ // isActive 一直是 true, 有人用控件高度变化来判断

                    // 关闭输入法
                    manager.hideSoftInputFromWindow(editText.windowToken, 0)

                    val r = Runnable {

                        // 打开输入法 
                        editText.requestFocus()
                        manager.showSoftInput(editText, 0)
                    }

                    // 设置一个延时执行方法
                    Handler(Looper.getMainLooper()).postDelayed(r, 3000)
                }

            }

AutoCompleteTextView

自动提示, 还有 multiAutoCompleteTextView 可以进行多个项提示

<AutoCompleteTextView
    android:completionHint="请输入" // 输入提示
    android:completionThreshold="2" // 输入多少个字母后进行提示
/>

val textEdit = this.findViewById<AutoCompleteTextView>(R.id.Demo_AutoCompleteTextView)
val arrayAdapter =
ArrayAdapter(this, android.R.layout.simple_list_item_1, names)
textEdit.setAdapter(arrayAdapter)


        val items = arrayOf("数据 1", "数据 2", "数据 3", "数据 4", "数据 5")

        val arrayAdapter = ArrayAdapter(this, R.layout.activity_simple_item, R.id.TextView, items)

        val autoCompleteTextView = this.findViewById<AutoCompleteTextView>(R.id.AutoCompleteTextView)
        autoCompleteTextView.setAdapter(arrayAdapter)

        // 可以进行多项输入进提示,每一项字符使用 setTokenizer 进行设置
        val multiAutoCompleteTextView = this.findViewById<MultiAutoCompleteTextView>(R.id.MultiAutoCompleteTextView)
        multiAutoCompleteTextView.setAdapter(arrayAdapter)
        multiAutoCompleteTextView.setTokenizer(MultiAutoCompleteTextView.CommaTokenizer())

SearchView

搜索控件

NumberPicker

数值选择控件

ScrollView/HorizontalScrollView

为控件增加滚动功能

日期时间控件

<!-- 选择日历 -->
<CalendarView />

<!-- 选择日期 -->
<DatePicker />

<!-- 选择时间 -->
<TimePicker />
DatePicker

显示一个日期列表进行选择

// 直接显示在界面中的控件
val datePicker = this.findViewById<DatePicker>(R.id.Test_DatePicker)
datePicker.setOnClickListener { // 点击后进行触发 
    this.showLog("当前选择: ${datePicker.year} - ${datePicker.month} - ${datePicker.dayOfMonth}")
}
// 显示一个窗口进行日期选择

// 获取当前时间 /*val time = LocalDateTime.now() // 新版本中才有*/
        val calendar = Calendar.getInstance() // current time
        val year = calendar.get(Calendar.YEAR)
        val month = calendar.get(Calendar.MONTH)
        val day = calendar.get(Calendar.DAY_OF_MONTH)

        // 设置选择时间后的回调接口
        val setDater = object: DatePickerDialog.OnDateSetListener  {
            override fun onDateSet(picker: DatePicker?, year: Int, month: Int, day: Int) {
                this@MainActivity_TimeCtrl.showDate(year, month, day)
                this@MainActivity_TimeCtrl.showLog("当前选择: $year - $month - $day ")
            }
        }

        // 显示日期对话框, 设置回调接口,并指定日期中显示的时间
        val datePickerDialog = DatePickerDialog(this, setDater, year, month, day)
        datePickerDialog.show()

动态读取资源

val name = "face$idx" // 资源文件名, 不带后缀 
val deftype = "drawable" // 资源的类型
val resourceId = this.resources.getIdentifier(name, deftype, this.packageName);

菜单

工具栏菜单
// 在工具栏菜单中显示 

    // 1. 重载 onCreateOptionsMenu 设置工具栏菜单内容 
   override fun onCreateOptionsMenu(menu: Menu?): Boolean {

       // 1.1. 这里载入菜单,看公共代码部分
        this.LoadMenuXml(menu)
        return super.onCreateOptionsMenu(menu)
    }

    // 2. 选中菜单后重载 onOptionsItemSelected 处理
    override fun onOptionsItemSelected(item: MenuItem): Boolean {

        // 2.1 处理秆的菜单,这部分看公共公代码
        this.ShowSelectedMenu(item)
        return super.onOptionsItemSelected(item)
    }
上下文菜单
// 常按某个菜单后弹出菜单 

// 1. 调用函数 registerForContextMenu 设置该控件支持上下文菜单 
    val text = this.findViewById<View>(R.id.ContextMenuTextView)
    this.registerForContextMenu(text)

// 2. 重载 onCreateContextMenu 设置上下文菜单 
    override fun onCreateContextMenu(
        menu: ContextMenu?,
        view: View?,
        menuInfo: ContextMenu.ContextMenuInfo?
    ) {
        // 这里载入菜单,看公共代码部分
        this.LoadMenuXml(menu)
        super.onCreateContextMenu(menu, view, menuInfo)
    }

// 3. 重载 onContextItemSelected 处理选中菜单事件
    override fun onContextItemSelected(item: MenuItem): Boolean {

        // 处理选中菜单 ,这部分看公共公代码
        this.ShowSelectedMenu(item)
        return super.onContextItemSelected(item)
    }
弹出菜单
// 代码动态弹出菜单(比如在点击按钮之类的时候)

// 1. 设置某个动作,比如点击按钮时
val button = this.findViewById<View>(R.id.PopupMenuButton)
        button.setOnClickListener {

            // 2. 创建弹出菜单对象
            val popupMenu = PopupMenu(this, button)
            popupMenu.let {
                // 3. 弹出菜单载入菜单内容,这里看公共代码
                LoadMenuXml(popupMenu.menu)
                // 4. 设置点击菜单后如何动作
                popupMenu.setOnMenuItemClickListener { it ->
                  // 5. 处理动作,这里看公共代码 
                  this.ShowSelectedMenu(it)
                    true
                }
                // 6. 弹出菜单 
                popupMenu.show()
            }
        }
一些公共代码
// 载入菜单     
private fun LoadMenuXml(menu: Menu?) {

        // 1. 使用 MenuInflater 载入菜单资源
        MenuInflater(this).inflate(R.menu.menu_main, menu)

        // 2. 代码动态载入菜单 
        val fontMenu = menu?.addSubMenu("菜单") // 2.1 动态插入子菜单 
        // 2.2 这里设置子菜单 
        fontMenu?.let {
            fontMenu.setIcon(R.drawable.lowercase_2_30px)
            fontMenu.setHeaderIcon(R.drawable.lowercase_2_30px)
            fontMenu.setHeaderTitle("选择字体大小")
                // 动态添加子菜单的子菜单项, 分组id, 菜单id, 排序编号, 菜单名称
                fontMenu.add(0, 0, 0, "子菜单1")
                fontMenu.add(0, 1, 0, "子菜单2")
                fontMenu.add(1, 2, 0, "子菜单3")
                fontMenu.add(1, 3, 0, "子菜单4")
        }
    }

AppBar

AppBar 按钮

在工具栏上显示可变图标


    <!-- 工具栏上显示公用控件 -->
    <item
        android:id="@+id/action_bar_click"
        app:showAsAction="always"
        app:actionViewClass="android.widget.SearchView" <!-- 加入公共控件, 这里是一个搜索按钮,点击会自动执行一些动作 -->
        android:showAsAction="always"
        android:actionViewClass="android.widget.SearchView"
        android:title="classView"></item>

    <!-- 工具栏上显示自定义控件 -->
    <item
        android:id="@+id/action_bar_botton"
        app:actionLayout="@layout/activity_main_menu"
        app:showAsAction="always"
        android:actionLayout="@layout/activity_main_menu" <!-- 使用指定布局,没有深入研究,以后再研究 -->
        android:showAsAction="always"
        android:title="classView"></item>
AppBar Tab 页

现在不推荐使用,但也挺方便的

// 1. 启用 tab 页   
this.supportActionBar?.let{ it ->
            it.navigationMode = ActionBar.NAVIGATION_MODE_TABS // 设置启动 tab 页
            it.addTab(it.newTab().setText("第一页").setTabListener(this)) // 添加 tab 页,并设置处理函数
            it.addTab(it.newTab().setText("第二页").setTabListener(this)) // 添加 tab 页,并设置处理函数
        }

// 要实现 ActionBar.TabListener ,响应 tab 页选择等动作
    override fun onTabSelected(tab: ActionBar.Tab?, ft: FragmentTransaction?) {
        showMessage("${tab?.text} 被选择")
    }

    override fun onTabUnselected(tab: ActionBar.Tab?, ft: FragmentTransaction?) {
        showMessage("${tab?.text} 取消选择")
    }

    override fun onTabReselected(tab: ActionBar.Tab?, ft: FragmentTransaction?) {
        showMessage("${tab?.text} 再次选择")
    }
CardView

我把它当 wpf 的 board 来用

 <androidx.cardview.widget.CardView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:minWidth="240dp"
        app:cardCornerRadius="10dp"
        app:contentPaddingBottom="12dp"
        app:contentPaddingLeft="8dp"
        app:contentPaddingRight="8dp"
        app:contentPaddingTop="12dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:orientation="vertical">

            <androidx.cardview.widget.CardView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_margin="8dp"
                app:cardCornerRadius="5dp" <!-- 圆角 -->
                app:cardElevation="5dp" <!-- z 阴影 -->
            >

                <ImageView
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:src="@drawable/aratar_59"></ImageView>

            </androidx.cardview.widget.CardView>

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="12dp"
                android:layout_marginTop="8dp"
                android:text="我是一段测试"
                android:textSize="18sp" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="12dp"
                android:padding="5dp"
                android:text="这里是小的任务" />

        </LinearLayout>

    </androidx.cardview.widget.CardView>

自绘

// 从 view 继承 实现一些事件
// 使用

        val root = this.findViewById<ViewGroup>(R.id.Root)
        val drawView = XDrawView(this)
        drawView.minimumWidth = root.width;
        drawView.minimumHeight = root.height;
        root.addView(drawView) // 将自定义控件加入到视图中

// 按一下显示一个点
class XDrawView(context: Context?) : View(context) {

    private val lastPoint = PointF()

    override fun onTouchEvent(event: MotionEvent?): Boolean {

        if (event != null) {
            lastPoint.x = event.x
            lastPoint.y = event.y

            this.invalidate()

        }

        return super.onTouchEvent(event)

    }

    override fun onFinishInflate() {
        this.paint.color = Color.RED
        super.onFinishInflate()

    }

    private val paint = Paint()

    @SuppressLint("DrawAllocation")
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        canvas.drawCircle(this.lastPoint.x, this.lastPoint.y, 15F, this.paint)
    }
}

常用对话框

Kotlin入门(20)几种常见的对话框 - aqi00 - 博客园 (cnblogs.com)

Toast

显示一小段提示,看起来不推荐了,推荐使用 Snackbars 或是 通知系统

提示框
// 生成一些测试数据
            val items = arrayOf("RED", "GREEN", "YELLOW", "BLACK", "MAGENTA", "PINK")
            val checkeds = items.map { false }.toBooleanArray()

            val alertDialog = AlertDialog.Builder(this).run {
                setTitle("我是标题")
                setIcon(R.drawable.firealarm)
                setPositiveButton("确定") { _, id -> showNote("setPositiveButton: $id") }
                setNegativeButton("取消") { _, id -> showNote("setNegativeButton: $id") }
                setNeutralButton("查看更多") { _, id -> showNote("setNeutralButton: $id") }

                // 使用 view 设置标题
                setCustomTitle()

                // 设置文本内容
                setMessage("我是消息内容")

                // 显示列表进行选择
                setItems(array) { _, idx -> showNote("setItems: $idx") }

                // 设置当单选
                setSingleChoiceItems(array, 0) { _, idx -> showNote("setSingleChoiceItems: $idx") }

                // 进行多选, 每选择一次就回调一次
                setMultiChoiceItems(
                    items,
                    checkeds // 设置初始时,哪些是选择的
                ) { _, which, isChecked -> showNote("setMultiChoiceItems: $which -> $isChecked") }

                // 使用 Adapter 初始化,这里使用 ArrayAdapter 为例子
                setAdapter(
                    ArrayAdapter(
                        this@MainActivity_SearchView,
                        android.R.layout.simple_list_item_1,
                        items
                    )
                ) { _, idx -> showNote("setAdapter: $idx ") }

                // 使用一个布局填充内容,这里没有设置怎么取内容,以后再说
                val view = layoutInflater.inflate(R.layout.activity_login, null)
                setView(view)

                create()
            }

            alertDialog.show()
选择框

见 spinner

进度框

因为是模式窗口,不推荐使用. 现在使用 ProgressBar

            val dialog = ProgressDialog(this)
            dialog.setTitle("请稍候")
            dialog.setMessage("正在努力加载页面")
            dialog.max = 100
            dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL) // ProgressDialog.STYLE_SPINNER

            dialog.setOnShowListener {

                thread {

                    (0..100).forEach {

                        Thread.sleep(250)
                        runOnUiThread { dialog.progress = it /* 设置进度 */}
                    }

                    runOnUiThread { dialog.cancel() /* 完成后关闭容器 */ }
                }
            }

            dialog.show()

一个通用界面结构

                                                                                                                                

布局

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android" 
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools" >

    <!-- DrawerLayout 滑出菜单 -->
    <!-- 里面 2 个组件,第一个为主界面,第二个为滑出内容 -->
    <androidx.drawerlayout.widget.DrawerLayout >

        <!-- 主界面区, -->
        <!-- CoordinatorLayout 会监听子控制自动调整,包含 Snackbar -->
        <androidx.coordinatorlayout.widget.CoordinatorLayout >

            <!-- AppBarLayout 可以与 RecyclerView 与 toolbar 配合, 实现上滚隐藏, 下滚显示的效果 -->
            <!-- app:layout_scrollFlags="enterAlways|scroll|snap" 向上滚动隐藏,向下滚动显示, -->
            <!-- app:layout_behavior="@string/appbar_scrolling_view_behavior" 滚动时通知 appbarlayout -->
            <com.google.android.material.appbar.AppBarLayout 
                android:layout_height="wrap_content">

                <!-- CollapsingToolbarLayout 自动收缩子控件, 可以设置 2 个控件, 设置 layout_collapseMode 属性 -->
                <com.google.android.material.appbar.CollapsingToolbarLayout 
                    app:layout_scrollFlags="enterAlways|scroll|snap" >

                    <!-- 横幅, CollapsingToolbarLayout - parallax 自动隐藏 -->
                    <ImageView 
                        android:layout_height="180dp" 
                        app:layout_collapseMode="parallax" />

                    <!-- 工具栏 CollapsingToolbarLayout - pin 一直显示 -->
                    <!-- 1. background 背景色 -->
                    <!-- 2. theme 因为默认主题为亮色,所以文字是显色,在 attr/colorPrimary 中显示不出现。所以将标题栏设置为 Dark 主题 -->
                    <!-- 3. popupTheme 因为 2 的原因,工具栏使用黑色主题,所以弹出菜单也是黑色。会比较难看,这里再调整为亮色   -->
                    <androidx.appcompat.widget.Toolbar 
                        android:layout_height="wrap_content"
                        android:background="?attr/colorPrimary"
                        android:minHeight="?attr/actionBarSize"
                        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
                        app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
                        app:layout_collapseMode="pin" />

                </com.google.android.material.appbar.CollapsingToolbarLayout>

            </com.google.android.material.appbar.AppBarLayout>

            <!-- SwipeRefreshLayout 子组件带有自动刷新功能, appbar_scrolling_view_behavior 与 AppBarLayout 配合 -->
            <androidx.swiperefreshlayout.widget.SwipeRefreshLayout 
                app:layout_behavior="@string/appbar_scrolling_view_behavior">

                <!-- 显示列表例子 -->
                <androidx.recyclerview.widget.RecyclerView 
                    android:layout_width="match_parent" android:layout_height="match_parent" />

            </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

            <!-- 一个浮动按钮, 使用 layout_anchor 与 layout_anchorGravity 将位置定位至其它控件上 -->
            <com.google.android.material.floatingactionbutton.FloatingActionButton 
                android:layout_width="wrap_content" 
                android:layout_height="wrap_content" 
                android:layout_margin="16dp" 
                app:layout_anchor="@id/toolbar"                 
                app:layout_anchorGravity="bottom|center" 
                android:src="@drawable/ali_months" />

        </androidx.coordinatorlayout.widget.CoordinatorLayout>

        <!-- 引用滑出区 -->
        <!-- 必须加入 layout_gravity 指定滑动方向, 不然会报错 -->
        <!-- headerLayout 指定滑出区的头部 -->
        <!-- menu 指定滑出区的菜单 -->
        <com.google.android.material.navigation.NavigationView 
            android:background="#FFF"
            app:headerLayout="@layout/mainactivity_x_nav_header"
            app:menu="@menu/mainactivity_x_nav_menu"        
            android:layout_gravity="start">

            <!-- 在滑出区中设置一个文本 -->
            <TextView            
            android:layout_gravity="bottom|right"
            android:layout_marginRight="12dp"
            android:layout_marginBottom="12dp"
            android:text="关于: xxxx"></TextView>

        </com.google.android.material.navigation.NavigationView>

    </androidx.drawerlayout.widget.DrawerLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

代码

工具栏及滑出区
  /** 初始化 toolbar */ 
    private fun initToolbar() {

        val toolbar = this.findViewById<Toolbar>(R.id.toolbar)
        this.setSupportActionBar(toolbar)

        supportActionBar?.let {
            it.setDisplayHomeAsUpEnabled(true) // 显示右上角图标, 默认为一个回退键 
            it.setHomeAsUpIndicator(R.drawable.menu_24px)
        }

        /** 滑动区滑动收起设置,
         * 如果不用 ActionBarDrawerToggle 需要
         * 1. 监听 DrawerLayout 的展开、收起事件,然后设置图标,而且它还带动画
         * 2. 点击 android.R.id.home 时做 DrawerLayout 的展开、收起动作 */
        val navigationDrawer = this.findViewById<DrawerLayout>(R.id.drawer_layout)
        val drawerToggle = ActionBarDrawerToggle(
            this,
            navigationDrawer,
            toolbar,
            R.string.drawer_open, // <-- ""
            R.string.drawer_close // <-- ""
        )
        navigationDrawer.addDrawerListener(drawerToggle)
        navigationDrawer.isClickable = true
        drawerToggle.syncState()
    }
滑出区中的通用菜单
    private fun initNavigationView() {

        val view = this.findViewById<NavigationView>(R.id.navigationView)
        view.setCheckedItem(R.id.nav_menu_call) // 设置第一个为被选择项
        view.itemIconTintList = null // 菜单图标设置为彩色
        view.setNavigationItemSelectedListener {
            val layout = this.findViewById<DrawerLayout>(R.id.drawer_layout)
            layout.close() // 选择选择后关闭滑出菜单
            true
        } 
    }
下拉刷新
   private fun initSwipeRefreshLayout() {

        val view = this.findViewById<SwipeRefreshLayout>(R.id.swipeRefreshLayout)

        view.setOnRefreshListener {
            thread {
                Thread.sleep(3000)
                runOnUiThread {
                    view.isRefreshing = false // 关闭刷新图标 
                }
            }
        }
    }

jetpack

LifecycleObserver

接收 View, Fragment, Application 的事件


// 现在好像是建议使用 DefaultLifecycleObserver, 然后 override 需要的函数
class MainLifecycleObserver() : DefaultLifecycleObserver {

    override fun onStart(owner: LifecycleOwner) {
        super.onStart(owner)
    }

    override fun onStop(owner: LifecycleOwner) {
        super.onStop(owner)
    }
}

// 监听, 在  View, Fragment, Application 中
this.lifecycle.addObserver(MainLifecycleObserver())

ViewModel

http://yifeiyuan.me/blog/52b29a03.html

ViewModel 对象,里面一般会有 LiveData 值, LiveData 对象中的值变更时发出通知 , 还没有试过,在不同的类中创建是否是同一个对象

它能感知生命周期

class MainViewModel(initVal: Int) : ViewModel() {

    val ageValue = MutableLiveData(initVal)
}

// 在 view 中调用, 多次调用会返回对应于 View 的唯一实例 
val viewModel = ViewModelProvider(this)[MainViewModel::class.java]

// 如果 ViewModel 带有参数,则需要使用 factory 创建
class MainViewModelFactory(private val initVal: Int) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return MainViewModel(initVal) as T
    }
}

// 使用工厂模式创建, 使用 ViewModelProvider 时传入参数
val viewModel = ViewModelProvider(this, MainViewModelFactory(100))[MainViewModel::class.java]

LiveData

http://yifeiyuan.me/blog/9d326805.html

值变更时发出通知, 它与LifecycleOwner (view之类) 对象进行绑定,在 view 在生命周期中才进行通知

简单数值
class MainViewModel(initVal: Int) : ViewModel() {

    private val ageValue = MutableLiveData(initVal) // 初始化值内容

    val age: LiveData<Int>
        get() = ageValue // 如果不想直接对外暴露可读写的 ageValue,这里使用只读 LiveData 进行包装   

    fun plusOne() { // 对 livedata 数据进行变更,会调用 observe 进行通知
        this.ageValue.value = (this.ageValue.value ?: 0) + 1
    }    
}

// 监控变量的变更
viewModel.age.observe(this, Observer {
    val textView = this.findViewById<TextView>(R.id.InfoTextView)
    textView.text = "$it"
})
Transformations.map 暴露对象中的某个值

data class UserData(var age: Int) {}

// 使用 ViewModelProvider 获取唯一实例
class MainViewModel(initVal: Int) : ViewModel(), DefaultLifecycleObserver {

    // 初始化变量
    private val userData = MutableLiveData(UserData(initVal))

    // 包装对象中的值
    val age = Transformations.map(this.userData) { data -> "${data.age} value" }

    fun plusOne() { // 变更值内容,必须直接变量 value 内容,如果变更属性则不会通知 
        this.userData.value = UserData((this.userData.value?.age ?: 0) + 1)
    }
}

// 监控变量的变更
viewModel.age.observe(this, Observer {
    val textView = this.findViewById<TextView>(R.id.InfoTextView)
    textView.text = "$it"
})
Transformations.switchMap 更新 LiveData

在某些场景中ViewModel 需要更新 LiveData 对象,会导致监听的 LiveData 对象变量,监听失败

使用 switchMap 的底层更新 LiveData 中的值(可能,没看过源代码)

class MainViewModel(initVal: Int) : ViewModel() {

    // 用来触发 switchMap 更新的参数值,
    // 如果没有参数值,则使用 private val refreshKeyData = MutableLiveData<Any?>(), 触发 this.refreshKeyData.value = this.refreshKeyData.value    
    private val userAge = MutableLiveData(initVal)

    // switchMap 监听某个 LiveData 作为调用参数进行同步更新 
    val userData: LiveData<UserData> =
        Transformations.switchMap(userAge) { it -> createUserData(it) }

    /** 模拟对象生成函数, 也许是一个生成器调用数据库或是 webapi */
    private fun createUserData(it: Int?): LiveData<UserData> {
        val data = UserData(it ?: 0)
        return MutableLiveData(data)
    }

    fun plusOne() { // 更新原始值
        this.userAge.value = (this.userAge.value ?: 0) + 1
    }
}

// 监控变量的变更, 这里监听的是 LIveData,所以如果直接变更 userData 会导致监听的是旧对象引用 
viewModel.userData.observe(this, Observer {
            val textView = this.findViewById<TextView>(R.id.InfoTextView)
            textView.text = "${it.age}"
})

Fragment

自定义控件继承至 view , Fragment 更轻量级。一些动作交给父级 View 来处理

Fragment 也有一些子类,比如 DialogFragment , ListFragment , PreferenceFragment , WebViewFragment 可以直接使用

创建 Fragment


1. 创建资源 xml 文件,与普通 activity 文件一样
2. 新建类
class LeftFragment : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_left, container, false)    // 创建视图
    }
}

直接引用

```xml

 <LinearLayout
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="horizontal">

     <fragment
         android:layout_width="140dp"
         android:layout_height="match_parent"
         android:name="xsoft.demo.xapp.LeftFragment" <!-- 直接在 xml 资源文件中引用,并且要指明类路径  --> 
         android:id="@+id/left_fragment"/>


 </LinearLayout>

```

动态调用
   <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">

        <!-- 资源文件中定义一个占位符 -->
      <FrameLayout
          android:id="@+id/framelayout_right"
          android:layout_width="wrap_content"
          android:layout_height="match_parent"
          android:layout_weight="1" />

    </LinearLayout>


    // 动态载入  fragment
    private fun loadFragment() {

        // 返回 fragment 管理器  
        val manager = this.fragmentManager
        manager?.let {

            val arguments = Bundle() // 准备一些参数传入
            arguments.putInt('test', 123) 
            val fragment = RightFragment()
            fragment.arguments = arguments // 传递一些参数 
            val transaction = manager.beginTransaction()
            transaction.replace(R.id.framelayout_right, fragment)
            transaction.addToBackStack(null) // 将当前 fragment 加入到栈中,可以进行后退操作
            transaction.commit()
        }
    }

    // 在 onCreateView 在查找按钮之类的操作,因为在 onCreate 中视图应该还没有创建
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        // 创建视图
        val view = inflater.inflate(R.layout.fragment_left, container, false)

        // 返回 view 并查找控件, 也可以使用
        val button = view.findViewById<Button>(R.id.Load_Fragment_Button)
        button?.setOnClickListener {
            this.loadFragment()
        }

        // 返回当前的 activity ,然后读取其它 fragment 并进行一些操作
        val fragment =
            activity?.supportFragmentManager?.findFragmentById(R.id.left_fragment)

        return view
    }

适配不同屏幕

设置不同的 activity.xml, 比如放在 layout 目录中与放在 large_layout 中的 activity.xml 组织不同布局的 Fragment 。 不同的分辨率设置见手册

Android 屏幕适配:最全面的解决方案 - 简书 (jianshu.com)

比如做一个新闻阅读,一个列表,一个显示内容
在手机端显示列表,点击一个条目后显示一个 activity, 如果在平板端,左右显示列表与内容
1. 一个 Fragment1 显示列表,一个 Fragment2 显示内容
2. 设置入口 activity (生成2个),一个在 layout 中 (嵌入列表 Fragment1), 一个在 layout-sw600dp 中 (嵌入列表 Fragment1 与内容 Fragment2)
3. 通过查找入口 activity 中带不带内容 Fragment2 看是哪个 activity
4. 点击列表中的某一项时如果是手机端,跳转到一个包括内容 Fragment2 的 activity 。 如果是平板中,则直接显示在当前的内容 Fragemnt2 中

一些可用的小知识

this.activity as? MainActivity // Fragment 中找到当前绑定的 activity
this.view // 返回当前 root view
val activity = viewGroup.context as? AppCompatActivity (或是 activity, 根据实际情况) // RecyclerView.ViewHolder 中返回 activity

val manager  = activity?.supportFragmentManager // 在 activity 中返回 fragment 管理器,并进一步进行管理 
val  fragment = manager?.findFragmentById(R.id.NewsContentFragment)

// 如果要访问控件请在 onViewCreated 中进行,因为 onCreate 中视图还没有创建, 如果要在 onCreateView 在访问,请 inflater.inflate 出 View 后,使用访问 View 处理

Intent

启动 Activity , Service , BroadcastReceiver 的参数

显式指定启动对象

// 显式指定
val intent = Intent()

// 指定启动对象包及类
intent.component = ComponentName(activity, MainActivity_BBS::class.java)
intent.setClass(activity, MainActivity::class::class.java) // 内部应该调用 setComponent 
intent.setClassName(activity, "${MainActivity_BBS::class.java}") // 内部应该是new 了 setComponent 

this.startActivity(intent)

使用 action 隐式指定启动对象


// AndroidManifest.xml 中为 Activity , Service , BroadcastReceiver 指定 intent-filter, 
// intent-filter 中指定一个或是多个 action 及 category
// 启动定义 Intent 的 action 及 category , 系统会自动查找匹配的 action/category 进行启用
// 如果有多个 Activity(Service\BroadcastReceiver) 符合条件,系统弹出对话框提示用户选择哪一个进行启动


// 隐式指定
val intent = Intent()

// 指定启动对象定义的 action, 只能指定一个
intent.action = "android.intent.action.MAIN"
// 指定启动对定义的 category, 可多次调用 addCategory 指定多个对象,如果不指定,则为 android.intent.category.DEFAULT
intent.addCategory("android.intent.category.DEFAULT")

this.startActivity(intent)

隐匿调用系统动作
// 返回首页
val intent = Intent()

intent.action = Intent.ACTION_MAIN
intent.addCategory(Intent.CATEGORY_HOME)

this.startActivity(intent)

Data 与 Type

// Intent 通过 Data 传入启动参数
// Data 格式为 Uri
// 但是有一个 Data 不足以定类型,比较指定一个文件路径,可能是图片也可以是音乐,那么可以指定 type
// type 为 MIME 格式
// data 与 type 单独指定会相互覆盖,请调用  setDataAndType 同时指定 
intent.data = Uri.parse("content://com.anroid.contacts/contacts/1")
intent.type = "png/image"
intent.setDataAndType(Uri.parse("content://com.anroid.contacts/contacts/1"), "png/image")

// AndroidManifest.xml 中设置 intent-filter 时可以指定 data 节点,其中指定mimeType, scheme, host, port path, pathPrefix, pathPattern 等参数来匹配传入的 data 参数 

Flag

启动时可以调用 setFlag 指定一个控制标志,比如将启动时不使用启动动画,置前操作之类的

广播机制

两种发送机制,一种是异步发送,所有接收器同时收到。一种是排序式,一个处理完后处理下一个。可以中断广播。

动态注册接收器

   lateinit var timeBroadcastReceiver: TimeBroadcastReceiver

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        // 3. 定义接收事件类型,这是一分钟一次的时间事件 
        val intentFilter = IntentFilter()
        intentFilter.addAction("android.intent.action.TIME_TICK")

        // 4. 进行动态注册接收器
        timeBroadcastReceiver = TimeBroadcastReceiver()
        registerReceiver(this.timeBroadcastReceiver, intentFilter)
    }

    override fun onDestroy(){
        // 5. 动态注销接收器
        unregisterReceiver(this.timeBroadcastReceiver)
    }

    // 1. 继承一个 BroadcastReceiver
    inner class TimeBroadcastReceiver : BroadcastReceiver(){

        // 2. 处理接收事件
        override fun onReceive(context: Context?, intent: Intent?) { ... }
    }

静态注册接收器

静态注册可以在程序未启动时也能接收到事件。不过大多数没有指明接收者的事件已经不允许静态注册接收器了。但是还保留了一些,比如: android.intent.action.BOOT_COMPLETED

<!-- 1. 继承一个 BroadcastReceiver -->
class BootCompletedReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) { ... }
}

<!-- 2. 编辑 AndroidManifest.xml  -->
        <receiver
            android:name=".BootCompletedReceiver"
            android:enabled="true" <!-- 3. 是否启用 --> 
            android:exported="true"> <!-- 4. 是否允许接收程序外的广播 -->
            <intent-filter>
                <!-- 5. 定义接收事件  -->
                <action android:name="android.intent.action.BOOT_COMPLETED" />
            </intent-filter>
        </receiver>

<!-- 6. 不过现在都是权限申请问题。以后再说 -->

发送广播

<!-- 1. 定义接收器 -->
<receiver android:name=".DemoBroadcastReceiver" android:enabled="true" android:exported="true">
    <intent-filter android:priority="100"> <!-- 如果是顺序广播,这里定义权重 -->
        <action android:name="xsoft.demo.xapp.DEMO_MESSAGE" />
    </intent-filter>
</receiver>

<!-- 2. 继承一个 BroadcastReceiver -->
class DemoBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {

        <!-- 3. 处理接收事件, 如果是顺序广播,可以停止继续广播 -->
        abortBroadcast()
    }
}

<!-- 4. 发送广播 -->
val intent = Intent("xsoft.demo.xapp.DEMO_MESSAGE")
intent.setPackage(packageName) <!-- 5. 指定消息的接收者所在包,目前不指定无法接收到消息 -->
sendBroadcast(intent) <!-- 6.1 发送广播 -->
sendOrderedBroadcast(intent, null) <!-- 6.2 发送顺序广播 -->

存储机制

文本文件读写

    private fun loadFile(fileName: String) {

        val input = openFileInput(fileName)
        val streamReader = InputStreamReader(input)
        val bufferedReader = BufferedReader(streamReader)
        bufferedReader.use {
            bufferedReader.forEachLine { it -> Log.d("Debug", it) }
        }
    }

    private fun saveFile(fileName: String) {

        val output = openFileOutput(fileName, Context.MODE_APPEND /* 追加模式 */ ) // Context.MODE_PRIVATE /* 覆盖模式 */
        val streamWriter = OutputStreamWriter(output)
        val bufferedWriter = BufferedWriter(streamWriter)
        bufferedWriter.use{
            it.write("测试内容")
        }
    }

键值读写

// 以 xml 格式进行文件存储 
// 作为应用设备不错
    private fun saveFile(fileName: String) {

        // 或是 getPreferences,以类名为文件名
        val preferences =
            this.getSharedPreferences(fileName, Context.MODE_PRIVATE /* 目前只有这一种模式可用 */)
        val edit = preferences.edit()
        edit.putString("key1", "我是测试")
        edit.putBoolean("key2", true)
        edit.clear() // 清空所有数据
        edit.apply() // 进行保存
    }

    private fun loadFile(fileName: String) {

        val preferences =
            this.getSharedPreferences(fileName, Context.MODE_PRIVATE /* 目前只有这一种模式可用 */)
        val value1 = preferences.getString("key1", "1111")
        val value2 = preferences.getBoolean("key2", false)

    }

SQLite



/** 数据数读写, 要继承 SQLiteOpenHelper, 实现 onCreate 与 onUpgrade */
class SQLiteDemo(context: Context, name: String, version: Int) :
    SQLiteOpenHelper(context, name, null, version) {

    companion object {

        /** 进行数据库演示调用  */
        fun test(context: Context, fileName: String) {

            val sqLiteDemo = SQLiteDemo(context, fileName, 1)
            sqLiteDemo.test()
        }
    }

    /* 数据库演示 */
    private fun test() {

        val tableName = "Books"

        val dbWriter = this.writableDatabase

        /** 插入数据 */
        /****************************************************************************************/
        val values = ContentValues().apply {
            put("author", "x")
            put("price", 12.5)
        }
        var rowId =
            dbWriter.insertWithOnConflict(tableName, null, values, SQLiteDatabase.CONFLICT_NONE)
        Log.d("db", "插入数据,编号: $rowId")

        /** 更新数据 */
        /****************************************************************************************/
        values.clear()
        values.put("price", 20.1)
        var count = dbWriter.update(tableName, values, "id = ?", arrayOf("$rowId"))
        Log.d("db", "更新 $count 条数据")

        /** 删除数据 */
        /****************************************************************************************/
        count = dbWriter.delete(tableName, "id = ?", arrayOf("$rowId"))
        Log.d("db", "删除 $count 条数据")

        /** 查询数据 */
        /****************************************************************************************/
        val dbReader = this.readableDatabase
        /** 传入表名,列名,条件,条件参数,是否分组,排序等参数  */
        var rows = dbReader.query(tableName, null, "id >= ?", arrayOf("0"), null, null, null, null)
        if (rows.moveToFirst()) {
            do {
                val author = rows.getString(rows.getColumnIndex("author"))
                val price = rows.getDouble(rows.getColumnIndex("price"))
                Log.d("db", "查询 $author $price ")
            } while (rows.moveToNext())
        }

        /** 直接运行 sql */
        /****************************************************************************************/
        if(true)
        {
            /** DatabaseUtils, 数据库辅助函数 https://www.apiref.com/android-zh/android/database/DatabaseUtils.html  */
            val count = DatabaseUtils.longForQuery(dbWriter, "select count(*) from books; ", null)
            Log.d("db", "插入数据 $count 条 ")
        }


        /** 直接运行 sql */
        /****************************************************************************************/
        // dbWriter.execSQL()
        dbReader.rawQuery("select * from Books;", null).use {
            if (it.moveToFirst()) {
                Log.d("db", "共返回 ${it.columnCount} 列 ")
            }

            for (idx in 0 until it.columnCount){
                Log.d("db", "第 $idx 列名 ${ it.getColumnName(idx) }")
            }
        }

        /** 事务 */
        /****************************************************************************************/
        dbWriter.beginTransaction() // 开始事务
        try {

            dbWriter.setTransactionSuccessful() // 设置操作成功
        }
        finally{
            dbWriter.endTransaction() // 结束事务
        }


    }

    /** 创建语句 */
    private val createSql: String =
        """
                CREATE TABLE "Books" (
                    id     INTEGER PRIMARY KEY AUTOINCREMENT,
                    author TEXT,
                    price  REAL
                );

            """.trimIndent()

    /** 进行数据为创建, 自动调用  */
    override fun onCreate(db: SQLiteDatabase?) {
        this.onUpgrade(db, 0, 100000) // 调用链式升级函数, 实际可以直接生成最后的数据表,
    }

    /** 进行数据为升级, 自动调用 */
    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
        // 我想到的升级链调用
        this.upgradeToVer3(db, oldVersion) // 假设最新版本是 3
    }

    private fun upgradeToVer3(db: SQLiteDatabase?, oldVersion: Int) {
        if(oldVersion >= 3 ){ return } // 不需要升级
        upgradeToVer2(db, oldVersion) // 先进行升 2 级操作
        TODO("进行 2 升级至 3 动作")
    }

    private fun upgradeToVer2(db: SQLiteDatabase?,oldVersion: Int) {
        if(oldVersion >= 2 ){ return } // 不需要升级
        upgradeToVer1(db, oldVersion)  // 先进行升 2 级操作
        TODO("进行 1 升级至 2 动作")
    }

    private fun upgradeToVer1(db: SQLiteDatabase?,oldVersion: Int) {
        db?.execSQL(createSql) // 创建数据库
    }

}

目录信息

因为空间有限,程序安装在内部的存储器中,数据一般推荐放在 SD 卡中 (目前手机都已经固话至手机中)。

Environment.get***Directory() 获取内部存储器相关目录

Environment.getExternalStoragePublicDirectory(***) 获取 SD 卡中的共有目录,一般因为权限问题,很多目录不能直接读写了。

Context.getExternalFilesDir(***) 获取 SD 卡中应用专属目录,没有权限问题

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMSt" />

val rootDirectory = Environment.getRootDirectory()
this.showLog("系统根目录 rootDirectory: $rootDirectory")

val dataDirectory = Environment.getDataDirectory()
this.showLog("系统数据目录 dataDirectory: $dataDirectory")

val downloadCacheDirectory = Environment.getDownloadCacheDirectory()
this.showLog("下载缓存目录 downloadCacheDirectory: $downloadCacheDirectory")

val externalStorageDirectory = Environment.getExternalStorageDirectory()
this.showLog("外部 SD 卡 externalStorageDirectory: $externalStorageDirectory")

val exStorageState = Environment.getExternalStorageState()
this.showLog("外部 SD 卡状态 exStorageState: $exStorageState")

// 公共数据目录, 现在一般不给权限  
val dcim = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)
this.showLog("DIRECTORY_DCIM $dcim")

// 公共数据目录, 现在一般不给权限  
val downloads = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
this.showLog("DIRECTORY_DOWNLOADS: $downloads")

// 应用专属目录 
val mydcim = this.getExternalFilesDir(Environment.DIRECTORY_DCIM)
this.showLog("mydcim: $mydcim")

// 应用专属目录 
val myPictures = this.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
this.showLog("myPictures: $myPictures")

// 应用专属目录 
val mydownloads = this.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
this.showLog("mydownloads: $mydownloads")

系统根目录 rootDirectory: /system
系统数据目录 dataDirectory: /data
下载缓存目录 downloadCacheDirectory: /data/cache
外部 SD 卡 externalStorageDirectory: /storage/emulated/0
外部 SD 卡状态 exStorageState: mounted
DIRECTORY_DCIM /storage/emulated/0/DCIM
DIRECTORY_DOWNLOADS: /storage/emulated/0/Download
mydcim: /storage/emulated/0/Android/data/xsoft.demo.myapplication/files/DCIM
myPictures: /storage/emulated/0/Android/data/xsoft.demo.myapplication/files/Pictures
mydownloads: /storage/emulated/0/Android/data/xsoft.demo.myapplication/files/Download

跨程序共享数据

获取一个程序与外部程序传递数据的通道

ContentProvider 由程序实现,借外部访问程序提供的数据。实现后提供一些增、查、改、删的功能,并要在 AndroidManifest.xml 中注册。见后面的例子。一般用的少,因为程序一般不提供外部访问

ContentResolver 程序使用该接口访问其它程序提供的数据(比如 ContentProvider )。一般读写系统数据(电话本)等比较多一些

ContentObserver 监听 ContentResolver 查询到的数据变量 , 提供方调用 notifyChange()

动态权限申请

1. 在 AndroidManifest.xml 中加入需要的权限 
<uses-permission android:name="android.permission.CALL_PHONE" />

2. 调用函数中申请权限
private fun autoCall(phone: String) {

    2.1  将调用设置为回调函数,如果有权限,直接调用,如果没有权限放到一个字典中在权限申请通过后进行调用 
    val callback: () -> Unit = { this.callPhone(phone) }

    2.2 调用我写的通用函数,进行权限查询并进行调用 
    QueryAndInvoke(callback, Manifest.permission.CALL_PHONE)
}

/** 线程安全的计数器, 为了生成一个唯一 id,用为回调函数标记  */
private val nextActiveCounter: AtomicInteger = AtomicInteger()

/** 缓存需要处理的回调函数, 使用唯一编号进行标记 */
private val nextActiveMap = mutableMapOf<Int, () -> Unit>()

3. 我写的通用处理函数,查询一个权限,如果有权限直接调用,如果没有权限请求权限后再次调用 
private fun QueryAndInvoke(callback: () -> Unit, premission: String) {

    3.1 检测权限        
    if (ContextCompat.checkSelfPermission(this, premission) != PackageManager.PERMISSION_GRANTED) {
        3.2 没有权限,将调用缓存起来
        val idx = nextActiveCounter.getAndIncrement() // 这里用了一个线程安全的计数器生成调用序号 private val nextActiveCounter : AtomicInteger = AtomicInteger()
        nextActiveMap[idx] = callback // 这里将回调的调用序号存储进字典
        3.3 申请权限, 传入要求的权限及调用序号
        ActivityCompat.requestPermissions(this, arrayOf(premission), idx)
    } else {
        3.4 有权限,直接调用
        callback.invoke()
    }
}

4. 根据授权情况进行下一步操作
override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<out String>, grantResults: IntArray ) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)

    4.1 取出待处理的回调
    val callback = nextActiveMap.remove(requestCode)

    4.2 如果授权成功,进行回调
    if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            callback?.invoke()
    }
}

5. 真实调用
private fun callPhone(phone: String) {

    // todo: 捕获到异常并进行提示
    check(!phone.isNullOrBlank()) { "电话号码未设置" }

    val intent = Intent(Intent.ACTION_CALL)
    intent.data = Uri.parse("tel:${phone.trim()}")
    startActivity(intent)
}
一个回调辅助类

自动生成一个唯一 id 对应于一个动作

/** 保存一些常用操作 */
class ActiveStore {

    /** 线程安全的计数器 */
    private val nextActiveCounter: AtomicInteger = AtomicInteger()

    /** 保存动作 */
    private val nextActiveMap = mutableMapOf<Int, (Intent?) -> Unit>()

    /** 保存一个动作 */
    fun putActive(active: (Intent?) -> Unit): Int {

        val idx = this.nextActiveCounter.getAndIncrement()
        this.nextActiveMap[idx] = active

        return idx
    }

    /** 执行一个动作 */
    private fun runActive(idx: Int, data: Intent?) {
        val active = this.remove(idx) ?: throw Resources.NotFoundException("找不到编号为 $idx 的动作")
        active(data)
    }

    /** 移除一个动作 */
    private fun remove(idx: Int): ((Intent?) -> Unit)? {
        return this.nextActiveMap.remove(idx)
    }

    /** 使用 onActivityResult 参数执行 */
    fun runWithActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        if (resultCode == Activity.RESULT_OK) {
            this.runActive(requestCode, data)
        } else {
            this.remove(requestCode)
        }
    }

}
一个权限的回调辅助类

第三方库 https://github.com/guolindev/PermissionX

/** 我写的一个简易保存权限申请动作 */
class PermissionsActionStore {

    // 定义动作 id
    private val nextActiveCounter: AtomicInteger = AtomicInteger()

    // 存储 id 与动作绑定
    private val nextActiveMap =
        mutableMapOf<Int, (permissions: Array<out String>, grantResults: IntArray) -> Unit>()

    /** 定义授权动作, 指定申请的一个权限  */
    fun putRequestPermissionsAction(
        activity: Activity,
        premission: String,
        grantedAction: () -> Unit, // 通过时执行动作
        deniedAction: () -> Unit = {}, // 禁用时执行动作
        rationaleAction: () -> Unit = {} // 如果没有权限时执行动作
    ): Int
    {
        return this.putRequestPermissionsAction(activity, arrayOf(premission), grantedAction, deniedAction, rationaleAction)
    }

    /** 定义授权动作,指定申请的多个权限  */
    fun putRequestPermissionsAction(
        activity: Activity,
        premissions: Array<String>,
        grantedAction: () -> Unit, // 通过时执行动作
        deniedAction: () -> Unit = {}, // 禁用时执行动作
        rationaleAction: () -> Unit = {} // 如果没有权限时执行动作
    ): Int {

        // 指定的权限是否都有
        val allSuccess = premissions.all { it -> ContextCompat.checkSelfPermission(activity, it) == PackageManager.PERMISSION_GRANTED }

        if (allSuccess) {
            // 已经有指定权限,直接调用
            grantedAction?.invoke()
            return -1
        }

        rationaleAction?.invoke()

        // 定义一个有权限反馈时怎么执行动作
        val action: (Array<out String>, IntArray) -> Unit = { permissions: Array<out String>, grantResults: IntArray ->

            if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                grantedAction?.invoke() // 通过时执行
            }
            else{
                deniedAction?.invoke() // 禁用时执行
            }
        }

        // 存储要执行的动作
        val id = this.nextActiveCounter.getAndIncrement()
        this.nextActiveMap[id] = action

        // 请求权限
        ActivityCompat.requestPermissions( activity, premissions, id)

        return id
    }

    /** 执行定义的授权动作, override fun onRequestPermissionsResult 中调用 */
    fun runRequestPermissionsAction(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {

        val action  = this.nextActiveMap.remove(requestCode)
        action?.invoke(permissions, grantResults)
    }
}

查询通讯录

0. AndroidManifest.xml 中加入相应的权限

1. 定义调用函数
val callback: () -> Unit = {

    this.sendLog("开始查询电话")

    3. 进行查询, 与数据库操作很象    
    // 如果插入为 this.contentResolver.insert
    // 如果删除为 this.contentResolver.delete
    // 如果更新为 this.contentResolver.update
    this.contentResolver.query( 
                ContactsContract.CommonDataKinds.Phone.CONTENT_URI, // 查询哪个表. 格式为 content://com.android.contacts/data/phones
                null, // 查询哪个列
                null,  // where 语句
                null, // where 参数
                null // 排序列
            )?.let {

                while (it.moveToNext()) {

                    4. 查询电话号码 
                    val number =
                        it.getString(it.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
        } } }

2. 我写的授权并调用函数, 见动态权限申请部分
this.QueryAndInvoke(callback, Manifest.permission.READ_CONTACTS)

监听数据更新 ContentObserver

调用后监听 ContentResolver 查询到的数据变量 , 提供方调用 notifyChange()

1. 实现一个 ContentObserver 接收监听内容   

class XContentObserver(handler: Handler?) : ContentObserver(handler) {
        // 监听的数据发生了变化
        override fun onChange(selfChange: Boolean) {
            super.onChange(selfChange)
        }
    }

2. 注册监听对象
val uri = Telephony.Sms.CONTENT_URI // 要监听的内容,比如接收到短信
val observer = XContentObserver(Handler())
this.contentResolver.registerContentObserver(uri, true, observer)

3. 释放监听对象
this.contentResolver.unregisterContentObserver(observer)

自定义 ContentProvider

说明
 * 自定义一个  ContentProvider
 * 先定义一个路径:com.x.xsoft.ltd.app.provider, 使用  定义一个
 * 完整的 uri 表名 content://path/table, 数据列 content://path/table/32
 * 定义一个 ContentProvider 子类
 * 在 AndroidManifest.xml 中注册 provider 绑定 ContentProvider 类与 路径
 * 使用 UriMatcher 辅助对处理的 url 进行判断
 * 完成 ContentProvider 子类
辅助函数
   companion object {
        // 1. 定义路径
        const val authoriy: String = "com.x.xsoft.ltd.app.provider"

        // 2. 构建查询的链接的辅助函数,可以生成完成的链接
        public fun buildUri(table: String, id: String? = null): Uri {
            check(table.isNotBlank()) { "没有指定 table" }

            return if (id.isNullOrBlank()) {
                Uri.parse("content://$authoriy/$table")
            } else {
                Uri.parse("content://$authoriy/$table/$id")
            }
        }
    }

    /** 本例中一项就是一个 book 对象 */
    inner class Book(val id: Int, var name: String, var price: Double)

    // 3. 定义 uriMatcher 用来辅助对链接进行判断 
    private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH) // 使用 UriMatcher 辅助解析查询的内容
    private val xContentProviderDir = 0 // 匹配到目录(或表)
    private val xContentProviderAllStrItem = 1 // 匹配到所有目录(或表)
    private val xContentProviderAllNumItem = 2 // 匹配到某一项
    private val books = mutableListOf<Book>() // 书本列表

    init {
        // 定义查询的 uriMatcher, 传入 uri 时可以判断是目录还是条目
        uriMatcher.addURI(authoriy, "book", xContentProviderDir) // 这里定义一个目录(或表)
        uriMatcher.addURI(authoriy, "book/*", xContentProviderAllStrItem) // 匹配所有目录下任意字符的子项
        uriMatcher.addURI(authoriy, "book/#", xContentProviderAllNumItem) // 匹配所有目录下任意数值的子项
        // 注意,注册 book/* 与 book/# 有冲突,* 会优先匹配
    }

    /** 辅助函数,从 ContentValues 中解析出 book */
    private fun parseBook(values: ContentValues?): Book {
        val name = values?.getAsString("name")
        val price = values?.getAsDouble("price")
        return Book(counter.getAndIncrement(), name ?: "", price ?: 32.0)
    }

插入
    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        val book: Book = this.parseBook(values) // 使用辅助函数将 ContentValues 转为 Book 对象

        return when (uriMatcher.match(uri)) { // 判断连接的形式,如果是约定的格式,
            xContentProviderDir -> { // 如果是指定目录的,则进行添加
                books.add(book) // 将对象加入到队列
                buildUri("book", "${book.id}") // 使用辅助函数返回 uri
            }
            else -> null
        }
    }
更新
    override fun update( uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>? ): Int {
        return when (uriMatcher.match(uri)) { // 判断链接的类型 
            xContentProviderAllNumItem -> {
                val id = uri.pathSegments[1] // uri 传入格式为 content://xxx/book/id, 所以 uri.pathSegments[1] 为 id,  uri.pathSegments[0] 为 book
                val book = this.books.firstOrNull { it.id == id.toInt() } // 查出 book 
                book?.let { it.name = book.name; it.price = book.price } // 进行更新 

                return if (book == null) { 0 } else { 1 } // 返回更新数
            }
            else -> { 0 }
        }
    }
删除
    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
        return when (uriMatcher.match(uri)) { // 判断链接的类型
            xContentProviderAllNumItem -> { // 
                val id = uri.pathSegments[1] // 返回连接的 id 部分, content://xxx/book/id
                val books = this.books.filter { it.id == id.toInt() } //列出的匹配对象
                this.books.forEach { this.books.remove(it) } // 移除所有匹配对象

                return books?.count() ?: 0
            }
            else -> { 0 }
        }
    }
查询
    override fun query(
        uri: Uri,
        projection: Array<out String>?, // 返回表名,
        selection: String?, // 查询条件
        selectionArgs: Array<out String>?, // 查询条件参数
        sortOrder: String? // 排序条件
    ): Cursor? {
        return when (uriMatcher.match(uri)) { // 判断链接的类型
            xContentProviderDir -> { // 匹配到 book 队列
                // 可以从数据库直接返回,这里使用 MatrixCursor 自定义一个游标对象
                val cursor = MatrixCursor(arrayOf("name", "price"))

                // 这里简单处理,不考虑查询条件,直接返回所有 book 
                this.books.forEach() {
                    val newRow = cursor.newRow()
                    newRow.add("id", it.id)
                    newRow.add("name", it.name)
                    newRow.add("price", it.price)
                }
                return cursor // 返回游标对象
            }
            else -> { null }
        }
    }
类型
        return when (uriMatcher.match(uri)) { // 使用 uriMatcher 判断类型,并返回约定的格式。格式有要求,请查资料进行返回。
            xContentProviderDir -> {
                "vnd.android.cursor.dir/vnd.$authoriy.book"
            }
            xContentProviderAllNumItem -> {
                "vnd.android.cursor.item/vnd.$authoriy.book"
            }
            xContentProviderAllStrItem -> {
                "vnd.android.cursor.item/vnd.$authoriy.book"
            }
            else -> {
                null
            }
        }
调用
        val resolver = this.contentResolver

        if (true) { // 插入操作
            val uri = XContentProvider.buildUri("book") // 构建查询 uri content://xxx/book
            val result = resolver.insert( // 插入返回完整的数据 uri
                uri,
                ContentValues().apply { put("name", "xxx"); put("price", 64); })
            val resultUri = Uri.parse(result.toString())
            val id = resultUri.pathSegments[1] // 从返回链接中链接 id
        }

        if (true) { // 更新操作
            val uri = XContentProvider.buildUri("book", "1") // 构建更新 uri content://xxx/book/1
            val result = resolver.update(  // 更新返回多少条数据更新了
                uri,
                ContentValues().apply { put("name", "xxx"); put("price", 64); }, null, null
            )
        }

        if (true) { // 删除操作
            val uri = XContentProvider.buildUri("book", "1") // 构建删除 uri content://xxx/book/1
            val result = resolver.delete(  // 删除,返回多少条数据更新了
                uri, null, null
            )
        }

        if (true) { // 查询操作
            val uri = XContentProvider.buildUri("book") // 构建查询 uri content://xxx/book
            val cursor: Cursor? = resolver.query(uri, null, "id = ?", arrayOf("32"), null) // 输入查询条件 实际不考虑
            cursor?.let {
                while (it.moveToNext()) { // 通过游标解析出内容
                    val name = it.getString(it.getColumnIndex("name"))
                    val price = it.getDouble(it.getColumnIndex("price"))
                    sendLog("name: $name price: $price")
                }
            }
        }

通知

// implementation "com.android.support:support-compat:28.0.0"


    /** 测试函数 */
    private fun testFunc() {

        // 1. 返回通知管理器
        val manager = this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

        // 2. 返回该通知准备使用的频道
        var channelId = this.getChannelId(manager)

        // 3. 创建通知内容
        val notification: Notification = this.createNotification(channelId)

        // 4. 定义一个通知编号,并进行通知
        val id = nextActiveCounter.getAndIncrement()
        manager.notify(id, notification) // 如果两次通知使用相同的 id, 则更新上次通知内容
        NotificationManagerCompat.from(this).apply{ notify(id, notification) } // 最新手册中的显示调用方式 
    }

    // 创建通知内容
    private fun createNotification(channelId: String): Notification {

        // 1. 通知的一些文字内容
        val sdf = SimpleDateFormat("dd/M/yyyy hh:mm:ss")
        val currentDate = sdf.format(Date())

        // 2. 创建点击后的动作, PendingIntent 作为满足条件后进行的动作
        val intent = Intent(Intent.ACTION_CALL)
        intent.data = Uri.parse("tel:13111111111")
        val pi: PendingIntent = PendingIntent.getActivity(this, 0, intent, 0)

        // 3. 创建通知内容
        val largeIcon = BitmapFactory.decodeResource(this.resources, R.drawable.whatsapp_512px) // 载入图像资源
        return NotificationCompat.Builder(this, channelId)
            .setContentTitle("通知标题")
            .setContentText("我是内容, 现在是 $currentDate") // 内容,但是太长会被截断
            .setSmallIcon(R.drawable.whatsapp_64px)
            .setLargeIcon(largeIcon) 
            .setStyle(NotificationCompat.BigPictureStyle().bigPicture(largeIcon)) // 设置背景,与其它 setStyle 只有一种生效
            .setStyle(NotificationCompat.BigTextStyle().bigText("这里可以放很长的文字 , 现在时间 $currentDate")) // 设置长文字不截断
            .setContentIntent(pi) // 设置点击后执行的动作
            .setAutoCancel(true) // 点击通知动作后自动关闭通知,或是调用 manager.cancel(id) 关闭通知
            .build()
    }

    // 1. 新版本的通知需要属于基个频道。
    // 2. 不同的频道可以定义名称及重要程度。以方便使用者进行管理
    private fun getChannelId(manager: NotificationManager): String {

        val channelId = "xChannelId" // 频道 id,自定义

        // 1. 低版本 android 不需要频道
        if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O ){ // andorid 8.0
            return ""
        }

        // 2. 如果频道已创建,直接返回频道 id
        var channel = manager.getNotificationChannel(channelId)
        if(channel != null ){
            return channelId // 频道已经存在,
        }

        // 3. 新建频道并指定这个频道的重要性, 以后可以在通知管理中进行管理
        channel = NotificationChannel(channelId, "x 的普通消息", NotificationManager.IMPORTANCE_HIGH)
        manager.createNotificationChannel(channel)

        return channelId
    }

调用摄像头与相册

主要是权限问题,一般建议使用 context.getExternalFilesDir() 获得应用对应的目录,这个目录下的文件读写没有限制

Android 10适配要点,作用域存储_郭霖的专栏-CSDN博客

Android 11新特性,Scoped Storage又有了新花样_郭霖的专栏-CSDN博客

Android 10、11 存储完全适配(上) - 简书 (jianshu.com)

调用摄像头

   // 1. 开始拍照
    private fun begin() {

        // 1.1. 创建一个文件并返回 uri
        var imageUri: Uri = this.createFile("output_image.jpg", "xsoft.demo.xapp.fileprovider")

        // 1.2. 定义拍照动作,并指定图像路径
        val intent = Intent("android.media.action.IMAGE_CAPTURE")
        intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri)

        // 1.3. 我的辅助函数,指定动作 id 及对应的 动作,并启用拍照动作
        val idx = this.activeStore.putActive { shwoImage(imageUri) }
        this.startActivityForResult(intent, idx)
    }

    /** 创建一个文件,并返回对应的 uri */
    private fun createFile(fileName: String, authority: String): Uri {

        // 1. 因为权限文件,不要直接读写 sd 卡上文件,使用 externalCacheDir 目录
        val file = File(this.externalCacheDir, fileName)
        if (file.exists()) { file.delete() }
        file.createNewFile()

        // 2. 返回文件对应的 uri, 因为新的系统使用 uri 来进行图像处理
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            FileProvider.getUriForFile(this, authority, file)
        } else {
            Uri.fromFile(file)
        }
    }

    /** 2. 动作的返回结果, 使用我的辅助函数 */
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        this.activeStore.runWithActivityResult(requestCode, resultCode, data)
    }

    /** 3. 将图像显示至 ImageView */
    private fun shwoImage(imageUri: Uri) {
        val stream = this.contentResolver.openInputStream(imageUri)
        stream.use {
            val bitmap = BitmapFactory.decodeStream(stream)
            val view = this.findViewById<ImageView>(R.id.ImageView)
            view.setImageBitmap(bitmap)
        }
    }

// AndroidManifest.xml 中定义一个文件共享配置 // 在 meta-data 中指定可以访问的文件
        <provider
            android:authorities="xsoft.demo.xapp.fileprovider"
            android:name="androidx.core.content.FileProvider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>

// 对应于 Res.xml/file_paths.xml, 指定可以读写 sd 卡的根目录
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="my_images" path="/" />
</paths>

访问相册

    /** 1. 打开文件选择 */
    private fun begin() {

        // 设置打开文件选择
        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
        intent.addCategory(Intent.CATEGORY_OPENABLE)
        intent.type = "image/*" // 文件类型

        val idx = activeStore.putActive { data -> shwoImage(data) }
        this.startActivityForResult(intent, idx)
    }

    /** 2. 动作的返回结果, 使用我的辅助函数 */
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        this.activeStore.runWithActivityResult(requestCode, resultCode, data)
    }

    /** 3. 将图像显示至 ImageView */
    private fun shwoImage(data: Intent?) {

        data?.data?.let{
            uri ->
            val descriptor = this.contentResolver.openFileDescriptor(uri, "r")
            descriptor?.use {
                val bitmap = BitmapFactory.decodeFileDescriptor(it.fileDescriptor)
                val view = this.findViewById<ImageView>(R.id.ImageView)
                view.setImageBitmap(bitmap)
            }
        }
    }

读写相册等权限

新版本已经不允许直接读写sd 卡下的文件

读写文件通过 MediaStore (contentResolver) 进行。


早期版本中加入 <application android:requestLegacyExternalStorage="true" /> 获得读写权限

然后动态申请权限,可以参考我的 PermissionsActionStore 类



1. 使用我的辅助函数请示权限,然后处理 listAllImage 获取所有图像数据

        val premissions = arrayOf(
            Manifest.permission.READ_EXTERNAL_STORAGE,
            Manifest.permission.WRITE_EXTERNAL_STORAGE
        )
        this.permissionsActionStore.putRequestPermissionsAction(this, premissions, { this.listAllImage() })

2. 获取所有图片数据,使用 contentResolver 读取数据

    fun listAllImage():Unit {

        val cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            null, null, null, "${MediaStore.MediaColumns.DATE_ADDED} desc")
        if (cursor != null) {
            while (cursor.moveToNext()) {
                val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
                val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
                this.sendLog("图像地址: $uri") // content://media/external/images/media/66

                processImage(uri) // 这里进行复制图片操作
            }
            cursor.close()
        }
    }

3. 进行图片处理,将指定地址的图片创建 bitmap 对象

    private fun processImage(uri: Uri) {
        val fd = contentResolver.openFileDescriptor(uri, "r")
        with(fd){
            val bitmap = BitmapFactory.decodeFileDescriptor(this?.fileDescriptor)
            with(bitmap){
                addBitmapToAlbum(bitmap, "我是副本", "image/jpeg", Bitmap.CompressFormat.JPEG )
            }
        }
    }

4. 进行图片复制,并加入到相册中

    fun addBitmapToAlbum(bitmap: Bitmap, displayName: String, mimeType: String, compressFormat: Bitmap.CompressFormat) {

        val fileName = "${displayName}_${ System.currentTimeMillis()}"

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {

            // 4.1 如果是新的系统使用 contentResolver 进行处理
            val values = ContentValues()
            values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) // 设置名称
            values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType) // 设置类型
            values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis() / 1000);
            values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis());
            values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES) // 文件保存路径, 其它 DIRECTORY_PICTURES、DIRECTORY_MOVIES、DIRECTORY_MUSIC
            values.put(MediaStore.Images.Media.IS_PENDING, true)

            val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) // 存储数据
            this.sendLog("新图片地址:$uri") // content://media/external/images/media/81

            if(uri != null){
                with( contentResolver.openOutputStream(uri)){
                    saveImageToStream(bitmap, compressFormat, this)
                }

                values.put(MediaStore.Images.Media.IS_PENDING, false) // 通知一下数据处理完成
                contentResolver.update(uri, values, null, null)

                this.sendLog("导入图片 $uri")
            }

        } else {

            // 4.2 如果是旧的系统,使用文件处理文件
            val directoryPath = "${Environment.getExternalStorageDirectory().path}/${Environment.DIRECTORY_PICTURES}"
            val directory = File(directoryPath)
            if(!directory.exists()){
                directory.mkdirs()
            }

            val filePath = "${directoryPath}/${fileName}.jpg"
            val file = File(filePath)
            saveImageToStream(bitmap, compressFormat, FileOutputStream(file))

            var intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE).apply {
                data = Uri.fromFile(file)
            }

            // 如果有多个图片,相册中只显示一个图片
            this@MainActivity.sendBroadcast(intent)

            this.sendLog("导入图片 $filePath")
        }
    }

5. 进行图片文件复制 

    private fun saveImageToStream(bitmap: Bitmap, compressFormat: Bitmap.CompressFormat, outputStream: OutputStream?) {

        with(outputStream){
            val success = bitmap.compress(compressFormat, 100, this)
        }
    }

选择文件


// 我的辅助函数,
        val id = this.activeStore.putActive {
            val uri = it?.data
            this@MainActivity.sendLog("选择文件:$uri")
        }
// 开始选择文件
        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
        intent.addCategory(Intent.CATEGORY_OPENABLE)
        intent.type = "*/*"
        startActivityForResult(intent, id)

要求整个设备权限


        // 1. 设置 AndroidManifest.xml
        <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" /> <!-- ignore 不加的话,系统会提示用户谨慎同意  -->

        // 2. 判断是否已经有权限了
        if(Build.VERSION.SDK_INT < Build.VERSION_CODES.R || Environment.isExternalStorageManager()) {
            Toast.makeText(this, "已获得访问所有文件权限", Toast.LENGTH_SHORT).show()
            return
        }

        // 3. 提示用户同意给权限
        val builder = AlertDialog.Builder(this).setMessage("本程序需要您同意允许访问所有文件权限")
            .setPositiveButton("确定") { _, _ ->
                // 4. 进入文件授权设置界面,由用户进行设置
                val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
                this.startActivity(intent)
            }
        builder.show()

第三方应用配合使用

第三方使用可能会要求旧版本中的传文件路径的函数。可以使用新版本的函数读取文件并保存至 context.getExternalFilesDir() 目录下,再行到文件路径,并将路径传入至第三方SDK中

修改第三方应用文件

默认信息下不允许第三方保存的图片等,需要请示权限。并且提示了一次申请多个文件的处理权限。这样删除、编辑等不需要一个一个文件进行申请了

1. Build.VERSION_CODES.R 之后的版本
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            val urisToModify = listAllImage()
            // createWriteRequest 多文件写入, 
            // createFavoriteRequest 多文件收藏, 
            // createTrashRequest 多文件移到回收站, 
            // createDeleteRequest 多文件删除
            val editPendingIntent = MediaStore.createWriteRequest(contentResolver, urisToModify)
            startIntentSenderForResult(editPendingIntent.intentSender, 1243, null, 0, 0, 0)
        }

2. 之前的版本
try {
    contentResolver.openFileDescriptor(imageContentUri, "w")?.use {
        Toast.makeText(this, "现在可以修改图片的灰度了", Toast.LENGTH_SHORT).show()
    }
} catch (securityException: SecurityException) { 
    3. 使用异常来判断是不是因为权限问题
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {

        4. 如果不是权限问题,直接再次抛出异常
        val recoverableSecurityException = securityException as?
            RecoverableSecurityException ?:
            throw RuntimeException(securityException.message, securityException)

        5. 申请权限,旧版本中只能一个一个文件申请
        val intentSender = recoverableSecurityException.userAction.actionIntent.intentSender
        intentSender?.let {
            startIntentSenderForResult(intentSender, IMAGE_REQUEST_CODE, null, 0, 0, 0, null)
        }
    } else {
        throw RuntimeException(securityException.message, securityException)
    }
}

线程

可以使用 Thread 子类,或直接使用函数 thread

# 继承 Thread
class Xthread : Thread() {

    override fun run() {
        val id = Thread.currentThread().id
        Log.d("测试", "thread $id")
    }
}

# 执行
val thread = Xthread()
Thread(thread).start()


# 辅助函数
thread {
    val id = Thread.currentThread().id
    Log.d("测试", "thread $id")
}


使用 Handle 发送消息至 UI 线程

        // 1. 定义一个消息泵消息处理函数
        val handler = object: Handler(){
            override fun handleMessage(msg: Message){
                val id = Thread.currentThread().id
                Log.d("测试", "消息泵线程 $id")
            }
        }

        // 2. 在主线程中
        val id = Thread.currentThread().id
        Log.d("测试", "主线程 $id")

        // 3. 启动线程
      thread {
          val id = Thread.currentThread().id
          Log.d("测试", "后台线程 $id")

          // 4. 发送消息至消息泵中
          val msg = Message() // todo: 设置一个 msg 属性
          handler.sendMessage(msg)
      }
高级 handle
  1. 每个线程中可以调用 Looper.perpare() 创建一个 messagequeue,
  2. 定义一个 Handler 对象处理当前线程中接收到的 message,
  3. 在其它线程中调用 handler 对的 sendMessage 发送消息至 messagequeue 中

Activity.runOnUiThread

在 Activity 中


        thread {
            // 线程部分
            runOnUiThread {
                // 切换至 ui 线程
            }
        }

View.Post / View.postDelayed

   thread {
            val view = this@AppBar.findViewById<View>(android.R.id.content).rootView
            view.post { /* 在 ui 线程中运行 */ } 
            view.postDelayed( { /* 在 ui 线程中运行 */}, 3000 ) 
        }

整合前后台线程处理

封装了 Handler, 不过已经不再推荐使用


    // 传入参数,进度显示,返回值
    class ThreadTask : AsyncTask<Unit, Int, Boolean>(){

        /** 执行任务, 后台线程中执行 */
        override fun doInBackground(vararg params: Unit?): Boolean {

            // 通知后台进度更新
            this.publishProgress(1)
            return true
        }

        /** 执行任务前调用, UI 上下文  */
        override fun onPreExecute() {
        }

        /** 更新进度, UI 上下文 */
        override fun onProgressUpdate(vararg values: Int?) {
        }

        /** 任务执行完成后调用, UI 上下文 */
        override fun onPostExecute(result: Boolean){
            Log.d("测试", 任务完成)
        }

    }

ThreadTask().execute()

Service

<!-- 1. 添加权限 -->
<service android:name=".MyService" android:enabled="true" android:exported="true"></service>

<!-- 2. 现在一个 Service -->
class MyService : Service() {


    override fun onBind(intent: Intent): IBinder {
        // return this.reportBinder , 不需要,抛出异常即可
    }

    override fun onCreate() {
        super.onCreate()
        this.showLog("onCreate, 第一次 startService 时执行")
    }

    override fun onStartCommand( intent: Intent?, flags: Int, startId: Int ): Int {
        this.showLog("onStartCommand, 每次 startService 时执行")

        thread{
            Thread.sleep(3000)
            this.stopSelf() // 从自身关闭
        }

        return super.onStartCommand(intent, flags, startId)
    }

    private fun showLog(message: String) {
        Log.d("测试", message)
    }

    override fun onDestroy() {
        super.onDestroy()
        this.showLog("onDestroy")
    }
}

<!-- 3. 启动或是停止 -->
val intent = Intent(this, MyService::class.java)
this.startService(intent)
this.stopService(intent)

Service 与 Activity 通讯


<!-- 1. 实现一个 Binder -->
class ReportBinder : Binder() {}

<!-- 2. 在 Service 中创建一个 binder 实例, 并在 onBind 中返回  -->
class MyService : Service() {

    val reportBinder: ReportBinder = ReportBinder()

    override fun onBind(intent: Intent): IBinder {
        return this.reportBinder
    }
}

<!-- 3. 在 Activity 中实现一个 ServiceConnection, 在 onServiceConnected 中能得到 Service 中的 binder 对象 -->
private val connection = object : ServiceConnection {

    override fun onServiceConnected(name: ComponentName, service: IBinder) {
        val reportBinder = service as ReportBinder
    }
}

<!-- 4. 启动或是停止服务并会传入 connection -->
val intent = Intent(this, MyService::class.java)
this.bindService(intent, this.connection, Context.BIND_AUTO_CREATE) // 使用 BIND_AUTO_CREATE, 自动创建 MyService 并调用 onCreate
this.unbindService(this.connection) // 停止服务

前台Service

默认应用服务在应用关闭后服务也会被关闭。可以创建前台服务,会在任务栏中显示一个通知。创建过程与通知也很像

<!-- 1. 声明权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />


<!-- 2. 实现服务 -->
class MyService : Service() {

    override fun onCreate() {
        super.onCreate()
        this.showLog("onCreate, 第一次 startService 时执行")

        <!-- 3. 在启动时创建通知 -->
        val manager =
            this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                MyService::class.simpleName,
                "前台服务",
                NotificationManager.IMPORTANCE_DEFAULT
            )
            manager.createNotificationChannel(channel)
        }

        val intent = Intent(this, MainActivity::class.java)
        val activity = PendingIntent.getActivity(this, 0, intent, 0)
        val notification = NotificationCompat.Builder(this, MyService::class.simpleName ?: "")
            .setContentTitle("前台服务")
            .setContentText("前台服务").setContentIntent(activity).build()

        <!-- 4. 设置为前台服务  -->
        this.startForeground(1, notification)

        thread{
            while (true){
                Thread.sleep(1000)
                this.showLog("我是通知")
            }
        }
    }
}

IntentService

Service 本身在主线程中运行,需要 thread {} 开始子线程。 IntentService 封装了这个流程

class MyService : IntentService("xService") {

    /** 这里在子线程下进行 */
     override fun onHandleIntent(intent: Intent?) {
        val name = Thread.currentThread().name
         this.showLog("onHandleIntent: $name")
     }

    /** 这里在主线程下进行 */
     override fun  onDestroy() {         
         val name = Thread.currentThread().name
         this.showLog("onDestroy: $name")
     }
 }

<!-- 2. 启动与服务一样处理 -->

协程

组合挂起函数 - Kotlin 语言中文站 (kotlincn.net)

在语法层面实现的。轻量级的切换。

GlobalScope.launch 创建一个顶层协程,使用成本比较高,不建议直接使用

runBlocking 创建一个顶层协程,会等待里面的子协程都结束后才结束。一般不推荐使用,调试时用一下

suspend 将一个函数声明为挂起函数,这样可以在协程中被调用

coroutineScope 在协程作用域或挂起函数中调用

launch 只能在协程作用域中调用

async 在协程中使用,并能返回值

  val thread = Thread.currentThread()
  Log.e("测试", "主线程信息:${thread.id}[${thread.name}]")

  // 创建一个协程的作用域区, 如果它结束了。内部协程强制结束
  GlobalScope.launch {

    // 暂停当前协程 1 秒
    delay(1000)

    // 暂停当前线程 1 秒
    Thread.sleep(1000)

  }

    // 创建一个协程的作用域区,会等待内部的子协程结束后才完成, 一般调试时使用
  runBlocking {

        showThreadInfo(3)

        /** 启动一个协程 */
        launch{

        }
  }

/** suspend 声明为挂起函数,可以在协程中被调用 */
private suspend fun showThreadInfo(id: Int) {

  val thread = Thread.currentThread()
  Log.e("测试", "线程信息$id:${thread.id}[${thread.name}]")
}

/** 使用了 coroutineScope ,可以在挂起函数内部调用 launch  */
private suspend fun showThreadInfo(id: Int) = coroutineScope {

  launch {
    val thread = Thread.currentThread()
    Log.e("测试", "线程信息$id:${thread.id}[${thread.name}]")
  }
}


协程与线程的关系

启动协程可以指定一种线程模式 (coroutineScope 无法指定), 比如 launch(Dispatchers.Default){ } , 可以使用 withContext 在协程中切换至指定的线程模式进行工作, 调用协程会被阻塞

  1. Dispatchers.Default 启用一个低并发的线程策略(默认值)
  2. Dispatchers.IO 启用一个高并发的线程策略
  3. Dispatchers.Main 使用主线程, 只在 android 中使用, 使用此模式,可以在协程中直接调用 UI 对象

// 启用了一个后台线程,协程在其中调度
GlobalScope.launch {
    //GlobalScope开启协程:DefaultDispatcher-worker-1
    Log.d(TAG, "GlobalScope开启协程:" + Thread.currentThread().name)
    //子线程中此处不可以做UI操作
}

// 使用 Dispatchers.Main 切换至 ui 线程
GlobalScope.launch {
    //GlobalScope开启协程:DefaultDispatcher-worker-1
    Log.d(TAG, "GlobalScope开启协程:" + Thread.currentThread().name)

    //子线程中此处不可以做UI操作
    //Toast.makeText(this@MainActivity, "GlobalScope开启协程", Toast.LENGTH_SHORT).show()

    withContext(Dispatchers.Main){
        Toast.makeText(this@MainActivity, "协程中切换线程", Toast.LENGTH_SHORT).show()
    }
}

// 使用 Dispatchers.Main 将协程调试为使用主线程
GlobalScope.launch(Dispatchers.Main) {
    //GlobalScope开启协程:main
    Log.d(TAG, "GlobalScope开启协程:" + Thread.currentThread().name)
    //可以做UI操作
    Toast.makeText(this@MainActivity, "GlobalScope开启协程", Toast.LENGTH_SHORT).show()
}

停止协程

// 协程返回值进行 cancel
val launch = GlobalScope.launch {}            
launch.cancel()


// 使用一个 job 进行控制
val job = Job()
val scope = CoroutineScope(job)

// 每次 launch 进行一次协程调用
scope.launch {}
scope.launch {}

// 等待协程完成
job.join() 

// 关闭协程, 强制引发 CancellationException 异常
job.cancel() // 之后调用 join 等待关闭完成
job.cancelAndJoin() // 合并调用 cancel join
停止协和详情
runBlocking {

            Log.d("Debug", "runBlocking:" + Thread.currentThread().name)

            val job = launch(Dispatchers.IO) {

                try {
                    for (idx in (0..100)) {
                        // 进行一些动作
                        if (!isActive) { // 检测是否进行了 cancel 动作,实操中没有用。                           
                            break
                        }
                    }
                } catch (err: CancellationException) {
                    // 检测到取消动作
                } catch (err: Throwable) {
                    Log.d("Debug", "发生错误 $err")
                } finally {
                    Log.d("Debug", "你被关闭了 ")

                    withContext(NonCancellable) {
                        // 放在这里的代码在 cancel() 时也会被强制执行
                    }
                }
            }

            job.cancelAndJoin()
        }
NonCancellable
withContext(NonCancellable) {
    // 放在这里的代码在 cancel() 时也会被强制执行
}
withTimeout

执行代码时如果超时抛出 TimeoutCancellationException ( 为 CancellationException 子类 ) 异常

withTimeout(1300L) {
    repeat(1000) { i ->
        println("I'm sleeping $i ...")
        delay(500L)
    }
}
withTimeoutOrNull

与 withTimeout 类似,但是执行超时时不抛出异常, 简单的返回 null

val result = withTimeoutOrNull(1300L) {
    repeat(1000) { i ->
        println("I'm sleeping $i ...")
        delay(500L)
    }
    "Done" // 在它运行得到结果之前取消它
}
println("Result is $result")

协程返回值

使用 async 函数返回一个 Deferred 对象。并调用 await() 返回结果值。async 可以指定线程模型

使用 withContext ,可以认为是 async().await()

注意此函数返回最后一行的内容,无法使用retrun


// 这里指定了线程模型,这里不阻塞调用
val task = async(Dispatchers.IO) {
    5 + 5
}

// 需要结果时调用 await 阻塞当前协程返回结果 
val result = task.await()
Log.d("测试", "返回结果: $result")


// 阻塞当前协程并返回结果 
val result = withContext(Dispatchers.IO){
   5 + 5
}

协程调用线程、回调、接口简化调用

相当于 javascript (Promise ) 的协程调用 。比如一些线程调用或是回调,在调时很复杂。可以使用将这些调用放在协程中以简化调用

  1. 将复杂调用放在 suspendCoroutine 中以阻塞当前协程
  2. suspendCoroutine 会传入一个 continuation
  3. 在复杂调用中调用 continuation.resume 返回结果 (或是调用 continuation.resumeWithException 返回一个异常)
  4. suspendCoroutine 根据不同操作返回结果或是抛出异常
  5. 对调用方来说不用各种回调之类的操作

1. 可以放在函数中调用,但是标记为 suspend 挂起函数, 
private suspend fun runThread(): Int {

    2. 调用 suspendCoroutine 函数,阻塞当前协程
    return suspendCoroutine { continuation ->
        3. 这里使用 thread 做演示
        thread {
                try {

                    4. 进行一些费时计算
                    val value = 1 / 0
                    5.1.  调用 resume 结束 suspendCoroutine 的等待,并返回结果 
                    continuation.resume(value)

                } catch (exception: Exception) {
                    5.2. 调用 resumeWithException 结束 suspendCoroutine 的等待,并抛出对应的异常
                    continuation.resumeWithException(exception)
                }
            }

}
/** 调用广义方法, 必须在协程中调用, 这样就很简单了 */
try {
    val value = runThread()

    withContext(Dispatchers.Main){ /* 切换至 ui 上下文进行处理 */ }

} catch (exception: Exception) {
    Log.e("测试", "发生错误", exception)
}

使用网络

WebView 控制,内嵌浏览器 基于网络的内容 | Android 开发者 | Android Developers

HttpURLConnection

OkHttp 类似于 restclient

Retrofit 定义一个接口。该接口对应于一个服务器上提供的服务。 使用 Retrofit 可以自动实现接口实例。调用接口实例即访问服务资源,并会自动进行转型。

其它

kotlin-logging

一个日志记录库, 对 Slf4j 的封闭, Slf4j 实现了一个java 中通用的日志记录接口,但是在 kotlin 中比较麻烦, 所以要找一个 kotlin 封装

MicroUtils/kotlin-logging: Lightweight logging framework for Kotlin. A convenient and performant logging library wrapping slf4j with Kotlin extensions (github.com)

关于Kotlin中日志的使用方法 - SegmentFault 思否

implementation 'io.github.microutils:kotlin-logging-jvm:2.1.20' // slf4j 调用的封装
implementation "ch.qos.logback:logback-classic:1.2.10" // slf4j 的一个实现

// 动态设置日志目录, 在程序启动中设置
val dir = instrumentation.targetContext.filesDir // 这是在测试时设置, 实际在 application 中设置
System.setProperty("LOG_HOME", dir.absolutePath) // 设置一个系统变量,程序自动设置该

// 动态设置日志记录的水平
val loggerFactory = LoggerFactory.getILoggerFactory()
if (loggerFactory is LoggerContext) {

    val logger = loggerFactory.getLogger("root") // root 要使用 getLogger, 其它调用 exists("name")
    logger?.let {
        logger.level = Level.ALL
    }
}

/** 对类的 log 扩展函数 , obj.log.error("test") */
val <reified T> T.log: KLogger
    inline get() = KotlinLogging.logger{T::class.java.name}

// 实例化日志记录器
val logger = KotlinLogging.logger { }

// 记录错误
val error = Exception("test")
logger.error(error) { "This is a error $error" }
logger.info { "我是测试" }
logger.info("有什么区别 ")
logger.debug { "我是 debug 日志" }
val thread = thread { logger.info { "我在线程中" } }
thread.join()

配置文件

<!-- 要保存于 src/main/resources/logback.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
    <!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径, 也可以动态设置该变量-->
    <!--<property name="LOG_HOME" value="/data/user/0/xsoft.demo.xapp/files" />-->

    <!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 按照每天生成日志文件 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--日志文件输出的文件名-->
            <FileNamePattern>${LOG_HOME}/xlog_%d{yyyy-MM-dd}.log</FileNamePattern>
            <!--日志文件保留天数-->
            <MaxHistory>7</MaxHistory>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
        <!--日志文件最大的大小-->
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>10MB</MaxFileSize>
        </triggeringPolicy>
    </appender>

    <!-- 日志输出级别 -->
    <root level="INFO">
        <appender-ref ref="STDOUT" />
        <appender-ref ref="FILE" />
    </root>

</configuration>

ROOM

google 官方 orm 仅支持 sqlite , 用这个吧,GreenDAO 似乎不支持 kotlin

需要注释类, 然后自动编译出对应的 ROM 类

注意:因为是自动编译的,要注意看 build 结果,有可能有问题,编译没成功

https://developer.android.com/jetpack/androidx/releases/room

https://developer.android.com/reference/android/arch/persistence/room/ColumnInfo

将Room的使用方式塞到脑子里 - 掘金 (juejin.cn)

定义


// 定义数据 dao ,  支持使用 data class, 也可以使用 class
@Entity(tableName = "my_user")
class User {

    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "my_id")
    var id: Long? = null

    @ColumnInfo(name = "my_name")
    var name: String = ""

    var birthday: LocalDateTime? = null

    @ColumnInfo(index = true, defaultValue = "-1")
    var childId: Int? = null

    @Ignore // Ignore 属性必须提供一个默认值或提示不包括该属性的构造参数
    var sex: String = "x"
}

/** room 数据转换器, 非基本数据使用该转换器 */
object DateTypeConverter {

    private val utcZoneOffset = ZoneOffset.of("Z")

    @TypeConverter
    fun fromTimestamp(value: Long?): LocalDateTime? {
        return value?.let { LocalDateTime.ofEpochSecond(it, 0, utcZoneOffset) }
    }

    @TypeConverter
    fun dateToTimestamp(date: LocalDateTime?): Long? {
        return date?.toEpochSecond(utcZoneOffset)
    }
}

/** 数据操作接口 */
@Dao
interface UserDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertUser(user: User): Long

    @Insert
    fun insertUsers(vararg users: User): List<Long>

    @Insert
    fun insertUsers(users: Collection<User>): Array<Long>

    @Delete
    fun delUser(user: User): Int

    @Delete
    fun delUsers(vararg users: User): Int

    @Update
    fun updateUser(user: User): Int

    @Update
    fun updateUsers(vararg users: User): Int

    @Query("select * from my_user where my_id = :id")
    fun queryUser(id: Long): User?

    @Query("select * from my_user")
    fun queryUsers(): List<User>

    @Query("insert into my_user ( my_name, birthday ) values(:name, :birthday)")
    fun insertUserRow(name: String, birthday: LocalDateTime): Long

    @Query("delete from my_user where my_id = :id")
    fun delUserRow(id: Long)

    @Query("update my_user set my_id = :name where my_id = :id")
    fun updateRow(id: Long, name: String)

    /** 返回总计 */
    @Query("select  count(my_id) from my_user ")
    abstract fun count(): Long

    /** 使用了事务 */
    @Transaction
    fun insertAndDeleteInTransaction(newUser: User, oldUser: User) {
        insertUser(newUser)
        delUser(oldUser)
    }
}


// 然后创建对应Migration
val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("alter table User add sex Text not null default '男' ")
    }
}


@Database(
    version = 1,
    entities = [User::class], // 定义 orm 的类
    exportSchema = false
)
@TypeConverters(DateTypeConverter::class)
abstract class CustomDatabase : RoomDatabase() {

    abstract fun userDao(): UserDao

    companion object {

        private var instance: CustomDatabase? = null

        @Synchronized
        fun get(context: Context): CustomDatabase {

            if (instance == null) {
                instance =
                    Room.databaseBuilder(context, CustomDatabase::class.java, "user.sqlite.db")
                        /*Room.inMemoryDatabaseBuilder(context, CustomDatabase::class.java) // 仅在内存中使用 */
                        .allowMainThreadQueries() // 应该默认不支持在主线程中调用 
                        .addMigrations(MIGRATION_1_2)
                        .build()
            }

            return instance!!
        }

    }
}

调用


val context = ApplicationProvider.getApplicationContext<Context>()
val db = CustomDatabase.get(context)
try {
    val userDao = db.userDao()

    val user = (User()).apply {
        name = "name"
        birthday = LocalDateTime.now()
    }

    val id = userDao.insertUser(user)
    logger.info("插入数据: $id")

    val id2 = userDao.insertUserRow("test", LocalDateTime.now())
    logger.info("插入数据: $id2")


    var queryUser = userDao.queryUser(id)
    var json = GsonUtils.toJson(queryUser)
    logger.info("插入数据: $id, $json")

    queryUser = userDao.queryUser(id2)
    json = GsonUtils.toJson(queryUser)
    logger.info("插入数据: $id, $json")

    val count:Long = userDao.count()
    logger.info("共有数据 $count 条 ")
}
finally {
    db.close()
}

测试 UI

/** ui 测试的基类 */
open class TestUiBase {


    /** 显示 note */
    protected fun showNote(message: String) {

        XHelper.showNote(this.context, message)
    }

    /**  测试 Fragment ui  */
    protected inline fun <reified F : Fragment> testFragment(
        argument: Bundle?,
        runAction: (FragmentScenario<F>) -> Unit = {}
    ) {

        this.showLog("进行 ${F::class.java} ui 测试")

        launchFragmentInContainer<F>(argument).use {

            runAction(it)

            waitUiStarted()

            this.waitUiExit()
        }

        this.showLog(" ${F::class.java} ui 测试完成")

    }

    /** 测试 Activity ui */
    protected inline fun <reified F : Activity> testActivity(
        intent: Intent? = null,
        argument: Bundle? = null,
        runAction: (ActivityScenario<F>) -> Unit = {}
    ) {

        this.showLog("进行 ${F::class.java} ui 测试")

        launchActivity<F>(intent, argument).use {

            runAction(it)

            waitUiStarted()

            this.waitUiExit()
        }

        this.showLog(" ${F::class.java} ui 测试完成")
    }

    /** 当前实例 */
    private val instrumentation: Instrumentation
        get() = TestUiBase.instrumentation

    /** 当前运行上下文   */
    val context: Context
        get() = this.instrumentation.targetContext

    /** 当前包名  */
    private val packageName: String
        get() = this.context.packageName

    /** 当前测试设备 */
    private fun getUiDevice(): UiDevice {
        return UiDevice.getInstance(this.instrumentation)
    }

    companion object {

        val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()

        init {

            // 日志目录
            val dir = instrumentation.targetContext.filesDir
            System.setProperty("LOG_HOME", dir.absolutePath)

            // 设置日志记录的水平
            val loggerFactory = LoggerFactory.getILoggerFactory()
            if (loggerFactory is LoggerContext) {

                val logger = loggerFactory.getLogger("root")
                logger?.let {
                    logger.level = Level.ALL
                }
            }
        }

        /** 对类的 log 扩展函数 , obj.log.error("test") */
        val <reified T> T.log: KLogger
            inline get() = KotlinLogging.logger{T::class.java.name}
    }


    /** 等待当前 ui 启动完成 */
    protected fun waitUiStarted() {

        this.showNote("等待界面初始化")

        val uiDevice = this.getUiDevice()

        /*uiDevice.waitForWindowUpdate(packageName, 5000)*/
        val pkg = By.pkg(packageName)
        uiDevice.wait(Until.hasObject(pkg), 10000)

        this.showNote("界面初始化完成")
    }

    /** 循环阻塞,直到当前 ui 退出  */
    protected fun waitUiExit() {

        val uiDevice = this.getUiDevice()
        val pkg = By.pkg(packageName)

        while (true) {

            /*uiDevice.findObject(By.pkg(packageName)) ?: break*/
            val found = uiDevice.wait(Until.hasObject(pkg), 15)
            if (!found) {
                break
            }
            Thread.sleep(250)
        }
    }

    /** 显示日志 */
    protected fun showLog(message: String?, error: Throwable? = null) {

        if(error != null){
            this.log.error(message, error)
        }
        else{
            this.log.info(message)
        }

        /*LogHelper.showLog(message, error)*/
    }

    /** 初始化设备参数  */
    protected fun initDevice() {

        val uiDevice = this.getUiDevice()

        uiDevice.freezeRotation()

        if (!uiDevice.isNaturalOrientation) {
            uiDevice.setOrientationNatural()
        }
    }
}

测试中显示 note

 /** 显示提示 */
        fun showNote(context: Context, message: String) {

            thread {
                Looper.prepare()

                Toast.makeText(context, message, Toast.LENGTH_LONG).show()

                val myLooper = Looper.myLooper()
                thread {
                    Thread.sleep(2000)
                    myLooper?.quitSafely()
                }

                Looper.loop()
            }
        }

kotlin-faker

模拟数据 https://serpro69.github.io/kotlin-faker/

图片生成 https://picsum.photos/

dependencies {
        implementation  'io.github.serpro69:kotlin-faker:1.7.1'
}

        /** 新版使用
        val fakerConfig = fakerConfig {
            locale = "zh-CN"
            random = java.util.Random(42) // 可选的随机数生成器
            uniqueGeneratorRetryLimit = 111 // 当生成唯一值时的重试次数
        }
        */


        // 已过时,请使用 fakerConfig
        val fakerConfig = FakerConfig.builder().create {
            locale = "zh-CN"
            random = java.util.Random(42) // 可选的随机数生成器
            uniqueGeneratorRetryLimit = 111 // 当生成唯一值时的重试次数
        }



        val faker = Faker(fakerConfig) 
        val names = (0..10).map { faker.animal.unique.name() } // 生成唯一值,但是可能会因为为了生成唯一值重复次数太多而失败
        val names = (0..100).map { faker.animal.name() } // 生成指定数量的随机值

Dagger 2 注入

https://jxiaow.gitee.io/posts/cbb172f8/

https://johnnyshieh.me/posts/dagger-advance/

https://yuweiguocn.github.io/dagger2-4/

引用

    ext {
        ext.dagger_version = '2.41'
    }

    api "com.google.dagger:dagger:$dagger_version"
    kapt "com.google.dagger:dagger-compiler:$dagger_version"

基础注入


// 1. 注入类的构造函数加入 @Inject 标志 
class Engine @Inject constructor() {
    var colors: String = "red"
}

// 3. 实现一个注入器
@Component
interface MachineComponent {
    fun inject(car: Car)
}

class Car {
    // 2. 待注入属性加入 @Inject 标志 
    @Inject
    lateinit var engine: Engine
}


// 4. 调用注入器进行注入 
val car = Car()
DaggerMachineComponent.create().inject(car)

自定义构造器 (Module 与 Provides)

// 如果注入类的构造需要一些自定义参数或第三方类库无法标志 @Inject
// 提供一个 Module 的注入类生成器, 在创建 Component 中标志 Module

/** 1.1 引擎 @Inject 标志构造函数 */
class Engine @Inject constructor() {
    var colors: String = "red"
}

/** 1.2 轮胎, 这里没用 @Inject  */
class Tyre constructor(val name: String, val num: Int) { }

/** 1.3 玻璃水, 这里没用 @Inject */
class GlassOfWater constructor(val name: String) { }

/** 2 @Module 标志构造器类,使用 @Provides 标志构造器函数以返回注入对象 */
@Module
class ModuleFactory(private val tyreName: String, private val waterName: String) {

    @Provides
    fun provideString(): Tyre {
        return Tyre(tyreName, 5) // 2.1 自定义生成注入对象 
    }

    @Provides
    fun GlassOfWater(): GlassOfWater {
        return GlassOfWater(waterName) // 2.2  自定义生成注入对象 
    }
}

// 3 @Component 标记注入器,并指定辅助使用的构造器,如果有多个,输入多个 modules
@Component(modules = [ModuleFactory::class])
interface MachineComponent {
    fun inject(car: Car)
}


class Car {

    // 4. 待注入属性加入 @Inject 标志 
    @Inject
    lateinit var engine: Engine

    // 4. 待注入属性加入 @Inject 标志 
    @Inject
    lateinit var glassOfWater: GlassOfWater

    // 4. 待注入属性加入 @Inject 标志 
    @Inject
    lateinit var tyre: Tyre
}


// 使用
val car = Car() // 被注入类

val builder = DaggerMachineComponent.builder()  // 构造注入器的构造器
builder.moduleFactory(ModuleFactory("韩泰", "龟牌")) // 设置构造器
val build = builder.build() // 构造注入器
build.inject(car) // 开始注入

// 链式调用 
DaggerMachineComponent.builder().moduleFactory(ModuleFactory("韩泰", "龟牌")).build().inject(car)
@Binds

与 @Provides 类似,作用于 @Module 标志的 接口 中,接收一个类型参数,然后返回该参数的函数上。我没有测试该功能

其它

延迟调用 Lazy

如果不想直接创建实例

class Car {
    // 支持注入一个 lazy 模式, 注意是 dagger.Lazy, 实现一个 get 函数
    @Inject    
    lateinit var engineLazy: dagger.Lazy<Engine>
}

// 调用, get (), 每次返回的是同一个实例,注意不是 Singleton 模式,每个新的 car 实例会有一个新的 lazy 实例
var engine1 = car.engineLazy.get()
var engine2 = car.engineLazy.get()
多次调用 Provider

如果多次调用进行优化


class Car {
    @Inject
    lateinit var  engineProvider: javax.inject.Provider<Engine>;
}

// 调用 
var engines =  (1..10).map { car.engineProvider.get() }
Module 区分 Provides

如果有多个 Provides 返回同一个实例,如何进行实例

Qualifier

定义属性(或是元数据)定义,然后标志在 Module 与 Inject 上

// 1. 定义一些标志 
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class  GlassOfWaterType1

// 1. 定义一些标志 
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class GlassOfWaterType2


@Module
class ModuleFactory(private val tyreName: String, private val waterName: String) {

    // 2. 标志以区分不同的工厂函数
    @GlassOfWaterType1
    @Provides
    fun GlassOfWater1(): GlassOfWater {
        return GlassOfWater(waterName)
    }

    // 2. 标志以区分不同的工厂函数
    @GlassOfWaterType2
    @Provides
    fun GlassOfWater2(): GlassOfWater {
        return GlassOfWater("${waterName}_xxxx")
    }
}

class Car {

    // 3. 注入时使用不同标志 
    @Inject
    @field:GlassOfWaterType1
    lateinit var glassOfWater1: GlassOfWater

    // 3. 注入时使用不同标志 
    @Inject
    @field:GlassOfWaterType2
    lateinit var glassOfWater2: GlassOfWater
}

Name

使用字符串来进行区分,后台其实调用了 Qualifier

比如下面 Module 中有两个 Provides 返回 GlassOfWater, 为了区分,使用 @Named 定义区分, 有时需要加入 @field:Named('') , 我实测,没有用 @field 也能成功


@Module
class ModuleFactory(private val tyreName: String, private val waterName: String) {

    @field:Named("Glass1")
    @Provides
    fun GlassOfWater1(): GlassOfWater {
        return GlassOfWater(waterName)
    }

    @field:Named("Glass2")
    @Provides
    fun GlassOfWater2(): GlassOfWater {
        return GlassOfWater("${waterName}_xxxx")
    }
}


// 调用方案 
class Car {
    // 使用 @Named 进行区分
    @Named("Glass1")
    @Inject
    lateinit var glassOfWater1: GlassOfWater

    // 使用 @Named 进行区分
    @Named("Glass2")
    @Inject
    lateinit var glassOfWater2: GlassOfWater
}

注入到 Set/Map

将 Module 中的多个同类 (或子类)返回值 注入至 set

Set

@Module
class ModuleFactory(private val tyreName: String, private val waterName: String) {

    @Provides
    @IntoSet // 注入至 set 返回类型或子类型
    fun GlassOfWater1(): GlassOfWater {
        return GlassOfWater(waterName)
    }

    @Provides
    @IntoSet // // 注入至 set 返回类型或子类型
    fun GlassOfWater2(): GlassOfWater {
        return GlassOfWater("${waterName}_xxxx")
    }

    @Provides
    @ElementsIntoSet // 返回一个 set, 可以为     return Collections.emptySet();
    fun GlassOfWaterSet(): Set<GlassOfWater> {
        return mutableSetOf(
            GlassOfWater("${waterName}_xxxx_set1"),
            GlassOfWater("${waterName}_xxxx_set2")
        )
    }
}

// 使用
class Car {
    // 注入到这里
    @Inject
    lateinit var GlassOfWaterSet: Set<GlassOfWater>
}
Map

@Module
class ModuleFactory(private val tyreName: String, private val waterName: String) {

    @Provides
    @IntoMap // 注入至 map ,key 使用 StringKey 指定
    @StringKey("water1")
    fun GlassOfWater1(): GlassOfWater {
        return GlassOfWater(waterName)
    }

    @Reusable
    /*@GlassOfWaterType2*/
    @Provides
    @IntoMap // 注入至 map ,key 使用 StringKey 指定
    @StringKey("water2")
    fun GlassOfWater2(): GlassOfWater {
        return GlassOfWater("${waterName}_xxxx")
    }
}

// 使用
class Car {
    // 注入到这里
    @Inject
    lateinit var GlassOfWaterMap: Map<String, GlassOfWater>
}
Scope 作用域

Scope 是元注解,只能标注目标类、@provide 方法和 @Component。并且 @Component 与 目标类或是 @provide 要一致

使用 Scope 后 Component 将拥有对应的注入对象的生命。并且 Component 中将只生成一次注入实例

Scope 作用域的本质:Component 间接持有依赖实例的引用,把实例的作用域与 Component 绑定

// 定义一个作用域
@Documented
@Retention(RUNTIME)
@Scope
public @interface MyScope {}

###### 唯一实例 Singleton

Singleton 作用域可以保证一个 Component 中的单例,但是如果产生多个 Component 实例,那么实例的单例就无法保证了。

其实就是一个自定义 Scope

// 1.2 在类中标志 
@Singleton
class Engine  @Inject constructor() {
    var colors: String = "red"
}

// 1.2 在构造工厂函数中标志 

@Module
class ModuleFactory(private val tyreName: String, private val waterName: String) {

    // 标志返回唯一实例
    @Singleton
    @Provides
    fun GlassOfWater2(): GlassOfWater {
        return GlassOfWater("${waterName}_xxxx")
    }
}

// 2. 注入器标志  Singleton
@Singleton 
@Component(modules = [ModuleFactory::class])
interface MachineComponent {
    fun inject(car: Car)
}


// 调用与其它没有什么区别 
val car1 = Car()
builder.build().inject(car1)
全局唯一实例 @Resuable

全局限制唯一实例,标记在类上或 @Provides 上。注意:在多线程情况下不会只生成一个实例。我简单测试时没有创建唯一实例.

https://jxiaow.gitee.io/posts/b74c4b6c/

https://juejin.cn/post/6911324538454147079

https://juejin.cn/post/6911324538454147079

@BindsInstance

在 @Component 中绑定一个实例,以使用或是注入。其实也可以在 Module 中保存一个实例并返回,使用 @BindsInstance 可以简化这个过程


@Component(modules = [ModuleFactory::class]) // 1. 根据需要这里指定一个工厂 modules
interface MachineBuilderComponent {

    // 1. 1 指定注入类
    fun inject(car: Car)

    // 1.2 设置 @Component.Builder 标志, 使用 Component.Builder 接管了 Component 自动生成的 build 与 modules 函数
    @Component.Builder
    interface Builder {

        // 2.1 必须返回 MachineBuilderComponent, 名称按约定为 build 
        fun build(): MachineBuilderComponent

        // 2.2 设置 modules 的设置函数, 注意这里没有使用 BindsInstance
        fun moduleFactory(factory: ModuleFactory): Builder

        // 2.3 使用 BindsInstance 设置要注入的实例
        @BindsInstance
        fun application(application: Application?): Builder

        // 2.3 使用 BindsInstance 设置要注入的实例
        @BindsInstance
        fun engine(engine: Engine): Builder
    }
}

// 使用 BindsInstance 标志后,注入的实例是唯一实例,实测会屏蔽掉 modules 中生成的相应实例,比如 Engine 实例 Module 中使用 @Provides 指定与 BindsInstance ,则 BindsInstance 传入的优先

测试

放在 Test 目录中的是 java 环境下的测试。比如  ```android.util.Log``` 就没有

放在 androidTest 目录中的会放在 android 中进行测试。可以使用  android 中环境。



```kotlin
// 比如 java 环境中没有 android.util.Log 可以模拟一个
package android.util;

object Log {
    fun d(tag: String, msg: String): Int {
        println("DEBUG: $tag: $msg")
        return 0
    }

    fun i(tag: String, msg: String): Int {
        println("INFO: $tag: $msg")
        return 0
    }

    fun w(tag: String, msg: String): Int {
        println("WARN: $tag: $msg")
        return 0
    }

    fun e(tag: String, msg: String): Int {
        println("ERROR: $tag: $msg")
        return 0
    } // add other methods if required...
}

事件

// 可以使用第三方 kotlin-events
// 使用 maven { url 'https://jitpack.io' } implementation 'com.github.stuhlmeier:kotlin-events:v2.0'

// 1. project 的 build.gradle 中添加 
allprojects {
    repositories {
        maven { url 'https://jitpack.io' } // 👈 加这一条
    }
}

// 2. module 的 build.gradle 中添加 
dependencies { implementation 'com.github.stuhlmeier:kotlin-events:v2.0' }

// 以全局方式使用
///////////////////////////////////////////////////////////////////////
val xEvent = event<XEvent>() // 声明事件
xEvent += { (sender, args) -> Log.i("Info", "事件发生:${sender::class}") } // 响应事件
xEvent(XEvent(this, XEventArgs("日志测试"))) // 引发事件


// 以类方式使用
///////////////////////////////////////////////////////////////////////
class LogDemo {
    // 定义事件
    val logEvent = event<XEvent>() 
    // 发送日志事件
    private fun sendLog(message: String, exception: Exception? = null) { logEvent(XEvent(this, XEventArgs(message, exception))) }
    // 引发日志事件
    fun raiseLog() { this.sendLog("我是日志") } 
}

// 使用
val logDemo = LogDemo()
logDemo.logEvent += { (sender, args) ->
    Log.i( "Info", "事件发生:${sender::class}, ${args.message}" )
}

logDemo.raiseLog()


🏝️👆👇👉👈  ☕️  👇👈👉👋👏👐👆☝👊✋✌✊👌👍👎

Kotlin 里那些「更方便的」 (rengwuxian.com)

Kotlin 的泛型 (rengwuxian.com)