Android Network Requests: A Detailed Technical Guide

举报
yd_217846120 发表于 2026/08/28 08:55:02 2026/08/28
【摘要】 Android Network Requests: A Detailed Technical Guide IntroductionNetwork communication is the backbone of almost every modern Android application. Whether you are fetching data from a remote serve...

Android Network Requests: A Detailed Technical Guide

Introduction

Network communication is the backbone of almost every modern Android application. Whether you are fetching data from a remote server, uploading user-generated content, or synchronizing local state with a backend, a robust and well-architected networking layer is essential. This article provides a detailed walkthrough of building a production-grade networking layer in Android using OkHttp, Retrofit, and RxJava for reactive programming.


1. The Evolution of Android Networking

Over the years, Android developers have moved through several paradigms for making network requests:

  1. HttpURLConnection — The built-in SDK option. Powerful but verbose, with no built-in connection pooling, retry logic, or interceptor support.
  2. Apache HttpClient — Deprecated and removed in API 23. Historically popular but no longer maintained on Android.
  3. Volley — Google’s library introduced at Google I/O 2013. Great for image loading and small JSON payloads, but less flexible for large data transfers or streaming.
  4. OkHttp — A modern, efficient HTTP client with connection pooling, transparent compression, response caching, and a powerful interceptor chain.
  5. Retrofit — A type-safe REST client built on top of OkHttp that turns HTTP APIs into Java/Kotlin interfaces through declarative annotations.
  6. RxJava adapters — Enable Retrofit to return Observable, Single, or Flowable types, allowing you to compose asynchronous network operations reactively.

For this guide, we will use the OkHttp + Retrofit + RxJava stack, which provides a clean separation of concerns, excellent testability, and powerful error-handling capabilities.


2. Project Setup

2.1 Declaring Dependencies

In your module-level build.gradle, add the following dependencies. These are Maven coordinates, not web addresses:

dependencies {
    // OkHttp for the underlying HTTP client
    implementation 'com.squareup.okhttp3:okhttp:4.12.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'

    // Retrofit for declarative API definitions
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

    // RxJava adapter so Retrofit can return reactive types
    implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0'

    // RxJava and RxAndroid
    implementation 'io.reactivex.rxjava3:rxjava:3.1.8'
    implementation 'io.reactivex.rxjava3:rxandroid:3.0.2'

    // Gson for JSON parsing
    implementation 'com.google.code.gson:gson:2.10.1'
}

2.2 Why These Libraries?

Library Responsibility
OkHttp Low-level HTTP transport, connection pooling, interceptors, caching
Retrofit Translates interface annotations into HTTP calls, parses responses
Gson Converter Converts JSON response bodies into Java objects automatically
RxJava Adapter Bridges Retrofit’s Call type into RxJava Observable/Single
RxAndroid Provides a Scheduler that runs on the Android main thread

3. Defining the Data Model

Before defining the API, we need data transfer objects (DTOs) that represent the JSON payloads. Suppose we are building an app that consumes a user-management API.

public class User {
    private long id;
    private String name;
    private String email;
    private String avatarUrl;
    private boolean active;

    // Getters and setters omitted for brevity.
    // In practice, generate them or use Lombok.
}

For list endpoints, the API often wraps results in an envelope:

public class ApiResponse<T> {
    private int code;
    private String message;
    private T data;

    public boolean isSuccess() {
        return code == 200;
    }

    public T getData() {
        return data;
    }
}

This generic wrapper lets us reuse the same envelope shape across all endpoints.


4. Defining the Retrofit API Interface

Retrofit’s core idea is that you describe your API as a Java interface annotated with HTTP method information. Retrofit generates the implementation at runtime.

import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.Path;
import retrofit2.http.Query;
import io.reactivex.rxjava3.core.Single;

public interface UserService {

    @GET("users")
    Single<ApiResponse<List<User>>> getUsers(
        @Query("page") int page,
        @Query("size") int size
    );

    @GET("users/{id}")
    Single<ApiResponse<User>> getUserById(@Path("id") long id);

    @POST("users")
    Single<ApiResponse<User>> createUser(@Body User user);
}

4.1 Annotation Reference

Annotation Purpose
@GET, @POST, @PUT, @DELETE, @PATCH HTTP method + relative path
@Path Substitutes a value into the URL path
@Query Appends a query parameter
@QueryMap Appends multiple query parameters from a Map
@Body Sends a serialized object as the request body
@Header Adds a dynamic header
@FormUrlEncoded + @Field Sends form-encoded data

