问题修复
维护 Android Java 项目时,按 MVVM…
在编写或审查 Android Java 源码、Gradle 配置、测试与 CI 时,快速套用 MVVM、ViewBinding、Repository、Espresso、GitHub Actions 和 Lint 规范。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 49/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 85/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
维护 Android Java 项目时,通用
解决思路
- 按 app/src/main/java/com/example/app/ 分层组织代码:data/local(Room database、SharedPreferences)、data/remote(Retrofit services、API clients)、data/repository(Repository implementations)、di(Hilt/Dagger)、domain/model、domain/repository、domain/usecase、ui/feature(FeatureActivity.java、FeatureFragment.java、FeatureViewModel.java)、ui/common、App.java。
- 将 res/layout、res/values、res/drawable 和 AndroidManifest.xml 放在 app/src/main/ 下;单元测试放 app/src/test/,仪器测试放 app/src/androidTest/。
- 在根目录放置 build.gradle(Project-level)、gradle.properties、settings.gradle、CLAUDE.md,在 app/ 下放置 app-level build.gradle。
- 在 app-level build.gradle 的 plugins 中应用 id 'com.android.application',设置 namespace 'com.example.app'、compileSdk 34。
- 在 defaultConfig 中设置 applicationId "com.example.app"、minSdk 24、targetSdk 34、versionCode 1、versionName "1.0"、testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"。
- 在 buildTypes.release 中设置 minifyEnabled true,并设置 proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'。
- 在 compileOptions 中设置 sourceCompatibility JavaVersion.VERSION_17、targetCompatibility JavaVersion.VERSION_17。
- 在 buildFeatures 中设置 viewBinding true。
- 添加 AndroidX 依赖:implementation 'androidx.core:core:1.12.0'、'androidx.appcompat:appcompat:1.6.1'、'com.google.android.material:material:1.11.0'、'androidx.constraintlayout:constraintlayout:2.1.4'。
- 添加 Lifecycle 依赖:implementation 'androidx.lifecycle:lifecycle-viewmodel:2.7.0'、'androidx.lifecycle:lifecycle-livedata:2.7.0'。
- 添加测试依赖:testImplementation 'junit:junit:4.13.2'、'org.mockito:mockito-core:5.8.0';androidTestImplementation 'androidx.test.ext:junit:1.1.5'、'androidx.test.espresso:espresso-core:3.5.1'。
- 实现 MVVM ViewModel:让 UserViewModel extends ViewModel 以持有 UI 状态并在配置变更后存活。
- 在 UserViewModel 构造中注入 UserRepository。
- 在 UserViewModel 中声明 MutableLiveData<User> user、MutableLiveData<Boolean> loading = new MutableLiveData<>(false)、MutableLiveData<String> error。
- 提供 getUser()、isLoading()、getError() 返回对应 LiveData。
- 实现 loadUser(String userId):先 loading.setValue(true)。
- 在 loadUser 中调用 repository.getUser(userId, new Callback<User>(){...})。
- 在回调 onSuccess(User result) 中执行 user.setValue(result)、loading.setValue(false)。
- 在回调 onError(String message) 中执行 error.setValue(message)、loading.setValue(false)。
- 定义 domain 层 UserRepository 接口:void getUser(String userId, Callback<User> callback)、void saveUser(User user, Callback<Void> callback)。
- 实现 UserRepositoryImpl implements UserRepository,并注入 UserApi api、UserDao dao。
- 在 UserRepositoryImpl.getUser 中先读取缓存:User cached = dao.getUserById(userId)。
- 如果 cached != null,则 callback.onSuccess(cached) 并 return。
- 如果无缓存,则调用 api.getUser(userId).enqueue(new retrofit2.Callback<User>(){...})。
- 在 Retrofit onResponse 中,如果 response.isSuccessful() 且 response.body() != null,则 dao.insert(response.body()) 并 callback.onSuccess(response.body());否则 callback.onError("Failed to load user")。
- 在 Retrofit onFailure 中调用 callback.onError(t.getMessage())。
- 在 MainActivity extends AppCompatActivity 中,onCreate 里用 binding = ActivityMainBinding.inflate(getLayoutInflater()),再 setContentView(binding.getRoot())。
- 在 Activity 中通过 viewModel = new ViewModelProvider(this).get(MainViewModel.class) 获取 ViewModel。
- 在 Activity 的 setupObservers 中:viewModel.getUser().observe(this, user -> binding.userName.setText(user.getName()))。
- 在 Activity 的 setupObservers 中:viewModel.isLoading().observe(this, isLoading -> binding.progressBar.setVisibility(isLoading ? View.VISIBLE : View.GONE))。
- 在 Activity 的 setupListeners 中:binding.refreshButton.setOnClickListener(v -> viewModel.loadUser(getCurrentUserId()))。
- 在 Activity.onDestroy 中设置 binding = null。
- 在 UserFragment extends Fragment 中,onCreateView 用 binding = FragmentUserBinding.inflate(inflater, container, false) 并 return binding.getRoot()。
- 在 Fragment.onViewCreated 中,通过 viewModel = new ViewModelProvider(requireActivity()).get(UserViewModel.class) 获取 ViewModel,并调用 setupObservers()。
- 在 Fragment 的 setupObservers 中:viewModel.getUser().observe(getViewLifecycleOwner(), user -> binding.userName.setText(user.getName()))。
- 在 Fragment.onDestroyView 中设置 binding = null 以防止内存泄漏。
- 编写单元测试:@RunWith(MockitoJUnitRunner.class)、@Mock UserRepository repository、@Rule InstantTaskExecutorRule instantTaskExecutorRule、@Before 创建 UserViewModel。
- 成功用例:用 doAnswer 获取 invocation.getArgument(1) 的 Callback<User>,调用 callback.onSuccess(expectedUser),并用 when(repository).getUser(eq("1"), any()) 打桩;调用 viewModel.loadUser("1");断言 getUser().getValue() 等于 expectedUser,isLoading().getValue() 为 false。
- 失败用例:用 doAnswer 获取 Callback<User>,调用 callback.onError("Network error"),并用 when(repository).getUser(eq("1"), any()) 打桩;调用 viewModel.loadUser("1");断言 getError().getValue() 等于 "Network error",isLoading().getValue() 为 false。
- 编写 Espresso 仪器测试:@RunWith(AndroidJUnit4.class),@Rule ActivityScenarioRule<MainActivity> activityRule = new ActivityScenarioRule<>(MainActivity.class)。
- 测试 userName:onView(withId(R.id.userName)).check(matches(isDisplayed()))。
- 测试 refreshButton:onView(withId(R.id.refreshButton)).perform(click()),再 onView(withId(R.id.progressBar)).check(matches(isDisplayed()))。
- 测试 userList:onView(withId(R.id.userList)).perform(RecyclerViewActions.scrollToPosition(10)),再 onView(withText("User 10")).check(matches(isDisplayed()))。
- 配置 GitHub Actions workflow:name: Android CI,on push branches [main],pull_request branches [main]。
- build job 使用 runs-on: ubuntu-latest。
- build job 步骤:uses: actions/checkout@v4。
- 使用 actions/setup-java@v4 设置 java-version: '17'、distribution: 'temurin'。
- 使用 gradle/actions/setup-gradle@v3 配置 Gradle。
- 执行 chmod +x gradlew 授予执行权限。
- 执行 ./gradlew lint。
- 执行 ./gradlew testDebugUnitTest。
- 执行 ./gradlew assembleDebug。
- 使用 actions/upload-artifact@v4 上传 APK:name: debug-apk,path: app/build/outputs/apk/debug/app-debug.apk。
- instrumentation-tests job 使用 runs-on: ubuntu-latest,并执行 actions/checkout@v4、actions/setup-java@v4(java-version 17、distribution temurin)。
- 在 instrumentation-tests 中启用 KVM:echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules;sudo udevadm control --reload-rules;sudo udevadm trigger --name-match=kvm。
- 使用 reactivecircus/android-emulator-runner@v2 运行仪器测试:api-level: 29,script: ./gradlew connectedDebugAndroidTest。
- 创建 lint.xml:将 HardcodedText severity="error"、MissingTranslation severity="error"、UnusedResources severity="warning"。
- 在 lint.xml 中设置 StaticFieldLeak severity="error"、HardcodedDebugMode severity="error"、AllowBackup severity="warning"。
- 在 lint.xml 中设置 ViewHolder severity="error"、Overdraw severity="warning"。
- 在 lint.xml 中为 InvalidPackage 配置 ignore path="**/test/**" 和 path="**/androidTest/**"。
- 在 build.gradle 的 android.lint 中设置 abortOnError true、warningsAsErrors false、checkReleaseBuilds true、xmlReport true、htmlReport true。
- Null-Safe Callbacks:定义 interface Callback<T>,包含 void onSuccess(T result) 和 void onError(String message)。
- 在 fetchData(Callback<Data> callback) 中先 if (callback == null) return。
- 在 fetchData 的 try 中执行 Data result = performFetch(),成功 callback.onSuccess(result);catch (Exception e) 中 callback.onError(e.getMessage())。
- Safe Context Usage:DataManager 构造中 this.appContext = context.getApplicationContext(),长生命周期对象使用 application context 防止 Activity 泄漏。
- 在回调更新 UI 前检查 Context context = getContext(); if (context == null || !isAdded()) return。
- Thread-Safe Singleton:ApiClient 中声明 private static volatile ApiClient instance。
- 实现 getInstance() 双重检查锁:if (instance == null) { synchronized (ApiClient.class) { if (instance == null) { instance = new ApiClient(); } } },返回 instance。
- ApiClient 构造中用 new Retrofit.Builder().baseUrl(BASE_URL).addConverterFactory(GsonConverterFactory.create()).build() 创建 Retrofit。
- 避免 Context leaks:不要在 static fields 或 singletons 中持有 Activity/Fragment 引用。
- 避免 callbacks 内存泄漏:使用 WeakReference 或在 onDestroy 中清除 callbacks。
- 避免在后台线程更新 UI:UI 变更必须 post 到 main thread。
- 避免硬编码字符串:所有用户可见文本使用 string resources。
- 避免 God Activities:Activity 保持 200 行以内,把逻辑提取到 ViewModel。
- 避免 NetworkOnMainThreadException:不要在 main thread 执行网络调用。
- 避免忽略 lifecycle:尊重 Activity/Fragment lifecycle states。
- 避免阻塞 main thread:main thread 操作保持 16ms 内。
- 避免不处理配置变更:用 ViewModel 在旋转时存活。
- 避免硬编码尺寸:使用 dp/sp 单位和 dimension resources。
- 避免深层视图层级:布局深度保持 10 层以内,使用 ConstraintLayout。
- 避免不关闭资源:总是关闭 Cursor、InputStream、database 连接。
适用边界 · 注意事项
- 不适用于 Kotlin、Jetpack Compose 或其他非 Java 的 Android 项目。
- 不适用于非 Android/Gradle 项目。
- GitHub Actions 部分仅适用于使用 GitHub Actions 的仓库,其他 CI 需要改写。