高阶技巧

全局Context

将Context设置成静态变量很容易会产生内存泄漏的问题,但是applicationContext全局只有一份,生命周期和APP的生命周期一致,所以选择强制忽略。

1
2
3
4
5
6
7
8
9
10
11
class MyApplication : Application() { 

companion object {
@SuppressLint("StaticFieldLeak")
lateinit var context: Context
}
override fun onCreate() {
super.onCreate()
context = applicationContext
}
}

还需要设置程序启动的时候应该初始化MyApplication类,而不是Application默认类,在AndroidManifest.xml文件中指定。

Intent传递对象

Serializable方式

Serializable是序列化的意思,表示将一个对象转换成可存储或可传输的状态。序列化后的对象可以在网络上进行传输,也可以存储到本地。

至于序列化的方法需要让一个类去实现Serializable这个接口。

1
2
3
4
5
6
7
8
9
10
11
12
class Person : Serializable { 
var name = ""
var age = 0
}

val person = Person()
person.name = "Tom"
person.age = 20
val intent = Intent(this, SecondActivity::class.java)
intent.putExtra("person_data", person)
startActivity(intent)

取出数据:

1
val person = intent.getSerializableExtra("person_data") as Person 

Parcelable方式

Parcelable方式的实现原理是将一个完整的对象进行分解,而分解后的每一部分都是Intent所支持的数据类型。

必须重写describeContents()和writeToParcel()这两个方法。

还必须在类中提供一个名为CREATOR的匿名类实现,需要重写 createFromParcel()和newArray()这两个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person : Parcelable { 
var name = ""
var age = 0
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(name) // 写出name
parcel.writeInt(age) // 写出age
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<Person> {
override fun createFromParcel(parcel: Parcel): Person {
val person = Person()
person.name = parcel.readString() ?: "" // 读取name
person.age = parcel.readInt() // 读取age
return person
}

override fun newArray(size: Int): Array<Person?> {
return arrayOfNulls(size)
}
}
}

取出数据:

1
val person = intent.getParcelableExtra("person_data") as Person

Serializable的方式较为简单,但由于会把整个对象进行序列化,因此效率会比Parcelable方式低一些,所以在通常情况下,还是更加推荐使用Parcelable的方式来实现Intent传递对象的功能。

定制日志工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
object LogUtil { 

private const val VERBOSE = 1

private const val DEBUG = 2

private const val INFO = 3

private const val WARN = 4

private const val ERROR = 5

private var level = VERBOSE

fun v(tag: String, msg: String) {
if (level <= VERBOSE) {
Log.v(tag, msg)
}
}

fun d(tag: String, msg: String) {
if (level <= DEBUG) {
Log.d(tag, msg)
}
}

fun i(tag: String, msg: String) {
if (level <= INFO) {
Log.i(tag, msg)
}
}

fun w(tag: String, msg: String) {
if (level <= WARN) {
Log.w(tag, msg)
}
}

fun e(tag: String, msg: String) {
if (level <= ERROR) {
Log.e(tag, msg)
}
}

}

这样可以通过调整源码的方式,随时使能或者失能日志调试。

调试代码

方法一就是从头到尾运行,打断点调试,不赘述。

方式二是运行到某一页面,中途进入调试模式,使用Attach Debugger to Android Process功能即可。

深色主题

Force Dark

Force Dark就是这样一种简单粗暴的转换方式,并且它的转换效果通常是不尽如人意的。

右击res目录→New→Directory,创建一个values-v29目录,然后右击values-v29目录→New→Values resource file,创建一个styles.xml文件:

1
2
3
4
5
6
7
8
<resources> 
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:forceDarkAllowed">true</item>
</style>
</resources>

values-v29目录是只有Android 10.0及以上的系统才会去读取的,因此这是一种系统差异型编程的实现方式。

DayNight

QQ_1737808082890

更改values/styles.xml中的代码:

1
2
3
4
5
6
7
8
9
10
<resources> 
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
...
</resources>

右击res目录 →New→Directory,创建一个values-night目录,然后右击values-night目录→New→Values resource file,创建一个colors.xml文件。接着在这个文件中指定深色主题下的颜色值:

1
2
3
4
5
<resources> 
<color name="colorPrimary">#303030</color>
<color name="colorPrimaryDark">#232323</color>
<color name="colorAccent">#008577</color>
</resources>

Kotlin课堂

Java与Kotlin代码之间的转换:

  • Java转Kotlin:Java赋值粘贴到kt文件中会自动提示转换,同时也可以使用导航栏中的Code→Convert Java File to Kotlin File功能,进行全项目转换。
  • Kotlin转Java:先转化为字节码,在反编译转化为Java,点击Tools→Kotlin→Show Kotlin Bytecode,再点击这个窗口左上角的“Decompile”按钮,就可以将这些Kotlin字节码反编译成Java代码。

探究Jetpack

Jetpack

QQ_1737786500747

以上是MVVM架构,Jetpack中的许多架构组件是专门为MVVM架构量身打造的,这一章主要介绍ViewModel、LiveData、Room。

简介

Jetpack中的组件有一个特点,它们大部分不依赖于任何Android系统版本,这意味着这些组件通常是定义在AndroidX库当中的,并且拥有非常好的向下兼容性。

ViewModel

ViewModel的一个重要作用就是可以帮助Activity分担一部分工作,它是专门用于存放与界面相关的数据的。也就是说,只要是界面上能看得到的数据,它的相关变量都应该存放在ViewModel中,而不是Activity中。

ViewModel还有一个非常重要的特性,它可以保证在手机屏幕发生旋转的时候不会被重新创建,只有当Activity退出的时候才会跟着Activity一起销毁。

QQ_1737786896959

基本用法

通常来讲,比较好的编程规范是给每一个Activity和Fragment都创建一个对应的ViewModel。

需要添加依赖。

1
2
3
4
dependencies { 
...
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
}

绝对不可以直接去创建ViewModel的实例,而是一定要通过ViewModelProvider来获取ViewModel的实例。

1
ViewModelProvider(<你的Activity或Fragment实例>).get(<你的ViewModel>::class.java) 

之所以要这么写,是因为ViewModel有其独立的生命周期,并且其生命周期要长于Activity。如果我们在onCreate()方法中创建ViewModel的实例,那么每次onCreate()方法执行的时候,ViewModel都会创建一个新的实例,这样当手机屏幕发生旋转的时候,就无法保留其中的数据了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MainViewModel : ViewModel() { 
var counter = 0
}

class MainActivity : AppCompatActivity() {
lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
plusOneBtn.setOnClickListener {
viewModel.counter++
refreshCounter()
}
refreshCounter()
}
private fun refreshCounter() {
infoText.text = viewModel.counter.toString()
}
}

传递构造参数

解决在构造函数中传递参数的问题,借助ViewModelProvider.Factory实现,必须实现create方法。

1
2
3
4
5
6
7
8
9
10
11
class MainViewModel(countReserved: Int) : ViewModel() { 
var counter = countReserved
}

class MainViewModelFactory(private val countReserved: Int) : ViewModelProvider.Factory {

override fun <T : ViewModel> create(modelClass: Class<T>): T {
return MainViewModel(countReserved) as T
}

}

Lifecycles

在一个非Activity的类中感知Activity生命周期。

隐式Fragment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyObserver { 
fun activityStart() {
}
fun activityStop() {
}
}
class MainActivity : AppCompatActivity() {
lateinit var observer: MyObserver
override fun onCreate(savedInstanceState: Bundle?) {
observer = MyObserver()
}
override fun onStart() {
super.onStart()
observer.activityStart()
}
override fun onStop() {
super.onStop()
observer.activityStop()
}
}

Lifecycles组件

Lifecycles可以让任何一个类都能轻松感知到Activity的生命周期,同时又不需要在Activity中编写大量的逻辑处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyObserver : LifecycleObserver { 
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun activityStart() {
Log.d("MyObserver", "activityStart")
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun activityStop() {
Log.d("MyObserver", "activityStop")
}
}
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
...
lifecycle.addObserver(MyObserver())
}
...
}

我们在方法上使用了@OnLifecycleEvent注解,并传入了一种生命周期事件。生命周期事件的类型一共有7种:ON_CREATE、ON_START、ON_RESUME、ON_PAUSE、ON_STOP和ON_DESTROY分别匹配Activity中相应的生命周期回调;另外还有一种ON_ANY类型,表示可以匹配Activity的任何生命周期回调。

LiveData

简单场景处理,同时将不会成为主流了,复杂场景需要学习flow、stateflow等替代。

LiveData是Jetpack提供的一种响应式编程组件,它可以包含任何类型的数据,并在数据发生变化的时候通知给观察者。

LiveData内部不会判断即将设置的数据和原有数据是否相同,只要调用了setValue()或postValue()方法,就一定会触发数据变化事件。

LiveData之所以能够成为Activity与ViewModel之间通信的桥梁,并且还不会有内存泄漏的风险,靠的就是Lifecycles组件。LiveData在内部使用了Lifecycles组件来自我感知生命周期的变化,从而可以在Activity销毁的时候及时释放引用,避免产生内存泄漏的问题。

