A robust, production-ready template for modern Android development that takes the pain out of setting up a new project. Built on the foundation of Now In Android's architecture, this template provides a comprehensive starting point for both new and experienced Android developers.
- Production-Ready Authentication: Firebase authentication with Google Sign-In and email/password, including secure credential management
- Clean Architecture: Clear separation of concerns with a modular, scalable architecture
- Modern Tech Stack: Leverages the latest Android development tools and libraries including Jetpack Compose, Kotlin Coroutines, and Dagger Hilt
- Type-Safe Navigation: Fully typed navigation using Kotlin serialization
- Robust Data Management: Complete data layer with Repository pattern, Room database, and Preferences DataStore
- Network Communication: Retrofit + OkHttp setup with proper error handling and interceptors
- CI/CD: Automate build, release and Play Store deployment using GitHub actions and Fastlane
Note
The codebase follows a set of conventions that prioritize simplicity and maintainability. Understanding these patterns will help you develop effectively.
Check out the whole list here.
- UI: Jetpack Compose, Material3, Navigation Compose
- DI: Dagger Hilt
- Async: Kotlin Coroutines & Flow
- Network: Retrofit, OkHttp, Kotlinx Serialization
- Storage: Room DB, DataStore Preferences
- Images: Coil
- Kotlin 2.0
- Gradle 8.11.1 with Version Catalogs
- Java 21
- Custom Gradle Convention Plugins
- Spotless for code formatting
- MVVM with Clean Architecture
- Repository Pattern
- Modular design with feature isolation
- Firebase Authentication
- Single Activity
- DataStore for preferences
- Kotlinx Serialization for JSON
- Type-safe navigation
- Debug/Release variants
- Firebase Crashlytics integration
- GitHub Actions CI/CD
- Automatic dependency updates with Renovate
- Code documentation with Dokka
graph TD
A[App Module] --> B[Auth]
A --> C[Storage:Preferences]
A --> D[Storage:Room]
A --> E[Network]
B --> F[Core:UI]
B --> C
B --> G[Core:Android]
C --> G
D --> G
E --> G
F --> G
The codebase follows a clean architecture pattern with clear separation of concerns across different layers. Each layer has specific responsibilities and dependencies flow inward, with the domain layer at the center.
The data layer is responsible for handling data operations and is organized into the following components:
- Data Sources: Located in
*DataSource
classes (e.g.,NetworkDataSource
,AuthDataSource
)- Handle raw data operations with external systems (API, database, etc.)
- Perform data transformations and mapping
- Example:
AuthDataSourceImpl
in the auth module handles raw Firebase authentication operations
Note
Data sources should expose Flow for observable data and suspend functions for one-shot operations:
interface DataSource {
fun observeData(): Flow<Data>
suspend fun updateData(data: Data)
}
- Models: Found in
models
packages across modules- Define data structures for external data sources
- Contain serialization/deserialization logic
- Example:
NetworkPost
in the network module represents raw API responses
Important
Always keep data models immutable and use data classes:
data class NetworkResponse(
val id: Int,
val data: String
)
The data layer is implemented across several modules:
network/
: Handles remote API communicationstorage/preferences/
: Manages local data persistence using DataStorestorage/room/
: Handles SQLite database operations using Room
Warning
Don't expose data source interfaces directly to ViewModels. Always go through repositories:
// DO THIS
class MyViewModel(
private val repository: MyRepository
)
// DON'T DO THIS
class MyViewModel(
private val dataSource: MyDataSource
)
The repository layer acts as a single source of truth and mediates between data sources:
- Repositories: Found in
repository
packages (e.g.,AuthRepository
)- Coordinate between multiple data sources
- Implement business logic for data operations
- Abstract data sources from the UI layer
- Handle caching strategies
- Example:
AuthRepositoryImpl
coordinates between Firebase Auth and local preferences
Key characteristics:
- Uses Kotlin Result type for error handling
- Implements caching where appropriate
- Exposes Kotlin Flow for reactive data updates
Important
Always return Result<T>
from repository methods. This ensures consistent error handling across the app:
suspend fun getData(): Result<Data> = suspendRunCatching {
dataSource.getData()
}
The UI layer follows an MVVM pattern and consists of:
-
ViewModels: Located in
ui
packages- Manage UI state and business logic
- Handle user interactions
- Communicate with repositories
- Example:
AuthViewModel
manages authentication state and user actions
-
Screens: Found in
ui
packages alongside their ViewModels- Compose UI components
- Handle UI layouts and styling
- Observe ViewModel state
- Example:
SignInScreen
displays login form and handles user input
-
State Management:
- Uses
UiState<T>
data class for managing loading, error, and success states - Employs
StateFlow
for reactive UI updates - Handles one-time events using
OneTimeEvent<T>
- Uses
Tip
Always use UiState
wrapper for ViewModel states. This ensures consistent error and loading handling across the app.
data class UiState<T : Any>(
val data: T,
val loading: Boolean = false,
val error: OneTimeEvent<Throwable?> = OneTimeEvent(null)
)
Warning
Don't create custom loading or error handling in individual screens. Use StatefulComposable instead:
// DON'T DO THIS
if (isLoading) {
CircularProgressIndicator()
}
// DO THIS
StatefulComposable(state = uiState) { data ->
// Your UI content
}
The typical data flow follows this pattern:
-
UI Layer:
User Action β ViewModel β Repository
-
Repository Layer:
Repository β Data Sources β External Systems
-
Data Flow Back:
External Systems β Data Sources β Repository β ViewModel β UI
The codebase uses several key data structures for state management:
-
UiState:
data class UiState<T : Any>( val data: T, val loading: Boolean = false, val error: OneTimeEvent<Throwable?> = OneTimeEvent(null) )
- Wraps UI data with loading and error states
- Used by ViewModels to communicate state to UI
-
Result:
- Used by repositories to handle success/failure
- Propagates errors up the stack
- Example:
Result<AuthUser>
for authentication operations
-
StateFlow:
- Used for reactive state management
- Provides hot, stateful event streams
- Example:
_authUiState: MutableStateFlow<UiState<AuthScreenData>>
-
OneTimeEvent:
- Errors propagate up through Result
- They get converted to OneTimeEvent when reaching the UI layer
- This ensures error Snackbars only show once and don't reappear on recomposition
Important
Use StatefulComposable
for screens that need loading or error handling. This component handles these states automatically, reducing boilerplate and ensuring consistent behavior.
StatefulComposable(
state = viewModel.state,
onShowSnackbar = { msg, action -> /* ... */ }
) { data ->
// Your UI content here
}
Tip
Use the provided extension functions for updating state:
// For regular state updates
_uiState.updateState { copy(value = newValue) }
// For async operations
_uiState.updateStateWith(viewModelScope) {
repository.someAsyncOperation()
}
This codebase prioritizes pragmatic simplicity over theoretical purity, making conscious tradeoffs that favor maintainability and readability over absolute correctness or flexibility. Here are some key examples of this philosophy:
Instead of implementing error and loading states individually for each screen, we handle these centrally through the StatefulComposable
:
@Composable
fun <T : Any> StatefulComposable(
state: UiState<T>,
onShowSnackbar: suspend (String, String?) -> Boolean,
content: @Composable (T) -> Unit
) {
content(state.data)
if (state.loading) {
// Centralized loading indicator
}
state.error.getContentIfNotHandled()?.let { error ->
// Centralized error handling
}
}
Tradeoff:
- β Simplicity: UI components only need to focus on their happy path
- β Consistency: Error and loading states behave uniformly across the app
- β Flexibility: Less control over specific error/loading UI for individual screens
While the NowInAndroid codebase promotes a functional approach using Flow operators and transformations, we opt for a more direct approach using MutableStateFlow:
// Our simplified approach
class AuthViewModel @Inject constructor(
private val authRepository: AuthRepository,
) : ViewModel() {
private val _authUiState = MutableStateFlow(UiState(AuthScreenData()))
val authUiState = _authUiState.asStateFlow()
fun updateEmail(email: String) {
_authUiState.updateState {
copy(
email = TextFiledData(
value = email,
errorMessage = if (email.isEmailValid()) null else "Email Not Valid"
)
)
}
}
}
Tradeoff:
- β Readability: State changes are explicit and easy to trace
- β Simplicity: Easier to manage multiple UI events and loading states
- β Debuggability: Direct state mutations are easier to debug
- β Purity: Less adherence to functional programming principles
- β Resource Management: No automatic cleanup of subscribers when the app is in background (compared to
SharingStarted.WhileSubscribed(5_000)
)
Note
These patterns are guidelines, not rules. The goal is to make the codebase more maintainable and easier to understand, not to restrict flexibility where it's truly needed.
- Android Studio Hedgehog or newer
- JDK 21
- Firebase account for authentication and crashlytics
- Clone and open project:
git clone https://github.com/atick-faisal/Jetpack-Compose-Starter.git
- Firebase setup:
- Create project in Firebase Console
- Download
google-services.json
toapp/
- Add SHA fingerprint to Firebase Console for Google Sign-In:
./gradlew signingReport
Note
Firebase authentication and crashlytics requires Firebase console setup and the google-services.json
file. I have provided a template to ensure a successful build. However, you need to provide your own in order to use all the functionalities.
- Debug builds:
./gradlew assembleDebug
- Create
keystore.properties
in project root:
storePassword=****
keyPassword=****
keyAlias=****
storeFile=keystore-file-name.jks
-
Place keystore file in
app/
-
Build release:
./gradlew assembleRelease
This guide walks through the process of adding a new feature to the app, following the established patterns and conventions.
Start by defining your data models in the appropriate layer:
- Network Models (if feature requires API calls):
// network/src/main/kotlin/dev/atick/network/models/
@Serializable
data class NetworkFeatureData(
val id: Int,
val title: String
)
- UI Models (what your screen will display):
// feature/src/main/kotlin/dev/atick/feature/models/
data class FeatureScreenData(
val title: String,
val description: String = "",
// ... other UI state
)
- Define the interface:
// feature/src/main/kotlin/dev/atick/feature/data/
interface FeatureDataSource {
suspend fun getFeatureData(): List<NetworkFeatureData>
fun observeFeatureData(): Flow<List<NetworkFeatureData>>
}
- Implement the data source:
class FeatureDataSourceImpl @Inject constructor(
private val api: FeatureApi,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
) : FeatureDataSource {
override suspend fun getFeatureData(): List<NetworkFeatureData> {
return withContext(ioDispatcher) {
api.getFeatureData()
}
}
override fun observeFeatureData(): Flow<List<NetworkFeatureData>> {
return flow {
// Implementation
}.flowOn(ioDispatcher)
}
}
- Define repository interface:
// feature/src/main/kotlin/dev/atick/feature/repository/
interface FeatureRepository {
suspend fun getFeatureData(): Result<List<FeatureData>>
}
- Implement repository:
class FeatureRepositoryImpl @Inject constructor(
private val dataSource: FeatureDataSource
) : FeatureRepository {
override suspend fun getFeatureData(): Result<List<FeatureData>> =
suspendRunCatching {
dataSource.getFeatureData().map { it.toFeatureData() }
}
}
// feature/src/main/kotlin/dev/atick/feature/ui/
@HiltViewModel
class FeatureViewModel @Inject constructor(
private val repository: FeatureRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(UiState(FeatureScreenData()))
val uiState = _uiState.asStateFlow()
init {
loadData()
}
private fun loadData() {
_uiState.updateStateWith(viewModelScope) {
repository.getFeatureData()
.map { data -> /* transform to screen data */ }
}
}
fun onUserAction(/* params */) {
_uiState.updateState {
copy(/* update state */)
}
}
}
- Create screen composable:
// feature/src/main/kotlin/dev/atick/feature/ui/
@Composable
fun FeatureRoute(
onShowSnackbar: suspend (String, String?) -> Boolean,
viewModel: FeatureViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
StatefulComposable(
state = uiState,
onShowSnackbar = onShowSnackbar
) { screenData ->
FeatureScreen(
screenData = screenData,
onAction = viewModel::onUserAction
)
}
}
@Composable
private fun FeatureScreen(
screenData: FeatureScreenData,
onAction: () -> Unit
) {
// UI implementation
}
- Add preview:
@DevicePreviews
@Composable
private fun FeatureScreenPreview() {
FeatureScreen(
screenData = FeatureScreenData(/* sample data */),
onAction = {}
)
}
- Define navigation endpoints:
// feature/src/main/kotlin/dev/atick/feature/navigation/
@Serializable
data object FeatureNavGraph
@Serializable
data object Feature
- Add navigation extensions:
fun NavController.navigateToFeature(navOptions: NavOptions? = null) {
navigate(Feature, navOptions)
}
fun NavGraphBuilder.featureScreen(
onShowSnackbar: suspend (String, String?) -> Boolean
) {
composable<Feature> {
FeatureRoute(
onShowSnackbar = onShowSnackbar
)
}
}
fun NavGraphBuilder.featureNavGraph(
nestedGraphs: NavGraphBuilder.() -> Unit
) {
navigation<FeatureNavGraph>(
startDestination = Feature
) {
nestedGraphs()
}
}
- Add module for data source:
@Module
@InstallIn(SingletonComponent::class)
abstract class DataSourceModule {
@Binds
@Singleton
abstract fun bindFeatureDataSource(
impl: FeatureDataSourceImpl
): FeatureDataSource
}
- Add module for repository:
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindFeatureRepository(
impl: FeatureRepositoryImpl
): FeatureRepository
}
β
Data models defined
β
Data source interface and implementation created
β
Repository interface and implementation created
β
ViewModel handling state and user actions
β
UI components with previews
β
Navigation setup
β
Dependency injection modules