Nimaz follows Clean Architecture principles to ensure a maintainable, testable, and scalable codebase. The architecture is organized into three distinct layers, each with specific responsibilities.
Architecture layers
The domain layer is the core of the application and has no dependencies on Android framework or external libraries.
Domain layer
The domain layer contains business logic and is framework-agnostic.
Structure
domain/
├── model/ # Domain models (data classes)
├── repository/ # Repository interfaces
└── usecase/ # Business logic use cases
Domain models
Domain models represent business entities independent of data sources:
// Example: Prayer time types
enum class PrayerType {
FAJR, SUNRISE, DHUHR, ASR, MAGHRIB, ISHA
}
Repository interfaces
Define contracts for data operations without implementation details:
interface QuranRepository {
suspend fun getSurahList (): List < Surah >
suspend fun getAyahsBySurah (surahNumber: Int ): List < Ayah >
fun observeBookmarks (): Flow < List < QuranBookmark >>
}
Use cases
Encapsulate single business operations. Nimaz uses grouped use cases for related functionality:
Available use case groups:
QuranUseCases - Quran reading, bookmarks, favorites, progress tracking
KhatamUseCases - Quran completion tracking, daily goals, statistics
AsmaUlHusnaUseCases - 99 Names of Allah
AsmaUnNabiUseCases - Names of Prophet Muhammad
ProphetUseCases - Stories of 25 Prophets
Example use case:
class GetSurahListUseCase ( private val repository: QuranRepository ) {
suspend operator fun invoke (): List < Surah > {
return repository. getSurahList ()
}
}
Grouping related use cases provides several benefits:
Easier dependency injection (inject one group vs. many individual use cases)
Clear feature boundaries
Simplified ViewModel constructors
Better code organization
Data layer
The data layer implements repository interfaces and manages data sources.
Structure
data/
├── audio/ # Adhan audio management
├── local/
│ ├── database/ # Room database
│ │ ├── dao/ # Data Access Objects
│ │ ├── entity/ # Room entities
│ │ └── NimazDatabase.kt
│ └── datastore/ # Preferences DataStore
└── repository/ # Repository implementations
Room database
Nimaz uses Room for local data persistence with 30+ entities:
@Database (
entities = [
// Quran
SurahEntity:: class ,
AyahEntity:: class ,
TranslationEntity:: class ,
QuranBookmarkEntity:: class ,
QuranFavoriteEntity:: class ,
ReadingProgressEntity:: class ,
SurahInfoEntity:: class ,
// Hadith
HadithBookEntity:: class ,
HadithEntity:: class ,
HadithBookmarkEntity:: class ,
// ... 20+ more entities
],
version = 10 ,
exportSchema = true
)
abstract class NimazDatabase : RoomDatabase () {
abstract fun quranDao (): QuranDao
abstract fun hadithDao (): HadithDao
abstract fun duaDao (): DuaDao
// ... more DAOs
}
The database uses migrations to preserve user data when the schema changes. See NimazDatabase.kt:124 for migration implementations.
Repository implementations
Repositories coordinate between local data sources:
Available repositories:
QuranRepositoryImpl
HadithRepositoryImpl
DuaRepositoryImpl
PrayerRepositoryImpl
FastingRepositoryImpl
TasbihRepositoryImpl
ZakatRepositoryImpl
TafseerRepositoryImpl
KhatamRepositoryImpl
AsmaUlHusnaRepositoryImpl
AsmaUnNabiRepositoryImpl
ProphetRepositoryImpl
class QuranRepositoryImpl @Inject constructor (
private val quranDao: QuranDao
) : QuranRepository {
override suspend fun getSurahList (): List < Surah > {
return quranDao. getAllSurahs (). map { it. toDomain () }
}
override fun observeBookmarks (): Flow < List < QuranBookmark >> {
return quranDao. observeBookmarks (). map { entities ->
entities. map { it. toDomain () }
}
}
}
DataStore preferences
User preferences are stored using Preferences DataStore:
class PreferencesDataStore @Inject constructor (
private val dataStore: DataStore < Preferences >
) {
val userPreferences: Flow < UserPreferences >
val appLanguage: Flow < String >
val fajrNotificationEnabled: Flow < Boolean >
// ... more preferences
}
Presentation layer
The presentation layer handles UI and user interactions using Jetpack Compose.
Structure
presentation/
├── components/
│ ├── atoms/ # Basic UI components
│ ├── molecules/ # Composite components
│ └── organisms/ # Complex UI sections
├── screens/ # Feature screens
│ ├── home/
│ ├── quran/
│ ├── prayer/
│ ├── settings/
│ └── ... 15+ more
├── theme/ # Material3 theme
└── viewmodel/ # ViewModels
Component hierarchy
Nimaz uses atomic design principles:
Basic, reusable UI building blocks (buttons, text fields, icons)
Composite components built from atoms (cards, list items, toolbars)
Complex UI sections combining molecules (prayer time card, Quran reader header)
ViewModels
ViewModels manage UI state and coordinate use cases:
Available ViewModels:
HomeViewModel
QuranViewModel
HadithViewModel
DuaViewModel
PrayerTrackerViewModel
FastingViewModel
TasbihViewModel
ZakatViewModel
KhatamViewModel
AsmaUlHusnaViewModel
AsmaUnNabiViewModel
ProphetViewModel
And more…
@HiltViewModel
class QuranViewModel @Inject constructor (
private val quranUseCases: QuranUseCases ,
private val khatamUseCases: KhatamUseCases ,
private val preferencesDataStore: PreferencesDataStore
) : ViewModel () {
private val _uiState = MutableStateFlow ( QuranUiState ())
val uiState: StateFlow < QuranUiState > = _uiState. asStateFlow ()
fun loadSurah (surahNumber: Int ) {
viewModelScope. launch {
val surah = quranUseCases. getSurahWithAyahs (surahNumber)
_uiState. update { it. copy (currentSurah = surah) }
}
}
}
ViewModels should never hold references to Activity or View instances to avoid memory leaks.
Dependency injection
Nimaz uses Hilt for dependency injection throughout all layers.
DI modules
Located in core/di/:
DatabaseModule.kt - Provides Room database and DAOs
@Module
@InstallIn (SingletonComponent:: class )
object DatabaseModule {
@Provides
@Singleton
fun provideNimazDatabase (
@ApplicationContext context: Context
): NimazDatabase {
return Room. databaseBuilder (
context,
NimazDatabase:: class .java,
NimazDatabase.DATABASE_NAME
). addMigrations (
NimazDatabase.MIGRATION_7_8,
NimazDatabase.MIGRATION_8_9
). build ()
}
}
DataStoreModule.kt - Provides DataStore for preferences
RepositoryModule.kt - Binds repository implementations to interfaces
@Binds
@Singleton
abstract fun bindQuranRepository (
quranRepositoryImpl: QuranRepositoryImpl
): QuranRepository
UseCaseModule.kt - Provides use case groups
@Provides
@Singleton
fun provideQuranUseCases (
repository: QuranRepository
): QuranUseCases {
return QuranUseCases (
getSurahList = GetSurahListUseCase (repository),
getSurahWithAyahs = GetSurahWithAyahsUseCase (repository),
// ... more use cases
)
}
Application class
The app is initialized in NimazApp.kt with @HiltAndroidApp:
@HiltAndroidApp
class NimazApp : Application (), Configuration. Provider {
@Inject lateinit var workerFactory: HiltWorkerFactory
@Inject lateinit var prayerNotificationScheduler: PrayerNotificationScheduler
override fun onCreate () {
super . onCreate ()
applySavedLocale ()
scheduleInitialNotifications ()
downloadDefaultAdhanIfNeeded ()
}
}
Data flow
The unidirectional data flow in Nimaz:
User interacts with Compose UI
UI calls ViewModel function
ViewModel invokes use case
Use case calls repository
Repository fetches/updates data
Data flows back through layers
ViewModel updates UI state
Compose UI recomposes with new state
Best practices
Single responsibility Each class has one clear purpose and reason to change
Dependency inversion Depend on abstractions (interfaces) not implementations
Reactive streams Use Flow for observable data streams
Immutable state UI state is immutable and updated via copy()
Testing strategy
Each layer is independently testable:
Domain : Test use cases in isolation with mocked repositories
Data : Test repositories with fake data sources
Presentation : Test ViewModels with fake use cases, test UI with Compose testing
// Example unit test
class GetSurahListUseCaseTest {
@Test
fun `invoke returns surah list from repository` () = runTest {
val mockRepository = mockk < QuranRepository >()
coEvery { mockRepository. getSurahList () } returns listOf ( /*...*/ )
val useCase = GetSurahListUseCase (mockRepository)
val result = useCase ()
assertThat (result). hasSize ( 114 )
}
}
When adding new features, create the domain layer first (models, repository interface, use cases), then implement the data layer, and finally build the UI.