AndroidJava开发 android-java

这项技能涉及使用MVVM架构、ViewBinding和Espresso测试的Android Java开发,包括项目结构、Gradle配置、架构模式、活动与片段模式、测试和GitHub Actions自动化。关键词:Android, Java, MVVM, ViewBinding, Espresso, 测试, 自动化。

移动开发 0 次安装 0 次浏览 更新于 3/4/2026

Android Java 技能

加载与:base.md


项目结构

project/
├── app/
│   ├── src/
│   │   ├── main/
│   │   │   ├── java/com/example/app/
│   │   │   │   ├── data/           # 数据层
│   │   │   │   │   ├── local/      # Room数据库,SharedPreferences
│   │   │   │   │   ├── remote/     # Retrofit服务,API客户端
│   │   │   │   │   └── repository/ # 仓库实现
│   │   │   │   ├── di/             # 依赖注入(Hilt/Dagger)
│   │   │   │   ├── domain/         # 业务逻辑
│   │   │   │   │   ├── model/      # 领域模型
│   │   │   │   │   ├── repository/ # 仓库接口
│   │   │   │   │   └── usecase/    # 用例
│   │   │   │   ├── ui/             # 表示层
│   │   │   │   │   ├── feature/    # 功能屏幕
│   │   │   │   │   │   ├── FeatureActivity.java
│   │   │   │   │   │   ├── FeatureFragment.java
│   │   │   │   │   │   └── FeatureViewModel.java
│   │   │   │   │   └── common/     # 共享UI组件
│   │   │   │   └── App.java        # 应用类
│   │   │   ├── res/
│   │   │   │   ├── layout/
│   │   │   │   ├── values/
│   │   │   │   └── drawable/
│   │   │   └── AndroidManifest.xml
│   │   ├── test/                   # 单元测试
│   │   └── androidTest/            # 仪器测试
│   └── build.gradle
├── build.gradle                    # 项目级构建文件
├── gradle.properties
├── settings.gradle
└── CLAUDE.md

Gradle 配置

App级build.gradle

plugins {
    id 'com.android.application'
}

android {
    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
    }
}

dependencies {
    // AndroidX
    implementation 'androidx.core:core:1.12.0'
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.11.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'

    // Lifecycle
    implementation 'androidx.lifecycle:lifecycle-viewmodel:2.7.0'
    implementation 'androidx.lifecycle:lifecycle-livedata:2.7.0'

    // 测试
    testImplementation 'junit:junit:4.13.2'
    testImplementation 'org.mockito:mockito-core:5.8.0'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

架构模式

ViewModel中的MVVM

// ViewModel - 持有UI状态,配置更改后存活
public class UserViewModel extends ViewModel {
    private final UserRepository repository;
    private final MutableLiveData<User> user = new MutableLiveData<>();
    private final MutableLiveData<Boolean> loading = new MutableLiveData<>(false);
    private final MutableLiveData<String> error = new MutableLiveData<>();

    public UserViewModel(UserRepository repository) {
        this.repository = repository;
    }

    public LiveData<User> getUser() {
        return user;
    }

    public LiveData<Boolean> isLoading() {
        return loading;
    }

    public LiveData<String> getError() {
        return error;
    }

    public void loadUser(String userId) {
        loading.setValue(true);
        repository.getUser(userId, new Callback<User>() {
            @Override
            public void onSuccess(User result) {
                user.setValue(result);
                loading.setValue(false);
            }

            @Override
            public void onError(String message) {
                error.setValue(message);
                loading.setValue(false);
            }
        });
    }
}

仓库模式

// 仓库接口(领域层)
public interface UserRepository {
    void getUser(String userId, Callback<User> callback);
    void saveUser(User user, Callback<Void> callback);
}

// 仓库实现(数据层)
public class UserRepositoryImpl implements UserRepository {
    private final UserApi api;
    private final UserDao dao;

    public UserRepositoryImpl(UserApi api, UserDao dao) {
        this.api = api;
        this.dao = dao;
    }

    @Override
    public void getUser(String userId, Callback<User> callback) {
        // 先尝试缓存,然后网络
        User cached = dao.getUserById(userId);
        if (cached != null) {
            callback.onSuccess(cached);
            return;
        }
        api.getUser(userId).enqueue(new retrofit2.Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                if (response.isSuccessful() && response.body() != null) {
                    dao.insert(response.body());
                    callback.onSuccess(response.body());
                } else {
                    callback.onError("Failed to load user");
                }
            }

            @Override
            public void onFailure(Call<User> call, Throwable t) {
                callback.onError(t.getMessage());
            }
        });
    }
}

