Migrate DataStore #7

Merged
Beowulf merged 4 commits from migrate_datastore into main 6 months ago
  1. 7
      app/build.gradle
  2. 107
      app/src/main/java/de/beocode/bestbefore/MainActivity.kt
  3. 22
      app/src/main/java/de/beocode/bestbefore/data/AppData.kt
  4. 46
      app/src/main/java/de/beocode/bestbefore/data/AppDataSerializer.kt
  5. 79
      app/src/main/java/de/beocode/bestbefore/repositories/AppDataRepository.kt
  6. 99
      app/src/main/java/de/beocode/bestbefore/repositories/DataRepository.kt
  7. 25
      app/src/main/java/de/beocode/bestbefore/screens/AddEditScreen.kt
  8. 10
      app/src/main/java/de/beocode/bestbefore/screens/MainScreen.kt
  9. 74
      app/src/main/java/de/beocode/bestbefore/viewmodels/MainViewModel.kt
  10. 11
      app/src/main/java/de/beocode/bestbefore/viewmodels/interfaces/MainViewModelInterface.kt
  11. 11
      app/src/main/java/de/beocode/bestbefore/viewmodels/previews/MainViewModelPreview.kt
  12. 1
      build.gradle

@ -1,6 +1,7 @@
plugins {
id "com.android.application"
id "org.jetbrains.kotlin.android"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.google.protobuf" version "0.8.17"
id "kotlin-kapt"
id "dagger.hilt.android.plugin"
@ -60,16 +61,18 @@ android {
}
dependencies {
implementation "androidx.core:core-ktx:1.7.0"
implementation "androidx.core:core-ktx:1.8.0"
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.material:material-icons-extended:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation "com.google.android.material:material:1.6.0"
implementation "com.google.android.material:material:1.6.1"
implementation "androidx.activity:activity-compose:1.4.0"
implementation "androidx.navigation:navigation-compose:2.4.2"
implementation "androidx.datastore:datastore:1.0.0"
implementation "com.google.protobuf:protobuf-javalite:3.18.0"
implementation "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2"
implementation "com.google.dagger:hilt-android:2.42"
kapt "com.google.dagger:hilt-compiler:2.42"

@ -5,13 +5,25 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.HiltAndroidApp
import de.beocode.bestbefore.app.BestBeforeApp
import de.beocode.bestbefore.data.AppData
import de.beocode.bestbefore.data.FoodItem
import de.beocode.bestbefore.data.appDataStore
import de.beocode.bestbefore.data.dataDataStore
import de.beocode.bestbefore.ui.theme.BestBeforeTheme
import de.beocode.bestbefore.viewmodels.MainViewModel
import de.beocode.bestbefore.viewmodels.interfaces.LocalUserState
import kotlinx.coroutines.flow.first
@HiltAndroidApp
class App : Application()
@ -25,8 +37,101 @@ class MainActivity : ComponentActivity() {
BestBeforeTheme {
CompositionLocalProvider(LocalUserState provides userState) {
BestBeforeApp()
var migration by remember { mutableStateOf(false) }
LaunchedEffect(key1 = Unit) {
migrateData { migration = it }
}
if (migration) {
AlertDialog(
title = {
Text(
text = "Data migration",
style = MaterialTheme.typography.h5
)
},
text = {
Column {
Text(
text = "Data is copied.\nThis may take a moment.",
fontSize = 20.sp
)
Spacer(modifier = Modifier.height(5.dp))
LinearProgressIndicator()
}
},
buttons = { },
onDismissRequest = { }
)
}
}
}
}
}
private suspend fun migrateData(update: (Boolean) -> Unit) {
val oldData = dataDataStore.data.first()
if (oldData.foodList.isNotEmpty()) {
runOnUiThread {
update(true)
}
val newData = AppData()
userState.setWarningDays(oldData.warningDays)
val foodList = newData.foodList.toMutableList()
for (food in oldData.foodList) {
foodList.add(
FoodItem(
id = food.id,
name = food.name,
ts = food.dateTS,
amount = food.amount,
place = food.destination
)
)
}
foodList.sortBy { it.ts }
try {
appDataStore.updateData { AppData ->
AppData.copy(
foodList = foodList
)
}
dataDataStore.updateData { Data ->
Data.toBuilder()
.clearFood()
.build()
}
} catch (e: Exception) {
e.printStackTrace()
}
val nameList = newData.nameSuggestions.toMutableList()
val placeList = newData.placeSuggestions.toMutableList()
for (name in oldData.nameList) {
nameList.add(name)
}
for (place in oldData.destinationList) {
placeList.add(place)
}
try {
appDataStore.updateData { AppData ->
AppData.copy(
nameSuggestions = nameList,
placeSuggestions = placeList
)
}
dataDataStore.updateData { Data ->
Data.toBuilder()
.clearName()
.clearDestination()
.build()
}
} catch (e: Exception) {
e.printStackTrace()
}
update(false)
}
}
}

@ -0,0 +1,22 @@
package de.beocode.bestbefore.data
import kotlinx.collections.immutable.persistentListOf
import kotlinx.serialization.Serializable
@Serializable
data class AppData(
val warningDays: Int = 3,
// kapt bug: Cannot use "PersistentList<FoodItem>"
val foodList: List<FoodItem> = persistentListOf(),
val nameSuggestions: List<String> = persistentListOf(),
val placeSuggestions: List<String> = persistentListOf()
)
@Serializable
data class FoodItem(
val id: Long = System.currentTimeMillis(),
val name: String,
val ts: Long,
val amount: Int,
val place: String = ""
)

@ -0,0 +1,46 @@
package de.beocode.bestbefore.data
import android.content.Context
import androidx.datastore.core.Serializer
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.dataStore
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import java.io.InputStream
import java.io.OutputStream
object AppDataSerializer : Serializer<AppData> {
override val defaultValue: AppData
get() = AppData()
override suspend fun readFrom(input: InputStream): AppData {
return try {
Json.decodeFromString(
deserializer = AppData.serializer(),
string = input.readBytes().decodeToString()
)
} catch (e: SerializationException) {
e.printStackTrace()
defaultValue
}
}
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun writeTo(t: AppData, output: OutputStream) {
output.write(
Json.encodeToString(
serializer = AppData.serializer(),
value = t
).encodeToByteArray()
)
}
}
val Context.appDataStore by dataStore(
fileName = "app-data.json",
serializer = AppDataSerializer,
corruptionHandler = ReplaceFileCorruptionHandler(
produceNewData = { AppData() }
)
)

@ -0,0 +1,79 @@
package de.beocode.bestbefore.repositories
import androidx.datastore.core.DataStore
import de.beocode.bestbefore.data.AppData
import de.beocode.bestbefore.data.FoodItem
import kotlinx.coroutines.flow.Flow
class AppDataRepository(private val appDataStore: DataStore<AppData>) {
fun getData(): Flow<AppData> {
return appDataStore.data
}
suspend fun setExpireWarning(days: Int) {
appDataStore.updateData { AppData ->
AppData.copy(
warningDays = days
)
}
}
suspend fun addFood(foodItem: FoodItem) {
appDataStore.updateData { AppData ->
val list = AppData.foodList.toMutableList()
list.add(foodItem)
list.sortBy { it.ts }
AppData.copy(
foodList = list
)
}
}
suspend fun updateFood(index: Int, foodItem: FoodItem) {
appDataStore.updateData { AppData ->
val list = AppData.foodList.toMutableList()
list[index] = foodItem
list.sortBy { it.ts }
AppData.copy(
foodList = list
)
}
}
suspend fun deleteFood(index: Int) {
appDataStore.updateData { AppData ->
val list = AppData.foodList.toMutableList()
list.removeAt(index)
AppData.copy(
foodList = list
)
}
}
suspend fun addName(name: String) {
appDataStore.updateData { AppData ->
val list = AppData.nameSuggestions.toMutableList()
list.add(name)
AppData.copy(
nameSuggestions = list
)
}
}
suspend fun addDestination(place: String) {
appDataStore.updateData { AppData ->
val list = AppData.placeSuggestions.toMutableList()
list.add(place)
AppData.copy(
placeSuggestions = list
)
}
}
suspend fun clearData() {
appDataStore.updateData {
AppData()
}
}
}

