nordiccalendar

A simple Material 3 native calendar app fitting my personal requirements
git clone git://archive.git.mtrnord.blog/MTRNord/nordiccalendar.git
Log | Files | Refs | README | LICENSE

commit 13b525ff9b839be11f005f1a3ba8b45533529534
parent 742d70bb437617758f0f381e26baa51189d4caaf
Author: MTRNord <MTRNord@users.noreply.github.com>
Date:   Tue, 29 Jul 2025 11:44:22 +0200

Use dependency injection

Diffstat:
Mapp/build.gradle.kts | 5+++++
Mapp/src/main/AndroidManifest.xml | 1+
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/MainActivity.kt | 13++++++++-----
Aapp/src/main/java/space/midnightthoughts/nordiccalendar/NordicCalendarApp.kt | 7+++++++
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/components/SidebarDrawer.kt | 4++--
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/data/CalendarRepository.kt | 21++++++---------------
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/screens/CalendarScreen.kt | 5+++--
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/screens/EventDetailsView.kt | 28++--------------------------
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/CalendarViewModel.kt | 16++++++++++------
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/EventDetailsViewModel.kt | 18+++++++++++-------
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/SidebarDrawerViewModel.kt | 17+++++++++++------
Mbuild.gradle.kts | 2++
Mgradle/libs.versions.toml | 9++++++++-
13 files changed, 76 insertions(+), 70 deletions(-)