活动与片段模式

带有ViewBinding的活动

public class MainActivity extends AppCompatActivity {
    private ActivityMainBinding binding;
    private MainViewModel viewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        viewModel = new ViewModelProvider(this).get(MainViewModel.class);
        setupObservers();
        setupListeners();
    }

    private void setupObservers() {
        viewModel.getUser().observe(this, user -> {
            binding.userName.setText(user.getName());
        });

        viewModel.isLoading().observe(this, isLoading -> {
            binding.progressBar.setVisibility(isLoading ? View.VISIBLE : View.GONE);
        });
    }

    private void setupListeners() {
        binding.refreshButton.setOnClickListener(v -> {
            viewModel.loadUser(getCurrentUserId());
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        binding = null;
    }
}

带有ViewBinding的片段

public class UserFragment extends Fragment {
    private FragmentUserBinding binding;
    private UserViewModel viewModel;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        binding = FragmentUserBinding.inflate(inflater, container, false);
        return binding.getRoot();
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        viewModel = new ViewModelProvider(requireActivity()).get(UserViewModel.class);
        setupObservers();
    }

    private void setupObservers() {
        viewModel.getUser().observe(getViewLifecycleOwner(), user -> {
            binding.userName.setText(user.getName());
        });
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        binding = null;  // 预防内存泄漏
    }
}

测试

使用JUnit和Mockito的单元测试

@RunWith(MockitoJUnitRunner.class)
public class UserViewModelTest {
    @Mock
    private UserRepository repository;

    @Rule
    public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();

    private UserViewModel viewModel;

    @Before
    public void setup() {
        viewModel = new UserViewModel(repository);
    }

    @Test
    public void loadUser_success_updatesUserLiveData() {
        // 安排
        User expectedUser = new User("1", "John Doe");
        doAnswer(invocation -> {
            Callback<User> callback = invocation.getArgument(1);
            callback.onSuccess(expectedUser);
            return null;
        }).when(repository).getUser(eq("1"), any());

        // 行动
        viewModel.loadUser("1");

        // 断言
        assertEquals(expectedUser, viewModel.getUser().getValue());
        assertFalse(viewModel.isLoading().getValue());
    }

    @Test
    public void loadUser_error_updatesErrorLiveData() {
        // 安排
        doAnswer(invocation -> {
            Callback<User> callback = invocation.getArgument(1);
            callback.onError("Network error");
            return null;
        }).when(repository).getUser(eq("1"), any());

        // 行动
        viewModel.loadUser("1");

        // 断言
        assertEquals("Network error", viewModel.getError().getValue());
        assertFalse(viewModel.isLoading().getValue());
    }
}

使用Espresso的仪器测试

@RunWith(AndroidJUnit4.class)
public class MainActivityTest {
    @Rule
    public ActivityScenarioRule<MainActivity> activityRule =
            new ActivityScenarioRule<>(MainActivity.class);

    @Test
    public void userName_isDisplayed() {
        onView(withId(R.id.userName))
                .check(matches(isDisplayed()));
    }

    @Test
    public void refreshButton_click_triggersRefresh() {
        onView(withId(R.id.refreshButton))
                .perform(click());

        onView(withId(R.id.progressBar))
                .check(matches(isDisplayed()));
    }

    @Test
    public void userList_scrollToItem_displaysCorrectly() {
        onView(withId(R.id.userList))
                .perform(RecyclerViewActions.scrollToPosition(10));

        onView(withText("User 10"))
                .check(matches(isDisplayed()));
    }
}