@ -1,99 +0,0 @@
package de.beocode.bestbefore.repositories
import androidx.datastore.core.DataStore
import de.beocode.bestbefore.Data
import de.beocode.bestbefore.FoodItem
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
class DataRepository(private val dataStore: DataStore<Data>) {
suspend fun setExpireWarning(days: Int) {
dataStore.updateData { Data ->
Data.toBuilder()
.setWarningDays(days)
.build()
}
}
suspend fun getExpireWarning(): Int {
val days = dataStore.data.map { Data ->
Data.warningDays
}
return days.first()
}
suspend fun addFood(foodItem: FoodItem) {
dataStore.updateData { Data ->
Data.toBuilder()
.addFood(foodItem.toBuilder().setId(System.currentTimeMillis()).build())
.build()
}
}
suspend fun updateFood(index: Int, foodItem: FoodItem) {
dataStore.updateData { Data ->
Data.toBuilder()
.setFood(index, foodItem)
.build()
}
}
suspend fun deleteFood(index: Int) {
dataStore.updateData { Data ->
Data.toBuilder()
.removeFood(index)
.build()
}
}
suspend fun getFoodList(): List<FoodItem> {
val foodList = dataStore.data.map { Data ->
Data.foodList
}
return foodList.first()
}
suspend fun addName(name: String) {
dataStore.updateData { Data ->
Data.toBuilder()
.addName(name)
.build()
}
}
suspend fun getNameList(): List<String> {
val nameList = dataStore.data.map { Data ->
Data.nameList
}
return nameList.first()
}
suspend fun addDestination(destination: String) {
dataStore.updateData { Data ->
Data.toBuilder()
.addDestination(destination)
.build()
}
}
suspend fun getDestinationList(): List<String> {
val destinationList = dataStore.data.map { Data ->
Data.destinationList
}
return destinationList.first()
}
suspend fun clearData() {
dataStore.updateData { Data ->
Data.toBuilder()
.clear()
.setWarningDays(3)
.build()
}
}
}

