九、Service
Service 是 Android 中实现程序后台运行的解决方案,它非常适合执行那些不需要和用户交互而且还要求长期运行的任务。Service 的运行不依赖于任何用户界面,即使程序被切换到后台,或者用户打开了另外一个应用程序,Service 仍然能够保持正常运行。不过需要注意的是,Service 并不是运行在一个独立的进程当中,而是依赖于创建 Service 时所在的应用程序进程。当某个应用程序进程被杀掉时,所有依赖于该进程的 Service 也会停止运行。另外,也不要被 Service 的后台概念所迷惑,实际上 Service 并不会自动开启线程,所有的代码都是默认运行在主线程当中的。也就是说,我们需要在 Service 的内部手动创建子线程,并在这里执行具体的任务,否则就有可能出现主线程被阻塞的情况。
1.Android 多线程编程
线程的基本用法
用法1:
class MyThread : Thread() {
override fun run() {
println("thread: ${Thread.currentThread().name}")
}
}
fun main() {
MyThread().start()
}
日志打印:
thread: Thread-0
用法2:
class MyThread : Runnable {
override fun run() {
println("thread: ${Thread.currentThread().name}")
}
}
fun main() {
val myThread = MyThread()
Thread(myThread).start()
}
日志打印:
thread: Thread-0
用法3:
fun main() {
Thread {
println("thread: ${Thread.currentThread().name}")
}.start()
}
日志打印:
thread: Thread-0
用法4:
fun main() {
thread { //Kotlin 内置的顶层函数
println("thread: ${Thread.currentThread().name}")
}
}
日志打印:
thread: Thread-0
在子线程中更新 UI
使用异步消息处理机制:
//MainActivity.kt
package com.tomorrow.kotlindemo
import android.os.Bundle
import android.os.Handler
import android.os.Message
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
import kotlin.concurrent.thread
class MainActivity : AppCompatActivity() {
companion object {
const val TAG = "MainActivity"
}
val handler = object : Handler() {
override fun handleMessage(msg: Message?) {
when (msg?.what) {
1 -> textView.text = "Nice to meet you"
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
thread {
val msg = Message()
msg.what = 1
handler.sendMessage(msg)
}
}
}
使用 AsyncTask
AsyncTask 背后的实现原理也是基于异步消息处理机制的,只是 Android 帮我们做了很好地封装而已。AsyncTask 是一个抽象类,如果我们想使用它,就必须创建一个子类去继承它。在继承时我们可以为 AsyncTask 类指定 3 个泛型参数:
- Params:在执行 AsyncTask 时需要传入的参数,可用于在后台任务中使用。
- Progress:在后台任务执行时,如果需要在界面上显示当前的进度,则使用这里指定的泛型作为进度单位。
- Result:当任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。
class DownloadTask : AsyncTask<Unit, Int, Boolean>() {
override fun onPreExecute() {
Log.d(TAG, "zwm, onPreExecute ${Thread.currentThread().name}")
}
override fun doInBackground(vararg params: Unit?): Boolean {
Log.d(TAG, "zwm, doInBackground ${Thread.currentThread().name}")
publishProgress(100)
return true
}
override fun onProgressUpdate(vararg values: Int?) {
Log.d(TAG, "zwm, onProgressUpdate ${Thread.currentThread().name}")
}
override fun onPostExecute(result: Boolean?) {
Log.d(TAG, "zwm, onPostExecute $result ${Thread.currentThread().name}")
}
}
DownloadTask().execute()
日志打印:
2020-10-22 16:38:15.395 6680-6680/com.tomorrow.kotlindemo D/MainActivity: zwm, onPreExecute main
2020-10-22 16:38:15.403 6680-6738/com.tomorrow.kotlindemo D/MainActivity: zwm, doInBackground AsyncTask #1
2020-10-22 16:38:16.398 6680-6680/com.tomorrow.kotlindemo D/MainActivity: zwm, onProgressUpdate main
2020-10-22 16:38:16.399 6680-6680/com.tomorrow.kotlindemo D/MainActivity: zwm, onPostExecute true main
2.Service 的基本用法
启动和停止 Service
//AndroidManifest.xml
<service
android:name=".MyService"
android:enabled="true"
android:exported="true">
</service>
//MyService.kt
package com.tomorrow.kotlindemo
import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.util.Log
class MyService : Service() {
companion object {
const val TAG = "MyService"
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onCreate() {
super.onCreate()
Log.d(TAG, "zwm, onCreate")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(TAG, "zwm, onStartCommand")
return super.onStartCommand(intent, flags, startId)
}
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "zwm, onDestroy")
}
}
//MainActivity.kt
package com.tomorrow.kotlindemo
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
companion object {
const val TAG = "MainActivity"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val intent = Intent(this, MyService::class.java)
startService(intent)
}
override fun onDestroy() {
super.onDestroy()
val intent = Intent(this, MyService::class.java)
stopService(intent)
}
}
日志打印:
2020-10-22 16:46:55.384 6970-6970/com.tomorrow.kotlindemo D/MyService: zwm, onCreate
2020-10-22 16:46:55.387 6970-6970/com.tomorrow.kotlindemo D/MyService: zwm, onStartCommand
2020-10-22 16:46:59.391 6970-6970/com.tomorrow.kotlindemo D/MyService: zwm, onDestroy
onCreate() 方法会在 Service 创建的时候调用,onStartCommand() 方法会在每次 Service 启动的时候调用,onDestroy() 方法会在 Service 销毁的时候调用。
Activity 和 Service 进行通信
//MyService.kt
package com.tomorrow.kotlindemo
import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.IBinder
import android.util.Log
class MyService : Service() {
companion object {
const val TAG = "MyService"
}
private val mBinder = DownloadBinder()
class DownloadBinder : Binder() {
fun startDownload() {
Log.d(TAG, "zwm, startDownload")
}
fun getProgress(): Int {
Log.d(TAG, "zwm, getProgress")
return 100
}
}
override fun onBind(intent: Intent?): IBinder {
Log.d(TAG, "zwm, onBind")
return mBinder
}
override fun onCreate() {
super.onCreate()
Log.d(TAG, "zwm, onCreate")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(TAG, "zwm, onStartCommand")
return super.onStartCommand(intent, flags, startId)
}
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "zwm, onDestroy")
}
}
//MainActivity.kt
package com.tomorrow.kotlindemo
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
companion object {
const val TAG = "MainActivity"
}
lateinit var downloadBinder: MyService.DownloadBinder
private val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
Log.d(TAG, "zwm, onServiceConnected $name")
downloadBinder = service as MyService.DownloadBinder
downloadBinder.startDownload()
downloadBinder.getProgress()
}
override fun onServiceDisconnected(name: ComponentName?) {
Log.d(TAG, "zwm, onServiceDisconnected: $name")
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val intent = Intent(this, MyService::class.java)
bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
override fun onDestroy() {
super.onDestroy()
unbindService(connection)
}
}
日志打印:
2020-10-22 17:01:31.305 7436-7436/com.tomorrow.kotlindemo D/MyService: zwm, onCreate
2020-10-22 17:01:31.305 7436-7436/com.tomorrow.kotlindemo D/MyService: zwm, onBind
2020-10-22 17:01:31.431 7436-7436/com.tomorrow.kotlindemo D/MainActivity: zwm, onServiceConnected ComponentInfo{com.tomorrow.kotlindemo/com.tomorrow.kotlindemo.MyService}
2020-10-22 17:01:31.432 7436-7436/com.tomorrow.kotlindemo D/MyService: zwm, startDownload
2020-10-22 17:01:31.432 7436-7436/com.tomorrow.kotlindemo D/MyService: zwm, getProgress
2020-10-22 17:01:35.441 7436-7436/com.tomorrow.kotlindemo D/MyService: zwm, onDestroy
onServiceConnected() 方法会在 Activity 与 Service 成功绑定的时候调用,而 onServiceDisconnected() 方法只有在 Service 的创建进程崩溃或者被杀掉的时候才会调用,这个方法不太常用。
另外需要注意,任何一个 Service 在整个应用程序范围内都是通用的,即 MyService 不仅可以和 MainActivity 绑定,还可以和任何一个其他的 Activity 进行绑定,而且在绑定完成后,它们都可以获取相同的 DownloadBinder 实例。
3.Service 的生命周期
当调用了 startService() 方法后,再去调用 stopService() 方法,这时 Service 中的 onDestroy() 方法就会执行,表示 Service 已经销毁了。类似地,当调用了 bindService() 方法后,再去调用 unbindService() 方法,onDestroy() 方法也会执行。但是需要注意,我们是完全有可能对一个 Service 既调用了 startService() 方法,又调用了 bindService() 方法,根据 Android 系统的机制,一个 Service 只要被启动或者被绑定了之后,就会处于运行状态,必须要让以上两种条件同时不满足,Service 才能被销毁。所以,这种情况下要同时调用 stopService() 和 unBindService() 方法,onDestroy() 方法才会执行。
4.Service 的更多技巧
使用前台 Service
从 Android 8.0 系统开始,只有当应用保持在前台可见状态的情况下,Service 才能保证稳定运行,一旦应用进入后台之后,Service 随时都有可能被系统回收。而如果你希望 Service 能够一直保持运行状态,就可以考虑使用前台 Service。前台 Service 和普通 Service 最大的区别就在于,它一直会有一个正在运行的图标在系统的状态栏显示,下拉状态栏后可以看到更加详细的信息,非常类似于通知的效果。另外,从 Android 9.0 开始,使用前台 Service 必须在 AndroidManifest.xml 文件中进行权限声明才行。
//AndroidManifest.xml
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
//MyService.kt
package com.tomorrow.kotlindemo
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
class MyService : Service() {
companion object {
const val TAG = "MyService"
}
override fun onBind(intent: Intent?): IBinder? {
Log.d(TAG, "zwm, onBind")
return null
}
override fun onCreate() {
super.onCreate()
Log.d(TAG, "zwm, onCreate")
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel("my_service", "前台 Service 通知", NotificationManager.IMPORTANCE_DEFAULT)
manager.createNotificationChannel(channel)
}
val intent = Intent(this, SecondActivity::class.java)
val pi = PendingIntent.getActivity(this, 0, intent, 0)
val notification = NotificationCompat.Builder(this, "my_service")
.setContentTitle("This is content title")
.setContentText("This is content text")
.setSmallIcon(R.drawable.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.ic_launcher_round))
.setContentIntent(pi)
.build()
startForeground(1, notification)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(TAG, "zwm, onStartCommand")
return super.onStartCommand(intent, flags, startId)
}
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "zwm, onDestroy")
}
}
//MainActivity.kt
package com.tomorrow.kotlindemo
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
companion object {
const val TAG = "MainActivity"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val intent = Intent(this, MyService::class.java)
startService(intent)
}
override fun onDestroy() {
super.onDestroy()
val intent = Intent(this, MyService::class.java)
stopService(intent)
}
}
使用 IntentService
//MyIntentService.kt
package com.tomorrow.kotlindemo
import android.app.IntentService
import android.content.Intent
import android.util.Log
class MyIntentService : IntentService("MyIntentService") {
override fun onHandleIntent(intent: Intent?) {
Log.d("TAG", "zwm, onHandleIntent thread: ${Thread.currentThread().name}")
}
override fun onDestroy() {
super.onDestroy()
Log.d("TAG", "zwm, onDestroy")
}
}
5.Kotlin:泛型的高级特性
对泛型进行实化
所有基于 JVM 的语言,它们的泛型功能都是通过类型擦除机制来实现的,其中当然也包括了 Kotlin。这种机制使得我们不可能使用 a is T 或者 T::class.java 这样的语法,因为 T 的实际类型在运行的时候已经被擦除了。然而不同的是,Kotlin 提供了一个内联函数的概念,内联函数中的代码会在编译的时候自动被替换到调用它的地方,这样的话也就不存在什么泛型擦除的问题了,因为代码在编译之后会直接使用实际的类型来替代内联函数中的泛型声明。
Kotlin 中是可以将内联函数中的泛型进行实化的。首先,该函数必须是内联函数才行,也就是要用 inline 关键字来修饰该函数。其次,在声明泛型的地方必须加上 reified 关键字来表示该泛型要进行实化:
inline fun <reified T> getGenericType() = T::class.java
fun main() {
val result1 = getGenericType<Int>()
val result2 = getGenericType<String>()
println("result1 is $result1")
println("result2 is $result2")
}
日志打印:
result1 is class java.lang.Integer
result2 is class java.lang.String
T.class 这样的语法在 Java 中是不合法的,而在 Kotlin 中,借助泛型实化功能就可以使用 T::class.java 这样的语法了。
泛型实化的应用:
//MainActivity.kt
package com.tomorrow.kotlindemo
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
companion object {
const val TAG = "MainActivity"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
startActivity<SecondActivity>(this) {
putExtra("param1", "data")
putExtra("param2", 123)
}
}
}
inline fun <reified T> startActivity(context: Context, block: Intent.() -> Unit) {
val intent = Intent(context, T::class.java)
intent.block()
context.startActivity(intent)
}
泛型的协变
一个泛型类或者泛型接口中的方法,它的参数列表是接收数据的地方,因此可以称它为 in 位置,而它的返回值是输出数据的地方,因此可以称它为 out 位置。
泛型协变(A 是 B 的子类,MyClass<A> 也是 MyClass<B> 的子类),out 关键字,out 位置:
open class Person(val name: String, val age: Int)
class Student(name: String, age: Int) : Person(name, age)
class Teacher(name: String, age: Int) : Person(name, age)
class SimpleData<out T>(val data: T?) {
fun get(): T? {
return data
}
}
fun handleMyData(data: SimpleData<Person>) {
val personData = data.get()
}
fun main() {
val student = Student("Tomy", 19)
val data = SimpleData<Student>(student) //SimpleData<Student>
handleMyData(data) //SimpleData<Person>
val studentData = data.get()
}
如果某个方法接收一个 List<Person> 类型的参数,而传入的却是一个 List<Student> 的实例,在 Java 中是不允许这么做的,而在 Kotlin 中是合法的,因为 Kotlin 已经默认给许多内置的 API 加上了协变声明,其中就包括了各种集合的类与接口。
List 简化版的源码:
public interface List<out E> : Collection<E> {
override val size: Int
override fun isEmpty(): Boolean
override fun contains(element: @UnsafeVariance E): Boolean
override fun iterator(): Iterator<E>
public operator fun get(index: Int): E
}
List 在泛型 E 的前面加上了 out 关键字,说明 List 在泛型 E 上是协变的。原则上在声明了协变之后,泛型 E 就只能出现在 out 位置上,但是在 contains() 方法中,泛型 E 仍然出现在了 in 位置。这么写本身是不合法的,因为在 in 位置上出现了泛型 E 就意味着会有类型转换的安全隐患,但是 contains() 方法的目的非常明确,它只是为了判断当前集合中是否包含参数中传入的这个元素,而并不会修改当前集合中的内容,因此这种操作实质上又是安全的。那么为了让编译器能够理解我们的这种操作是安全的,这里在泛型 E 的前面又加上了一个 @UnsafeVariance 注解,这样编译器就会允许泛型 E 出现在 in 位置上了。但是如果你滥用这个功能,导致运行时出现了类型转换异常,Kotlin 对此是不负责的。
泛型的逆变
泛型逆变(A 是 B 的子类,MyClass<B> 是 MyClass<A> 的子类),in 关键字,in 位置:
open class Person(val name: String, val age: Int)
class Student(name: String, age: Int) : Person(name, age)
class Teacher(name: String, age: Int) : Person(name, age)
class SimpleData<in T> {
fun print(param: T) {
println("zwm, param: $param")
}
}
fun handleMyData(data: SimpleData<Student>) {
val student = Student("Tomy", 30)
data.print(student)
}
fun main() {
val person = SimpleData<Person>() //SimpleData<Person>
handleMyData(person) //SimpleData<Student>
}
Kotlin 在提供协变和逆变功能时,就已经把各种潜在的类型转换安全隐患全部考虑进去了。只要我们严格按照其语法规则,让泛型在协变时只出现在 out 位置上,逆变时只出现在 in 位置上,就不会存在类型转换异常的情况。虽然 @UnsafeVariance 注解可以打破这一语法规则,但同时也会带来额外的风险,所以你在使用 @UnsafeVariance 注解时,必须很清楚自己在干什么才行。
逆变功能在 Kotlin 内置 API 中的应用,比较典型的例子就是 Comparable 的使用,源码定义如下:
public interface Comparable<in T> {
public operator fun compareTo(other: T): Int
}
Comparable 在 T 这个泛型上就是逆变的,如果我们使用 Comparable<Person> 实现了让两个 Person 对象比较大小的逻辑,那么用这段逻辑去比较两个 Student 对象的大小也一定是成立的。因此让 Comparable<Person> 成为 Comparable<Student> 的子类合情合理,这也是逆变非常典型的应用:
open class Person(val name: String, val age: Int) : Comparable<Person> {
override fun compareTo(other: Person) = if(age > other.age) 1 else if(age == other.age) 0 else -1
}
class Student(name: String, age: Int) : Person(name, age)
class Teacher(name: String, age: Int) : Person(name, age)
fun main() {
val student1 = Student("S1", 30)
val student2 = Student("S2", 20)
println("zwm, student1 vs student2: ${student1.compareTo(student2)}")
}
十、网络技术
1.WebView 的用法
//AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
android:usesCleartextTraffic="true"
//MainActivity.kt
package com.tomorrow.kotlindemo
import android.os.Bundle
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
webView.settings.javaScriptEnabled = true
webView.webViewClient = WebViewClient()
webView.loadUrl("https://www.baidu.com")
}
}
2.使用 HttpURLConnection
package com.tomorrow.kotlindemo
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
import kotlin.concurrent.thread
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
sendRequestWithHttpURLConnection()
}
private fun sendRequestWithHttpURLConnection() {
thread {
var connection: HttpURLConnection? = null
try {
val response = StringBuilder()
val url = URL("https://www.baidu.com")
connection = url.openConnection() as HttpURLConnection
connection.connectTimeout = 8000
connection.readTimeout = 8000
val input = connection.inputStream
val reader = BufferedReader(InputStreamReader(input))
reader.use {
reader.forEachLine {
response.append(it)
}
}
showResponse(response.toString())
} catch (e: Exception) {
e.printStackTrace()
} finally {
connection?.disconnect()
}
}
}
private fun showResponse(response: String) {
runOnUiThread {
textView.text = response
}
}
}
发起一条 POST 请求:
connection.requestMethod = "POST"
val output = DataOutputStream(connection.outputStream)
output.writeBytes("username=admin&password=12345")
3.使用 OkHttp
//build.gradle
implementation 'com.squareup.okhttp3:okhttp:4.1.0'
//MainActivity.kt
package com.tomorrow.kotlindemo
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
import okhttp3.OkHttpClient
import okhttp3.Request
import kotlin.concurrent.thread
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
sendRequestWithOkHttp()
}
private fun sendRequestWithOkHttp() {
thread {
try {
val client = OkHttpClient()
val request = Request.Builder()
.url("https://www.baidu.com")
.build()
val response = client.newCall(request).execute()
val responseData = response.body?.string()
if(responseData != null) {
showResponse(response.toString())
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private fun showResponse(response: String) {
runOnUiThread {
textView.text = response
}
}
}
发起一条 POST 请求:
val requestBody = FormBody.Builder()
.add("username", "admin")
.add("password", "12345")
.build()
val request = Request.Builder()
.url("https://www.baidu.com")
.post(requestBody)
.build()
4.解析 XML 格式数据
在网络上传输数据时最常用的格式有两种:XML 和 JSON。
Pull 解析方式
//res/raw/test.xml
<?xml version="1.0" encoding="utf-8"?>
<apps>
<app>
<id>1</id>
<name>简书</name>
<version>1.0</version>
</app>
<app>
<id>2</id>
<name>高德地图</name>
<version>2.0</version>
</app>
</apps>
//MainActivity.kt
package com.tomorrow.kotlindemo
import android.net.Uri
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserFactory
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
parseXMLWithPull()
}
private fun parseXMLWithPull() {
try {
val uri = Uri.parse("android.resource://$packageName/${R.raw.test}")
val factory = XmlPullParserFactory.newInstance()
val xmlPullParser = factory.newPullParser()
xmlPullParser.setInput(contentResolver.openInputStream(uri), "utf-8")
var eventType = xmlPullParser.eventType
var id = ""
var name = ""
var version = ""
while (eventType != XmlPullParser.END_DOCUMENT) {
val nodeName = xmlPullParser.name
Log.d("TAG", "zwm, nodeName: $nodeName")
when (eventType) {
XmlPullParser.START_TAG -> {
when (nodeName) {
"id" -> id = xmlPullParser.nextText()
"name" -> name = xmlPullParser.nextText()
"version" -> version = xmlPullParser.nextText()
}
}
XmlPullParser.END_TAG -> {
if("app" == nodeName) {
Log.d("TAG", "zwm, id: $id, name: $name, version: $version")
}
}
}
eventType = xmlPullParser.next()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
SAX 解析方式
//ContentHandler.kt
package com.tomorrow.kotlindemo
import android.util.Log
import org.xml.sax.Attributes
import org.xml.sax.helpers.DefaultHandler
class ContentHandler : DefaultHandler() {
private var nodeName = ""
private lateinit var id: StringBuilder
private lateinit var name: StringBuilder
private lateinit var version: StringBuilder
override fun startDocument() {
super.startDocument()
id = StringBuilder()
name = StringBuilder()
version = StringBuilder()
}
override fun startElement(uri: String?, localName: String?, qName: String?, attributes: Attributes?) {
super.startElement(uri, localName, qName, attributes)
nodeName = localName!!
Log.d("TAG", "zwm, uri: $uri, localName: $localName, qName: $qName, attributes: $attributes")
}
override fun characters(ch: CharArray?, start: Int, length: Int) {
super.characters(ch, start, length)
when (nodeName) {
"id" -> id.append(ch, start, length)
"name" -> name.append(ch, start, length)
"version" -> version.append(ch, start, length)
}
}
override fun endElement(uri: String?, localName: String?, qName: String?) {
super.endElement(uri, localName, qName)
if("app" == localName) {
Log.d("TAG", "zwm, id: $id, name: $name, version: $version")
id.setLength(0)
name.setLength(0)
version.setLength(0)
}
}
override fun endDocument() {
super.endDocument()
}
}
//MainActivity.kt
package com.tomorrow.kotlindemo
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import org.xml.sax.InputSource
import javax.xml.parsers.SAXParserFactory
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
parseXMLWithSAX()
}
private fun parseXMLWithSAX() {
try {
val uri = Uri.parse("android.resource://$packageName/${R.raw.test}")
val factory = SAXParserFactory.newInstance()
val xmlReader = factory.newSAXParser().xmlReader
val handler = ContentHandler()
xmlReader.contentHandler = handler
xmlReader.parse(InputSource(contentResolver.openInputStream(uri)))
} catch (e: Exception) {
e.printStackTrace()
}
}
}
DOM 解析方式
除了 Pull 解析和 SAX 解析之外,还有一种 DOM 解析方式也比较常用。
5.解析 JSON 格式数据
使用 JSONObject
//MainActivity.kt
package com.tomorrow.kotlindemo
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import org.json.JSONArray
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
parseJSONWithJSONObject()
}
private fun parseJSONWithJSONObject() {
try {
val jsonData = "[{\"id\":1,\"name\":\"简书\",\"version\":\"1.0\"},{\"id\":2,\"name\":\"高德地图\",\"version\":\"2.0\"}]"
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("TAG", "zwm, id: $id, name: $name, version: $version")
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
使用 GSON
//build.gradle
implementation 'com.google.code.gson:gson:2.8.5'
//AppInfo.kt
package com.tomorrow.kotlindemo
data class AppInfo(val id: String, val name: String, val version: String)
//MainActivity.kt
package com.tomorrow.kotlindemo
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
parseJSONWithGSON()
}
private fun parseJSONWithGSON() {
try {
val jsonData = "[{\"id\":1,\"name\":\"简书\",\"version\":\"1.0\"},{\"id\":2,\"name\":\"高德地图\",\"version\":\"2.0\"}]"
val gson = Gson()
val typeOf = object : TypeToken<List<AppInfo>>(){}.type
val appList = gson.fromJson<List<AppInfo>>(jsonData, typeOf)
for(app in appList) {
Log.d("TAG", "zwm, id: ${app.id}, name: ${app.name}, version: ${app.version}")
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
6.网络请求回调的实现方式
//HttpCallbackListener.kt
package com.tomorrow.kotlindemo
interface HttpCallbackListener {
fun onFinish(response: String)
fun onError(e: Exception)
}
//HttpUtil.kt
package com.tomorrow.kotlindemo
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
import kotlin.concurrent.thread
object HttpUtil {
fun sendHttpRequest(address: String, listener: HttpCallbackListener) {
thread {
var connection: HttpURLConnection? = null
try {
val response = StringBuilder()
val url = URL(address)
connection = url.openConnection() as HttpURLConnection
connection.connectTimeout = 8000
connection.readTimeout = 8000
val input = connection.inputStream
val reader = BufferedReader(InputStreamReader(input))
reader.use {
reader.forEachLine {
response.append(it)
}
}
listener.onFinish(response.toString())
} catch (e: Exception) {
e.printStackTrace()
listener.onError(e)
} finally {
connection?.disconnect()
}
}
}
fun sendOkHttpRequest(address: String, callback: okhttp3.Callback) {
val client = OkHttpClient()
val request = Request.Builder()
.url(address)
.build()
client.newCall(request).enqueue(callback)
}
}
//MainActivity.kt
package com.tomorrow.kotlindemo
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Response
import java.io.IOException
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
HttpUtil.sendHttpRequest("https://www.baidu.com", object : HttpCallbackListener{
override fun onFinish(response: String) {
Log.d("TAG", "zwm, onFinish: $response")
}
override fun onError(e: Exception) {
}
})
HttpUtil.sendOkHttpRequest("https://www.baidu.com", object : Callback {
override fun onResponse(call: Call, response: Response) {
Log.d("TAG", "zwm, onResponse: ${response.body?.string()}")
}
override fun onFailure(call: Call, e: IOException) {
}
})
}
}
需要注意的是,不管是使用 HttpURLConnection 还是 OkHttp,最终的回调接口都还是在子线程中运行的,因此我们不可以在这里执行任何的 UI 操作,除非借助 runOnUiThread() 方法来进行线程转换。
7.使用 Retrofit
OkHttp 侧重的是底层通信的实现,而 Retrofit 侧重的是上层接口的封装。事实上,Retrofit 就是 Square 公司在 OkHttp 的基础上进一步开发出来的应用层网络通信库,使得我们可以用更加面向对象的思维进行网络操作。
Retrofit 的基本用法
//build.gradle
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
//Gist.kt
package com.tomorrow.kotlindemo
data class Gist(val files: Map<String, GistFile>)
data class GistFile(val content: String)
//GistService.kt
package com.tomorrow.kotlindemo
import retrofit2.Call
import retrofit2.http.GET
interface GistService {
@GET("gists/c2a7c39532239ff261be")
fun getInfo(): Call<Gist>
}
//MainActivity.kt
package com.tomorrow.kotlindemo
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
sendHttpRequest()
}
fun sendHttpRequest() {
val retrofit = Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val gistService = retrofit.create(GistService::class.java)
gistService.getInfo().enqueue(object : retrofit2.Callback<Gist> {
override fun onResponse(call: retrofit2.Call<Gist>, response: retrofit2.Response<Gist>) {
Log.d("TAG", "zwm, onResponse: ${response.body()}")
}
override fun onFailure(call: retrofit2.Call<Gist>, t: Throwable) {
}
})
}
}
由于 Retrofit 是基于 OkHttp 开发的,因此添加上述第一条依赖会自动将 Retrofit、OkHttp 和 Okio 这几个库一起下载,我们无须再手动引入 OkHttp 库。另外,Retrofit 还会将服务器返回的 JSON 数据自动解析成对象,因此上述第二条依赖就是一个 Retrofit 的转换库,它是借助 GSON 来解析 JSON 数据的,所以会自动将 GSON 库一起下载下来,这样我们也不用手动引入 GSON 库了。处理 GSON 之外,Retrofit 还支持各种其他主流的 JSON 解析库,包括 Jackson、Moshi 等,不过毫无疑问 GSON 是最常用的。
网络请求方法的返回值必须声明成 Retrofit 中内置的 Call 类型,并通过泛型来指定服务器响应的数据应该转换成什么对象。另外,Retrofit 还提供了强大的 Call Adapters 功能来允许我们自定义方法返回值的类型,比如 Retrofit 结合 RxJava 使用就可以将返回值声明成 Observable、Flowable 等类型。
需要注意的是,当发起请求的时候,Retrofit 会自动在内部开启子线程,当数据回调到 Callback 中之后,Retrofit 又会自动切换回主线程,整个操作过程中我们都不用考虑线程切换问题。
处理复杂的接口地址类型
- 动态配置请求路径
@GET("gists/{page}/c2a7c39532239ff261be")
fun getData(@Path("page") page: Int): Call<Gist>
- 动态配置请求参数
@GET("gists/c2a7c39532239ff261be")
fun getData(@Query("u") user: String): Call<Gist>
- 接收任意类型的响应数据
@POST("gists/c2a7c39532239ff261be")
fun createData(@Body data: Gist): Call<ResponseBody>
由于 POST、PUT、PATCH、DELETE 这几种请求类型与 GET 请求不同,它们更多是用于操作服务器上的数据,而不是获取服务器上的数据,所以通常它们对于服务器响应的数据并不关心,这个时候就可以使用 ResponseBody,表示 Retrofit 能够接收任意类型的响应数据,并且不会对响应数据进行解析。
- 指定请求头
@Headers("User-Agent: okhttp", "Cache-Control: max-age=0")
@GET("gists/c2a7c39532239ff261be")
fun getInfo(): Call<Gist>
- 动态配置请求头
@GET("gists/c2a7c39532239ff261be")
fun getInfo(@Header("Cache-Control") cacheControl: String): Call<Gist>
Retrofit 构建器的最佳写法
//ServiceCreator.kt
package com.tomorrow.kotlindemo
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object ServiceCreator {
private const val BASE_URL = "https://api.github.com/"
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
fun <T> create(serviceClass: Class<T>): T = retrofit.create(serviceClass)
inline fun <reified T> create(): T = create(T::class.java) //使用泛型实化功能进一步优化
}
//MainActivity.kt
package com.tomorrow.kotlindemo
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
sendHttpRequest()
}
fun sendHttpRequest() {
val gistService = ServiceCreator.create(GistService::class.java)
gistService.getInfo().enqueue(object : retrofit2.Callback<Gist> {
override fun onResponse(call: retrofit2.Call<Gist>, response: retrofit2.Response<Gist>) {
Log.d("TAG", "zwm, onResponse: ${response.body()}")
}
override fun onFailure(call: retrofit2.Call<Gist>, t: Throwable) {
}
})
val gistService2 = ServiceCreator.create<GistService>() //使用泛型实化功能进一步优化
gistService2.getInfo().enqueue(object : retrofit2.Callback<Gist> {
override fun onResponse(call: retrofit2.Call<Gist>, response: retrofit2.Response<Gist>) {
Log.d("TAG", "zwm, onResponse: ${response.body()}")
}
override fun onFailure(call: retrofit2.Call<Gist>, t: Throwable) {
}
})
}
}
8.Kotlin:协程
协程属于 Kotlin 中非常有特色的一项技术,因为大部分编程语言中是没有协程这个概念的。协程其实和线程是有点类似的,可以简单地理解成一种轻量级的线程。线程是非常重量级的,它需要依赖操作系统的调度才能实现不同线程之间的切换。而使用协程却可以仅在编程语言的层面就能实现不同协程之间的切换,从而大大提升了开发编程的运行效率。协程允许我们在单线程模式下模拟多线程编程的效果,代码执行时的挂起与恢复完全是由编程语言来控制的,和操作系统无关。这种特性使得高并发程序的运行效率得到了极大的提升。
协程的基本用法
添加依赖:
//build.gradle
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3"
第二个依赖库是在 Android 项目中才会用到的。
例1:
package com.tomorrow.kotlindemo
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
fun main() {
GlobalScope.launch {
println("zwm, GlobalScope launch")
}
}
GlobalScope.launch 函数每次创建的都是一个顶层协程,这种协程当应用程序运行结束时也会跟着一起结束。上述例子中的日志是无法打印出来的,这是因为代码块中的代码还没来得及运行,应用程序就结束了。
例2:
fun main() {
GlobalScope.launch {
println("zwm, GlobalScope launch")
}
Thread.sleep(1000)
}
日志打印:
zwm, GlobalScope launch
使用 Thread.sleep() 方法让主线程阻塞 1 秒钟,日志能够打印出来了。
例3:
fun main() {
GlobalScope.launch {
println("zwm, GlobalScope launch")
delay(1500)
println("zwm, GlobalScope launch end")
}
Thread.sleep(1000)
}
日志打印:
zwm, GlobalScope launch
新增的一条日志没有打印出来。delay() 函数是一个非阻塞式的挂起函数,它只会挂起当前协程,并不会影响其它协程的运行。注意,delay() 函数只能在协程的作用域或其他挂起函数中调用。
例4:
fun main() {
runBlocking {
println("zwm, runBlocking")
delay(1500)
println("zwm, runBlocking end")
}
}
日志打印:
zwm, runBlocking
zwm, runBlocking end
runBlocking() 函数同样会创建一个协程的作用域,但是它可以保证在协程作用域内的所有代码和子协程没有全部执行完之前一直阻塞当前线程。需要注意的是,runBlocking() 函数通常只应该在测试环境下使用,在正式环境中使用容易产生一些性能上的问题。
例5:
fun main() {
runBlocking {
println("zwm, runBlocking")
launch {
println("zwm, launch1")
delay(1000)
println("zwm, launch1 end")
}
launch {
println("zwm, launch2")
delay(1000)
println("zwm, launch2 end")
}
println("zwm, runBlocking end")
}
}
日志打印:
zwm, runBlocking
zwm, runBlocking end
zwm, launch1
zwm, launch2
zwm, launch1 end
zwm, launch2 end
launch() 函数和 GlobalScope.launch() 函数不同。首先它必须在协程的作用域中才能调用,其次它会在当前协程的作用域下创建子协程。子协程的特点是如果外层作用域的协程结束了,该作用域下的所有子协程也会一同结束。
例6:
fun main() {
runBlocking {
println("zwm, runBlocking")
printDot()
println("zwm, runBlocking end")
}
}
suspend fun printDot() {
println(".")
delay(1000)
}
日志打印:
zwm, runBlocking
.
zwm, runBlocking end
Kotlin 提供了一个 suspend 关键字,使用它可以将任意函数声明成挂起函数,而挂起函数之间都是可以互相调用的。但是,suspend 关键字只能将一个函数声明成挂起函数,是无法给它提供协程作用域的。比如我们无法在 printDot() 函数中调用 launch() 函数,因为 launch() 函数要求必须在协程作用域当中才能调用。
例7:
fun main() {
runBlocking {
println("zwm, runBlocking")
printDot()
println("zwm, runBlocking end")
}
}
suspend fun printDot() = coroutineScope {
println("zwm, coroutineScope")
launch {
println("zwm, launch")
println(".")
delay(1000)
println("zwm, launch end")
}
println("zwm, coroutineScope end")
}
日志打印:
zwm, runBlocking
zwm, coroutineScope
zwm, coroutineScope end
zwm, launch
.
zwm, launch end
zwm, runBlocking end
coroutineScope() 函数也是一个挂起函数,因此可以在任何其它挂起函数中调用,它的特点是会继承外部的协程作用域并创建一个子作用域,借助这个特性,我们就可以给任意挂起函数提供协程作用域了。另外,coroutineScope() 函数和 runBlocking() 函数还有点类似,它可以保证其作用域内的所有代码和子协程在全部执行完之前,会一直阻塞当前协程。虽然看上去 coroutineScope() 函数和 runBlocking() 函数的作用是非常类似的,但是 coroutineScope() 函数只会阻塞当前协程,既不影响其他协程,也不影响任何线程,因此是不会造成任何性能上的问题的。而 runBlocking() 函数由于会阻塞当前线程,如果你恰好又在主线程当中调用它的话,那么就有可能会导致界面卡死的情况,所以不太推荐在实际项目中使用。
更多的作用域构建器
取消协程方法:
val job = GlobalScope.launch {
//处理具体的逻辑
}
job.cancel()
实际项目中比较常用的写法:
val job = Job()
val scope = CoroutineScope(job)
scope.launch {
//处理具体的逻辑
}
job.cancel()
现在所有调用 CoroutineScope 的 launch 函数所创建的协程,都会被关联在 Job 对象的作用域下面。这样只需要调用一次 cancel() 方法,就可以将同一作用域内的所有协程全部取消,从而大大降低了协程的管理成本。不过相比之下,CoroutineScope() 函数更适合用于实际项目当中,如果只是在 main() 函数中编写一些学习测试用的代码,还是使用 runBlocking() 函数最为方便。
获取结果:
fun main() {
runBlocking {
val start = System.currentTimeMillis()
val result1 = async {
delay(1000)
5 + 5
}.await()
val result2 = async {
delay(1000)
4 + 6
}.await()
println("result is ${result1 + result2}")
val end = System.currentTimeMillis()
println("cost ${end - start} ms")
}
}
日志打印:
result is 20
cost 2049 ms
fun main() {
runBlocking {
val start = System.currentTimeMillis()
val deferred1 = async {
delay(1000)
5 + 5
}
val deferred2 = async {
delay(1000)
4 + 6
}
println("result is ${deferred1.await() + deferred2.await()}")
val end = System.currentTimeMillis()
println("cost ${end - start} ms")
}
}
日志打印:
result is 20
cost 1036 ms
async() 函数必须在协程作用域当中才能调用,它会创建一个新的子协程并返回一个 Deferred 对象,如果我们想要获取 async 函数代码块的执行结果,只需要调用 Deferred 对象的 await() 方法即可。事实上,在调用了 async() 函数之后,代码块中的代码就会立刻开始执行。当调用 await() 方法时,如果代码块中的代码还没执行完,那么 await() 方法会将当前协程阻塞住,直到可以获取 async() 函数的执行结果。
withContext() 函数:
fun main() {
runBlocking {
val result = withContext(Dispatchers.Default) {
5 + 5
}
println(result)
}
}
日志打印:
10
withContext() 函数是一个挂起函数,大体可以将它理解成 async 函数的一种简化版写法。调用 withContext() 函数之后,会立即执行代码块中的代码,同时将当前协程阻塞住。当代码块中的代码全部执行完之后,会将最后一行的执行结果作为 withContext() 函数的返回值返回,因此基本上相当于 val result = async { 5 + 5 }.await() 的写法。唯一不同的是,withContext() 函数强制要求我们指定一个线程参数。
协程是一种轻量级的线程的概念,因此很多传统编程情况下需要开启多线程执行的并发任务,现在只需要在一个线程下开启多个协程来执行就可以了。但是这并不意味着我们就永远不需要开启线程了,比如说 Android 中要求网络请求必须在子线程中进行,即使你开启了协程去执行网络请求,假如它是主线程当中的协程,那么程序仍然会出错。这个时候我们就应该通过线程参数给协程指定一个具体的运行线程。线程参数主要有以下 3 种值可选:
- Dispatchers.Default:表示会使用一种默认低并发的线程策略,当你要执行的代码属于计算密集型任务时,开启过高的并发反而可能会影响任务的运行效率,此时就可以使用 Dispatchers.Default。
- Dispatchers.IO:表示会使用一种较高并发的线程策略,当你要执行的代码大多数时间是在阻塞和等待中,比如说执行网络请求时,为了能够支持更高的并发数量,此时就可以使用 Dispatchers.IO。
- Dispatchers.Main:表示不会开启子线程,而是在 Android 主线程中执行代码,但是这个值只能在 Android 项目中使用。
事实上,在我们之前所学的协程作用域构建器中,除了 coroutineScope() 函数之外,其他所有的函数都是可以指定这样一个线程参数的,只不过 withContext() 函数是强制要求指定的,而其他函数则是可选的。
使用协程简化回调的写法
举个例子:
fun main() {
runBlocking {
val result = withContext(Dispatchers.Default) {
println("withContext")
var output = 88
thread {
println("thread run")
for(i in 0..1000000) {
i * 100
}
println("thread end")
output = 99
}
println("withContext end")
output
}
println(result)
}
}
日志打印:
withContext
withContext end
thread run
88
注意,以上方法不能获取线程执行结果。
借助 suspendCoroutine() 函数我们就能将传统回调机制的写法大幅简化。suspendCoroutine() 函数必须在协程作用域或挂起函数中才能调用,它接收一个 Lambda 表达式参数,主要作用是将当前协程立即挂起,然后在一个普通的线程中执行 Lambda 表达式中的代码。Lambda 表达式的参数列表上会传入一个 Continuation 参数,调用它的 resume() 方法或 resumeWithException() 可以让协程恢复执行。
//HttpUtil.kt
package com.tomorrow.kotlindemo
import android.util.Log
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
object HttpUtil {
suspend fun request(address: String): String {
Log.d("TAG","zwm, request: $address")
return suspendCoroutine { continuation ->
Log.d("TAG","zwm, suspendCoroutine")
sendHttpRequest(address, object : HttpCallbackListener {
override fun onFinish(response: String) {
Log.d("TAG","zwm, request onFinish")
continuation.resume(response)
}
override fun onError(e: Exception) {
Log.d("TAG","zwm, request onError")
continuation.resumeWithException(e)
}
})
Log.d("TAG","zwm, suspendCoroutine end")
}
}
fun sendHttpRequest(address: String, listener: HttpCallbackListener) {
var connection: HttpURLConnection? = null
try {
val response = StringBuilder()
val url = URL(address)
connection = url.openConnection() as HttpURLConnection
connection.connectTimeout = 8000
connection.readTimeout = 8000
val input = connection.inputStream
val reader = BufferedReader(InputStreamReader(input))
reader.use {
reader.forEachLine {
response.append(it)
}
}
listener.onFinish(response.toString())
} catch (e: Exception) {
e.printStackTrace()
listener.onError(e)
} finally {
connection?.disconnect()
}
}
}
//MainActivity.kt
package com.tomorrow.kotlindemo
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
GlobalScope.launch {
Log.d("TAG","zwm, GlobalScope launch")
getBaiduResponse()
Log.d("TAG","zwm, GlobalScope launch end")
}
}
suspend fun getBaiduResponse() {
try {
val response = HttpUtil.request("https://www.baidu.com")
Log.d("TAG","zwm, response: $response")
} catch (e: Exception) {
e.printStackTrace()
}
}
}
日志打印:
2020-10-27 09:02:22.540 24260-24891/? D/TAG: zwm, GlobalScope launch
2020-10-27 09:02:22.541 24260-24891/? D/TAG: zwm, request: https://www.baidu.com
2020-10-27 09:02:22.543 24260-24891/? D/TAG: zwm, suspendCoroutine
2020-10-27 09:02:22.953 24260-24891/com.tomorrow.kotlindemo D/TAG: zwm, request onFinish
2020-10-27 09:02:22.953 24260-24891/com.tomorrow.kotlindemo D/TAG: zwm, suspendCoroutine end
2020-10-27 09:02:22.954 24260-24891/com.tomorrow.kotlindemo D/TAG: zwm, response: <!DOCTYPE html><!--STATUS OK--><html> ...
2020-10-27 09:02:22.954 24260-24891/com.tomorrow.kotlindemo D/TAG: zwm, GlobalScope launch end
事实上,suspendCoroutine() 函数几乎可以用于简化任何回调的写法,比如之前使用 Retrofit 来发起网络请求需要这样写:
//MainActivity.kt
package com.tomorrow.kotlindemo
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
GlobalScope.launch {
Log.d("TAG", "zwm, GlobalScope launch")
try {
val gist = ServiceCreator.create<GistService>().getInfo().await()
Log.d("TAG", "zwm, gist get success")
} catch (e: Exception) {
e.printStackTrace()
}
Log.d("TAG", "zwm, GlobalScope launch end")
}
}
suspend fun <T> Call<T>.await(): T {
Log.d("TAG", "zwm, await")
return suspendCoroutine { continuation ->
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
Log.d("TAG", "zwm, onResponse")
val body = response.body()
if(body != null) continuation.resume(body)
else continuation.resumeWithException(RuntimeException("response body is null"))
}
override fun onFailure(call: Call<T>, t: Throwable) {
Log.d("TAG", "zwm, onFailure")
continuation.resumeWithException(t)
}
})
}
}
}
日志打印:
2020-10-27 09:37:54.278 29823-30340/? D/TAG: zwm, GlobalScope launch
2020-10-27 09:37:54.349 29823-30340/? D/TAG: zwm, await
2020-10-27 09:37:55.609 29823-29823/com.tomorrow.kotlindemo D/TAG: zwm, onResponse
2020-10-27 09:37:55.610 29823-30340/com.tomorrow.kotlindemo D/TAG: zwm, gist get success
2020-10-27 09:37:55.610 29823-30340/com.tomorrow.kotlindemo D/TAG: zwm, GlobalScope launch end
十一、Material Design
1.Toolbar
//res/values/styles.xml
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.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/menu/main.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/add_item"
android:title="Add"/>
<item
android:id="@+id/remove_item"
android:title="Remove"/>
</menu>
//res/layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
</FrameLayout>
//MainActivity.kt
package com.tomorrow.kotlindemo
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when (item?.itemId) {
R.id.add_item -> Toast.makeText(this, "You clicked Add item", Toast.LENGTH_SHORT).show()
R.id.remove_item -> Toast.makeText(this, "You clicked Remove item", Toast.LENGTH_SHORT).show()
}
return true
}
}
2.滑动菜单
DrawerLayout
//res/values/styles.xml
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.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/layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
</FrameLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="#FFF"
android:text="This is menu"
android:textSize="30sp"/>
</androidx.drawerlayout.widget.DrawerLayout>
//MainActivity.kt
package com.tomorrow.kotlindemo
import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GravityCompat
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
supportActionBar?.let {
it.setDisplayHomeAsUpEnabled(true)
it.setHomeAsUpIndicator(R.drawable.ic_launcher)
}
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when (item?.itemId) {
android.R.id.home -> drawerLayout.openDrawer(GravityCompat.START)
}
return true
}
}
NavigationView
//build.gradle
implementation 'com.google.android.material:material:1.0.0'
implementation 'de.hdodenhof:circleimageview:3.0.1'
//res/values/styles.xml
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.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/menu/nav_menu.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single">
<item
android:id="@+id/navCall"
android:icon="@drawable/ic_launcher"
android:title="Call"/>
<item
android:id="@+id/navFriends"
android:icon="@drawable/ic_launcher"
android:title="Friends"/>
<item
android:id="@+id/navLocation"
android:icon="@drawable/ic_launcher"
android:title="Location"/>
</group>
</menu>
//res/layout/nav_header.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="180dp"
android:padding="10dp"
android:background="@color/colorPrimary">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/iconImage"
android:layout_width="70dp"
android:layout_height="70dp"
android:src="@drawable/ic_launcher"
android:layout_centerInParent="true"/>
<TextView
android:id="@+id/mailText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:text="tomorrow@qq.com"
android:textColor="#FFF"
android:textSize="14sp"/>
<TextView
android:id="@+id/userText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/mailText"
android:text="Tomorrow"
android:textColor="#FFF"
android:textSize="14sp"/>
</RelativeLayout>
//res/layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
</FrameLayout>
<com.google.android.material.navigation.NavigationView
android:id="@+id/navView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start"
app:menu="@menu/nav_menu"
app:headerLayout="@layout/nav_header"/>
</androidx.drawerlayout.widget.DrawerLayout>
//MainActivity.kt
package com.tomorrow.kotlindemo
import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GravityCompat
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
supportActionBar?.let {
it.setDisplayHomeAsUpEnabled(true)
it.setHomeAsUpIndicator(R.drawable.ic_launcher)
}
navView.setCheckedItem(R.id.navCall)
navView.setNavigationItemSelectedListener {
drawerLayout.closeDrawers()
true
}
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when (item?.itemId) {
android.R.id.home -> drawerLayout.openDrawer(GravityCompat.START)
}
return true
}
}
3.悬浮按钮和可交互提示
FloatingActionButton
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:src="@drawable/ic_launcher"
app:elevation="8dp"/>
fab.setOnClickListener{
Toast.makeText(this, "FAB clicked", Toast.LENGTH_SHORT).show()
}
Snackbar
fab.setOnClickListener{ view ->
Snackbar.make(view, "Data deleted", Snackbar.LENGTH_SHORT)
.setAction("Undo") {
Toast.makeText(this, "Data restored", Toast.LENGTH_SHORT).show()
}
.show()
}
CoordinatorLayout
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:src="@drawable/ic_launcher"
app:elevation="8dp"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView
android:id="@+id/navView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start"
app:menu="@menu/nav_menu"
app:headerLayout="@layout/nav_header"/>
</androidx.drawerlayout.widget.DrawerLayout>
fab.setOnClickListener{ view ->
Snackbar.make(view, "Data deleted", Snackbar.LENGTH_SHORT)
.setAction("Undo") {
Toast.makeText(this, "Data restored", Toast.LENGTH_SHORT).show()
}
.show()
}
4.卡片式布局
MaterialCardView
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
app:cardCornerRadius="4dp">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/fruitImage"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp"/>
<TextView
android:id="@+id/fruitName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
AppBarLayout
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
当 AppBarLayout 接收到滚动事件的时候,它内部的子控件其实是可以指定如何去影响这些事件的,通过 app:layout_scrollFlags 属性就能实现:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:layout_scrollFlags="scroll|enterAlways|snap"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
app:layout_scrollFlags 属性值的作用:
- scroll:表示当 RecyclerView 向上滚动的时候,Toolbar 会跟着一起向上滚动并实现隐藏。
- enterAlways:表示当 RecyclerView 向下滚动的时候,Toolbar 会跟着一起向下滚动并重新显示。
- snap:表示当 Toolbar 还没有完全隐藏或显示的时候,会根据当前滚动的距离,自动选择是隐藏还是显示。
5.下拉刷新
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:layout_scrollFlags="scroll|enterAlways|snap"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
swipeRefresh.setColorSchemeResources(R.color.colorPrimary)
swipeRefresh.setOnRefreshListener {
refreshFruits(adapter)
}
private fun refreshFruits(adapter: FruitsAdapter) {
thread {
Thread.sleep(2000)
runOnUiThread {
initFruits()
adapter.notifyDataSetChanged()
swipeRefresh.isRefreshing = false
}
}
}
由于 RecyclerView 现在变成了 SwipeRefreshLayout 的子控件,因此之前使用的 app:layout_behavior 声明的布局行为现在也要移到 SwipeRefreshLayout 中才行。
6.可折叠式标题栏
CollapsingToolbarLayout
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBar"
android:layout_width="match_parent"
android:layout_height="250dp">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsingToolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:contentScrim="@color/colorAccent"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/ic_launcher"
app:layout_collapseMode="parallax" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"/>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="@drawable/ic_launcher"
app:layout_anchor="@id/appBar"
app:layout_anchorGravity="bottom|end"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
- app:contentScrim 属性用于指定 CollapsingToolbarLayout 在趋于折叠状态以及折叠之后的背景色。
- app:layout_scrollFlags 属性之前是给 Toolbar 指定的,现在也移到外面来了。其中 scroll 表示 CollapsingToolbarLayout 会随着内容详情的滚动一起滚动,exitUntilCollapsed 表示当 CollapsingToolbarLayout 随着滚动完成折叠之后就保留在界面上,不再移出屏幕。
- app:layout_collapseMode 属性用于指定当前控件在 CollapsingToolbarLayout 折叠过程中的折叠模式,pin 表示在折叠的过程中位置始终保持不变,parallax 表示会在折叠的过程中产生一定的错位偏移。
充分利用系统状态栏空间
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBar"
android:layout_width="match_parent"
android:layout_height="250dp"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsingToolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
android:fitsSystemWindows="true"
app:contentScrim="@color/colorAccent"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/ic_launcher"
android:fitsSystemWindows="true"
app:layout_collapseMode="parallax" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"/>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="@drawable/ic_launcher"
app:layout_anchor="@id/appBar"
app:layout_anchorGravity="bottom|end"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
</resources>
需要将 ImageView 及布局结构中的所有父布局都设置 android:fitsSystemWindows 属性,并且还需要在程序的主题中将状态栏颜色指定成透明色,即设置 android:statusBarColor 属性。
7.Kotlin:编写好用的工具方法
求 N 个数的最大最小值
fun <T : Comparable<T>> max(vararg nums: T): T {
if(nums.isEmpty()) throw RuntimeException("Params can not be empty.")
var maxNum = nums[0]
for(num in nums) {
if(num > maxNum) {
maxNum = num
}
}
return maxNum
}
fun main() {
println("${max(123, 456)} ${max(1.2, 2.3, 3.4)}")
}
日志打印:
456 3.4
简化 Toast 的用法
fun String.showToast(context: Context, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(context, this, duration).show()
}
fun Int.showToast(context: Context, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(context, this, duration).show()
}
"Hello Kotlin".showToast(this)
R.string.app_name.showToast(this)
简化 Snackbar 的用法
fun View.showSnackbar(text: String, actionText: String? = null, duration: Int = Snackbar.LENGTH_SHORT, block:(() -> Unit)? = null) {
var snackbar = Snackbar.make(this, text, duration)
if(actionText != null && block != null) {
snackbar.setAction(actionText) {
block()
}
}
snackbar.show()
}
fun View.showSnackbar(resId: Int, actionText: String? = null, duration: Int = Snackbar.LENGTH_SHORT, block:(() -> Unit)? = null) {
var snackbar = Snackbar.make(this, resId, duration)
if(actionText != null && block != null) {
snackbar.setAction(actionText) {
block()
}
}
snackbar.show()
}
appBar.showSnackbar("Hello Kotlin", "Undo") {
"Undo Done".showToast(this)
}
appBar.showSnackbar(R.string.app_name)
以上分别应用了顶层函数、扩展函数以及高阶函数的知识,还用到了 vararg、参数默认值等技巧。
网友评论