diff --git a/app/build.gradle.kts b/app/build.gradle.kts @@ -2,6 +2,8 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) + alias(libs.plugins.ksp) + alias(libs.plugins.hilt.android) } android { @@ -58,6 +60,9 @@ dependencies { implementation(libs.androidx.navigation.compose) implementation(libs.androidx.foundation.foundation) implementation(libs.autolinktext) + implementation(libs.hilt.android) + ksp(libs.hilt.android.compiler) + implementation(libs.hilt.navigation.compose) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ <uses-permission android:name="android.permission.WRITE_CALENDAR" /> <application + android:name=".NordicCalendarApp" android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/MainActivity.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/MainActivity.kt @@ -43,7 +43,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavBackStackEntry import androidx.navigation.NavHostController import androidx.navigation.NavType @@ -51,6 +51,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument +import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch import space.midnightthoughts.nordiccalendar.components.AppScaffold import space.midnightthoughts.nordiccalendar.onboarding.OnBoardItem @@ -69,6 +70,7 @@ sealed class Destinations(val route: String) { object EventDetails : Destinations("eventDetails/{eventId}") } +@AndroidEntryPoint class MainActivity : ComponentActivity() { @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { @@ -226,9 +228,11 @@ fun IntroScreen(navController: NavHostController, onFinish: (() -> Unit)? = null } @Composable -fun CalendarView(backStackEntry: NavBackStackEntry, navController: NavHostController) { - val calendarViewModel: CalendarViewModel = viewModel() - +fun CalendarView( + backStackEntry: NavBackStackEntry, + navController: NavHostController +) { + val calendarViewModel: CalendarViewModel = hiltViewModel() val selectedTab = remember(calendarViewModel) { calendarViewModel.selectedTab @@ -259,7 +263,6 @@ fun CalendarView(backStackEntry: NavBackStackEntry, navController: NavHostContro onTabSelected = { calendarViewModel.setTab(it) }, modifier = innerPadding, navController = navController, - calendarViewModel = calendarViewModel, ) } } diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/NordicCalendarApp.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/NordicCalendarApp.kt @@ -0,0 +1,7 @@ +package space.midnightthoughts.nordiccalendar + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class NordicCalendarApp : Application() diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/components/SidebarDrawer.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/components/SidebarDrawer.kt @@ -25,7 +25,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import androidx.navigation.NavGraph.Companion.findStartDestination import kotlinx.coroutines.launch @@ -39,7 +39,7 @@ fun SidebarDrawer( selectedDestination: String, drawerState: DrawerState ) { - val viewModel: SidebarDrawerViewModel = viewModel() + val viewModel: SidebarDrawerViewModel = hiltViewModel() val calendars = remember(viewModel) { viewModel.calendars diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/data/CalendarRepository.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/data/CalendarRepository.kt @@ -2,6 +2,7 @@ package space.midnightthoughts.nordiccalendar.data import android.content.Context import androidx.core.content.edit +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -12,16 +13,17 @@ import kotlinx.coroutines.launch import space.midnightthoughts.nordiccalendar.util.Calendar import space.midnightthoughts.nordiccalendar.util.CalendarData import space.midnightthoughts.nordiccalendar.util.Event +import javax.inject.Inject +import javax.inject.Singleton -class CalendarRepository private constructor() { +@Singleton +class CalendarRepository @Inject constructor(@ApplicationContext context: Context) { private val calendarData = CalendarData() - // StateFlow für Kalenderliste private val _calendarsFlow = MutableStateFlow<List<Calendar>>(emptyList()) val calendarsFlow: StateFlow<List<Calendar>> = _calendarsFlow.asStateFlow() private val repoScope = CoroutineScope(Dispatchers.IO) - // StateFlow für Zeitbereich und Events private val _startMillis = MutableStateFlow(System.currentTimeMillis()) val startMillis: StateFlow<Long> = _startMillis.asStateFlow() private val _endMillis = MutableStateFlow(System.currentTimeMillis()) @@ -29,8 +31,7 @@ class CalendarRepository private constructor() { private val _eventsFlow = MutableStateFlow<List<Event>>(emptyList()) val eventsFlow: StateFlow<List<Event>> = _eventsFlow.asStateFlow() - // Context muss jetzt bei jedem Aufruf übergeben werden - fun initialize(context: Context) { + init { loadCalendarsToFlow(context) repoScope.launch { combine(_calendarsFlow, _startMillis, _endMillis) { calendars, start, end -> @@ -95,14 +96,4 @@ class CalendarRepository private constructor() { fun getEventById(context: Context, eventId: Long): Event? { return calendarData.getEventById(context.contentResolver, eventId) } - - companion object { - @Volatile - private var INSTANCE: CalendarRepository? = null - fun getInstance(): CalendarRepository { - return INSTANCE ?: synchronized(this) { - INSTANCE ?: CalendarRepository().also { INSTANCE = it } - } - } - } } diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/CalendarScreen.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/CalendarScreen.kt @@ -53,6 +53,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -69,11 +70,11 @@ import java.util.Locale @OptIn(ExperimentalMaterial3Api::class) @Composable fun CalendarScreen( + modifier: Modifier = Modifier, + calendarViewModel: CalendarViewModel = viewModel(), selectedTab: Int, onTabSelected: (Int) -> Unit, - modifier: Modifier = Modifier, navController: NavController, - calendarViewModel: CalendarViewModel, ) { val events = remember(calendarViewModel) { calendarViewModel.events diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/EventDetailsView.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/EventDetailsView.kt @@ -19,11 +19,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner -import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavBackStackEntry import androidx.navigation.NavHostController import sh.calvin.autolinktext.rememberAutoLinkText @@ -38,27 +34,7 @@ fun EventDetailsView( backStackEntry: NavBackStackEntry, navController: NavHostController, ) { - val owner = LocalViewModelStoreOwner.current - val context = - androidx.compose.ui.platform.LocalContext.current.applicationContext as android.app.Application - val eventId = backStackEntry.arguments?.getString("eventId") - val viewModel: EventDetailsViewModel = if (owner != null && eventId != null) { - viewModel( - viewModelStoreOwner = owner, - key = "eventDetails_$eventId", - factory = object : ViewModelProvider.Factory { - override fun <T : ViewModel> create(modelClass: Class<T>): T { - @Suppress("UNCHECKED_CAST") - return EventDetailsViewModel( - app = context, - savedStateHandle = SavedStateHandle(mapOf("eventId" to eventId)) - ) as T - } - } - ) - } else { - throw IllegalStateException("ViewModelStoreOwner or eventId is null") - } + val viewModel: EventDetailsViewModel = hiltViewModel() val event = remember(viewModel) { viewModel.event }.collectAsState(initial = null) diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/CalendarViewModel.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/CalendarViewModel.kt @@ -1,18 +1,23 @@ package space.midnightthoughts.nordiccalendar.viewmodels -import android.app.Application -import androidx.lifecycle.AndroidViewModel +import android.content.Context +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.stateIn import space.midnightthoughts.nordiccalendar.data.CalendarRepository +import javax.inject.Inject -class CalendarViewModel(app: Application) : AndroidViewModel(app) { - private val repository = CalendarRepository.getInstance() - +@HiltViewModel +class CalendarViewModel @Inject constructor( + @ApplicationContext private val context: Context, + private val repository: CalendarRepository +) : ViewModel() { val events = repository.eventsFlow.stateIn( viewModelScope, SharingStarted.WhileSubscribed(5000), @@ -27,7 +32,6 @@ class CalendarViewModel(app: Application) : AndroidViewModel(app) { init { // Repository initialisieren (Context benötigt) - repository.initialize(app) setTab(0) } diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/EventDetailsViewModel.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/EventDetailsViewModel.kt @@ -1,21 +1,25 @@ package space.midnightthoughts.nordiccalendar.viewmodels -import android.app.Application -import androidx.lifecycle.AndroidViewModel +import android.content.Context import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import space.midnightthoughts.nordiccalendar.data.CalendarRepository import space.midnightthoughts.nordiccalendar.util.Event +import javax.inject.Inject -class EventDetailsViewModel( - app: Application, +@HiltViewModel +class EventDetailsViewModel @Inject constructor( + @ApplicationContext private val context: Context, + private val repository: CalendarRepository, savedStateHandle: SavedStateHandle -) : AndroidViewModel(app) { - private val repository = CalendarRepository.getInstance() +) : ViewModel() { private val _event = MutableStateFlow<Event?>(null) val event: StateFlow<Event?> = _event.asStateFlow() @@ -28,7 +32,7 @@ class EventDetailsViewModel( private fun loadEvent(eventId: Long) { viewModelScope.launch { - val event = repository.getEventById(getApplication<Application>(), eventId) + val event = repository.getEventById(context, eventId) _event.value = event } } diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/SidebarDrawerViewModel.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/SidebarDrawerViewModel.kt @@ -1,17 +1,22 @@ package space.midnightthoughts.nordiccalendar.viewmodels -import android.app.Application -import androidx.lifecycle.AndroidViewModel +import android.content.Context +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import space.midnightthoughts.nordiccalendar.data.CalendarRepository import space.midnightthoughts.nordiccalendar.util.Calendar +import javax.inject.Inject -class SidebarDrawerViewModel(app: Application) : AndroidViewModel(app) { - private val repository = CalendarRepository.getInstance() - +@HiltViewModel +class SidebarDrawerViewModel @Inject constructor( + @ApplicationContext private val context: Context, + private val repository: CalendarRepository +) : ViewModel() { val calendars = repository.calendarsFlow.stateIn( viewModelScope, SharingStarted.WhileSubscribed(5000), @@ -21,7 +26,7 @@ class SidebarDrawerViewModel(app: Application) : AndroidViewModel(app) { fun toggleCalendar(calendar: Calendar) { viewModelScope.launch { repository.setCalendarSelected( - getApplication<Application>(), + context, calendar.id, !calendar.selected ) diff --git a/build.gradle.kts b/build.gradle.kts @@ -3,4 +3,6 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.compose) apply false + alias(libs.plugins.hilt.android) apply false + alias(libs.plugins.ksp) apply false } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml @@ -12,6 +12,9 @@ accompanistPermissions = "0.37.3" navigationCompose = "2.9.2" foundation = "1.8.3" autolinktext = "2.0.2" +hilt = "2.57" +ksp = "2.2.0-2.0.2" +hiltNavigationCompose = "1.2.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -32,9 +35,13 @@ accompanist-permissions = { group = "com.google.accompanist", name = "accompanis androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } androidx-foundation-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" } autolinktext = { module = "sh.calvin.autolinktext:autolinktext", version.ref = "autolinktext" } +hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } +hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } +hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } - +hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }