文章目录
十一、网络
11.1 WebView
通过WebView可在自己的APP内嵌一个浏览器, 而不用跳转到系统浏览器, 也不需要自己造浏览器的轮子
新建一个WebViewTest项目, 布局如下
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
<uses-permission android:name="android.permission.INTERNET" />
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
webView.settings.javaScriptEnabled=true
// 当需要从一个网页跳转到另一个网页时,我们希望目标网页仍然在当前WebView中显示,而不是打开系统浏览器
webView.webViewClient = WebViewClient()
webView.loadUrl("https://www.baidu.com")
}
}
11.2 用 HTTP 访问网络
11.2.1 HttpURLConnection
新建一个NetWorkTest, 布局如下
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<Button
android:id="@+id/sendRequestBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Send Request" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:id="@+id/responseText"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</ScrollView>
</LinearLayout>
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
sendRequestBtn.setOnClickListener {
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) {
// 因为Android是不允许在子线程中进行UI操作的, 所以需用此函数指定线程
runOnUiThread {
// 在这里进行UI操作,将结果显示到界面上
responseText.text = response
}
}
}
11.2.2 OkHttp 库
第三方的库, 比原生更好用, 是事实标准
dependencies {
...
implementation 'com.squareup.okhttp3:okhttp:4.1.0'
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
sendRequestBtn.setOnClickListener {
sendRequestWithHttpURLConnection()
}
}
private fun sendRequestWithHttpURLConnection() {
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(responseData)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private fun showResponse(response: String) {
// 因为Android是不允许在子线程中进行UI操作的, 所以需用此函数指定线程
runOnUiThread {
// 在这里进行UI操作,将结果显示到界面上
responseText.text = response
}
}
}
11.3 网络请求的回调实现方式
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)
}
}
// 回调onFinish()方法
listener.onFinish(response.toString())
} catch (e: Exception) {
e.printStackTrace()
// 回调onError()方法
listener.onError(e)
} finally {
connection?.disconnect()
}
}
}
}
interface HttpCallbackListener {
fun onFinish(response: String)
fun onError(e: Exception)
}
这样调用
object HttpUtil {
...
fun sendOkHttpRequest(address: String, callback: okhttp3.Callback) {
val client = OkHttpClient()
val request = Request.Builder()
.url(address)
.build()
client.newCall(request).enqueue(callback)
}
}
HttpUtil.sendOkHttpRequest(address, object : Callback {
override fun onResponse(call: Call, response: Response) {
// 得到服务器返回的具体内容
val responseData = response.body?.string()
}
override fun onFailure(call: Call, e: IOException) {
// 在这里对异常情况进行处理
}
})
11.4 Retrofit 网络库
- 可以配置好一个根路径,然后在指定服务器接口地址时只需要使用相对路径即可,这样就不用每次都指定完整的URL地址了
- 允许我们对服务器接口进行归类,将功能同属一类的服务器接口定义到同一个接口文件当中,从而让代码结构变得更加合理
- 不用关心网络通信的细节,只需要在接口文件中声明一系列方法和返回值,然后通过注解的方式指定该方法对应哪个服务器接口,以及需要提供哪些参数。当我们在程序中调用该方法时,Retrofit会自动向对应的服务器接口发起请求,并将响应的数据解析成返回值声明的类型。这就使得我们可以用更加面向对象的思维来进行网络操作
dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
}
布局如下
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<Button
android:id="@+id/getAppDataBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Get App Data" />
</LinearLayout>
调用方式如下
class App(val id: String, val name: String, val version: String)
interface AppService {
// 当调用getAppData()方法时Retrofit会发起一条GET请求,请求的地址就是我们在@GET注解中传入的具体参数
// 返回值必须声明成Retrofit中内置的Call类型,并通过泛型来指定服务器响应的数据应该转换成什么对象
@GET("get_data.json")
fun getAppData(): Call<List<App>>
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
getAppDataBtn.setOnClickListener {
val retrofit = Retrofit.Builder()
.baseUrl("http://10.0.2.2/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val appService = retrofit.create(AppService::class.java)
// 调用getAppData()会返回会返回Call<List<App>>对象
// 再调用enqueue(), retrofit会根据注解中配置的服务器接口地址, 去请求网络
appService.getAppData().enqueue(object : Callback<List<App>> {
override fun onResponse(call: Call<List<App>>, response: Response<List<App>>) {
val list = response.body() // 得到retrofit解析后的对象, 即List<App>
if (list != null) {
for (app in list) {
Log.d("MainActivity", "id is ${
app.id}")
Log.d("MainActivity", "name is ${
app.name}")
Log.d("MainActivity", "version is ${
app.version}")
}
}
}
override fun onFailure(call: Call<List<App>>, t: Throwable) {
t.printStackTrace()
}
})
}
}
}
AndroidManifest.xml如下
<uses-permission android:name="android.permission.INTERNET" />
11.4.1 复杂接口参数
// GET http://example.com/get_data.json
interface ExampleService {
@GET("get_data.json")
fun getData(): Call<Data>
}
// GET http://example.com/<page>/get_data.json
interface ExampleService {
@GET("{page}/get_data.json")
fun getData(@Path("page") page: Int): Call<Data>
}
// GET http://example.com/get_data.json?u=<user>&t=<token>
interface ExampleService {
@GET("get_data.json")
fun getData(@Query("u") user: String, @Query("t") token: String): Call<Data>
}
// POST http://example.com/data/create
// {"id": 1, "content": "The description for this data."}
interface ExampleService {
@POST("data/create")
fun createData(@Body data: Data): Call<ResponseBody>
}
// GET http://example.com/get_data.json
// User-Agent: okhttp
// Cache-Control: max-age=0
interface ExampleService {
@GET("get_data.json")
fun getData(@Header("User-Agent") userAgent: String,
@Header("Cache-Control") cacheControl: String): Call<Data>
}
通常用单例类实现, 只需要在调用create()方法时针对不同的Service接口传入相应的Class类型即可
object ServiceCreator {
private const val BASE_URL = "http://10.0.2.2/"
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
fun <T> create(serviceClass: Class<T>): T = retrofit.create(serviceClass)
}
使用时, 这样调用
val appService = ServiceCreator.create(AppService::class.java)
// 或
object ServiceCreator {
...
inline fun <reified T> create(): T = create(T::class.java)
}
val appService = ServiceCreator.create<AppService>()
十二 Jetpack 组件
12.1 ViewModel
是Jetpack最重要的组件, 为了解决传统的Activity需要处理逻辑/UI展示/网络回调的太耦合的问题, 其专门存放UI相关的数据
当手机横竖屏旋转时, Activity会被重新创建而使UI数据丢失, 而ViewModel可保证跟随Activity生命周期同步, 避免因横竖屏导致UI数据的丢失
dependencies {
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
}
每一个Activity和Fragment都创建一个对应的ViewModel
我们为MainActivity创建一个对应的MainViewModel类,并让它继承自ViewModel
class MainViewModel : ViewModel() {
}
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/infoText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:textSize="32sp"/>
<Button
android:id="@+id/plusOneBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="Plus One"/>
</LinearLayout>
class MainViewModel : ViewModel() {
var counter = 0
}
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProvider(this)[MainViewModel::class.java]
plusOneBtn.setOnClickListener {
viewModel.counter++
refreshCounter()
}
refreshCounter()
}
private fun refreshCounter() {
infoText.text = viewModel.counter.toString()
}
}
- Factory向ViewModel传参数
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
}
}
class MainActivity : AppCompatActivity() {
lateinit var viewModel: MainViewModel
lateinit var sp: SharedPreferences
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
sp = getPreferences(Context.MODE_PRIVATE)
val countReserved = sp.getInt("count_reserved", 0)
// 通过ViewModelProvider来获取ViewModel的实例, 可保证ViewModel有独立的声明周期且长于Activity
viewModel = ViewModelProvider(this)[MainViewModel::class.java]
plusOneBtn.setOnClickListener {
viewModel.counter++
refreshCounter()
}
refreshCounter()
}
private fun refreshCounter() {
infoText.text = viewModel.counter.toString()
}
override fun onPause() {
super.onPause()
sp.edit {
putInt("count_reserved", viewModel.counter)
}
}
}
12.2 Lifecycles
在编写Android应用程序的时候,可能会经常遇到需要感知Activity生命周期的情况。比如说,
某个界面中发起了一条网络请求,但是当请求得到响应的时候,界面或许已经关闭了,这个时
候就不应该继续对响应的结果进行处理。因此,我们需要能够时刻感知到Activity的生命周期,
以便在适当的时候进行相应的逻辑控制。
感知Activity的生命周期并不复杂,早在第3章的时候我们就学习过Activity完整的生命周期流
程。但问题在于,在一个Activity中去感知它的生命周期非常简单,而如果要在一个非Activity
的类中去感知Activity的生命周期,应该怎么办呢?
这种需求是广泛存在的,同时也衍生出了一系列的解决方案,比如通过在Activity中嵌入一个隐
藏的Fragment来进行感知,或者通过手写监听器的方式来进行感知,等等
class MyObserver {
fun activityStart() {
}
fun activityStop() {
}
}
// 这里我们为了让MyObserver能够感知到Activity的生命周期,需要专门在
// MainActivity中重写相应的生命周期方法,然后再通知给MyObserver。这种实现方式虽然是
// 可以正常工作的,但是不够优雅,需要在Activity中编写太多额外的逻辑
class MainActivity : AppCompatActivity() {
lateinit var observer: MyObserver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
observer = MyObserver()
}
override fun onStart() {
super.onStart()
observer.activityStart()
}
override fun onStop() {
super.onStop()
observer.activityStop()
}
}
而Lifecycles组件就是为了解决这个问题而出现的,它可以让任何一个类都能轻松感知到
Activity的生命周期,同时又不需要在Activity中编写大量的逻辑处理。
下面我们就通过具体的例子来学习Lifecycles组件的用法。新建一个MyObserver类,并让
它实现LifecycleObserver接口,代码如下所示:
// 这里我们为了让MyObserver能够感知到Activity的生命周期,需要专门在
// MainActivity中重写相应的生命周期方法,然后再通知给MyObserver。这种实现方式虽然是
// 可以正常工作的,但是不够优雅,需要在Activity中编写太多额外的逻辑
class MainActivity : AppCompatActivity() {
lateinit var observer: MyObserver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 只要添加这样一行代码,MyObserver就能自动感知到Activity的生命周期了
lifecycle.addObserver(MyObserver())
}
}
class MyObserver(val lifecycle: Lifecycle) : LifecycleObserver {
// 上使用了@OnLifecycleEvent注解,并传入了一种生命周期事件。
// 生命周期事件的类型一共有7种:ON_CREATE、ON_START、ON_RESUME、ON_PAUSE、ON_STOP和ON_DESTROY
// 分别匹配Activity中相应的生命周期回调;另外还有一种ON_ANY类型,表示可以匹配Activity的任何生命周期回调。
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun activityStart() {
Log.d("MyObserver", "activityStart")
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun activityStop() {
Log.d("MyObserver", "activityStop")
}
}
可在任何地方调用lifecycle.currentState
来主动获知当前的生命周期状态, 其返回值为如下枚举有INITIALIZED、DESTROYED、CREATED、STARTED、RESUMED
这5种
12.3 LiveData
响应式组件, 可包含任意类型的数据, 当数据变化时通知观察者, 其特别适合于ViewModel
结合使用
class MainViewModel(countReserved: Int) : ViewModel() {
// MutableLiveData是一种可变的LiveData,它有3种读写数据的方法,分别是getValue()、setValue()和postValue()方法
val counter = MutableLiveData<Int>()
init {
counter.value = countReserved
}
fun plusOne() {
val count = counter.value ?: 0
counter.value = count + 1
}
fun clear() {
counter.value = 0
}
}
- LiveData的map()方法,可将实际包含数据的LiveData和仅用于观察数据的LiveData进行转换。
data class User(var firstName: String, var lastName: String, var age: Int)
class MainViewModel(countReserved: Int) : ViewModel() {
private val userLiveData = MutableLiveData<User>()
// 第一个参数是原始的LiveData对象;第二个参数是一个转换函数
// 当userLiveData的数据发生变化时,map()方法会监听到变化并执行转换函数中的逻辑,然后再将转换之后的数据通知给userName的观察者
val userName: LiveData<String> = Transformations.map(userLiveData) {
user ->
"${
user.firstName} ${
user.lastName}"
}
}
- LiveData的switchMap()方法可以调其他接口
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
}
}
12.3 Room
要由Entity、Dao和Database这3部分组成
- Entity。用于定义封装实际数据的实体类,每个实体类都会在数据库中有一张对应的表,并
且表中的列是根据实体类中的字段自动生成的。 - Dao。是数据访问对象的意思,通常会在这里对数据库的各项操作进行封装,在实际
编程的时候,逻辑层就不需要和底层数据库打交道了,直接和Dao层进行交互即可。 - Database。用于定义数据库中的关键信息,包括数据库的版本号、包含哪些实体类以及提
供Dao层的访问实例。
@Entity
data class User(var firstName: String, var lastName: String, var age: Int) {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}
@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
}
12.4 WorkManager
用于处理一些要求定时执行的任务,它可以根据操作系统的版本自动选择底层是使用AlarmManager实现还是JobScheduler实现
WorkManager和Service并不相同,也没有直接的联系。
- Service是Android系统的四大组件之一,它在没有被销毁的情况下是一直保持在后台运行的。
- WorkManager只是一个处理定时任务的工具,它可以保证即使在应用退出甚至手机重启的情况下,之前注册的任务仍然将会得到执行
- 因此WorkManager很适合用于执行一些定期和服务器进行交互的任务,比如周期性地同步数据,等等
- WorkManager注册的周期性任务不能保证一定会准时执行,这并不是bug,而是系统为了减少电量消耗,可能会将触发时间临近的几个任务放在一起执行,这样可以大幅度地减少CPU被唤醒的次数,从而有效延长电池的使用时间
dependencies {
implementation "androidx.work:work-runtime:2.2.0"
}
class SimpleWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
// doWork()方法不会运行在主线程当中,因此你可以放心地在这里执行耗时逻辑,不过这里简单起见只是打印了一行日志。
override fun doWork(): Result {
Log.d("SimpleWorker", "do work in SimpleWorker")
// 成功就返回Result.success(),失败就返回Result.failure()
return Result.success()
}
}
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java).build()
val request = PeriodicWorkRequest.Builder(SimpleWorker::class.java, 15, TimeUnit.MINUTES).build()
WorkManager.getInstance(context).enqueue(request)
链式任务: 假设这里定义了3个独立的后台任务:同步数据、压缩数据和上传数据。现在我们想要实现先同
步、再压缩、最后上传的功能,就可以借助链式任务来实现,代码示例如下
val sync = ...
val compress = ...
val upload = ...
WorkManager.getInstance(this)
.beginWith(sync)
.then(compress)
.then(upload)
.enqueue()