Skip to content

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

FrontendAndroidDescription
fetch() / axiosOkHttp / RetrofitHTTP client
localStorageSharedPreferencesSimple key-value storage
IndexedDBSQLite / RoomStructured local database
JSON.parse()Gson / MoshiJSON parsing
Request interceptorsOkHttp InterceptorRequest/response middleware
CORS / Token refreshInterceptor chainAuthentication handling

1. OkHttp: Low-Level HTTP Client

1.1 Basic Requests

java
// 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

java
// 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):

java
// 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

java
// 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

java
// 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

java
// 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

java
// 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

java
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

java
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

java
// 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:

java
// 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:

java
// 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)

java
// 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, or ContentValues, 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

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 Defining DAOs (Data Access Objects)

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();

    // Reactive query with LiveData (automatically notifies when data changes)
    @Query("SELECT * FROM users")
    LiveData<List<UserEntity>> observeAllUsers();
}

5.3 Defining the Database

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 Usage

java
// ⚠️ 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

java
// 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

LibraryDependencyEraDescription
OkHttp 3.x/4.xcom.squareup.okhttp3:okhttp2015+Current mainstream choice
Retrofit 2.xcom.squareup.retrofit2:retrofit2016+Current mainstream choice
Volleycom.android.volley:volley2013+By Google, may appear in older projects
AsyncHttpClientcom.loopj.android:android-async-http2012+No longer maintained
HttpURLConnectionBuilt into JavaOriginalLowest-level API, found in very old projects

6.2 Volley Example (May Encounter in Legacy Projects)

java
// 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

java
// 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 ConceptAndroid EquivalentNotes
fetch(url, options)OkHttp client.newCall(request)HTTP requests
axios.create({ baseURL })Retrofit.Builder().baseUrl()API client
axios.interceptorsOkHttp InterceptorRequest/response middleware
response.dataresponse.body()Response body
JSON.parse()gson.fromJson()JSON parsing
localStorage.setItem()SharedPreferences.Editor.putString()Simple storage
IndexedDB / sqlite.jsRoom / SQLiteStructured storage
Prisma / TypeORMRoom (@Entity + @Dao)ORM
SWR / React QueryRepository + LiveDataData 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.

A Java Android development tutorial for frontend developers