From bbf6cc6679497e36eb46b7e58d9aca3a4507f7b0 Mon Sep 17 00:00:00 2001 From: TongTongStudio Date: Fri, 21 Aug 2026 19:10:09 +0800 Subject: [PATCH] =?UTF-8?q?refactor(mvvm):=20=E9=87=8D=E6=9E=84=E9=97=B9?= =?UTF-8?q?=E9=92=9F=E4=B8=8E=E5=BA=94=E7=94=A8=E5=88=97=E8=A1=A8ViewModel?= =?UTF-8?q?=E4=B8=BAAndroidViewModel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将ViewModel改为继承BaseAndroidViewModel,通过构造器注入Application,使用addDisposable管理RxJava订阅,LiveData改为私有并提供getter,移除RxLifecycle依赖。 --- AGENTS.md | 1059 +++++++++++++++++ .../activity/alarm/add/AlarmAddActivity.java | 4 +- .../activity/alarm/add/AlarmAddViewModel.java | 84 +- .../alarm/edit/AlarmEditActivity.java | 6 +- .../alarm/edit/AlarmEditViewModel.java | 108 +- .../alarm/list/AlarmListActivity.java | 6 +- .../alarm/list/AlarmListViewModel.java | 121 +- .../dialer/activity/app/AppListActivity.java | 16 +- .../dialer/activity/app/AppListViewModel.java | 175 +-- .../contact/add/ContactAddActivity.java | 6 +- .../contact/add/ContactAddViewModel.java | 34 +- .../contact/edit/ContactEditActivity.java | 19 +- .../contact/edit/ContactEditViewModel.java | 24 +- .../contact/list/ContactListActivity.java | 2 +- .../contact/list/ContactListViewModel.java | 17 +- .../contact/test/ContactTestActivity.java | 6 +- .../contact/test/ContactTestViewModel.java | 30 +- .../dialer/activity/main/MainActivity.java | 64 +- .../dialer/activity/main/MainViewModel.java | 97 +- .../settings/call/SettingsCallActivity.java | 2 +- .../settings/call/SettingsCallViewModel.java | 4 +- .../settings/home/SettingsActivity.java | 8 +- .../settings/home/SettingsViewModel.java | 35 +- .../settings/utils/SettingsUtilsActivity.java | 2 +- .../utils/SettingsUtilsViewModel.java | 5 +- .../activity/template/TemplateActivity.java | 2 +- .../activity/template/TemplateViewModel.java | 4 +- .../weather/main/WeatherMainActivity.java | 13 +- .../weather/main/WeatherMainViewModel.java | 26 +- .../com/ttstd/dialer/base/BaseFragment.java | 103 +- .../base/mvvm/BaseAndroidViewModel.java | 135 +++ .../dialer/base/mvvm/BaseMvvmActivity.java | 161 ++- .../ttstd/dialer/base/mvvm/BaseViewModel.java | 133 ++- .../ttstd/dialer/base/mvvm/IMvvmUiState.java | 22 + .../mvvm/fragment/BaseMvvmDialogFragment.java | 132 +- .../base/mvvm/fragment/BaseMvvmFragment.java | 164 ++- .../dialer/fragment/app/AppFragment.java | 8 +- .../dialer/fragment/app/AppViewModel.java | 62 +- .../fragment/contact/ContactFragment.java | 10 +- .../fragment/contact/ContactViewModel.java | 27 +- .../dialog/contact/call/CallFragment.java | 2 +- .../dialog/contact/call/CallViewModel.java | 4 +- .../delete/ContactDeleteDialogFragment.java | 2 +- .../delete/ContactDeleteViewModel.java | 4 +- .../contact/edit/EditDialogFragment.java | 2 +- .../dialog/contact/edit/EditViewModel.java | 4 +- .../dialog/dock/DockAppDialogFagment.java | 10 +- .../dialog/dock/DockAppViewModel.java | 5 +- .../dialog/move/MoveAppDialogFagment.java | 10 +- .../dialog/move/MoveAppViewModel.java | 4 +- .../shortcut/ShortcutDialogFagment.java | 10 +- .../dialog/shortcut/ShortcutViewModel.java | 4 +- .../dialer/fragment/home/HomeFragment.java | 2 +- .../dialer/fragment/home/HomeViewModel.java | 4 +- .../fragment/settings/SettingsFragment.java | 2 +- .../fragment/settings/SettingsViewModel.java | 4 +- .../ttstd/dialer/network/OkHttpManager.java | 7 + 57 files changed, 2236 insertions(+), 780 deletions(-) create mode 100644 AGENTS.md create mode 100644 app/src/main/java/com/ttstd/dialer/base/mvvm/BaseAndroidViewModel.java create mode 100644 app/src/main/java/com/ttstd/dialer/base/mvvm/IMvvmUiState.java diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f8f3215 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,1059 @@ +# AIAGENTS.md — Android MVVM 架构规范 + +> 本文件用于指导 AI Agent 在 Android 项目中正确生成 MVVM 架构代码。 +> 所有新增/修改的 Activity、Fragment、ViewModel 必须遵循以下规范。 + +--- + +## 一、架构总览 + +``` +┌─────────────────────────────────────────────────────┐ +│ View (UI) │ +│ Activity / Fragment / ViewBinding │ +│ 职责:渲染 UI、接收用户输入、订阅 ViewModel 状态 │ +└────────────────────┬────────────────────────────────┘ + │ 调用方法 / 观察 LiveData + ▼ +┌─────────────────────────────────────────────────────┐ +│ ViewModel │ +│ 持有 UI 状态、处理业务逻辑、暴露数据 │ +│ 职责:不持有 View 引用、不直接操作数据层 │ +└────────────────────┬────────────────────────────────┘ + │ 调用 + ▼ +┌─────────────────────────────────────────────────────┐ +│ Repository │ +│ 统一数据源入口(网络 + 本地缓存) │ +└────────────────────┬────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ Model (Data) │ +│ Retrofit / Room / SharedPreferences │ +└─────────────────────────────────────────────────────┘ +``` + +--- + +## 二、核心原则 + +| 原则 | 说明 | +|------|------| +| **单向数据流** | View → 调用 ViewModel 方法 → ViewModel 更新 LiveData → View 被动刷新 | +| **ViewModel 不持有 View 引用** | 禁止传入 Activity / Fragment / View / Context | +| **View 不直接访问数据层** | 所有数据必须通过 ViewModel → Repository | +| **生命周期安全** | ViewModel 中的数据操作需在 `viewModelScope` 或 `Lifecycle` 感知下执行 | +| **可测试性** | ViewModel 不依赖 Android Framework,便于单元测试 | + +--- + +## 三、基类代码 + +### 3.1 BaseViewModel(无需 Context 时使用) + +> **适用场景:** ViewModel 不需要 `Context`(如纯内存计算、通过 Repository 访问数据等)。 +> 若需要 `Context` / `Application`,请使用 **3.2 BaseAndroidViewModel**。 + +```java +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; + +/** + * MVVM ViewModel 基类(无 Context 版本) + * + * 职责: + * - 持有 UI 状态(LiveData) + * - 处理业务逻辑 + * - 暴露数据给 View 层 + * - 通过 CompositeDisposable 统一管理 RxJava 异步任务 + * + * 禁止: + * - 持有任何 View 引用(Activity / Fragment / View / Context) + * - 直接进行网络请求或数据库操作(必须通过 Repository) + * - 在 ViewModel 中弹 Toast / Dialog / Snackbar + * - 手动创建 Thread / Executor 执行异步任务(必须使用 RxJava + CompositeDisposable) + * - 忘记将 Disposable add 到 disposables 中 + */ +public abstract class BaseViewModel extends ViewModel { + + // ============================================================ + // 通用 UI 状态 + // ============================================================ + + /** 全局加载状态 */ + private final MutableLiveData loadingLiveData = new MutableLiveData<>(false); + + /** 全局错误提示 */ + private final MutableLiveData errorLiveData = new MutableLiveData<>(); + + /** 全局成功提示 */ + private final MutableLiveData successLiveData = new MutableLiveData<>(); + + // ============================================================ + // RxJava 异步管理(核心) + // ============================================================ + + /** + * CompositeDisposable 用于统一管理所有 RxJava 订阅。 + * 在 onCleared() 中自动 dispose,防止内存泄漏。 + * + * 使用方式: + * disposables.add( + * repository.fetchUser(id) + * .subscribeOn(Schedulers.io()) + * .observeOn(AndroidSchedulers.mainThread()) + * .subscribe(user -> { ... }, throwable -> { ... }) + * ); + */ + protected final CompositeDisposable disposables = new CompositeDisposable(); + + // ============================================================ + // 对外暴露(只读) + // ============================================================ + + public LiveData getLoadingLiveData() { + return loadingLiveData; + } + + public LiveData getErrorLiveData() { + return errorLiveData; + } + + public LiveData getSuccessLiveData() { + return successLiveData; + } + + // ============================================================ + // 受保护的方法(子类调用) + // ============================================================ + + protected void setLoading(boolean loading) { + loadingLiveData.postValue(loading); + } + + protected void setError(String message) { + errorLiveData.postValue(message); + } + + protected void setSuccess(String message) { + successLiveData.postValue(message); + } + + /** + * 便捷方法:将 Disposable 添加到 CompositeDisposable 中管理。 + * 子类在发起 RxJava 请求时调用此方法。 + */ + protected void addDisposable(Disposable disposable) { + disposables.add(disposable); + } + + // ============================================================ + // 生命周期 + // ============================================================ + + @Override + protected void onCleared() { + super.onCleared(); + // ViewModel 销毁时,自动取消所有正在执行的 RxJava 任务 + if (!disposables.isDisposed()) { + disposables.clear(); + } + } +} +``` + +--- + +### 3.2 BaseAndroidViewModel(需要 Context 时使用) + +> **适用场景:** ViewModel 需要 `Context` / `Application`(如读取 `SharedPreferences`、访问 `Resources`、使用 `SystemService` 等)。 +> 继承 `AndroidViewModel`,通过 `getApplication()` 获取 `Application` 上下文(**不会泄漏**)。 +> 其余代码结构与 `BaseViewModel` 完全一致。 + +```java +import android.app.Application; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; + +/** + * MVVM ViewModel 基类(带 Context 版本) + * + * 继承 AndroidViewModel,可通过 getApplication() 获取 Application 级别的 Context。 + * Application Context 生命周期与进程一致,不会造成内存泄漏。 + * + * 职责: + * - 持有 UI 状态(LiveData) + * - 处理业务逻辑 + * - 暴露数据给 View 层 + * - 通过 CompositeDisposable 统一管理 RxJava 异步任务 + * - 可通过 getApplication() 获取 Application Context(安全) + * + * 禁止: + * - 持有任何 View 引用(Activity / Fragment / View) + * - 使用 Activity Context 或持有 View 的 Context(只能使用 Application) + * - 直接进行网络请求或数据库操作(必须通过 Repository) + * - 在 ViewModel 中弹 Toast / Dialog / Snackbar + * - 手动创建 Thread / Executor 执行异步任务(必须使用 RxJava + CompositeDisposable) + * - 忘记将 Disposable add 到 disposables 中 + */ +public abstract class BaseAndroidViewModel extends AndroidViewModel { + + // ============================================================ + // 通用 UI 状态 + // ============================================================ + + /** 全局加载状态 */ + private final MutableLiveData loadingLiveData = new MutableLiveData<>(false); + + /** 全局错误提示 */ + private final MutableLiveData errorLiveData = new MutableLiveData<>(); + + /** 全局成功提示 */ + private final MutableLiveData successLiveData = new MutableLiveData<>(); + + // ============================================================ + // RxJava 异步管理(核心) + // ============================================================ + + /** + * CompositeDisposable 用于统一管理所有 RxJava 订阅。 + * 在 onCleared() 中自动 dispose,防止内存泄漏。 + */ + protected final CompositeDisposable disposables = new CompositeDisposable(); + + // ============================================================ + // 构造方法 + // ============================================================ + + public BaseAndroidViewModel(@NonNull Application application) { + super(application); + } + + // ============================================================ + // 对外暴露(只读) + // ============================================================ + + public LiveData getLoadingLiveData() { + return loadingLiveData; + } + + public LiveData getErrorLiveData() { + return errorLiveData; + } + + public LiveData getSuccessLiveData() { + return successLiveData; + } + + // ============================================================ + // 受保护的方法(子类调用) + // ============================================================ + + protected void setLoading(boolean loading) { + loadingLiveData.postValue(loading); + } + + protected void setError(String message) { + errorLiveData.postValue(message); + } + + protected void setSuccess(String message) { + successLiveData.postValue(message); + } + + /** + * 便捷方法:将 Disposable 添加到 CompositeDisposable 中管理。 + * 子类在发起 RxJava 请求时调用此方法。 + */ + protected void addDisposable(Disposable disposable) { + disposables.add(disposable); + } + + // ============================================================ + // Context 安全访问 + // ============================================================ + + /** + * 获取 Application Context(安全,不会泄漏)。 + * 子类可通过此方法访问: + * - getApplication().getSharedPreferences(...) + * - getApplication().getResources().getString(...) + * - getApplication().getSystemService(...) + */ + @NonNull + protected Application getAppContext() { + return getApplication(); + } + + // ============================================================ + // 生命周期 + // ============================================================ + + @Override + protected void onCleared() { + super.onCleared(); + // ViewModel 销毁时,自动取消所有正在执行的 RxJava 任务 + if (!disposables.isDisposed()) { + disposables.clear(); + } + } +} +``` + +> **选择指南:** +> | 需求 | 继承 | +> |------|------| +> | 不需要 Context,纯数据驱动 | `BaseViewModel` | +> | 需要 SharedPreferences / Resources / SystemService | `BaseAndroidViewModel` | +> | 需要 Activity Context(弹 Dialog 等) | ❌ 不允许,应通过 View 层 LiveData 回调处理 | + +--- + +### 3.3 BaseMvvmActivity + +```java +/** + * MVVM Activity 基类 + * + * 职责: + * - 绑定 ViewBinding + * - 初始化 ViewModel + * - 订阅 ViewModel 中的 LiveData + * - 将用户操作转发给 ViewModel + * + * 禁止: + * - 在 Activity 中直接进行网络请求或数据库操作 + * - 持有可变 LiveData 的引用 + * - 在 Activity 中写业务逻辑 + * + * @param ViewModel 类型,必须继承 BaseViewModel 或 BaseAndroidViewModel + * @param ViewBinding 类型 + */ +public abstract class BaseMvvmActivity + extends AppCompatActivity { + + // ============================================================ + // 成员 + // ============================================================ + + protected VM viewModel; + protected VB binding; + + // ============================================================ + // 抽象方法(子类必须实现) + // ============================================================ + + /** 创建 ViewModel 实例 */ + protected abstract VM createViewModel(); + + /** 创建 ViewBinding 实例 */ + protected abstract VB createViewBinding(); + + /** 初始化界面(如设置 Adapter、Listener 等) */ + protected abstract void initView(); + + /** 订阅 ViewModel 的 LiveData */ + protected abstract void observeViewModel(); + + // ============================================================ + // 生命周期 + // ============================================================ + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // 1. 创建 ViewBinding + binding = createViewBinding(); + setContentView(binding.getRoot()); + + // 2. 创建 ViewModel + viewModel = createViewModel(); + + // 3. 订阅 ViewModel + observeViewModel(); + + // 4. 初始化界面 + initView(); + + // 5. 加载数据 + onLoadData(); + } + + /** + * 页面创建完成后加载数据 + * 子类可重写此方法触发首次数据加载 + */ + protected void onLoadData() { + // 默认空实现 + } + + // ============================================================ + // 通用订阅(可选重写) + // ============================================================ + + /** + * 订阅全局通用状态(loading / error / success) + * 子类可在 observeViewModel() 中调用 super.observeCommonState() + */ + protected void observeCommonState() { + viewModel.getLoadingLiveData().observe(this, loading -> { + if (loading != null && loading) { + showLoading(); + } else { + hideLoading(); + } + }); + + viewModel.getErrorLiveData().observe(this, error -> { + if (error != null && !error.isEmpty()) { + showError(error); + } + }); + + viewModel.getSuccessLiveData().observe(this, msg -> { + if (msg != null && !msg.isEmpty()) { + showSuccess(msg); + } + }); + } + + // ============================================================ + // UI 辅助方法(子类可重写) + // ============================================================ + + protected void showLoading() { + // 默认实现:可替换为自定义 LoadingDialog + // LoadingDialog.show(this); + } + + protected void hideLoading() { + // LoadingDialog.dismiss(); + } + + protected void showError(String message) { + Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); + } + + protected void showSuccess(String message) { + Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); + } + + // ============================================================ + // 禁止使用的模式(编译期约束) + // ============================================================ + + /** + * @deprecated 禁止在 Activity 中直接创建 Handler 做延时操作 + * → 应使用 ViewModel + LiveData 驱动 + */ + @Deprecated + protected Handler getHandler() { + throw new UnsupportedOperationException("禁止在 MVVM Activity 中使用 Handler"); + } +} +``` + +--- + +### 3.4 BaseMvvmFragment + +```java +/** + * MVVM Fragment 基类 + * + * 职责: + * - 绑定 ViewBinding + * - 初始化 ViewModel(默认与 Activity 共享 ViewModel) + * - 订阅 ViewModel 中的 LiveData + * - 将用户操作转发给 ViewModel + * + * 禁止: + * - 在 Fragment 中直接进行网络请求或数据库操作 + * - 持有可变 LiveData 的引用 + * - 在 Fragment 中写业务逻辑 + * - 使用 getActivity() 强转后直接操作 Activity(应通过 ViewModel 通信) + * + * @param ViewModel 类型,必须继承 BaseViewModel 或 BaseAndroidViewModel + * @param ViewBinding 类型 + */ +public abstract class BaseMvvmFragment + extends Fragment { + + // ============================================================ + // 成员 + // ============================================================ + + protected VM viewModel; + protected VB binding; + + /** 是否与 Activity 共享 ViewModel(默认 true) */ + protected boolean shareViewModelWithActivity() { + return true; + } + + // ============================================================ + // 抽象方法(子类必须实现) + // ============================================================ + + /** 创建 ViewModel 实例 */ + protected abstract VM createViewModel(); + + /** 创建 ViewBinding 实例 */ + protected abstract VB createViewBinding(@NonNull LayoutInflater inflater, @Nullable ViewGroup container); + + /** 初始化界面 */ + protected abstract void initView(); + + /** 订阅 ViewModel 的 LiveData */ + protected abstract void observeViewModel(); + + // ============================================================ + // 生命周期 + // ============================================================ + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + binding = createViewBinding(inflater, container); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + // 1. 创建 ViewModel + viewModel = createViewModel(); + + // 2. 订阅 ViewModel + observeViewModel(); + + // 3. 初始化界面 + initView(); + + // 4. 加载数据 + onLoadData(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + // 释放 ViewBinding 引用,防止内存泄漏 + binding = null; + } + + /** + * 页面创建完成后加载数据 + * 子类可重写此方法触发首次数据加载 + */ + protected void onLoadData() { + // 默认空实现 + } + + // ============================================================ + // 通用订阅(可选调用) + // ============================================================ + + /** + * 订阅全局通用状态(loading / error / success) + */ + protected void observeCommonState() { + if (getViewLifecycleOwner() == null) return; + + viewModel.getLoadingLiveData().observe(getViewLifecycleOwner(), loading -> { + if (loading != null && loading) { + showLoading(); + } else { + hideLoading(); + } + }); + + viewModel.getErrorLiveData().observe(getViewLifecycleOwner(), error -> { + if (error != null && !error.isEmpty()) { + showError(error); + } + }); + + viewModel.getSuccessLiveData().observe(getViewLifecycleOwner(), msg -> { + if (msg != null && !msg.isEmpty()) { + showSuccess(msg); + } + }); + } + + // ============================================================ + // UI 辅助方法(子类可重写) + // ============================================================ + + protected void showLoading() { + // 可替换为自定义 LoadingDialog + } + + protected void hideLoading() { + // LoadingDialog.dismiss(); + } + + protected void showError(String message) { + if (getContext() != null) { + Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show(); + } + } + + protected void showSuccess(String message) { + if (getContext() != null) { + Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show(); + } + } + + // ============================================================ + // 工具方法 + // ============================================================ + + /** + * 获取 Activity 级别的 ViewModel(用于 Fragment 间通信) + */ + protected T getActivityViewModel(Class vmClass) { + return new ViewModelProvider(requireActivity()).get(vmClass); + } + + // ============================================================ + // 禁止使用的模式 + // ============================================================ + + /** + * @deprecated 禁止在 Fragment 中直接使用 getActivity() 强转 + * → 应通过 ViewModel 或接口回调通信 + */ + @Deprecated + protected T getActivityAs(Class clazz) { + throw new UnsupportedOperationException("禁止强转 Activity,请使用 ViewModel 通信"); + } +} +``` + +--- + +## 四、使用示例 + +### 4.1 无需 Context 的 ViewModel(继承 BaseViewModel) + +```java +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import io.reactivex.disposables.Disposable; + +public class UserViewModel extends BaseViewModel { + + private final MutableLiveData userLiveData = new MutableLiveData<>(); + private final UserRepository repository; + + public UserViewModel(UserRepository repository) { + this.repository = repository; + } + + public LiveData getUser() { + return userLiveData; + } + + public void loadUser(String userId) { + setLoading(true); + + Disposable disposable = repository.fetchUser(userId) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + user -> { + userLiveData.setValue(user); + setSuccess("加载成功"); + setLoading(false); + }, + throwable -> { + setError(throwable.getMessage()); + setLoading(false); + } + ); + + // 必须添加到 disposables 中,确保 ViewModel 销毁时自动取消 + addDisposable(disposable); + } + + public void updateUserName(String userId, String newName) { + setLoading(true); + + Disposable disposable = repository.updateName(userId, newName) + .andThen(repository.fetchUser(userId)) // 更新后刷新数据 + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + user -> { + userLiveData.setValue(user); + setSuccess("更新成功"); + setLoading(false); + }, + throwable -> { + setError(throwable.getMessage()); + setLoading(false); + } + ); + + addDisposable(disposable); + } +} +``` + +> **说明:** `UserRepository` 的方法返回 `Single` / `Completable` 等 RxJava 类型, +> 由 ViewModel 通过 `disposables` 统一管理订阅生命周期。 + +--- + +### 4.2 需要 Context 的 ViewModel(继承 BaseAndroidViewModel) + +```java +import android.app.Application; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import io.reactivex.disposables.Disposable; + +/** + * 需要读取 SharedPreferences / Resources 等系统服务时使用 BaseAndroidViewModel + */ +public class SettingsViewModel extends BaseAndroidViewModel { + + private final MutableLiveData appVersionLiveData = new MutableLiveData<>(); + private final MutableLiveData darkModeLiveData = new MutableLiveData<>(); + private final SettingsRepository repository; + + public SettingsViewModel(Application application, SettingsRepository repository) { + super(application); + this.repository = repository; + } + + public LiveData getAppVersion() { + return appVersionLiveData; + } + + public LiveData getDarkMode() { + return darkModeLiveData; + } + + /** + * 读取 Application Context 中的 SharedPreferences + */ + public void loadSettings() { + setLoading(true); + + // 通过 getAppContext() 安全获取 Application Context + String version = getAppContext() + .getPackageManager() + .getPackageInfo(getAppContext().getPackageName(), 0) + .versionName; + appVersionLiveData.postValue(version); + + // 从 SharedPreferences 读取设置 + boolean darkMode = getAppContext() + .getSharedPreferences("settings", Application.MODE_PRIVATE) + .getBoolean("dark_mode", false); + darkModeLiveData.postValue(darkMode); + + setLoading(false); + } + + /** + * 保存设置到 SharedPreferences + 远程同步 + */ + public void saveDarkMode(boolean enabled) { + setLoading(true); + + // 本地持久化(使用 Application Context,安全) + getAppContext() + .getSharedPreferences("settings", Application.MODE_PRIVATE) + .edit() + .putBoolean("dark_mode", enabled) + .apply(); + + // 远程同步 + Disposable disposable = repository.syncDarkMode(enabled) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + () -> { + darkModeLiveData.setValue(enabled); + setSuccess("设置已保存"); + setLoading(false); + }, + throwable -> { + setError(throwable.getMessage()); + setLoading(false); + } + ); + + addDisposable(disposable); + } +} +``` + +> **关键点:** +> - 构造函数必须接收 `Application` 并传给 `super(application)` +> - 通过 `getAppContext()` 获取 Context,绝不使用 Activity Context +> - 其余 RxJava 订阅管理与 `BaseViewModel` 完全一致 + +### 4.3 具体 Activity(使用 BaseViewModel) + +```java +public class UserProfileActivity extends BaseMvvmActivity { + + @Override + protected UserViewModel createViewModel() { + UserRepository repository = new UserRepository(); + return new ViewModelProvider(this, new ViewModelProvider.Factory() { + @Override + public T create(Class modelClass) { + return (T) new UserViewModel(repository); + } + }).get(UserViewModel.class); + } + + @Override + protected ActivityUserProfileBinding createViewBinding() { + return ActivityUserProfileBinding.inflate(getLayoutInflater()); + } + + @Override + protected void initView() { + binding.btnRefresh.setOnClickListener(v -> { + viewModel.loadUser("123"); + }); + + binding.btnUpdate.setOnClickListener(v -> { + viewModel.updateUserName("123", "新名字"); + }); + } + + @Override + protected void observeViewModel() { + observeCommonState(); + + viewModel.getUser().observe(this, user -> { + if (user != null) { + binding.tvName.setText(user.getName()); + binding.tvAge.setText(String.valueOf(user.getAge())); + } + }); + } +} +``` + +### 4.4 具体 Fragment + +```java +public class UserDetailFragment extends BaseMvvmFragment { + + @Override + protected UserViewModel createViewModel() { + UserRepository repository = new UserRepository(); + return new ViewModelProvider(requireActivity(), new ViewModelProvider.Factory() { + @Override + public T create(Class modelClass) { + return (T) new UserViewModel(repository); + } + }).get(UserViewModel.class); + } + + @Override + protected FragmentUserDetailBinding createViewBinding(LayoutInflater inflater, ViewGroup container) { + return FragmentUserDetailBinding.inflate(inflater, container, false); + } + + @Override + protected void initView() { + binding.tvTitle.setText("用户详情"); + } + + @Override + protected void observeViewModel() { + observeCommonState(); + + viewModel.getUser().observe(getViewLifecycleOwner(), user -> { + if (user != null) { + binding.tvEmail.setText(user.getEmail()); + } + }); + } +} +``` + +### 4.5 具体 Activity(使用 BaseAndroidViewModel) + +```java +public class SettingsActivity extends BaseMvvmActivity { + + @Override + protected SettingsViewModel createViewModel() { + SettingsRepository repository = new SettingsRepository(); + // BaseAndroidViewModel 需要传入 Application + return new ViewModelProvider(this, new ViewModelProvider.Factory() { + @Override + public T create(Class modelClass) { + return (T) new SettingsViewModel(getApplication(), repository); + } + }).get(SettingsViewModel.class); + } + + @Override + protected ActivitySettingsBinding createViewBinding() { + return ActivitySettingsBinding.inflate(getLayoutInflater()); + } + + @Override + protected void initView() { + binding.switchDarkMode.setOnCheckedChangeListener((buttonView, isChecked) -> { + viewModel.saveDarkMode(isChecked); + }); + } + + @Override + protected void observeViewModel() { + observeCommonState(); + + viewModel.getAppVersion().observe(this, version -> { + binding.tvVersion.setText("版本:" + version); + }); + + viewModel.getDarkMode().observe(this, enabled -> { + binding.switchDarkMode.setChecked(enabled != null && enabled); + }); + } + + @Override + protected void onLoadData() { + viewModel.loadSettings(); + } +} +``` + +--- + +## 五、禁止规范(AI Agent 必须遵守) + +### 5.1 ❌ ViewModel 层禁止 + +| 禁止行为 | 原因 | +|----------|------| +| 持有 Activity / Fragment / View 引用 | 导致内存泄漏,ViewModel 生命周期长于 View | +| 在 BaseViewModel 中持有任何 Context 引用 | BaseViewModel 不应依赖 Context,如需 Context 应改用 BaseAndroidViewModel | +| 在 BaseAndroidViewModel 中使用 Activity Context | 只能使用 Application Context(通过 getAppContext()),Activity Context 会泄漏 | +| 直接进行网络请求(如 new OkHttpCall()) | 应委托给 Repository | +| 直接操作数据库(如 Room DAO 调用) | 应委托给 Repository | +| 弹 Toast / Dialog / Snackbar | ViewModel 不应感知 UI 组件 | +| 手动创建 Thread / Executor / AsyncTask | 必须使用 RxJava + CompositeDisposable | +| 发起 RxJava 请求后不 add 到 disposables | 导致内存泄漏,ViewModel 销毁后任务仍在执行 | +| 在 onCleared 之外手动调用 `disposables.clear()` | 应由基类统一管理,子类不应干预销毁时机 | +| 暴露 MutableLiveData 给外部 | 应只暴露 LiveData(不可变) | +| 在 ViewModel 中写死 Context 相关资源(R.string.xxx) | 降低可测试性,应通过 Repository 或资源 ID 传递 | +| 创建独立的 Disposable 而不加入 CompositeDisposable | 必须统一通过 `addDisposable()` 管理 | + +### 5.2 ❌ View 层(Activity / Fragment)禁止 + +| 禁止行为 | 原因 | +|----------|------| +| 直接调用 Retrofit / OkHttp / Room | 破坏分层架构 | +| 持有 MutableLiveData 引用并直接 setValue | 破坏单向数据流 | +| 在 Activity/Fragment 中写业务逻辑 | 降低可维护性和可测试性 | +| 使用 findViewById 替代 ViewBinding | 低效且易出错 | +| 在 onDestroyView 之后访问 binding | 导致空指针 | +| 使用静态变量存储 View / Context | 严重内存泄漏 | +| 在 Fragment 中通过 getActivity() 强转调用 Activity 方法 | 应使用 ViewModel 或接口回调 | + +### 5.3 ❌ 数据层禁止 + +| 禁止行为 | 原因 | +|----------|------| +| Repository 直接持有 ViewModel 引用 | 数据层不应感知 UI 层 | +| Model 中混杂业务逻辑 | Model 只做数据定义和存储 | +| 在 Repository 中弹 Toast / Dialog | 数据层不应感知 UI | + +--- + +## 六、代码生成检查清单 + +AI Agent 在生成 MVVM 相关代码时,必须逐项确认: + +- [ ] ViewModel 是否继承 `BaseViewModel` 或 `BaseAndroidViewModel`? +- [ ] 需要 Context 的 ViewModel 是否改为继承 `BaseAndroidViewModel`(而非在 BaseViewModel 中持有 Context)? +- [ ] Activity 是否继承 `BaseMvvmActivity`? +- [ ] Fragment 是否继承 `BaseMvvmFragment`? +- [ ] ViewModel 中是否没有任何 View / Activity Context 引用? +- [ ] BaseAndroidViewModel 中是否只通过 `getAppContext()` 获取 Application Context? +- [ ] 所有数据获取是否通过 Repository? +- [ ] View 层是否只通过 LiveData 观察数据? +- [ ] View 层是否只调用 ViewModel 方法,不直接操作数据? +- [ ] MutableLiveData 是否声明为 `private`,对外只暴露 `LiveData`? +- [ ] Fragment 的 `onDestroyView` 中是否置空了 binding? +- [ ] 是否没有使用 `findViewById`? +- [ ] 所有 RxJava 订阅是否通过 `addDisposable()` 添加到 `disposables` 中? +- [ ] ViewModel 中是否没有手动创建 Thread / Executor / AsyncTask? +- [ ] Repository 方法返回类型是否为 RxJava 类型(Single / Observable / Completable / Maybe)? + +--- + +## 七、目录结构建议 + +``` +app/src/main/java/com/example/ +├── base/ +│ ├── BaseViewModel.java (无需 Context 时继承) +│ ├── BaseAndroidViewModel.java (需要 Context 时继承) +│ ├── BaseMvvmActivity.java +│ └── BaseMvvmFragment.java +├── model/ +│ ├── User.java +│ └── ApiResponse.java +├── repository/ +│ ├── UserRepository.java +│ ├── SettingsRepository.java +│ └── RemoteDataSource.java +├── viewmodel/ +│ ├── UserViewModel.java (extends BaseViewModel) +│ ├── SettingsViewModel.java (extends BaseAndroidViewModel) +│ └── HomeViewModel.java +├── view/ +│ ├── activity/ +│ │ ├── UserProfileActivity.java +│ │ └── SettingsActivity.java +│ └── fragment/ +│ └── UserDetailFragment.java +└── network/ + ├── ApiService.java + └── RetrofitClient.java +``` + +--- + +## 八、所需依赖(build.gradle) + +```groovy +dependencies { + // Jetpack ViewModel & LiveData + implementation 'androidx.lifecycle:lifecycle-viewmodel:2.6.2' + implementation 'androidx.lifecycle:lifecycle-livedata:2.6.2' + + // RxJava 3 + implementation 'io.reactivex.rxjava3:rxjava:3.1.8' + implementation 'io.reactivex.rxjava3:rxandroid:3.0.2' + + // Retrofit + RxJava adapter(如网络层使用 Retrofit) + implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0' + + // ViewBinding(在 android {} 块中启用) + // buildFeatures { viewBinding true } +} +``` + +--- + +> **最后更新:** 2026-08-21 +> **维护者:** Android Team +> **适用版本:** Android SDK 21+ / Jetpack ViewModel 2.5+ / RxJava 3.x / ViewBinding enabled diff --git a/app/src/main/java/com/ttstd/dialer/activity/alarm/add/AlarmAddActivity.java b/app/src/main/java/com/ttstd/dialer/activity/alarm/add/AlarmAddActivity.java index 6f15a98..dcbf083 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/alarm/add/AlarmAddActivity.java +++ b/app/src/main/java/com/ttstd/dialer/activity/alarm/add/AlarmAddActivity.java @@ -45,8 +45,8 @@ public class AlarmAddActivity extends BaseMvvmActivity { + protected void observeViewModel() { + mViewModel.getSaveResult().observe(this, success -> { if (success) { TipDialog.show("添加成功", WaitDialog.TYPE.SUCCESS); finish(); diff --git a/app/src/main/java/com/ttstd/dialer/activity/alarm/add/AlarmAddViewModel.java b/app/src/main/java/com/ttstd/dialer/activity/alarm/add/AlarmAddViewModel.java index dad773b..7d26914 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/alarm/add/AlarmAddViewModel.java +++ b/app/src/main/java/com/ttstd/dialer/activity/alarm/add/AlarmAddViewModel.java @@ -1,40 +1,38 @@ package com.ttstd.dialer.activity.alarm.add; -import android.content.Context; +import android.app.Application; +import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; -import com.trello.rxlifecycle4.RxLifecycle; -import com.trello.rxlifecycle4.android.ActivityEvent; import com.ttstd.dialer.alarmclock.AlarmManagerHelper; -import com.ttstd.dialer.base.mvvm.BaseViewModel; -import com.ttstd.dialer.databinding.ActivityAlarmAddBinding; +import com.ttstd.dialer.base.mvvm.BaseAndroidViewModel; import com.ttstd.dialer.db.alarm.AlarmInfo; import com.ttstd.dialer.db.alarm.AlarmRepository; import com.ttstd.dialer.utils.Logger; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.annotations.NonNull; import io.reactivex.rxjava3.core.Completable; import io.reactivex.rxjava3.core.Single; -import io.reactivex.rxjava3.core.SingleObserver; -import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.schedulers.Schedulers; -public class AlarmAddViewModel extends BaseViewModel { +public class AlarmAddViewModel extends BaseAndroidViewModel { private static final String TAG = "AlarmAddViewModel"; - private AlarmRepository mRepository; + private final AlarmRepository mRepository; private int mVolume = 80; private int mSnoozeInterval = 5; - @Override - public void setContext(Context context) { - super.setContext(context); - mRepository = new AlarmRepository(context); + private final MutableLiveData mSaveResult = new MutableLiveData<>(); + + public AlarmAddViewModel(Application application) { + super(application); + mRepository = new AlarmRepository(application); } - public MutableLiveData mSaveResult = new MutableLiveData<>(); + public LiveData getSaveResult() { + return mSaveResult; + } public void saveAlarm(int hour, int minute, String label, boolean vibration, int repeatType) { AlarmInfo alarmInfo = new AlarmInfo(); @@ -47,43 +45,31 @@ public class AlarmAddViewModel extends BaseViewModel mRepository.insertAlarm(alarmInfo)) + addDisposable(Single.fromCallable(() -> mRepository.insertAlarm(alarmInfo)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY)) - .subscribe(new SingleObserver() { - @Override - public void onSubscribe(@NonNull Disposable d) { - Logger.d(TAG, "开始保存闹钟"); - } + .subscribe(aLong -> { + Logger.d(TAG, "闹钟保存成功,ID: " + aLong); + alarmInfo.setId(aLong.intValue()); - @Override - public void onSuccess(Long aLong) { - Logger.d(TAG, "闹钟保存成功,ID: " + aLong); - alarmInfo.setId(aLong.intValue()); + // 计算并设置系统闹钟 + long nextTime = AlarmManagerHelper.calculateNextTime(alarmInfo); + alarmInfo.setNextTriggerTime(nextTime); - // 计算并设置系统闹钟 - long nextTime = AlarmManagerHelper.calculateNextTime(alarmInfo); - alarmInfo.setNextTriggerTime(nextTime); - - // 更新数据库中的下次触发时间 - Completable.fromAction(() -> mRepository.updateNextTriggerTime(alarmInfo.getId(), nextTime)) - .subscribeOn(Schedulers.io()) - .subscribe(() -> { - AlarmManagerHelper.setExactAlarm(getSafeContext(), alarmInfo.getId(), nextTime); - mSaveResult.postValue(true); - }, throwable -> { - Logger.e(TAG, "更新下次触发时间失败: " + throwable.getMessage()); - mSaveResult.postValue(true); // 依然认为保存成功,只是系统闹钟可能没设好 - }); - } - - @Override - public void onError(@NonNull Throwable e) { - Logger.e(TAG, "闹钟保存失败: " + e.getMessage()); - mSaveResult.setValue(false); - } - - }); + // 更新数据库中的下次触发时间 + addDisposable(Completable.fromAction(() -> mRepository.updateNextTriggerTime(alarmInfo.getId(), nextTime)) + .subscribeOn(Schedulers.io()) + .subscribe(() -> { + AlarmManagerHelper.setExactAlarm(getAppContext(), alarmInfo.getId(), nextTime); + mSaveResult.postValue(true); + }, throwable -> { + Logger.e(TAG, "更新下次触发时间失败: " + throwable.getMessage()); + mSaveResult.postValue(true); // 依然认为保存成功,只是系统闹钟可能没设好 + })); + }, + throwable -> { + Logger.e(TAG, "闹钟保存失败: " + throwable.getMessage()); + mSaveResult.setValue(false); + })); } } diff --git a/app/src/main/java/com/ttstd/dialer/activity/alarm/edit/AlarmEditActivity.java b/app/src/main/java/com/ttstd/dialer/activity/alarm/edit/AlarmEditActivity.java index 52f798b..4d767c7 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/alarm/edit/AlarmEditActivity.java +++ b/app/src/main/java/com/ttstd/dialer/activity/alarm/edit/AlarmEditActivity.java @@ -51,8 +51,8 @@ public class AlarmEditActivity extends BaseMvvmActivity { + protected void observeViewModel() { + mViewModel.getUpdateResult().observe(this, success -> { if (success) { TipDialog.show("保存成功", WaitDialog.TYPE.SUCCESS); finish(); @@ -61,7 +61,7 @@ public class AlarmEditActivity extends BaseMvvmActivity { + mViewModel.getDeleteResult().observe(this, success -> { if (success) { TipDialog.show("删除成功", WaitDialog.TYPE.SUCCESS); finish(); diff --git a/app/src/main/java/com/ttstd/dialer/activity/alarm/edit/AlarmEditViewModel.java b/app/src/main/java/com/ttstd/dialer/activity/alarm/edit/AlarmEditViewModel.java index ad0a9ed..745036f 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/alarm/edit/AlarmEditViewModel.java +++ b/app/src/main/java/com/ttstd/dialer/activity/alarm/edit/AlarmEditViewModel.java @@ -1,40 +1,42 @@ package com.ttstd.dialer.activity.alarm.edit; -import android.content.Context; +import android.app.Application; +import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; -import com.trello.rxlifecycle4.RxLifecycle; -import com.trello.rxlifecycle4.android.ActivityEvent; import com.ttstd.dialer.alarmclock.AlarmManagerHelper; -import com.ttstd.dialer.base.mvvm.BaseViewModel; -import com.ttstd.dialer.databinding.ActivityAlarmEditBinding; +import com.ttstd.dialer.base.mvvm.BaseAndroidViewModel; import com.ttstd.dialer.db.alarm.AlarmInfo; import com.ttstd.dialer.db.alarm.AlarmRepository; import com.ttstd.dialer.utils.Logger; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.annotations.NonNull; import io.reactivex.rxjava3.core.Completable; -import io.reactivex.rxjava3.core.CompletableObserver; -import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.schedulers.Schedulers; -public class AlarmEditViewModel extends BaseViewModel { +public class AlarmEditViewModel extends BaseAndroidViewModel { private static final String TAG = "AlarmEditViewModel"; - private AlarmRepository mRepository; + private final AlarmRepository mRepository; - @Override - public void setContext(Context context) { - super.setContext(context); - mRepository = new AlarmRepository(context); - } - - public MutableLiveData mUpdateResult = new MutableLiveData<>(); - public MutableLiveData mDeleteResult = new MutableLiveData<>(); + private final MutableLiveData mUpdateResult = new MutableLiveData<>(); + private final MutableLiveData mDeleteResult = new MutableLiveData<>(); private AlarmInfo mOriginalAlarm; + public AlarmEditViewModel(Application application) { + super(application); + mRepository = new AlarmRepository(application); + } + + public LiveData getUpdateResult() { + return mUpdateResult; + } + + public LiveData getDeleteResult() { + return mDeleteResult; + } + public void loadAlarmInfo(AlarmInfo alarmInfo) { this.mOriginalAlarm = alarmInfo; } @@ -57,58 +59,36 @@ public class AlarmEditViewModel extends BaseViewModel mRepository.updateAlarm(alarmInfo)) + addDisposable(Completable.fromAction(() -> mRepository.updateAlarm(alarmInfo)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY)) - .subscribe(new CompletableObserver() { - @Override - public void onSubscribe(@NonNull Disposable d) { - Logger.d(TAG, "开始更新闹钟"); - } - - @Override - public void onError(@NonNull Throwable e) { - Logger.e(TAG, "闹钟更新失败: " + e.getMessage()); - mUpdateResult.setValue(false); - } - - @Override - public void onComplete() { - Logger.d(TAG, "更新闹钟完成"); - if (alarmInfo.isEnabled()) { - AlarmManagerHelper.setExactAlarm(getSafeContext(), alarmInfo.getId(), alarmInfo.getNextTriggerTime()); - } else { - AlarmManagerHelper.cancelAlarm(getSafeContext(), alarmInfo.getId()); - } - mUpdateResult.setValue(true); - } - }); + .subscribe(() -> { + Logger.d(TAG, "更新闹钟完成"); + if (alarmInfo.isEnabled()) { + AlarmManagerHelper.setExactAlarm(getAppContext(), alarmInfo.getId(), alarmInfo.getNextTriggerTime()); + } else { + AlarmManagerHelper.cancelAlarm(getAppContext(), alarmInfo.getId()); + } + mUpdateResult.setValue(true); + }, + throwable -> { + Logger.e(TAG, "闹钟更新失败: " + throwable.getMessage()); + mUpdateResult.setValue(false); + })); } public void deleteAlarm(AlarmInfo alarmInfo) { - Completable.fromAction(() -> mRepository.deleteAlarm(alarmInfo)) + addDisposable(Completable.fromAction(() -> mRepository.deleteAlarm(alarmInfo)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY)) - .subscribe(new CompletableObserver() { - @Override - public void onSubscribe(@NonNull Disposable d) { - Logger.d(TAG, "开始删除闹钟"); - } - - @Override - public void onError(@NonNull Throwable e) { - Logger.e(TAG, "闹钟删除失败: " + e.getMessage()); - mDeleteResult.setValue(false); - } - - @Override - public void onComplete() { - Logger.d(TAG, "删除闹钟完成"); - AlarmManagerHelper.cancelAlarm(getSafeContext(), alarmInfo.getId()); - mDeleteResult.setValue(true); - } - }); + .subscribe(() -> { + Logger.d(TAG, "删除闹钟完成"); + AlarmManagerHelper.cancelAlarm(getAppContext(), alarmInfo.getId()); + mDeleteResult.setValue(true); + }, + throwable -> { + Logger.e(TAG, "闹钟删除失败: " + throwable.getMessage()); + mDeleteResult.setValue(false); + })); } } diff --git a/app/src/main/java/com/ttstd/dialer/activity/alarm/list/AlarmListActivity.java b/app/src/main/java/com/ttstd/dialer/activity/alarm/list/AlarmListActivity.java index 37a8dc1..0bccc17 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/alarm/list/AlarmListActivity.java +++ b/app/src/main/java/com/ttstd/dialer/activity/alarm/list/AlarmListActivity.java @@ -109,8 +109,8 @@ public class AlarmListActivity extends BaseMvvmActivity>() { + protected void observeViewModel() { + mViewModel.getAlarmListData().observe(this, new Observer>() { @Override public void onChanged(List alarmInfos) { mViewDataBinding.swipeRefreshLayout.setRefreshing(false); @@ -121,7 +121,7 @@ public class AlarmListActivity extends BaseMvvmActivity() { + mViewModel.getDeleteLiveData().observe(this, new Observer() { @Override public void onChanged(Boolean success) { if (success) { diff --git a/app/src/main/java/com/ttstd/dialer/activity/alarm/list/AlarmListViewModel.java b/app/src/main/java/com/ttstd/dialer/activity/alarm/list/AlarmListViewModel.java index c05bdef..d3cf4ed 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/alarm/list/AlarmListViewModel.java +++ b/app/src/main/java/com/ttstd/dialer/activity/alarm/list/AlarmListViewModel.java @@ -1,14 +1,12 @@ package com.ttstd.dialer.activity.alarm.list; -import android.content.Context; +import android.app.Application; +import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; -import com.trello.rxlifecycle4.RxLifecycle; -import com.trello.rxlifecycle4.android.ActivityEvent; import com.ttstd.dialer.alarmclock.AlarmManagerHelper; -import com.ttstd.dialer.base.mvvm.BaseViewModel; -import com.ttstd.dialer.databinding.ActivityAlarmListBinding; +import com.ttstd.dialer.base.mvvm.BaseAndroidViewModel; import com.ttstd.dialer.db.alarm.AlarmInfo; import com.ttstd.dialer.db.alarm.AlarmRepository; import com.ttstd.dialer.utils.Logger; @@ -16,111 +14,76 @@ import com.ttstd.dialer.utils.Logger; import java.util.List; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.annotations.NonNull; import io.reactivex.rxjava3.core.Completable; -import io.reactivex.rxjava3.core.CompletableObserver; import io.reactivex.rxjava3.core.Single; -import io.reactivex.rxjava3.core.SingleObserver; -import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.schedulers.Schedulers; -public class AlarmListViewModel extends BaseViewModel { +public class AlarmListViewModel extends BaseAndroidViewModel { private static final String TAG = "AlarmListViewModel"; - private AlarmRepository mRepository; + private final AlarmRepository mRepository; - @Override - public void setContext(Context context) { - super.setContext(context); - mRepository = new AlarmRepository(context); + private final MutableLiveData> mAlarmListData = new MutableLiveData<>(); + private final MutableLiveData mDeleteLiveData = new MutableLiveData<>(); + + public AlarmListViewModel(Application application) { + super(application); + mRepository = new AlarmRepository(application); } - public MutableLiveData> mAlarmListData = new MutableLiveData<>(); + public LiveData> getAlarmListData() { + return mAlarmListData; + } + + public LiveData getDeleteLiveData() { + return mDeleteLiveData; + } public void getAllAlarms() { - Single.fromCallable(() -> mRepository.getAllAlarms()) + addDisposable(Single.fromCallable(() -> mRepository.getAllAlarms()) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY)) - .subscribe(new SingleObserver>() { - @Override - public void onSubscribe(@NonNull Disposable d) { - Logger.d(TAG, "开始加载闹钟列表"); - } - - @Override - public void onSuccess(List alarmInfos) { - Logger.d(TAG, "加载闹钟列表成功,数量: " + alarmInfos.size()); - mAlarmListData.setValue(alarmInfos); - } - - @Override - public void onError(@NonNull Throwable e) { - Logger.e(TAG, "加载闹钟列表失败: " + e.getMessage()); - mAlarmListData.setValue(null); - } - }); + .subscribe(alarmInfos -> { + Logger.d(TAG, "加载闹钟列表成功,数量: " + alarmInfos.size()); + mAlarmListData.setValue(alarmInfos); + }, + throwable -> { + Logger.e(TAG, "加载闹钟列表失败: " + throwable.getMessage()); + mAlarmListData.setValue(null); + })); } public void updateAlarmEnabled(AlarmInfo alarmInfo) { - Completable.fromAction(() -> { + addDisposable(Completable.fromAction(() -> { mRepository.updateAlarmEnabled(alarmInfo.getId(), alarmInfo.isEnabled()); if (alarmInfo.isEnabled()) { long nextTime = AlarmManagerHelper.calculateNextTime(alarmInfo); alarmInfo.setNextTriggerTime(nextTime); mRepository.updateNextTriggerTime(alarmInfo.getId(), nextTime); - AlarmManagerHelper.setExactAlarm(getSafeContext(), alarmInfo.getId(), nextTime); + AlarmManagerHelper.setExactAlarm(getAppContext(), alarmInfo.getId(), nextTime); Logger.d(TAG, "updateAlarmEnabled: 闹钟状态开启"); } else { - AlarmManagerHelper.cancelAlarm(getSafeContext(), alarmInfo.getId()); + AlarmManagerHelper.cancelAlarm(getAppContext(), alarmInfo.getId()); Logger.d(TAG, "updateAlarmEnabled: 闹钟状态关闭"); - } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY)) - .subscribe(new CompletableObserver() { - @Override - public void onSubscribe(@NonNull Disposable d) { - } - - @Override - public void onComplete() { - Logger.d(TAG, "updateAlarmEnabled: 更新闹钟状态成功"); - } - - @Override - public void onError(@NonNull Throwable e) { - Logger.e(TAG, "updateAlarmEnabled: 更新闹钟状态失败: " + e.getMessage()); - } - }); + .subscribe(() -> Logger.d(TAG, "updateAlarmEnabled: 更新闹钟状态成功"), + throwable -> Logger.e(TAG, "updateAlarmEnabled: 更新闹钟状态失败: " + throwable.getMessage()))); } - public MutableLiveData mDeleteLiveData = new MutableLiveData<>(); - public void deleteAlarm(AlarmInfo alarmInfo) { - Completable.fromAction(() -> mRepository.deleteAlarm(alarmInfo)) + addDisposable(Completable.fromAction(() -> mRepository.deleteAlarm(alarmInfo)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY)) - .subscribe(new CompletableObserver() { - @Override - public void onSubscribe(@NonNull Disposable d) { - Logger.d(TAG, "开始删除闹钟"); - } - - @Override - public void onError(@NonNull Throwable e) { - Logger.e(TAG, "删除闹钟失败: " + e.getMessage()); - mDeleteLiveData.setValue(false); - } - - @Override - public void onComplete() { - Logger.d(TAG, "删除闹钟完成"); - AlarmManagerHelper.cancelAlarm(getSafeContext(), alarmInfo.getId()); - mDeleteLiveData.setValue(true); - } - }); + .subscribe(() -> { + Logger.d(TAG, "删除闹钟完成"); + AlarmManagerHelper.cancelAlarm(getAppContext(), alarmInfo.getId()); + mDeleteLiveData.setValue(true); + }, + throwable -> { + Logger.e(TAG, "删除闹钟失败: " + throwable.getMessage()); + mDeleteLiveData.setValue(false); + })); } } diff --git a/app/src/main/java/com/ttstd/dialer/activity/app/AppListActivity.java b/app/src/main/java/com/ttstd/dialer/activity/app/AppListActivity.java index b383326..b373ffb 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/app/AppListActivity.java +++ b/app/src/main/java/com/ttstd/dialer/activity/app/AppListActivity.java @@ -159,13 +159,13 @@ public class AppListActivity extends BaseMvvmActivityon(LiveDataAction.ACTION_UPDATE_APPS) .observe(this, event -> { mViewModel.getDbAppList(); mViewModel.getHotseatApp(); }); - mViewModel.mHotseatAppData.observe(this, new Observer>() { + mViewModel.getHotseatAppData().observe(this, new Observer>() { @Override public void onChanged(List appInfos) { if (appInfos != null && !appInfos.isEmpty()) { @@ -177,18 +177,15 @@ public class AppListActivity extends BaseMvvmActivity>() { + mViewModel.getDesktopSortAppData().observe(this, new Observer>() { @Override public void onChanged(List appInfos) { mMoreAppAdapter.setAppInfos(appInfos); } }); - mViewModel.getDbAppList(); - - mViewModel.mAppUpdateData.observe(this, new Observer() { + mViewModel.getAppUpdateData().observe(this, new Observer() { @Override public void onChanged(Integer integer) { if (integer > 0) { @@ -198,7 +195,12 @@ public class AppListActivity extends BaseMvvmActivity { +public class AppListViewModel extends BaseAndroidViewModel { private static final String TAG = "AppListViewModel"; - private AppRepository mAppRepository; + private final AppRepository mAppRepository; - @Override - public void setContext(Context context) { - super.setContext(context); - mAppRepository = new AppRepository(context); + private final MutableLiveData> mAllAppData = new MutableLiveData<>(); + private final MutableLiveData> mHotseatAppData = new MutableLiveData<>(); + private final MutableLiveData> mDesktopSortAppData = new MutableLiveData<>(); + private final MutableLiveData mAppUpdateData = new MutableLiveData<>(); + + public AppListViewModel(Application application) { + super(application); + mAppRepository = new AppRepository(application); } - public MutableLiveData> mAllAppData = new MutableLiveData<>(); + public LiveData> getAllAppData() { + return mAllAppData; + } + + public LiveData> getHotseatAppData() { + return mHotseatAppData; + } + + public LiveData> getDesktopSortAppData() { + return mDesktopSortAppData; + } + + public LiveData getAppUpdateData() { + return mAppUpdateData; + } public void getAllApp() { - Observable.fromCallable(new Callable>() { + addDisposable(Observable.fromCallable(new Callable>() { @Override public List call() throws Exception { return mAppRepository.getAllApp(); } - }).compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY)) + }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Observer>() { - @Override - public void onSubscribe(@NonNull Disposable d) { - Logger.e("getAllApp", "onSubscribe: "); - } - - @Override - public void onNext(@NonNull List appInfos) { - Logger.e("getAllApp", "onNext: " + appInfos); - mAllAppData.setValue(appInfos); - } - - @Override - public void onError(@NonNull Throwable e) { - Logger.e("getAllApp", "onError: " + e.getMessage()); - } - - @Override - public void onComplete() { - Logger.e("getAllApp", "onComplete: "); - } - }); + .subscribe(appInfos -> { + Logger.e("getAllApp", "onNext: " + appInfos); + mAllAppData.setValue(appInfos); + }, + throwable -> Logger.e("getAllApp", "onError: " + throwable.getMessage()))); } - public MutableLiveData> mHotseatAppData = new MutableLiveData<>(); - public void getHotseatApp() { - Observable.fromCallable(new Callable>() { + addDisposable(Observable.fromCallable(new Callable>() { @Override public List call() throws Exception { return mAppRepository.getHotseatApp(); } - }).compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY)) + }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Observer>() { - @Override - public void onSubscribe(@NonNull Disposable d) { - Logger.e("getHotseatApp", "onSubscribe: "); - } - - @Override - public void onNext(@NonNull List appInfos) { - Logger.e("getHotseatApp", "onNext: " + appInfos); - mHotseatAppData.setValue(appInfos); - } - - @Override - public void onError(@NonNull Throwable e) { - Logger.e("getHotseatApp", "onError: " + e.getMessage()); - } - - @Override - public void onComplete() { - Logger.e("getHotseatApp", "onComplete: "); - } - }); + .subscribe(appInfos -> { + Logger.e("getHotseatApp", "onNext: " + appInfos); + mHotseatAppData.setValue(appInfos); + }, + throwable -> Logger.e("getHotseatApp", "onError: " + throwable.getMessage()))); } - public MutableLiveData> mDesktopSortAppData = new MutableLiveData<>(); - public void getDbAppList() { - Observable.fromCallable(new Callable>() { + addDisposable(Observable.fromCallable(new Callable>() { @Override public List call() throws Exception { return mAppRepository.getInsideApp(); } - }).compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY)) + }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Observer>() { - @Override - public void onSubscribe(@NonNull Disposable d) { - Logger.e("getDbAppList", "onSubscribe: "); - } - - @Override - public void onNext(@NonNull List appInfos) { - Logger.e("getDbAppList", "onNext: " + appInfos); - mDesktopSortAppData.setValue(appInfos); - } - - @Override - public void onError(@NonNull Throwable e) { - Logger.e("getDbAppList", "onError: " + e.getMessage()); - } - - @Override - public void onComplete() { - Logger.e("getDbAppList", "onComplete: "); - } - }); + .subscribe(appInfos -> { + Logger.e("getDbAppList", "onNext: " + appInfos); + mDesktopSortAppData.setValue(appInfos); + }, + throwable -> Logger.e("getDbAppList", "onError: " + throwable.getMessage()))); } - public MutableLiveData mAppUpdateData = new MutableLiveData<>(); - public void updateAppInfo(AppInfo appInfo) { - Observable.fromCallable(new Callable() { + addDisposable(Observable.fromCallable(new Callable() { @Override public Integer call() throws Exception { if (appInfo.getOutside() == 0) { @@ -166,32 +122,15 @@ public class AppListViewModel extends BaseViewModel() { - @Override - public void onSubscribe(@NonNull Disposable d) { - Logger.e("updateAppInfo", "onSubscribe: "); - } - - @Override - public void onNext(@NonNull Integer row) { - Logger.e("updateAppInfo", "onNext: " + row); - mAppUpdateData.setValue(row); - AppManager.getInstance().reorderAppPositions(); - } - - @Override - public void onError(@NonNull Throwable e) { - Logger.e("updateAppInfo", "onError: " + e.getMessage()); - } - - @Override - public void onComplete() { - Logger.e("updateAppInfo", "onComplete: "); - } - }); + .subscribe(row -> { + Logger.e("updateAppInfo", "onNext: " + row); + mAppUpdateData.setValue(row); + AppManager.getInstance().reorderAppPositions(); + }, + throwable -> Logger.e("updateAppInfo", "onError: " + throwable.getMessage()))); } } diff --git a/app/src/main/java/com/ttstd/dialer/activity/contact/add/ContactAddActivity.java b/app/src/main/java/com/ttstd/dialer/activity/contact/add/ContactAddActivity.java index da2ce88..ac64707 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/contact/add/ContactAddActivity.java +++ b/app/src/main/java/com/ttstd/dialer/activity/contact/add/ContactAddActivity.java @@ -80,8 +80,8 @@ public class ContactAddActivity extends BaseMvvmActivity() { + protected void observeViewModel() { + mViewModel.getOnlineIdData().observe(this, new Observer() { @Override public void onChanged(Long aLong) { if (aLong > 0) { @@ -92,7 +92,7 @@ public class ContactAddActivity extends BaseMvvmActivity() { + mViewModel.getDbIdData().observe(this, new Observer() { @Override public void onChanged(Long aLong) { PopTip.show("添加成功").iconSuccess(); diff --git a/app/src/main/java/com/ttstd/dialer/activity/contact/add/ContactAddViewModel.java b/app/src/main/java/com/ttstd/dialer/activity/contact/add/ContactAddViewModel.java index 4b9b155..3d101ee 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/contact/add/ContactAddViewModel.java +++ b/app/src/main/java/com/ttstd/dialer/activity/contact/add/ContactAddViewModel.java @@ -1,13 +1,12 @@ package com.ttstd.dialer.activity.contact.add; -import android.content.Context; +import android.app.Application; +import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; -import com.trello.rxlifecycle4.android.ActivityEvent; -import com.ttstd.dialer.base.mvvm.BaseViewModel; +import com.ttstd.dialer.base.mvvm.BaseAndroidViewModel; import com.ttstd.dialer.data.repository.ContactDataRepository; -import com.ttstd.dialer.databinding.ActivityContactAddBinding; import com.ttstd.dialer.db.contact.ContactInfo; import com.ttstd.dialer.utils.Logger; @@ -17,22 +16,27 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; * 新增联系人 ViewModel(Google 标准 MVVM): * 网络优先 / 本地降级的策略已下沉到 Repository,ViewModel 只负责状态分发。 */ -public class ContactAddViewModel extends BaseViewModel { +public class ContactAddViewModel extends BaseAndroidViewModel { private static final String TAG = "ContactAddViewModel"; - private ContactDataRepository mRepository; + private final ContactDataRepository mRepository; - /** - * 服务端保存成功返回的 id - */ - public MutableLiveData mOnlineIdData = new MutableLiveData<>(); + /** 服务端保存成功返回的 id */ + private final MutableLiveData mOnlineIdData = new MutableLiveData<>(); /** 网络失败降级到本地 Room 保存返回的 id */ - public MutableLiveData mDbIdData = new MutableLiveData<>(); + private final MutableLiveData mDbIdData = new MutableLiveData<>(); - @Override - public void setContext(Context context) { - super.setContext(context); - mRepository = ContactDataRepository.getInstance(context); + public ContactAddViewModel(Application application) { + super(application); + mRepository = ContactDataRepository.getInstance(application); + } + + public LiveData getOnlineIdData() { + return mOnlineIdData; + } + + public LiveData getDbIdData() { + return mDbIdData; } /** diff --git a/app/src/main/java/com/ttstd/dialer/activity/contact/edit/ContactEditActivity.java b/app/src/main/java/com/ttstd/dialer/activity/contact/edit/ContactEditActivity.java index 9872c92..121d98b 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/contact/edit/ContactEditActivity.java +++ b/app/src/main/java/com/ttstd/dialer/activity/contact/edit/ContactEditActivity.java @@ -35,15 +35,7 @@ public class ContactEditActivity extends BaseMvvmActivity() { + } + + @Override + protected void initView() { + + } + + @Override + protected void observeViewModel() { + mViewModel.getBooleanMutableLiveData().observe(this, new Observer() { @Override public void onChanged(Boolean aBoolean) { if (aBoolean) { diff --git a/app/src/main/java/com/ttstd/dialer/activity/contact/edit/ContactEditViewModel.java b/app/src/main/java/com/ttstd/dialer/activity/contact/edit/ContactEditViewModel.java index 22bc0c4..66d3b91 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/contact/edit/ContactEditViewModel.java +++ b/app/src/main/java/com/ttstd/dialer/activity/contact/edit/ContactEditViewModel.java @@ -1,15 +1,14 @@ package com.ttstd.dialer.activity.contact.edit; -import android.content.Context; +import android.app.Application; +import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import com.kongzue.dialogx.dialogs.PopTip; -import com.trello.rxlifecycle4.android.ActivityEvent; -import com.ttstd.dialer.base.mvvm.BaseViewModel; +import com.ttstd.dialer.base.mvvm.BaseAndroidViewModel; import com.ttstd.dialer.data.ApiException; import com.ttstd.dialer.data.repository.ContactDataRepository; -import com.ttstd.dialer.databinding.ActivityContactEditBinding; import com.ttstd.dialer.db.contact.ContactInfo; import com.ttstd.dialer.utils.Logger; @@ -18,17 +17,20 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; /** * 编辑联系人 ViewModel(Google 标准 MVVM):只依赖 Repository。 */ -public class ContactEditViewModel extends BaseViewModel { +public class ContactEditViewModel extends BaseAndroidViewModel { private static final String TAG = "ContactEditViewModel"; - private ContactDataRepository mRepository; + private final ContactDataRepository mRepository; - public MutableLiveData mBooleanMutableLiveData = new MutableLiveData<>(); + private final MutableLiveData mBooleanMutableLiveData = new MutableLiveData<>(); - @Override - public void setContext(Context context) { - super.setContext(context); - mRepository = ContactDataRepository.getInstance(context); + public ContactEditViewModel(Application application) { + super(application); + mRepository = ContactDataRepository.getInstance(application); + } + + public LiveData getBooleanMutableLiveData() { + return mBooleanMutableLiveData; } /** diff --git a/app/src/main/java/com/ttstd/dialer/activity/contact/list/ContactListActivity.java b/app/src/main/java/com/ttstd/dialer/activity/contact/list/ContactListActivity.java index 44b2576..4fc5659 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/contact/list/ContactListActivity.java +++ b/app/src/main/java/com/ttstd/dialer/activity/contact/list/ContactListActivity.java @@ -159,7 +159,7 @@ public class ContactListActivity extends BaseMvvmActivity>>() { @Override diff --git a/app/src/main/java/com/ttstd/dialer/activity/contact/list/ContactListViewModel.java b/app/src/main/java/com/ttstd/dialer/activity/contact/list/ContactListViewModel.java index f9dc64e..bd45a05 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/contact/list/ContactListViewModel.java +++ b/app/src/main/java/com/ttstd/dialer/activity/contact/list/ContactListViewModel.java @@ -1,15 +1,13 @@ package com.ttstd.dialer.activity.contact.list; -import android.content.Context; +import android.app.Application; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; -import com.trello.rxlifecycle4.android.ActivityEvent; -import com.ttstd.dialer.base.mvvm.BaseViewModel; +import com.ttstd.dialer.base.mvvm.BaseAndroidViewModel; import com.ttstd.dialer.data.Resource; import com.ttstd.dialer.data.repository.ContactDataRepository; -import com.ttstd.dialer.databinding.ActivityContactListBinding; import com.ttstd.dialer.db.contact.ContactInfo; import com.ttstd.dialer.utils.Logger; @@ -21,18 +19,17 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; * 联系人列表 ViewModel(Google 标准 MVVM): * 只依赖 Repository,向 UI 暴露 LiveData<Resource<T>>,不感知网络/缓存/数据库细节。 */ -public class ContactListViewModel extends BaseViewModel { +public class ContactListViewModel extends BaseAndroidViewModel { private static final String TAG = "ContactListViewModel"; - private ContactDataRepository mRepository; + private final ContactDataRepository mRepository; private final MutableLiveData>> mContactList = new MutableLiveData<>(); private final MutableLiveData mDeleteResult = new MutableLiveData<>(); - @Override - public void setContext(Context context) { - super.setContext(context); - mRepository = ContactDataRepository.getInstance(context); + public ContactListViewModel(Application application) { + super(application); + mRepository = ContactDataRepository.getInstance(application); } public LiveData>> getContactListData() { diff --git a/app/src/main/java/com/ttstd/dialer/activity/contact/test/ContactTestActivity.java b/app/src/main/java/com/ttstd/dialer/activity/contact/test/ContactTestActivity.java index ed50718..03f76b2 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/contact/test/ContactTestActivity.java +++ b/app/src/main/java/com/ttstd/dialer/activity/contact/test/ContactTestActivity.java @@ -162,8 +162,8 @@ public class ContactTestActivity extends BaseMvvmActivity>() { + protected void observeViewModel() { + mViewModel.getOnlineContactListData().observe(this, new Observer>() { @Override public void onChanged(List contactInfos) { if (contactInfos == null) { @@ -177,7 +177,7 @@ public class ContactTestActivity extends BaseMvvmActivity>() { + mViewModel.getLocalContactListData().observe(this, new Observer>() { @Override public void onChanged(List contactInfos) { if (contactInfos == null) { diff --git a/app/src/main/java/com/ttstd/dialer/activity/contact/test/ContactTestViewModel.java b/app/src/main/java/com/ttstd/dialer/activity/contact/test/ContactTestViewModel.java index 0a102ed..667b0d1 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/contact/test/ContactTestViewModel.java +++ b/app/src/main/java/com/ttstd/dialer/activity/contact/test/ContactTestViewModel.java @@ -1,13 +1,12 @@ package com.ttstd.dialer.activity.contact.test; -import android.content.Context; +import android.app.Application; +import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; -import com.trello.rxlifecycle4.android.ActivityEvent; -import com.ttstd.dialer.base.mvvm.BaseViewModel; +import com.ttstd.dialer.base.mvvm.BaseAndroidViewModel; import com.ttstd.dialer.data.repository.ContactDataRepository; -import com.ttstd.dialer.databinding.ActivityContactTestBinding; import com.ttstd.dialer.db.contact.ContactInfo; import com.ttstd.dialer.utils.Logger; @@ -18,19 +17,26 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; /** * 联系人测试页 ViewModel(Google 标准 MVVM):只依赖 Repository。 */ -public class ContactTestViewModel extends BaseViewModel { +public class ContactTestViewModel extends BaseAndroidViewModel { private static final String TAG = "ContactTestViewModel"; - private ContactDataRepository mRepository; + private final ContactDataRepository mRepository; - @Override - public void setContext(Context context) { - super.setContext(context); - mRepository = ContactDataRepository.getInstance(context); + private final MutableLiveData> mOnlineContactListData = new MutableLiveData<>(); + private final MutableLiveData> mLocalContactListData = new MutableLiveData<>(); + + public ContactTestViewModel(Application application) { + super(application); + mRepository = ContactDataRepository.getInstance(application); } - public MutableLiveData> mOnlineContactListData = new MutableLiveData<>(); - public MutableLiveData> mLocalContactListData = new MutableLiveData<>(); + public LiveData> getOnlineContactListData() { + return mOnlineContactListData; + } + + public LiveData> getLocalContactListData() { + return mLocalContactListData; + } /** * 网络联系人(走多级缓存 + SWR,数据已按 position 排序) diff --git a/app/src/main/java/com/ttstd/dialer/activity/main/MainActivity.java b/app/src/main/java/com/ttstd/dialer/activity/main/MainActivity.java index 89dabda..277ee44 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/main/MainActivity.java +++ b/app/src/main/java/com/ttstd/dialer/activity/main/MainActivity.java @@ -205,8 +205,39 @@ public class MainActivity extends BaseMvvmActivityon(LiveDataAction.ACTION_UPDATE_APPS) + .observe(this, event -> { + mViewModel.getOutsideApp(); + mViewModel.getHotseatApp(); + }); + + mViewModel.getDesktopSortAppData().observe(this, new Observer>() { + @Override + public void onChanged(List appInfos) { + mAppInfos = appInfos; + Logger.e(TAG, "onChanged: mAppInfos size = " + mAppInfos.size()); + setAppList(); + } + }); + + mViewModel.getHotseatAppData().observe(this, new Observer>() { + @Override + public void onChanged(List appInfos) { + if (appInfos != null && !appInfos.isEmpty()) { + GridLayoutManager layoutManager = (GridLayoutManager) mViewDataBinding.gvApp.getLayoutManager(); + if (layoutManager != null) { + layoutManager.setSpanCount(Math.max(1, appInfos.size())); + } + } + mAppGridAdapter.setAppInfos(appInfos); + } + }); + } + + @Override + protected void onLoadData() { + Log.e(TAG, "onLoadData: "); registerReceivers(); AppManager.getInstance().addProgressCallback(new AppManager.ProgressCallback() { @Override @@ -219,37 +250,8 @@ public class MainActivity extends BaseMvvmActivityon(LiveDataAction.ACTION_UPDATE_APPS) - .observe(this, event -> { - mViewModel.getOutsideApp(); - mViewModel.getHotseatApp(); - }); - - mViewModel.mDesktopSortAppData.observe(this, new Observer>() { - @Override - public void onChanged(List appInfos) { - mAppInfos = appInfos; - Logger.e(TAG, "onChanged: mAppInfos size = " + mAppInfos.size()); - setAppList(); - } - }); mViewModel.getOutsideApp(); - - mViewModel.mHotseatAppData.observe(this, new Observer>() { - @Override - public void onChanged(List appInfos) { - if (appInfos != null && !appInfos.isEmpty()) { - GridLayoutManager layoutManager = (GridLayoutManager) mViewDataBinding.gvApp.getLayoutManager(); - if (layoutManager != null) { - layoutManager.setSpanCount(Math.max(1, appInfos.size())); - } - } - mAppGridAdapter.setAppInfos(appInfos); - } - }); mViewModel.getHotseatApp(); - } @Override diff --git a/app/src/main/java/com/ttstd/dialer/activity/main/MainViewModel.java b/app/src/main/java/com/ttstd/dialer/activity/main/MainViewModel.java index df8c9e7..75346a6 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/main/MainViewModel.java +++ b/app/src/main/java/com/ttstd/dialer/activity/main/MainViewModel.java @@ -1,13 +1,11 @@ package com.ttstd.dialer.activity.main; -import android.content.Context; +import android.app.Application; +import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; -import com.trello.rxlifecycle4.RxLifecycle; -import com.trello.rxlifecycle4.android.ActivityEvent; -import com.ttstd.dialer.base.mvvm.BaseViewModel; -import com.ttstd.dialer.databinding.ActivityMainBinding; +import com.ttstd.dialer.base.mvvm.BaseAndroidViewModel; import com.ttstd.dialer.db.app.AppInfo; import com.ttstd.dialer.db.app.AppRepository; import com.ttstd.dialer.utils.Logger; @@ -16,91 +14,60 @@ import java.util.List; import java.util.concurrent.Callable; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.annotations.NonNull; import io.reactivex.rxjava3.core.Observable; -import io.reactivex.rxjava3.core.Observer; -import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.schedulers.Schedulers; -public class MainViewModel extends BaseViewModel { +public class MainViewModel extends BaseAndroidViewModel { private static final String TAG = "MainViewModel"; - private AppRepository mAppRepository; + private final AppRepository mAppRepository; - @Override - public void setContext(Context context) { - super.setContext(context); - mAppRepository = new AppRepository(context); + private final MutableLiveData> mDesktopSortAppData = new MutableLiveData<>(); + private final MutableLiveData> mHotseatAppData = new MutableLiveData<>(); + + public MainViewModel(Application application) { + super(application); + mAppRepository = new AppRepository(application); } - public MutableLiveData> mDesktopSortAppData = new MutableLiveData<>(); + public LiveData> getDesktopSortAppData() { + return mDesktopSortAppData; + } + + public LiveData> getHotseatAppData() { + return mHotseatAppData; + } public void getOutsideApp() { - Observable.fromCallable(new Callable>() { + addDisposable(Observable.fromCallable(new Callable>() { @Override public List call() throws Exception { return mAppRepository.getOutsideAppWithoutHotseat(); } - }).compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY)) + }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Observer>() { - @Override - public void onSubscribe(@NonNull Disposable d) { - Logger.e("getOutsideApp", "onSubscribe: "); - } - - @Override - public void onNext(@NonNull List appInfos) { - Logger.e("getOutsideApp", "onNext: " + appInfos); - mDesktopSortAppData.setValue(appInfos); - } - - @Override - public void onError(@NonNull Throwable e) { - Logger.e("getOutsideApp", "onError: " + e.getMessage()); - } - - @Override - public void onComplete() { - Logger.e("getOutsideApp", "onComplete: "); - } - }); + .subscribe(appInfos -> { + Logger.e("getOutsideApp", "onNext: " + appInfos); + mDesktopSortAppData.setValue(appInfos); + }, + throwable -> Logger.e("getOutsideApp", "onError: " + throwable.getMessage()))); } - public MutableLiveData> mHotseatAppData = new MutableLiveData<>(); - public void getHotseatApp() { - Observable.fromCallable(new Callable>() { + addDisposable(Observable.fromCallable(new Callable>() { @Override public List call() throws Exception { return mAppRepository.getHotseatApp(); } - }).compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY)) + }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Observer>() { - @Override - public void onSubscribe(@NonNull Disposable d) { - Logger.e("getHotseatApp", "onSubscribe: "); - } - - @Override - public void onNext(@NonNull List appInfos) { - Logger.e("getHotseatApp", "onNext: " + appInfos); - mHotseatAppData.setValue(appInfos); - } - - @Override - public void onError(@NonNull Throwable e) { - Logger.e("getHotseatApp", "onError: " + e.getMessage()); - } - - @Override - public void onComplete() { - Logger.e("getHotseatApp", "onComplete: "); - } - }); + .subscribe(appInfos -> { + Logger.e("getHotseatApp", "onNext: " + appInfos); + mHotseatAppData.setValue(appInfos); + }, + throwable -> Logger.e("getHotseatApp", "onError: " + throwable.getMessage()))); } } diff --git a/app/src/main/java/com/ttstd/dialer/activity/settings/call/SettingsCallActivity.java b/app/src/main/java/com/ttstd/dialer/activity/settings/call/SettingsCallActivity.java index fea813c..b3bde3b 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/settings/call/SettingsCallActivity.java +++ b/app/src/main/java/com/ttstd/dialer/activity/settings/call/SettingsCallActivity.java @@ -60,7 +60,7 @@ public class SettingsCallActivity extends BaseMvvmActivity { +public class SettingsCallViewModel extends BaseViewModel { } diff --git a/app/src/main/java/com/ttstd/dialer/activity/settings/home/SettingsActivity.java b/app/src/main/java/com/ttstd/dialer/activity/settings/home/SettingsActivity.java index 6a7919f..fa59e0c 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/settings/home/SettingsActivity.java +++ b/app/src/main/java/com/ttstd/dialer/activity/settings/home/SettingsActivity.java @@ -44,11 +44,15 @@ public class SettingsActivity extends BaseMvvmActivity { +public class SettingsViewModel extends BaseViewModel { public void snRegister() { - OkHttpManager.getInstance().getSnRegisterObservable(getLifecycle()) - .subscribe(new Observer() { - @Override - public void onSubscribe(@NonNull Disposable d) { - Logger.e("snRegister", "onSubscribe: "); - } - - @Override - public void onNext(@NonNull BaseResponse baseResponse) { - Logger.e("snRegister", "onNext: " + baseResponse); - } - - @Override - public void onError(@NonNull Throwable e) { - Logger.e("snRegister", "onError: " + e.getMessage()); - } - - @Override - public void onComplete() { - Logger.e("snRegister", "onComplete: "); - } - }); - - + addDisposable(OkHttpManager.getInstance().getSnRegisterObservable() + .subscribe(baseResponse -> Logger.e("snRegister", "onNext: " + baseResponse), + throwable -> Logger.e("snRegister", "onError: " + throwable.getMessage()))); } } diff --git a/app/src/main/java/com/ttstd/dialer/activity/settings/utils/SettingsUtilsActivity.java b/app/src/main/java/com/ttstd/dialer/activity/settings/utils/SettingsUtilsActivity.java index c40a2a0..5a4546a 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/settings/utils/SettingsUtilsActivity.java +++ b/app/src/main/java/com/ttstd/dialer/activity/settings/utils/SettingsUtilsActivity.java @@ -140,7 +140,7 @@ public class SettingsUtilsActivity extends BaseMvvmActivity { +public class SettingsUtilsViewModel extends BaseViewModel { } diff --git a/app/src/main/java/com/ttstd/dialer/activity/template/TemplateActivity.java b/app/src/main/java/com/ttstd/dialer/activity/template/TemplateActivity.java index f0fd635..028c075 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/template/TemplateActivity.java +++ b/app/src/main/java/com/ttstd/dialer/activity/template/TemplateActivity.java @@ -31,7 +31,7 @@ public class TemplateActivity extends BaseMvvmActivity { +public class TemplateViewModel extends BaseViewModel { } diff --git a/app/src/main/java/com/ttstd/dialer/activity/weather/main/WeatherMainActivity.java b/app/src/main/java/com/ttstd/dialer/activity/weather/main/WeatherMainActivity.java index cf659c5..c6ac7ff 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/weather/main/WeatherMainActivity.java +++ b/app/src/main/java/com/ttstd/dialer/activity/weather/main/WeatherMainActivity.java @@ -67,23 +67,23 @@ public class WeatherMainActivity extends BaseMvvmActivity() { + mViewModel.getWeatherNowData().observe(this, new Observer() { @Override public void onChanged(WeatherNow weatherNow) { mViewDataBinding.setWeatherNow(weatherNow); } }); - mViewModel.mWeather24hData.observe(this, new Observer>() { + mViewModel.getWeather24hData().observe(this, new Observer>() { @Override public void onChanged(List weatherHourlies) { mHourlyWeatherAdapter.setWeatherHourlyList(weatherHourlies); } }); - mViewModel.mWeatherDailyListData.observe(this, new Observer>() { + mViewModel.getWeatherDailyListData().observe(this, new Observer>() { @Override public void onChanged(List weatherDailies) { Optional dailyOptional = weatherDailies.stream().filter(new Predicate() { @@ -101,7 +101,10 @@ public class WeatherMainActivity extends BaseMvvmActivity { +public class WeatherMainViewModel extends BaseViewModel { private static final String TAG = "WeatherMainViewModel"; - public MutableLiveData mWeatherNowData = new MutableLiveData<>(); + private final MutableLiveData mWeatherNowData = new MutableLiveData<>(); + private final MutableLiveData> mWeather24hData = new MutableLiveData<>(); + private final MutableLiveData> mWeatherDailyListData = new MutableLiveData<>(); + + public LiveData getWeatherNowData() { + return mWeatherNowData; + } + + public LiveData> getWeather24hData() { + return mWeather24hData; + } + + public LiveData> getWeatherDailyListData() { + return mWeatherDailyListData; + } public void getWeatherNow(String adCode) { Logger.e(TAG, "getWeatherNow: " + adCode); @@ -48,8 +61,6 @@ public class WeatherMainViewModel extends BaseViewModel> mWeather24hData = new MutableLiveData<>(); - public void getWeather24h(String adCode) { Logger.e(TAG, "getWeather24h: " + adCode); WeatherManager.getInstance().getWeather24HourAdCode(adCode, new Callback() { @@ -74,9 +85,6 @@ public class WeatherMainViewModel extends BaseViewModel> mWeatherDailyListData = new MutableLiveData<>(); - public void getWeather10D(String adCode) { Logger.e(TAG, "getWeather10D: " + adCode); WeatherManager.getInstance().getWeather10DayAdCode(adCode, new Callback() { diff --git a/app/src/main/java/com/ttstd/dialer/base/BaseFragment.java b/app/src/main/java/com/ttstd/dialer/base/BaseFragment.java index a7ed578..9bb4737 100644 --- a/app/src/main/java/com/ttstd/dialer/base/BaseFragment.java +++ b/app/src/main/java/com/ttstd/dialer/base/BaseFragment.java @@ -1,44 +1,135 @@ package com.ttstd.dialer.base; import android.os.Bundle; +import android.util.Log; + +import androidx.annotation.Nullable; import com.ttstd.dialer.base.rx.BaseRxFragment; +/** + * Fragment 基类,提供标准的懒加载框架 + *

