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

@@ -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()
}
}