Skip to main content
Nimaz uses DataStore to persist user preferences with type-safe, asynchronous storage.

PreferencesDataStore

The PreferencesDataStore class provides centralized access to all app settings through Kotlin Flow. Source: data/local/datastore/PreferencesDataStore.kt

Preference categories

Location & prayer settings

val latitude: Flow<Double>
val longitude: Flow<Double>
val locationName: Flow<String>
val calculationMethod: Flow<String>
val asrMethod: Flow<String>
val highLatitudeRule: Flow<String>
val prayerNotificationsEnabled: Flow<Boolean>
Stores:
  • User location coordinates
  • Prayer time calculation methods
  • Asr juristic method (Shafi’i or Hanafi)
  • High latitude adjustment rules
  • Global notification toggle

Prayer adjustments

val fajrAdjustment: Flow<Int>
val sunriseAdjustment: Flow<Int>
val dhuhrAdjustment: Flow<Int>
val asrAdjustment: Flow<Int>
val maghribAdjustment: Flow<Int>
val ishaAdjustment: Flow<Int>
Allows ±30 minute adjustments per prayer.

Notification settings

val fajrNotificationEnabled: Flow<Boolean>
val sunriseNotificationEnabled: Flow<Boolean>
val dhuhrNotificationEnabled: Flow<Boolean>
val asrNotificationEnabled: Flow<Boolean>
val maghribNotificationEnabled: Flow<Boolean>
val ishaNotificationEnabled: Flow<Boolean>

val showReminderBefore: Flow<Boolean>
val notificationReminderMinutes: Flow<Int>
Per-prayer notification toggles plus optional pre-reminders (default 15 minutes before).

Adhan sounds

val adhanSound: Flow<String>
val fajrAdhanSound: Flow<String>
val dhuhrAdhanSound: Flow<String>
val asrAdhanSound: Flow<String>
val maghribAdhanSound: Flow<String>
val ishaAdhanSound: Flow<String>
Supports:
  • Global adhan sound (default)
  • Per-prayer adhan overrides
  • Options: MISHARY, ABDUL_BASIT, MAKKAH, SIMPLE_BEEP

Theme & appearance

val themeMode: Flow<String>
val dynamicColor: Flow<Boolean>
val showIslamicPatterns: Flow<Boolean>
  • Theme modes: "system", "light", "dark"
  • Dynamic colors: Material You theming on Android 12+
  • Islamic patterns: Decorative background patterns

Display preferences

val use24HourFormat: Flow<Boolean>
val useHijriPrimary: Flow<Boolean>
val showCountdownTimer: Flow<Boolean>
val showQuickActions: Flow<Boolean>
  • Time format (12h/24h)
  • Primary calendar (Hijri or Gregorian)
  • Prayer countdown visibility
  • Quick action buttons on home screen

Interaction settings

val hapticFeedback: Flow<Boolean>
val animationsEnabled: Flow<Boolean>
  • Vibration on interactions
  • UI animations toggle

Quran preferences

val selectedTranslation: Flow<String>
val arabicFontSize: Flow<Float>
val translationFontSize: Flow<Float>
val showTransliteration: Flow<Boolean>
val continuousReadingMode: Flow<Boolean>
val showTajweedColors: Flow<Boolean>
Controls:
  • Translation language
  • Font sizes (14sp - 32sp range)
  • Transliteration display
  • Reading mode (ayah-by-ayah or continuous)
  • Tajweed color highlighting

Tasbih preferences

val tasbihVibration: Flow<Boolean>
val tasbihSound: Flow<Boolean>
Counter feedback options.

Language

val appLanguage: Flow<String>
Supported: "en", "tr", "id", "ms", "fr", "de", "" (system default)

Reading preferences

suspend fun <T> get(key: Preferences.Key<T>, defaultValue: T): T
Synchronous read (suspends until value is available):
val method = preferencesDataStore.calculationMethod.first()
Reactive read (collects updates):
preferencesDataStore.themeMode.collect { mode ->
    // Update UI when preference changes
}

Writing preferences

All preferences have corresponding setter functions:
suspend fun setLatitude(value: Double)
suspend fun setLongitude(value: Double)
suspend fun setCalculationMethod(value: String)
suspend fun setThemeMode(value: String)
// ... etc.
All write operations are suspending functions that must be called from a coroutine scope.

Example: Updating theme

class SettingsViewModel @Inject constructor(
    private val preferencesDataStore: PreferencesDataStore
) : ViewModel() {
    
    fun updateTheme(mode: ThemeMode) {
        viewModelScope.launch {
            val modeString = when (mode) {
                ThemeMode.LIGHT -> "light"
                ThemeMode.DARK -> "dark"
                ThemeMode.SYSTEM -> "system"
            }
            preferencesDataStore.setThemeMode(modeString)
        }
    }
}
Source: presentation/viewmodel/SettingsViewModel.kt

Preferences in MainActivity

MainActivity reads theme and display preferences on app launch:
val themeModeString by preferencesDataStore.themeMode.collectAsState(initial = "system")
val dynamicColor by preferencesDataStore.dynamicColor.collectAsState(initial = false)
val hapticEnabled by preferencesDataStore.hapticFeedback.collectAsState(initial = true)
val animationsEnabled by preferencesDataStore.animationsEnabled.collectAsState(initial = true)
val use24HourFormat by preferencesDataStore.use24HourFormat.collectAsState(initial = false)
val useHijriPrimary by preferencesDataStore.useHijriPrimary.collectAsState(initial = false)
val showIslamicPatterns by preferencesDataStore.showIslamicPatterns.collectAsState(initial = true)
val localeCode by preferencesDataStore.appLanguage.collectAsState(initial = "en")
Source: MainActivity.kt:48-55

Preferences in NimazApp

On app startup, NimazApp reads language and notification settings:
private fun applySavedLocale() {
    CoroutineScope(Dispatchers.IO + SupervisorJob()).launch {
        try {
            val langCode = preferencesDataStore.appLanguage.first()
            if (langCode.isNotEmpty() && langCode != "en") {
                LocaleHelper.setLocale(this@NimazApp, langCode)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
}

private fun scheduleInitialNotifications() {
    CoroutineScope(Dispatchers.IO + SupervisorJob()).launch {
        try {
            val prefs = preferencesDataStore.userPreferences.first()
            if (prefs.latitude != 0.0 && prefs.longitude != 0.0) {
                // Schedule prayer notifications based on preferences
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
}
Source: NimazApp.kt:47-92

Dependency injection

PreferencesDataStore is provided via Hilt:
@Module
@InstallIn(SingletonComponent::class)
object DataStoreModule {
    @Provides
    @Singleton
    fun providePreferencesDataStore(
        @ApplicationContext context: Context
    ): PreferencesDataStore {
        return PreferencesDataStore(context)
    }
}
Source: core/di/DataStoreModule.kt

Data migration

If migrating from SharedPreferences:
val dataStore = context.createDataStore(
    name = "nimaz_preferences",
    migrations = listOf(
        SharedPreferencesMigration(
            context = context,
            sharedPreferencesName = "old_preferences"
        )
    )
)
DataStore is asynchronous. Avoid blocking the main thread when reading preferences. Always use Flow collection or first() within a coroutine.

Best practices

ViewModels should expose preferences as StateFlow or collect them as UI state.
val themeMode = preferencesDataStore.themeMode
    .stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000),
        initialValue = "system"
    )
Use edit to batch multiple preference updates in a single transaction.
All preferences have type-safe defaults defined in PreferencesDataStore.
DataStore operations can throw IOExceptions. Wrap in try-catch blocks.

Build docs developers (and LLMs) love