另外,由于要减少性能消耗,当Activity处于不可见状态的时候(比如手机息屏,或者被其他的Activity遮挡),如果LiveData中的数据发生了变化,是不会通知给观察者的。只有当Activity重新恢复可见状态时,才会将数据通知给观察者。如果在Activity处于不可见状态的时候,LiveData发生了多次数据变化,当Activity恢复可见状态时,只有最新的那份数据才会通知给观察者,前面的数据在这种情况下相当于已经过期了,会被直接丢弃。

基本用法

注意Activity不能将实例传给ViewModel,否则会因为Activity无法释放而造成内存泄露。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class MainViewModel(countReserved: Int) : ViewModel() { 
val counter = MutableLiveData<Int>()
init {
counter.value = countReserved
}
fun plusOne() {
val count = counter.value ?: 0
// setValue()方法用于给LiveData设置数据,但是只能在主线程中调用;postValue()方法用于在非主线程中给LiveData设置数据。
counter.value = count + 1
}
fun clear() {
counter.value = 0
}
}

class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
plusOneBtn.setOnClickListener {
viewModel.plusOne()
}
clearBtn.setOnClickListener {
viewModel.clear()
}
// 没有省略Observer,省略了就是JAVA的但抽象函数API
viewModel.counter.observe(this, Observer { count ->
infoText.text = count.toString()
})
}
override fun onPause() {
super.onPause()
sp.edit {
putInt("count_reserved", viewModel.counter.value ?: 0)
}
}
}

对于Observer不能省略的原因:这是一种非常特殊的情况,因为observe()方法接收的另一个参数LifecycleOwner也是一个单抽象方法接口。当一个Java方法同时接收两个单抽象方法接口参数时,要么同时使用函数式API的写法,要么都不使用函数式API的写法。由于我们第一个参数传的是this,因此第二个参数就无法使用函数式API的写法了。

不过在2019年的Google I/O大会上,Android团队官宣了Kotlin First,并且承诺未来会在Jetpack中提供更多专门面向Kotlin语言的API。lifecycle-livedata-ktx就是一个专门为Kotlin语言设计的库,这个库在2.2.0版本中加入了对observe()方法的语法扩展。现在就可以使用以下写法:

1
2
3
viewModel.counter.observe(this) { count -> 
infoText.text = count.toString()
}

推荐写法

由于上面的做法暴露了可变的LiveData给外部,推荐永远只暴露并不可变的LiveData给外部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MainViewModel(countReserved: Int) : ViewModel() { 
// 自定义getter,属于Kotlin中的属性访问器
val counter: LiveData<Int>
get() = _counter
private val _counter = MutableLiveData<Int>()
init {
_counter.value = countReserved
}

fun plusOne() {
val count = _counter.value ?: 0
_counter.value = count + 1
}

fun clear() {
_counter.value = 0
}

}

map和switchMap

看map()方法,这个方法的作用是将实际包含数据的LiveData和仅用于观察数据的LiveData进行转换。

1
2
3
4
5
6
7
8
class MainViewModel(countReserved: Int) : ViewModel() { 

private val userLiveData = MutableLiveData<User>()
val userName: LiveData<String> = Transformations.map(userLiveData) { user ->
"${user.firstName} ${user.lastName}"
}
...
}

switchMap()方法,将这个LiveData对象转换成另外一个可观察的LiveData对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
data class User(var firstName: String, var lastName: String, var age: Int) 
object Repository {
fun getUser(userId: String): LiveData<User> {
val liveData = MutableLiveData<User>()
liveData.value = User(userId, userId, 0)
return liveData
}
}
class MainViewModel(countReserved: Int) : ViewModel() {
...
private val userIdLiveData = MutableLiveData<String>()
val user: LiveData<User> = Transformations.switchMap(userIdLiveData) { userId ->
Repository.getUser(userId)
}
fun getUser(userId: String) {
userIdLiveData.value = userId
}
}
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
getUserBtn.setOnClickListener {
val userId = (0..10000).random().toString()
viewModel.getUser(userId)
}
viewModel.user.observe(this, Observer { user ->
infoText.text = user.firstName
})
}
...

通过getUser(userId) 改变userIdLiveData -> 在switchMap观察到userIdLiveData改变后,执行lambda表达式中的内容 -> user.observe观察到user变化,回调执行lambda表达式中内容。

userIdLiveData是对外不可见的,是不能在外部观察的;user是可见的,是可以在外部观察的,从而将这个LiveData对象转换成另外一个可观察的LiveData对象。

Room

ORM也叫对象关系映射,将面向对象的语言和面向关系的数据库之间建立一种映射关系,这就是ORM了。

Room的整体结构。它主要由Entity、Dao和Database这3部分组成,详细说明如下。

  • Entity。用于定义封装实际数据的实体类,每个实体类都会在数据库中有一张对应的表,并且表中的列是根据实体类中的字段自动生成的。
  • Dao。Dao是数据访问对象的意思,通常会在这里对数据库的各项操作进行封装,在实际编程的时候,逻辑层就不需要和底层数据库打交道了,直接和Dao层进行交互即可。
  • Database。用于定义数据库中的关键信息,包括数据库的版本号、包含哪些实体类以及提 供Dao层的访问实例。

由于Room会根据我们在项目中声明的注解来动态生成代码,因此这里一定要使用kapt引入Room的编译时注解库,而启用编译时注解功能则一定要先添加kotlin-kapt插件。注意,kapt只能在Kotlin项目中使用,如果是Java项目的话,使用annotationProcessor即可。

Entity

1
2
3
4
5
@Entity 
data class User(var firstName: String, var lastName: String, var age: Int) {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}

Dao

这部分是Room用法中最关键的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Dao 
interface UserDao {
@Insert
fun insertUser(user: User): Long
@Update
fun updateUser(newUser: User)
@Query("select * from User")
fun loadAllUsers(): List<User>
@Query("select * from User where age > :age")
fun loadUsersOlderThan(age: Int): List<User>
@Delete
fun deleteUser(user: User)

@Query("delete from User where lastName = :lastName")
fun deleteUserByLastName(lastName: String): Int

}

但是如果想要从数据库中查询数据,或者使用非实体类参数来增删改数据,那么就必须编写SQL语句了。Room是支持在编译时动态检查SQL语句语法的,如果编写的SQL语句有语法错误,编译的时候就会直接报错。

Database

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Database(version = 1, entities = [User::class]) 
abstract class AppDatabase : RoomDatabase() {

abstract fun userDao(): UserDao

companion object {

private var instance: AppDatabase? = null

@Synchronized
fun getDatabase(context: Context): AppDatabase {
instance?.let {
return it
}
return Room.databaseBuilder(context.applicationContext,
AppDatabase::class.java, "app_database")
.build().apply {
instance = this
}
}
}

}

AppDatabase类必须继承自RoomDatabase类,并且一定要使用abstract关键字将它声明成抽象类。

databaseBuilder()方法接收3个参数,注意第一个参数一定要使用applicationContext,而不能使用普通的context,否则容易出现内存泄漏的情况。

另外,由于数据库操作属于耗时操作,Room默认是不允许在主线程中进行数据库操作的,为了方便测试,Room还提供了一个更加简单的方法,这个方法建议只在测试环境下使用,如下所示:

1
2
3
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java,"app_database") 
.allowMainThreadQueries()
.build()

数据库升级

一般升级:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Database(version = 2, entities = [User::class, Book::class]) 
abstract class AppDatabase : RoomDatabase() {

abstract fun userDao(): UserDao

abstract fun bookDao(): BookDao

companion object {

val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("create table Book (id integer primary
key autoincrement not null, name text not null,
pages integer not null)")
}
}

private var instance: AppDatabase? = null

fun getDatabase(context: Context): AppDatabase {
instance?.let {
return it
}
return Room.databaseBuilder(context.applicationContext,
AppDatabase::class.java, "app_database")
.addMigrations(MIGRATION_1_2)
.build().apply {
instance = this
}
}
}
}

注意:Book表的建表语句必须和Book实体类中声明的结构完全一致,否则Room就会抛出异常。

数据库结构变化升级:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Database(version = 3, entities = [User::class, Book::class]) 
abstract class AppDatabase : RoomDatabase() {
...
companion object {
...
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("alter table Book add column author text not null
default 'unknown'")
}
}

private var instance: AppDatabase? = null

fun getDatabase(context: Context): AppDatabase {
...
return Room.databaseBuilder(context.applicationContext,
AppDatabase::class.java, "app_database")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build().apply {
instance = this
}
}
}
}

比较有难度的就是编写SQL语句。

WorkManager

WorkManager和Service并不相同,也没有直接的联系。

  • Service是Android系统的四大组件之一,它在没有被销毁的情况下是一直保持在后台运行的。

  • 而WorkManager只是一个处理定时任务的工具,它可以保证即使在应用退出甚至手机重启的情况下,之前注册的任务仍然将会得到执行,因此WorkManager很适合用于执行一些定期和服务器进行交互的任务,比如周期性地同步数据,等等。

  • 使用WorkManager注册的周期性任务不能保证一定会准时执行,这并不是bug,而是系统为了减少电量消耗,可能会将触发时间临近的几个任务放在一起执行,这样可以大幅度地减少CPU被唤醒的次数,从而有效延长电池的使用时间