@ -3,7 +3,6 @@ package de.beocode.bestbefore.screens
import android.app.DatePickerDialog
import android.content.Context
import android.content.res.Configuration
import android.util.Log
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
@ -25,7 +24,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import de.beocode.bestbefore.FoodItem
import de.beocode.bestbefore.data.FoodItem
import de.beocode.bestbefore.R
import de.beocode.bestbefore.screens.general.BusyIndicator
import de.beocode.bestbefore.screens.general.TopBarBackSafe
@ -68,10 +67,10 @@ fun AddEditScreen(
if (item != null) {
itemIndex = index
name = name.copy(item!!.name)
ts = item!!.dateTS
ts = item!!.ts
date = convertTsToString(context, ts)
amount = amount.copy(item!!.amount.toString())
place = place.copy(item!!.destination)
place = place.copy(item!!.place)
}
}
}
@ -98,7 +97,7 @@ fun AddEditScreen(
}
LaunchedEffect(key1 = Unit) {
userState.getSuggestions()
//userState.getSuggestions()
focusRequester.requestFocus()
}
@ -170,7 +169,7 @@ fun AddEditScreen(
Modifier,
addUpdateItem,
place,
userState.destinationSuggestions
userState.placeSuggestions
) { place = it }
}
}
@ -269,12 +268,12 @@ fun addUpdateItem(
) {
scope.launch {
if (name.isNotBlank() && date.isNotBlank() && amount != null && amount > 0) {
var item = FoodItem.newBuilder()
.setName(name)
.setDateTS(ts)
.setAmount(amount)
.setDestination(place)
.build()
var item = FoodItem(
name = name,
ts = ts,
amount = amount,
place = place
)
if (index == null || id == null) {
userState.addFood(item) { success ->
@ -290,7 +289,7 @@ fun addUpdateItem(
}
}
} else {
item = item.toBuilder().setId(id).build()
item = item.copy(id = id)
userState.updateFood(index, item) { success ->
if (success) {
success()

@ -29,7 +29,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import de.beocode.bestbefore.FoodItem
import de.beocode.bestbefore.data.FoodItem
import de.beocode.bestbefore.R
import de.beocode.bestbefore.router.Screens
import de.beocode.bestbefore.screens.general.*
@ -176,7 +176,7 @@ private fun FoodCard(item: FoodItem) {
maxLines = 1,
style = MaterialTheme.typography.h2
)
ExpireText(item.dateTS)
ExpireText(item.ts)
Text(
text = context.resources.getQuantityString(
R.plurals.amount,
@ -186,9 +186,9 @@ private fun FoodCard(item: FoodItem) {
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
if (item.destination != "")
if (item.place != "")
Text(
text = context.getString(R.string.place, item.destination),
text = context.getString(R.string.place, item.place),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = MaterialTheme.typography.body2
@ -223,7 +223,7 @@ private fun ExpireText(ts: Long) {
private fun EmptyScreen() {
val context = LocalContext.current
Column(
modifier = Modifier.fillMaxSize(),
modifier = Modifier.fillMaxSize().padding(10.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {

@ -8,10 +8,14 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import de.beocode.bestbefore.FoodItem
import de.beocode.bestbefore.data.dataDataStore
import de.beocode.bestbefore.repositories.DataRepository
import de.beocode.bestbefore.data.FoodItem
import de.beocode.bestbefore.data.appDataStore
import de.beocode.bestbefore.repositories.AppDataRepository
import de.beocode.bestbefore.viewmodels.interfaces.MainViewModelInterface
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -20,37 +24,43 @@ private const val LOG_TAG = "BBMainViewModel"
@HiltViewModel
class MainViewModel @Inject constructor(application: Application) : MainViewModelInterface,
ViewModel() {
private val dataRepository = DataRepository(application.dataDataStore)
private val dataRepository = AppDataRepository(application.appDataStore)
override var isBusy by mutableStateOf(false)
override var expireWarning by mutableStateOf(0)
override var foodList by mutableStateOf(emptyList<FoodItem>())
override var foodList by mutableStateOf(persistentListOf<FoodItem>())
override var filteredFoodList by mutableStateOf(emptyList<FoodItem>())
override var nameSuggestions by mutableStateOf(emptyList<String>())
override var destinationSuggestions by mutableStateOf(emptyList<String>())
override var nameSuggestions by mutableStateOf(persistentListOf<String>())
override var placeSuggestions by mutableStateOf(persistentListOf<String>())
init {
isBusy = true
viewModelScope.launch {
expireWarning = dataRepository.getExpireWarning()
dataRepository.getData()
.catch { it.printStackTrace() }
.collect {
expireWarning = it.warningDays
gotFoodList(it.foodList)
nameSuggestions = it.nameSuggestions.toPersistentList()
Log.d(LOG_TAG, "Got ${nameSuggestions.size} name suggestions")
placeSuggestions = it.placeSuggestions.toPersistentList()
Log.d(LOG_TAG, "Got ${placeSuggestions.size} name suggestions")
}
}
getFoodList()
}
override fun getFoodList() {
viewModelScope.launch {
foodList = dataRepository.getFoodList()
filteredFoodList = foodList.sortedBy { it.dateTS }
Log.d(LOG_TAG, "Got ${foodList.size} items")
isBusy = false
}
private fun gotFoodList(list: List<FoodItem>) {
foodList = list.toPersistentList()
filteredFoodList = foodList
Log.d(LOG_TAG, "Got ${foodList.size} items")
isBusy = false
}
override fun searchFoodList(search: String) {
filteredFoodList = foodList.filter {
it.name.contains(search, true)
}.sortedBy { it.dateTS }
}
}
override fun addFood(foodItem: FoodItem, success: (Boolean) -> Unit) {
@ -59,15 +69,14 @@ class MainViewModel @Inject constructor(application: Application) : MainViewMode
try {
dataRepository.addFood(foodItem)
Log.d(LOG_TAG, "Added ${foodItem.name}")
getFoodList()
success(true)
} catch (e: Exception) {
e.printStackTrace()
isBusy = false
success(false)
}
getFoodList()
}
addSuggestions(foodItem.name, foodItem.destination)
addSuggestions(foodItem.name, foodItem.place)
}
override fun updateFood(index: Int, foodItem: FoodItem, success: (Boolean) -> Unit) {
@ -76,15 +85,16 @@ class MainViewModel @Inject constructor(application: Application) : MainViewMode
try {
dataRepository.updateFood(index, foodItem)
Log.d(LOG_TAG, "Updated ${foodItem.name}")
getFoodList()
if (foodItem == foodList[index])
isBusy = false
success(true)
} catch (e: Exception) {
e.printStackTrace()
isBusy = false
success(false)
}
getFoodList()
}
addSuggestions(foodItem.name, foodItem.destination)
addSuggestions(foodItem.name, foodItem.place)
}
override fun deleteFood(foodItem: FoodItem) {
@ -93,7 +103,6 @@ class MainViewModel @Inject constructor(application: Application) : MainViewMode
val index = foodList.indexOf(foodItem)
dataRepository.deleteFood(index)
Log.d(LOG_TAG, "Deleted ${foodItem.name}")
getFoodList()
}
}
@ -101,27 +110,14 @@ class MainViewModel @Inject constructor(application: Application) : MainViewMode
viewModelScope.launch {
if (!nameSuggestions.contains(name))
dataRepository.addName(name)
if (!destinationSuggestions.contains(destination) && destination.isNotBlank())
if (!placeSuggestions.contains(destination) && destination.isNotBlank())
dataRepository.addDestination(destination)
}
}
override fun getSuggestions() {
Log.d(LOG_TAG, "Get suggestions...")
viewModelScope.launch {
nameSuggestions = dataRepository.getNameList()
Log.d(LOG_TAG, "Got ${nameSuggestions.size} name suggestions")
}
viewModelScope.launch {
destinationSuggestions = dataRepository.getDestinationList()
Log.d(LOG_TAG, "Got ${destinationSuggestions.size} destination suggestions")
}
}
override fun setWarningDays(days: Int) {
viewModelScope.launch {
dataRepository.setExpireWarning(days)
expireWarning = dataRepository.getExpireWarning()
Log.d(LOG_TAG, "Set expire warning to $days days")
}
}
@ -130,8 +126,6 @@ class MainViewModel @Inject constructor(application: Application) : MainViewMode
isBusy = true
try {
dataRepository.clearData()
expireWarning = dataRepository.getExpireWarning()
getFoodList()
Log.d(LOG_TAG, "Deleted all data")
success(true)
} catch (e: Exception) {

@ -1,19 +1,19 @@
package de.beocode.bestbefore.viewmodels.interfaces
import androidx.compose.runtime.compositionLocalOf
import de.beocode.bestbefore.FoodItem
import de.beocode.bestbefore.data.FoodItem
import kotlinx.collections.immutable.PersistentList
interface MainViewModelInterface {
var isBusy: Boolean
var expireWarning: Int
var foodList: List<FoodItem>
var foodList: PersistentList<FoodItem>
var filteredFoodList: List<FoodItem>
var nameSuggestions: List<String>
var destinationSuggestions: List<String>
var nameSuggestions: PersistentList<String>
var placeSuggestions: PersistentList<String>
fun getFoodList()
fun searchFoodList(search: String)
fun addFood(foodItem: FoodItem, success: (Boolean) -> Unit)
@ -21,7 +21,6 @@ interface MainViewModelInterface {
fun deleteFood(foodItem: FoodItem)
fun addSuggestions(name: String, destination: String)
fun getSuggestions()
fun setWarningDays(days: Int)

@ -4,20 +4,20 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import de.beocode.bestbefore.FoodItem
import de.beocode.bestbefore.data.FoodItem
import de.beocode.bestbefore.viewmodels.interfaces.MainViewModelInterface
import kotlinx.collections.immutable.persistentListOf
class MainViewModelPreview : MainViewModelInterface, ViewModel() {
override var isBusy by mutableStateOf(false)
override var expireWarning by mutableStateOf(0)
override var foodList by mutableStateOf(emptyList<FoodItem>())
override var foodList by mutableStateOf(persistentListOf<FoodItem>())
override var filteredFoodList by mutableStateOf(emptyList<FoodItem>())
override var nameSuggestions by mutableStateOf(emptyList<String>())
override var destinationSuggestions by mutableStateOf(emptyList<String>())
override var nameSuggestions by mutableStateOf(persistentListOf<String>())
override var placeSuggestions by mutableStateOf(persistentListOf<String>())
override fun getFoodList() { }
override fun searchFoodList(search: String) { }
override fun addFood(foodItem: FoodItem, success: (Boolean) -> Unit) { }
@ -25,7 +25,6 @@ class MainViewModelPreview : MainViewModelInterface, ViewModel() {
override fun deleteFood(foodItem: FoodItem) { }
override fun addSuggestions(name: String, destination: String) { }
override fun getSuggestions() { }
override fun setWarningDays(days: Int) { }

@ -3,6 +3,7 @@ buildscript {
compose_version = '1.1.1'
}
dependencies {
classpath 'org.jetbrains.kotlin:kotlin-serialization:1.6.10'
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.40.1'
}
}// Top-level build file where you can add configuration options common to all sub-projects/modules.

Loading…
Cancel
Save