feat: Implement core data architecture (Room, Repository, ViewModels) and UI integration

This commit is contained in:
2025-10-10 22:56:12 +02:00
parent d38c4c5517
commit 8d1f45c93f
19 changed files with 441 additions and 31 deletions

View File

@@ -1,7 +1,7 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.google.devtools.ksp)
}
android {
@@ -37,18 +37,28 @@ android {
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get()
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)
// Room Database
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

View File

@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools">
<application
android:name=".NoteshopApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"

View File

@@ -0,0 +1,31 @@
package de.lxtools.noteshop
import android.app.Application
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.viewModelFactory
import androidx.lifecycle.viewmodel.initializer
import de.lxtools.noteshop.ui.notes.NotesViewModel
import de.lxtools.noteshop.ui.shopping.ShoppingListsViewModel
/**
* Provides Factory to create ViewModels for the entire app
*/
object AppViewModelProvider {
val Factory = viewModelFactory {
// Initializer for NotesViewModel
initializer {
val application = (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as NoteshopApplication)
NotesViewModel(
application.container.noteshopRepository
)
}
// Initializer for ShoppingListsViewModel
initializer {
val application = (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as NoteshopApplication)
ShoppingListsViewModel(
application.container.noteshopRepository
)
}
}
}

View File

@@ -5,6 +5,7 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -12,9 +13,8 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
@@ -41,11 +41,15 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.lxtools.noteshop.ui.notes.NotesScreen
import de.lxtools.noteshop.ui.notes.NotesViewModel
import de.lxtools.noteshop.ui.shopping.ShoppingListsScreen
import de.lxtools.noteshop.ui.shopping.ShoppingListsViewModel
import de.lxtools.noteshop.ui.theme.NoteshopTheme
import kotlinx.coroutines.launch
@@ -69,7 +73,10 @@ class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppShell() {
fun AppShell(
notesViewModel: NotesViewModel = viewModel(factory = AppViewModelProvider.Factory),
shoppingListsViewModel: ShoppingListsViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
var currentScreen: Screen by remember { mutableStateOf(Screen.ShoppingLists) }
@@ -144,10 +151,10 @@ fun AppShell() {
) {
when (currentScreen) {
is Screen.ShoppingLists -> {
Text(text = "Hier kommen die Einkaufslisten hin.")
ShoppingListsScreen(viewModel = shoppingListsViewModel)
}
is Screen.Notes -> {
Text(text = "Hier kommen die Notizen hin.")
NotesScreen(viewModel = notesViewModel)
}
}
}

View File

@@ -0,0 +1,40 @@
package de.lxtools.noteshop
import android.app.Application
import de.lxtools.noteshop.data.AppDatabase
import de.lxtools.noteshop.data.NoteshopRepository
import de.lxtools.noteshop.data.OfflineNoteshopRepository
/**
* The application class, which creates and holds the dependency container.
*/
class NoteshopApplication : Application() {
/**
* AppContainer instance used by the rest of classes to obtain dependencies
*/
lateinit var container: AppContainer
override fun onCreate() {
super.onCreate()
container = AppDataContainer(this)
}
}
/**
* Interface for the dependency container.
*/
interface AppContainer {
val noteshopRepository: NoteshopRepository
}
/**
* Implementation for the dependency container.
*/
class AppDataContainer(private val context: Application) : AppContainer {
override val noteshopRepository: NoteshopRepository by lazy {
OfflineNoteshopRepository(
noteDao = AppDatabase.getDatabase(context).noteDao(),
shoppingListDao = AppDatabase.getDatabase(context).shoppingListDao()
)
}
}

View File

@@ -0,0 +1,37 @@
package de.lxtools.noteshop.data
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(
entities = [Note::class, ShoppingList::class, ShoppingListItem::class],
version = 1,
exportSchema = false // For simplicity, we disable schema exporting for now
)
abstract class AppDatabase : RoomDatabase() {
abstract fun noteDao(): NoteDao
abstract fun shoppingListDao(): ShoppingListDao
companion object {
// Volatile ensures that the instance is always up-to-date and the same for all execution threads.
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
// If the INSTANCE is not null, then return it, otherwise create the database
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"noteshop_database"
).build()
INSTANCE = instance
// return instance
instance
}
}
}
}

View File

@@ -0,0 +1,12 @@
package de.lxtools.noteshop.data
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "notes")
data class Note(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val title: String,
val content: String
)

View File

@@ -0,0 +1,28 @@
package de.lxtools.noteshop.data
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
@Dao
interface NoteDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(note: Note)
@Update
suspend fun update(note: Note)
@Delete
suspend fun delete(note: Note)
@Query("SELECT * FROM notes WHERE id = :id")
fun getNote(id: Int): Flow<Note?>
@Query("SELECT * FROM notes ORDER BY id DESC")
fun getAllNotes(): Flow<List<Note>>
}

View File

@@ -0,0 +1,27 @@
package de.lxtools.noteshop.data
import kotlinx.coroutines.flow.Flow
/**
* Repository that provides insert, update, delete, and retrieve of data from a given data source.
*/
interface NoteshopRepository {
/**
* Retrieve all the notes from the the data source.
*/
fun getAllNotesStream(): Flow<List<Note>>
/**
* Retrieve all the shopping lists with their items from the data source.
*/
fun getAllShoppingListsWithItemsStream(): Flow<List<ShoppingListWithItems>>
// Add other necessary methods for inserting, updating, deleting data
}
class OfflineNoteshopRepository(private val noteDao: NoteDao, private val shoppingListDao: ShoppingListDao) : NoteshopRepository {
override fun getAllNotesStream(): Flow<List<Note>> = noteDao.getAllNotes()
override fun getAllShoppingListsWithItemsStream(): Flow<List<ShoppingListWithItems>> = shoppingListDao.getListsWithItems()
}

