mcpField
A
android-java

android-java

github.com/alinaqi开发711669 次安装2 周前更新
代码仓库

关于 android-java

android-java 是面向 Android Java 开发者的实践指南,涵盖项目结构、架构模式、界面组件和测试。它还整理了常见安全做法与反模式,帮助开发者构建和维护应用。

主要功能

  1. 01Android 项目结构与 Gradle 配置
  2. 02MVVM 与 Repository 模式
  3. 03Activity 和 Fragment 的 ViewBinding
  4. 04JUnit、Mockito 与 Espresso 测试
  5. 05Android 常见反模式与安全实践

适用场景

  1. 01规划 Android Java 应用的分层结构
  2. 02为 Activity 和 Fragment 设计界面代码
  3. 03编写 ViewModel 单元测试与界面测试

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/alinaqi/maggy --skill android-java

安装后在 Claude Code 中输入/android-java确认已加载。

详细介绍

Android Java 技能


项目结构

project/
├── app/
│   ├── src/
│   │   ├── main/
│   │   │   ├── java/com/example/app/
│   │   │   │   ├── data/           # Data layer
│   │   │   │   │   ├── local/      # Room database, SharedPreferences
│   │   │   │   │   ├── remote/     # Retrofit services, API clients
│   │   │   │   │   └── repository/ # Repository implementations
│   │   │   │   ├── di/             # Dependency injection (Hilt/Dagger)
│   │   │   │   ├── domain/         # Business logic
│   │   │   │   │   ├── model/      # Domain models
│   │   │   │   │   ├── repository/ # Repository interfaces
│   │   │   │   │   └── usecase/    # Use cases
│   │   │   │   ├── ui/             # Presentation layer
│   │   │   │   │   ├── feature/    # Feature screens
│   │   │   │   │   │   ├── FeatureActivity.java
│   │   │   │   │   │   ├── FeatureFragment.java
│   │   │   │   │   │   └── FeatureViewModel.java
│   │   │   │   │   └── common/     # Shared UI components
│   │   │   │   └── App.java        # Application class
│   │   │   ├── res/
│   │   │   │   ├── layout/
│   │   │   │   ├── values/
│   │   │   │   └── drawable/
│   │   │   └── AndroidManifest.xml
│   │   ├── test/                   # Unit tests
│   │   └── androidTest/            # Instrumentation tests
│   └── build.gradle
├── build.gradle                    # Project-level build file
├── gradle.properties
├── settings.gradle
└── CLAUDE.md

Gradle 配置

应用级 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'

    // Testing
    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 - holds UI state, survives configuration changes
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);
            }
        });
    }
}
Repository 模式
// Repository interface (domain layer)
public interface UserRepository {
    void getUser(String userId, Callback<User> callback);
    void saveUser(User user, Callback<Void> callback);
}

// Repository implementation (data layer)
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) {
        // Try cache first, then network
        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());
            }
        });
    }
}

Activity 和 Fragment 模式

使用 ViewBinding 的 Activity
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 的 Fragment
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;  // Prevent memory leaks
    }
}

测试

使用 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() {
        // Arrange
        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());

        // Act
        viewModel.loadUser("1");

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

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

        // Act
        viewModel.loadUser("1");

        // Assert
        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 lint 选项
android {
    lint {
        abortOnError true
        warningsAsErrors false
        checkReleaseBuilds true
        xmlReport true
        htmlReport true
    }
}

常见模式

空安全回调
// Define callback interface
public interface Callback<T> {
    void onSuccess(T result);
    void onError(String message);
}

// Use with null checks
public void fetchData(Callback<Data> callback) {
    if (callback == null) return;

    try {
        Data result = performFetch();
        callback.onSuccess(result);
    } catch (Exception e) {
        callback.onError(e.getMessage());
    }
}
安全使用 Context
// Use application context for long-lived objects
public class DataManager {
    private final Context appContext;

    public DataManager(Context context) {
        // Always use application context to prevent Activity leaks
        this.appContext = context.getApplicationContext();
    }
}

// Check for null context in callbacks
private void updateUI() {
    Context context = getContext();
    if (context == null || !isAdded()) return;
    // Safe to use context
}
线程安全的单例
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 反模式

  • ❌ Context 泄漏 - 切勿在静态字段或单例中持有 Activity/Fragment 引用
  • ❌ 回调中的内存泄漏 - 始终使用 WeakReference,或在 onDestroy 中清除回调
  • ❌ 在后台线程更新 UI - 更新 UI 时始终将任务提交到主线程
  • ❌ 硬编码字符串 - 所有面向用户的文本都应使用字符串资源
  • ❌ 臃肿的 Activity - 将 Activity 控制在 200 行以内,并将逻辑提取到 ViewModel 中
  • ❌ NetworkOnMainThreadException - 切勿在主线程上执行网络调用
  • ❌ 忽略生命周期 - 始终遵循 Activity/Fragment 的生命周期状态
  • ❌ 阻塞主线程 - 将主线程操作控制在 16ms 以内
  • ❌ 未处理配置更改 - 使用 ViewModel 在屏幕旋转后保留状态
  • ❌ 硬编码尺寸 - 使用 dp/sp 单位和尺寸资源
  • ❌ 视图层级过深 - 将布局深度控制在 10 层以内,并使用 ConstraintLayout
  • ❌ 未关闭资源 - 始终关闭 Cursor、InputStream 和数据库连接

评论

支持 Markdown