Compare commits
1 Commits
main
...
2-Migrate-
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ddad7e5e7 |
@@ -4,6 +4,7 @@ plugins {
|
||||
alias(libs.plugins.kotlin.compose) // v1.5.0: Jetpack Compose Compiler
|
||||
alias(libs.plugins.ktlint) // ✅ v1.6.1: Reaktiviert nach Code-Cleanup
|
||||
alias(libs.plugins.detekt)
|
||||
alias(libs.plugins.ksp)
|
||||
}
|
||||
|
||||
import java.util.Properties
|
||||
@@ -162,6 +163,15 @@ dependencies {
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
|
||||
// Koin
|
||||
implementation(libs.koin.android)
|
||||
implementation(libs.koin.androidx.compose)
|
||||
|
||||
// Room
|
||||
implementation(libs.room.runtime)
|
||||
implementation(libs.room.ktx)
|
||||
ksp(libs.room.compiler)
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// 🆕 v1.8.0: Homescreen Widgets
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -36,8 +36,6 @@ import android.widget.CheckBox
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import dev.dettmer.simplenotes.sync.WebDavSyncService
|
||||
|
||||
@@ -6,6 +6,10 @@ import dev.dettmer.simplenotes.utils.Logger
|
||||
import dev.dettmer.simplenotes.sync.NetworkMonitor
|
||||
import dev.dettmer.simplenotes.utils.NotificationHelper
|
||||
import dev.dettmer.simplenotes.utils.Constants
|
||||
import dev.dettmer.simplenotes.di.appModule
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.android.ext.koin.androidLogger
|
||||
import org.koin.core.context.startKoin
|
||||
|
||||
class SimpleNotesApplication : Application() {
|
||||
|
||||
@@ -30,6 +34,12 @@ class SimpleNotesApplication : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
startKoin {
|
||||
androidLogger() // Log Koin events
|
||||
androidContext(this@SimpleNotesApplication) // Provide context to modules
|
||||
modules(appModule)
|
||||
}
|
||||
|
||||
val prefs = getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
// 🔧 Hotfix v1.6.2: Migrate offline mode setting BEFORE any ViewModel initialization
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package dev.dettmer.simplenotes.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import dev.dettmer.simplenotes.storage.AppDatabase
|
||||
import dev.dettmer.simplenotes.storage.NotesStorage
|
||||
import dev.dettmer.simplenotes.ui.main.MainViewModel
|
||||
import dev.dettmer.simplenotes.utils.Constants
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
|
||||
val appModule = module {
|
||||
single {
|
||||
Room.databaseBuilder(
|
||||
androidContext(),
|
||||
AppDatabase::class.java,
|
||||
"notes_database"
|
||||
).build()
|
||||
}
|
||||
|
||||
single { get<AppDatabase>().noteDao() }
|
||||
single { get<AppDatabase>().deletedNoteDao() }
|
||||
|
||||
// Provide SharedPreferences
|
||||
single {
|
||||
androidContext().getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
single { NotesStorage(androidContext(), get()) }
|
||||
|
||||
viewModel { MainViewModel(get(), get()) }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package dev.dettmer.simplenotes.storage
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import dev.dettmer.simplenotes.storage.dao.DeletedNoteDao
|
||||
import dev.dettmer.simplenotes.storage.dao.NoteDao
|
||||
import dev.dettmer.simplenotes.storage.entity.DeletedNoteEntity
|
||||
import dev.dettmer.simplenotes.storage.entity.NoteEntity
|
||||
|
||||
@Database(entities = [NoteEntity::class, DeletedNoteEntity::class], version = 1)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun noteDao(): NoteDao
|
||||
abstract fun deletedNoteDao(): DeletedNoteDao
|
||||
}
|
||||
@@ -1,76 +1,58 @@
|
||||
package dev.dettmer.simplenotes.storage
|
||||
|
||||
import android.content.Context
|
||||
import dev.dettmer.simplenotes.models.DeletionTracker
|
||||
import dev.dettmer.simplenotes.models.Note
|
||||
import dev.dettmer.simplenotes.models.SyncStatus
|
||||
import dev.dettmer.simplenotes.storage.entity.DeletedNoteEntity
|
||||
import dev.dettmer.simplenotes.storage.entity.NoteEntity
|
||||
import dev.dettmer.simplenotes.utils.DeviceIdGenerator
|
||||
import dev.dettmer.simplenotes.utils.Logger
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.io.File
|
||||
|
||||
class NotesStorage(private val context: Context) {
|
||||
class NotesStorage(
|
||||
private val context: Context,
|
||||
database: AppDatabase
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "NotesStorage"
|
||||
// 🔒 v1.7.2 (IMPL_001): Mutex für thread-sichere Deletion Tracker Operationen
|
||||
private val deletionTrackerMutex = Mutex()
|
||||
}
|
||||
|
||||
private val notesDir: File = File(context.filesDir, "notes").apply {
|
||||
if (!exists()) mkdirs()
|
||||
private val noteDao = database.noteDao()
|
||||
private val deletedNoteDao = database.deletedNoteDao()
|
||||
|
||||
suspend fun saveNote(note: NoteEntity) {
|
||||
noteDao.saveNote(note)
|
||||
}
|
||||
|
||||
fun saveNote(note: Note) {
|
||||
val file = File(notesDir, "${note.id}.json")
|
||||
file.writeText(note.toJson())
|
||||
suspend fun loadNote(id: String): NoteEntity? {
|
||||
return noteDao.getNote(id)
|
||||
}
|
||||
|
||||
fun loadNote(id: String): Note? {
|
||||
val file = File(notesDir, "$id.json")
|
||||
return if (file.exists()) {
|
||||
Note.fromJson(file.readText())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
suspend fun loadAllNotes(): List<NoteEntity> {
|
||||
return noteDao.getAllNotes()
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt alle Notizen aus dem lokalen Speicher.
|
||||
*
|
||||
* 🔀 v1.8.0: Sortierung entfernt — wird jetzt im ViewModel durchgeführt,
|
||||
* damit der User die Sortierung konfigurieren kann.
|
||||
*/
|
||||
fun loadAllNotes(): List<Note> {
|
||||
return notesDir.listFiles()
|
||||
?.filter { it.extension == "json" }
|
||||
?.mapNotNull { Note.fromJson(it.readText()) }
|
||||
?: emptyList()
|
||||
}
|
||||
suspend fun deleteNote(id: String): Boolean {
|
||||
val deletedRows = noteDao.deleteNoteById(id)
|
||||
|
||||
fun deleteNote(id: String): Boolean {
|
||||
val file = File(notesDir, "$id.json")
|
||||
val deleted = file.delete()
|
||||
|
||||
if (deleted) {
|
||||
if (deletedRows > 0) {
|
||||
Logger.d(TAG, "🗑️ Deleted note: $id")
|
||||
|
||||
// Track deletion to prevent zombie notes
|
||||
val deviceId = DeviceIdGenerator.getDeviceId(context)
|
||||
trackDeletion(id, deviceId)
|
||||
trackDeletionSafe(id, deviceId)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return deleted
|
||||
}
|
||||
|
||||
fun deleteAllNotes(): Boolean {
|
||||
suspend fun deleteAllNotes(): Boolean {
|
||||
return try {
|
||||
val notes = loadAllNotes()
|
||||
val deviceId = DeviceIdGenerator.getDeviceId(context)
|
||||
|
||||
for (note in notes) {
|
||||
deleteNote(note.id) // Uses trackDeletion() automatically
|
||||
// Batch tracking and deleting
|
||||
notes.forEach { note ->
|
||||
trackDeletionSafe(note.id, deviceId)
|
||||
}
|
||||
noteDao.deleteAllNotes()
|
||||
|
||||
Logger.d(TAG, "🗑️ Deleted all notes (${notes.size} notes)")
|
||||
true
|
||||
@@ -82,101 +64,28 @@ class NotesStorage(private val context: Context) {
|
||||
|
||||
// === Deletion Tracking ===
|
||||
|
||||
private fun getDeletionTrackerFile(): File {
|
||||
return File(context.filesDir, "deleted_notes.json")
|
||||
}
|
||||
|
||||
fun loadDeletionTracker(): DeletionTracker {
|
||||
val file = getDeletionTrackerFile()
|
||||
if (!file.exists()) {
|
||||
return DeletionTracker()
|
||||
}
|
||||
|
||||
return try {
|
||||
val json = file.readText()
|
||||
DeletionTracker.fromJson(json) ?: DeletionTracker()
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "Failed to load deletion tracker", e)
|
||||
DeletionTracker()
|
||||
}
|
||||
}
|
||||
|
||||
fun saveDeletionTracker(tracker: DeletionTracker) {
|
||||
try {
|
||||
val file = getDeletionTrackerFile()
|
||||
file.writeText(tracker.toJson())
|
||||
|
||||
if (tracker.deletedNotes.size > 1000) {
|
||||
Logger.w(TAG, "⚠️ Deletion tracker large: ${tracker.deletedNotes.size} entries")
|
||||
}
|
||||
|
||||
Logger.d(TAG, "✅ Deletion tracker saved (${tracker.deletedNotes.size} entries)")
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "Failed to save deletion tracker", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔒 v1.7.2 (IMPL_001): Thread-sichere Deletion-Tracking mit Mutex
|
||||
*
|
||||
* Verhindert Race Conditions bei Batch-Deletes durch exklusiven Zugriff
|
||||
* auf den Deletion Tracker.
|
||||
*
|
||||
* @param noteId ID der gelöschten Notiz
|
||||
* @param deviceId Geräte-ID für Konflikt-Erkennung
|
||||
*/
|
||||
suspend fun trackDeletionSafe(noteId: String, deviceId: String) {
|
||||
deletionTrackerMutex.withLock {
|
||||
val tracker = loadDeletionTracker()
|
||||
tracker.addDeletion(noteId, deviceId)
|
||||
saveDeletionTracker(tracker)
|
||||
Logger.d(TAG, "📝 Tracked deletion (mutex-protected): $noteId")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy-Methode ohne Mutex-Schutz.
|
||||
* Verwendet für synchrone Aufrufe wo Coroutines nicht verfügbar sind.
|
||||
*
|
||||
* @deprecated Verwende trackDeletionSafe() für Thread-Safety wo möglich
|
||||
*/
|
||||
fun trackDeletion(noteId: String, deviceId: String) {
|
||||
val tracker = loadDeletionTracker()
|
||||
tracker.addDeletion(noteId, deviceId)
|
||||
saveDeletionTracker(tracker)
|
||||
// Room handles internal transactions and thread-safety natively.
|
||||
// The Mutex is no longer required.
|
||||
deletedNoteDao.trackDeletion(DeletedNoteEntity(noteId, deviceId))
|
||||
Logger.d(TAG, "📝 Tracked deletion: $noteId")
|
||||
}
|
||||
|
||||
fun isNoteDeleted(noteId: String): Boolean {
|
||||
val tracker = loadDeletionTracker()
|
||||
return tracker.isDeleted(noteId)
|
||||
suspend fun isNoteDeleted(noteId: String): Boolean {
|
||||
return deletedNoteDao.isNoteDeleted(noteId)
|
||||
}
|
||||
|
||||
fun clearDeletionTracker() {
|
||||
saveDeletionTracker(DeletionTracker())
|
||||
suspend fun clearDeletionTracker() {
|
||||
deletedNoteDao.clearTracker()
|
||||
Logger.d(TAG, "🗑️ Deletion tracker cleared")
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔄 v1.7.0: Reset all sync statuses to PENDING when server changes
|
||||
* This ensures notes are uploaded to the new server on next sync
|
||||
*/
|
||||
fun resetAllSyncStatusToPending(): Int {
|
||||
val notes = loadAllNotes()
|
||||
var updatedCount = 0
|
||||
|
||||
notes.forEach { note ->
|
||||
if (note.syncStatus == dev.dettmer.simplenotes.models.SyncStatus.SYNCED) {
|
||||
val updatedNote = note.copy(syncStatus = dev.dettmer.simplenotes.models.SyncStatus.PENDING)
|
||||
saveNote(updatedNote)
|
||||
updatedCount++
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun resetAllSyncStatusToPending(): Int {
|
||||
val updatedCount = noteDao.updateSyncStatus(
|
||||
oldStatus = SyncStatus.SYNCED,
|
||||
newStatus = SyncStatus.PENDING
|
||||
)
|
||||
Logger.d(TAG, "🔄 Reset sync status for $updatedCount notes to PENDING")
|
||||
return updatedCount
|
||||
}
|
||||
|
||||
|
||||
fun getNotesDir(): File = notesDir
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package dev.dettmer.simplenotes.storage.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import dev.dettmer.simplenotes.storage.entity.DeletedNoteEntity
|
||||
|
||||
@Dao
|
||||
interface DeletedNoteDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun trackDeletion(deletedNote: DeletedNoteEntity)
|
||||
|
||||
@Query("SELECT EXISTS(SELECT 1 FROM deleted_notes WHERE noteId = :noteId)")
|
||||
suspend fun isNoteDeleted(noteId: String): Boolean
|
||||
|
||||
@Query("DELETE FROM deleted_notes")
|
||||
suspend fun clearTracker()
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package dev.dettmer.simplenotes.storage.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import dev.dettmer.simplenotes.models.SyncStatus
|
||||
import dev.dettmer.simplenotes.storage.entity.NoteEntity
|
||||
|
||||
@Dao
|
||||
interface NoteDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun saveNote(note: NoteEntity)
|
||||
|
||||
@Query("SELECT * FROM notes WHERE id = :id")
|
||||
suspend fun getNote(id: String): NoteEntity?
|
||||
|
||||
@Query("SELECT * FROM notes")
|
||||
suspend fun getAllNotes(): List<NoteEntity>
|
||||
|
||||
@Query("DELETE FROM notes WHERE id = :id")
|
||||
suspend fun deleteNoteById(id: String): Int
|
||||
|
||||
@Query("DELETE FROM notes")
|
||||
suspend fun deleteAllNotes(): Int
|
||||
|
||||
@Query("UPDATE notes SET syncStatus = :newStatus WHERE syncStatus = :oldStatus")
|
||||
suspend fun updateSyncStatus(oldStatus: SyncStatus, newStatus: SyncStatus): Int
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package dev.dettmer.simplenotes.storage.entity
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "deleted_notes")
|
||||
data class DeletedNoteEntity(
|
||||
@PrimaryKey val noteId: String,
|
||||
val deviceId: String,
|
||||
val deletedAt: Long = System.currentTimeMillis()
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
package dev.dettmer.simplenotes.storage.entity
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import dev.dettmer.simplenotes.models.SyncStatus
|
||||
|
||||
@Entity(tableName = "notes")
|
||||
data class NoteEntity(
|
||||
@PrimaryKey val id: String,
|
||||
val content: String,
|
||||
val timestamp: Long,
|
||||
val syncStatus: SyncStatus
|
||||
)
|
||||
@@ -44,6 +44,7 @@ import dev.dettmer.simplenotes.utils.Constants
|
||||
import dev.dettmer.simplenotes.utils.Logger
|
||||
import dev.dettmer.simplenotes.utils.NotificationHelper
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
/**
|
||||
* Main Activity with Jetpack Compose UI
|
||||
@@ -65,7 +66,7 @@ class ComposeMainActivity : ComponentActivity() {
|
||||
private const val REQUEST_SETTINGS = 1002
|
||||
}
|
||||
|
||||
private val viewModel: MainViewModel by viewModels()
|
||||
private val viewModel: MainViewModel by viewModel()
|
||||
|
||||
private val prefs by lazy {
|
||||
getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
@@ -59,6 +59,7 @@ import dev.dettmer.simplenotes.ui.main.components.NotesStaggeredGrid
|
||||
import dev.dettmer.simplenotes.ui.main.components.SyncProgressBanner
|
||||
import dev.dettmer.simplenotes.ui.main.components.SyncStatusLegendDialog
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
private const val TIMESTAMP_UPDATE_INTERVAL_MS = 30_000L
|
||||
|
||||
@@ -74,7 +75,7 @@ private const val TIMESTAMP_UPDATE_INTERVAL_MS = 30_000L
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
viewModel: MainViewModel,
|
||||
viewModel: MainViewModel = koinViewModel(),
|
||||
onOpenNote: (String?) -> Unit,
|
||||
onOpenSettings: () -> Unit,
|
||||
onCreateNote: (NoteType) -> Unit
|
||||
|
||||
@@ -2,7 +2,9 @@ package dev.dettmer.simplenotes.ui.main
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dev.dettmer.simplenotes.models.Note
|
||||
import dev.dettmer.simplenotes.models.SortDirection
|
||||
@@ -34,7 +36,10 @@ import kotlinx.coroutines.withContext
|
||||
*
|
||||
* Manages notes list, sync state, and deletion with undo.
|
||||
*/
|
||||
class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
class MainViewModel(
|
||||
private val storage: NotesStorage,
|
||||
private val prefs: SharedPreferences
|
||||
) : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "MainViewModel"
|
||||
@@ -42,9 +47,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private const val PREF_LAST_AUTO_SYNC_TIME = "last_auto_sync_timestamp"
|
||||
}
|
||||
|
||||
private val storage = NotesStorage(application)
|
||||
private val prefs = application.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// Notes State
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
@@ -207,7 +209,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private suspend fun loadNotesAsync() {
|
||||
val allNotes = storage.loadAllNotes()
|
||||
val pendingIds = _pendingDeletions.value
|
||||
val filteredNotes = allNotes.filter { it.id !in pendingIds }
|
||||
val filteredNotes = allNotes.filter { it.id !in pendingIds }.map { Note(
|
||||
id = it.id,
|
||||
content = it.content
|
||||
) }
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
// Phase 3: Detect if a new note was added at the top
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package dev.dettmer.simplenotes
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,9 @@ composeBom = "2026.01.00"
|
||||
navigationCompose = "2.7.6"
|
||||
lifecycleRuntimeCompose = "2.7.0"
|
||||
activityCompose = "1.8.2"
|
||||
room = "2.6.1"
|
||||
ksp = "2.0.0-1.0.21"
|
||||
koin = "3.5.3"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
@@ -37,6 +40,14 @@ androidx-compose-material-icons = { group = "androidx.compose.material", name =
|
||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
|
||||
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycleRuntimeCompose" }
|
||||
# Room Database
|
||||
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
|
||||
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
|
||||
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
|
||||
# Core Koin for Kotlin projects
|
||||
koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" }
|
||||
# Koin for Jetpack Compose integration
|
||||
koin-androidx-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
@@ -44,4 +55,5 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }
|
||||
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
|
||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user