View File

@@ -0,0 +1,11 @@
package de.lxtools.noteshop.data
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "shopping_lists")
data class ShoppingList(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val name: String
)

View File

@@ -0,0 +1,43 @@
package de.lxtools.noteshop.data
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
@Dao
interface ShoppingListDao {
// --- ShoppingList specific methods ---
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertList(list: ShoppingList)
@Update
suspend fun updateList(list: ShoppingList)
@Delete
suspend fun deleteList(list: ShoppingList)
// --- ShoppingListItem specific methods ---
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertItem(item: ShoppingListItem)
@Update
suspend fun updateItem(item: ShoppingListItem)
@Delete
suspend fun deleteItem(item: ShoppingListItem)
// --- Methods for combined data ---
@Transaction
@Query("SELECT * FROM shopping_lists")
fun getListsWithItems(): Flow<List<ShoppingListWithItems>>
@Transaction
@Query("SELECT * FROM shopping_lists WHERE id = :listId")
fun getListWithItems(listId: Int): Flow<ShoppingListWithItems?>
}

View File

@@ -0,0 +1,27 @@
package de.lxtools.noteshop.data
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(
tableName = "shopping_list_items",
foreignKeys = [
ForeignKey(
entity = ShoppingList::class,
parentColumns = ["id"],
childColumns = ["list_id"],
onDelete = ForeignKey.CASCADE // If a list is deleted, its items are also deleted
)
]
)
data class ShoppingListItem(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val name: String,
@ColumnInfo(name = "is_checked")
val isChecked: Boolean = false,
@ColumnInfo(name = "list_id", index = true)
val listId: Int
)

View File

@@ -0,0 +1,13 @@
package de.lxtools.noteshop.data
import androidx.room.Embedded
import androidx.room.Relation
data class ShoppingListWithItems(
@Embedded val shoppingList: ShoppingList,
@Relation(
parentColumn = "id",
entityColumn = "list_id"
)
val items: List<ShoppingListItem>
)

View File

@@ -0,0 +1,28 @@
package de.lxtools.noteshop.ui.notes
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun NotesScreen(viewModel: NotesViewModel, modifier: Modifier = Modifier) {
val uiState by viewModel.uiState.collectAsState()
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(text = "Anzahl der Notizen: ${uiState.noteList.size}")
// TODO: Implement LazyColumn to display actual notes
uiState.noteList.forEach { note ->
Text(text = "- ${note.title}: ${note.content}")
}
}
}

View File

@@ -0,0 +1,28 @@
package de.lxtools.noteshop.ui.notes
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.lxtools.noteshop.data.Note
import de.lxtools.noteshop.data.NoteshopRepository
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
class NotesViewModel(noteshopRepository: NoteshopRepository) : ViewModel() {
val uiState: StateFlow<NotesUiState> = noteshopRepository.getAllNotesStream().map { NotesUiState(it) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS),
initialValue = NotesUiState()
)
companion object {
private const val TIMEOUT_MILLIS = 5_000L
}
}
data class NotesUiState(
val noteList: List<Note> = listOf()
)

View File

@@ -0,0 +1,28 @@
package de.lxtools.noteshop.ui.shopping
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun ShoppingListsScreen(viewModel: ShoppingListsViewModel, modifier: Modifier = Modifier) {
val uiState by viewModel.uiState.collectAsState()
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(text = "Anzahl der Einkaufslisten: ${uiState.shoppingLists.size}")
// TODO: Implement LazyColumn to display actual shopping lists
uiState.shoppingLists.forEach { listWithItems ->
Text(text = "- ${listWithItems.shoppingList.name} (${listWithItems.items.size} Artikel)")
}
}
}

View File

@@ -0,0 +1,28 @@
package de.lxtools.noteshop.ui.shopping
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.lxtools.noteshop.data.NoteshopRepository
import de.lxtools.noteshop.data.ShoppingListWithItems
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
class ShoppingListsViewModel(noteshopRepository: NoteshopRepository) : ViewModel() {
val uiState: StateFlow<ShoppingListsUiState> = noteshopRepository.getAllShoppingListsWithItemsStream().map { ShoppingListsUiState(it) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS),
initialValue = ShoppingListsUiState()
)
companion object {
private const val TIMEOUT_MILLIS = 5_000L
}
}
data class ShoppingListsUiState(
val shoppingLists: List<ShoppingListWithItems> = listOf()
)

View File

@@ -2,5 +2,4 @@
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
}

View File

@@ -1,32 +1,44 @@
[versions]
agp = "8.13.0"
kotlin = "2.0.21"
coreKtx = "1.10.1"
activity-compose = "1.9.0"
compose-bom = "2024.06.00"
espresso-core = "3.6.1"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
lifecycleRuntimeKtx = "2.6.1"
activityCompose = "1.8.0"
composeBom = "2024.09.00"
junit-version = "1.2.1"
lifecycle-runtime-ktx = "2.8.3"
lifecycle-viewmodel-compose = "2.8.3"
core-ktx = "1.13.1"
room = "2.6.1"
ksp = "1.9.22-1.0.17"
agp = "8.4.1"
kotlin = "1.9.22"
compose-compiler = "1.5.8"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "core-ktx" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" }
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" }
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso-core" }
androidx-junit = { module = "androidx.test.ext:junit", version.ref = "junit-version" }
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-ktx" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle-viewmodel-compose" }
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
androidx-compose-ui = { module = "androidx.compose.ui:ui" }
androidx-compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics" }
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" }
androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
junit = { module = "junit:junit", version.ref = "junit" }
# Room database libraries
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
[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" }
google-devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
[bundles]