基本用法

  1. 定义一个后台任务,并实现具体的任务逻辑;
  2. 配置该后台任务的运行条件和约束信息,并构建后台任务请求;
  3. 将该后台任务请求传入WorkManager的enqueue()方法中,系统会在合适的时间运行。

定义后台任务:

1
2
3
4
5
6
class SimpleWorker(context: Context, params: WorkerParameters) : Worker(context, params) { 
override fun doWork(): Result {
Log.d("SimpleWorker", "do work in SimpleWorker")
return Result.success()
}
}

doWork()方法不会运行在主线程当中。

配置该后台任 务的运行条件和约束信息:

这一步最为复杂。

1
2
3
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java).build() 
val request = PeriodicWorkRequest.Builder(SimpleWorker::class.java, 15,
TimeUnit.MINUTES).build() // 运行周期间隔不能短于15分钟

调用enqueue()方法执行:

1
WorkManager.getInstance(context).enqueue(request) 

处理复杂任务

WorkManager拥有以下功能:

  1. 让后台任务在指定的延迟时间后运行;
  2. 比如说给后台任务请求添加标签,可以通过标签来取消后台任务请求;
  3. doWork()方法中返回了Result.retry(),那么是可以结合setBackoffCriteria()方法来重新执行任务的;
  4. 对运行结果进行监听;
  5. 链式任务。

setBackoffCriteria()时间设置不能小于10s,同时可以设置线性延迟或者指数的方式延迟。

这是因为绝大多数的国产手机厂商在进行Android系统定制的时候会增加一个一键关闭的功能,允许用户一键杀死所有非白名单的应用程序。而被杀死的应用程序既无法接收广播,也无法运行WorkManager的后台任务。因此,这里给你的建议就是,WorkManager可以用,但是千万别依赖它去实现什么核心功能,因为它在国产手机上可能会非常不稳定


Kotlin课堂(了解)

DSL的全称是领域特定语言(Domain Specific Language),它是编程语言赋予开发者的一种特殊能力,通过它我们可以编写出一些看似脱离其原始语法结构的代码,从而构建出一种专有的语法结构。

比如Gradle是一种基于Groovy语言的构建工具,因此如下的语法结构其实就是Groovy提供的DSL功能,build.gradle文件如下:

1
2
3
4
dependencies { 
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
}

我们实现Kotlin版本的DSL功能,代码如下:

1
2
3
4
5
6
7
8
9
10
11
class Dependency { 
val libraries = ArrayList<String>()
fun implementation(lib: String) {
libraries.add(lib)
}
}
fun dependencies(block: Dependency.() -> Unit): List<String> {
val dependency = Dependency()
dependency.block()
return dependency.libraries
}

好了,现在可以使用以下的语法,build.gradle文件如下:

1
2
3
4
dependencies { 
implementation("com.squareup.retrofit2:retrofit:2.6.1")
implementation("com.squareup.retrofit2:converter-gson:2.6.1")
}

这就是Kotlin版本的Gradle构建文件。

Material Design实战

Material Design

简介

在2014年Google I/O大会上重磅推出的一套全新的界面设计语言——Material Design。

ToolBar

colorAccent,表达了一种强调的意思,比如一些控件的选中状态也会使用colorAccent的颜色;

使用xmlns:app指定了一个新的命名空间,这是由于许多Material属性是在新系统中新增的,老系统中并不存在,为了能够兼容老系统,就不能使用android:attribute这样的写法了,而是应该使用app:attribute;

Toolbar比较常用的功能:

  1. 修改标题栏上显示的文字内容;
  2. 通过标签来定义action按钮。

滑动菜单

滑动菜单由两部分组成:DrawerLayout和NavigationView。

其余不赘述。

悬浮按钮和可交互提示

FloatingActionButton即悬浮按钮控件。

Snackbar是在屏幕下展示的提示性按钮。

CoordinatorLayout可以说是一个加强版的FrameLayout,由AndroidX库提供,它拥有一些额外的Material能力。

卡片式布局

MaterialCardView是实现卡片式布局效果的重要部件。MaterialCardView也是一个FrameLayout,只是额外提供了圆角和阴影等效果,看上去会有立体的感觉。

Glide是一个超级强大的开源图片加载库,它不仅可以用于加载本地图片,还可以加载网络图片、GIF图片甚至是本地视频。

AppBarLayout

AppBarLayout是一个垂直方向的LinearLayout,它在内部做了很多滚动事件的封装,并应用了一些Material Design的设计理念。

下拉刷新

SwipeRefreshLayout就是用于实现下拉刷新功能的核心类,由AndroidX库提供。

可折叠式标题栏

CollapsingToolbarLayout是一个作用于Toolbar基础之上的布局,它也是由Material库提供的。


Kotlin课堂

本章节介绍了Toast、Snackbar的简化使用方法,没有新的语法点,不赘述。


Git时间

1
2
3
4
5
6
7
git branch [name] # 创建一个分支
git checkout [name] # 切换分支
git branch -D [name] # 分支删除
git merge [name] # 合并name到当前分支
git push origin master # origin部分指定的是远程版本库的Git地址,master部分指定的是同步到哪一个分支上
git fetch origin master # 同步下来的代码并不会合并到任何分支上,而是会存放到一个origin/master分支上
git pull origin master # 而pull命令则是相当于将fetch和merge这两个命令放在一起执行

网络技术

网络技术

WebView

用于在程序里简单展示网页。

注意,使用网络功能需要访问权限的。

使用HTTP 访问网络

对于HTTP,需要在改AndroidManifest.xml中配置,允许进行http的明文数据传输。

简单使用

主要使用HttpURLConnection实现。

注意,常用的Http请求有两种,GET和POST。GET表示获取数据,POST表示提交数据,每条数据使用键值对形式存在,中间使用&隔开。

OkHttp

这是第三方开源的项目,使用如下:

  1. 添加依赖;
  2. 创建一个OkHttpClient实例;
  3. 创建Request对象,并在.build()方法之前可以添加很多前缀来丰富这个对象;
  4. 调用newCall()方法来创建Call对象,并调用execute()来执行。

解析XML数据

网络返回数据主要是xml格式或者json格式的。

解析xml格式的数据主要方式为Pull和SAX两种方式。

Pull解析

  1. 创建XmlPullParserFactory实例,并借助这个实例得到XmlPullParser对象;
  2. 调用XmlPullParser的setInput()方法将服务器返回的XML数据设置进去;
  3. 通过getEventType()可以得到当前的解析事件,然后在一个while循环中不断地进行解析,如果当前的解析事件不等于XmlPullParser.END_DOCUMENT,说明解析工作还没完成,调用next()方法后可以获取下一个解析事件。

SAX解析

  1. 新建一个类继承自DefaultHandler,并重写父类的5个方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class MyHandler : DefaultHandler() { 
    // 解析开始时调用
    override fun startDocument() {
    }
    // 开始解析节点时调用
    override fun startElement(uri: String, localName: String, qName: String, attributes: Attributes) {
    }
    // 获取节点内容时调用
    override fun characters(ch: CharArray, start: Int, length: Int) {
    }
    // 结束解析该节点时调用
    override fun endElement(uri: String, localName: String, qName: String) {
    }
    // 结束解析时调用
    override fun endDocument() {
    }
    }
  2. 创建一个SAXParserFactory的对象;

  3. 然后再获取XMLReader对象;

  4. 接着将我们编写的ContentHandler的实例设置到XMLReader中,最后调用parse()方法开始执行解析。

解析JSON数据

JSON的好处在于体积小,能装载的信息量更大。

JSON的解析方式很多种,推荐使用官方的JSONObject和谷歌开源GSON。

JSONObject解析

基本就是键值对的映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private fun parseJSONWithJSONObject(jsonData: String) { 
try {
val jsonArray = JSONArray(jsonData)
for (i in 0 until jsonArray.length()) {
val jsonObject = jsonArray.getJSONObject(i)
val id = jsonObject.getString("id")
val name = jsonObject.getString("name")
val version = jsonObject.getString("version")
Log.d("MainActivity", "id is $id")
Log.d("MainActivity", "name is $name")
Log.d("MainActivity", "version is $version")
}
} catch (e: Exception) {
e.printStackTrace()
}
}

GSON解析

它的强大之处就在于可以将一段JSON格式的字符串自动映射成一个对象

使用GSON需要添加依赖库。

1
2
3
4
5
6
7
8
9
10
class Person(val name:String, val age:Int)

// 数据:{"name":"Tom","age":20}
val gson = Gson()
val person = gson.fromJson(jsonData, Person::class.java)

// 如果需要解析的是一段JSON数组,需要借助TypeToken
// [{"name":"Tom","age":20}, {"name":"Jack","age":25}, {"name":"Lily","age":22}]
val typeOf = object : TypeToken<List<Person>>() {}.type
val people = gson.fromJson<List<Person>>(jsonData, typeOf)

网络回调

实现网络回调的方式可以手动实现,具体逻辑为:

  1. 创建一个监听器类,创建完成时的处理方法和错误时的处理方法;
  2. 开启线程发起网络请求,收到网络回复之后调用监听器的完成方法,遇到错误调用监听器的错误方法;

如果使用OkHttp库,可以使用sendOkHttpRequest()方法中的okhttp3.Callback参数,直接编写回调类传入即可。

Retrofit——最好用

这个和OkHttp库师出同门,是在OkHttp和GSON基础上的进一步封装,面向开发者更加友好,更加体现面向对象的编程思维。

而Retrofit的用法就是基于以下几点来设计的:

  1. 首先我们可以配置好一个根路径,然后在指定服务器接口地址时只需要使用相对路径即可;
  2. 另外,Retrofit允许我们对服务器接口进行归类,将功能同属一类的服务器接口定义到同一个接口文件当中,从而让代码结构变得更加合理。
  3. 最后,我们也完全不用关心网络通信的细节,只需要在接口文件中声明一系列方法和返回值,然后通过注解的方式指定该方法对应哪个服务器接口,以及需要提供哪些参数。当我们在程序中调用该方法时,Retrofit会自动向对应的服务器接口发起请求,并将响应的数据解析成返回值声明的类型。

具体用法不赘述,可以查看书籍,很好懂的。


Kotlin课堂

主要和协程相关。

它其实和线程是有点类似的,可以简单地将它理解成一种轻量级的线程。线程是非常重量级的,它需要依靠操作系统的调度才能实现不同线程之间的切换。协程仅在编程语言的层面实现不同协程之间的切换,从而大大提升了并发编程的运行效率。

协程不吃资源,线程很吃操作系统资源的。

基本用法

协程没有定义在Kotlin的官方API里,需要添加依赖才能够使用。

GlobalScope.launch

开启协程,GlobalScope.launch函数可以创建一个协程的作用域,每次创建的都是一个顶层协程,这种协程当应用程序运行结束时也会跟着一起结束。

1
2
3
4
5
6
7
8
fun main() { 
GlobalScope.launch {
println("codes run in coroutine scope")
delay(1500)
println("codes run in coroutine scope finished")
}
Thread.sleep(1000)
}

delay()函数是一个非阻塞式的挂起函数,它只会挂起当前协程,并不会影响其他协程的运行。而Thread.sleep()方法会阻塞当前的线程,这样运行在该线程下的所有协程都会被阻塞。注意,delay()函数只能在协程的作用域或其他挂起函数中调用。

runBlocking

runBlocking函数同样会创建一个协程的作用域,但是它可以保证在协程作用域内的所有代码和子协程没有全部执行完之前一直阻塞当前线程。需要注意的是,runBlocking函数通常只应该在测试环境下使用,在正式环境中使用容易产生一些性能上的问题

1
2
3
4
5
6
7
fun main() { 
runBlocking {
println("codes run in coroutine scope")
delay(1500)
println("codes run in coroutine scope finished")
}
}

创建多条协程

使用launch函数。注意这里的launch函数和我们刚才所使用的GlobalScope.launch函数不同。首先它必须在协程的作用域中才能调用,其次它会在当前协程的作用域下创建子协程。GlobalScope.launch函数创建的永远是顶层协程,这一点和线程比较像,因为线程也没有层级这一说,永远都是顶层的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun main() { 
runBlocking {
launch {
println("launch1")
delay(1000)
println("launch1 finished")
}
launch {
println("launch2")
delay(1000)
println("launch2 finished")
}
}
}

挂起函数

Kotlin提供了一个suspend关键字,使用它可以将任意函数声明成挂起函数,而挂起函数之间都是可以互相调用的。

1
2
3
4
suspend fun printDot() { 
println(".")
delay(1000)
}

suspend关键字只能将一个函数声明成挂起函数,无法提供协程作用域,因此无法在内部调用launch函数。

coroutineScope

coroutineScope函数也是一个挂起函数,因此可以在任何其他挂起函数中调用。它的特点是会继承外部的协程的作用域并创建一个子协程,借助这个特性,我们就可以给任意挂起函数提供协程作用域了。另外,coroutineScope函数和runBlocking函数还有点类似,它可以保证其作用域内的所有代码和子协程在全部执行完之前,外部的协程会一直被挂起。

但是coroutineScope函数只会阻塞当前协程,既不影响其他协程,也不影响任何线程,因此是不会造成任何性能上的问题的。

更多作用域

常用写法

不太建议使用顶层协程,管理成本很高:

1
2
3
4
val job = GlobalScope.launch { 
// 处理具体的逻辑
}
job.cancel()

使用job.cancel() 可以取消协程,主要是每一个顶层协程都需要一一取消。

实际项目中常用写法:

1
2
3
4
5
6
val job = Job() 
val scope = CoroutineScope(job)
scope.launch {
// 处理具体的逻辑
}
job.cancel()

注意这里的CoroutineScope()是个函数,虽然它的命名更像是一个类。CoroutineScope()函数会返回一个CoroutineScope对象,这种语法结构的设计更像是我们创建了一个CoroutineScope的实例,可能也是Kotlin有意为之的。

现在所有调用CoroutineScope的launch函数所创建的协程,都会被关联在Job对象的作用域下面。这样只需要调用一次cancel()方法,就可以将同一作用域内的所有协程全部取消,从而大大降低了协程管理的成本。

获取执行结果

获取协程的执行结果,使用async函数。async函数必须在协程作用域当中才能调用,它会创建一个新的子协程并返回一个Deferred对象,如果我们想要获取async函数代码块的执行结果,只需要调用Deferred对象的await()方法即可。

1
2
3
4
5
6
7
8
fun main() { 
runBlocking {
val result = async {
5 + 5
}.await()
println(result)
}
}

async函数是一个串行的关系。

withContext

withContext()函数是一个挂起函数,大体可以将它理解成async函数的一种简化版写法。

1
2
3
4
5
6
7
8
fun main() { 
runBlocking {
val result = withContext(Dispatchers.Default) {
5 + 5
}
println(result)
}
}

调用withContext()函数之后,会立即执行代码块中的代码,同时将外部协程挂起。当代码块中的代码全部执行完之后,会将最后一行的执行结果作为withContext()函数的返回值返回。

withContext()函数强制要求我们指定一个线程参数,线程参数主要有以下3种值可选:Dispatchers.Default、Dispatchers.IO和Dispatchers.Main。

  • Dispatchers.Default表示会使用一种默认低并发的线程策略,当你要执行的代码属于计算密集型任务时,开启过高的并发反而可能会影响任务的运行效率,此时就可以使用Dispatchers.Default。
  • Dispatchers.IO表示会使用一种较高并发的线程策略,当你要执行的代码大多数时间是在阻塞和等待中,比如说执行网络请求时,为了能够支持更高的并发数量,此时就可以使用Dispatchers.IO。
  • Dispatchers.Main则表示不会开启子线程,而是在Android主线程中执行代码,但是这个值只能在Android项目中使用,纯Kotlin程序使用这种类型的线程参数会出现错误。

简化回调

只需要借助suspendCoroutine函数就能将传统回调机制的写法大幅简化。

suspendCoroutine函数必须在协程作用域或挂起函数中才能调用,它接收一个Lambda表达式参数,主要作用是将当前协程立即挂起,然后在一个普通的线程中执行Lambda表达式中的代码。Lambda表达式的参数列表上会传入一个Continuation参数,调用它的resume()方法或resumeWithException()可以让协程恢复执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
HttpUtil.sendHttpRequest(address, object : HttpCallbackListener { 
override fun onFinish(response: String) {
// 得到服务器返回的具体内容
}
override fun onError(e: Exception) {
// 在这里对异常情况进行处理
}
})

// 等价于以下写法
suspend fun request(address: String): String {
return suspendCoroutine { continuation ->
HttpUtil.sendHttpRequest(address, object : HttpCallbackListener {
override fun onFinish(response: String) {
continuation.resume(response)
}
override fun onError(e: Exception) {
continuation.resumeWithException(e)
}
})
}
}

事实上,suspendCoroutine函数几乎可以用于简化任何回调的写法。

后台Service

探究Service

Service并不是运行在一个独立的进程当中的,而是依赖于创建Service时所在的应用程序进程。当某个应用程序进程被杀掉时,所有依赖于该进程的Service也会停止运行。

另外,也不要被Service的后台概念所迷惑,实际上Service并不会自动开启线程,所有的代码都是默认运行在主线程当中的。

我们需要在Service的内部手动创建子线程,并在这里执行具体的任务,否则就有可能出现主线程被阻塞的情况。

多线程编程

基本用法

  • 继承Thread,然后重写父类的run()方法,并调用start()方法启动;
  • 使用Runnable接口,并调用start方法;
  • thread顶层函数(Kotlin),在里面写Lambda即可。

更新UI

Android的UI是线程不安全的。要更新应用程序里的UI元素,必须在主线程中进行,否则就会出现异常,因此常常使用异步消息处理的方式来更新UI元素。

Android中的异步消息处理主要由4个部分组成:Message、Handler、MessageQueue和Looper。

  • Message是在线程之间传递的消息,它可以在内部携带少量的信息,用于在不同线程之间传递数据。
  • Handler顾名思义也就是处理者的意思,它主要是用于发送和处理消息的。
  • MessageQueue是消息队列的意思,它主要用于存放所有通过Handler发送的消息。
  • Looper是每个线程中的MessageQueue的管家,调用Looper的loop()方法后,就会进入一个无限循环当中,然后每当发现MessageQueue中存在一条消息时,就会将它取出,并传递到**Handler的handleMessage()**方法中。

异步消息处理流程:

  1. 首先需要在主线程当中创建一个Handler对象,并重写handleMessage()方法。
  2. 然后当子线程中需要进行UI操作时,就创建一个Message对象,并通过Handler将这条消息发送出去。
  3. 之后这条消息会被添加到MessageQueue的队列中等待被处理,而Looper则会一直尝试从MessageQueue中取出待处理消息,最后分发回Handler的handleMessage()方法中。
  4. 由于Handler在主线程中创建,所以handleMessage()方法中的代码也会在主线程中运行,于是我们在这里就可以安心地进行UI操作了。

QQ_1737703522609

安卓也使用AsyncTask工具来在子线程中进行UI操作,其基本使用步骤如下:

  1. 继承AsyncTask类,指定三个泛型参数(传入参数、进度单位、结果返回);
  2. 重写AsyncTask的方法,如onPreExecute(),doInBackground(),onProgressUpdate(),onPostExecute();
  3. 调用execute()启动任务。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class DownloadTask : AsyncTask<Unit, Int, Boolean>() { 

override fun onPreExecute() {
progressDialog.show() // 显示进度对话框
}

override fun doInBackground(vararg params: Unit?) = try {
while (true) {
val downloadPercent = doDownload() // 这是一个虚构的方法
publishProgress(downloadPercent)
if (downloadPercent >= 100) {
break
}
}
true
} catch (e: Exception) {
false
}

override fun onProgressUpdate(vararg values: Int?) {
// 在这里更新下载进度
progressDialog.setMessage("Downloaded ${values[0]}%")
}

override fun onPostExecute(result: Boolean) {
progressDialog.dismiss()// 关闭进度对话框
// 在这里提示下载结果
if (result) {
Toast.makeText(context, "Download succeeded", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, " Download failed", Toast.LENGTH_SHORT).show()
}
}

}

Service

创建

  1. 继承Service类;
  2. 按功能重写生命周期回调函数;
  3. 在AndroidManifest.xml文件中注册。

启动和停止

启动和停止借助Intent实现。

现在只有当应用保持在前台可见状态的情况下,Service才能保证稳定运行,一旦应用进入后台之后,Service随时都有可能被系统回收。

如果你真的非常需要长期在后台执行一些任务,可以使用前台Service或者WorkManager。

区别:onCreate()方法是在Service第一次创建的时候调用的,而onStartCommand()方法则在每次启动Service的时候都会调用。

和Activity通信

使用binder类实现通信。

  1. 在service中创建binder类,根据自己的需要写方法,并实例化;
  2. 在service的onBinder函数中返回自己的实例化binder;
  3. 在Activity中借助Intent,使用bindService()绑定Service;
  4. 使用unbindService()可以解绑。

生命周期

63d2db246138ca89cd35f0285fb69d92

根据Android系统的机制,一个Service只要被启动或者被绑定了之后,就会处于运行状态,必须要让以上两种条件同时不满足,Service才能被销毁。所以,这种情况下要同时调用stopService()和 unbindService()方法,onDestroy()方法才会执行。

前台Service

前台Service和普通Service最大的区别就在于,它一直会有一个正在运行的图标在系统的状态栏显示,下拉状态栏后可以看到更加详细的信息,非常类似于通知的效果。

前台Service的创建很大程度上和通知的创建类似,唯一不同是调用startForeground()进行通知显示。此外,前台Service需要进行权限声明。

IntentService

本质是简化Android多线程编程,提供的一个异步的、会自动停止的Service。

  1. 这里首先要求必须先调用父类的构造函数,并传入一个字符串,这个字符串可以随意指定,只在调试的时候有用;
  2. 实现onHandleIntent方法,处理耗时逻辑;
  3. 可以重写了onDestroy()方法,进行结尾工作。

[!CAUTION]

以下内容晦涩难懂,出于自己单方面理解的内容居多,不保证下面的理解是正确的。

如果你发现不正确的地方,可以在评论区指出。

Kotlin课堂

泛型的高级特性

主要学习泛型的实化、协变和逆变。

弄懂泛型之前,首先区别一件事情,一般的类型转化。

在进行非泛型的赋值时,子类对象可以赋值给父类对象的,但是父类对象不能赋值给子类对象。其实原因很好懂,子类包含比父类更多的信息和功能,子类赋值给父类无非就是将多余的信息和功能给禁用掉;相反,父类赋值给子类,则需要增添对于的信息和功能才行,很明显这是不可能的。同时,父类转向子类或者不同子类之间相互转换也是不被允许的(强制转换暂时不讨论)。

而在泛型类型中,Kotlin 不允许类型的替换或转换,因为泛型的类型在编译时已经是固定的,不会发生类型推断,因此不加特殊语法,不能实现协变和逆变的功能。

实化

Java的泛型擦除机制:即对于泛型的约束只在编译时期存在,运行的时候不进行约束(这是兼容性导致的)。所有基于JVM的语言,它们的泛型功能都是通过类型擦除机制来实现的,包括Kotlin。

简而言之,不允许将子类的泛型对象赋值给父类的泛型类型声明。

类型擦除就是在编译时期将泛型位置替换为泛型上界,如果没有设置泛型上界就替换为object。

由于Kotlin有内联函数,因此可以稍微改善泛型擦除机制的问题。使用reified关键字可以将泛型实化,通俗来讲就是将泛型类型使用传入的类型进行替换,更加偏重于内联的功能。

1
2
// 以下写法就是合理的
inline fun <reified T> getGenericType() = T::class.java

协变

协变通俗简短来讲就是允许子类对象给父类的泛型类型声明,同时设置为可读。

错误的根本原因:泛型不变性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class SimpleData<T> { 
private var data: T? = null

fun set(t: T?) {
data = t
}

fun get(): T? {
return data
}
}

fun main() {
val student = Student("Tom", 19)
val data = SimpleData<Student>()
data.set(student)

// 错误:无法将 SimpleData<Student> 传递给 SimpleData<Person>
// 在泛型类型中,Kotlin 不允许类型的替换或转换,因为泛型的类型在编译时已经是固定的,不会发生类型推断。
// 这是协变和逆变存在的原因
handleSimpleData(data) // 这里报错
val studentData = data.get()
}

// 这是协变不能写入的原因
// 光看这个函数,这是可行的。
// 但是传入的是SimpleData<Student>类型,只是屏蔽信息成为SimpleData<Person>类型
fun handleSimpleData(data: SimpleData<Person>) {
val teacher = Teacher("Jack", 35)
data.set(teacher) // 子类型之间强制转化失败,会报错
}

关键字是out

逆变

协变通俗简短来讲就是允许父类对象给子类的泛型类型声明,同时设置为可写。

错误的根本原因:泛型不变性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 例子1:
interface Transformer<T> {
fun transform(t: T): String
}

fun main() {
val trans = object : Transformer<Person> {
override fun transform(t: Person): String {
return "${t.name} ${t.age}"
}
}
// 错误:无法将 SimpleData<Person> 传递给 SimpleData<Student>
// 在泛型类型中,Kotlin 不允许类型的替换或转换,因为泛型的类型在编译时已经是固定的,不会发生类型推断。
// 这是协变和逆变存在的原因
handleTransformer(trans) // 这行代码会报错
}

// 这是逆变不能写入的原因
// 光看这个函数,这是可行的。
// 但是传入的是SimpleData<Person>类型,只是假设成为SimpleData<Student>类型,因此是不可读的
fun handleTransformer(trans: Transformer<Student>) {
val student = Student("Tom", 19)
val result = trans.transform(student) // 这里是正确的,将子类型转化为父类型
}


// 例子2:
interface Transformer<T> {
fun transform(name: String, age: Int): T
}
fun main() {
val trans = object : Transformer<Person> {
override fun transform(name: String, age: Int): Person {
return Teacher(name, age)
}
}
handleTransformer(trans)
}
fun handleTransformer(trans: Transformer<Student>) {
val result = trans.transform("Tom", 19) // 强制转化,将Teacher转化为Student错误
}

关键字是in

使用多媒体

多媒体

使用通知

创建通知

每条通知都要属于一个对应的渠道,通知渠道一旦创建之后就不能再修改。

  1. 首先需要一个NotificationManager对通知进行管理,可以通过调用Context的getSystemService()方法获取;
  2. 接下来要使用NotificationChannel类构建一个通知渠道,并调用NotificationManager的createNotificationChannel()方法完成创建。
  3. AndroidX库中提供了一个NotificationCompat类,使用这个类的构造器创建Notification对象;
  4. 调用NotificationManager的notify()方法让通知显示出来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MainActivity : AppCompatActivity() { 

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as
NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel("normal", "Normal",NotificationManager.
IMPORTANCE_DEFAULT)
manager.createNotificationChannel(channel)
}
sendNotice.setOnClickListener {
val notification = NotificationCompat.Builder(this, "normal")
.setContentTitle("This is content title")
.setContentText("This is content text")
.setSmallIcon(R.drawable.small_icon)
.setLargeIcon(BitmapFactory.decodeResource(resources,
R.drawable.large_icon))
.build()
manager.notify(1, notification)
}
}
}