GitHub Actions

name: Android CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v3

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      - name: Run Lint
        run: ./gradlew lint

      - name: Run Unit Tests
        run: ./gradlew testDebugUnitTest

      - name: Build Debug APK
        run: ./gradlew assembleDebug

      - name: Upload APK
        uses: actions/upload-artifact@v4
        with:
          name: debug-apk
          path: app/build/outputs/apk/debug/app-debug.apk

  instrumentation-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Enable KVM
        run: |
          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

      - name: Run Instrumentation Tests
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 29
          script: ./gradlew connectedDebugAndroidTest

Lint 配置

lint.xml

<?xml version="1.0" encoding="UTF-8"?>
<lint>
    <!-- 将这些视为错误 -->
    <issue id="HardcodedText" severity="error" />
    <issue id="MissingTranslation" severity="error" />
    <issue id="UnusedResources" severity="warning" />

    <!-- 内存泄漏检测 -->
    <issue id="StaticFieldLeak" severity="error" />

    <!-- 安全 -->
    <issue id="HardcodedDebugMode" severity="error" />
    <issue id="AllowBackup" severity="warning" />

    <!-- 性能 -->
    <issue id="ViewHolder" severity="error" />
    <issue id="Overdraw" severity="warning" />

    <!-- 忽略测试 -->
    <issue id="InvalidPackage">
        <ignore path="**/test/**" />
        <ignore path="**/androidTest/**" />
    </issue>
</lint>

build.gradle 代码检查选项

android {
    lint {
        abortOnError true
        warningsAsErrors false
        checkReleaseBuilds true
        xmlReport true
        htmlReport true
    }
}

常见模式

空安全回调

// 定义回调接口
public interface Callback<T> {
    void onSuccess(T result);
    void onError(String message);
}

// 使用空检查
public void fetchData(Callback<Data> callback) {
    if (callback == null) return;

    try {
        Data result = performFetch();
        callback.onSuccess(result);
    } catch (Exception e) {
        callback.onError(e.getMessage());
    }
}

安全上下文使用

// 使用应用程序上下文进行长期对象
public class DataManager {
    private final Context appContext;

    public DataManager(Context context) {
        // 始终使用应用程序上下文以防止活动泄漏
        this.appContext = context.getApplicationContext();
    }
}

// 在回调中检查空上下文
private void updateUI() {
    Context context = getContext();
    if (context == null || !isAdded()) return;
    // 安全使用上下文
}

线程安全的单例

public class ApiClient {
    private static volatile ApiClient instance;
    private final Retrofit retrofit;

    private ApiClient() {
        retrofit = new Retrofit.Builder()
                .baseUrl(BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .build();
    }

    public static ApiClient getInstance() {
        if (instance == null) {
            synchronized (ApiClient.class) {
                if (instance == null) {
                    instance = new ApiClient();
                }
            }
        }
        return instance;
    }
}

Android 反模式

  • 上下文泄漏 - 永远不要在静态字段或单例中持有Activity/Fragment引用
  • 回调中的内存泄漏 - 总是使用WeakReference或在onDestroy中清除回调
  • 在后台线程更新UI - 总是在主线程上发布UI更改
  • 硬编码字符串 - 使用字符串资源进行所有用户可见的文本
  • 上帝活动 - 保持活动在200行以下,提取逻辑到ViewModels
  • NetworkOnMainThreadException - 永远不要在主线程上执行网络调用
  • 忽略生命周期 - 总是尊重Activity/Fragment生命周期状态
  • 阻塞主线程 - 保持主线程操作在16ms以下
  • 不处理配置更改 - 使用ViewModel来生存旋转
  • 硬编码尺寸 - 使用dp/sp单位和尺寸资源
  • 深度视图层次结构 - 保持布局深度在10级以下,使用ConstraintLayout
  • 不关闭资源 - 始终关闭Cursor,InputStream,数据库连接