First version with MVVM

This commit is contained in:
2025-01-31 21:32:28 +01:00
parent 8902b17d66
commit 450713ae38
10 changed files with 278 additions and 68 deletions

View File

@@ -8,6 +8,6 @@ plugins {
alias(libs.plugins.kotlinJvm) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
kotlin("plugin.serialization") version "1.9.22" apply false
}

View File

@@ -4,11 +4,17 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig
val lifecycle_version = "2.6.1"
val arch_version = "2.2.0"
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidApplication)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
}
kotlin {
@@ -66,10 +72,19 @@ kotlin {
implementation(compose.ui)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
implementation(libs.androidx.lifecycle.livedata.core)
implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.lifecycle.runtime.compose)
implementation(libs.lifecycle.viewmodel.compose)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(projects.shared)
implementation(project.dependencies.platform(libs.koin.bom))
implementation(libs.koin.core)
implementation(libs.koin.compose)
implementation(libs.koin.compose.viewmodel)
}
desktopMain.dependencies {
implementation(compose.desktop.currentOs)
@@ -106,6 +121,7 @@ android {
}
dependencies {
implementation(libs.androidx.lifecycle.livedata.core.ktx)
debugImplementation(compose.uiTooling)
}

View File

@@ -29,76 +29,23 @@ import service.TermService
import service.TermServiceImpl
import service.SearchType
import service.DatabaseFactory
import org.koin.compose.KoinApplication
import org.koin.compose.koinInject
import org.koin.core.parameter.parametersOf
import org.neutrino.ktans.di.appModule
import org.koin.compose.viewmodel.dsl.viewModelOf
import org.neutrino.ktans.view.MainView
@Composable
fun SearchBarTextField() {
val query = remember { mutableStateOf("") }
val termService = TermServiceImpl()
var showResults by remember { mutableStateOf(false) }
var terms: List<TermFull>? = null
TextField(
value = query.value,
onValueChange = {
query.value = it
var sType = SearchType.START
sType = if (query.value.length > 2) {
SearchType.START
} else {
SearchType.EXACT
}
val translation = getTranslationForLanguages("an","sl") ?: return@TextField
println(translation)
terms = termService.getTranslationForTerm(it,translation,sType)
showResults = terms != null
println(terms)
},
placeholder = { Text("Prelož...") },
singleLine = true,
leadingIcon = { Icon(Icons.Filled.Search, contentDescription = "Search Icon") },
trailingIcon = {
if (query.value.isNotEmpty()) {
IconButton(onClick = { query.value = "" }) {
Icon(Icons.Filled.Clear, contentDescription = "Clear Text")
}
}
},
modifier = Modifier
.fillMaxWidth()
)
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) {
if (showResults && terms != null) {
for (term in terms!!) {
Text(term.string1)
Text("${term.string2},")
}
}
}
}
@Composable
@Preview
fun App() {
DatabaseFactory.connectAndMigrate()
DatabaseFactory.connectAll()
MaterialTheme {
var showContent by remember { mutableStateOf(false) }
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
SearchBarTextField()
Button(onClick = { showContent = !showContent }) {
Text("Click me!")
}
AnimatedVisibility(showContent) {
val greeting = remember { Greeting().greet() }
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
Image(painterResource(Res.drawable.compose_multiplatform), null)
Text("Compose: $greeting")
}
}
KoinApplication(application = {
modules(appModule())
}) {
MaterialTheme {
MainView()
}
}
}

View File

@@ -0,0 +1,14 @@
package org.neutrino.ktans.di
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
import service.TermServiceImpl
import service.DatabaseFactory
import org.koin.compose.viewmodel.dsl.viewModelOf
import org.neutrino.ktans.viewmodel.MainModelView
import org.koin.core.module.dsl.factoryOf
fun appModule() = module {
singleOf(::TermServiceImpl)
viewModelOf(::MainModelView)
}

View File

