Module 07: Networking & Data Persistence
Objective: Master networking in Android (OkHttp, Retrofit) and data storage solutions (SharedPreferences, SQLite, Room), and learn to handle asynchronous data flows.
Frontend Comparison
| Frontend | Android | Description |
|---|---|---|
fetch() / axios | OkHttp / Retrofit | HTTP client |
localStorage | SharedPreferences | Simple key-value storage |
IndexedDB | SQLite / Room | Structured local database |
JSON.parse() | Gson / Moshi | JSON parsing |
| Request interceptors | OkHttp Interceptor | Request/response middleware |
| CORS / Token refresh | Interceptor chain | Authentication handling |
1. OkHttp: Low-Level HTTP Client
1.1 Basic Requests
// Create OkHttpClient (typically a singleton)
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();
// GET request
Request request = new Request.Builder()
.url("https://api.example.com/users")
.addHeader("Authorization", "Bearer " + token)
.build();
// ⚠️ Network requests must run on a background thread
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
// Request failed
Log.e("API", "Request failed", e);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
String body = response.body().string();
// ⚠️ Callback runs on a background thread; switch to main thread to update UI
runOnUiThread(() -> {
textView.setText(body);
});
}
}
});1.2 POST Requests
// JSON request body
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();
// Form request
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 Interceptors
Similar to request/response middleware in the frontend world (e.g., axios interceptors):
// Add common headers (similar to axios request interceptors)
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);
})
// Logging interceptor (for debugging)
.addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
.build();2. Retrofit: Declarative API Client
Retrofit is a high-level wrapper built on top of OkHttp. It uses interface annotations to define APIs, similar to tRPC or OpenAPI client generators in the frontend world.
2.1 Defining API Interfaces
// Define the API interface
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);
// Form submission
@FormUrlEncoded
@POST("auth/login")
Call<LoginResponse> loginForm(
@Field("username") String username,
@Field("password") String password
);
// File upload
@Multipart
@POST("upload")
Call<UploadResponse> uploadFile(
@Part MultipartBody.Part file,
@Part("description") RequestBody description
);
}2.2 Creating a Retrofit Instance
// Create Retrofit instance (typically a singleton)
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttpClient) // Reuse OkHttp client configuration
.addConverterFactory(GsonConverterFactory.create()) // JSON → Java objects
.build();
ApiService api = retrofit.create(ApiService.class);2.3 Making Requests
// Make a request (callback approach)
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();
// Update UI (mind the thread switch)
runOnUiThread(() -> {
adapter.submitList(users);
});
} else {
// Server returned an error status code
int code = response.code();
Log.e("API", "Error: " + code);
}
}
@Override
public void onFailure(Call<List<User>> call, Throwable t) {
// Network error or parsing error
Log.e("API", "Network request failed", t);
}
});
// Cancel a request (e.g., when the page is destroyed)
call.cancel();2.4 Token Refresh Interceptor
// Automatic token refresh (common pattern in legacy projects)
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new Authenticator() {
@Override
public Request authenticate(Route route, Response response) {
// Automatically refresh token when a 401 is received
String newToken = refreshToken();
if (newToken != null) {
return response.request().newBuilder()
.header("Authorization", "Bearer " + newToken)
.build();
}
return null; // Refresh failed, give up retrying
}
})
.build();3. Gson: JSON Parsing
3.1 Data Model Definition
public class User {
// @SerializedName: maps JSON field names (when Java field names don't match JSON keys)
@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;
// Nested objects
@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;
// ...
}
// Generic response wrapper (nearly ubiquitous in legacy projects)
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 Using Gson Directly
Gson gson = new Gson();
// JSON → Java object
String json = "{\"user_id\":1,\"user_name\":\"张三\",\"email\":\"zhang@example.com\"}";
User user = gson.fromJson(json, User.class);
// Java object → JSON
String jsonOutput = gson.toJson(user);
// JSON array → 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: Simple Key-Value Storage
// Get a SharedPreferences instance
SharedPreferences prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE);
// Write data
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(); // Asynchronous write (recommended)
// editor.commit(); // Synchronous write (not recommended, blocks the thread)
// Read data
String name = prefs.getString("user_name", "default_value");
int userId = prefs.getInt("user_id", 0);
boolean isLoggedIn = prefs.getBoolean("is_logged_in", false);
// Delete
editor.remove("user_name").apply();
// Clear all
editor.clear().apply();Common use cases:
- Storing login tokens
- Saving user preferences (theme, language, etc.)
- Recording first-launch flags
- Caching simple configuration values
4.1 Practical SharedPreferences Wrapper
Real projects typically wrap SharedPreferences in a utility class to avoid scattering key strings everywhere:
// Common pattern from actual tutorial projects
public class SharedUtil {
private static SharedUtil mUtil; // singleton instance
private SharedPreferences mShared; // SP object
private SharedPreferences.Editor mEditor; // editor
// Singleton access
public static SharedUtil getInstance(Context ctx) {
if (mUtil == null) {
mUtil = new SharedUtil(ctx);
}
return mUtil;
}
private SharedUtil(Context ctx) {
// "share" is the SP file name, MODE_PRIVATE means private access
mShared = ctx.getSharedPreferences("share", Context.MODE_PRIVATE);
}
// Write string
public void writeString(String key, String value) {
mEditor = mShared.edit();
mEditor.putString(key, value);
mEditor.apply();
}
// Read string
public String readString(String key, String defaultValue) {
return mShared.getString(key, defaultValue);
}
// Write boolean
public void writeBoolean(String key, boolean value) {
mEditor = mShared.edit();
mEditor.putBoolean(key, value);
mEditor.apply();
}
// Read boolean
public boolean readBoolean(String key, boolean defaultValue) {
return mShared.getBoolean(key, defaultValue);
}
}
// Usage (frontend analogy: wrapping localStorage in a utility class)
SharedUtil shared = SharedUtil.getInstance(this);
shared.writeString("user_name", "John");
shared.writeBoolean("is_logged_in", true);
String name = shared.readString("user_name", "");4.5 SQLiteOpenHelper: Raw SQLite (Legacy Projects)
Before Room, Android used SQLiteOpenHelper to work with SQLite directly. This pattern is common in legacy projects:
// From actual tutorial project UserDBHelper.java
// Frontend analogy: like manually wrapping IndexedDB before libraries like Dexie
public class UserDBHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "user.db"; // database file name
private static final int DB_VERSION = 1; // database version
private static UserDBHelper mHelper = null; // singleton instance
private SQLiteDatabase mDB = null; // database connection
public static final String TABLE_NAME = "user_info"; // table name
// Private constructor (singleton)
private UserDBHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
// Get singleton instance
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;
}
// Create table (runs on first install)
@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);
}
// Runs on database upgrade
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion < 2) {
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN phone VARCHAR");
}
}
// Insert data (similar to 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);
}
// Query data (similar to 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)); // column 2 is name
info.setAge(cursor.getInt(2)); // column 3 is age
info.setPhone(cursor.getString(4)); // column 5 is phone
list.add(info);
}
cursor.close();
return list;
}
// Delete data
public int delete(String condition) {
mDB = mHelper.getWritableDatabase();
return mDB.delete(TABLE_NAME, condition, null);
}
}
// Usage
UserDBHelper dbHelper = UserDBHelper.getInstance(this, 1);
dbHelper.openWriteLink();
dbHelper.insert(new UserInfo("John", 25, "13800001234"));
List<UserInfo> users = dbHelper.query("age > 20");
dbHelper.closeLink();Room Entity Annotation Example (from actual project)
// From actual tutorial project BookInfo.java
// Room uses annotations to replace manual table creation, similar to Prisma / TypeORM decorators
@Entity // marks as Room entity, auto-generates table
public class BookInfo {
@PrimaryKey // marks as primary key
@NonNull // primary key must be non-null
private String name; // book title
private String author; // author
private String press; // publisher
private double price; // price
// Getters and Setters...
}SQLiteOpenHelper vs Room: Legacy projects use SQLiteOpenHelper with manual SQL; new projects use Room annotations. If you see
extends SQLiteOpenHelper,rawQuery, orContentValues, it's the raw SQLite pattern.
5. Room: SQLite ORM
Room is Google's official SQLite abstraction layer, similar to Dexie.js or Prisma in the frontend world.
5.1 Defining Entities
@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 Defining DAOs (Data Access Objects)
@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();
// Reactive query with LiveData (automatically notifies when data changes)
@Query("SELECT * FROM users")
LiveData<List<UserEntity>> observeAllUsers();
}5.3 Defining the Database
@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 Usage
// ⚠️ Room operations must run on a background thread
new Thread(() -> {
UserDao dao = AppDatabase.getInstance(this).userDao();
// Insert
UserEntity user = new UserEntity();
user.setName("张三");
user.setEmail("zhangsan@example.com");
dao.insertUser(user);
// Query
List<UserEntity> users = dao.getAllUsers();
// Reactive query (using LiveData)
runOnUiThread(() -> {
dao.observeAllUsers().observe(this, users -> {
// Automatically called back when the database changes
adapter.submitList(users);
});
});
}).start();5.5 Database Migrations
// Add a migration when upgrading the schema version
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. Networking Libraries Commonly Found in Legacy Projects
6.1 Identifying the Tech Stack
| Library | Dependency | Era | Description |
|---|---|---|---|
| OkHttp 3.x/4.x | com.squareup.okhttp3:okhttp | 2015+ | Current mainstream choice |
| Retrofit 2.x | com.squareup.retrofit2:retrofit | 2016+ | Current mainstream choice |
| Volley | com.android.volley:volley | 2013+ | By Google, may appear in older projects |
| AsyncHttpClient | com.loopj.android:android-async-http | 2012+ | No longer maintained |
| HttpURLConnection | Built into Java | Original | Lowest-level API, found in very old projects |
6.2 Volley Example (May Encounter in Legacy Projects)
// Volley request queue (singleton)
RequestQueue queue = Volley.newRequestQueue(this);
// GET request
StringRequest request = new StringRequest(Request.Method.GET, url,
response -> {
// Success (already on the main thread)
textView.setText(response);
},
error -> {
// Failure
textView.setText("Request failed");
}
);
queue.add(request);7. Common Data Flow Patterns
7.1 Repository Pattern
// Unified data source (network + local cache)
public class UserRepository {
private ApiService api;
private UserDao userDao;
public UserRepository(ApiService api, UserDao userDao) {
this.api = api;
this.userDao = userDao;
}
// Return cached data first, then fetch from network
public void getUsers(DataCallback<List<User>> callback) {
// Return local data first
List<UserEntity> cached = userDao.getAllUsers();
if (cached != null && !cached.isEmpty()) {
callback.onSuccess(convertToUsers(cached));
}
// Then fetch from the network
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();
// Save locally
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);
}
}
});
}
}Frontend Developer Cheat Sheet
| Frontend Concept | Android Equivalent | Notes |
|---|---|---|
fetch(url, options) | OkHttp client.newCall(request) | HTTP requests |
axios.create({ baseURL }) | Retrofit.Builder().baseUrl() | API client |
axios.interceptors | OkHttp Interceptor | Request/response middleware |
response.data | response.body() | Response body |
JSON.parse() | gson.fromJson() | JSON parsing |
localStorage.setItem() | SharedPreferences.Editor.putString() | Simple storage |
IndexedDB / sqlite.js | Room / SQLite | Structured storage |
| Prisma / TypeORM | Room (@Entity + @Dao) | ORM |
| SWR / React Query | Repository + LiveData | Data fetching + caching |
AbortController.abort() | call.cancel() | Cancel requests |
Next Steps
Now that you've mastered data fetching and storage, move on to Module 08: Architecture Patterns & Navigation to learn how to organize your codebase.