创建通知渠道的代码只在第一次执行的时候才会创建,当下次再执行创建代码时,系统会检测到该通知渠道已经存在了,因此不会重复创建,也并不会影响运行效率.

点击通知

使用PendingIntent实现通知的点击效果。

PendingIntent从名字上看起来就和Intent有些类似,PendingIntent倾向于在某个合适的时机执行某个动作。所 以,也可以把PendingIntent简单地理解为延迟执行的Intent。

  1. 创建意图;
  2. 获取PendingIntent的实例,可以根据需求来选择是使用getActivity()方法、getBroadcast()方法,还是getService()方法;
  3. 在构造器NotificationCompat.Builder中连缀一个setContentIntent()方法;
  4. 连缀setAutoCancel()方法设置通知消失或者显示调用NotificationManager的cancel()方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MainActivity : AppCompatActivity() { 

override fun onCreate(savedInstanceState: Bundle?) {
...
sendNotice.setOnClickListener {
val intent = Intent(this, NotificationActivity::class.java)
val pi = PendingIntent.getActivity(this, 0, intent, 0)
val notification = NotificationCompat.Builder(this, "normal")
.setContentTitle("This is content title")
.setContentText("This is content text")
.setSmallIcon(R.drawable.small_icon)
.setLargeIcon(BitmapFactory.decodeResource(resources,
R.drawable.large_icon))
.setContentIntent(pi)
.setAutoCancel(true)
.build()
manager.notify(1, notification)
}
}

}

设置通知样式

使用setStyle()方法,传入不同对象。比如NotificationCompat.BigTextStyle对象,这个对象就是用于封装长文字信息的;NotificationCompat.BigPictureStyle对象,这个对象就是用于设置大图片的。

设置重要等级

开发者只能在创建通知渠道的时候为它指定初始的重要等级,如果用户不认可这个重要等级的话,可以随时进行修改,开发者对此无权再进行调整和变更

调用摄像头和相册

调用摄像头

  1. 创建File对象,用于存放摄像头拍摄的照片;
  2. 判断设备运行版本,如果运行设备的系统版本低于Android 7.0,就调用Uri的fromFile()方法将File对象转换成Uri对象,这个Uri对象标识着output_image.jpg这张图片的本地真实路径。否则,就调用FileProvider的getUriForFile()方法将File对象转换成一个封装过的Uri对象;
  3. 构建意图对象,并将意图指定为获取图片,并调用Intent的putExtra()方法指定图片的输出地址;
  4. 最后调用startActivityForResult()启动Activity,并重写onActivityResult()方法,可调用BitmapFactory的decodeStream()方法将output_image.jpg这张照片解析成Bitmap对象,然后把它设置到ImageView中显示出来。

由于使用了FileProvider,它的本质是一个向外提供的ContentProvider,因此一旦使用,必须要进行注册。

调用相册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class MainActivity : AppCompatActivity() { 
...
val fromAlbum = 2
override fun onCreate(savedInstanceState: Bundle?) {
...
fromAlbumBtn.setOnClickListener {
// 打开文件选择器
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
// 指定只显示图片
intent.type = "image/*"
startActivityForResult(intent, fromAlbum)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
...
fromAlbum -> {
if (resultCode == Activity.RESULT_OK && data != null) {
data.data?.let { uri ->
// 将选择的图片显示
val bitmap = getBitmapFromUri(uri)
imageView.setImageBitmap(bitmap)
}
}
}
}
}
private fun getBitmapFromUri(uri: Uri) = contentResolver
.openFileDescriptor(uri, "r")?.use {
BitmapFactory.decodeFileDescriptor(it.fileDescriptor)
}
...
}

这样通过文件管理器的方式实现似乎不需要进行注册额外权限。

目前我们的实现还不算完美,因为如果某些图片的像素很高,直接加载到内存中就有可能会导致程序崩溃。更好的做法是根据项目的需求先对图片进行适当的压缩,然后再加载到内存中。

播放多媒体文件

在Android中播放音频文件一般是使用MediaPlayer类实现的。

QQ_1737181035574

播放视频文件其实并不比播放音频文件复杂,主要是使用VideoView类来实现的。

QQ_1737181087153


Kotlin课堂

infix 函数

Kotlin提供了一种高级语法糖特性:infix函数,把编程语言函数调用的语法规则进行调整。

1
2
3
4
infix fun String.beginsWith(prefix: String) = startsWith(prefix) 
if ("Hello Kotlin" beginsWith "Hello") {
// 处理具体的逻辑
}

infix函数允许我们将函数调用时的小数点、括号等计算机相关的语法去掉,从而使用一种更接近英语的语法来编写程序,让代码看起来更加具有可读性

infix函数有两个比较严格的限制:

  • 首先,infix函数是不能定义成顶层函数的,它必须是某个类的成员函数,可以使用扩展函数的方式将它定义到某个类当中;
  • 其次,infix函数必须接收且只能接收一个参数,至于参数类型是没有限制的。

典型应用:to结构

1
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that) 

Git进阶

使用.gitignore文件进行管理,Android Studio在创建项目的时候会自动帮我们创建出两个.gitignore 文件,进行初步的版本控制忽略。

以下是常见的命令:

1
2
3
4
5
git status # 查看修改情况
git diff [文件路径] # 查看文件变动情况 [文件路径]可忽略
git checkout [文件路径] # 撤销修改,只能撤销还未add的修改
git reset HEAD [文件路径] # 撤销已经add的修改
git log [id] # 查看提交记录,使用ID可以进行具体的查看

跨程序共享数据

ContentProvider

ContentProvider主要用于在不同的应用程序之间实现数据共享的功能。ContentProvider可以选择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄漏的风险。

运行时权限

Android现在将常用的权限大致归成了两类,一类是普通权限,一类是危险权限。

  • 普通权限指的是那些不会直接威胁到用户的安全和隐私的权限,对于这部分权限申请,系统会自动帮我们进行授权,不需要用户手动操作。
  • 危险权限则表示那些可能会触及用户隐私或者对设备安全性造成影响的权限,如获取设备联系人信息、定位设备的地理位置等,对于这部分权限申请,必须由用户手动授权才可以。

QQ_1737172894800

  1. 第一步先判断用户是不是已经给过我们授权了,借助的是ContextCompat.checkSelfPermission()方法;
  2. 如果没有授权的话,则需要调用ActivityCompat.requestPermissions()方法向用户申请授权;
  3. 重写回调onRequestPermissionsResult()函数,而授权的结果则会封装在grantResults参数当中。这里需要判断一下最后的授权结果写逻辑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class MainActivity : AppCompatActivity() { 

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
makeCall.setOnClickListener {
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.CALL_PHONE), 1)
} else {
call()
}
}
}

override fun onRequestPermissionsResult(requestCode: Int,
permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
1 -> {
if (grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED) {
call()
} else {
Toast.makeText(this, "You denied the permission",
Toast.LENGTH_SHORT).show()
}
}
}
}

private fun call() {
try {
val intent = Intent(Intent.ACTION_CALL)
intent.data = Uri.parse("tel:10086")
startActivity(intent)
} catch (e: SecurityException) {
e.printStackTrace()
}
}

}

访问其他程序中的数据

ContentProvider的用法一般有两种:一种是使用现有的ContentProvider读取和操作相应程序中的数据;另一种是创建自己的ContentProvider,给程序的数据提供外部访问接口。

如果想要访问ContentProvider中共享的数据,就一定要借助ContentResolver类,可以通过Context中的getContentResolver()方法获取该类的实例。ContentResolver中提供了一系列的方法用于对数据进行增删改查操作,其中insert()方法用于添加数据,update()方法用于更新数据,delete()方法用于删除数据,query()方法用于查询数据。

不同于SQLiteDatabase,ContentResolver中的增删改查方法都是不接收表名参数的,而是使用一个Uri参数代替,这个参数被称为内容URI。内容URI给ContentProvider中的数据建立了唯一标识符,它主要由两部分组成:authority和path。我们还需要在内容URI字符串的头部加上协议声明content。

  • authority是用于对不同的应用程序做区分的,一般为了避免冲突,会采用应用包名的方式进行命名。
  • path则是用于对同一应用程序中不同的表做区分的,通常会添加到authority的后面。
1
2
content://com.example.app.provider/table1 
content://com.example.app.provider/table2

创建自己的ContentProvider

可以通过新建一个类去继承ContentProvider的方式来实现。ContentProvider类中有6个抽象方法,我们在使用子类继承它的时候,需要将这6个方法全部重写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MyProvider : ContentProvider() { 
override fun onCreate(): Boolean {
return false
}
override fun query(uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
return null
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
return null
}

override fun update(uri: Uri, values: ContentValues?, selection: String?,
selectionArgs: Array<String>?): Int {
return 0
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
return 0
}
override fun getType(uri: Uri): String? {
return null
}
}

