An Android application for browsing GitHub users and their details, with the ability to save favorites for quick access.
- Browse all GitHub users with infinite scroll pagination (20 items per page).
- Offline support — previously loaded users are cached locally and accessible without a network connection.
- Mark and unmark any user as a favorite directly from the list.
- If loading the next page fails, an inline retry option is shown.
- View profile information: avatar, location, follower and following counts, and GitHub profile link.
- Add or remove the user from favorites with a single tap.
- View all saved favorite users in a dedicated screen.
- Remove favorites with a confirmation dialog to prevent accidental deletion.
- Supports Dark Mode
- Supports Dynamic Color (Android 12+)
- Supports adaptive layouts for both User List and User Details on large screens and foldables.
-
User List:
https://github-users.example.com/usersadb shell am start -a android.intent.action.VIEW -d "https://github-users.example.com/users" -
User Details:
https://github-users.example.com/users/{username}adb shell am start -a android.intent.action.VIEW -d "https://github-users.example.com/users/huuphuoc1396"
Note:
https://github-users.example.comis a demo domain that has not been verified. You must enable it manually in the app's supported links settings.
The project follows Clean Architecture in a multi-module setup. Each feature is split into an api module (public contract) and an impl module (private implementation), with shared infrastructure extracted into core modules.
:app ← wires everything together, owns AppNavigatorImpl and DI bindings
core/
common/ ← shared Kotlin contracts (models, dispatchers, Result helpers) — no Android, no Hilt
ui/ ← AppTheme, shared Compose components (AppButton, LoadingScreen, ErrorScreen, etc.)
navigation/ ← AppNavigator interface, NavigationIntent, Route, NavigationEffects
network/ ← Retrofit, OkHttpClient, safeApiCall, ApiException — owns NetworkModule
database/ ← AppDatabase, all DAOs, Room migrations, SQLCipher setup — owns DatabaseModule
security/ ← CMake/C++ key provider, SSL certificate handling
config/ ← build-flavor-aware configuration (base URLs, feature flags)
feature/
users/
api/ ← UserDetailsDestination (nav contract); UserModel exposed via core:common
impl/ ← UserRepository, UserApi, UserRemoteMediator, UserListViewModel, UserDetailsViewModel
favorites/
api/ ← FavoriteRepository interface, AddFavoriteUseCase, RemoveFavoriteUseCase,
IsFavoriteUseCase, GetFavoritesUseCase
impl/ ← FavoriteRepositoryImpl, FavoritesViewModel
graph TD
app(":app")
subgraph feature["Feature Modules"]
fu_api(":feature:users:api")
fu_impl(":feature:users:impl")
ff_api(":feature:favorites:api")
ff_impl(":feature:favorites:impl")
end
subgraph core["Core Modules"]
core_common(":core:common")
core_ui(":core:ui")
core_nav(":core:navigation")
core_net(":core:network")
core_sec(":core:security")
core_cfg(":core:config")
core_db(":core:database")
end
%% app dependencies
app --> fu_api
app --> fu_impl
app --> ff_api
app --> ff_impl
app --> core_ui
app --> core_nav
app --> core_cfg
app --> core_common
%% feature:users:impl dependencies
fu_impl --> fu_api
fu_impl --> ff_api
fu_impl --> core_ui
fu_impl --> core_nav
fu_impl --> core_net
fu_impl --> core_db
fu_impl --> core_common
%% feature:favorites:api dependencies
ff_api --> core_common
%% feature:favorites:impl dependencies
ff_impl --> ff_api
ff_impl --> fu_api
ff_impl --> core_ui
ff_impl --> core_nav
ff_impl --> core_net
ff_impl --> core_db
ff_impl --> core_common
%% core:ui dependencies
core_ui --> core_common
%% core:network dependencies
core_net --> core_common
%% core:database dependencies
core_db --> core_sec
%% core:config dependencies
core_cfg --> core_net
core_cfg --> core_sec
:app → feature:x:impl → feature:x:api → core:*
:app → feature:x:api
feature:x:impl → feature:y:api ← allowed (public contract only)
feature:x:impl → feature:y:impl ← forbidden
core:* → feature:* ← forbidden
| Layer | Location | Responsibility |
|---|---|---|
| Presentation | feature:x:impl/presentation/ |
ViewModel, UiState, Screen composables |
| Domain | feature:x:api/domain/ or feature:x:impl/domain/ |
Use cases, repository interfaces, domain models |
| Data | feature:x:impl/data/ |
Repository impls, Retrofit API, Room DAOs, mappers |
| Infrastructure | core:network, core:database, core:security |
Network stack, database, encryption |
- Write operations: Repositories return
Result<Unit>. Errors are caught at the repository boundary withrunCatching {}and surfaced to the ViewModel via.onFailure {}. - Reactive flows: Repositories return
Flow<T>. ViewModels apply.catch {}before.stateIn()to prevent silent Flow termination on errors.
- Jetpack Compose — declarative UI, no Fragments or XML layouts
- Material 3 with dynamic color and dark mode support
- Adaptive layouts for large screens
- Coil 3 — async image loading
- Navigation Compose with Kotlin Serialization for type-safe destinations
- Retrofit + OkHttp + Gson
- Custom
safeApiCallwrapper maps HTTP errors to typedApiException - Features contribute interceptors via
@IntoSet—NetworkModuleis never modified per feature
- Room + Paging 3 for paginated user list with offline cache
- SQLCipher for encrypted local storage (disabled in
dev, enabled instag/prod) - DataStore for key-value persistence
- SSL Pinning — certificate pinned in
core:securityvia OkHttpCertificatePinner - Database encryption — SQLCipher, key managed by
core:security - Native key storage — sensitive keys stored in C++ via CMake, harder to decompile than JVM bytecode
- R8 — minification and obfuscation enabled in release builds
- LeakCanary — memory leak detection in debug builds
- Timber — structured logging
Tests use JUnit 4, MockK, Turbine, Kotest, and Robolectric.
Coverage is measured with Kover across all modules. Generate an HTML report:
./gradlew koverHtmlReportDevDebug
Test coverage targets:
| Area | What is tested |
|---|---|
| ViewModels | All user interactions, UiState transitions, error paths |
| Use cases | Delegation to repository, Result propagation |
| Repositories | DAO/API calls, runCatching failure paths |
| Mappers | Domain ↔ UI model conversions |
| Flavor | App ID suffix | DB encryption | Notes |
|---|---|---|---|
dev |
.dev |
disabled | For local development and inspection |
stag |
.stag |
enabled | Mirrors production security settings |
prod |
(none) | enabled | Production release |
- Android Studio Ladybug or newer
- JDK 11+
- Android Gradle Plugin 8.6.1+
- Kotlin 2.0.20+
- NDK and CMake (required for
core:security)
| Property | Value |
|---|---|
minSdk |
28 |
targetSdk |
36 |
compileSdk |
36 |
- Clone the repository:
git clone https://github.com/huuphuoc1396/android-github-users.git
- Open the
android-github-users/directory in Android Studio. - Copy
credentials.cppintocore/security/cpp/. - Select a Build Variant (e.g.
devDebug). - Run the
:appconfiguration.
This project is licensed under the MIT License — see the LICENSE file for details.
For any inquiries, reach out to Phuoc Bui.