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,数据库连接