+ * 兼容两种宿主场景: + * 1. ViewPager(v1) + FragmentPagerAdapter:通过 {@link #setUserVisibleHint} 判断可见性(老接口,deprecated) + * 2. ViewPager2 / FragmentTransaction.setMaxLifecycle:通过 onResume/onPause 判断可见性 + *

+ * 懒加载核心思路: + * 1. 视图创建完成({@code isViewInitiated}) + 当前页可见({@code isVisibleToUser}) 两个条件同时满足时, + * 才触发首次加载({@link #onLazyLoad()}),之后切回该页不再重复触发。 + * 2. {@link #fetchData()} 每次可见都会回调,用于需要刷新的场景。 + * 3. View 销毁重建后自动重置懒加载状态,保证重建后的页面能重新渲染。 + */ public abstract class BaseFragment extends BaseRxFragment { + private static final String TAG = "BaseFragment"; + + /** 视图是否已创建(onViewCreated 之后为 true) */ protected boolean isViewInitiated; + /** 当前 Fragment 是否对用户可见 */ protected boolean isVisibleToUser; + /** 首次数据是否已初始化 */ protected boolean isDataInitiated; - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); + public void onViewCreated(@androidx.annotation.NonNull android.view.View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); isViewInitiated = true; + // 视图已创建,检查是否满足懒加载条件 prepareFetchData(); } @Override + public void onResume() { + super.onResume(); + // ViewPager2 / setMaxLifecycle 场景下没有 setUserVisibleHint 回调,用 onResume 判断可见性; + // 兼容 ViewPager(v1):off-screen 页 getUserVisibleHint()==false,不会被误判为可见 + if (!isViewInitiated || isHidden() || !getUserVisibleHint()) { + return; + } + setVisibleToUser(true); + } + + @Override + public void onPause() { + // ViewPager2 / setMaxLifecycle 场景下用 onPause 判断不可见 + if (isViewInitiated && !isHidden() && getUserVisibleHint()) { + setVisibleToUser(false); + } + super.onPause(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + // View 销毁重建后,重置懒加载状态,保证重建后的页面能重新渲染控件 + // 否则 Fragment 实例保留的 isDataInitiated=true 会导致重建后不再触发 onLazyLoad,控件消失 + isDataInitiated = false; + isViewInitiated = false; + isVisibleToUser = false; + } + + /** + * ViewPager(v1) + FragmentPagerAdapter 场景下,通过此回调精确判断当前页是否可见 + */ + @Override + @Deprecated public void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); - this.isVisibleToUser = isVisibleToUser; + if (!isViewInitiated) { + return; + } + setVisibleToUser(isVisibleToUser); + } + + /** + * 统一更新可见性并触发懒加载判断 + */ + private void setVisibleToUser(boolean visible) { + if (this.isVisibleToUser == visible) { + return; + } + this.isVisibleToUser = visible; prepareFetchData(); } + /** + * 触发真实数据加载,子类重写实现具体加载逻辑 + */ public abstract void fetchData(); + /** + * 懒加载入口:视图已创建 且 当前页可见 时触发一次 + */ public boolean prepareFetchData() { return prepareFetchData(false); } + /** + * 懒加载入口 + * + * @param forceUpdate 为 true 时,即使已加载过也强制重新加载 + */ public boolean prepareFetchData(boolean forceUpdate) { if (isVisibleToUser && isViewInitiated && (!isDataInitiated || forceUpdate)) { + if (!isDataInitiated) { + Log.d(TAG, "lazyLoad trigger: " + getClass().getSimpleName()); + onLazyLoad(); + } + // 只在非强制刷新时标记为已初始化,保证首屏只渲染一次 + if (!forceUpdate) { + isDataInitiated = true; + } fetchData(); - //注释掉保证每次都更新数据 -// isDataInitiated = true; return true; } return false; } + /** + * 首次可见时回调,用于做一次性的重操作(如创建控件、加载图片等) + * 默认空实现,子类按需重写 + */ + protected void onLazyLoad() { + } } diff --git a/app/src/main/java/com/ttstd/dialer/base/mvvm/BaseAndroidViewModel.java b/app/src/main/java/com/ttstd/dialer/base/mvvm/BaseAndroidViewModel.java new file mode 100644 index 0000000..c7671e1 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/base/mvvm/BaseAndroidViewModel.java @@ -0,0 +1,135 @@ +package com.ttstd.dialer.base.mvvm; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import io.reactivex.rxjava3.disposables.CompositeDisposable; +import io.reactivex.rxjava3.disposables.Disposable; + +/** + * MVVM ViewModel 基类(带 Context 版本) + * + * 继承 {@link AndroidViewModel},可通过 {@link #getAppContext()} 获取 Application 级别的 Context。 + * Application Context 生命周期与进程一致,不会造成内存泄漏。 + * + * 职责: + * - 持有 UI 状态(LiveData) + * - 处理业务逻辑 + * - 暴露数据给 View 层 + * - 通过 {@link #addDisposable(Disposable)} 统一管理 RxJava 异步任务 + * - 可通过 {@link #getAppContext()} 获取 Application Context(安全) + * + * 禁止: + * - 持有任何 View 引用(Activity / Fragment / View) + * - 使用 Activity Context 或持有 View 的 Context(只能使用 Application) + * - 直接进行网络请求或数据库操作(必须通过 Repository) + * - 在 ViewModel 中弹 Toast / Dialog / Snackbar + * - 手动创建 Thread / Executor / AsyncTask 执行异步任务(必须使用 RxJava + {@link #addDisposable(Disposable)}) + * - 忘记将 Disposable add 到 {@link #mDisposables} 中 + */ +public abstract class BaseAndroidViewModel extends AndroidViewModel implements IMvvmUiState { + + // ============================================================ + // 通用 UI 状态 + // ============================================================ + + /** 全局加载状态 */ + private final MutableLiveData mLoadingLiveData = new MutableLiveData<>(false); + + /** 全局错误提示 */ + private final MutableLiveData mErrorLiveData = new MutableLiveData<>(); + + /** 全局成功提示 */ + private final MutableLiveData mSuccessLiveData = new MutableLiveData<>(); + + // ============================================================ + // RxJava 异步管理(核心) + // ============================================================ + + /** + * CompositeDisposable 用于统一管理所有 RxJava 订阅。 + * 在 onCleared() 中自动 dispose,防止内存泄漏。 + */ + protected final CompositeDisposable mDisposables = new CompositeDisposable(); + + // ============================================================ + // 构造方法 + // ============================================================ + + public BaseAndroidViewModel(@NonNull Application application) { + super(application); + } + + // ============================================================ + // 对外暴露(只读) + // ============================================================ + + public LiveData getLoadingLiveData() { + return mLoadingLiveData; + } + + public LiveData getErrorLiveData() { + return mErrorLiveData; + } + + public LiveData getSuccessLiveData() { + return mSuccessLiveData; + } + + // ============================================================ + // 受保护的方法(子类调用) + // ============================================================ + + protected void setLoading(boolean loading) { + mLoadingLiveData.postValue(loading); + } + + protected void setError(String message) { + mErrorLiveData.postValue(message); + } + + protected void setSuccess(String message) { + mSuccessLiveData.postValue(message); + } + + /** + * 便捷方法:将 Disposable 添加到 {@link #mDisposables} 中管理。 + * 子类在发起 RxJava 请求时调用此方法。 + */ + protected void addDisposable(Disposable disposable) { + mDisposables.add(disposable); + } + + // ============================================================ + // Context 安全访问 + // ============================================================ + + /** + * 获取 Application Context(安全,不会泄漏)。 + * 子类可通过此方法访问: + * - getApplication().getSharedPreferences(...) + * - getApplication().getResources().getString(...) + * - getApplication().getSystemService(...) + */ + @NonNull + protected Application getAppContext() { + return getApplication(); + } + + // ============================================================ + // 生命周期 + // ============================================================ + + @Override + protected void onCleared() { + super.onCleared(); + // ViewModel 销毁时,自动取消所有正在执行的 RxJava 任务 + if (!mDisposables.isDisposed()) { + mDisposables.clear(); + } + } +} diff --git a/app/src/main/java/com/ttstd/dialer/base/mvvm/BaseMvvmActivity.java b/app/src/main/java/com/ttstd/dialer/base/mvvm/BaseMvvmActivity.java index 6b9db5b..b5eb25e 100644 --- a/app/src/main/java/com/ttstd/dialer/base/mvvm/BaseMvvmActivity.java +++ b/app/src/main/java/com/ttstd/dialer/base/mvvm/BaseMvvmActivity.java @@ -1,6 +1,8 @@ package com.ttstd.dialer.base.mvvm; import android.os.Bundle; +import android.os.Handler; +import android.widget.Toast; import androidx.annotation.Nullable; import androidx.databinding.DataBindingUtil; @@ -14,6 +16,33 @@ import com.ttstd.dialer.utils.Logger; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; +/** + * MVVM Activity 基类 + * + * 职责: + * - 绑定 DataBinding + * - 初始化 ViewModel(默认通过反射自动创建) + * - 订阅 ViewModel 中的 LiveData({@link #observeViewModel()}) + * - 订阅全局通用状态({@link #observeCommonState()}) + * - 页面创建完成后加载数据({@link #onLoadData()}) + * - 将用户操作转发给 ViewModel + * + * 生命周期顺序(onCreate): + * 1. 创建 DataBinding + * 2. 创建 ViewModel({@link #createViewModel()}) + * 3. 订阅通用状态({@link #observeCommonState()}) + * 4. 订阅业务状态({@link #observeViewModel()}) + * 5. 初始化界面({@link #initDataBinding()} / {@link #initView()}) + * 6. 加载数据({@link #onLoadData()}) + * + * 禁止: + * - 在 Activity 中直接进行网络请求或数据库操作(必须通过 ViewModel → Repository) + * - 持有可变 LiveData 的引用(应通过 LiveData 回调观察) + * - 在 Activity 中写业务逻辑 + * + * @param ViewModel 类型,必须继承 {@link BaseViewModel} 或 {@link BaseAndroidViewModel} + * @param ViewDataBinding 类型 + */ public abstract class BaseMvvmActivity extends BaseTransparentActivity { private static final String TAG = BaseMvvmActivity.class.getSimpleName(); @@ -22,39 +51,131 @@ public abstract class BaseMvvmActivity vmClass; + // ============================================================ + // 生命周期 + // ============================================================ + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - //ViewDataBinding + + // 1. 创建 DataBinding mViewDataBinding = DataBindingUtil.setContentView(this, getLayoutId()); mViewDataBinding.setLifecycleOwner(this); - //ViewModel - vmClass = (Class) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0]; - boolean isAbstract = Modifier.isAbstract(vmClass.getModifiers()); - Logger.e(TAG, "isLocalClass:" + vmClass.getSimpleName().equals(ViewModel.class.getSimpleName()) + " isAbstract:" + isAbstract); - if (!isAbstract) {//不是一个抽象类 - mViewModel = new ViewModelProvider(this).get(vmClass); - if (mViewModel instanceof BaseViewModel) { - BaseViewModel baseVm = (BaseViewModel) mViewModel; - baseVm.setContext(this); - baseVm.setVDBinding(mViewDataBinding); - baseVm.setLifecycle(getLifecycleSubject()); - } - } + + // 2. 创建 ViewModel + mViewModel = createViewModel(); + + // 3. 订阅通用状态 + observeCommonState(); + + // 4. 订阅业务状态 + observeViewModel(); + + // 5. 初始化界面 initDataBinding(); initView(); - initData(); + + // 6. 加载数据 + onLoadData(); } + // ============================================================ + // 抽象方法(子类必须实现) + // ============================================================ + + /** 创建 ViewModel 实例(默认通过反射自动创建,子类可按需重写) */ + protected VM createViewModel() { + vmClass = (Class) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0]; + boolean isAbstract = Modifier.isAbstract(vmClass.getModifiers()); + if (isAbstract) { + Logger.e(TAG, "ViewModel is abstract: " + vmClass.getSimpleName()); + return null; + } + return new ViewModelProvider(this).get(vmClass); + } + + /** 初始化 DataBinding(如设置 setClick、绑定表达式等) */ protected abstract void initDataBinding(); - /** - * 初始化视图 - */ + /** 初始化视图(如设置 Adapter、Listener 等) */ protected abstract void initView(); + /** 订阅 ViewModel 中的业务 LiveData */ + protected abstract void observeViewModel(); + + // ============================================================ + // 可选重写 + // ============================================================ + /** - * 初始化数据 + * 页面创建完成后加载数据 + * 子类可重写此方法触发首次数据加载 */ - protected abstract void initData(); + protected void onLoadData() { + // 默认空实现 + } + + /** + * 订阅全局通用状态(loading / error / success) + * 子类可在 {@link #observeViewModel()} 中调用 super.observeCommonState(),或直接重写 + */ + protected void observeCommonState() { + if (!(mViewModel instanceof IMvvmUiState)) return; + IMvvmUiState vm = (IMvvmUiState) mViewModel; + + vm.getLoadingLiveData().observe(this, loading -> { + if (loading != null && loading) { + showLoading(); + } else { + hideLoading(); + } + }); + + vm.getErrorLiveData().observe(this, error -> { + if (error != null && !error.isEmpty()) { + showError(error); + } + }); + + vm.getSuccessLiveData().observe(this, msg -> { + if (msg != null && !msg.isEmpty()) { + showSuccess(msg); + } + }); + } + + // ============================================================ + // UI 辅助方法(子类可重写) + // ============================================================ + + protected void showLoading() { + // 默认实现:可替换为自定义 LoadingDialog + // LoadingDialog.show(this); + } + + protected void hideLoading() { + // LoadingDialog.dismiss(); + } + + protected void showError(String message) { + Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); + } + + protected void showSuccess(String message) { + Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); + } + + // ============================================================ + // 禁止使用的模式(编译期约束) + // ============================================================ + + /** + * @deprecated 禁止在 MVVM Activity 中直接创建 Handler 做延时操作 + * → 应使用 ViewModel + LiveData 驱动 + */ + @Deprecated + protected Handler getHandler() { + throw new UnsupportedOperationException("禁止在 MVVM Activity 中使用 Handler"); + } } diff --git a/app/src/main/java/com/ttstd/dialer/base/mvvm/BaseViewModel.java b/app/src/main/java/com/ttstd/dialer/base/mvvm/BaseViewModel.java index 0be84a2..ac7b371 100644 --- a/app/src/main/java/com/ttstd/dialer/base/mvvm/BaseViewModel.java +++ b/app/src/main/java/com/ttstd/dialer/base/mvvm/BaseViewModel.java @@ -1,75 +1,106 @@ package com.ttstd.dialer.base.mvvm; -import android.content.Context; - -import androidx.databinding.ViewDataBinding; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; -import java.lang.ref.WeakReference; - import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.Disposable; -import io.reactivex.rxjava3.subjects.BehaviorSubject; -public abstract class BaseViewModel extends ViewModel { +/** + * MVVM ViewModel 基类(无 Context 版本) + * + * 职责: + * - 持有 UI 状态(LiveData) + * - 处理业务逻辑 + * - 暴露数据给 View 层 + * - 通过 {@link #addDisposable(Disposable)} 统一管理 RxJava 异步任务 + * + * 禁止: + * - 持有任何 View 引用(Activity / Fragment / View / Context) + * - 直接进行网络请求或数据库操作(必须通过 Repository) + * - 在 ViewModel 中弹 Toast / Dialog / Snackbar + * - 手动创建 Thread / Executor / AsyncTask 执行异步任务(必须使用 RxJava + {@link #addDisposable(Disposable)}) + * - 忘记将 Disposable add 到 {@link #mDisposables} 中 + * + * 若需要 Context / Application(如 SharedPreferences、AlarmManager 等),请继承 + * {@link BaseAndroidViewModel},通过 {@code getAppContext()} 获取 Application Context。 + */ +public abstract class BaseViewModel extends ViewModel implements IMvvmUiState { + + // ============================================================ + // 通用 UI 状态 + // ============================================================ + + /** 全局加载状态 */ + private final MutableLiveData mLoadingLiveData = new MutableLiveData<>(false); + + /** 全局错误提示 */ + private final MutableLiveData mErrorLiveData = new MutableLiveData<>(); + + /** 全局成功提示 */ + private final MutableLiveData mSuccessLiveData = new MutableLiveData<>(); + + // ============================================================ + // RxJava 异步管理(核心) + // ============================================================ /** - * Google 标准写法:ViewModel 内统一管理 Rx 订阅, - * onCleared 时自动取消,防止内存泄漏。 + * CompositeDisposable 用于统一管理所有 RxJava 订阅。 + * 在 onCleared() 中自动 dispose,防止内存泄漏。 */ protected final CompositeDisposable mDisposables = new CompositeDisposable(); + // ============================================================ + // 对外暴露(只读) + // ============================================================ + + public LiveData getLoadingLiveData() { + return mLoadingLiveData; + } + + public LiveData getErrorLiveData() { + return mErrorLiveData; + } + + public LiveData getSuccessLiveData() { + return mSuccessLiveData; + } + + // ============================================================ + // 受保护的方法(子类调用) + // ============================================================ + + protected void setLoading(boolean loading) { + mLoadingLiveData.postValue(loading); + } + + protected void setError(String message) { + mErrorLiveData.postValue(message); + } + + protected void setSuccess(String message) { + mSuccessLiveData.postValue(message); + } + /** - * 将订阅托管给 ViewModel 生命周期 + * 便捷方法:将 Disposable 添加到 {@link #mDisposables} 中管理。 + * 子类在发起 RxJava 请求时调用此方法,确保 ViewModel 销毁时自动取消订阅。 */ protected void addDisposable(Disposable disposable) { mDisposables.add(disposable); } + // ============================================================ + // 生命周期 + // ============================================================ + @Override protected void onCleared() { - mDisposables.clear(); super.onCleared(); - } - - /** - * 当前viewmodel对应的页面binding - */ - protected VDB binding; - - public void setVDBinding(ViewDataBinding vdBinding) { - binding = (VDB) vdBinding; - } - - public VDB getVDBinding() { - if (binding == null) { - throw new NullPointerException("BaseViewModel >> getVDBinding >> null!!!"); + // ViewModel 销毁时,自动取消所有正在执行的 RxJava 任务 + if (!mDisposables.isDisposed()) { + mDisposables.clear(); } - return binding; - } - - private WeakReference weakContext; - - public void setContext(Context context) { - if (weakContext == null) { - weakContext = new WeakReference<>(context.getApplicationContext()); - } - } - - public Context getSafeContext() { - if (weakContext == null) { - throw new NullPointerException("BaseViewModel >> getCtx >> null!!!"); - } - return weakContext.get(); - } - - private BehaviorSubject mBehaviorSubject; - - public void setLifecycle(BehaviorSubject subject) { - this.mBehaviorSubject = (BehaviorSubject) subject; - } - - public BehaviorSubject getLifecycle() { - return mBehaviorSubject; } } diff --git a/app/src/main/java/com/ttstd/dialer/base/mvvm/IMvvmUiState.java b/app/src/main/java/com/ttstd/dialer/base/mvvm/IMvvmUiState.java new file mode 100644 index 0000000..837a129 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/base/mvvm/IMvvmUiState.java @@ -0,0 +1,22 @@ +package com.ttstd.dialer.base.mvvm; + +import androidx.lifecycle.LiveData; + +/** + * MVVM ViewModel 通用 UI 状态接口 + * + * {@link BaseViewModel} 与 {@link BaseAndroidViewModel} 均实现本接口, + * 使基类({@link BaseMvvmActivity} / {@link BaseMvvmFragment} / {@link BaseMvvmDialogFragment}) + * 能以统一的 {@code VM extends ViewModel} 泛型约束订阅 loading / error / success 状态。 + */ +public interface IMvvmUiState { + + /** 全局加载状态(只读) */ + LiveData getLoadingLiveData(); + + /** 全局错误提示(只读) */ + LiveData getErrorLiveData(); + + /** 全局成功提示(只读) */ + LiveData getSuccessLiveData(); +} diff --git a/app/src/main/java/com/ttstd/dialer/base/mvvm/fragment/BaseMvvmDialogFragment.java b/app/src/main/java/com/ttstd/dialer/base/mvvm/fragment/BaseMvvmDialogFragment.java index bca7b21..c65a688 100644 --- a/app/src/main/java/com/ttstd/dialer/base/mvvm/fragment/BaseMvvmDialogFragment.java +++ b/app/src/main/java/com/ttstd/dialer/base/mvvm/fragment/BaseMvvmDialogFragment.java @@ -6,6 +6,7 @@ import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.Toast; import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; @@ -16,20 +17,35 @@ import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; import com.ttstd.dialer.base.BaseDialogFragment; -import com.ttstd.dialer.base.mvvm.BaseViewModel; +import com.ttstd.dialer.base.mvvm.IMvvmUiState; import com.ttstd.dialer.utils.Logger; import java.lang.ref.WeakReference; import java.lang.reflect.ParameterizedType; /** - * 优化生命周期管理,去掉冗余的 Bundle 定义 - * @param ViewModel 类型 + * MVVM DialogFragment 基类 + * + * 职责: + * - 绑定 DataBinding + * - 初始化 ViewModel(默认通过反射自动创建) + * - 订阅 ViewModel 中的 LiveData({@link #observeViewModel()}) + * - 订阅全局通用状态({@link #observeCommonState()}) + * - 页面创建完成后加载数据({@link #onLoadData()}) + * - 将用户操作转发给 ViewModel + * + * 禁止: + * - 在 DialogFragment 中直接进行网络请求或数据库操作(必须通过 ViewModel → Repository) + * - 持有可变 LiveData 的引用(应通过 LiveData 回调观察) + * - 在 DialogFragment 中写业务逻辑 + * + * @param ViewModel 类型,必须继承 {@link BaseViewModel} 或 {@link BaseAndroidViewModel} * @param ViewDataBinding 类型 */ public abstract class BaseMvvmDialogFragment extends BaseDialogFragment { + protected final String mTag = this.getClass().getSimpleName(); - + protected boolean mIsVisible; protected boolean mHasPrepare; @@ -48,18 +64,16 @@ public abstract class BaseMvvmDialogFragment(context); } + // ============================================================ + // 生命周期 + // ============================================================ + @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // ViewModel 初始化移至 onCreate + // ViewModel 初始化 try { - Class vmClass = (Class) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0]; - mViewModel = new ViewModelProvider(this).get(vmClass); - if (mViewModel instanceof BaseViewModel) { - BaseViewModel baseVm = (BaseViewModel) mViewModel; - baseVm.setContext(getContext()); - baseVm.setLifecycle(getLifecycleSubject()); - } + mViewModel = createViewModel(); } catch (Exception e) { Logger.e(mTag, "ViewModel initialization failed: " + e.getMessage()); } @@ -68,10 +82,8 @@ public abstract class BaseMvvmDialogFragment) mViewModel).setVDBinding(mViewDataBinding); - } return mViewDataBinding.getRoot(); } @@ -80,9 +92,17 @@ public abstract class BaseMvvmDialogFragment) mViewModel).setVDBinding(null); - } mViewDataBinding = null; } @@ -122,14 +139,85 @@ public abstract class BaseMvvmDialogFragment vmClass = (Class) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0]; + return new ViewModelProvider(this).get(vmClass); + } + @LayoutRes protected abstract int getLayoutId(); + /** 初始化 DataBinding(如设置 setClick、绑定表达式等) */ protected abstract void initDataBinding(); - protected abstract void initView(Bundle bundle); + /** 初始化视图(如设置 Adapter、Listener 等) */ + protected abstract void initView(Bundle savedInstanceState); - protected abstract void initData(Bundle savedInstanceState); + /** 订阅 ViewModel 中的业务 LiveData */ + protected abstract void observeViewModel(); + + // ============================================================ + // 可选重写 + // ============================================================ + + /** + * 页面创建完成后加载数据 + * 子类可重写此方法触发首次数据加载 + */ + protected void onLoadData() { + // 默认空实现 + } + + /** + * 订阅全局通用状态(loading / error / success) + */ + protected void observeCommonState() { + if (!(mViewModel instanceof IMvvmUiState)) return; + IMvvmUiState vm = (IMvvmUiState) mViewModel; + + vm.getLoadingLiveData().observe(getViewLifecycleOwner(), loading -> { + if (loading != null && loading) { + showLoading(); + } else { + hideLoading(); + } + }); + + vm.getErrorLiveData().observe(getViewLifecycleOwner(), error -> { + if (error != null && !error.isEmpty()) { + showError(error); + } + }); + + vm.getSuccessLiveData().observe(getViewLifecycleOwner(), msg -> { + if (msg != null && !msg.isEmpty()) { + showSuccess(msg); + } + }); + } + + // ============================================================ + // UI 辅助方法(子类可重写) + // ============================================================ + + protected void showLoading() { + } + + protected void hideLoading() { + } + + protected void showError(String message) { + Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show(); + } + + protected void showSuccess(String message) { + Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show(); + } @Override public void onConfigurationChanged(Configuration newConfig) { diff --git a/app/src/main/java/com/ttstd/dialer/base/mvvm/fragment/BaseMvvmFragment.java b/app/src/main/java/com/ttstd/dialer/base/mvvm/fragment/BaseMvvmFragment.java index 9210917..e495ded 100644 --- a/app/src/main/java/com/ttstd/dialer/base/mvvm/fragment/BaseMvvmFragment.java +++ b/app/src/main/java/com/ttstd/dialer/base/mvvm/fragment/BaseMvvmFragment.java @@ -6,6 +6,7 @@ import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.Toast; import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; @@ -16,27 +17,41 @@ import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; import com.ttstd.dialer.base.BaseFragment; -import com.ttstd.dialer.base.mvvm.BaseViewModel; +import com.ttstd.dialer.base.mvvm.IMvvmUiState; import com.ttstd.dialer.utils.Logger; import java.lang.ref.WeakReference; import java.lang.reflect.ParameterizedType; /** - * 优化生命周期管理,去掉冗余的 Bundle 定义 - * @param ViewModel 类型 + * MVVM Fragment 基类 + * + * 职责: + * - 绑定 DataBinding + * - 初始化 ViewModel(默认通过反射自动创建) + * - 订阅 ViewModel 中的 LiveData({@link #observeViewModel()}) + * - 订阅全局通用状态({@link #observeCommonState()}) + * - 页面创建完成后加载数据({@link #onLoadData()}) + * - 支持懒加载({@link #onEnter()} / {@link #onExit()}) + * - 将用户操作转发给 ViewModel + * + * 禁止: + * - 在 Fragment 中直接进行网络请求或数据库操作(必须通过 ViewModel → Repository) + * - 持有可变 LiveData 的引用(应通过 LiveData 回调观察) + * - 在 Fragment 中写业务逻辑 + * - 使用 getActivity() 强转后直接操作 Activity(应通过 ViewModel 通信) + * + * @param ViewModel 类型,必须继承 {@link BaseViewModel} 或 {@link BaseAndroidViewModel} * @param ViewDataBinding 类型 */ public abstract class BaseMvvmFragment extends BaseFragment { + protected final String mTag = this.getClass().getSimpleName(); - - /** - * 是否可见(用于懒加载逻辑) - */ + + /** 是否可见(用于懒加载逻辑) */ protected boolean mIsVisible; - /** - * View 是否准备完毕 - */ + + /** View 是否准备完毕 */ protected boolean mHasPrepare; protected VM mViewModel; @@ -54,18 +69,16 @@ public abstract class BaseMvvmFragment(context); } + // ============================================================ + // 生命周期 + // ============================================================ + @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); // ViewModel 初始化 try { - Class vmClass = (Class) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0]; - mViewModel = new ViewModelProvider(this).get(vmClass); - if (mViewModel instanceof BaseViewModel) { - BaseViewModel baseVm = (BaseViewModel) mViewModel; - baseVm.setContext(getContext()); - baseVm.setLifecycle(getLifecycleSubject()); - } + mViewModel = createViewModel(); } catch (Exception e) { Logger.e(mTag, "ViewModel initialization failed: " + e.getMessage()); } @@ -74,11 +87,8 @@ public abstract class BaseMvvmFragment) mViewModel).setVDBinding(mViewDataBinding); - } return mViewDataBinding.getRoot(); } @@ -86,11 +96,18 @@ public abstract class BaseMvvmFragment) mViewModel).setVDBinding(null); - } mViewDataBinding = null; } @@ -127,14 +141,91 @@ public abstract class BaseMvvmFragment vmClass = (Class) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0]; + return new ViewModelProvider(this).get(vmClass); + } + @LayoutRes protected abstract int getLayoutId(); + /** 初始化 DataBinding(如设置 setClick、绑定表达式等) */ protected abstract void initDataBinding(); - protected abstract void initView(Bundle bundle); + /** 初始化视图(如设置 Adapter、Listener 等) */ + protected abstract void initView(Bundle savedInstanceState); - protected abstract void initData(Bundle savedInstanceState); + /** 订阅 ViewModel 中的业务 LiveData */ + protected abstract void observeViewModel(); + + // ============================================================ + // 可选重写 + // ============================================================ + + /** + * 页面创建完成后加载数据 + * 子类可重写此方法触发首次数据加载 + */ + protected void onLoadData() { + // 默认空实现 + } + + /** + * 订阅全局通用状态(loading / error / success) + * 子类可在 {@link #observeViewModel()} 中调用 super.observeCommonState(),或直接重写 + */ + protected void observeCommonState() { + if (!(mViewModel instanceof IMvvmUiState)) return; + IMvvmUiState vm = (IMvvmUiState) mViewModel; + + vm.getLoadingLiveData().observe(getViewLifecycleOwner(), loading -> { + if (loading != null && loading) { + showLoading(); + } else { + hideLoading(); + } + }); + + vm.getErrorLiveData().observe(getViewLifecycleOwner(), error -> { + if (error != null && !error.isEmpty()) { + showError(error); + } + }); + + vm.getSuccessLiveData().observe(getViewLifecycleOwner(), msg -> { + if (msg != null && !msg.isEmpty()) { + showSuccess(msg); + } + }); + } + + // ============================================================ + // UI 辅助方法(子类可重写) + // ============================================================ + + protected void showLoading() { + // 默认实现:可替换为自定义 LoadingDialog + } + + protected void hideLoading() { + } + + protected void showError(String message) { + Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show(); + } + + protected void showSuccess(String message) { + Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show(); + } + + // ============================================================ + // 懒加载回调(子类可重写) + // ============================================================ @Override public void onConfigurationChanged(Configuration newConfig) { @@ -150,4 +241,17 @@ public abstract class BaseMvvmFragment() { + protected void observeViewModel() { + Logger.e(TAG, "observeViewModel: "); + Logger.e(TAG, "observeViewModel: " + mViewModel.getAppUpdateData()); + mViewModel.getAppUpdateData().observe(getViewLifecycleOwner(), new Observer() { @Override public void onChanged(Integer integer) { if (integer > 0) { diff --git a/app/src/main/java/com/ttstd/dialer/fragment/app/AppViewModel.java b/app/src/main/java/com/ttstd/dialer/fragment/app/AppViewModel.java index 229ee72..a8e0d21 100644 --- a/app/src/main/java/com/ttstd/dialer/fragment/app/AppViewModel.java +++ b/app/src/main/java/com/ttstd/dialer/fragment/app/AppViewModel.java @@ -1,11 +1,8 @@ package com.ttstd.dialer.fragment.app; -import android.content.Context; +import android.app.Application; -import com.trello.rxlifecycle4.RxLifecycle; -import com.trello.rxlifecycle4.android.FragmentEvent; -import com.ttstd.dialer.base.mvvm.BaseViewModel; -import com.ttstd.dialer.databinding.FragmentAppBinding; +import com.ttstd.dialer.base.mvvm.BaseAndroidViewModel; import com.ttstd.dialer.db.app.AppInfo; import com.ttstd.dialer.db.app.AppRepository; import com.ttstd.dialer.livedata.SingleLiveEvent; @@ -14,31 +11,27 @@ import com.ttstd.dialer.utils.Logger; import java.util.concurrent.Callable; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.annotations.NonNull; import io.reactivex.rxjava3.core.Observable; -import io.reactivex.rxjava3.core.Observer; -import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.schedulers.Schedulers; -public class AppViewModel extends BaseViewModel { +public class AppViewModel extends BaseAndroidViewModel { private static final String TAG = "AppViewModel"; - private AppRepository mAppRepository; + private final AppRepository mAppRepository; - @Override - public void setContext(Context context) { - super.setContext(context); - mAppRepository = new AppRepository(context); + private final SingleLiveEvent mAppUpdateData = new SingleLiveEvent<>(); + + public AppViewModel(Application application) { + super(application); + mAppRepository = new AppRepository(application); } - public SingleLiveEvent mAppUpdateData = new SingleLiveEvent<>(); - -// public void resetLiveData() { -// mAppUpdateData = new MutableLiveData<>(); -// } + public SingleLiveEvent getAppUpdateData() { + return mAppUpdateData; + } public void updateAppInfo(AppInfo appInfo) { - Observable.fromCallable(new Callable() { + addDisposable(Observable.fromCallable(new Callable() { @Override public Integer call() throws Exception { // 根据状态自动设置位置为最大值 @@ -53,30 +46,13 @@ public class AppViewModel extends BaseViewModel() { - @Override - public void onSubscribe(@NonNull Disposable d) { - Logger.e("updateAppInfo", "onSubscribe: "); - } - - @Override - public void onNext(@NonNull Integer row) { - Logger.e("updateAppInfo", "onNext: " + row); - mAppUpdateData.setValue(row); - } - - @Override - public void onError(@NonNull Throwable e) { - Logger.e("updateAppInfo", "onError: " + e.getMessage()); - } - - @Override - public void onComplete() { - Logger.e("updateAppInfo", "onComplete: "); - } - }); + .subscribe(row -> { + Logger.e("updateAppInfo", "onNext: " + row); + mAppUpdateData.setValue(row); + }, + throwable -> Logger.e("updateAppInfo", "onError: " + throwable.getMessage()))); } } diff --git a/app/src/main/java/com/ttstd/dialer/fragment/contact/ContactFragment.java b/app/src/main/java/com/ttstd/dialer/fragment/contact/ContactFragment.java index 8774f7a..f961bbb 100644 --- a/app/src/main/java/com/ttstd/dialer/fragment/contact/ContactFragment.java +++ b/app/src/main/java/com/ttstd/dialer/fragment/contact/ContactFragment.java @@ -56,9 +56,8 @@ public class ContactFragment extends BaseMvvmFragment>() { + protected void observeViewModel() { + mViewModel.getContactListData().observe(getViewLifecycleOwner(), new Observer>() { @Override public void onChanged(List contactInfos) { Logger.e(TAG, "mContactListData: " + contactInfos); @@ -68,6 +67,11 @@ public class ContactFragment extends BaseMvvmFragment { +public class ContactViewModel extends BaseAndroidViewModel { private static final String TAG = "ContactViewModel"; - private ContactDataRepository mRepository; + private final ContactDataRepository mRepository; private boolean isDataLoaded = false; - public MutableLiveData> mContactListData = new MutableLiveData<>(); + private final MutableLiveData> mContactListData = new MutableLiveData<>(); - @Override - public void setContext(Context context) { - super.setContext(context); - mRepository = ContactDataRepository.getInstance(context); + public ContactViewModel(Application application) { + super(application); + mRepository = ContactDataRepository.getInstance(application); + } + + public LiveData> getContactListData() { + return mContactListData; } public void preloadData() { diff --git a/app/src/main/java/com/ttstd/dialer/fragment/dialog/contact/call/CallFragment.java b/app/src/main/java/com/ttstd/dialer/fragment/dialog/contact/call/CallFragment.java index f4d9ef0..a448b54 100644 --- a/app/src/main/java/com/ttstd/dialer/fragment/dialog/contact/call/CallFragment.java +++ b/app/src/main/java/com/ttstd/dialer/fragment/dialog/contact/call/CallFragment.java @@ -59,7 +59,7 @@ public class CallFragment extends BaseMvvmDialogFragment { +public class CallViewModel extends BaseViewModel { } diff --git a/app/src/main/java/com/ttstd/dialer/fragment/dialog/contact/delete/ContactDeleteDialogFragment.java b/app/src/main/java/com/ttstd/dialer/fragment/dialog/contact/delete/ContactDeleteDialogFragment.java index 12d8a90..6497f3f 100644 --- a/app/src/main/java/com/ttstd/dialer/fragment/dialog/contact/delete/ContactDeleteDialogFragment.java +++ b/app/src/main/java/com/ttstd/dialer/fragment/dialog/contact/delete/ContactDeleteDialogFragment.java @@ -66,7 +66,7 @@ public class ContactDeleteDialogFragment extends BaseMvvmDialogFragment { +public class ContactDeleteViewModel extends BaseViewModel { } diff --git a/app/src/main/java/com/ttstd/dialer/fragment/dialog/contact/edit/EditDialogFragment.java b/app/src/main/java/com/ttstd/dialer/fragment/dialog/contact/edit/EditDialogFragment.java index 97304e3..f2a062b 100644 --- a/app/src/main/java/com/ttstd/dialer/fragment/dialog/contact/edit/EditDialogFragment.java +++ b/app/src/main/java/com/ttstd/dialer/fragment/dialog/contact/edit/EditDialogFragment.java @@ -66,7 +66,7 @@ public class EditDialogFragment extends BaseMvvmDialogFragment { +public class EditViewModel extends BaseViewModel { } diff --git a/app/src/main/java/com/ttstd/dialer/fragment/dialog/dock/DockAppDialogFagment.java b/app/src/main/java/com/ttstd/dialer/fragment/dialog/dock/DockAppDialogFagment.java index cde2003..90dea33 100644 --- a/app/src/main/java/com/ttstd/dialer/fragment/dialog/dock/DockAppDialogFagment.java +++ b/app/src/main/java/com/ttstd/dialer/fragment/dialog/dock/DockAppDialogFagment.java @@ -87,11 +87,6 @@ public class DockAppDialogFagment extends BaseMvvmDialogFragment{ +public class DockAppViewModel extends BaseViewModel { } diff --git a/app/src/main/java/com/ttstd/dialer/fragment/dialog/move/MoveAppDialogFagment.java b/app/src/main/java/com/ttstd/dialer/fragment/dialog/move/MoveAppDialogFagment.java index fb60270..1988fe2 100644 --- a/app/src/main/java/com/ttstd/dialer/fragment/dialog/move/MoveAppDialogFagment.java +++ b/app/src/main/java/com/ttstd/dialer/fragment/dialog/move/MoveAppDialogFagment.java @@ -82,11 +82,6 @@ public class MoveAppDialogFagment extends BaseMvvmDialogFragment{ +public class MoveAppViewModel extends BaseViewModel { } diff --git a/app/src/main/java/com/ttstd/dialer/fragment/dialog/shortcut/ShortcutDialogFagment.java b/app/src/main/java/com/ttstd/dialer/fragment/dialog/shortcut/ShortcutDialogFagment.java index 2633065..df9cb6e 100644 --- a/app/src/main/java/com/ttstd/dialer/fragment/dialog/shortcut/ShortcutDialogFagment.java +++ b/app/src/main/java/com/ttstd/dialer/fragment/dialog/shortcut/ShortcutDialogFagment.java @@ -87,11 +87,6 @@ public class ShortcutDialogFagment extends BaseMvvmDialogFragment{ +public class ShortcutViewModel extends BaseViewModel { } diff --git a/app/src/main/java/com/ttstd/dialer/fragment/home/HomeFragment.java b/app/src/main/java/com/ttstd/dialer/fragment/home/HomeFragment.java index cfa51e5..33b43b6 100644 --- a/app/src/main/java/com/ttstd/dialer/fragment/home/HomeFragment.java +++ b/app/src/main/java/com/ttstd/dialer/fragment/home/HomeFragment.java @@ -116,7 +116,7 @@ public class HomeFragment extends BaseMvvmFragment() { @Override diff --git a/app/src/main/java/com/ttstd/dialer/fragment/home/HomeViewModel.java b/app/src/main/java/com/ttstd/dialer/fragment/home/HomeViewModel.java index d347c6d..16f5918 100644 --- a/app/src/main/java/com/ttstd/dialer/fragment/home/HomeViewModel.java +++ b/app/src/main/java/com/ttstd/dialer/fragment/home/HomeViewModel.java @@ -1,10 +1,8 @@ package com.ttstd.dialer.fragment.home; -import com.trello.rxlifecycle4.android.FragmentEvent; import com.ttstd.dialer.base.mvvm.BaseViewModel; -import com.ttstd.dialer.databinding.FragmentHomeBinding; -public class HomeViewModel extends BaseViewModel { +public class HomeViewModel extends BaseViewModel { } diff --git a/app/src/main/java/com/ttstd/dialer/fragment/settings/SettingsFragment.java b/app/src/main/java/com/ttstd/dialer/fragment/settings/SettingsFragment.java index 72fd9df..87d01ac 100644 --- a/app/src/main/java/com/ttstd/dialer/fragment/settings/SettingsFragment.java +++ b/app/src/main/java/com/ttstd/dialer/fragment/settings/SettingsFragment.java @@ -37,7 +37,7 @@ public class SettingsFragment extends BaseMvvmFragment { +public class SettingsViewModel extends BaseViewModel { } diff --git a/app/src/main/java/com/ttstd/dialer/network/OkHttpManager.java b/app/src/main/java/com/ttstd/dialer/network/OkHttpManager.java index b324a62..b21b71c 100644 --- a/app/src/main/java/com/ttstd/dialer/network/OkHttpManager.java +++ b/app/src/main/java/com/ttstd/dialer/network/OkHttpManager.java @@ -319,6 +319,13 @@ public class OkHttpManager { return schedule(getApiService(SnApi.class).register(DeviceManagerService.getInstance().getSerial(), Build.MODEL), provider); } + /** + * 设备注册(由 ViewModel 通过 addDisposable 管理订阅生命周期) + */ + public Observable getSnRegisterObservable() { + return schedule(getApiService(SnApi.class).register(DeviceManagerService.getInstance().getSerial(), Build.MODEL)); + } + /** * 上传设备基本信息 */