@@ -0,0 +1,114 @@
package org.neutrino.ktans.view
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.Image
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.material.TextField
import androidx.compose.material.IconButton
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Clear
import androidx.compose.runtime.Composable
import kotlinx.coroutines.flow.observeOn
import org.koin.compose.koinInject
import org.neutrino.ktans.viewmodel.MainModelView
import org.neutrino.ktans.viewmodel.Status
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun RowScope.TableCell(
text: String,
weight: Float
) {
Text(
text = text,
Modifier
.border(1.dp, Color.Black)
.weight(weight)
.padding(8.dp)
)
}
@Composable
fun SearchBarTextField(viewModel: MainModelView) {
val query = remember { mutableStateOf("") }
Column {
TextField(
value = query.value,
onValueChange = {
query.value = it
viewModel.getTerms(query.value)
},
placeholder = { Text("Prelož...") },
singleLine = true,
leadingIcon = { Icon(Icons.Filled.Search, contentDescription = "Search Icon") },
trailingIcon = {
if (query.value.isNotEmpty()) {
IconButton(onClick = {
query.value = ""
viewModel.clearSearch()
}) {
Icon(Icons.Filled.Clear, contentDescription = "Clear Text")
}
}
},
modifier = Modifier
.fillMaxWidth()
)
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) {
val terms by viewModel.terms.collectAsState()
if (terms != null) {
SelectionContainer {
Column {
Row(Modifier.background(Color.Gray)) {
TableCell(text = "Anglicky", weight = .5f)
TableCell(text = "Slovensky", weight = .5f)
}
val column1Weight = .5f // 50%
val column2Weight = .5f // 50%
// The LazyColumn will be our table. Notice the use of the weights below
LazyColumn(Modifier.fillMaxSize().padding(0.dp)) {
// Here is the header
items(items = terms!!, itemContent = { t ->
Row(Modifier.fillMaxWidth()) {
TableCell(text = t.string1, weight = column1Weight)
TableCell(text = t.string2, weight = column2Weight)
}
})
}
}
}
}
}
}
}
@Composable
fun MainView(viewModel: MainModelView = koinInject<MainModelView>(),) {
SearchBarTextField(viewModel)
}

View File

@@ -0,0 +1,32 @@
package org.neutrino.ktans.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import models.TermFull
import service.TermService
import service.TermServiceImpl
import service.DatabaseFactory.getTranslationForLanguages
import service.SearchType
import kotlinx.coroutines.Dispatchers
class MainModelView(private val repository: TermServiceImpl): ViewModel() {
val terms : StateFlow<List<TermFull>?> get() = _terms
private val _terms = MutableStateFlow<List<TermFull>?>(null)
fun getTerms(term: String) {
viewModelScope.launch(Dispatchers.IO) {
var sType = SearchType.START
sType = if (term.length > 2) SearchType.START else SearchType.EXACT
val trans = getTranslationForLanguages("an","sl") ?: return@launch
val transTerms = repository.getTranslationForTerm(term,trans,sType)
_terms.value = transTerms
}
}
fun clearSearch() {
_terms.value = listOf()
}
}

View File

@@ -0,0 +1,32 @@
package org.neutrino.ktans.viewmodel
/**
* A generic class that holds a value with its loading status.
* @param <T>
</T> */
data class State<out T>(val status: Status, val data: T?, val message: String?)
{
companion object {
fun <T> success(data: T?): State<T> {
return State(Status.SUCCESS, data, null)
}
fun <T> empty(): State<T> {
return State(Status.SUCCESS, null, null)
}
fun <T> error(msg: String?): State<T> {
return State(Status.ERROR, null, msg)
}
fun <T> loading(): State<T> {
return State(Status.LOADING, null, null)
}
}
}
enum class Status {
SUCCESS,
ERROR,
LOADING
}

View File

@@ -2,12 +2,15 @@ package org.neutrino.ktans
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import service.DatabaseFactory
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "KTrans",
) {
DatabaseFactory.connectAndMigrate()
DatabaseFactory.connectAll()
App()
}
}

View File

