别在Android ViewModel里处理异常啦,真的很坑!
ViewModel 简介
在 Android 开发的 MVVM 架构中,ViewModel 扮演着举足轻重的角色。它就像是一个可靠的管家,负责管理与 UI 相关的数据,确保在配置变更(如屏幕旋转)时数据不会丢失 。比如在一个新闻类应用中,ViewModel 可以负责从网络或本地数据库获取新闻列表数据,并将这些数据提供给 UI 层展示。当用户旋转屏幕时,Activity 或 Fragment 会重新创建,但 ViewModel 中的数据依然存在,避免了重新加载数据的繁琐过程和可能出现的闪烁、卡顿等不良用户体验。又比如在电商应用的购物车功能中,ViewModel 可以管理购物车中的商品列表、商品数量等数据,在用户进行页面切换、屏幕旋转等操作时,保证购物车数据的一致性和完整性。
ViewModel 的生命周期与 Activity 或 Fragment 不同,它的存活时间更长,直到关联的 Activity 或 Fragment 被彻底销毁才会被清除。这一特性使得它成为存储和处理 UI 相关数据的理想场所,能够有效地分离业务逻辑和 UI 逻辑,让代码的结构更加清晰、可维护。既然 ViewModel 有这么多优点,在处理异常这件事上,它是不是也同样合适呢?这就需要我们进一步探讨了。
常规做法:直接在 ViewModel 处理异常
在 Android 开发中,不少开发者习惯在 ViewModel 中直接处理异常。比如在进行网络请求时,当遇到网络连接失败、服务器返回错误等异常情况,会在 ViewModel 的方法中直接捕获并处理这些异常 。假设我们有一个获取用户信息的 ViewModel 方法:
public class UserViewModel extends ViewModel {
public void fetchUserInfo() {
// 模拟网络请求
try {
// 这里执行网络请求逻辑,获取用户信息
User user = networkService.fetchUser();
// 将获取到的用户信息存储或通知UI更新
userLiveData.setValue(user);
} catch (IOException e) {
// 直接在ViewModel中处理异常
Log.e("UserViewModel", "网络请求失败", e);
// 可以在这里设置一个默认的用户信息或者错误提示信息给UI
User defaultUser = new User();
defaultUser.setName("获取信息失败");
userLiveData.setValue(defaultUser);
}
}
}
从代码实现角度看,这种做法似乎很直接,能够快速解决异常带来的问题,让程序不至于因为异常而崩溃。但从整体架构和开发规范来看,它存在诸多弊端。
在 MVVM 架构中,ViewModel 主要职责是管理 UI 相关的数据和业务逻辑,将异常处理直接放在 ViewModel 中,会导致 ViewModel 的职责过重,代码变得臃肿且难以维护。当项目规模逐渐增大,业务逻辑变得复杂时,ViewModel 中不仅要处理正常的业务流程,还要兼顾各种异常情况的处理,代码会变得混乱不堪,可读性和可维护性大幅下降。
直接在 ViewModel 中处理异常,会使得 UI 层失去对异常的感知能力。比如在一个电商应用中,如果在 ViewModel 中处理了商品加载失败的异常,直接显示一个默认的商品信息,而 UI 层无法得知这是因为异常导致的信息显示,就无法做出更合适的交互反馈,比如提示用户重新加载、显示网络错误图标等,从而影响用户体验。而且,这种做法也不利于代码的复用,当其他地方需要处理相同类型的异常时,无法直接复用 UI 层的异常处理逻辑,增加了开发成本。
问题剖析:为什么不直接在 ViewModel 中处理异常
(一)职责单一性被破坏
ViewModel 的主要职责是管理与 UI 相关的数据和业务逻辑,它就像一个专注于数据管理的 “数据管家” 。以一个音乐播放应用为例,ViewModel 应该负责管理歌曲列表数据、播放状态(如播放、暂停、停止)等与 UI 展示密切相关的数据,以及处理如切换歌曲、调整音量等业务逻辑。
但如果在 ViewModel 中直接处理异常,就好比让一个专注于数据管理的管家突然还要承担起 “问题消防员” 的角色,职责变得混乱。比如在加载歌曲列表时遇到网络异常,如果在 ViewModel 中直接处理这个异常,代码就会变成这样:
public class MusicViewModel extends ViewModel {
public void loadSongs() {
try {
// 模拟网络请求获取歌曲列表
List<Song> songs = networkService.fetchSongs();
songLiveData.setValue(songs);
} catch (IOException e) {
// 直接在ViewModel中处理异常
Log.e("MusicViewModel", "加载歌曲列表失败", e);
// 设置一个默认的歌曲列表或者错误提示信息给UI
List<Song> defaultSongs = new ArrayList<>();
Song errorSong = new Song();
errorSong.setName("加载歌曲失败");
defaultSongs.add(errorSong);
songLiveData.setValue(defaultSongs);
}
}
}
随着业务的发展,类似的异常处理逻辑会越来越多,ViewModel 中不仅要处理正常的数据获取和业务操作,还要花费大量精力处理各种异常情况,导致代码变得臃肿不堪。原本清晰的业务逻辑被异常处理代码穿插其中,可读性和可维护性大大降低,就像一个整洁有序的房间被各种杂物堆满,难以找到真正需要的东西。
(二)UI 层与业务逻辑层耦合加深
在 MVVM 架构中,UI 层(如 Activity、Fragment)和业务逻辑层(ViewModel)应该保持相对独立,这样可以提高代码的可维护性和可扩展性 。当在 ViewModel 中直接处理异常时,就会打破这种独立性,导致 UI 层与业务逻辑层耦合加深。
例如,假设我们在 ViewModel 中处理了图片加载失败的异常,并直接设置了一个默认图片显示给 UI。当 UI 层需要根据不同的异常情况展示不同的用户提示(如网络异常时提示 “请检查网络连接”,图片格式错误时提示 “图片格式不支持”)时,由于异常处理逻辑在 ViewModel 中,UI 层无法直接获取到具体的异常信息,就很难做出合适的交互反馈。这就好比 UI 层是一个 “盲人”,无法根据异常情况做出准确的判断和行动,只能依赖 ViewModel 这个 “领路人” 给出的固定指示,而无法根据实际情况灵活调整。
再比如,如果 UI 层想要统一处理所有的异常,以实现统一的异常提示风格和交互效果,由于异常在 ViewModel 中已经被处理,UI 层就失去了统一处理的机会,增加了开发和维护的难度。一旦业务逻辑发生变化,如异常类型增多或处理方式改变,可能需要同时修改 ViewModel 和 UI 层的代码,牵一发而动全身,违背了 “开闭原则”,使代码的可维护性大打折扣。
(三)不利于异常的统一管理和处理
在一个大型项目中,异常的统一管理和处理至关重要。它可以确保整个应用的异常处理逻辑一致,便于维护和排查问题 。如果在每个 ViewModel 中都直接处理异常,就会导致异常处理逻辑分散在各个 ViewModel 中,难以进行统一管理。
以一个电商应用为例,商品模块、订单模块、支付模块等都有各自的 ViewModel,如果每个 ViewModel 都自行处理异常,当需要统一修改异常提示信息或添加新的异常处理逻辑时,就需要逐个修改每个 ViewModel 中的代码,工作量巨大且容易遗漏。而且,不同的开发者可能会采用不同的异常处理方式,导致代码风格不一致,增加了团队协作的难度。
而如果将异常处理逻辑集中在一个地方,如在 BaseActivity 或全局的异常处理类中,就可以方便地对所有异常进行统一管理和处理。可以根据不同的异常类型返回统一格式的错误信息,或者进行统一的日志记录、上报等操作,使代码结构更加清晰,维护更加方便。就像一个公司有统一的规章制度和管理流程,各个部门只需按照规定执行,就能保证整个公司的高效运转,而不是每个部门都自行制定规则,导致混乱和效率低下。
正确做法:将异常传递给 UI 层处理
(一)使用 LiveData 传递异常信息
LiveData 作为一种可观察的数据持有者类,在 MVVM 架构中扮演着数据传递和通知的关键角色,非常适合用于在 ViewModel 和 UI 层之间传递异常信息 。它具有生命周期感知能力,只有当观察者(通常是 UI 层组件,如 Activity 或 Fragment)处于活跃状态(STARTED、RESUMED)时,才会通知其数据的变化,这就避免了在 UI 不可见时进行无效的通知,从而提高了应用的性能和稳定性。
在 ViewModel 中,我们可以定义一个 LiveData 对象来专门存储异常信息。当业务逻辑中出现异常时,将异常信息设置到这个 LiveData 对象中,UI 层通过观察这个 LiveData 对象,就能及时获取到异常信息并进行相应处理。具体代码示例如下:
public class UserViewModel extends ViewModel {
private MutableLiveData<Exception> exceptionLiveData = new MutableLiveData<>();
private MutableLiveData<User> userLiveData = new MutableLiveData<>();
public LiveData<Exception> getExceptionLiveData() {
return exceptionLiveData;
}
public LiveData<User> getUserLiveData() {
return userLiveData;
}
public void fetchUserInfo() {
// 模拟网络请求
try {
// 这里执行网络请求逻辑,获取用户信息
User user = networkService.fetchUser();
userLiveData.setValue(user);
} catch (IOException e) {
// 将异常信息设置到LiveData中
exceptionLiveData.setValue(e);
}
}
}
在上述代码中,fetchUserInfo方法尝试从网络获取用户信息。如果请求过程中出现IOException异常,就会将该异常通过exceptionLiveData.setValue(e)方法传递出去。UI 层只需要观察exceptionLiveData,就能获取到异常信息,从而进行相应处理,实现了异常信息从 ViewModel 到 UI 层的传递。
(二)在 UI 层根据异常类型进行相应处理
当 UI 层接收到来自 ViewModel 传递的异常信息后,就可以根据异常类型进行不同的处理,从而为用户提供更加友好和准确的交互反馈 。比如,在一个社交应用中,如果获取好友列表时遇到网络异常,我们可以提示用户检查网络连接;如果是服务器返回的数据格式错误,我们可以提示用户稍后重试,并将错误信息上报给开发者。
以Activity为例,具体代码如下:
public class MainActivity extends AppCompatActivity {
private UserViewModel userViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
userViewModel = new ViewModelProvider(this).get(UserViewModel.class);
userViewModel.getExceptionLiveData().observe(this, new Observer<Exception>() {
@Override
public void onChanged(Exception e) {
if (e instanceof IOException) {
// 处理网络异常
Toast.makeText(MainActivity.this, "网络连接失败,请检查网络", Toast.LENGTH_SHORT).show();
} else if (e instanceof JsonParseException) {
// 处理数据解析异常
Toast.makeText(MainActivity.this, "数据解析错误,请稍后重试", Toast.LENGTH_SHORT).show();
} else {
// 处理其他异常
Toast.makeText(MainActivity.this, "发生未知错误,请稍后重试", Toast.LENGTH_SHORT).show();
}
}
});
userViewModel.getUserLiveData().observe(this, new Observer<User>() {
@Override
public void onChanged(User user) {
// 更新UI显示用户信息
TextView nameTextView = findViewById(R.id.nameTextView);
nameTextView.setText(user.getName());
}
});
// 触发获取用户信息的操作
userViewModel.fetchUserInfo();
}
}
在上述代码中,MainActivity通过userViewModel.getExceptionLiveData().observe方法观察异常信息。当接收到异常时,根据异常类型进行判断。如果是IOException,则提示用户检查网络连接;如果是JsonParseException,则提示用户数据解析错误;对于其他类型的异常,统一提示用户发生未知错误。这样,根据不同的异常类型进行针对性处理,能够更好地满足用户需求,提升用户体验,同时也使得异常处理逻辑更加清晰和可维护。