Get the most out of this template with these useful tips and features.
The core:android
module contains several helpful extensions and utilities:
// String Extensions
email.isEmailValid() // Email validation
password.isPasswordValid() // Password validation
name.isValidFullName() // Name validation
// Number Formatting
number.format(nDecimal = 2) // Format numbers with specific decimals
timestamp.asFormattedDateTime() // Convert timestamp to readable date
// Context Extensions
context.showToast(message) // Show toast message
context.hasPermission(permission) // Check single permission
context.isAllPermissionsGranted() // Check multiple permissions
context.createNotificationChannel() // Create notification channel
The core:ui/components
directory contains pre-built Material3 composables:
// Pre-built components
JetpackButton(onClick = {})
JetpackOutlinedButton(onClick = {})
JetpackTextButton(onClick = {})
JetpackTextFiled(value = "", onValueChange = {})
JetpackLoadingWheel(contentDesc = "")
SwipeToDismiss(onDelete = {})
Tip
Always use these pre-built components instead of creating new ones. They provide consistent styling and can be modified centrally.
The core:ui/theme
directory contains theming utilities:
// Custom theme
JetpackTheme(
darkTheme = isDarkTheme,
disableDynamicTheming = true
) {
// Your content
}
// Background with gradient
AppGradientBackground(
gradientColors = gradientColors
) {
// Your content
}
The core:network
module provides network state monitoring:
class YourViewModel @Inject constructor(
private val networkUtils: NetworkUtils
) : ViewModel() {
init {
networkUtils.networkState
.onEach { state ->
when (state) {
NetworkState.CONNECTED -> // Handle connected state
NetworkState.LOST
-> // Handle disconnected state
NetworkState.UNAVAILABLE
-> // Handle unavailable state
}
}
.launchIn(viewModelScope)
}
}
The sync
module provides a robust way to synchronize local data with remote services:
// 1. Make your repository syncable
interface YourRepository : Syncable {
suspend fun sync(): Flow<SyncProgress>
}
// 2. Inject SyncManager in the repository implementation
class YourRepositoryImpl @Inject constructor(
private val syncManager: SyncManager
) : YourRepository {
fun requestSync() {
syncManager.requestSync()
}
}
// 3. Inject repository into SyncWorker
@HiltWorker
class SyncWorker @AssistedInject constructor(
private val yourRepository: YourRepository
) : CoroutineWorker {
override suspend fun doWork(): Result {
yourRepository.sync()
.collect { progress ->
setProgress(progress)
}
return Result.success()
}
}
Note
The sync module is already integrated with WorkManager for background synchronization. Just implement the Syncable interface in your repositories and inject them into the SyncWorker.
The template includes the gradle-secrets-plugin for secure credentials management:
# local.properties (git-ignored)
API_KEY=your_actual_api_key
API_SECRET=your_actual_secret
# secrets.defaults.properties (version controlled)
API_KEY=dummy_api_key
API_SECRET=dummy_secret
Access secrets as BuildConfig
fields:
val apiKey = BuildConfig.apiKey
Tip
Use secrets.defaults.properties to provide dummy values for CI/CD environments while keeping sensitive data in local.properties.
The template comes with Dokka setup for documentation generation:
# Generate markdown documentation
./gradlew dokkaGfmMultiModule --no-configuration-cache
# The generated docs will be available in:
# build/dokka/gfmMultiModule/
MkDocs is also configured to create beautiful Material theme documentation:
# mkdocs.yml is already configured with:
# - Material theme
# - Search functionality
# - Code highlighting
# - Navigation structure
Note
If you're using GitHub, the documentation is automatically generated and published through the docs workflow.
// Regular state updates
_uiState.updateState {
copy(value = newValue)
}
// Async operations that doesn't return anything
_uiState.updateWith(viewModelScope) {
repository.someAsyncOperation()
}
// Async operations that returns results
_uiState.updateStateWith(viewModelScope) {
repository.someAsyncOperation()
}
@Composable
fun YourScreen(
onShowSnackbar: suspend (String, SnackbarAction, Throwable?) -> Boolean
) {
StatefulComposable(
state = uiState,
onShowSnackbar = onShowSnackbar
) { data ->
// Your UI content
}
}
Note
The error snackbar automatically includes a "Report" button that logs the error stack trace to Firebase Analytics.
// In ViewModel
class ScreenViewModel : ViewModel() {
fun undoDelete(itemId: String) {
_uiState.updateStateWith(viewModelScope) {
repository.undoDelete(itemId)
}
}
}
// In UI
StatefulComposable(
state = screenUiState,
onShowSnackbar = onShowSnackbar,
) { screenData ->
LaunchedEffect(screenData.deletedItemId, onShowSnackbar) {
val itemId = screenData.deletedItemId.getContentIfNotHandled()
if (itemId != null) {
val actionPerformed = onShowSnackbar(
"Item deleted",
SnackbarAction.UNDO,
null
)
if (actionPerformed) viewModel.undoDelete(itemId)
}
}
}
Use the provided preview annotations for efficient UI development:
@Composable
@PreviewThemes // Preview in both light and dark themes
@PreviewDevices // Preview on different device sizes
fun YourComposablePreview() {
JetpackTheme {
YourComposable()
}
}
Preview devices include:
- Phone (360x640dp)
- Landscape (640x360dp)
- Foldable (673x841dp)
- Tablet (1280x800dp)
// Run with timeout
val result = suspendCoroutineWithTimeout(5.seconds) {
// Your async operation
}
// Safe coroutine execution
val result = suspendRunCatching {
// Your operation that might fail
}
// Network-bound resource handling
fun getData(): Flow<Resource<Data>> = networkBoundResource(
query = { localDataSource.getData() },
fetch = { apiService.getData() },
saveFetchResult = { localDataSource.saveData(it) }
)
// Lifecycle aware Flow collection
lifeCycleOwner.collectWithLifecycle(flow) { data ->
// Handle data
}
// LiveData observation
lifeCycleOwner.observe(liveData) { data ->
// Handle data
}
// One-time event observation
lifeCycleOwner.observeEvent(eventLiveData) { event ->
// Handle one-time event
}
-
Use Pre-built Components
- Leverage the components in
core:ui/components
- Maintain consistent styling across the app
- Centralize design system changes
- Leverage the components in
-
Error Handling
- Use
suspendRunCatching
in repositories - Let
StatefulComposable
handle UI states - Leverage the built-in error reporting
- Use
-
State Management
- Use state update helpers consistently
- Leverage
JetpackAppState
for app-wide state - Follow unidirectional data flow
-
Network Handling
- Monitor network state with
NetworkUtils
- Use network-bound resources for caching
- Handle offline scenarios gracefully
- Monitor network state with
-
Resource Loading
- Use
DynamicAsyncImage
for images - Leverage
JetpackLoadingWheel
- Follow Material3 loading patterns
- Use
-
Documentation
- Use Dokka for code documentation
- Keep documentation up to date
- Leverage the automated docs workflow
-
Secrets Management
- Store sensitive data in local.properties
- Provide defaults in secrets.defaults.properties
- Use buildConfigField for config values
Tip
Explore the core
modules thoroughly - they contain many utilities that can save you time and
ensure consistency across your app.