借助UriMatcher这个类就可以轻松地实现匹配内容URI的功能。

getType()方法。它是所有的ContentProvider都必须提供的一个方法,用于获取Uri对象所对应的MIME类型

MIME字符串主要由3部分组成,Android对这3个部分做了如下格式规定

  • 必须以vnd开头。
  • 如果内容URI以路径结尾,则后接android.cursor.dir/;如果内容URI以id结尾,则后接android.cursor.item/。
  • 最后接上vnd.<authority>.<path>。

注意,自己提供ContentProvider需要使用<provider>进行注册。


Kotlin课堂

泛型

泛型允许我们在不指定具体类型的情况下进行编程,这样编写出来的代码将会拥有更好的扩展性。

泛型主要有两种定义方式:一种是定义泛型类,另一种是定义泛型方法。

1
2
3
4
5
6
7
8
9
10
11
class MyClass<T> { 
fun method(param: T): T {
return param
}
}

class MyClass {
fun <T> method(param: T): T {
return param
}
}

Kotlin还拥有非常出色的类型推导机制,可以在函数内部进行推导,也可以根据传参进行推导,常常可以省略指定泛型类型。

Kotlin还允许我们对泛型的类型进行限制。比如将method()方法的泛型上界设置为Number类型,只能将method()方法的泛型指定成数字类型,比如Int、Float、Double等。但是如果你指定成字符串类型,就肯定会报错。

1
2
3
4
5
6
7
class MyClass { 

fun <T : Number> method(param: T): T {
return param
}

}

委托

委托是一种设计模式,它的基本理念是:操作对象自己不会去处理某段逻辑,而是会把工作委托给另外一个辅助对象去处理。

Kotlin中支持委托功能的,并且将委托功能分为了两种:类委托和委托属性

类委托

类委托的核心思想是将一个类的具体实现委托给另一个类去完成。

我们让大部分的方法实现调用辅助对象中的方法,少部分的方法实现由自己来重写,甚至加入一些自己独有的方法,这就是委托模式的意义所在。(在一定程度上拓展函数也可以办到?)

Kotlin中委托使用的关键字是by:

1
2
class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet { 
}

属性委托

委托属性的核心思想是将一个属性(字段)的具体实现委托给另一个去完成。

注意都是委托给类完成!!!

1
2
3
class MyClass { 
var p by Delegate()
}

使用by关键字连接了左边的p属性和右边的Delegate实例,这种写法就代表着将p属性的具体实现委托给了Delegate类去完成。当调用p属性的时候会自动调用Delegate类的getValue()方法,当给p属性赋值的时候会自动调用Delegate类的setValue()方法。

在Delegate类中我们必须实现getValue()和setValue()这两个方法,并且都要使用operator关键字进行声明。

懒加载解密

对by lazy的工作原理进行解密了,它的基本语法结构如下:

1
val p by lazy { ... } 

lazy在这里只是一个高阶函数而已。在lazy函数中会创建并返回一个Delegate对象,当我们调用p属性的时候,其实调用的是Delegate对象的getValue()方法,因此只有在被调用时候才会加载。

数据存储

持久化方案

Android系统中主要提供了3种方式用于简单地实现数据持久化功能:文件存储、SharedPreferences存储以及数据库存储。

文件存储

核心技术就是Context类中提供的openFileInput()和openFileOutput()方法,之后就是利用各种流来进行读写操作。

写入数据

  1. 创建openFileOutput()方法,可以用于将数据存储到指定的文件中;
  2. 构建出一个OutputStreamWriter对象,接着再使用OutputStreamWriter构建出一个BufferedWriter对象;
  3. 使用一个use函数,保证在Lambda 表达式中的代码全部执行完之后自动将外层的流关闭,不需要我们手动去关闭流。

文件名不可以包含路径,因为所有的文件都默认存储到/data/data/<package name>/files/目录下。

使用“Device File Explorer”可以查看设备文件。

读取数据

  1. 使用openFileInput()函数进行读取,并返回一个FileInputStream对象;
  2. 构建出一个InputStreamReader对象,接着再使用InputStreamReader构建出一个BufferedReader对象;
  3. 使用一个use函数,通过BufferedReader将文件中的数据一行行读取出来。

SharedPreferences 存储

SharedPreferences是使用键值对的方式来存储数据的。

存储

SharedPreferences文件都是存放在/data/data/<package name>/shared_prefs/目录下的。

  1. 首先需要获取SharedPreferences对象;
  2. 调用SharedPreferences对象的edit()方法获取一个SharedPreferences.Editor对象;
  3. 向SharedPreferences.Editor对象中添加数据,比如添加一个布尔型数据就使用putBoolean()方法,添加一个字符串则使用putString()方法,以此类推;
  4. 调用apply()方法将添加的数据提交,从而完成数据存储操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MainActivity : AppCompatActivity() { 

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
saveButton.setOnClickListener {
val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
editor.putString("name", "Tom")
editor.putInt("age", 28)
editor.putBoolean("married", false)
editor.apply()
}
}

}

读取

  1. 首先通过getSharedPreferences()方法得到SharedPreferences对象;
  2. 然后分别调用它的getString()、getInt()和getBoolean()等方法即可获取数据。

SQLite 数据库存储

安装Database Navigator插件工具。

创建数据库

提供了一个SQLiteOpenHelper帮助类,借助这个类可以非常简单地对数据库进行创建和升级。

继承SQLiteOpenHelper抽象类,重写SQLiteOpenHelper中两个抽象方法:onCreate()和onUpgrade();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyDatabaseHelper(val context: Context, name: String, version: Int) : 
SQLiteOpenHelper(context, name, null, version) {

private val createBook = "create table Book (" +
" id integer primary key autoincrement," +
"author text," +
"price real," +
"pages integer," +
"name text)"

override fun onCreate(db: SQLiteDatabase) {
db.execSQL(createBook)
Toast.makeText(context, "Create succeeded", Toast.LENGTH_SHORT).show()
}

override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("drop table if exists Book")
db.execSQL("drop table if exists Category")
onCreate(db)
}

}

添加数据

操作数据有两种方案,一种是使用SQL语句,一种是使用Android提供的封装函数。

  • 使用SQL语句:除了查询数据的时候调用的是SQLiteDatabase的rawQuery()方法,其他操作都是调用的execSQL()方法。

  • 使用SQLiteDatabase对象,借助这个对象可以对数据进行CRUD操作:

    1. SQLiteDatabase中提供了一个insert()方法,专门用于添加数据;

    2. SQLiteDatabase中提供了一个非常好用的update()方法,用于对数据进行更新;

    3. SQLiteDatabase中提供了一个delete()方法,专门用于删除数据;

    4. SQLiteDatabase中还提供了一个query()方法用于对数据进行查询,这个方法的参数非常复杂,最短的一个方法重载也需要传入7个参数。

      QQ_1737171509823

使用事务

事务的特性可以保证让一系列的操作要么全部完成,要么一个都不会完成

  1. 首先调用SQLiteDatabase的beginTransaction()方法开启一个事务;
  2. 然后在一个异常捕获的代码块中执行具体的数据库操作;
  3. 当所有的操作都完成之后,调用setTransactionSuccessful()表示事务已经执行成功了;
  4. 最后在finally代码块中调用endTransaction()结束事务。

升级数据库的最佳写法的本质就是使用小于等于符号判定版本,并写若干个if语句。


Kotlin课堂

简化SharedPreferences 的用法

1
2
3
4
5
6
7
8
9
10
fun SharedPreferences.edit(block: SharedPreferences.Editor.() -> Unit) { 
val editor = edit()
editor.block()
editor.apply()
}
getSharedPreferences("data", Context.MODE_PRIVATE).edit {
putString("name", "Tom")
putInt("age", 28)
putBoolean("married", false)
}

其实Google提供的KTX扩展库中已经包含了上述 SharedPreferences的简化用法。

简化ContentValues 的用法

在Kotlin中使用A to B这样的语法结构会创建一个Pair对象。

使用vararg关键字,指可变参数列表,我们允许向这个方法传入任意多个参数,这些参数都会被赋值到使用vararg声明的这一个变量上面,然后使用for-in循环可以将传入的所有参数遍历出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fun cvOf(vararg pairs: Pair<String, Any?>): ContentValues { 
val cv = ContentValues()
for (pair in pairs) {
val key = pair.first
val value = pair.second
when (value) {
is Int -> cv.put(key, value)
is Long -> cv.put(key, value)
is Short -> cv.put(key, value)
is Float -> cv.put(key, value)
is Double -> cv.put(key, value)
is Boolean -> cv.put(key, value)
is String -> cv.put(key, value)
is Byte -> cv.put(key, value)
is ByteArray -> cv.put(key, value)
null -> cv.putNull(key)
}
}
return cv
}
val values = cvOf("name" to "Game of Thrones", "author" to "George Martin",
"pages" to 720, "price" to 20.85)
db.insert("Book", null, values)

