chore(mvvm): CreateCategoryActivity ViewModel
This commit is contained in:
parent
98dc983435
commit
fa2bb2c78a
|
@ -48,6 +48,7 @@ dependencies {
|
|||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
|
||||
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||
implementation("com.squareup.retrofit2:retrofit:2.11.0")
|
||||
implementation("com.squareup.retrofit2:converter-gson:2.11.0")
|
||||
|
|
|
@ -18,6 +18,7 @@ import com.p_vacho.neat_calendar.R
|
|||
import com.p_vacho.neat_calendar.adapters.CategoryAdapter
|
||||
import com.p_vacho.neat_calendar.api.RetrofitClient
|
||||
import com.p_vacho.neat_calendar.api.models.CategoryResponse
|
||||
import com.p_vacho.neat_calendar.viewmodels.CategoryMode
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
|
@ -2,42 +2,24 @@ package com.p_vacho.neat_calendar.activities
|
|||
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.dhaval2404.colorpicker.ColorPickerDialog
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.google.gson.Gson
|
||||
import com.p_vacho.neat_calendar.R
|
||||
import com.p_vacho.neat_calendar.api.RetrofitClient
|
||||
import com.p_vacho.neat_calendar.api.models.CategoryRequest
|
||||
import com.p_vacho.neat_calendar.api.models.CategoryResponse
|
||||
import com.p_vacho.neat_calendar.api.models.PartialCategoryRequest
|
||||
import com.p_vacho.neat_calendar.api.models.ValidationError
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import retrofit2.HttpException
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
enum class CategoryMode {
|
||||
CREATE, EDIT
|
||||
}
|
||||
import com.p_vacho.neat_calendar.viewmodels.CategoryMode
|
||||
import com.p_vacho.neat_calendar.viewmodels.CreateCategoryViewModel
|
||||
|
||||
class CreateCategoryActivity : AppCompatActivity() {
|
||||
private var selectedColor by Delegates.notNull<Int>()
|
||||
private var existingCategory: CategoryResponse? = null
|
||||
private lateinit var mode: CategoryMode
|
||||
private val viewModel: CreateCategoryViewModel by viewModels()
|
||||
|
||||
|
||||
// UI components
|
||||
private lateinit var etCategoryName: TextInputEditText
|
||||
private lateinit var btnColorPicker: MaterialButton
|
||||
private lateinit var btnSaveCategory: MaterialButton
|
||||
|
@ -45,114 +27,72 @@ class CreateCategoryActivity : AppCompatActivity() {
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContentView(R.layout.activity_create_category)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||
insets
|
||||
}
|
||||
|
||||
initializeViews()
|
||||
mode = intent.getStringExtra("mode")!!.let { CategoryMode.valueOf(it) }
|
||||
val mode = intent.getStringExtra("mode")?.let { CategoryMode.valueOf(it) } ?: CategoryMode.CREATE
|
||||
@Suppress("DEPRECATION")
|
||||
val category = intent.getParcelableExtra<CategoryResponse>("category")
|
||||
|
||||
if (mode == CategoryMode.EDIT) {
|
||||
@Suppress("DEPRECATION")
|
||||
existingCategory = intent.getParcelableExtra("category")!!
|
||||
|
||||
etCategoryName.setText(existingCategory?.name)
|
||||
selectedColor = existingCategory?.color?.toArgb() ?: ContextCompat.getColor(this, R.color.category_indicator_color)
|
||||
btnSaveCategory.setText(R.string.update_category)
|
||||
} else {
|
||||
selectedColor = ContextCompat.getColor(this, R.color.category_indicator_color)
|
||||
}
|
||||
|
||||
btnColorPicker.iconTint = ColorStateList.valueOf(selectedColor)
|
||||
initializeViews(mode)
|
||||
viewModel.initialize(mode, category)
|
||||
setupObservers()
|
||||
setupListeners()
|
||||
}
|
||||
|
||||
private fun initializeViews() {
|
||||
private fun initializeViews(mode: CategoryMode) {
|
||||
etCategoryName = findViewById(R.id.etCategoryName)
|
||||
btnColorPicker = findViewById(R.id.btnColorPicker)
|
||||
btnSaveCategory = findViewById(R.id.btnSaveCategory)
|
||||
btnCancel = findViewById(R.id.btnCancel)
|
||||
|
||||
if (mode == CategoryMode.EDIT) {
|
||||
btnSaveCategory.setText(R.string.update_category)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupObservers() {
|
||||
lifecycleScope.launchWhenStarted {
|
||||
viewModel.state.collect { state ->
|
||||
// Only update the text if it's different to avoid overwriting user input
|
||||
if (etCategoryName.text?.toString() != state.categoryName) {
|
||||
etCategoryName.setText(state.categoryName)
|
||||
}
|
||||
|
||||
btnColorPicker.iconTint = ColorStateList.valueOf(state.selectedColor)
|
||||
|
||||
if (state.errorMessage != null) {
|
||||
Toast.makeText(this@CreateCategoryActivity, state.errorMessage, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
// Handle successful category save
|
||||
state.successCategory?.let { category ->
|
||||
val intent = Intent().apply {
|
||||
if (state.isCreateMode) {
|
||||
putExtra("newCategory", category)
|
||||
} else {
|
||||
putExtra("editedCategory", category)
|
||||
}
|
||||
}
|
||||
setResult(RESULT_OK, intent)
|
||||
finish() // Finish activity and return the result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupListeners() {
|
||||
btnColorPicker.setOnClickListener { openColorPickerDialog() }
|
||||
btnSaveCategory.setOnClickListener { saveCategory() }
|
||||
etCategoryName.addTextChangedListener { viewModel.onCategoryNameChanged(it.toString()) }
|
||||
btnSaveCategory.setOnClickListener { viewModel.saveCategory() }
|
||||
btnCancel.setOnClickListener { finish() }
|
||||
}
|
||||
|
||||
private fun saveCategory() {
|
||||
val categoryName = etCategoryName.text.toString().trim()
|
||||
if (categoryName.isEmpty()) {
|
||||
Toast.makeText(this, getString(R.string.please_enter_a_category_name), Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val resultCategory = if (mode == CategoryMode.CREATE) {
|
||||
val request =
|
||||
CategoryRequest(name = categoryName, color = Color.valueOf(selectedColor))
|
||||
RetrofitClient.categoryService.createCategory(request)
|
||||
} else {
|
||||
val request = PartialCategoryRequest(
|
||||
name = categoryName.takeIf { it != existingCategory?.name },
|
||||
color = Color.valueOf(selectedColor)
|
||||
.takeIf { it != existingCategory?.color }
|
||||
)
|
||||
RetrofitClient.categoryService.updateCategory(existingCategory!!.id, request)
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) { handleCategorySaved(resultCategory) }
|
||||
} catch (e: HttpException) {
|
||||
if (e.code() != 422) {
|
||||
throw e
|
||||
}
|
||||
|
||||
val errorBody = e.response()?.errorBody()?.string()
|
||||
val validationError = Gson().fromJson(errorBody, ValidationError::class.java)
|
||||
val errMsg = validationError.detail.joinToString("\n")
|
||||
|
||||
Log.e("CategoryCreate", "Got HTTP 422: $validationError")
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
this@CreateCategoryActivity,
|
||||
getString(R.string.failed_to_save_category, errMsg),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCategorySaved(category: CategoryResponse) {
|
||||
Log.w("CreateCategory", "Ending")
|
||||
val intent = Intent().apply {
|
||||
if (mode == CategoryMode.CREATE) {
|
||||
putExtra("newCategory", category)
|
||||
Log.w("CreateCategory", "New")
|
||||
} else {
|
||||
putExtra("editedCategory", category)
|
||||
Log.w("CreateCategory", "Edit")
|
||||
}
|
||||
}
|
||||
setResult(RESULT_OK, intent)
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun openColorPickerDialog() {
|
||||
ColorPickerDialog
|
||||
.Builder(this)
|
||||
.setTitle(getString(R.string.select_color))
|
||||
.setDefaultColor(selectedColor)
|
||||
.setColorListener { color, _ ->
|
||||
selectedColor = color
|
||||
btnColorPicker.iconTint = ColorStateList.valueOf(color)
|
||||
}
|
||||
.setDefaultColor(viewModel.state.value.selectedColor)
|
||||
.setColorListener { color, _ -> viewModel.onColorSelected(color) }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
package com.p_vacho.neat_calendar.viewmodels
|
||||
|
||||
import android.app.Application
|
||||
import android.graphics.Color
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.gson.Gson
|
||||
import com.p_vacho.neat_calendar.R
|
||||
import com.p_vacho.neat_calendar.api.RetrofitClient
|
||||
import com.p_vacho.neat_calendar.api.models.CategoryRequest
|
||||
import com.p_vacho.neat_calendar.api.models.CategoryResponse
|
||||
import com.p_vacho.neat_calendar.api.models.PartialCategoryRequest
|
||||
import com.p_vacho.neat_calendar.api.models.ValidationError
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import retrofit2.HttpException
|
||||
|
||||
enum class CategoryMode {
|
||||
CREATE, EDIT
|
||||
}
|
||||
|
||||
data class CreateCategoryState(
|
||||
val categoryName: String = "",
|
||||
val selectedColor: Int = Color.GRAY,
|
||||
val isLoading: Boolean = false,
|
||||
val errorMessage: String? = null,
|
||||
val successCategory: CategoryResponse? = null,
|
||||
val isCreateMode: Boolean = true,
|
||||
)
|
||||
|
||||
class CreateCategoryViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private val _state = MutableStateFlow(CreateCategoryState())
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
private var existingCategory: CategoryResponse? = null
|
||||
|
||||
fun initialize(mode: CategoryMode, category: CategoryResponse?) {
|
||||
_state.value = _state.value.copy(
|
||||
categoryName = category?.name.orEmpty(),
|
||||
selectedColor = category?.color?.toArgb()
|
||||
?: getApplication<Application>().getColor(R.color.category_indicator_color),
|
||||
isCreateMode = mode == CategoryMode.CREATE
|
||||
)
|
||||
existingCategory = category
|
||||
}
|
||||
|
||||
fun onCategoryNameChanged(name: String) {
|
||||
if (name != _state.value.categoryName) {
|
||||
_state.value = _state.value.copy(categoryName = name)
|
||||
}
|
||||
}
|
||||
|
||||
fun onColorSelected(color: Int) {
|
||||
if (color != _state.value.selectedColor) {
|
||||
_state.value = _state.value.copy(selectedColor = color)
|
||||
}
|
||||
}
|
||||
|
||||
fun saveCategory() {
|
||||
val categoryName = _state.value.categoryName.trim()
|
||||
if (categoryName.isEmpty()) {
|
||||
_state.value = _state.value.copy(
|
||||
errorMessage = getApplication<Application>().getString(
|
||||
R.string.please_enter_a_category_name
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
_state.value = _state.value.copy(isLoading = true, errorMessage = null)
|
||||
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val resultCategory = if (_state.value.isCreateMode) {
|
||||
val request = CategoryRequest(
|
||||
name = categoryName,
|
||||
color = Color.valueOf(_state.value.selectedColor)
|
||||
)
|
||||
RetrofitClient.categoryService.createCategory(request)
|
||||
} else {
|
||||
existingCategory?.let { category ->
|
||||
val request = PartialCategoryRequest(
|
||||
name = categoryName.takeIf { it != category.name },
|
||||
color = Color.valueOf(_state.value.selectedColor).takeIf { it != category.color }
|
||||
)
|
||||
RetrofitClient.categoryService.updateCategory(category.id, request)
|
||||
} ?: throw IllegalStateException("Category is required for EDIT mode")
|
||||
}
|
||||
|
||||
_state.value = _state.value.copy(successCategory = resultCategory)
|
||||
} catch (e: HttpException) {
|
||||
if (e.code() != 422) throw e
|
||||
val errorBody = e.response()?.errorBody()?.string()
|
||||
val validationError = Gson().fromJson(errorBody, ValidationError::class.java)
|
||||
val errMsg = validationError.detail.joinToString("\n")
|
||||
|
||||
Log.e("CategoryCreate", "Got HTTP 422: $validationError")
|
||||
|
||||
_state.value = _state.value.copy(
|
||||
errorMessage = getApplication<Application>().getString(R.string.failed_to_save_category, errMsg)
|
||||
)
|
||||
} finally {
|
||||
_state.value = _state.value.copy(isLoading = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue