模块 07:网络请求与数据持久化
目标:掌握 Android 中的网络请求(OkHttp、Retrofit)和数据存储方案(SharedPreferences、SQLite、Room),学会处理异步数据流。
前端对照
| 前端 | Android | 说明 |
|---|---|---|
fetch() / axios | OkHttp / Retrofit | HTTP 客户端 |
localStorage | SharedPreferences | 简单键值存储 |
IndexedDB | SQLite / Room | 结构化本地数据库 |
JSON.parse() | Gson / Moshi | JSON 解析 |
| 请求拦截器 | OkHttp Interceptor | 请求/响应中间件 |
| CORS / Token 刷新 | Interceptor 链 | 认证处理 |
1. OkHttp:底层 HTTP 客户端
1.1 基础请求
java
// 创建 OkHttpClient(通常是单例)
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();
// GET 请求
Request request = new Request.Builder()
.url("https://api.example.com/users")
.addHeader("Authorization", "Bearer " + token)
.build();
// ⚠️ 网络请求必须在后台线程
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
// 请求失败
Log.e("API", "请求失败", e);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
String body = response.body().string();
// ⚠️ 回调在后台线程,更新 UI 需要切回主线程
runOnUiThread(() -> {
textView.setText(body);
});
}
}
});1.2 POST 请求
java
// JSON 请求体
MediaType JSON = MediaType.parse("application/json; charset=utf-8");
String jsonBody = "{\"name\":\"张三\",\"email\":\"zhangsan@example.com\"}";
RequestBody body = RequestBody.create(jsonBody, JSON);
Request request = new Request.Builder()
.url("https://api.example.com/users")
.post(body)
.build();
// 表单请求
RequestBody formBody = new FormBody.Builder()
.add("username", "zhangsan")
.add("password", "123456")
.build();
Request formRequest = new Request.Builder()
.url("https://api.example.com/login")
.post(formBody)
.build();1.3 Interceptor(拦截器)
类似前端的请求/响应中间件(axios 拦截器):
java
// 添加公共 Header(类似 axios 的请求拦截器)
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(chain -> {
Request original = chain.request();
Request request = original.newBuilder()
.header("Authorization", "Bearer " + getToken())
.header("Content-Type", "application/json")
.build();
return chain.proceed(request);
})
// 日志拦截器(调试用)
.addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
.build();2. Retrofit:声明式 API 客户端
Retrofit 是建立在 OkHttp 之上的高层封装,使用接口注解定义 API,类似前端的 tRPC 或 OpenAPI 客户端生成器。
2.1 定义 API 接口
java
// 定义 API 接口
public interface ApiService {
@GET("users")
Call<List<User>> getUsers(@Query("page") int page, @Query("limit") int limit);
@GET("users/{id}")
Call<User> getUserById(@Path("id") int userId);
@POST("users")
Call<User> createUser(@Body CreateUserRequest request);
@PUT("users/{id}")
Call<User> updateUser(@Path("id") int id, @Body UpdateUserRequest request);
@DELETE("users/{id}")
Call<Void> deleteUser(@Path("id") int id);
@POST("auth/login")
Call<LoginResponse> login(@Body LoginRequest request);
// 表单提交
@FormUrlEncoded
@POST("auth/login")
Call<LoginResponse> loginForm(
@Field("username") String username,
@Field("password") String password
);
// 文件上传
@Multipart
@POST("upload")
Call<UploadResponse> uploadFile(
@Part MultipartBody.Part file,
@Part("description") RequestBody description
);
}2.2 创建 Retrofit 实例
java
// 创建 Retrofit 实例(通常是单例)
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttpClient) // 复用 OkHttp 客户端配置
.addConverterFactory(GsonConverterFactory.create()) // JSON → Java 对象
.build();
ApiService api = retrofit.create(ApiService.class);2.3 发起请求
java
// 发起请求(回调方式)
Call<List<User>> call = api.getUsers(1, 20);
call.enqueue(new Callback<List<User>>() {
@Override
public void onResponse(Call<List<User>> call, Response<List<User>> response) {
if (response.isSuccessful()) {
List<User> users = response.body();
// 更新 UI(注意线程切换)
runOnUiThread(() -> {
adapter.submitList(users);
});
} else {
// 服务器返回错误状态码
int code = response.code();
Log.e("API", "Error: " + code);
}
}
@Override
public void onFailure(Call<List<User>> call, Throwable t) {
// 网络错误或解析错误
Log.e("API", "网络请求失败", t);
}
});
// 取消请求(如页面销毁时)
call.cancel();2.4 Token 刷新拦截器
java
// 自动刷新 Token(老项目中常见模式)
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new Authenticator() {
@Override
public Request authenticate(Route route, Response response) {
// 当收到 401 时自动刷新 Token
String newToken = refreshToken();
if (newToken != null) {
return response.request().newBuilder()
.header("Authorization", "Bearer " + newToken)
.build();
}
return null; // 刷新失败,放弃重试
}
})
.build();3. Gson:JSON 解析
3.1 数据模型定义
java
public class User {
// @SerializedName:映射 JSON 字段名(当 Java 字段名与 JSON 不一致时)
@SerializedName("user_id")
private int id;
@SerializedName("user_name")
private String name;
private String email;
@SerializedName("is_vip")
private boolean isVip;
@SerializedName("created_at")
private String createdAt;
// 嵌套对象
@SerializedName("address")
private Address address;
// Getters
public int getId() { return id; }
public String getName() { return name; }
public String getEmail() { return email; }
public boolean isVip() { return isVip; }
}
public class Address {
private String city;
private String street;
// ...
}
// 通用响应包装(老项目中几乎都有)
public class ApiResponse<T> {
@SerializedName("code")
private int code;
@SerializedName("message")
private String message;
@SerializedName("data")
private T data;
public boolean isSuccess() { return code == 0 || code == 200; }
public T getData() { return data; }
public String getMessage() { return message; }
}3.2 手动使用 Gson
java
Gson gson = new Gson();
// JSON → Java 对象
String json = "{\"user_id\":1,\"user_name\":\"张三\",\"email\":\"zhang@example.com\"}";
User user = gson.fromJson(json, User.class);
// Java 对象 → JSON
String jsonOutput = gson.toJson(user);
// JSON 数组 → List
String jsonArray = "[{\"user_id\":1},{\"user_id\":2}]";
Type listType = new TypeToken<List<User>>(){}.getType();
List<User> users = gson.fromJson(jsonArray, listType);4. SharedPreferences:简单键值存储
java
// 获取 SharedPreferences 实例
SharedPreferences prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE);
// 写入数据
SharedPreferences.Editor editor = prefs.edit();
editor.putString("user_name", "张三");
editor.putInt("user_id", 123);
editor.putBoolean("is_logged_in", true);
editor.putFloat("score", 95.5f);
editor.putStringSet("tags", new HashSet<>(Arrays.asList("java", "android")));
editor.apply(); // 异步写入(推荐)
// editor.commit(); // 同步写入(不推荐,会阻塞线程)
// 读取数据
String name = prefs.getString("user_name", "默认值");
int userId = prefs.getInt("user_id", 0);
boolean isLoggedIn = prefs.getBoolean("is_logged_in", false);
// 删除
editor.remove("user_name").apply();
// 清空
editor.clear().apply();常见用途:
- 存储登录 Token
- 保存用户设置(主题、语言等)
- 记录首次启动标记
- 缓存简单配置
4.1 实际项目中的 SharedPreferences 封装
老项目中通常会把 SharedPreferences 封装成工具类,避免到处写 key 字符串:
java
// 来自实际教程项目的封装模式
public class SharedUtil {
private static SharedUtil mUtil; // 单例实例
private SharedPreferences mShared; // SP 对象
private SharedPreferences.Editor mEditor; // 编辑器
// 单例模式获取实例
public static SharedUtil getInstance(Context ctx) {
if (mUtil == null) {
mUtil = new SharedUtil(ctx);
}
return mUtil;
}
private SharedUtil(Context ctx) {
// "share" 是 SP 文件名,MODE_PRIVATE 表示私有模式
mShared = ctx.getSharedPreferences("share", Context.MODE_PRIVATE);
}
// 写入字符串
public void writeString(String key, String value) {
mEditor = mShared.edit();
mEditor.putString(key, value);
mEditor.apply();
}
// 读取字符串
public String readString(String key, String defaultValue) {
return mShared.getString(key, defaultValue);
}
// 写入布尔值
public void writeBoolean(String key, boolean value) {
mEditor = mShared.edit();
mEditor.putBoolean(key, value);
mEditor.apply();
}
// 读取布尔值
public boolean readBoolean(String key, boolean defaultValue) {
return mShared.getBoolean(key, defaultValue);
}
}
// 使用方式(前端类比:类似封装 localStorage 工具类)
SharedUtil shared = SharedUtil.getInstance(this);
shared.writeString("user_name", "张三");
shared.writeBoolean("is_logged_in", true);
String name = shared.readString("user_name", "");4.5 SQLiteOpenHelper:原生 SQLite 数据库(老项目)
在 Room 出现之前,Android 使用 SQLiteOpenHelper 直接操作 SQLite。老项目中大量存在:
java
// 来自实际教程项目 UserDBHelper.java
// 前端类比:类似手动封装 IndexedDB,Room 之前就是这种方式
public class UserDBHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "user.db"; // 数据库文件名
private static final int DB_VERSION = 1; // 数据库版本号
private static UserDBHelper mHelper = null; // 单例实例
private SQLiteDatabase mDB = null; // 数据库连接
public static final String TABLE_NAME = "user_info"; // 表名
// 私有构造方法(单例模式)
private UserDBHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
// 单例获取实例
public static UserDBHelper getInstance(Context context, int version) {
if (version > 0 && mHelper == null) {
mHelper = new UserDBHelper(context, version);
} else if (mHelper == null) {
mHelper = new UserDBHelper(context);
}
return mHelper;
}
// 创建表(首次安装时执行)
@Override
public void onCreate(SQLiteDatabase db) {
String createSql = "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " ("
+ "_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,"
+ "name VARCHAR NOT NULL,"
+ "age INTEGER NOT NULL,"
+ "height INTEGER NOT NULL,"
+ "phone VARCHAR,"
+ "password VARCHAR"
+ ");";
db.execSQL(createSql);
}
// 数据库升级时执行
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// 版本升级时添加新列
if (oldVersion < 2) {
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN phone VARCHAR");
}
}
// 插入数据(类似 SQL INSERT)
public long insert(UserInfo user) {
ContentValues values = new ContentValues();
values.put("name", user.getName());
values.put("age", user.getAge());
values.put("phone", user.getPhone());
mDB = mHelper.getWritableDatabase();
return mDB.insert(TABLE_NAME, null, values);
}
// 查询数据(类似 SQL SELECT)
public List<UserInfo> query(String condition) {
List<UserInfo> list = new ArrayList<>();
mDB = mHelper.getReadableDatabase();
String sql = "SELECT * FROM " + TABLE_NAME
+ (condition != null ? " WHERE " + condition : "");
Cursor cursor = mDB.rawQuery(sql, null);
while (cursor.moveToNext()) {
UserInfo info = new UserInfo();
info.setName(cursor.getString(1)); // 第 2 列是 name
info.setAge(cursor.getInt(2)); // 第 3 列是 age
info.setPhone(cursor.getString(4)); // 第 5 列是 phone
list.add(info);
}
cursor.close();
return list;
}
// 删除数据
public int delete(String condition) {
mDB = mHelper.getWritableDatabase();
return mDB.delete(TABLE_NAME, condition, null);
}
}
// 使用方式
UserDBHelper dbHelper = UserDBHelper.getInstance(this, 1);
dbHelper.openWriteLink();
dbHelper.insert(new UserInfo("张三", 25, "13800001234"));
List<UserInfo> users = dbHelper.query("age > 20");
dbHelper.closeLink();Room Entity 注解示例(来自实际项目)
java
// 来自实际教程项目 BookInfo.java
// Room 用注解替代手动建表,类似 Prisma / TypeORM 的装饰器
@Entity // 标记为 Room 实体,自动生成表
public class BookInfo {
@PrimaryKey // 标记为主键
@NonNull // 主键必须非空
private String name; // 书籍名称
private String author; // 作者
private String press; // 出版社
private double price; // 价格
// Getters and Setters...
}SQLiteOpenHelper vs Room:老项目用 SQLiteOpenHelper 手动写 SQL,新项目用 Room 注解。如果你看到
extends SQLiteOpenHelper、rawQuery、ContentValues,这就是原生 SQLite 写法。
5. Room:SQLite ORM
Room 是 Google 官方的 SQLite 抽象层,类似前端的 Dexie.js 或 Prisma。
5.1 定义实体(Entity)
java
@Entity(tableName = "users")
public class UserEntity {
@PrimaryKey(autoGenerate = true)
private int id;
@ColumnInfo(name = "user_name")
private String name;
@ColumnInfo(name = "email")
private String email;
@ColumnInfo(name = "avatar_url")
private String avatarUrl;
@ColumnInfo(name = "created_at")
private long createdAt;
// Getters and Setters...
}5.2 定义 DAO(数据访问对象)
java
@Dao
public interface UserDao {
@Query("SELECT * FROM users ORDER BY created_at DESC")
List<UserEntity> getAllUsers();
@Query("SELECT * FROM users WHERE id = :userId")
UserEntity getUserById(int userId);
@Query("SELECT * FROM users WHERE user_name LIKE '%' || :keyword || '%'")
List<UserEntity> searchUsers(String keyword);
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertUser(UserEntity user);
@Insert
void insertUsers(List<UserEntity> users);
@Update
void updateUser(UserEntity user);
@Delete
void deleteUser(UserEntity user);
@Query("DELETE FROM users")
void deleteAllUsers();
// LiveData 响应式查询(数据变化时自动通知)
@Query("SELECT * FROM users")
LiveData<List<UserEntity>> observeAllUsers();
}5.3 定义数据库
java
@Database(entities = {UserEntity.class}, version = 1, exportSchema = false)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
private static volatile AppDatabase INSTANCE;
public static AppDatabase getInstance(Context context) {
if (INSTANCE == null) {
synchronized (AppDatabase.class) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(
context.getApplicationContext(),
AppDatabase.class,
"app_database"
).build();
}
}
}
return INSTANCE;
}
}5.4 使用
java
// ⚠️ Room 操作必须在后台线程
new Thread(() -> {
UserDao dao = AppDatabase.getInstance(this).userDao();
// 插入
UserEntity user = new UserEntity();
user.setName("张三");
user.setEmail("zhangsan@example.com");
dao.insertUser(user);
// 查询
List<UserEntity> users = dao.getAllUsers();
// 响应式查询(使用 LiveData)
runOnUiThread(() -> {
dao.observeAllUsers().observe(this, users -> {
// 数据库变化时自动回调
adapter.submitList(users);
});
});
}).start();5.5 数据库迁移
java
// 版本升级时添加迁移
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE users ADD COLUMN phone TEXT");
}
};
Room.databaseBuilder(context, AppDatabase.class, "app_database")
.addMigrations(MIGRATION_1_2)
.build();6. 老项目中常见的网络库
6.1 技术栈识别
| 网络库 | 依赖标识 | 时期 | 说明 |
|---|---|---|---|
| OkHttp 3.x/4.x | com.squareup.okhttp3:okhttp | 2015+ | 目前主流 |
| Retrofit 2.x | com.squareup.retrofit2:retrofit | 2016+ | 目前主流 |
| Volley | com.android.volley:volley | 2013+ | Google 出品,老项目可能用 |
| AsyncHttpClient | com.loopj.android:android-async-http | 2012+ | 已停维护 |
| HttpURLConnection | Java 内置 | 原始 | 最底层,极老项目 |
6.2 Volley 示例(老项目可能遇到)
java
// Volley 请求队列(单例)
RequestQueue queue = Volley.newRequestQueue(this);
// GET 请求
StringRequest request = new StringRequest(Request.Method.GET, url,
response -> {
// 成功(已在主线程)
textView.setText(response);
},
error -> {
// 失败
textView.setText("请求失败");
}
);
queue.add(request);7. 常见数据流模式
7.1 Repository 模式
java
// 统一数据源(网络 + 本地缓存)
public class UserRepository {
private ApiService api;
private UserDao userDao;
public UserRepository(ApiService api, UserDao userDao) {
this.api = api;
this.userDao = userDao;
}
// 先返回缓存,再请求网络
public void getUsers(DataCallback<List<User>> callback) {
// 先返回本地数据
List<UserEntity> cached = userDao.getAllUsers();
if (cached != null && !cached.isEmpty()) {
callback.onSuccess(convertToUsers(cached));
}
// 再请求网络数据
api.getUsers(1, 50).enqueue(new Callback<ApiResponse<List<User>>>() {
@Override
public void onResponse(Call<ApiResponse<List<User>>> call,
Response<ApiResponse<List<User>>> response) {
if (response.isSuccessful() && response.body().isSuccess()) {
List<User> users = response.body().getData();
// 保存到本地
userDao.insertUsers(convertToEntities(users));
callback.onSuccess(users);
}
}
@Override
public void onFailure(Call<ApiResponse<List<User>>> call, Throwable t) {
if (cached == null || cached.isEmpty()) {
callback.onError(t);
}
}
});
}
}前端开发者备忘
| 前端数据概念 | Android 对应 | 备注 |
|---|---|---|
fetch(url, options) | OkHttp client.newCall(request) | HTTP 请求 |
axios.create({ baseURL }) | Retrofit.Builder().baseUrl() | API 客户端 |
axios.interceptors | OkHttp Interceptor | 请求/响应中间件 |
response.data | response.body() | 响应体 |
JSON.parse() | gson.fromJson() | JSON 解析 |
localStorage.setItem() | SharedPreferences.Editor.putString() | 简单存储 |
IndexedDB / sqlite.js | Room / SQLite | 结构化存储 |
| Prisma / TypeORM | Room(@Entity + @Dao) | ORM |
| SWR / React Query | Repository + LiveData | 数据获取+缓存 |
AbortController.abort() | call.cancel() | 取消请求 |
下一步
掌握了数据获取和存储后,接下来学习 模块 08:架构模式与导航,了解如何组织代码结构。