Notice that each method returns a Single<T>. This is an RxJava type that emits exactly one item or an error — a perfect fit for network calls that complete once.


5. Building the Retrofit Instance

Creating a Retrofit instance is expensive, so it should be done once and reused. A common pattern is a singleton holder class.

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;

public class NetworkClient {

    private static final String BASE_URL = "https://api.example.com/v1/";
    private static volatile Retrofit retrofit;

    private NetworkClient() {}

    public static Retrofit getRetrofit() {
        if (retrofit == null) {
            synchronized (NetworkClient.class) {
                if (retrofit == null) {
                    retrofit = buildRetrofit();
                }
            }
        }
        return retrofit;
    }

    public static <S> S createService(Class<S> serviceClass) {
        return getRetrofit().create(serviceClass);
    }

    private static Retrofit buildRetrofit() {
        HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
        logging.setLevel(HttpLoggingInterceptor.Level.BODY);

        OkHttpClient okHttpClient = new OkHttpClient.Builder()
            .addInterceptor(new AuthInterceptor("your-token-here"))
            .addInterceptor(logging)
            .connectTimeout(15, TimeUnit.SECONDS)
            .readTimeout(20, TimeUnit.SECONDS)
            .writeTimeout(20, TimeUnit.SECONDS)
            .retryOnConnectionFailure(true)
            .build();

        return new Retrofit.Builder()
            .baseUrl(BASE_URL)
            .client(okHttpClient)
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(RxJava3CallAdapterFactory.create())
            .build();
    }
}

5.1 The Double-Checked Locking Pattern

The getRetrofit() method uses double-checked locking to ensure thread-safe lazy initialization without the performance cost of synchronizing on every call. The volatile keyword guarantees the partially-constructed object is never published to other threads.


6. Custom Interceptors

Interceptors sit between your application code and the raw HTTP call. They are the ideal place for cross-cutting concerns like authentication, logging, and request rewriting.

6.1 Authentication Interceptor

import okhttp3.Interceptor;
import okhttp3.Response;

public class AuthInterceptor implements Interceptor {

    private final String token;

    public AuthInterceptor(String token) {
        this.token = token;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request original = chain.request();
        Request authenticated = original.newBuilder()
            .header("Authorization", "Bearer " + token)
            .header("Accept", "application/json")
            .build();
        return chain.proceed(authenticated);
    }
}

6.2 Error-Unwrapping Interceptor

Sometimes the server returns HTTP 200 but embeds a business error code in the body. You can intercept and normalize this:

public class ErrorNormalizingInterceptor implements Interceptor {

    @Override
    public Response intercept(Chain chain) throws IOException {
        Response response = chain.proceed(chain.request());
        if (response.isSuccessful()) {
            ResponseBody body = response.body();
            if (body != null) {
                String rawJson = body.string();
                try {
                    JsonObject json = JsonParser.parseString(rawJson).getAsJsonObject();
                    int code = json.get("code").getAsInt();
                    if (code != 200) {
                        String msg = json.get("message").getAsString();
                        throw new ApiException(code, msg);
                    }
                } catch (JsonSyntaxException ignored) {
                    // Not JSON, fall through.
                }
                // Rebuild the body because we consumed it.
                return response.newBuilder()
                    .body(ResponseBody.create(rawJson, body.contentType()))
                    .build();
            }
        }
        return response;
    }
}

Important: Once you call body.string(), the body is consumed and cannot be read again. You must rebuild the response with a fresh ResponseBody if downstream code needs the content.


7. Making Requests with RxJava

This is where the reactive approach shines. Instead of using callbacks (which lead to nested, hard-to-read code), we compose operations declaratively.

7.1 A Simple Fetch

UserService service = NetworkClient.createService(UserService.class);

service.getUsers(1, 20)
    .subscribeOn(Schedulers.io())          // Run network call on IO thread
    .observeOn(AndroidSchedulers.mainThread()) // Emit result on main thread
    .subscribe(
        response -> showUsers(response.getData()),
        error -> showError(error.getMessage())
    );

7.2 Chaining Requests with flatMap

Suppose fetching a user requires first looking up their ID by email, then fetching the full profile:

service.getUserByEmail("alice@example.com")
    .flatMap(userSummary -> service.getUserById(userSummary.getId()))
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(
        fullUser -> showProfile(fullUser),
        error -> showError(error.getMessage())
    );

flatMap takes the emission of the first Single and transforms it into a new Single — effectively chaining dependent asynchronous operations without callback nesting.

7.3 Combining Parallel Requests with zip

To load a user’s profile and their order history simultaneously:

Single<ApiResponse<User>> profileSingle = service.getUserById(userId);
Single<ApiResponse<List<Order>>> ordersSingle = service.getOrders(userId);

Single.zip(
    profileSingle, ordersSingle,
    (profileResponse, ordersResponse) -> {
        ProfileData data = new ProfileData();
        data.setUser(profileResponse.getData());
        data.setOrders(ordersResponse.getData());
        return data;
    }
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
    data -> renderProfile(data),
    error -> showError(error.getMessage())
);

zip waits for all sources to complete and then combines their results. Both network calls run concurrently on the IO scheduler pool.

7.4 Retrying with retryWhen

Network conditions on mobile are unreliable. A transient failure should not immediately surface to the user:

service.getUsers(1, 20)
    .retryWhen(errors -> errors
        .zipWith(Flowable.range(1, 3), (error, attempt) -> attempt)
        .flatMap(attempt -> {
            long delay = (long) Math.pow(2, attempt); // Exponential backoff
            return Flowable.timer(delay, TimeUnit.SECONDS);
        })
    )
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(...);

This retries up to 3 times with exponential backoff (1s, 2s, 4s), then propagates the error if all attempts fail.


8. Managing Subscriptions and Lifecycle

A critical mistake is forgetting to dispose of subscriptions. If a network call completes after the user has navigated away from an Activity or Fragment, updating the UI will crash or leak memory.

8.1 Using CompositeDisposable

public class UserListActivity extends AppCompatActivity {

    private final CompositeDisposable disposables = new CompositeDisposable();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        loadUsers();
    }

    private void loadUsers() {
        Disposable d = NetworkClient.createService(UserService.class)
            .getUsers(1, 20)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(
                response -> showUsers(response.getData()),
                error -> showError(error.getMessage())
            );
        disposables.add(d);
    }

    @Override
    protected void onDestroy() {
        disposables.clear();
        super.onDestroy();
    }
}

disposables.clear() cancels all active subscriptions, preventing callbacks from firing after the activity is destroyed.

8.2 Binding to Android Lifecycle (Alternative)

If you use Android’s Lifecycle components, you can create a custom ObservableTransformer that automatically disposes when the lifecycle reaches DESTROYED:

public class LifecycleTransformer<T> implements ObservableTransformer<T, T> {

    private final LifecycleOwner lifecycleOwner;

    public LifecycleTransformer(LifecycleOwner owner) {
        this.lifecycleOwner = owner;
    }

    @Override
    public Observable<T> apply(Observable<T> upstream) {
        return upstream.takeUntil(
            Observable.create(emitter -> {
                LifecycleObserver observer = new LifecycleObserver() {
                    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
                    void onDestroy() {
                        emitter.onComplete();
                    }
                };
                lifecycleOwner.getLifecycle().addObserver(observer);
            })
        );
    }
}

9. Centralized Error Handling

Different failure modes require different handling. A centralized error mapper keeps your subscription lambdas clean.

public class NetworkError extends RuntimeException {

    public enum Kind {
        NETWORK,       // No connectivity, timeout, DNS failure
        HTTP,          // Server returned 4xx or 5xx
        BUSINESS,      // HTTP 200 but business code indicates failure
        UNEXPECTED     // Serialization error, null pointer, etc.
    }

    private final Kind kind;
    private final int code;
    private final String message;

    public NetworkError(Kind kind, int code, String message) {
        super(message);
        this.kind = kind;
        this.code = code;
        this.message = message;
    }

    // Getters omitted.
}

A transformer that converts raw Throwables into NetworkErrors:

public class ErrorTransformer<T> implements SingleTransformer<T, T> {

    @Override
    public Single<T> apply(Single<T> upstream) {
        return upstream.onErrorResumeNext(throwable -> {
            NetworkError error;
            if (throwable instanceof IOException) {
                error = new NetworkError(
                    NetworkError.Kind.NETWORK, -1,
                    "Network unavailable. Please check your connection."
                );
            } else if (throwable instanceof HttpException) {
                HttpException http = (HttpException) throwable;
                error = new NetworkError(
                    NetworkError.Kind.HTTP, http.code(),
                    "Server error: " + http.code()
                );
            } else if (throwable instanceof ApiException) {
                ApiException api = (ApiException) throwable;
                error = new NetworkError(
                    NetworkError.Kind.BUSINESS, api.getCode(),
                    api.getMessage()
                );
            } else {
                error = new NetworkError(
                    NetworkError.Kind.UNEXPECTED, -1,
                    "An unexpected error occurred."
                );
            }
            return Single.error(error);
        });
    }
}