@@ -11,16 +11,27 @@ androidx-espresso-core = "3.6.1"
androidx-lifecycle = "2.8.4"
androidx-material = "1.12.0"
androidx-test-junit = "1.2.1"
androidxLifecycleLivedataKtx = "2.6.1"
androidxRuntimeLivedata = "x.x.x"
assertjVersion = "3.26.3"
compose-multiplatform = "1.7.0"
coreKtx = "1.9.0"
exposedVersion = "0.55.0"
flywayVersion = "10.20.1"
h2 = "2.3.232"
hikariCpVersion = "5.1.0"
koin = "4.0.2"
koin-bom = "4.0.2"
koinComposeMultiplatform = "4.0.2"
koin-compose = "1.0.4"
hiltNavigationCompose = "1.2.0"
junit = "4.13.2"
junitVersion = "5.11.3"
kformat = "0.11"
koinAndroidxCompose = "4.0.2)"
koinCore = "3.1.2"
kotlin = "2.1.0"
ksp = "2.1.0-1.0.29"
kotlinSerializationCompilerPluginEmbeddable = "1.9.22"
kotlinx-coroutines = "1.9.0"
kotlinxSerializationCore = "1.8.0"
@@ -45,12 +56,38 @@ ktorServerCore = "3.0.2"
ktorServerCors = "3.0.2"
ktorServerNetty = "3.0.2"
ktorServerRequestValidation = "3.0.2"
lifecycleLivedataCore = "2.8.7"
lifecycleLivedataKtx = "2.8.7"
lifecycleLivedataKtxVersion = "x.x.x"
lifecycleRuntimeCompose = "2.8.7"
lifecycle = "2.3.1"
lifecycleRuntimeComposeVersion = "2.6.1"
lifecycleViewmodelKtx = "2.6.1"
lifecycleViewmodelCompose = "2.8.4"
logback = "1.5.12"
material = "1.7.6"
psqlVersion = "42.7.4"
restAssuredVersion = "5.5.0"
runtimeLivedata = "1.7.6"
runtimeLivedataVersion = "1.0.0-beta01"
symbolProcessingApi = "2.1.0-1.0.29"
lifecycleLivedataCoreKtx = "2.8.7"
[libraries]
androidx-core-ktx-v190 = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
androidx-lifecycle-livedata-core = { module = "androidx.lifecycle:lifecycle-livedata-core", version.ref = "lifecycleLivedataCore" }
androidx-lifecycle-livedata-ktx-v261 = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "androidxLifecycleLivedataKtx" }
androidx-lifecycle-livedata-ktx-v287 = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycleLivedataKtx" }
androidx-lifecycle-livedata-ktx-vxxx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycleLivedataKtxVersion" }
androidx-lifecycle-runtime-compose-v261 = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleRuntimeComposeVersion" }
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidxLifecycleLivedataKtx" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" }
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleLivedataCore" }
androidx-lifecycle-viewmodel-ktx-v261 = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
androidx-lifecycle-viewmodel-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "androidxLifecycleLivedataKtx" }
androidx-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata", version.ref = "runtimeLivedata" }
androidx-runtime-livedata-v100beta01 = { module = "androidx.compose.runtime:runtime-livedata", version.ref = "runtimeLivedataVersion" }
androidx-runtime-livedata-vxxx = { module = "androidx.compose.runtime:runtime-livedata", version.ref = "androidxRuntimeLivedata" }
assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertjVersion" }
exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposedVersion" }
exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposedVersion" }
@@ -59,17 +96,23 @@ exposed-json = { module = "org.jetbrains.exposed:exposed-json", version.ref = "e
flyway-core = { module = "org.flywaydb:flyway-core", version.ref = "flywayVersion" }
h2 = { module = "com.h2database:h2", version.ref = "h2" }
hikaricp = { module = "com.zaxxer:HikariCP", version.ref = "hikariCpVersion" }
insert-koin-koin-compose = { module = "io.insert-koin:koin-compose" }
io-ktor-ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }
io-ktor-ktor-server-netty = { module = "io.ktor:ktor-server-netty" }
junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junitVersion" }
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junitVersion" }
kformat = { module = "de.m3y.kformat:kformat", version.ref = "kformat" }
koin-compose = { module = "io.insert-koin:koin-compose" }
koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koinComposeMultiplatform" }
kotlin-serialization-compiler-plugin-embeddable = { module = "org.jetbrains.kotlin:kotlin-serialization-compiler-plugin-embeddable", version.ref = "kotlinSerializationCompilerPluginEmbeddable" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" }
androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycle" }
androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-junit" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-espresso-core" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" }
@@ -113,6 +156,8 @@ ktor-server-netty-v302 = { module = "io.ktor:ktor-server-netty", version.ref = "
ktor-server-request-validation = { module = "io.ktor:ktor-server-request-validation", version.ref = "ktorServerRequestValidation" }
ktor-server-test-host-jvm = { module = "io.ktor:ktor-server-test-host-jvm" }
ktor-server-websockets = { module = "io.ktor:ktor-server-websockets", version.ref = "ktor" }
lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleRuntimeCompose" }
lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" }
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
ktor-server-core = { module = "io.ktor:ktor-server-core-jvm", version.ref = "ktor" }
ktor-server-netty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "ktor" }
@@ -120,6 +165,10 @@ ktor-server-tests = { module = "io.ktor:ktor-server-tests-jvm", version.ref = "k
material = { module = "androidx.compose.material:material", version.ref = "material" }
postgresql = { module = "org.postgresql:postgresql", version.ref = "psqlVersion" }
rest-assured = { module = "io.rest-assured:rest-assured", version.ref = "restAssuredVersion" }
symbol-processing-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "symbolProcessingApi" }
koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin-bom" }
koin-core = { module = "io.insert-koin:koin-core" }
androidx-lifecycle-livedata-core-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-core-ktx", version.ref = "lifecycleLivedataCoreKtx" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }

View File

@@ -3,6 +3,8 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig
val ktorVersion = "3.0.2"
val exposedVersion = "0.55.0"
val h2Version = "2.3.232"
@@ -18,7 +20,6 @@ plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary)
kotlin("plugin.serialization")
}
repositories {
@@ -94,6 +95,8 @@ kotlin {
implementation(libs.flyway.core)
implementation(libs.logback)
implementation(libs.kformat)
implementation(project.dependencies.platform(libs.koin.bom))
implementation(libs.koin.core)
}
}