模块 07:网络请求与数据持久化
目标:掌握 Android 中的网络请求(OkHttp、Retrofit)和数据存储方案(SharedPreferences、SQLite、Room),学会处理异步数据流。
学习目标
读完这一章,你将能够:
- [ ] 说出 OkHttp 和 Retrofit 的分工:前者是底层 HTTP 客户端,后者是声明式 API 封装
- [ ] 解释为什么网络请求不能放在主线程,并知道如何用
runOnUiThread切回主线程更新 UI - [ ] 用 Retrofit + Gson 走通"定义接口注解 → 发起请求 → JSON 自动转 Java 对象"的完整流程
- [ ] 区分 SharedPreferences、SQLiteOpenHelper、Room 三种本地存储方案各自的适用场景
- [ ] 看懂老项目里
SQLiteOpenHelper手写 SQL、ContentValues、Cursor这些原生写法 - [ ] 理解 Repository 模式如何把网络和本地数据源统一管理起来
学习建议:这一章会同时出现网络和存储两大块,概念多但每个都很具体。建议先抓住"数据从哪来、放到哪、怎么取"这条主线——把 OkHttp / Retrofit / Gson 当作联网这条线的三个搭档,把 SharedPreferences / SQLite / Room 当作存储这条线的三个层次,对照着读就不容易乱。
概念入门:App 怎么联网、数据又存到哪里?
先想一个日常场景:你打开一个新闻 App,屏幕上立刻刷出今天的头条;你点开一篇,下面的评论也跟着加载出来;你切换到深色主题,下次再打开 App 依然是深色。这些事情背后只干了两件事——联网拿数据和把数据存到本地。本章就讲这两件事。
先说联网。App 想从服务器拿到数据,本质上和浏览器打开一个网址是一回事:发起一个 HTTP 请求,服务器返回一段文本(通常是 JSON 格式),App 再把这段文本解读成自己能用的数据。Android 里干这件事最常用的工具叫 OkHttp——它是一个 HTTP 客户端,负责"把请求发出去、把响应收回来",相当于送信的邮差。但 OkHttp 用起来比较啰嗦,每次都要手写 URL、拼请求体、自己解析返回的字符串。于是就有了 Retrofit——它建立在 OkHttp 之上,你只要写一个 Java 接口、加上几个注解(比如 @GET("users")),它就自动帮你生成调用代码,并把返回的 JSON 自动翻译成 Java 对象。一句话概括:OkHttp 是干活的,Retrofit 是管事的。
这里有个绕不开的规矩:网络请求不能在主线程做。主线程是 Android 专门用来画界面的线程,每秒要刷新几十次屏幕;如果你在主线程里去等服务器响应(可能要等一两秒),界面就会卡死,甚至被系统弹出"应用无响应"杀掉。所以 OkHttp / Retrofit 都要求把请求丢到后台线程,等数据回来后再切回主线程更新 UI——这一章你会反复看到 runOnUiThread(...) 这种切换线程的写法。
再说存储。App 经常需要把数据存到手机本地,比如登录 Token、用户设置、缓存的数据。Android 提供了三个层次的选择,由简到繁:
- SharedPreferences——一个简单的"键值对"存储,你存一个
user_name = "张三",下次用user_name这个键就能取回来。适合存零散的小配置。 - SQLite 数据库——一个真正的关系型数据库,能建表、能写 SQL 查询,适合存结构化、量大的数据。Android 早期只能用
SQLiteOpenHelper直接手写 SQL 语句,老项目里全是这种代码。 - Room——Google 后来推出的 ORM(对象关系映射)框架,本质上还是 SQLite,但你可以用 Java 注解(
@Entity、@Dao)定义表和操作,不用手写 SQL,编译时还能帮你检查语法错误。
最后单独说一下 Gson,它解决一个独立的小问题:服务器返回的 JSON 是一串文本,而 Java 程序需要的是对象。Gson 的工作就是在这两者之间翻译——把 JSON 字符串变成 Java 对象,或反过来。Retrofit 默认就是用 Gson 来做这种翻译的。
如果你有前端开发经验,可以用下面的对照表快速建立映射;零基础读者直接进入 第 1 节 即可。
前端开发者速查:网络与存储概念对照
| 前端 | 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 基础请求
// 创建 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 请求
// 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 拦截器):
// 添加公共 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 接口
// 定义 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 实例
// 创建 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 发起请求
// 发起请求(回调方式)
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 刷新拦截器
// 自动刷新 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 数据模型定义
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
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:简单键值存储
// 获取 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 字符串:
// 来自实际教程项目的封装模式
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。老项目中大量存在:
// 来自实际教程项目 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 注解示例(来自实际项目)
// 来自实际教程项目 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)
@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(数据访问对象)
@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 定义数据库
@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 使用
// ⚠️ 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 数据库迁移
// 版本升级时添加迁移
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 示例(老项目可能遇到)
// 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 模式
// 统一数据源(网络 + 本地缓存)
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() | 取消请求 |
本章小结
回顾一下这一章的要点:
- OkHttp 是底层 HTTP 客户端,负责发请求、收响应;Retrofit 建立在 OkHttp 之上,用接口注解(
@GET/@POST)声明式地定义 API,并把 JSON 自动转成 Java 对象 - 网络请求不能在主线程,否则会卡死 UI 甚至触发 ANR;数据回来后要用
runOnUiThread(...)切回主线程才能更新 UI - Interceptor 是 OkHttp 的中间件机制,类似 axios 的请求拦截器,常用来加 Token、记日志、自动刷新失效的 Token
- Gson 负责在 JSON 字符串和 Java 对象之间互转;
@SerializedName用来解决字段名不一致的问题 - SharedPreferences 是键值对存储,适合存 Token、设置等零散小数据,写入用
apply()(异步)而非commit()(同步) - SQLiteOpenHelper 是老项目原生 SQLite 写法,需要手写 SQL、
ContentValues、Cursor,看到extends SQLiteOpenHelper就是它 - Room 是 Google 官方 ORM,用
@Entity定义表、@Dao定义操作,编译时检查 SQL,新项目首选 - Repository 模式把网络和本地数据源统一封装,给上层提供一个干净的取数入口
网络和存储都搞定后,下一章我们会进入架构层面——看看这些零散的联网、存储、UI 代码该怎么组织,以及 ViewModel、Jetpack Navigation 这些组件怎么用。
下一步
掌握了数据获取和存储后,接下来学习 模块 08:架构模式与导航,了解如何组织代码结构。