A native Android client built against a REST job-board API: register or log in, browse paginated offers, and keep a local list of the ones worth coming back to — with the token lifecycle and the HTTP layer handled once, centrally, rather than re-derived in every screen.
The problem & constraints
A mobile client for an authenticated API has three problems that are easy to solve badly: the auth token arrives after startup and changes on login/logout, every request needs the same headers, and rebuilding the HTTP stack per call quietly throws away the connection pool. The constraint was to keep all of that in one place so the activities only deal with typed calls and results.
Approach
- A single Retrofit instance cached statically and rebuilt only when the token actually changes, so the OkHttp connection pool and thread pool survive across requests.
- An OkHttp interceptor attaches
Accept: application/jsonand, when a token is present,Authorization: Bearer …— screens never touch headers. - API surface split by concern into typed interfaces:
AuthApi(register / login / logout / me) andJobsApi(list / create), with a genericPagedResponse<T>wrapper so pagination is modeled once rather than per endpoint. - Favorites toggled locally as a
Set<String>of offer IDs, so the list survives restarts and stays readable without a round trip.
Described in full:
- activities — typed calls only → AuthApi — register, login, — logout, me
- activities — typed calls only → JobsApi — list, create — PagedResponse<T>
- activities — typed calls only → SharedPreferences — favorites: Set<String> (local)
- AuthApi — register, login, — logout, me → Retrofit + OkHttp — cached, keyed on token — Accept + Bearer
- JobsApi — list, create — PagedResponse<T> → Retrofit + OkHttp — cached, keyed on token — Accept + Bearer
- Retrofit + OkHttp — cached, keyed on token — Accept + Bearer → job-board API (bearer)
Trade-offs made
Decision
A cached Retrofit instance keyed on the token, over building one per call: keeps connection reuse and avoids re-allocating the client on every request, at the cost of explicit invalidation logic — the instance has to know when the token changed, which is a small piece of state I now own rather than avoid.
Decision
Favorites in SharedPreferences rather than a Room table: a set of integer IDs doesn't need a schema, a migration, or a DAO, and the read/write cost is negligible at this size. Room and WorkManager are wired in for the sync path where a real local mirror of the offer list does need structure.
Results
A working login → paginated browse → favorite loop against a live API, with the HTTP and auth concerns isolated behind two interfaces and an interceptor instead of spread across the activities.
What I'd do differently
Gate HttpLoggingInterceptor at Level.BODY behind BuildConfig.DEBUG
— as it stands, a release build would write bearer tokens and full
response bodies to logcat, which is the single change I'd make before
shipping anything. I'd also move favorites server-side: they're currently
device-local, so the same account on a second device starts empty.