Skip to content

Commit d7de3f2

Browse files
authored
Merge pull request #40 from gdg-berlin-android/work/detail-view-model
Refactors detail view to use ViewModel
2 parents 4e3c975 + 619fe22 commit d7de3f2

6 files changed

Lines changed: 207 additions & 132 deletions

File tree

app/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ dependencies {
5757
implementation(libs.androidx.adaptive)
5858
implementation(libs.androidx.adaptive.layout)
5959
implementation(libs.androidx.adaptive.navigation)
60+
implementation(libs.androidx.lifecycle.runtime.compose)
61+
implementation(libs.androidx.lifecycle.viewmodel.compose)
6062

6163
// Patch annotations + processor
6264
implementation(project(":patch-annotations"))

app/src/main/java/de/berlindroid/zepatch/MainActivity.kt

Lines changed: 40 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,15 @@ import androidx.compose.material3.adaptive.navigation.NavigableListDetailPaneSca
5151
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
5252
import androidx.compose.material3.rememberTopAppBarState
5353
import androidx.compose.runtime.Composable
54+
import androidx.compose.runtime.LaunchedEffect
5455
import androidx.compose.runtime.getValue
55-
import androidx.compose.runtime.mutableIntStateOf
56-
import androidx.compose.runtime.mutableStateOf
57-
import androidx.compose.runtime.remember
5856
import androidx.compose.runtime.rememberCoroutineScope
59-
import androidx.compose.runtime.setValue
6057
import androidx.compose.ui.Modifier
6158
import androidx.compose.ui.graphics.ImageBitmap
6259
import androidx.compose.ui.input.nestedscroll.nestedScroll
6360
import androidx.compose.ui.platform.LocalContext
6461
import androidx.compose.ui.unit.dp
62+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
6563
import com.embroidermodder.punching.Histogram
6664
import de.berlindroid.zepatch.PatchablePreviewMode.BITMAP
6765
import de.berlindroid.zepatch.PatchablePreviewMode.COMPOSABLE
@@ -75,9 +73,9 @@ import de.berlindroid.zepatch.ui.theme.ZePatchTheme
7573
import de.berlindroid.zepatch.utils.multiLet
7674
import de.berlindroid.zepatch.utils.uppercaseWords
7775
import kotlinx.coroutines.launch
76+
import androidx.lifecycle.viewmodel.compose.viewModel as lifecycleViewModel
7877

79-
80-
private enum class PatchablePreviewMode {
78+
enum class PatchablePreviewMode {
8179
COMPOSABLE, BITMAP, REDUCED_BITMAP, STITCHES
8280
}
8381

@@ -197,18 +195,20 @@ private fun PatchableDetail(
197195
onBackClick: () -> Unit,
198196
patchable: @Composable (Boolean, (ImageBitmap) -> Unit) -> Unit,
199197
) {
200-
var imageBitmap by remember { mutableStateOf<ImageBitmap?>(null) }
201-
var reducedImageBitmap by remember { mutableStateOf<ImageBitmap?>(null) }
202-
var reducedHistogram by remember { mutableStateOf<Histogram?>(null) }
203-
var colorCount by remember { mutableIntStateOf(3) }
204-
var embroideryData by remember { mutableStateOf<ByteArray?>(null) }
205-
var embroideryPreviewImage by remember { mutableStateOf<ImageBitmap?>(null) }
206-
207198
val context = LocalContext.current
199+
200+
val viewModel: WizardViewModel = lifecycleViewModel()
201+
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
202+
203+
// Reset wizard state when a different patchable is opened
204+
LaunchedEffect(name) {
205+
viewModel.reset()
206+
}
207+
208208
val launcher = rememberLauncherForActivityResult(
209209
contract = ActivityResultContracts.StartActivityForResult()
210210
) { result ->
211-
savePesAfterSelection(context, result, embroideryData)
211+
savePesAfterSelection(context, result, uiState.embroideryData)
212212
}
213213

214214
Scaffold(
@@ -232,38 +232,33 @@ private fun PatchableDetail(
232232
Column(
233233
modifier = Modifier.padding(innerPadding),
234234
) {
235-
var currentMode by remember { mutableStateOf(COMPOSABLE) }
236-
237-
ProgressPills(imageBitmap, reducedImageBitmap, currentMode)
235+
ProgressPills(uiState.imageBitmap, uiState.reducedImageBitmap, uiState.previewMode)
238236

239237
WizardContent(
240-
currentMode,
241-
imageBitmap,
242-
colorCount,
243-
reducedImageBitmap,
244-
reducedHistogram,
238+
uiState.previewMode,
239+
uiState.imageBitmap,
240+
uiState.colorCount,
241+
uiState.reducedImageBitmap,
242+
uiState.reducedHistogram,
245243
name,
246-
onBitmapUpdated = { imageBitmap = it },
247-
onColorCountUpdated = { colorCount = it },
248-
onReducedUpdated = { img, histo ->
249-
reducedImageBitmap = img;reducedHistogram = histo
250-
},
251-
onEmbroideryUpdated = { data, preview ->
252-
embroideryData = data
253-
embroideryPreviewImage = preview
254-
},
255-
patchable
244+
onBitmapUpdated = viewModel::updateBitmap,
245+
onColorCountUpdated = viewModel::updateColorCount,
246+
computeReducedBitmap = viewModel::computeReducedBitmap,
247+
patchable = patchable
256248
)
257249

258250
WizardButtons(
259-
currentMode,
260-
imageBitmap,
261-
reducedImageBitmap,
262-
embroideryData,
251+
uiState.previewMode,
252+
uiState.imageBitmap,
253+
uiState.reducedImageBitmap,
254+
uiState.embroideryData,
263255
name,
264256
launcher
265257
) {
266-
currentMode = it
258+
viewModel.setPreviewMode(it)
259+
if (it == REDUCED_BITMAP) {
260+
viewModel.computeReducedBitmap()
261+
}
267262
}
268263
}
269264
}
@@ -279,8 +274,7 @@ private fun WizardContent(
279274
name: String,
280275
onBitmapUpdated: (ImageBitmap) -> Unit,
281276
onColorCountUpdated: (Int) -> Unit,
282-
onReducedUpdated: (ImageBitmap, Histogram) -> Unit,
283-
onEmbroideryUpdated: (ByteArray, ImageBitmap) -> Unit,
277+
computeReducedBitmap: () -> Unit,
284278
patchable: @Composable (Boolean, (ImageBitmap) -> Unit) -> Unit,
285279
) {
286280
Card(
@@ -301,15 +295,20 @@ private fun WizardContent(
301295
image = imageBitmap,
302296
colorCount = colorCount,
303297
onColorCountChanged = onColorCountUpdated,
304-
onReduced = onReducedUpdated,
298+
computeReducedBitmap = computeReducedBitmap,
299+
reducedImage = reducedImageBitmap,
305300
)
306301

307302
STITCHES -> reducedImageBitmap?.multiLet(reducedHistogram) { img, histo ->
303+
val vm: WizardViewModel = lifecycleViewModel()
304+
val ui by vm.uiState.collectAsStateWithLifecycle()
308305
BitmapToStitches(
309306
reducedImageBitmap = img,
310307
reducedHistogram = histo,
311308
name = name,
312-
onEmbroidery = onEmbroideryUpdated
309+
onCreateEmbroidery = { n, b, h -> vm.createEmbroidery(n, b, h) },
310+
previewImage = ui.embroideryPreviewImage,
311+
creatingEmbroidery = ui.creatingEmbroidery,
313312
)
314313
}
315314
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package de.berlindroid.zepatch
2+
3+
import android.app.Application
4+
import android.graphics.Bitmap
5+
import android.graphics.BitmapFactory
6+
import androidx.compose.ui.graphics.ImageBitmap
7+
import androidx.compose.ui.graphics.asAndroidBitmap
8+
import androidx.compose.ui.graphics.asImageBitmap
9+
import androidx.core.graphics.scale
10+
import androidx.lifecycle.AndroidViewModel
11+
import androidx.lifecycle.viewModelScope
12+
import com.embroidermodder.punching.Histogram
13+
import com.embroidermodder.punching.reduceColors
14+
import de.berlindroid.zepatch.PatchablePreviewMode.COMPOSABLE
15+
import de.berlindroid.zepatch.stiches.StitchToPES
16+
import kotlinx.coroutines.Dispatchers
17+
import kotlinx.coroutines.flow.MutableStateFlow
18+
import kotlinx.coroutines.flow.StateFlow
19+
import kotlinx.coroutines.flow.asStateFlow
20+
import kotlinx.coroutines.flow.update
21+
import kotlinx.coroutines.launch
22+
23+
/**
24+
* ViewModel to manage the Wizard screen state.
25+
*/
26+
27+
class WizardViewModel(application: Application) : AndroidViewModel(application) {
28+
29+
data class UIState(
30+
val imageBitmap: ImageBitmap? = null,
31+
val reducedImageBitmap: ImageBitmap? = null,
32+
val reducedHistogram: Histogram? = null,
33+
val colorCount: Int = 3,
34+
val embroideryData: ByteArray? = null,
35+
val embroideryPreviewImage: ImageBitmap? = null,
36+
val creatingEmbroidery: Boolean = false,
37+
val error: String? = null,
38+
val previewMode: PatchablePreviewMode = COMPOSABLE,
39+
)
40+
41+
private val _uiState = MutableStateFlow(UIState())
42+
val uiState: StateFlow<UIState> = _uiState.asStateFlow()
43+
44+
/** Resets the whole wizard state back to initial defaults. */
45+
fun reset() {
46+
_uiState.update { UIState() }
47+
}
48+
49+
fun setPreviewMode(mode: PatchablePreviewMode) {
50+
_uiState.update { it.copy(previewMode = mode) }
51+
}
52+
53+
fun updateColorCount(count: Int) {
54+
_uiState.update { it.copy(colorCount = count) }
55+
}
56+
57+
fun updateBitmap(bitmap: ImageBitmap) {
58+
_uiState.update {
59+
it.copy(
60+
imageBitmap = bitmap,
61+
// Reset downstream results when source bitmap changes
62+
reducedImageBitmap = null,
63+
reducedHistogram = null,
64+
embroideryData = null,
65+
embroideryPreviewImage = null,
66+
)
67+
}
68+
}
69+
70+
fun computeReducedBitmap() {
71+
val state = _uiState.value
72+
val image = state.imageBitmap ?: return
73+
val colorCount = state.colorCount
74+
viewModelScope.launch(Dispatchers.IO) {
75+
val aspect = image.width / image.height.toFloat()
76+
val (reducedBmp, histogram) = image.asAndroidBitmap()
77+
.copy(Bitmap.Config.ARGB_8888, false)
78+
.scale((512 * aspect).toInt(), 512, false)
79+
.reduceColors(colorCount)
80+
81+
_uiState.update {
82+
it.copy(
83+
reducedImageBitmap = reducedBmp.asImageBitmap(),
84+
reducedHistogram = histogram,
85+
)
86+
}
87+
}
88+
}
89+
90+
fun createEmbroidery(
91+
name: String,
92+
bitmap: ImageBitmap,
93+
histogram: Histogram,
94+
) {
95+
viewModelScope.launch(Dispatchers.IO) {
96+
try {
97+
_uiState.update { it.copy(creatingEmbroidery = true, error = null) }
98+
99+
val aspect = bitmap.width / bitmap.height.toFloat()
100+
val embroidery = StitchToPES.createEmbroideryFromBitmap(
101+
name = name,
102+
bitmap = bitmap.asAndroidBitmap(),
103+
histogram = histogram,
104+
mmWidth = 500f * aspect,
105+
mmHeight = 500f,
106+
mmDensityX = 4f,
107+
mmDensityY = 2f,
108+
)
109+
110+
val context = getApplication<Application>().applicationContext
111+
val pes = StitchToPES.convert(context, embroidery)
112+
val png = StitchToPES.convert(context, embroidery, "png")
113+
114+
val previewImage = if (png == null || png.isEmpty()) {
115+
null
116+
} else {
117+
val decoded = BitmapFactory.decodeByteArray(png, 0, png.size)
118+
decoded.scale(decoded.width * 2, decoded.height * 2).asImageBitmap()
119+
}
120+
121+
_uiState.update {
122+
it.copy(
123+
embroideryData = pes,
124+
embroideryPreviewImage = previewImage,
125+
creatingEmbroidery = false,
126+
)
127+
}
128+
} catch (t: Throwable) {
129+
_uiState.update {
130+
it.copy(
131+
creatingEmbroidery = false,
132+
error = t.message ?: t.toString(),
133+
)
134+
}
135+
}
136+
}
137+
}
138+
139+
}

0 commit comments

Comments
 (0)