异常与代码处理
学习如何在 Android 中处理错误和异常。
异常简介
每个程序,无论新旧,都具备异常处理机制,因为异常总是会发生。问题不在于异常本身,而在于如何妥善处理它们,以避免程序崩溃和用户端出现意外情况。不良的异常处理会导致糟糕的用户体验。
异常处理能够控制程序中的错误,通过抛出异常信息来防止程序非正常终止。这意味着整个程序无需终止,异常可以通过 try/catch 机制被捕获。
什么是异常?
异常是一种通常在编译时或运行时发生的状况,表示程序可以从中恢复的情况,例如系统崩溃或空指针异常。
异常分为两类:检查型异常和非检查型异常。
检查型异常是逻辑上的异常,表明代码存在问题,但程序仍可恢复。例如,调用 API 时发生错误,可以在编译时通过 try/catch 块捕获。
非检查型异常(也称为运行时异常)通常表明程序存在严重问题,且无法恢复。例如 NullPointerException,就是由于尝试访问不存在的对象或变量引发的。
异常处理
异常处理是指响应可能破坏程序正常流程的事件的过程,这些事件是在编译期间因异常发生而产生的。
它们由 Java 中的 Exception 类表示,该类是 Java 和 Kotlin 中所有异常的父类。
Kotlin 不包含检查型异常,只支持非检查型异常。Java 同时拥有这两种异常类型,它们可以在运行时被捕获。所有这些异常类都继承自 Throwable 类。
为了处理错误,我们可以使用 try/catch 方法,避免程序崩溃和不必要的中断。
try/catch 方法
我们使用 try...catch 表达式来捕获代码中的异常。
以下是基本结构:
1try { 2 // 一些代码 3} catch (e: SomeException) { 4 // 处理逻辑 5}
try 表达式后可以跟 catch、finally 或两者都有,如下所示:
1try { 2 // 一些代码 3} catch (e: SomeException) { 4 // 处理逻辑 5} finally { 6 // 可选的 finally 块 7}
还可以有多个 catch 块,但至少需要一个。
下面我们看几个示例。
1 2fun main(args : Array<String>){ 3 var number = 20 / 0 // 抛出异常 4 println(number) 5}
从数学上讲,除以零是不允许的。因此,如果执行上述代码,将抛出 ArithmeticException。
我们需要使用 try/catch 块来处理这个异常。
1 2import kotlin.ArithmeticException 3 4fun main(args : Array<String>){ 5 try{ 6 var number = 20 / 0 7 } 8 catch(e: ArithmeticException){ 9 // 捕获并处理异常 10 println("不允许除以零") 11 } 12}
在上面的例子中,我们已处理了可能发生的异常,现在异常被成功捕获,程序不再意外终止,而是打印我们指定的错误信息。
我们还可以将 try/catch 块作为表达式使用,并为其添加返回值,如下所示:
返回值可以是 try 块的最后一个表达式,也可以是 catch 块的最后一个表达式。
1 2fun test(a: Int, b: Int) : Any { 3 return try { 4 a/b 5 //println("结果为: "+ a / b) 6 } 7 catch(e:Exception){ 8 println(e) 9 "不允许除以零" 10 } 11} 12// 主函数 13fun main(args: Array<String>) { 14 // 调用 test 函数 15 var result1 = test(10,2) // 执行 try 块 16 println(result1) 17 var result = test(10,0) // 执行 catch 块 18 println(result) 19}
在上述代码中,try 块被写成一个表达式,预期返回一个整数值。
首先传入 10 和 2,返回 5;但在第二次运算中,10 不能除以 0,出现了错误。幸运的是,异常被成功捕获,程序没有中断,而是返回了 catch 块中的表达式结果。
处理 API 响应
Android 应用程序经常通过特定的 API 结构向远程服务器请求数据,并利用现代工具和库(如 Retrofit、LiveData 和 OkHttp)完成数据获取操作。
因此,必须采取适当的措施,确保成功、失败和错误都能被正确处理并呈现给用户。
在创建用于处理 API 响应事件的类时,我们使用密封类(sealed classes)。通过密封类,我们可以处理一组预定义的子类,并根据从 API 获取的不同响应,执行相应的具体操作。
密封类(Sealed Classes)
密封类代表受限制的类层次结构。这意味着它们对继承具有更强的控制力:密封类的所有子类在编译时已知且数量固定,外部类无法继承它们。继承自密封类的类必须与密封类位于同一文件中。
密封类默认是抽象的,因此不能直接实例化。
让我们在应用程序中添加以下密封类,用于处理 API 响应。首先创建一个名为 utility 的包,然后在其中创建一个名为 Resource 的类,并加入以下代码:
1sealed class Resource<T>( 2 val data: T? = null, 3 val throwable: Throwable? = null 4) { 5 class Success<T>(data: T?) : Resource<T>(data) 6 class Error<T>(throwable: Throwable, data: T? = null) : Resource<T>(data, throwable) 7 class Loading<T>(data: T? = null) : Resource<T>() 8}
常量
在 Kotlin 中,我们可以使用 const 关键字,或将所有常量放在一个 companion object 中(相当于 Java 中的静态成员)。在我们的应用中,我们将常量放入 companion object,如下所示:
1package com.example.globalnewsapp.util 2 3class Constants { 4 5 companion object { 6 const val BASE_URL = "https://newsapi.org" 7 const val SEARCH_NEWS_TIME_DELAY = 500L 8 const val QUERY_PAGE_SIZE = 20 9 } 10}
RecyclerViewDiffUtil 类
DiffUtil 是一个使用 Eugene W. Myers 差异算法计算两个列表之间差异的类。它输出一个列表,通过计算两个列表之间的差异,将第一个列表转换为第二个列表,并在更新操作中避免重新构建可见视图,从而节省性能开销。
让我们将其添加到我们的 utility 包中,并学习其工作原理。
1class RecyclerViewDiffUtil { 2 3 companion object { 4 5 val differCallBack = object : DiffUtil.ItemCallback<Article>() { 6 override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean { 7 return oldItem.url == newItem.url 8 } 9 10 override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean { 11 return oldItem == newItem 12 } 13 } 14 15 val favouriteNewsDifferCallBack = object: DiffUtil.ItemCallback<FavouriteArticles>() { 16 override fun areItemsTheSame(oldItem: FavouriteArticles, newItem: FavouriteArticles): Boolean { 17 return oldItem.url == newItem.url 18 } 19 20 override fun areContentsTheSame(oldItem: FavouriteArticles, newItem: FavouriteArticles): Boolean { 21 return oldItem == newItem 22 } 23 } 24 } 25}
DiffUtil 用于提升 RecyclerView 的性能。它提供了一种智能方式来更新列表,而不是使用 notifyDataSetChanged 方法——后者代价高昂,因为它反复更新整个列表,消耗大量带宽,导致用户体验延迟。这会使应用响应变慢。幸运的是,DiffUtil 可以解决这一问题。
DiffUtil 包含两个抽象方法和一个非抽象方法,必须被重写。它们分别是:
areItemsTheSame(oldItem: Article, newItem: Article):判断两个项目是否代表RecyclerView中相同的对象。areContentsTheSame(oldItem: Article, newItem: Article):判断两个项目是否具有相同的数据,若相同则返回true。
DiffUtil 类将在适配器中加载 RecyclerView 时被实现。