引言
Android开发是一个充满挑战但回报丰厚的领域。对于初学者来说,从零开始学习Android编程可能会感到不知所措。本文旨在通过详细的实例分析和实战指南,帮助你从零开始掌握Android开发的核心技能,并提供常见问题的解决方案。我们将通过一个完整的项目实例,逐步讲解Android开发的各个方面,包括环境搭建、UI设计、数据处理、网络请求、性能优化等。
第一部分:环境搭建与项目创建
1.1 安装Android Studio
Android Studio是Google官方推荐的Android开发IDE。以下是安装步骤:
- 下载Android Studio:访问Android Studio官网下载最新版本。
- 安装:运行安装程序,按照向导完成安装。确保勾选”Android SDK”和”Android Virtual Device”。
- 配置SDK:首次启动时,Android Studio会引导你配置Android SDK。选择默认路径或自定义路径,建议使用默认路径。
- 验证安装:创建一个新的空白项目,运行模拟器或连接真机测试。
1.2 创建第一个Android项目
- 打开Android Studio,点击”New Project”。
- 选择”Empty Activity”模板。
- 配置项目信息:
- Name: HelloWorld
- Package name: com.example.helloworld
- Save location: 选择项目保存路径
- Language: Java或Kotlin(推荐Kotlin)
- Minimum SDK: 选择API 21(Android 5.0)或更高
- 点击”Finish”,等待项目构建完成。
1.3 项目结构解析
一个典型的Android项目包含以下主要目录:
app/
├── src/
│ ├── main/
│ │ ├── java/ # Java/Kotlin源代码
│ │ ├── res/ # 资源文件
│ │ │ ├── layout/ # XML布局文件
│ │ │ ├── values/ # 字符串、颜色等资源
│ │ │ └── drawable/ # 图片资源
│ │ └── AndroidManifest.xml # 应用清单文件
│ └── test/ # 单元测试
├── build.gradle # 模块级构建配置
└── gradle.properties # Gradle属性配置
第二部分:UI设计与布局
2.1 布局基础
Android提供了多种布局方式,最常用的是ConstraintLayout和LinearLayout。
示例:使用ConstraintLayout创建登录界面
activity_login.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".LoginActivity">
<EditText
android:id="@+id/etUsername"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="用户名"
android:inputType="text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginTop="100dp"
android:layout_marginHorizontal="32dp" />
<EditText
android:id="@+id/etPassword"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="密码"
android:inputType="textPassword"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/etUsername"
android:layout_marginTop="16dp"
android:layout_marginHorizontal="32dp" />
<Button
android:id="@+id/btnLogin"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="登录"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/etPassword"
android:layout_marginTop="32dp"
android:layout_marginHorizontal="32dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
2.2 自定义View
当系统提供的控件无法满足需求时,可以创建自定义View。
示例:创建一个简单的圆形进度条
CircularProgressBar.java:
package com.example.helloworld;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
public class CircularProgressBar extends View {
private Paint backgroundPaint;
private Paint progressPaint;
private RectF arcRect;
private float progress = 0;
private float maxProgress = 100;
public CircularProgressBar(Context context) {
super(context);
init();
}
public CircularProgressBar(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CircularProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
backgroundPaint = new Paint();
backgroundPaint.setColor(Color.LTGRAY);
backgroundPaint.setStyle(Paint.Style.STROKE);
backgroundPaint.setStrokeWidth(20);
backgroundPaint.setAntiAlias(true);
progressPaint = new Paint();
progressPaint.setColor(Color.BLUE);
progressPaint.setStyle(Paint.Style.STROKE);
progressPaint.setStrokeWidth(20);
progressPaint.setAntiAlias(true);
progressPaint.setStrokeCap(Paint.Cap.ROUND);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
int padding = 20;
arcRect = new RectF(padding, padding, w - padding, h - padding);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 绘制背景圆环
canvas.drawArc(arcRect, 0, 360, false, backgroundPaint);
// 绘制进度圆环
float sweepAngle = (progress / maxProgress) * 360;
canvas.drawArc(arcRect, -90, sweepAngle, false, progressPaint);
}
public void setProgress(float progress) {
this.progress = progress;
invalidate(); // 重绘View
}
public void setMaxProgress(float maxProgress) {
this.maxProgress = maxProgress;
}
}
在布局中使用自定义View:
<com.example.helloworld.CircularProgressBar
android:id="@+id/circularProgressBar"
android:layout_width="200dp"
android:layout_height="200dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
第三部分:Activity与Fragment
3.1 Activity生命周期
Activity的生命周期方法包括:
onCreate(): Activity被创建时调用onStart(): Activity变为可见时调用onResume(): Activity获得焦点时调用onPause(): Activity失去焦点时调用onStop(): Activity不再可见时调用onDestroy(): Activity被销毁时调用
示例:生命周期方法日志记录
MainActivity.java:
package com.example.helloworld;
import android.os.Bundle;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "Lifecycle";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d(TAG, "onCreate: Activity创建");
}
@Override
protected void onStart() {
super.onStart();
Log.d(TAG, "onStart: Activity开始可见");
}
@Override
protected void onResume() {
super.onResume();
Log.d(TAG, "onResume: Activity获得焦点");
}
@Override
protected void onPause() {
super.onPause();
Log.d(TAG, "onPause: Activity失去焦点");
}
@Override
protected void onStop() {
super.onStop();
Log.d(TAG, "onStop: Activity不再可见");
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy: Activity被销毁");
}
}
3.2 Fragment的使用
Fragment是可复用的UI模块,可以在多个Activity中使用。
示例:创建一个简单的Fragment
BlankFragment.java:
package com.example.helloworld;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
public class BlankFragment extends Fragment {
private static final String ARG_PARAM1 = "param1";
private String mParam1;
public BlankFragment() {
// Required empty public constructor
}
public static BlankFragment newInstance(String param1) {
BlankFragment fragment = new BlankFragment();
Bundle args = new Bundle();
args.putString(ARG_PARAM1, param1);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
mParam1 = getArguments().getString(ARG_PARAM1);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_blank, container, false);
TextView textView = view.findViewById(R.id.textView);
textView.setText(mParam1);
return view;
}
}
fragment_blank.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".BlankFragment">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Fragment内容"
android:textSize="18sp" />
</LinearLayout>
在Activity中使用Fragment:
// 在MainActivity的onCreate方法中
getSupportFragmentManager().beginTransaction()
.replace(R.id.fragment_container, BlankFragment.newInstance("Hello Fragment"))
.commit();
第四部分:数据存储
4.1 SharedPreferences
SharedPreferences是Android中最简单的数据存储方式,适合存储少量键值对数据。
示例:使用SharedPreferences保存用户偏好设置
SettingsManager.java:
package com.example.helloworld;
import android.content.Context;
import android.content.SharedPreferences;
public class SettingsManager {
private static final String PREF_NAME = "app_settings";
private static final String KEY_USERNAME = "username";
private static final String KEY_THEME = "theme";
private static final String KEY_NOTIFICATIONS = "notifications";
private SharedPreferences sharedPreferences;
public SettingsManager(Context context) {
sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
}
public void saveUsername(String username) {
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(KEY_USERNAME, username);
editor.apply(); // 异步提交
}
public String getUsername() {
return sharedPreferences.getString(KEY_USERNAME, "Guest");
}
public void saveTheme(String theme) {
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(KEY_THEME, theme);
editor.apply();
}
public String getTheme() {
return sharedPreferences.getString(KEY_THEME, "Light");
}
public void saveNotificationsEnabled(boolean enabled) {
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean(KEY_NOTIFICATIONS, enabled);
editor.apply();
}
public boolean areNotificationsEnabled() {
return sharedPreferences.getBoolean(KEY_NOTIFICATIONS, true);
}
}
4.2 SQLite数据库
对于复杂数据,SQLite是Android内置的关系型数据库。
示例:创建一个简单的用户数据库
UserDatabaseHelper.java:
package com.example.helloworld;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import java.util.ArrayList;
import java.util.List;
public class UserDatabaseHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "users.db";
private static final int DATABASE_VERSION = 1;
private static final String TABLE_USERS = "users";
private static final String COLUMN_ID = "id";
private static final String COLUMN_NAME = "name";
private static final String COLUMN_EMAIL = "email";
private static final String COLUMN_AGE = "age";
public UserDatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
String CREATE_USERS_TABLE = "CREATE TABLE " + TABLE_USERS + "("
+ COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ COLUMN_NAME + " TEXT,"
+ COLUMN_EMAIL + " TEXT,"
+ COLUMN_AGE + " INTEGER" + ")";
db.execSQL(CREATE_USERS_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_USERS);
onCreate(db);
}
public void addUser(User user) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(COLUMN_NAME, user.getName());
values.put(COLUMN_EMAIL, user.getEmail());
values.put(COLUMN_AGE, user.getAge());
db.insert(TABLE_USERS, null, values);
db.close();
}
public User getUser(int id) {
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor = db.query(TABLE_USERS,
new String[]{COLUMN_ID, COLUMN_NAME, COLUMN_EMAIL, COLUMN_AGE},
COLUMN_ID + "=?",
new String[]{String.valueOf(id)},
null, null, null);
if (cursor != null) {
cursor.moveToFirst();
User user = new User(
cursor.getInt(0),
cursor.getString(1),
cursor.getString(2),
cursor.getInt(3)
);
cursor.close();
return user;
}
return null;
}
public List<User> getAllUsers() {
List<User> userList = new ArrayList<>();
String selectQuery = "SELECT * FROM " + TABLE_USERS;
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor = db.rawQuery(selectQuery, null);
if (cursor.moveToFirst()) {
do {
User user = new User(
cursor.getInt(0),
cursor.getString(1),
cursor.getString(2),
cursor.getInt(3)
);
userList.add(user);
} while (cursor.moveToNext());
}
cursor.close();
return userList;
}
public int updateUser(User user) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(COLUMN_NAME, user.getName());
values.put(COLUMN_EMAIL, user.getEmail());
values.put(COLUMN_AGE, user.getAge());
return db.update(TABLE_USERS, values, COLUMN_ID + "=?",
new String[]{String.valueOf(user.getId())});
}
public void deleteUser(int id) {
SQLiteDatabase db = this.getWritableDatabase();
db.delete(TABLE_USERS, COLUMN_ID + "=?",
new String[]{String.valueOf(id)});
db.close();
}
}
User.java (实体类):
package com.example.helloworld;
public class User {
private int id;
private String name;
private String email;
private int age;
public User() {}
public User(int id, String name, String email, int age) {
this.id = id;
this.name = name;
this.email = email;
this.age = age;
}
// Getters and Setters
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
}
4.3 Room数据库(推荐)
Room是Google推荐的SQLite抽象层,提供了更简洁的API和编译时检查。
示例:使用Room实现用户数据库
User.java (实体类):
package com.example.helloworld;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
@Entity(tableName = "users")
public class User {
@PrimaryKey(autoGenerate = true)
private int id;
private String name;
private String email;
private int age;
// Getters and Setters
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
}
UserDao.java (数据访问对象):
package com.example.helloworld;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
import java.util.List;
@Dao
public interface UserDao {
@Insert
void insert(User user);
@Update
void update(User user);
@Delete
void delete(User user);
@Query("SELECT * FROM users")
List<User> getAllUsers();
@Query("SELECT * FROM users WHERE id = :id")
User getUserById(int id);
@Query("DELETE FROM users")
void deleteAll();
}
AppDatabase.java (数据库类):
package com.example.helloworld;
import androidx.room.Database;
import androidx.room.RoomDatabase;
@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
在Application类中初始化数据库:
package com.example.helloworld;
import android.app.Application;
import androidx.room.Room;
public class MyApp extends Application {
private static AppDatabase database;
@Override
public void onCreate() {
super.onCreate();
database = Room.databaseBuilder(getApplicationContext(),
AppDatabase.class, "user-database").build();
}
public static AppDatabase getDatabase() {
return database;
}
}
第五部分:网络请求
5.1 使用Retrofit进行网络请求
Retrofit是Square公司开发的HTTP客户端,是Android开发中最流行的网络库之一。
示例:使用Retrofit获取GitHub用户信息
添加依赖 (build.gradle):
dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.1'
}
创建数据模型:
package com.example.helloworld;
import com.google.gson.annotations.SerializedName;
public class GitHubUser {
@SerializedName("login")
private String login;
@SerializedName("id")
private int id;
@SerializedName("avatar_url")
private String avatarUrl;
@SerializedName("name")
private String name;
@SerializedName("public_repos")
private int publicRepos;
// Getters and Setters
public String getLogin() { return login; }
public void setLogin(String login) { this.login = login; }
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getAvatarUrl() { return avatarUrl; }
public void setAvatarUrl(String avatarUrl) { this.avatarUrl = avatarUrl; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getPublicRepos() { return publicRepos; }
public void setPublicRepos(int publicRepos) { this.publicRepos = publicRepos; }
}
创建API接口:
package com.example.helloworld;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Path;
public interface GitHubApi {
@GET("users/{username}")
Call<GitHubUser> getUser(@Path("username") String username);
}
创建Retrofit实例:
package com.example.helloworld;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class RetrofitClient {
private static final String BASE_URL = "https://api.github.com/";
private static Retrofit retrofit = null;
public static Retrofit getClient() {
if (retrofit == null) {
// 添加日志拦截器
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.build();
retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build();
}
return retrofit;
}
}
在Activity中使用:
package com.example.helloworld;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.textView);
fetchGitHubUser("octocat");
}
private void fetchGitHubUser(String username) {
GitHubApi api = RetrofitClient.getClient().create(GitHubApi.class);
Call<GitHubUser> call = api.getUser(username);
call.enqueue(new Callback<GitHubUser>() {
@Override
public void onResponse(Call<GitHubUser> call, Response<GitHubUser> response) {
if (response.isSuccessful() && response.body() != null) {
GitHubUser user = response.body();
String result = String.format(
"用户名: %s\nID: %d\n姓名: %s\n公开仓库数: %d",
user.getLogin(), user.getId(), user.getName(), user.getPublicRepos()
);
textView.setText(result);
Log.d(TAG, "用户信息: " + result);
} else {
Log.e(TAG, "请求失败: " + response.code());
textView.setText("请求失败: " + response.code());
}
}
@Override
public void onFailure(Call<GitHubUser> call, Throwable t) {
Log.e(TAG, "网络错误: " + t.getMessage());
textView.setText("网络错误: " + t.getMessage());
}
});
}
}
5.2 处理网络错误和超时
示例:添加超时和错误处理
修改RetrofitClient:
package com.example.helloworld;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import java.util.concurrent.TimeUnit;
public class RetrofitClient {
private static final String BASE_URL = "https://api.github.com/";
private static Retrofit retrofit = null;
public static Retrofit getClient() {
if (retrofit == null) {
// 添加日志拦截器
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
// 配置超时和重试
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.connectTimeout(30, TimeUnit.SECONDS) // 连接超时
.readTimeout(30, TimeUnit.SECONDS) // 读取超时
.writeTimeout(30, TimeUnit.SECONDS) // 写入超时
.retryOnConnectionFailure(true) // 连接失败重试
.build();
retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build();
}
return retrofit;
}
}
第六部分:常见问题解决方案
6.1 内存泄漏
问题描述:Activity被销毁后,由于某些引用未被释放,导致内存无法回收。
解决方案:
- 避免静态引用Context: “`java // 错误示例 private static Context context; // 可能导致内存泄漏
// 正确示例 private Context context; // 使用非静态引用
2. **及时取消异步任务**:
```java
@Override
protected void onDestroy() {
super.onDestroy();
// 取消所有网络请求
if (call != null && !call.isCanceled()) {
call.cancel();
}
// 取消所有动画
if (animator != null) {
animator.cancel();
}
}
使用弱引用:
private static class MyHandler extends Handler { private final WeakReference<MainActivity> weakActivity; public MyHandler(MainActivity activity) { weakActivity = new WeakReference<>(activity); } @Override public void handleMessage(Message msg) { MainActivity activity = weakActivity.get(); if (activity != null) { // 处理消息 } } }
6.2 ANR(应用无响应)
问题描述:应用在主线程执行耗时操作,导致界面卡顿或无响应。
解决方案:
使用异步任务:
// 使用AsyncTask(已废弃,但原理相同) private class DownloadTask extends AsyncTask<String, Integer, String> { @Override protected String doInBackground(String... urls) { // 在后台线程执行耗时操作 return downloadData(urls[0]); } @Override protected void onPostExecute(String result) { // 在主线程更新UI updateUI(result); } }使用协程(Kotlin):
// 在Kotlin中使用协程 private fun fetchData() { lifecycleScope.launch { try { val data = withContext(Dispatchers.IO) { // 在IO线程执行网络请求 apiService.getData() } // 在主线程更新UI updateUI(data) } catch (e: Exception) { // 处理错误 } } }使用RxJava:
// 使用RxJava进行异步处理 Observable.fromCallable(() -> { // 耗时操作 return fetchData(); }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(data -> { // 更新UI updateUI(data); }, error -> { // 处理错误 });
6.3 网络请求失败
问题描述:网络请求失败,可能由于网络问题、服务器错误或配置错误。
解决方案:
检查网络权限:
<!-- AndroidManifest.xml --> <uses-permission android:name="android.permission.INTERNET" />添加网络状态检查:
public boolean isNetworkAvailable(Context context) { ConnectivityManager cm = (ConnectivityManager) context .getSystemService(Context.CONNECTIVITY_SERVICE); if (cm != null) { NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); return activeNetwork != null && activeNetwork.isConnectedOrConnecting(); } return false; }使用OkHttp的拦截器处理错误:
public class ErrorInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); Response response = chain.proceed(request); if (!response.isSuccessful()) { // 处理错误响应 String errorBody = response.body().string(); throw new IOException("HTTP " + response.code() + ": " + errorBody); } return response; } }
6.4 布局性能问题
问题描述:布局嵌套过深导致渲染性能下降。
解决方案:
- 使用ConstraintLayout减少嵌套:
“`xml
<TextView
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" />
2. **使用ViewStub延迟加载**:
```xml
<ViewStub
android:id="@+id/stub"
android:layout="@layout/complex_layout"
android:inflatedId="@+id/inflated_view" />
- 使用RecyclerView替代ListView:
// RecyclerView更高效,支持局部刷新 RecyclerView recyclerView = findViewById(R.id.recyclerView); recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.setAdapter(new MyAdapter(dataList));
6.5 权限处理问题
问题描述:Android 6.0+需要动态请求权限。
解决方案:
检查和请求权限:
private void checkPermissions() { String[] permissions = { Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE }; List<String> permissionsToRequest = new ArrayList<>(); for (String permission : permissions) { if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { permissionsToRequest.add(permission); } } if (!permissionsToRequest.isEmpty()) { ActivityCompat.requestPermissions(this, permissionsToRequest.toArray(new String[0]), REQUEST_CODE_PERMISSIONS); } }处理权限请求结果:
@Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == REQUEST_CODE_PERMISSIONS) { boolean allGranted = true; for (int result : grantResults) { if (result != PackageManager.PERMISSION_GRANTED) { allGranted = false; break; } } if (allGranted) { // 权限已授予,执行相关操作 startCamera(); } else { // 权限被拒绝,显示提示 showPermissionDeniedDialog(); } } }
第七部分:性能优化
7.1 内存优化
使用LeakCanary检测内存泄漏:
// build.gradle debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'优化图片加载:
// 使用Glide加载图片 Glide.with(context) .load(imageUrl) .override(200, 200) // 限制尺寸 .centerCrop() .into(imageView);
7.2 网络优化
使用缓存:
// 添加缓存拦截器 public class CacheInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); Response response = chain.proceed(request); // 缓存策略 return response.newBuilder() .header("Cache-Control", "public, max-age=60") .removeHeader("Pragma") .build(); } }使用HTTP/2:
// OkHttp默认支持HTTP/2,确保使用最新版本 implementation 'com.squareup.okhttp3:okhttp:4.9.1'
7.3 UI优化
使用ViewHolder模式:
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> { private List<String> dataList; public static class ViewHolder extends RecyclerView.ViewHolder { TextView textView; public ViewHolder(View itemView) { super(itemView); textView = itemView.findViewById(R.id.textView); } } @Override public void onBindViewHolder(ViewHolder holder, int position) { holder.textView.setText(dataList.get(position)); } }使用DiffUtil优化列表更新:
// 在Adapter中使用DiffUtil public void updateData(List<String> newData) { DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff( new MyDiffCallback(dataList, newData)); dataList = newData; diffResult.dispatchUpdatesTo(this); }
第八部分:实战项目:天气应用
8.1 项目概述
我们将创建一个简单的天气应用,展示从网络获取的天气数据。
8.2 项目结构
WeatherApp/
├── app/
│ ├── src/
│ │ ├── main/
│ │ │ ├── java/com/example/weatherapp/
│ │ │ │ ├── MainActivity.java
│ │ │ │ ├── WeatherAdapter.java
│ │ │ │ ├── WeatherData.java
│ │ │ │ ├── WeatherApi.java
│ │ │ │ └── RetrofitClient.java
│ │ │ ├── res/
│ │ │ │ ├── layout/
│ │ │ │ │ ├── activity_main.xml
│ │ │ │ │ └── item_weather.xml
│ │ │ │ └── values/
│ │ │ │ └── strings.xml
│ │ │ └── AndroidManifest.xml
│ │ └── test/
│ └── build.gradle
└── build.gradle
8.3 实现步骤
8.3.1 添加依赖
build.gradle (Module: app):
dependencies {
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// RecyclerView
implementation 'androidx.recyclerview:recyclerview:1.2.1'
// Glide for image loading
implementation 'com.github.bumptech.glide:glide:4.12.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
}
8.3.2 创建数据模型
WeatherData.java:
package com.example.weatherapp;
import com.google.gson.annotations.SerializedName;
import java.util.List;
public class WeatherData {
@SerializedName("main")
private Main main;
@SerializedName("weather")
private List<Weather> weather;
@SerializedName("name")
private String name;
@SerializedName("dt")
private long dt;
public Main getMain() { return main; }
public List<Weather> getWeather() { return weather; }
public String getName() { return name; }
public long getDt() { return dt; }
public static class Main {
@SerializedName("temp")
private double temp;
@SerializedName("humidity")
private int humidity;
@SerializedName("pressure")
private int pressure;
public double getTemp() { return temp; }
public int getHumidity() { return humidity; }
public int getPressure() { return pressure; }
}
public static class Weather {
@SerializedName("description")
private String description;
@SerializedName("icon")
private String icon;
public String getDescription() { return description; }
public String getIcon() { return icon; }
}
}
8.3.3 创建API接口
WeatherApi.java:
package com.example.weatherapp;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Query;
public interface WeatherApi {
@GET("weather")
Call<WeatherData> getCurrentWeather(
@Query("q") String city,
@Query("appid") String apiKey,
@Query("units") String units
);
}
8.3.4 创建Retrofit客户端
RetrofitClient.java:
package com.example.weatherapp;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class RetrofitClient {
private static final String BASE_URL = "https://api.openweathermap.org/data/2.5/";
private static Retrofit retrofit = null;
public static Retrofit getClient() {
if (retrofit == null) {
retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
}
return retrofit;
}
}
8.3.5 创建适配器
WeatherAdapter.java:
package com.example.weatherapp;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import java.util.List;
public class WeatherAdapter extends RecyclerView.Adapter<WeatherAdapter.ViewHolder> {
private List<WeatherData> weatherDataList;
public WeatherAdapter(List<WeatherData> weatherDataList) {
this.weatherDataList = weatherDataList;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_weather, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
WeatherData data = weatherDataList.get(position);
holder.cityName.setText(data.getName());
holder.temperature.setText(String.format("%.1f°C", data.getMain().getTemp()));
holder.description.setText(data.getWeather().get(0).getDescription());
// 加载天气图标
String iconUrl = "https://openweathermap.org/img/wn/" +
data.getWeather().get(0).getIcon() + "@2x.png";
Glide.with(holder.itemView.getContext())
.load(iconUrl)
.into(holder.weatherIcon);
}
@Override
public int getItemCount() {
return weatherDataList.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
TextView cityName;
TextView temperature;
TextView description;
ImageView weatherIcon;
public ViewHolder(@NonNull View itemView) {
super(itemView);
cityName = itemView.findViewById(R.id.cityName);
temperature = itemView.findViewById(R.id.temperature);
description = itemView.findViewById(R.id.description);
weatherIcon = itemView.findViewById(R.id.weatherIcon);
}
}
}
8.3.6 创建布局文件
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<EditText
android:id="@+id/etCity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="输入城市名称"
android:padding="16dp"
android:layout_margin="16dp" />
<Button
android:id="@+id/btnSearch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="查询天气"
android:layout_marginHorizontal="16dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="16dp" />
</LinearLayout>
item_weather.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp">
<ImageView
android:id="@+id/weatherIcon"
android:layout_width="60dp"
android:layout_height="60dp"
android:src="@mipmap/ic_launcher" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="16dp">
<TextView
android:id="@+id/cityName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="城市名称"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/temperature"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="25°C"
android:textSize="16sp" />
<TextView
android:id="@+id/description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="晴天"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
8.3.7 实现MainActivity
MainActivity.java:
package com.example.weatherapp;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class MainActivity extends AppCompatActivity {
private EditText etCity;
private Button btnSearch;
private RecyclerView recyclerView;
private WeatherAdapter adapter;
private List<WeatherData> weatherDataList;
private static final String API_KEY = "YOUR_API_KEY"; // 替换为你的API密钥
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initViews();
setupRecyclerView();
setupListeners();
}
private void initViews() {
etCity = findViewById(R.id.etCity);
btnSearch = findViewById(R.id.btnSearch);
recyclerView = findViewById(R.id.recyclerView);
}
private void setupRecyclerView() {
weatherDataList = new ArrayList<>();
adapter = new WeatherAdapter(weatherDataList);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(adapter);
}
private void setupListeners() {
btnSearch.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String city = etCity.getText().toString().trim();
if (city.isEmpty()) {
Toast.makeText(MainActivity.this, "请输入城市名称", Toast.LENGTH_SHORT).show();
return;
}
fetchWeatherData(city);
}
});
}
private void fetchWeatherData(String city) {
WeatherApi api = RetrofitClient.getClient().create(WeatherApi.class);
Call<WeatherData> call = api.getCurrentWeather(city, API_KEY, "metric");
call.enqueue(new Callback<WeatherData>() {
@Override
public void onResponse(Call<WeatherData> call, Response<WeatherData> response) {
if (response.isSuccessful() && response.body() != null) {
WeatherData data = response.body();
weatherDataList.add(data);
adapter.notifyItemInserted(weatherDataList.size() - 1);
recyclerView.scrollToPosition(weatherDataList.size() - 1);
} else {
Toast.makeText(MainActivity.this,
"查询失败: " + response.code(), Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<WeatherData> call, Throwable t) {
Toast.makeText(MainActivity.this,
"网络错误: " + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
}
8.3.8 添加网络权限
AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.weatherapp">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.Light.DarkActionBar">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
8.4 项目优化
8.4.1 添加加载状态
修改MainActivity:
private void fetchWeatherData(String city) {
// 显示加载状态
showLoading();
WeatherApi api = RetrofitClient.getClient().create(WeatherApi.class);
Call<WeatherData> call = api.getCurrentWeather(city, API_KEY, "metric");
call.enqueue(new Callback<WeatherData>() {
@Override
public void onResponse(Call<WeatherData> call, Response<WeatherData> response) {
hideLoading();
if (response.isSuccessful() && response.body() != null) {
WeatherData data = response.body();
weatherDataList.add(data);
adapter.notifyItemInserted(weatherDataList.size() - 1);
recyclerView.scrollToPosition(weatherDataList.size() - 1);
} else {
Toast.makeText(MainActivity.this,
"查询失败: " + response.code(), Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<WeatherData> call, Throwable t) {
hideLoading();
Toast.makeText(MainActivity.this,
"网络错误: " + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
private void showLoading() {
// 显示加载进度条
// 可以使用ProgressBar或自定义加载视图
}
private void hideLoading() {
// 隐藏加载进度条
}
8.4.2 添加错误处理
创建错误处理工具类:
package com.example.weatherapp;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.widget.Toast;
public class NetworkUtils {
public static boolean isNetworkAvailable(Context context) {
ConnectivityManager cm = (ConnectivityManager) context
.getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm != null) {
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
return activeNetwork != null && activeNetwork.isConnectedOrConnecting();
}
return false;
}
public static void showNetworkError(Context context) {
Toast.makeText(context, "网络不可用,请检查网络连接", Toast.LENGTH_SHORT).show();
}
}
修改MainActivity:
private void fetchWeatherData(String city) {
if (!NetworkUtils.isNetworkAvailable(this)) {
NetworkUtils.showNetworkError(this);
return;
}
// 继续执行网络请求...
}
第九部分:进阶主题
9.1 Jetpack组件
9.1.1 ViewModel
ViewModel用于管理UI相关的数据,生命周期更长。
示例:使用ViewModel管理天气数据:
package com.example.weatherapp;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import java.util.List;
public class WeatherViewModel extends ViewModel {
private MutableLiveData<List<WeatherData>> weatherDataList = new MutableLiveData<>();
private MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
private MutableLiveData<String> errorMessage = new MutableLiveData<>();
public LiveData<List<WeatherData>> getWeatherData() {
return weatherDataList;
}
public LiveData<Boolean> getIsLoading() {
return isLoading;
}
public LiveData<String> getErrorMessage() {
return errorMessage;
}
public void fetchWeatherData(String city, String apiKey) {
isLoading.setValue(true);
// 网络请求逻辑
// ...
isLoading.setValue(false);
}
}
9.1.2 LiveData
LiveData是可观察的数据持有者,具有生命周期感知能力。
示例:在Activity中使用LiveData:
public class MainActivity extends AppCompatActivity {
private WeatherViewModel viewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
viewModel = new ViewModelProvider(this).get(WeatherViewModel.class);
// 观察数据变化
viewModel.getWeatherData().observe(this, weatherDataList -> {
if (weatherDataList != null) {
adapter.setData(weatherDataList);
}
});
viewModel.getIsLoading().observe(this, isLoading -> {
if (isLoading) {
showLoading();
} else {
hideLoading();
}
});
viewModel.getErrorMessage().observe(this, errorMessage -> {
if (errorMessage != null && !errorMessage.isEmpty()) {
Toast.makeText(this, errorMessage, Toast.LENGTH_SHORT).show();
}
});
}
}
9.2 Kotlin协程
Kotlin协程是Android开发中处理异步操作的推荐方式。
示例:使用协程进行网络请求
添加依赖:
dependencies {
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0'
}
修改WeatherApi接口:
interface WeatherApi {
@GET("weather")
suspend fun getCurrentWeather(
@Query("q") city: String,
@Query("appid") apiKey: String,
@Query("units") units: String
): WeatherData
}
修改MainActivity为Kotlin:
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: WeatherViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProvider(this).get(WeatherViewModel::class.java)
// 使用协程进行网络请求
lifecycleScope.launch {
try {
val weatherData = withContext(Dispatchers.IO) {
// 在IO线程执行网络请求
api.getCurrentWeather(city, API_KEY, "metric")
}
// 在主线程更新UI
updateUI(weatherData)
} catch (e: Exception) {
// 处理错误
showError(e.message)
}
}
}
}
9.3 依赖注入
9.3.1 Dagger Hilt
Dagger Hilt是Google推荐的依赖注入框架。
添加依赖:
// build.gradle (Project)
buildscript {
dependencies {
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.38.1'
}
}
// build.gradle (Module: app)
plugins {
id 'dagger.hilt.android.plugin'
}
dependencies {
implementation 'com.google.dagger:hilt-android:2.38.1'
annotationProcessor 'com.google.dagger:hilt-compiler:2.38.1'
}
创建Application类:
@HiltAndroidApp
public class MyApp extends Application {
}
创建Module:
@Module
@InstallIn(SingletonComponent.class)
public class NetworkModule {
@Provides
@Singleton
public Retrofit provideRetrofit() {
return new Retrofit.Builder()
.baseUrl("https://api.openweathermap.org/data/2.5/")
.addConverterFactory(GsonConverterFactory.create())
.build();
}
@Provides
@Singleton
public WeatherApi provideWeatherApi(Retrofit retrofit) {
return retrofit.create(WeatherApi.class);
}
}
在Activity中使用:
@AndroidEntryPoint
public class MainActivity extends AppCompatActivity {
@Inject
WeatherApi weatherApi;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 现在可以直接使用weatherApi
}
}
第十部分:发布与测试
10.1 单元测试
示例:测试UserDatabaseHelper
UserDatabaseHelperTest.java:
package com.example.helloworld;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.List;
import static org.junit.Assert.*;
@RunWith(AndroidJUnit4.class)
public class UserDatabaseHelperTest {
private UserDatabaseHelper dbHelper;
private Context context;
@Before
public void setUp() {
context = ApplicationProvider.getApplicationContext();
dbHelper = new UserDatabaseHelper(context);
}
@After
public void tearDown() {
dbHelper.close();
}
@Test
public void testAddUser() {
User user = new User(0, "Test User", "test@example.com", 25);
dbHelper.addUser(user);
List<User> users = dbHelper.getAllUsers();
assertFalse(users.isEmpty());
assertEquals("Test User", users.get(0).getName());
}
@Test
public void testGetUser() {
User user = new User(0, "Test User", "test@example.com", 25);
dbHelper.addUser(user);
User retrievedUser = dbHelper.getUser(1);
assertNotNull(retrievedUser);
assertEquals("Test User", retrievedUser.getName());
}
@Test
public void testUpdateUser() {
User user = new User(0, "Test User", "test@example.com", 25);
dbHelper.addUser(user);
user.setName("Updated User");
user.setEmail("updated@example.com");
user.setAge(30);
int rowsAffected = dbHelper.updateUser(user);
assertEquals(1, rowsAffected);
User updatedUser = dbHelper.getUser(1);
assertEquals("Updated User", updatedUser.getName());
assertEquals("updated@example.com", updatedUser.getEmail());
assertEquals(30, updatedUser.getAge());
}
@Test
public void testDeleteUser() {
User user = new User(0, "Test User", "test@example.com", 25);
dbHelper.addUser(user);
dbHelper.deleteUser(1);
User deletedUser = dbHelper.getUser(1);
assertNull(deletedUser);
}
}
10.2 UI测试
示例:使用Espresso测试登录界面
LoginActivityTest.java:
package com.example.helloworld;
import androidx.test.core.app.ActivityScenario;
import androidx.test.espresso.Espresso;
import androidx.test.espresso.action.ViewActions;
import androidx.test.espresso.assertion.ViewAssertions;
import androidx.test.espresso.matcher.ViewMatchers;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
@RunWith(AndroidJUnit4.class)
public class LoginActivityTest {
@Test
public void testLoginSuccess() {
ActivityScenario<LoginActivity> scenario = ActivityScenario.launch(LoginActivity.class);
// 输入用户名
onView(withId(R.id.etUsername))
.perform(ViewActions.typeText("testuser"));
// 输入密码
onView(withId(R.id.etPassword))
.perform(ViewActions.typeText("password123"));
// 点击登录按钮
onView(withId(R.id.btnLogin))
.perform(ViewActions.click());
// 验证登录成功后的界面
onView(withText("登录成功"))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()));
scenario.close();
}
@Test
public void testEmptyFields() {
ActivityScenario<LoginActivity> scenario = ActivityScenario.launch(LoginActivity.class);
// 直接点击登录按钮
onView(withId(R.id.btnLogin))
.perform(ViewActions.click());
// 验证错误提示
onView(withText("用户名不能为空"))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()));
scenario.close();
}
}
10.3 应用发布
10.3.1 生成签名密钥
# 使用keytool生成签名密钥
keytool -genkey -v -keystore my-release-key.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000
10.3.2 配置签名
gradle.properties:
MYAPP_RELEASE_STORE_FILE=my-release-key.keystore
MYAPP_RELEASE_KEY_ALIAS=my-key-alias
MYAPP_RELEASE_STORE_PASSWORD=your_password
MYAPP_RELEASE_KEY_PASSWORD=your_password
build.gradle (Module: app):
android {
signingConfigs {
release {
storeFile file(MYAPP_RELEASE_STORE_FILE)
storePassword MYAPP_RELEASE_STORE_PASSWORD
keyAlias MYAPP_RELEASE_KEY_ALIAS
keyPassword MYAPP_RELEASE_KEY_PASSWORD
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
10.3.3 生成APK/AAB
- 在Android Studio中,点击”Build” → “Generate Signed Bundle / APK”
- 选择”Android App Bundle”(推荐)或”APK”
- 选择签名配置
- 点击”Finish”生成发布包
总结
本文通过详细的实例分析和实战指南,从零开始介绍了Android开发的各个方面。我们涵盖了环境搭建、UI设计、数据存储、网络请求、常见问题解决方案、性能优化、实战项目以及进阶主题。通过学习和实践这些内容,你可以逐步掌握Android开发的核心技能,并能够独立开发功能完整的Android应用。
记住,Android开发是一个持续学习的过程。随着技术的不断发展,新的工具和框架不断涌现。保持好奇心,不断实践,参与开源项目,阅读官方文档,都是提升技能的好方法。祝你在Android开发的道路上取得成功!
