Kotlin
Kotlin的历史
Kotlin由Jet Brains公司开发设计,2011年公布第一版,2012年开源。
2016年发布1.0正式版,并且Jet Brains在IDEA加入对Kotlin的支持,安卓自此又有新的选择。
2019年谷歌宣布Kotlin成为安卓第一开发语言,安卓程序员由java转Kotlin已经迫在眉睫。
Kotlin的工作原理
语言分为解释型和编译型两种
语言类型
编译型
编译器直接将源代码一次性编译成二进制文件,计算机可直接执行,例如C,C++。
优点:一次编译,即可运行,运行期不需要编译,运行效率高。
缺点:不同操作系统需要不同的机器码,且修改代码需要真个模块重新编译
解释型
程序运行时,解释器会将源码一行一行实时解析成二进制再执行。例如JS,Python。
优点:平台兼容性好,安装对应的虚拟机即可运行。
缺点:运行时需要解释执行,效率较低。
Java的语言类型
java准确来说属于混合型语言,但更偏向于解释型。
编译:java存在JIT和AOT,JIT即时编译将可将热点代码直接编译成机器码,AOT预先编译可再安装时把代码编译成机器码
解释:java运行时需编译成class文件,java虚拟机再解释执行.class。
Kotlin的运行原理
java虚拟机只认class文件, 虚拟机不会关心class时java文件编译来的,还是其他文件编译来的。那此时我们创造一套自己的语法规则,再做一个对应的编译器,,则可让我们的语言跑在java虚拟机上。Kotlin则是此原理,运行前会先编译成class,再供java虚拟机运行。
语法
变量
变量的声明
Kotlin使用var,val来声明变量,注意:Kotlin不再需要;来结尾
var 可变变量,对应java的非final变量
var b = 1
val不可变变量,对应java的final变量
val a = 1
两种变量并未声明类型,这是因为Kotlin存在类型推导机制,上述的a,b会默认为Int。假设想声明具体类型,则需下面的方式
var c: Int = 1
基本类型
Kotlin不再存在基本类型,将全部使用对象类型
Java基本类型 | Kotlin对象类型 | 对象类型说明 |
int | Int | 整型 |
long | Long | 长整型 |
short | Short | 短整型 |
float | Float | 单精度浮点型 |
double | Double | 双精度浮点型 |
boolean | Boolean | 布尔型 |
char | Char | 字符型 |
byte | Byte | 字节型 |
var和val的本质区别
Kotlin此设计的原因则是防止非final的滥用,若一个变量永远不被修改则有必要给其加上final,使其他人看代码时更好理解。
后期我们写代码时则可先使用val,若真的需要修改再改为var
无参无返回值
fun test() {
}
有参有返回值
参数的类型需要写在形参名后面中间使用:连接多个参数使用,分割",“返回值使用”:"拼接
fun add(a: Int, b: Int): Int {
return a + b
}
声明技巧
当函数体只有一行代码时可直接使用下面方式声明方法
fun add (a: Int, b: Int): Int = a + b
Kotlin存在类型推导,返回值类型也可省略
fun add (a: Int, b: Int) = a + b
函数的调用
fun main() {
test()
print(add(1, 2))
}
//运行结果
//test
//3
if语句
Kotlin中的选择控制有两种方式。if和when
与Java的if区别不大,实现一个返回最大值的函数
fun max(a: Int, b: Int): Int {
if (a > b) return a
else return b
}
Kotlin的if可以包含返回值,if语句的最后一行会作为返回值返回
fun max(a: Int, b: Int): Int {
return if (a > b) a else b
}
上述我们说过一行代码可省略返回值
fun max(a: Int, b: Int) = if (a > b) a else b
查看对应的Java文件,其上述实现都与下面代码等价
public static final int max(int a, int b) {
return a > b ? a : b;
}
when
实现一个查询成绩的函数,用户传入名字,返回成绩级别
if实现
Kotlin的if语句必须要有else,不然会报错
fun getScore(name: String) = if (name == "Tom") "不及格"
else if (name == "Jim") "及格"
else if (name == "Pony") "良好"
else if (name == "Tony") "优秀"
else "名字非法"
Kotlin中==等价于Java的equals比较的时是对象里的内容, === 等价于Java的==,比较的为对象的引用。
when实现
也必须实现else,否则报错
fun getScore(name: String) = when(name) {
"Tom" -> "不及格"
"Jim" -> "及格"
"Pony" -> "良好"
"Tony" -> "优秀"
else -> "名字非法"
}
when支持参数检查
fun checkNumber(num: Number) {
when (num) {
is Int -> println("Int")
is Double -> println("Double")
else -> println("others")
}
}
when也可不传递形参
使用Boolean使when更加灵活
fun getScore(name: String) = when {
name == "Tom" -> "不及格"
name == "Jim" -> "及格"
name == "Pony" -> "良好"
name == "Tony" -> "优秀"
else -> "名字非法"
}
-> 后不仅可以只执行一行代码,可以多行,看一个比较复杂的例子:
fun getScore(name: String) = when {
//若name以Tom开头则命中此分支
name.startsWith("Tom") -> {
//处理
println("你好,我是Tom开头的同学")
"不及格"
}
name == "Jim" -> "及格"
name == "Pony" -> "良好"
name == "Tony" -> "优秀"
else -> "名字非法"
}
循环语句
Kotlin有两种循环方式,while和for-in,while与java中的while没有区别,for-in是对Java for-each的加强,Kotlin舍弃了for-i的写法
while不再赘述,在学习for-in之前需要明确一个概念-区间
val range = 0..10 //区间代表[0,10]
for-in需借助区间来使用
fun main() {
val range = 0..10
for (i in range) { //也可直接for (i in 0..10)
println(i)
}
//输出结果为 从0打印到10
}
0..10 代表双闭区间,如果想使用左闭右开呢,需要借助until关键字
fun main() {
for (i in 0 until 10) {
println(i)
}
//输出结果为 从0打印到9
}
上述实现是逐步进行相当于i++,Kotlin也支持跳步
fun main() {
for (i in 0 until 10 step 2) {
println(i)
}
//输出结果为0,2,4,6,8
}
上述实现都是升序,Kotlin也可降序循环
fun main() {
for (i in 10 downTo 1) {
println(i)
}
//输出结果为10 - 1
}
for-in不仅可对区间进行遍历,还可对集合进行遍历,后续在集合处进行展示。
创建Person类,并声明name,age,创建printInfo方法
class Person {
var name = ""
var age = 0
fun printInfo() {
println(name +"'s age is " + age)
}
}
在main方法中声明一个Person对象并调用printInfo方法
fun main() {
val person = Person()
person.name = "zjm"
person.age = 20
person.printInfo()
}
//结果如下zjm's age is 20
继承
声明Student类继承Person,Kotlin中继承使用**:**,后接父类的构造,为什么需要构造后续讲解
class Student : Person(){ //此时Person报错
var number = ""
var grade = 0
fun study() {
println(name + "is studying")
}
}
Person类当前不可继承,查看Person对应的java文件
public final class Person {
...
}
Person类为final不可被继承,因此需借助open关键字
只需在Person类前加上open
open class Person {
...
}
此时Person的java文件变为
public class Person {
...
}
此时Student将不再报错
构造分为主构造和此构造
主构造
主构造直接写在类后面
修改Student类
class Student(val number: String, val grade: Int) : Person(){
...
}
在创建Student对象时,如下创建
val student = Student("1234", 90)
因之前Person还有name和age,下面修改Person类的主构造
open class Person(val name: String, val age: Int) {
...
}
此时Student报错,因为继承Person时,后边使用的是Person()无参构造,上面我们修改了Person的构造,则不存在无参构造了。
再修改Student
class Student(name: String, age: Int, val number: String, val grade: Int) : Person(name, age){
...
}
此时不在报错,声明方式如下
val student = Student("zjm", 20, "1234", 90)
在构造时需要进行特殊处理怎么办,Kotlin提供了init结构体,主构造的逻辑可在init中处理
open class Person(val name: String, val age: Int) {
init {
println("name is" + name)
println("age is" + age)
}
}
上述修改都为主构造,那如果类想有多个构造怎么办,此时需借助次构造
次构造
此时实现Student的另外两个构造
三个参数的构造,name,age,number,grade不传参默认为``0
无参构造,字符串默认为"",int默认为0
class Student(name: String, age: Int, val number: String, val grade: Int) : Person(name, age){
constructor(name: String, age: Int, number: String) : this(name, age, number, 0) {
}
constructor() : this("", 0, "", 0) {
}
...
}
创建如下:
fun main() {
val student1 = Student("zjm", 20, "123", 90)
val student2 = Student("zjm", 20, "123")
val student3 = Student()
}
无主构造
若类不使用主构造,则后续继承类也不需要使用构造即可去掉继承类的(),次构造可以调用父类构造super进行初始化,但是次构造的参数在其他地方无法引用
class Student : Person {
constructor(name: String, age: Int, number: String) : super(name, age) {
}
fun study() {
//name,age可使用
println(name + "is studying")
//使用number则会报错,若number是主构造的参数则可引用
//println(number) 报红
}
}
接口
和Java中的接口定义类似
interface Study {
fun study()
fun readBooks()
fun doHomework()
}
接口的继承
继承接口只需在后用","拼接,需实现Study声明的全部函数
class Student(name: String, age: Int, val number: String, val grade: Int) : Person(name, age), Study{
...
override fun study() {
TODO("Not yet implemented")
}
override fun readBooks() {
TODO("Not yet implemented")
}
override fun doHomework() {
TODO("Not yet implemented")
}
}
Kotlin支持接口方法的默认实现,JDK1.8以后也支持此功能,方法有默认实现则继承类无需必须实现此方法
interface Study {
fun study() {
println("study")
}
fun readBooks()
fun doHomework()
}
权限修饰符
Java和Kotlin的不同如下表所示:
修饰符 | Java | Kotlin |
public | 所有类可见 | 所有类可见(默认) |
private | 当前类可见 | 当前类可见 |
protected | 当前类,子类,同包下类可见 | 当前类,子类可见 |
default | 同包下类可见(默认) | 无 |
internal | 无 | 同模块下的类可见 |
Kotlin引入internal,摒弃了default
使用:
类上
public open class Person(val name: String, val age: Int){...}
变量上
private val value = 1
方法上
private fun test() {
}
数据类和单例类
数据类
数据类则只处理数据相关,与Java Bean类似,通常需要实现其get,set,hashCode,equals,toString等方法
下面实现UserBean,包含id,name,pwd属性
Java编写入如下:
public class UserBean {
private String id;
private String name;
private String pwd;
public UserBean() {
}
public UserBean(String id, String name, String pwd) {
this.id = id;
this.name = name;
this.pwd = pwd;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserBean userBean = (UserBean) o;
return Objects.equals(id, userBean.id) && Objects.equals(name, userBean.name) && Objects.equals(pwd, userBean.pwd);
}
@Override
public int hashCode() {
return Objects.hash(id, name, pwd);
}
@Override
public String toString() {
return "UserBean{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", pwd='" + pwd + '\'' +
'}';
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
}
一行代码即可搞定,Kotlin会自动实现上述方法。
data class UserBean(val id: String, val name: String, val pwd: String)
若无data关键字,上述方法(hashCode,equals,toString)无法正常运行,去掉data查看Kotlin对应的java文件:
public final class UserBean {
@NotNull
private final String id;
@NotNull
private final String name;
@NotNull
private final String pwd;
@NotNull
public final String getId() {
return this.id;
}
@NotNull
public final String getName() {
return this.name;
}
@NotNull
public final String getPwd() {
return this.pwd;
}
public UserBean(@NotNull String id, @NotNull String name, @NotNull String pwd) {
Intrinsics.checkNotNullParameter(id, "id");
Intrinsics.checkNotNullParameter(name, "name");
Intrinsics.checkNotNullParameter(pwd, "pwd");
super();
this.id = id;
this.name = name;
this.pwd = pwd;
}
}
发现上面代码既无hashCode,equals,toString也无set
加上data且把变量改为var,对应的java文件如下:
public final class UserBean {
@NotNull
private String id;
@NotNull
private String name;
@NotNull
private String pwd;
@NotNull
public final String getId() {
return this.id;
}
public final void setId(@NotNull String var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
this.id = var1;
}
@NotNull
public final String getName() {
return this.name;
}
public final void setName(@NotNull String var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
this.name = var1;
}
@NotNull
public final String getPwd() {
return this.pwd;
}
public final void setPwd(@NotNull String var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
this.pwd = var1;
}
public UserBean(@NotNull String id, @NotNull String name, @NotNull String pwd) {
Intrinsics.checkNotNullParameter(id, "id");
Intrinsics.checkNotNullParameter(name, "name");
Intrinsics.checkNotNullParameter(pwd, "pwd");
super();
this.id = id;
this.name = name;
this.pwd = pwd;
}
@NotNull
public final String component1() {
return this.id;
}
@NotNull
public final String component2() {
return this.name;
}
@NotNull
public final String component3() {
return this.pwd;
}
@NotNull
public final UserBean copy(@NotNull String id, @NotNull String name, @NotNull String pwd) {
Intrinsics.checkNotNullParameter(id, "id");
Intrinsics.checkNotNullParameter(name, "name");
Intrinsics.checkNotNullParameter(pwd, "pwd");
return new UserBean(id, name, pwd);
}
// $FF: synthetic method
public static UserBean copy$default(UserBean var0, String var1, String var2, String var3, int var4, Object var5) {
if ((var4 & 1) != 0) {
var1 = var0.id;
}
if ((var4 & 2) != 0) {
var2 = var0.name;
}
if ((var4 & 4) != 0) {
var3 = var0.pwd;
}
return var0.copy(var1, var2, var3);
}
@NotNull
public String toString() {
return "UserBean(id=" + this.id + ", name=" + this.name + ", pwd=" + this.pwd + ")";
}
public int hashCode() {
String var10000 = this.id;
int var1 = (var10000 != null ? var10000.hashCode() : 0) * 31;
String var10001 = this.name;
var1 = (var1 + (var10001 != null ? var10001.hashCode() : 0)) * 31;
var10001 = this.pwd;
return var1 + (var10001 != null ? var10001.hashCode() : 0);
}
public boolean equals(@Nullable Object var1) {
if (this != var1) {
if (var1 instanceof UserBean) {
UserBean var2 = (UserBean)var1;
if (Intrinsics.areEqual(this.id, var2.id) && Intrinsics.areEqual(this.name, var2.name) && Intrinsics.areEqual(this.pwd, var2.pwd)) {
return true;
}
}
return false;
} else {
return true;
}
}
}
此时则和手动编写的java bean功能一样了,所有方法都可正常运行
单例类
目前Java使用最广的单例模式的实现如下:
public class Singleton {
private Singleton() {
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
public void test() {
...
}
}
生成代码如下:
object Singleton {
fun test() {
...
}
}
其对应的java文件如下,和上述使用最多的java单例实现类似
public final class Singleton {
@NotNull
public static final Singleton INSTANCE;
public final void test() {
}
private Singleton() {
}
static {
Singleton var0 = new Singleton();
INSTANCE = var0;
}
}
使用如下:
fun main() {
Singleton.test() //对应的java代码为Singleton.INSTANCE.test();
}
Lambda
许多高级语言都支持Lambda,java在jdk1.8以后才支持Lamda语法,Lamda是Kotlin的灵魂所在,此小节对Lambda的基础进行学习,并借助集合练习。
List
fun main() {
//常规创建
val list = ArrayList<Int>()
list.add(1)
list.add(2)
list.add(3)
//listOf不可变,后续不可添加删除,只能查
val list1 = listOf<Int>(1, 2, 3 ,4 ,5)
list1.add(6)//报错
//mutableListOf,后续可添加删除
val list2 = mutableListOf<Int>(1, 2, 3 ,4 ,5)
list2.add(6)
//循环
for (value in list2) {
println(value)
}
}
Set
set用法与List类似,只是把listOf替换为mapOf
Map
fun main() {
val map = HashMap<String, String>()
map.put("1", "zjm")
map.put("2", "ljn")
//Kotlin中map支持类似下标的赋值和访问
map["3"] = "lsb"
map["4"] = "lyx"
println(map["2"])
println(map.get("1"))
//不可变
val map1 = mapOf<String, String>("1" to "zjm", "2" to "ljn")
map1["3"] = "lsb" //报错
//可变
val map2 = mutableMapOf<String, String>("1" to "zjm", "2" to "ljn")
map2["3"] = "lsb"
for ((key, value) in map) {
println(key + " " + value)
}
}
Lambda的使用
方法在传递参数时都是普通变量,而Lambda可以传递一段代码
Lambda表达式的语法结构
{参数名1: 参数类型, 参数名2:参数类型 -> 函数体}
Kotlin的list提供了maxByOrNull函数,返回当前list中xx最大的元素,XX是我们定义的条件,可能为长度,可能是别的,我们拿长度举例。
若不使用maxBy,实现如下
fun main() {
val list = listOf<String>("a", "aba", "aabb", "a")
var maxStr = ""
for (str in list) {
if (str.length > maxStr.length) {
maxStr = str;
}
}
println(maxStr)
}
maxByOrNull是一个普通方法,需要一个Lambda参数,下面结合Lambda使用maxByOrNull
fun main() {
val list = listOf<String>("a", "aba", "aabb", "a")
var lambda = {str: String -> str.length}
var maxStr = list.maxByOrNull(lambda)
println(maxStr)
}
直接当成参数也可传递
var maxStr = list.maxByOrNull({str: String -> str.length})
若Lambda为方法的最后一个参数,则可将{}提到外面
var maxStr = list.maxByOrNull() {str: String -> str.length}
若有且仅有一个参数且是Lambda,则可去掉()
var maxStr = list.maxByOrNull {str: String -> str.length}
Kotlin拥有出色的类型推导机制,Lambda参数过多时可省略参数类型
var maxStr = list.maxByOrNull {str -> str.length}
若Lambda只有一个参数,则可用it替代参数名
var maxStr = list.maxByOrNull {it.length}
集合还有许多此类函数
创建list,后续操作都由此list转换
val list = listOf<String>("a", "aba", "aabb", "a")
map 映射,返回新集合,将集合中的元素映射成另一个值
val newList = list.map { it.toUpperCase() }//将集合中的元素都准换成大写
filter过滤,返回新集合,将集合中的元素进行筛选
val newList = list.filter { it.length > 3 }//筛选出长度大于3的元素
any返回Boolean,集合中是否存在元素满足Lambda的条件,有则返回true,无则false
val isAny = list.any {it.length > 10} //返回false
all返回Boolean,集合中元素是否全部满足满足Lambda的条件,有则返回true,无则false
val isAll = list.all {it.length > 0} //返回true
Lambda的简单使用到这就结束了
Java函数式API的使用
Kotlin调用Java方法,若该方法接收一个Java单抽象方法接口参数,则可使用函数式API。Java单抽象方法接口指的是接口只声明一个方法,若有多个方法则无法使用函数式API。
Java单抽象方法接口例如Runnable
public interface Runnable {
void run();
}
在Java中启动一个线程如下:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("test");
}
}).start();
Kotlin启动线程如下:
Kotlin摒弃了new,若想声明匿名内部类必须使用object
Thread(object : Runnable {
override fun run() {
println("test")
}
}).start()
因Runnable是Java单抽象方法接口,可对代码进行简化
Thread(Runnable {
println("test")
}).start()
Runnable接口只用一个方法,使用Lambda也不会有歧义,Kotlin知道此Lambda一定实现的为run函数,借用Lambda进一步简化:
Thread({
println("test")
}).start()
又因Thread只需一个参数Runnable参数,则可省略()
Thread {
println("test")
}.start()
与上类似的,click也使用上述方法
button.setOnClickListener { println("test") }
这种方式可极大缩减代码量
空指针检查机制
国外统计程序出现最多的异常为空指针异常,Kotlin存在编译时检查系统帮助我们发现空指针异常。
查看下面Java代码
public void doStudy(Study study) {
study.doHomework();
study.readBooks();
}
上述代码时存在空指针风险的,传入null,则程序崩溃,对其进行改进
public void doStudy(Study study) {
if (study != null) {
study.doHomework();
study.readBooks();
}
}
对于Kotlin来讲任何参数和变量不能为空
fun study(study: Study) {
study.doHomework()
study.readBooks()
}
fun main() {
study(null) //报错
study(Student()) //正确
}
Kotlin把空指针异常的检查提前到了编译期,若空指针则编译期就会崩溃,避免在运行期出现问题
若我们有特殊的需求可能需要传递null参数,参数则按照下面声明
fun study(study: Study?) {
study.doHomework() //报错
study.readBooks() //报错
}
?的意思则是当前参数可为空,如果可为空的话,则此对象调用的方法必须要保证对象不为空,上面代码没有保证,则报错,修改如下
fun study(study: Study?) {
if (study != null) {
study.doHomework()
study.readBooks()
}
}
也可借助判空辅助工具
?.
其含义是?前面对象不为空才执行.后面的方法
fun study(study: Study?) {
study?.doHomework()
study?.readBooks()
}
?:
其含义是?前不为空则返回问号前的值,为空则返回:后的值
比如
val c = if (a !=null ) {
a
} else {
b
}
借助?:则可简化为
val c = a ?: b
再比如
fun getTextLength(text: String?): Int {
if (text != null) {
return text.length
}
return 0
}
借助?: 则可简化为
fun getTextLength(text: String?) = text?.length ?: 0
!!
有些时候我们想要强行通过编译,就需要依靠!!,这时就是程序员来保证安全
fun study(study: Study?) {
//假设此时为空抛出异常,则和java一样
study!!.doHomework()
study!!.readBooks()
}
let函数
let不是关键字,而是一个函数,提供了函数式API的编程接口,会将调用者作为参数传递到Lambda表达式,调用之后会立马执行Lambda表达式的逻辑
obj.let { it -> //it就是obj
//编写操作
}
比如上面函数
fun study(study: Study?) {
study.doHomework() //报错
study.readBooks() //报错
}
借助let则可改为
fun study(study: Study?) {
//此时靠?.则保证了study肯定不为空,才会执行let函数
study?.let {
//it为study
it.doHomework()
it.readBooks()
}
}
全局判空注意事项
//全局变量
var study: Study? = null
fun study() {
//报错
if (study != null) {
study.readBooks()
study.doHomework()
}
}
因全局变量随时有可能被其他线程修改,即使判空处理也不能保证其没有空指针风险,而let则可规避上述问题
var study: Study? = null
fun study() {
study?.let {
it.doHomework()
it.readBooks()
}
}
内嵌表达式
之前我们拼接字符串都是下面这样
var name = "zjm"
var age = 20
println("My name is " + name + ". I am " + age + ".")
//打印结果
//My name is zjm. I am 20.
现在靠着Kotlin提供的内嵌表达式则不需要拼接,只需要下面这样则可实现
var name = "zjm"
var age = 20
println("My name is $name. I am $age." )
//打印结果
//My name is zjm. I am 20.
内嵌表达式还支持复杂的操作
${程序员想要的操作}
var name = "zjm"
var age = 20
println("My name is ${if (1 < 2) "zjm" else "ljn"}. I am $age." )
//打印结果
//My name is zjm. I am 20.
函数的参数默认值
Kotlin支持函数存在默认值,使用如下
fun main() {
myPrint(1)
myPrint(1, "lalala")
}
fun myPrint(value: Int, str: String = "hello") {
println("num is $value, str is $str")
}
//结果如下
//num is 1, str is hello
//num is 1, str is lalala
若value想为默认值,则会报错,因为在使用时传入的第一个参数他认为是int的,传入字符串会类型不匹配
fun main() {
myPrint("zjm")//报错
}
fun myPrint(value: Int = 100, str: String) {
println("num is $value, str is $str")
}
Kotlin提供了一种键值对传参来解决上述问题
fun main() {
myPrint(str = "zjm") //正确调用
}
fun myPrint(value: Int = 100, str: String) {
println("num is $value, str is $str")
}
回顾之前的主次构造,Student如下
class Student(name: String, age: Int, val number: String, val grade: Int) : Person(name, age){
constructor(name: String, age: Int, number: String) : this(name, age, number, 0) {
}
...
}
上述的此构造借助参数默认值技巧是可以不写的,将第四个参数默认值为0 即可
class Student(name: String, age: Int, val number: String, val grade: Int = 0) : Person(name, age){
...
}
一文快速入门 Kotlin 协程 - 掘金 (juejin.cn)
Compose
Compose 分享 · 语雀
Jetpack Compose 是用于构建原生 Android 界面的新工具包。
它使用更少的代码、强大的工具和直观的 Kotlin APl,可以帮助您简化并加快 Android 界面开发,打造生动而精彩的应用。
它可让您更快速、更轻松地构建 Android 界面
为何要选择 Compose
很多 Android 开发都会问:View 已经这么成熟了,为何我要引入 Compose?
历史也总是惊人的相似,React 横空出世时,很多前端同学也会问:jQuery 已经如此强大了,为何要引入 JSX、Virtual DOM?
争论总是无效的,时间会慢慢证明谁才会成为真正的主宰。
现在的前端同学,可能连 jQuery 是什么都不知道了。其作为曾经前端的主宰,何其强大,却也经受不住来自 React 的降维打击。回看这端历史,那我们选择 Compose 就显得很自然了。
另一个大趋势是 Kotlin 跨平台的逐渐兴起与成熟,也会推动 Compose 成为 Fultter 之外的选择,而且可以不用学习那除了写 Flutter 就完全没用的 Dart 语言。
但是,我也不推荐大家随随便便就把 Compose 接入的项目中。因为,国内的开发现状就是那样,迭代速度要求快,但是也要追求稳定。而接入 Compose 到使用 Compose 快速迭代,也是有一个痛苦的过程的,搞不好就要背锅,现在这环境,背锅可能就代表被裁了。
所以目前 Compose 依旧只能作为简历亮点而非必备点。可是如果你不学,万一被要求是必备点,那该怎么办?
所以即使你不喜欢 Compose 这一套,那为了饭碗,该掌握的还是得掌握,毕竟市场饱和,我们是被挑选的哪一方。
Compose 的思想
声明式 UI
Compose 的思想与 React、View、Fultter、SwiftUI 都是一脉相传,那就是数据驱动 UI 与 声明式 UI。以前的 View 体系,我们称它为命令 UI。
命令式 UI 是我们拿到 View 的句柄,然后通过执行命令,主动更新它的的颜色、文字等等
声明式 UI 则是我们构建一个状态机,描述各个状态下 UI 是个什么样子的。
那些写 Compose 怎么都不顺手的童鞋,就是总想拿 View 的句柄,但又拿不到,所以就很痛苦,但如果转换到状态机的思维上,去定义各种情景的状态,那写起来就非常舒服了。
Compose 从 View 体系进化的点就是它贴近于真实的 UI 世界。因为每个界面就是一个复杂的状态机,以往我们命令式的操作,我们依旧要定义一套状态系统,某种状态更新为某种 UI,有时候处理得不好,还会出现状态错乱的问题。 Compose 则强制我们要思考 UI 的状态机该是怎样子的。
Virtual DOM
在 Compose 的世界中,是没有介绍 Virtual DOM 这一概念的,但我觉得理解 Virtual DOM 能够帮助我们更好的理解 Compose。 Virtual DOM 的诞生,一个原因是因为 DOM/View 节点实在是太重了,所以我们不能在数据变更时删除这个节点再重新创建,我们也不没有办法通过 diff 的方式去追踪到底发生了哪些变更。但大佬们的思维就比较活跃,因为开发过程中关注的一个 DOM/ View 的属性是很少的,所以就创造了一个轻量级的数据结构来表示一个 DOM/View 节点,由于数据结构比较轻量,那么销毁创建就可以随意点。每次更新状态,我可以用新状态去创造一个新的 Virtual DOM Tree, 然后与旧的 Virtual DOM Tree 进行 diff,然后将 diff 的结果更新到 DOM / View 上去, React Native 就是把前端的 DOM 变成移动端的 View,因而开启了 UI 跨平台动态化的大门。
那这和 Compose 有什么关系呢?我们可以认为,Compose 的函数让我们来生成 Virtual DOM 树,Compose 内部叫 SlotTable,框架用了全新的内部结构来代表 DOM 节点。每次我们状态的变更,就会触发 Composable 函数重新执行以生成新的 Virtual DOM,这个过程叫做 Recomposition。
所以重点来了,发生状态更新后,框架会首先去重新生成 Virtual DOM 树,交给底层去比对变更,最终渲染输出。如果我们频繁的变更状态,那就会频繁的触发 Recomposition,如果每次还是重新生成一个巨大的 Virtual DOM 树,那框架内部的 diff 就会非常耗时,那么性能问题随之就来了,这是很多同学用 Compose 写出的代码卡顿的原因。
Compose 性能最佳实践
如果我们有了 Virtual DOM 这一层认识,那么就能够想到该怎样去保持 Compose 的高性能了,那就是
1.减少 Composable 函数自身的计算
2.减小状态变更的频次
3.减小状态变更的造成 Recomposition 的范围以减小 diff 更新量
4.减小 Recomposition 时的变更量以减小 diff 更新量
减少 Composable 函数自身的计算
这个很好理解,如果 Recomposition 发生了,那么整个函数就会重新执行,如果有复杂的计算逻辑,那就会造成函数本身的消耗很大,而解决措施也简单,就是通过 remember 缓存计算结果
@Composable
func Test(){
val ret = remember(arg1, arg2) { // 通过参数判断是否要重新计算
// 复杂的计算逻辑
}
}
减少状态变更的频次
这个主要是减少无效的状态变更,如果有多个状态,其每个状态下的执行结果是一样的,那这些状态间的变更就没有意义了,应该统一成唯一的状态。
其实官方在 mutableStateOf 的入参 policy 上已经定制了几种判断状态值是否变更的策略:
StructuralEqualityPolicy: 通过值判等(==)的来看其是否发生变更
ReferentialEqualityPolicy: 必须是同一个对象(===)才算未发生变更
NeverEqualPolicy : 总是触发状态变更
默认为 StructuralEqualityPolicy,也符合一般情况的要求。
除此之外,我们减小状态变更频率的手段就是 derivedStateOf。 它的用途主要是我们就是将多个状态值收归为统一的状态值, 例如:
1.列表是否滚动到了顶部,我们拿到的 scorllY 是很频繁变更的值,但我们关注的只是 scorllY == 0
2.根据内容为空判定发送按钮是否可点击,我们关注的是 input.isNotBlank()
3.多个输入的联合校验
4…
我们以发送按钮为例:
@Composable
func Test(){
val input = remember {
mutabtleStateOf('')
}
val canSend = remember {
derivedStateOf { input.value.isNotBlank() }
}
// 使用 canSend
SendButton(canSend)
// 其它很多代码
}
这样子,我们可以多次更新 input 的值,但是只有当 canSend 发生变更时才会触发 Test 的 Recomposition。
减小状态变更的造成 Recomposition 的范围
Recomposition 是以函数为作用范围的,所以某个状态触发了 Recomposition,那么这个函数就会重新执行一次。但需要注意的是,不是状态定义的函数执行Recomposition,而是状态读取的函数会触发 Recomposition。
还是以上面的输入的例子为例。 如果我在 Test 函数执行期内读取了 input.value, 那么 input 变更时就会触发 Test 函数的重组。注意的是函数执行期内读取,而不是函数代码里写了 input.value。上面 canSend 的 derivedStateOf 虽然也有调用 input.value,但因为它是以 lambda 的形式存在,不是会在执行 Test 函数时就执行,所以不会因为 input.value 变更就造成 Test 的 Recomposition。
但如果我在函数体内使用 input.value,例如:
@Composable
func Test(){
val input = remember {
mutabtleStateOf('')
}
val canSend = remember {
derivedStateOf { input.value.isNotBlank() }
}
Text(input.value)
SendButton(canSend)
OtherCode(arg1, arg2)
OtherCode1(arg1, arg2)
}
那就会因为 input 的变更而造成 Test 的重组, canSend 使用 derivedStateOf 也就是做无用功了。更严重的是可能有很多其它与 input 无关的代码也会再次执行。
所以我们需要把状态变更触发 Recomposition 的代码用一个子组件来承载:
@Composable
func InputText(input: () -> String){
Text(input())
}
@Composable
func Test(){
val input = remember {
mutabtleStateOf('')
}
val canSend = remember {
derivedStateOf { input.value.isNotBlank() }
}
InputText {
input.value
}
SendButton(canSend)
OtherCode(arg1, arg2)
OtherCode1(arg1, arg2)
}
我们重新创建了一个 InputText 函数,然后通过 lambda 的形式传递 input,因而现在 input 变更造成的 Recomposition 就局限于 InputText 了,而其它的无关代码就不会被执行,这样范围就大大缩减了。
减小 Recomposition 时的变更量
加入我们的函数 Recomposition 的范围已经没办法缩减了,例如上面 canSend 变更触发 Test 的 Recomposition,这造成 OtherCode 组件的重新执行好像无法避免了。其实官方也想到了这种情况,所以它框架还会判断 OtherCode 的参数是否发生了变更,依此来判断 OtherCode 函数是否需要重新执行。如果参数没有变更,那么就可以开心的跳过它,那么 Recomposition 的变更量就大幅减小了。
那么怎么判断参数没有发生变更呢?如果是基础类型和data class 等的数据结果还好,可以通过值判等的形式看其是否变更。但如果是列表或者自定义的数据结构就麻烦了。 因为框架无法知道其内部是否发生了变更。
以 a: List 为例,虽然重组时我拿到的是同一个对象 a, 但其实现类可能是 ArraryList, 并且可能调用 add/remove 等方法变更了数据结构。所以在保证正确性优先的情况下,框架只得重新调用整个函数。
@Composable
fun SubTest(a: List<String>){
//...
}
@Composable
fun Test(){
val input = remember {
mutabtleStateOf('')
}
val a = remember {
mutableStateOf(ArrayList<String>())
}
// 因为读取了 input.value, 所以每次 input 变更,都会早成 Test 的 Recomposition
Test(input.value)
// 而因为 a 是个 List,所以每次 SubTest 也会执行 Recomposition
SubTest(a)
}
那要怎么规避这个问题呢? 那就是使用 kotlinx-collections-immutable 提供的 ImmutableList 等数据结构,如此就可以帮助框架正确的判断数据是否发生了变更。
@Composable
fun SubTest(a: PersistentList<String>){
//...
}
@Composable
fun Test(){
val input = remember {
mutabtleStateOf('')
}
val a = remember {
mutableStateOf(persistentListOf<String>())
}
// 因为读取了 input.value, 所以每次 input 变更,都会早成 Test 的 Recomposition
Test(input.value)
// 而因为 a 是个 List,所以每次 SubTest 也会执行 Recomposition
SubTest(a)
}
而如果是我们自己定义的数据结构,如果是非 data class,那就要我们主动加上 @Stable 注解,告诉框架这个数据结构是不会发生变更,或者其变更我们都会用状态机去处理的。特别需要注意的是使用 java 作为实体类而给 compose 使用的情况,那就是非常不友好了。
对于列表而言,我们往往需要用 for 循环或者 LazyColumn 之类的方式使用:
@Composable
fun SubTest(list: PersistentList<ItemData>){
for(item in list){
Item(item)
}
}
这个写法,如果 list 不会变更,那也没什么问题,可是如果列表发生了变更,例如原本是 12345, 我删了一项变成 1345。
那么在 Recomposition 的时候,框架在比对变更时,发现从第二项开始就全不同了,那么剩下的 Item 就得全部重新重组一次了,这也是非常耗费性能的,所以框架提供了 key 的功能,通过它,框架可以检测列表的 Item 移动的情况。
@Composable
fun SubTest(list: PersistentList<ItemData>){
for(item in list){
key(item.id){
Item(item)
}
}
}
不过需要注意的是 key 需要具有唯一性。 LazyColumn 的 item 也有 key 的功能,其作用类似,其还有 contentType 的传参,其作用和 RecyclerView 的多 itemType 类似,也是一个可以使用的优化措施。
最后
Compose 业务上能做的优化大体上就是这些了。总之我们就是我们要保持组件的颗粒度尽可能的小,容易变动的要独立出来,非常稳定的也要独立出来,尽量使用 Immutable 的数据结构。 如此之后, Compose 的流畅度还是非常不错的。
如果还觉得卡,那多半是因为你使用的是 Debug 包,Compose 会在 Debug 包加很多调试信息,会很影响其流畅度的。切换到 Release 包,可能丝滑感就出来了。
现代化 Android 开发:Jetpack Compose 最佳实践_安卓 compose-CSDN博客
Compose 初上手
注解
@Compose 所有的组合函数都必须添加 @Compose 注解才可以。 被 @Compose 注解的方法只能被同类型的方法调用。
@Preview 使用该注解的方法可以不在运行 App 的情况下就可以查看布局。@Preview 中常用的参数如下:
name: String: 为该Preview命名,该名字会在布局预览中显示。
showBackground: Boolean: 是否显示背景,true为显示。
backgroundColor: Long: 设置背景的颜色。
showDecoration: Boolean: 是否显示Statusbar和Toolbar,true为显示。
group: String: 为该Preview设置group名字,可以在UI中以group为单位显示。
fontScale: Float: 可以在预览中对字体放大,范围是从0.01。
widthDp: Int: 在Compose中渲染的最大宽度,单位为dp。
heightDp: Int: 在Compose中渲染的最大高度,单位为dp。
申明性编程范式
长期以来,android 的视图结构一直可以表示为界面微件数。由于应用的状态会因用户交互等因素而发生变化,因此界面层次结构需要进行更新以显示当前的数据,最常见的就是 findviewById 等函数遍历树,并调用设置数据的方法等改变节点,这些方法会改变微件的内部状态
再过去的几年中,整个行业已经转向声明性界面模型,该模型大大的简化了构建和更新界面管理的工程设计,改技术的工作原理是在改建上重头生成整个屏幕,然后执行必要的更改。此方法可以避免手动更新有状态视图结构的复杂性。Compose 是一个声明性的界面框架。
重新生成整个屏幕所面临的一个难题是,在时间,计算力和电量方面可能成本高昂,为了减轻这一成本,Compose 会智能的选择在任何时间需要重新绘制界面的那些部分。这回对设计界面的组件有一定影响。
组合函数
Jetpack Compose 是围绕可组合函数构建的,这些函数就是要显示在界面上的元素,在函数中只需要描述应用界面形状和数据依赖关系,而不用去关系界面的构建过程,
如果需要创建组合函数,只需要将 @Composeable 注解添加到对于的函数上即可,需要注意的是组合函数的名称一般都是以大写字母开头的,如下:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
PrimaryTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Greeting("Android")
}
}
}
}
}
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!", fontSize = 18.sp, color = Color.Red)
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
PrimaryTheme {
Greeting("Android")
}
}
setContent 块 定义了 Activity 的布局,我们不需要去定义 XML 的布局内容,只需要在其中调用组合函数即可。
上面的 一个简单的示例Greeting 微件,它接收 String 而发出的一个显示问候消息的 Text 微件。此函数不会返回任何内容,因为他们描述所需的屏幕状态,而不是构造界面微件。
其中 Greeting 就是一个非常简单的可组合函数,里面定义了一个 Text,顾名思义,就是用来显示一段文本
并且,我们可以在 Test 函数上添加 @PreView 注释,这样就可以非常方便的进行预览。
声明式范式转变
在 Compose 的声明方法中,微件相对无状态,并且不提供 get,set 方法。实际上,微件微件不会以对象的形式提供。你可以通过调用带有不同参数的统一可组合函数来更新界面。这使得架构模式,如 ViewModel 变得很容易。
引用逻辑为顶级可组合函数提供数据。该函数通过调用其他可组合函数来使用这些数据来描述界面。将适当的数据传递给这些可组合函数,并沿层次结构向下传递数据。
当用户与界面交互时,界面发起 onClick事件。这些事件会通知应用逻辑,应用逻辑可以改变应用状态。当状态发生变化时,系统就会重新调用可组合函数。这回导致重新绘制界面描述,此过程称为重组。
动态内容
由于可组合函是 kotlin 编写的,因此他们可以像任何 kotlin 代码一样动态,例如,假设你想要的构建一个界面,如下:
@Composable
fun Greeting(names: List<String>) {
for (name in names) {
Text("Hello $name")
}
}
此函数接受一个列表,每位每个列表元素生成一个 Text。可组合函数可能性非常复杂,你可以使用 if 语句来确定是否需要显示特定的界面元素。例如循环,辅助函数等。你拥有地城语言的灵活性,这种强大的功能和灵活性是 JetpackCompose 的主要优势之一。
重组
在 Compose 中,你可以用新数据再次调用某个可组合函数,这回导致组合函数重新进行重组。系统会根据需要使用新数据重新绘制发出的微件。Compose 框架可以只能的重组已经更改的组件。
例如,下面这个可组合函数,用于显示一个按钮:
@Composable
fun ClickCounter(clicks: Int, onClick: () -> Unit) {
Button(onClick = onClick) {
Text("I've been clicked $clicks times")
}
}
每次点击按钮,就会更新 clicks 的值,Compose 会再次调用 lambda 与 Text 函数以显示新值,此过程称为 重组。不依赖该值的其他元素不会重组。
重组是指在输入更改的时候再次调用可组合函数的过程。当函数更改时,会发生这种情况。当 Compose 根据新输入重组时,它仅调用可能已经更改的函数或 lambad,而跳过其余函数或 lambda。通过跳过岂会为更改参数的函数或者 lambda ,Compose 可以高效的重组。
切勿依赖于执行可组合函数所产生的附带效应,因为可能会跳过函数的重组,如果这样做,用户可能在应用中遇到奇怪且不可预测的行为。例如:
写入共享对象的属性
更新 viewmodel 中的可观察项
更新共享偏好设置
可组合函数可能会每一帧一样的频繁执行,例如呈现动画的时候。所以可组合函数需要快速执行,所以避免在组合函数中出现卡顿,如果你需要执行高昂的操作,请在狗太协程中执行,并将结果作为参数传递给可组合函数。
例如下面代码,应该将 sp 读取的操作放在 viewmode 中,然后在回调中触发更新:
@Composable
fun SharedPrefsToggle(
text: String,
value: Boolean,
onValueChanged: (Boolean) -> Unit
) {
Row {
Text(text)
Checkbox(checked = value, onCheckedChange = onValueChanged)
}
}
可组合函数可以按照任何顺序执行
如果你看到了可组合函数的代码,可能会认为他们按照顺序运行。但实际上未必是这样。如果某个可组合函数包含对其他组合代码的调用,这些函数可以按照顺序执行。
Compose 可以选择识别出某些界面元素的优先级高于其他界面元素,因此首先绘制这些元素。
假设你有如下代码:
@Composable
fun ButtonRow() {
MyFancyNavigation {
StartScreen()
MiddleScreen()
EndScreen()
}
}
对于这三个的调用可以按照任何顺序进行。这意味着你不能让某个函数设置一个全局变量(附带效应),并让别的函数利用这个全局变量而发生更改。所以每个函数都应该独立。
可组合函数可以并行运行
Compose 可以通过并行运行可组合函数来优化重组。这样依赖,Compose 就可以利用多个核心,并按照较低的优先级运行可组合函数(不在屏幕上)
这种优化方方式意味着可组合函数可能会在后台的线程池中执行,如果某个可组合函数对 viewModel 调用一个函数,则 Compose 可能会同时从多个线程调动该函数。
为了确保应用可以正常运行,所有的组合都不应该有附带效应,而应该通过始终在界面线程上执行的 onClick 等回调触发附带效应。
调用某个可组合函数时,调用可能发生在与调用方不同的线程上。这意味着,应避免修改可组合函数 lambda 中的变量代码,基因为此类代码并非线程安全代码,又因为他是可组合 lambda 不允许的附带效应。
下面展示了一个可组合函数,他显示了一个列表已经数量。
@Composable
fun ListComposable(myList: List<String>) {
Row(horizontalArrangement = Arrangement.SpaceBetween) {
Column {
for (item in myList) {
Text("Item: $item")
}
}
Text("Count: ${myList.size}")
}
}
此函数没有附带效应,他会将输出列表转为界面。才代码非常适合展示小列表。不过此函数写入局部变量,则这并不是非线程安全或者正确的代码:
@Composable
@Deprecated("Example with bug")
fun ListWithBug(myList: List<String>) {
var items = 0
Row(horizontalArrangement = Arrangement.SpaceBetween) {
Column {
for (item in myList) {
Text("Item: $item")
items++ // Avoid! Side-effect of the column recomposing.
}
}
Text("Count: $items")
}
}
在上面例子中,每次重组都会修改 items。这可以在动画的第一帧,或者在列表更新的时候。但不管怎么样,界面都会显示出错误的数量。因此 Compose 不支持这样的写入操作。通过静止此类操作,我们允许框架更改线程以执行可组合 lambda。
重组跳过尽可能多的内容
如果界面某些部分无需,Compose 会尽力只重组需要更新的部分。这意味着,他可以跳过某些内容以重新运行单个按钮的可组合项,而不执行树中其上面或下面的任何可组合项。
每个可组合函数和 lambda 都可以自行重组。以下演示了在呈现列表时重组如何跳过某些元素:
/**
* Display a list of names the user can click with a header
*/
@Composable
fun NamePicker(
header: String,
names: List<String>,
onNameClicked: (String) -> Unit
) {
Column {
// this will recompose when [header] changes, but not when [names] changes
Text(header, style = MaterialTheme.typography.h5)
Divider()
// LazyColumn is the Compose version of a RecyclerView.
// The lambda passed to items() is similar to a RecyclerView.ViewHolder.
LazyColumn {
items(names) { name ->
// When an item's [name] updates, the adapter for that item
// will recompose. This will not recompose when [header] changes
NamePickerItem(name, onNameClicked)
}
}
}
}
/**
* Display a single name the user can click.
*/
@Composable
private fun NamePickerItem(name: String, onClicked: (String) -> Unit) {
Text(name, Modifier.clickable(onClick = { onClicked(name) }))
}
这些作用域中的每一个都可能是在重组期间执行唯一一个作用域。当 header 发生更改时,Compose 可能会跳至 Column lambda 。二部执行他的任何父项。此外,执行 Colum 时,如果 names 未更改,Compose 可能会旋转跳过 LazyColum 的项。
同样,执行所有组合函数或者 lambda 都应该没有附带效应。当需要执行附带效应时,应该通过回调触发。
重组是乐观操作
只要 Compose 任务某个可组合函数可能已经更改,就会开始重组。重组是乐观操作,也就是说 Compose 预计会在参数再次更改之前完成重组。如果某个参数在重组完成之间发生改变,Compose 可能会取消重组,并使用新的参数重新开始。
取消重组后,Compose 会从重组中舍弃界面树。如有附带效应依赖于显示的界面,即使取消了组成操作,也会应用该附带效应。这可能导致应用状态不一致。
确保每个可组合函数和 lambda 都幂等,且没有附带效应,以处理乐观的重组
可组合函数可能会非常频繁的运行
在某些情况下,可能针对界面每一帧运行一个可组合函数,如果该函数成本高昂,可能会导致界面卡顿。
例如,你的微件重试读取设备配置,或者读取 sp,他可能会在一秒钟内读取这些数据上百次,这回对性能造成灾难性的影响。
如果您的可组合函数需要数据,它应为相应的数据定义参数。然后,您可以阿静成本高昂的工作移到其他线程,并使用 mutableStateOf 或者 LiveData 将相应的数据传递给 Compose。
主题
//深色
val DarkColorScheme = darkColors(
primary = Purple80,
onPrimary = Color(0xFFFFFFFF),
secondary = PurpleGrey80,
)
//亮色
val LightColorScheme = lightColors(
primary = Purple40,
onPrimary = Color(0xFF333333),
secondary = PurpleGrey40,
)
@Composable
fun PrimaryTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
MaterialTheme(
colors = LightColorScheme,
typography = Typography,
content = content
)
}
默认的主题定义如上所示,最终会调用 MaterialTheme。
Material 主题主要包含三个属性,分别是 颜色,排版,和内容,Api 如下:
@Composable
fun MaterialTheme(
colors: Colors = MaterialTheme.colors, // 颜色集合
typography: Typography = MaterialTheme.typography, // 排版集合
shapes: Shapes = MaterialTheme.shapes, // 形状集合
content: @Composable () -> Unit // 要展示的内容
)
颜色
class Colors(
primary: Color, // 主颜色,屏幕和元素都用这个颜色
primaryVariant: Color, // 用于区分主颜色,比如app bar和system bar
secondary: Color, // 强调色,悬浮按钮,单选/复选按钮,高亮选中的文本,链接和标题
secondaryVariant: Color, // 用于区分强调色
background: Color, // 背景色,在可滚动项下面展示
surface: Color, // 表层色,展示在组件表层,比如卡片,清单和菜单(CardView,SheetLayout,Menu)等
error: Color, // 错误色,展示错误信息,比如TextField的提示信息
onPrimary: Color, // 在主颜色primary之上的文本和图标的颜色
onSecondary: Color, // 在强调色secondary之上的文本和图标的颜色
onBackground: Color, // 在背景色background之上的文本和图标的颜色
onSurface: Color, // 在表层色surface之上的文本和图标的颜色
onError: Color, // 在错误色error之上的文本和图标的颜色
isLight: Boolean // 是否是浅色模式
)
更多的可以查看 lightColorScheme 函数。
排版
@Immutable
class Typography internal constructor(
val h1: TextStyle,
val h2: TextStyle,
val h3: TextStyle,
val h4: TextStyle,
val h5: TextStyle,
val h6: TextStyle,
val subtitle1: TextStyle,
val subtitle2: TextStyle,
val body1: TextStyle,
val body2: TextStyle,
val button: TextStyle,
val caption: TextStyle,
val overline: TextStyle
)
形状
class Shapes(
// 小组件使用的形状,比如: Button,SnackBar,悬浮按钮等
val small: CornerBasedShape = RoundedCornerShape(4.dp),
// 中组件使用的形状,比如Card(就是CardView),AlertDialog等
val medium: CornerBasedShape = RoundedCornerShape(4.dp),
// 大组件使用的形状,比如ModalDrawer或者ModalBottomSheetLayout(就是抽屉布局和清单布局)
val large: CornerBasedShape = RoundedCornerShape(0.dp),
)
使用
setContent {
PrimaryTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.color.background
) {
Greeting("Android")
}
}
}
@Composable
fun PrimaryTheme(
themeType: ThemeType = themeTypeState.value,
content: @Composable () -> Unit
) {
val shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(8.dp),
large = RoundedCornerShape(12.dp),
)
MaterialTheme(
colors = getThemeForTheme(themeType),
typography = Typography,
shapes = shapes,
content = content
)
}
UI
SetContent
setContent {
PrimaryTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Greeting("Android")
}
}
}
同 Android 中的 SetContentView。
Theme
创建项目之后,就会生成一个 项目名称+Theme 的 @Compose 方法,我们可以通过更改其中的颜色来完成对主题的修改。具体如上面的主题所示.
Modifier
Modifier 本质是一个接口,可以用来修饰各种布局,例如 宽高,padding 等,常见的如下:
padding:有四个重载方法
plus:将其他的 Modifer 加入到当前的 Modifer 中。
fillMaxHeight,fillMaxWidth,fillmaxSize:类似于 match_parent,填充整个父 Layout
with,height,size :设置宽高度
rtl,ltr:开始布局的方向
widthIn,heightIn,sizeIn 设置布局的宽度和高度的最大值和最小值
gravity:元素的位置,
等等
需要注意的是 Modifier 系列的方法都支持链式调用
Column,Row
类似于 LinearLayout,Column 是横向的,Row 是竖向的。有四个参数:
Modifer: 具体值如上述所示
verticalArrangement:子元素竖向的排列规则 常见的就是,上下左右中,比较特殊的就是 SpaceEvenly 均匀分配, SpaceBetween 第一个元素前和最后一个元素后没有空隙,其他的按比例放入。 SpaceAround 把整体中的一半空隙凭据放入第一个和最后一个的开始和结束,剩余的一半等比放入各个元素。
horizontalAlignment:和上面一个,只不过方向不同
content:要显示的内容
栗子:@Composable () -> Unit
setContent {
PrimaryTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Column {
Row {
Button(
onClick = { themeTypeState.value = ThemeType.RED_THEME },
modifier = Modifier.width(100.dp),
colors = ButtonDefaults.buttonColors(containerColor = Color.Yellow)
) {
Greeting(name = "按钮1")
}
Button(
onClick = { themeTypeState.value = ThemeType.GREEN_THEME },
modifier = Modifier.width(100.dp),
colors = ButtonDefaults.elevatedButtonColors()
) {
Greeting(name = "按钮2")
}
}
Greeting(name = "Hello Android")
Greeting(name = "Hello 345")
}
}
}
}
Text
fun Text(
text: String, //显示内容
modifier: Modifier = Modifier, //修饰,可修改透明度,边框,背景等
color: Color = Color.Unspecified, //文字颜色
fontSize: TextUnit = TextUnit.Unspecified,// size
fontStyle: FontStyle? = null, //文字样式,粗体,斜体等
fontWeight: FontWeight? = null,//文字厚度
fontFamily: FontFamily? = null,//字体
letterSpacing: TextUnit = TextUnit.Unspecified, //用于与文本相关的维度值的单位。该组件还在测试中
textDecoration: TextDecoration? = null,//文字装饰,中划线,下划线
textAlign: TextAlign? = null,对齐方式
lineHeight: TextUnit = TextUnit.Unspecified,//行高
overflow: TextOverflow = TextOverflow.Clip,//如何处理溢出,默认裁切
softWrap: Boolean = true,//是否软换行
maxLines: Int = Int.MAX_VALUE,//最大行数
onTextLayout: (TextLayoutResult) -> Unit = {},//计算布局时回调
style: TextStyle = LocalTextStyle.current //文本的样式配置,如颜色、字体、行高等。
)
modifier:在此处用来修饰 Text,Modifer 提供了很多扩展,如透明度,背景,边框等
示例:
@Composable
fun Greeting(name: String) {
Text(
text = name,
fontSize = 18.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.height(30.dp)
)
}
Button
fun Button(
onClick: () -> Unit,//点击时调用
modifier: Modifier = Modifier,//同上
enabled: Boolean = true,//是否启用
elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),// z轴上的高度
shape: Shape = FilledButtonTokens.ContainerShape.toShape(),
border: BorderStroke? = null,
colors: ButtonColors = ButtonDefaults.buttonColors(),
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
content: @Composable RowScope.() -> Unit
)
shape 调整 button 的样式,例如 RoundedCornerShape 是圆角矩形的样式,CircleShape 是圆形的样式,CutCornerShape 是切角样式
border 外边框,默认是 null,Border 有两种使用方式,1 Border(size: Dp, color: Color),2 Border(size: Dp, brush: Brush) 。 第二种需要自己创建一个笔刷,去绘制外边框,例如要实现渐变的外边框。
colors 按钮的颜色,默认是 ButtonDefaults.buttonColors() 。可选的有:
其中可以设置按钮的背景色,未启用的颜色等。
栗子:
Button(
onClick = { themeTypeState.value = ThemeType.GREEN_THEME },
modifier = Modifier.width(100.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Yellow,
contentColor = Color.Red,
disabledContainerColor = Color.Black,
disabledContentColor = Color.Green
)
) {
Greeting(name = "按钮2")
}
OutLinedButton
具有外边框的按钮,内部使用的也是 Button。默认会有一个边框,其参数和 Button 一致,效果如下
TextButton
默认的 button 在有主题的时候,默认背景是主题颜色,而 textButton 背景默认是透明的。TextButton 默认使用的颜色是 ButtonDefaults.textButtonColors()
Image
@Composable
fun Image(
painter: Painter,
bitmap: ImageBitmap, //
contentDescription: String?,
modifier: Modifier = Modifier,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null
)
painter:图片资源,使用 PainterResource 来完成。
contentDescription:无障碍提示文本信息
contentScale :类似于 ImageView 中的 scaleType 属性。
colorFilter:将某种颜色应用到图片上
alpha:不透明度
示例
@Composable
@Preview
fun Image() {
Image(
painter = painterResource(id = R.drawable.one),
contentDescription = "无障碍提示",
contentScale = ContentScale.Crop,
modifier = Modifier
.width(100.dp)
.height(100.dp)
)
}
像一些圆图或者边框啥的就可以在 modifer 中直接设置了,如下:
@Composable
@Preview
fun Image() {
Image(
painter = painterResource(id = R.drawable.one),
contentDescription = "无障碍提示",
contentScale = ContentScale.Crop,
modifier = Modifier
.width(100.dp)
.height(100.dp)
.clip(shape = CircleShape)
.border(2.dp, color = Color.Red, shape = CircleShape)
)
}
加载网路图片
加载网路图片需要借助第三方库 coil,使用方式如下:
//图片加载库
implementation("io.coil-kt:coil:2.0.0")
implementation("io.coil-kt:coil-compose:2.0.0")
@Composable
@Preview
fun Image() {
AsyncImage(
model = "https://img0.baidu.com/it/u=3147375221,1813079756&fm=253&fmt=auto&app=120&f=JPEG?w=500&h=836",
contentDescription = "无障碍提示",
contentScale = ContentScale.Crop,
modifier = Modifier
.width(100.dp)
.height(100.dp)
.clip(shape = CircleShape)
.border(2.dp, color = Color.Red, shape = CircleShape)
)
}
Spacer
和原生的一样,需要空白区域时可以使用 Spacer ,使用方式如下:
Spacer(modifier = Modifier.height(100.dp))
Surface
对内容进行装饰,例如设置背景,shape 等
fun Surface(
modifier: Modifier = Modifier,
shape: Shape = Shapes.None,
color: Color = MaterialTheme.colorScheme.surface,
contentColor: Color = contentColorFor(color),
tonalElevation: Dp = 0.dp,
shadowElevation: Dp = 0.dp,
border: BorderStroke? = null,
content: @Composable () -> Unit
)
color :设置 Surface 的背景色,默认是主题中的 surface 颜色。
contentColor:此 Surface 为其子级提供的首选内容颜色。默认为 [color] 的匹配内容颜色,或者如果 [color] 不是来自主题的颜色,这将保持在此 Surface 上方设置的相同值。
tonalElevation:当 [color] 为 [ColorScheme.surface] 时,高程越高,浅色主题颜色越深,深色主题颜色越浅。
shadowElevation:阴影大小
Scaffold
脚手架的意思,和 Flutter 中的 Scaffold 是一样的,通过 Scaffold 我看可以快速的对页面进行布局,例如设置导航栏,侧滑栏,底部导航等等。
fun Scaffold(
modifier: Modifier = Modifier,
topBar: @Composable () -> Unit = {},
bottomBar: @Composable () -> Unit = {},
snackbarHost: @Composable () -> Unit = {},
floatingActionButton: @Composable () -> Unit = {},
floatingActionButtonPosition: FabPosition = FabPosition.End,
containerColor: Color = MaterialTheme.colorScheme.background,
contentColor: Color = contentColorFor(containerColor),
content: @Composable (PaddingValues) -> Unit
)
topBar:Toolbar,常用的有 CenterAlignedTopAppBar,SmallTopAppBar,MediumTopAppBar 等。
bootomBar:底部导航栏
snackbarHost:
floatingActionButton:按钮
floatingActionButtonPosition:按钮位置
containerColor:背景颜色
contentColor:内容首选颜色
看一个栗子:
Scaffold(
topBar = {
//.....
},
bottomBar = bottomBar,
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding
(top = it.calculateTopPadding(), bottom = it.calculateBottomPadding())
) {
content.invoke(it)
}
}
需要注意的是,如果使用了 toolbar 或者 bootomBar,就会把 content 中的内容挡住,这个时候就需要使用 PaddingValue 设置内边距了。
还有一点须要注意,如果要使用沉浸式状态栏,就需要自定义 topBar 了,要不然状态栏会被 topBar 覆盖。下面代码是设置沉浸式状态栏的。
///系统 UI 控制器
implementation "com.google.accompanist:accompanist-systemuicontroller:0.24.8-beta"
//正确获取状态栏高度
api "com.google.accompanist:accompanist-insets-ui:0.24.8-beta"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
SetImmersion()
PrimaryTheme {
SetContent()
}
}
}
@Composable
private fun SetImmersion() {
if (isImmersion()) {
val systemUiController = rememberSystemUiController()
SideEffect {
systemUiController.run {
setSystemBarsColor(color = Color.Transparent, darkIcons = isDark())
setNavigationBarColor(color = Color.Black)
}
}
}
}
底部导航栏
@Composable
fun MainCompose(navController: NavHostController, mainBottomState: MutableState<Int>) {
SetScaffold(
bottomBar = {
BottomBar(mainBottomState)
}
) {
when (mainBottomState.value) {
0 -> HomeCompos(navController)
1 -> ProjectCompos()
2 -> FLCompos()
else -> UserCompos()
}
}
}
@Composable
private fun BottomBar(mainBottomState: MutableState<Int>) {
BottomNavigation(
backgroundColor = MaterialTheme.colors.background,
) {
navigationItems.forEachIndexed { index, navigationItem ->
BottomNavigationItem(
selected = mainBottomState.value == index,
onClick = {
mainBottomState.value = index
},
icon = {
Icon(
imageVector = navigationItem.icon,
contentDescription = navigationItem.name
)
},
label = {
BottomText(
isSelect = mainBottomState.value == index,
name = navigationItem.name
)
},
selectedContentColor = Color.White,
unselectedContentColor = Color.Black
)
}
}
}
@Composable
fun BottomText(isSelect: Boolean, name: String) {
if (isSelect) {
Text(
text = name,
color = MaterialTheme.colors.primary,
fontSize = 12.sp
)
} else {
Text(
text = name,
color = Color.Black,
fontSize = 12.sp
)
}
}
Android | Compose 初上手_android compose-CSDN博客
Compose 和 Android 传统View 互相调用
1. 前言
Compose 具有超强的兼容性,兼容现有的所有代码,Compose 能够与现有 View 体系并存,可实现渐进式替换。这就很有意义了,我们可以在现有项目中一小块一小块逐步地替换Compose,或者在旧项目中实现新的需求的时候,使用Compose。
今天,我们就来演示一下,Compose和Android View怎么互相调用,以及在双层嵌套(原生View嵌套Compose,Compose中又嵌套原生View)的情况下,在最外层原生View中,怎么获取到Compose内部的原生View。
2. Android 传统 View 调用 Compose
新建项目的时候选择 Empty Activity
2.2 项目添加Compose配置
2.2.1 在android代码块添加
在app的build.config android代码块中添加
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.1.1' //对应Kotlin版本1.6.10
}
注意这里的kotlinCompilerExtensionVersion,和Kotlin的版本对应
kotlin版本 : 1.6.10 对应 kotlinCompilerExtensionVersion '1.1.1'
kotlin版本 : 1.7.0 对应 kotlinCompilerExtensionVersion '1.2.0'
kotlin版本 : 1.7.20 对应 kotlinCompilerExtensionVersion '1.3.2'
kotlin版本 : 1.8.10 对应 kotlinCompilerExtensionVersion '1.4.3'
kotlin版本 : 1.9.10 对应 kotlinCompilerExtensionVersion '1.5.3'
2.2.2 在dependencies中添加依赖
在app的build.config dependencies代码块中添加
dependencies {
//...省略...
def compose_ui_version = '1.1.1'
implementation "androidx.compose.ui:ui:$compose_ui_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"
implementation 'androidx.activity:activity-compose:1.3.1'
implementation 'androidx.compose.material:material:1.1.1'
}
Compose 最新的版本可以使用 物料清单 (BoM) 来进行依赖,这样会更方便
比如 implementation platform('androidx.compose:compose-bom:2023.01.00')
2.3 定义Compose函数
在MainActivity.kt中定义Compose函数
@Composable
fun ComposeContent() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = "Hello world!")
}
}
2.4 修改xml文件
在activity_main.xml中添加androidx.compose.ui.platform.ComposeView
2.5 关联Compose函数
在MainActivity.kt中,先通过findViewById找到ComposeView,然后通过composeView.setContent将Android 传统View和Compose建立关联。
2.6 运行项目
可以发现界面显示如下,成功在传统View项目中调用了Compose了
3. Compose中调用Android View
3.1 调用传统View的日历
3.1.1 使用AndroidView
在@Composable内使用: androidx.compose.ui.viewinterop.AndroidView,然后在factory里面返回原生View即可
3.1.2 显示效果如下
3.2 调用传统View的WebView
3.2.1 添加网络权限
首先需要在AndroidManifest.xml中添加网络权限
3.2.2 首先要注册WebView的生命周期
3.2.3 创建有状态的WebView
创建有状态的WebView,并注册生命周期
3.2.4 调用Android View
3.2.5 显示效果如下所示
4. 双层嵌套,获取AndroidView中的原生View id
有时候,我们会遇到这种情况,就是在原生项目了,页面中有部分使用了Compose,然后在Compose中又有部分组件使用了原生View,这种情况下,要如何取到AndroidView中的原生View id 呢 ?
4.1 在定义Xml中定义ComposeView
4.2 关联Compose函数
在MainActivity.kt中,先通过findViewById找到ComposeView,然后通过composeView.setContent将Android 传统View和Compose建立关联。
4.3 创建ids.xml,定义原生view id
在resources/values目录下创建ids.xml
4.4 实现ComposeContent
4.5 在外层的原生代码处,获取Compose中的原生View
在原生代码的地方,通过composeView.findViewById查找id为my_calendar_view的原生View
注意这里的window?.decorView?.post : 必须在页面加载完成后,才能查找到my_calendar_view对应的原生View,如果直接在onCreate里面去查找,会发现composeView.findViewById<CalendarView>(calendarViewId)返回的是null
4.6 运行项目
选择任意一个日期,可以发现弹出的toast是!!!! year年month月day日,即原生的setOnDateChangeListener覆盖了Compose中的setOnDateChangeListener监听,这样说明我们也在原生代码处,取到了Compose内部的原生View了。
Compose 和 Android 传统View 互相调用_compose androidview-CSDN博客
Android Jetpack
Android Jetpack 是一套用于简化 Android 应用开发的库、工具和指南集合。它提供了一种组件化的架构方式,帮助开发者更轻松地构建稳定、高效和可扩展的 Android 应用。
Android Jetpack 包含多个组件,可以根据不同的需求选择使用,其中一些核心组件包括:
- LiveData:一种可感知生命周期的数据持有类,可供应用程序的不同组件之间共享数据。
- ViewModel:用于存储和管理与界面相关的数据,以支持应用程序配置更改时的数据持久性。
- Room:提供了一个抽象层,使得在 SQLite 数据库上进行更方便的访问和操作。
- Navigation:用于实现应用内导航的组件,简化了界面之间的跳转和传递数据的操作。
- WorkManager:用于处理后台任务,如延迟执行、周期性执行和网络连接状态变化后执行。
- Paging:用于加载和显示大型数据集的分页库,可以实现无限滚动列表等功能。
- Data Binding:用于在布局文件中绑定UI元素和数据。
- WorkManager:用于管理后台任务和调度。
- Security:用于加密和解密数据,以及处理其他与安全相关的任务。
- CameraX:用于简化相机应用程序的开发。
- Hilt:用于依赖注入。
除了这些核心组件外,Jetpack 还包括其他组件,如安全性组件、应用启动优化组件、测试组件等,可以根据项目的需要选择使用。
以下是一个简单示例,展示了如何在Android应用程序中使用Hilt进行依赖注入:
首先,在您的项目的build.gradle文件中添加Hilt依赖:
接下来,在您的Application类上添加@HiltAndroidApp注解,这将告诉Hilt这是您的应用程序的入口点:
然后,在您的依赖关系图(Dependency Graph)中,您可以使用注解来标记依赖项的创建和提供方式。例如,假设您有一个名为MyRepository的类,它需要一个MyApiService实例作为依赖项:
请注意,@Inject注解表示MyApiService是MyRepository的依赖项。
然后,您需要在依赖关系图中提供MyApiService的实例。您可以使用@Provides注解来完成这个任务。例如:
在这个示例中,@Provides注解表示提供了MyApiService的实例。
最后,在使用MyRepository的地方,您可以使用@Inject注解来自动注入依赖项。例如,在Activity中:
现在,当您使用MainActivity时,MyRepository的实例将由Hilt自动注入。
请注意,为了使Hilt能够工作,您还需要在应用程序的组件(如Activity、Fragment等)上添加@AndroidEntryPoint注解。例如,在MainActivity上:
· Android官方架构组件Lifecycle:生命周期组件详解&原理分析
· Android官方架构组件ViewModel:从前世今生到追本溯源
· Android官方架构组件LiveData: 观察者模式领域二三事
· Android官方架构组件Paging:分页库的设计美学
· Android官方架构组件Paging-Ex:为分页列表添加Header和Footer
· Android官方架构组件Paging-Ex:列表状态的响应式管理
· Android官方架构组件Navigation:大巧不工的Fragment管理框架
· Android官方架构组件DataBinding-Ex:双向绑定篇