Kotlin中的Smart Cast功能。比如when语句进入Int条件分支后,这个条件下面的value会被自动转换成Int类型,而不再是Any?类型。

KTX库中也提供 了一个具有同样功能的contentValuesOf()方法:

1
2
3
val values = contentValuesOf("name" to "Game of Thrones", "author" to "George Martin", 
"pages" to 720, "price" to 20.85)
db.insert("Book", null, values)

广播

广播机制

简介

Android中的广播主要可以分为两种类型:标准广播和有序广播。

  • 标准广播:异步执行的广播。
  • 有序广播:同步执行的广播,并且可截断。

接收广播

监听广播需要事先注册。注册 BroadcastReceiver的方式一般有两种:在代码中注册和在AndroidManifest.xml中注册。其中前者也被称为动态注册,后者也被称为静态注册。

动态注册

  1. 创建意图过滤器IntentFilter;
  2. 添加监听广播;
  3. 使用接收器添加意图。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MainActivity : AppCompatActivity() { 
lateinit var timeChangeReceiver: TimeChangeReceiver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val intentFilter = IntentFilter()
intentFilter.addAction("android.intent.action.TIME_TICK")
timeChangeReceiver = TimeChangeReceiver()
registerReceiver(timeChangeReceiver, intentFilter)
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(timeChangeReceiver)
}
}
}
inner class TimeChangeReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Toast.makeText(context, "Time has changed", Toast.LENGTH_SHORT).show()
}
}
}

如果你想查看完整的系统广播列表,可以到如下的路径中去查看:<Android SDK> /platforms/<任意android api版本>/data/broadcast_actions.txt

静态注册

所有隐式广播都不允许使用静态注册的方式来接收。隐式广播指的是那些没有具体指定发送给哪个应用程序的广播,大多数系统广播属于隐式广播,但是少数特殊的系统广播目前仍然允许使用静态注册的方式来接收。这些特殊的系统广播列表详见https://developer.android.google.cn/guide/components/broadcast-exceptions.html。

静态的BroadcastReceiver一定要在AndroidManifest.xml文件中注册才可以使用。在<receiver>标签中进行配置,并进行权限声明。

BroadcastReceiver中是不允许开启线程的,当onReceive()方法运行了较长时间而没有结束时,程序就会出现错误。因此回调函数中不要添加过多的逻辑或者耗时操作。


发送广播

标准广播

而默认情况下我们发出 的自定义广播都是隐式广播。因此这里一定要调用setPackage()方法,指定这条广播是发送给哪个应用程序的,从而让它变成一条显式广播,否则静态注册的BroadcastReceiver将无法接收到这条广播。

  1. 创建意图对象(Intent对象),并把要发送的广播的值传入;
  2. 传入包名;
  3. 发送广播。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MainActivity : AppCompatActivity() { 
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
val intent = Intent("com.example.broadcasttest.MY_BROADCAST")
intent.setPackage(packageName)
sendBroadcast(intent)
}
...
}
...
}

有序广播

发送有序广播只需要改动一行代码,即将sendBroadcast()方法改成sendOrderedBroadcast()方法。

在注册接收器的时候,通过android:priority属性给BroadcastReceiver设置了优先级,优先级高的先接收到广播。调用了abortBroadcast()方法,就表示将这条广播截断

应用

强制下线功能。

强制下线功能需要先关闭所有的Activity,然后回到登录界面。

实际操作不赘述。


Kotlin课堂

高阶函数

如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么该函数就称为高阶函数。

函数类型:

1
(String, Int) -> Unit 

->左边的部分就是用来声明该函数接收什么参数的,多个参数之间使用逗号隔开,如果不接收任何参数,写一对空括号就可以了。而->右边的部分用于声明该函数的返回值是什么类型,如果没有返回值就使用Unit。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun plus(num1: Int, num2: Int): Int { 
return num1 + num2
}
fun minus(num1: Int, num2: Int): Int {
return num1 - num2
}
fun main() {
val num1 = 100
val num2 = 80
val result1 = num1AndNum2(num1, num2, ::plus)
val result2 = num1AndNum2(num1, num2, ::minus)
println("result1 is $result1")
println("result2 is $result2")
}

::plus和::minus这种写法,这是一种函数引用方式的写法,表示将plus()和minus()函数作为参数传递给num1AndNum2()函数。

内联函数

内联函数的用法非常简单,只需要在定义高阶函数时加上inline关键字的声明即可。

内联的函数类型参数在编译的时候会被进行代码替换,因此它没有真正的参数属性。非内联的函数类型参数可以自由地传递给其他任何函数,因为它就是一个真实的参数。

内联函数所引用的Lambda表达式中是可以使用return关键字来进行函数返回的,而非内联函数只能进行局部返回。非内联函数Lambda表达式中是不允许直接使用return关键字的,这里使用了return@printString的写法,表示进行局部返回。

声明了crossinline之后,我们就无法在调用runRunnable函数时的Lambda表达式中使用return关键字进行函数返回了,但是仍然可以使用return@runRunnable的写法进行局部返回。将高阶函数声明成内联函数是一种良好的编程习惯。


Git

常用命令如下:

1
2
3
4
git init # 创建代码仓库
git add [文件路径] # 添加代码
git add . # 添加所有改动
git commit -m "备注" # 提交代码

探究Fragment

Fragment

注意使用AndroidX库中的Fragment,系统内置的已经废除。

补充:layout_gravity和gravity的区别:gravity作用内容为组件内部的,layout_gravity作用于自身相对于父容器的位置。

使用

简单使用

fragment标签中需要使用tools:layout来设置属性预览,否则无法出现预览画面。

动态添加

主要分为五步:

  1. 创建待添加Fragment的实例;
  2. 取FragmentManager,在Activity中可以直接调用getSupportFragmentManager()方法获取;
  3. 开启一个事务,通过调用beginTransaction()方法开启;
  4. 向容器内添加或替换Fragment,一般使用replace()方法实现,需要传入容器的id和待添加的Fragment实例;
  5. 提交事务,调用commit()方法来完成。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MainActivity : AppCompatActivity() { 

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
replaceFragment(AnotherRightFragment())
}
replaceFragment(RightFragment())
}

private fun replaceFragment(fragment: Fragment) {
val fragmentManager = supportFragmentManager
val transaction = fragmentManager.beginTransaction()
transaction.replace(R.id.rightLayout, fragment)
transaction.commit()
}

}

上述代码直接获取fragmentManager方式已经废弃,问题转变为在activity中获取fragment的实例。

返回栈

解决按下Back键程序就会直接退出的问题,可以使用addToBackStack()方法。

和Activity的交互

Fragment和Activity是各自存在于一个独立的类当中的,它们之间并没有那么明显的方式来直接进行交互。

如果想要在Activity中调用Fragment里的方法,FragmentManager提供了一个类似于 findViewById()的方法,专门用于从布局文件中获取Fragment的实例。

1
val fragment = supportFragmentManager.findFragmentById(R.id.leftFrag) as LeftFragment 

在Fragment中调用 Activity,在每个Fragment中都可以通过调用getActivity()方法来得到 和当前Fragment相关联的Activity实例。

1
2
3
if (activity != null) { 
val mainActivity = activity as MainActivity
}

不同的Fragment之间进行通信,首先在一个Fragment中 可以得到与它相关联的Activity,然后再通过这个Activity去获取另外一个Fragment的实例, 这样就实现了不同Fragment之间的通信功能

生命周期

QQ_1737118026269

看懂图熟悉即可。

在Fragment中你也可以通过onSaveInstanceState()方法来保存数据,因为进入停止状态的Fragment有可能在系统内存不足的时候被回收。保存下来的数据在onCreate()、onCreateView()和onActivityCreated()这3个方法中你都可以重新得到,它们都含有一个Bundle类型的savedInstanceState参数。

动态加载布局

限定符

限定符是根据文件夹的名字,根据设备情况,来选择读取文件夹的一种方式。

QQ_1737118464517

最小宽度限定符

最小宽度限定符允许我们对屏幕的宽度指定一个最小值(以dp为单位),然后以这个最小值为临界点,屏幕宽度大于这个值的设备就加载一个布局,屏幕宽度小于这个值的设备就加载另一个布局。如layout-sw600dp这就意味着,当程序运行在屏幕宽度大于等于600 dp的设备上时,会加载layout-sw600dp中的布局。


Kotlin课堂

拓展函数

扩展函数表示即使在不修改某个类的源码的情况下,仍然可以打开这个类,向该类添加新的函数。

1
2
3
fun ClassName.methodName(param1: Int, param2: Int): Int { 
return 0
}

定义扩展函数只需要在函数名的前面加上一个ClassName.的语法结构,就表示将该函数添加到指定类当中了。

QQ20250112-220451

向哪个类中添加扩展函数,就定义一个同名的Kotlin文件,这样便于你以后查找。最好将它定义成顶层方法,这样可以让扩展函数拥有全局的访问域。

运算重载符

运算符重载使用的是operator关键字,只要在指定函数的前面加上operator关键字,就可以实现运算符重载的功能了。

指定函数常见如下:

QQ_1737118967572

Kotlin允许我们对同一个运算符进行多重重载。