Usage:

service.getUsers(1, 20)
    .compose(new ErrorTransformer<>())
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(
        response -> showUsers(response.getData()),
        error -> handleNetworkError((NetworkError) error)
    );

10. Caching Strategies

OkHttp supports HTTP caching out of the box. To enable it, configure a Cache on the OkHttpClient:

int cacheSize = 10 * 1024 * 1024; // 10 MB
Cache cache = new Cache(context.getCacheDir(), cacheSize);

OkHttpClient client = new OkHttpClient.Builder()
    .cache(cache)
    .build();

10.1 Network-First, Cache-Fallback

For data that should be fresh but can tolerate staleness, use a request that prefers the network and falls back to cache:

Request networkRequest = new Request.Builder()
    .url(url)
    .cacheControl(CacheControl.FORCE_NETWORK)
    .build();

If the network fails, retry with CacheControl.FORCE_CACHE.

10.2 Cache-First for Offline Support

Request cacheRequest = new Request.Builder()
    .url(url)
    .cacheControl(new CacheControl.Builder()
        .maxStale(7, TimeUnit.DAYS)
        .build())
    .build();

This serves cached responses up to 7 days old, which is useful for rarely-changing reference data.


11. Testing the Network Layer

Retrofit’s interface-based design makes testing straightforward. You can point Retrofit at a MockWebServer from OkHttp’s test module:

testImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0'

Example test:

public class UserServiceTest {

    private MockWebServer server;
    private UserService service;

    @Before
    public void setUp() throws IOException {
        server = new MockWebServer();
        server.start();

        Retrofit retrofit = new Retrofit.Builder()
            .baseUrl(server.url("/").toString())
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(RxJava3CallAdapterFactory.create())
            .build();

        service = retrofit.create(UserService.class);
    }

    @After
    public void tearDown() throws IOException {
        server.shutdown();
    }

    @Test
    public void getUserById_parsesResponse() throws Exception {
        server.enqueue(new MockResponse()
            .setBody("{\"code\":200,\"message\":\"ok\",\"data\":{\"id\":1,\"name\":\"Alice\"}}")
            .setResponseCode(200));

        ApiResponse<User> response = service.getUserById(1)
            .blockingGet();

        assertEquals(200, response.getCode());
        assertEquals("Alice", response.getData().getName());
    }
}

MockWebServer lets you enqueue canned responses and assert on the requests your code actually made, giving you full control over the network layer in tests without touching a real server.


12. Best Practices Summary

  1. Initialize Retrofit once. Use a singleton. Rebuilding it on every call wastes resources and defeats connection pooling.
  2. Always dispose subscriptions. Use CompositeDisposable and clear it in onDestroy() to prevent memory leaks and crashes.
  3. Subscribe on IO, observe on main. Network calls must never run on the main thread; UI updates must never run on a background thread.
  4. Centralize error handling. A single ErrorTransformer keeps business logic clean and ensures consistent user-facing error messages.
  5. Use interceptors for cross-cutting concerns. Auth tokens, logging, and header injection belong in interceptors, not scattered through your API calls.
  6. Enable logging only in debug builds. HttpLoggingInterceptor at Level.BODY leaks sensitive data and adds overhead — restrict it to non-release builds.
  7. Set reasonable timeouts. Mobile networks are slow. 15–30 seconds is a reasonable range; shorter timeouts cause false failures on poor connections.
  8. Cache when possible. Reference data and images benefit enormously from caching, reducing bandwidth and improving perceived performance.
  9. Prefer Single over Observable for one-shot calls. It communicates intent clearly and prevents accidental multiple emissions.
  10. Test with MockWebServer. Do not rely on live servers in unit tests — they are slow, flaky, and non-deterministic.

13. Conclusion

A well-structured Android networking layer built on OkHttp, Retrofit, and RxJava provides:

  • Declarative API definitions through annotated interfaces.
  • Reactive composition that eliminates callback hell and makes complex async flows readable.
  • Powerful middleware via the interceptor chain for auth, logging, and error normalization.
  • Excellent testability through interface-based design and MockWebServer.

By following the patterns in this guide — single-instance Retrofit, proper subscription management, centralized error handling, and thoughtful caching — you can build a networking layer that is resilient to the realities of mobile connectivity, easy to maintain, and a pleasure to test.

The key insight is that reactive programming is not just about threading — it is about treating network calls as composable streams of data, which lets you express complex operations like retries, parallel fetches, and dependent chains as clean, declarative pipelines.

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。