chore(mvvm): CategoryActivity ViewModel
This commit is contained in:
parent
fa2bb2c78a
commit
6832d25163
|
@ -7,30 +7,28 @@ import android.widget.ImageButton
|
|||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.p_vacho.neat_calendar.MyApplication
|
||||
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.CategoriesViewModel
|
||||
import com.p_vacho.neat_calendar.viewmodels.CategoryEvent
|
||||
import com.p_vacho.neat_calendar.viewmodels.CategoryMode
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class CategoriesActivity : AppCompatActivity() {
|
||||
private val viewModel: CategoriesViewModel by viewModels()
|
||||
private lateinit var adapter: CategoryAdapter
|
||||
|
||||
private lateinit var rvCategories: RecyclerView
|
||||
private lateinit var btnBack: ImageButton
|
||||
private lateinit var btnAddCategory: ImageButton
|
||||
private lateinit var tvEmptyState: TextView
|
||||
|
||||
private lateinit var categories: MutableList<CategoryResponse>
|
||||
|
||||
private val createActivityLauncher = registerForActivityResult(
|
||||
androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
|
@ -42,8 +40,11 @@ class CategoriesActivity : AppCompatActivity() {
|
|||
val editedCategory: CategoryResponse? = result.data?.getParcelableExtra("editedCategory")
|
||||
|
||||
if (newCategory != null && editedCategory != null) throw IllegalStateException("Got both edit & new response")
|
||||
if (newCategory != null) categoryCreateReply(newCategory, CategoryMode.CREATE)
|
||||
if (editedCategory != null) categoryCreateReply(editedCategory, CategoryMode.EDIT)
|
||||
|
||||
when {
|
||||
newCategory != null -> viewModel.handleCategoryAddOrEdit(newCategory, CategoryMode.CREATE)
|
||||
editedCategory != null -> viewModel.handleCategoryAddOrEdit(editedCategory, CategoryMode.EDIT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -58,55 +59,47 @@ class CategoriesActivity : AppCompatActivity() {
|
|||
insets
|
||||
}
|
||||
|
||||
initializeViews()
|
||||
|
||||
// Observers
|
||||
viewModel.categoryEvents.observe(this) { event ->
|
||||
when (event) {
|
||||
is CategoryEvent.Init -> adapter.setCategories(event.categories)
|
||||
is CategoryEvent.Add -> adapter.addCategory(event.category)
|
||||
is CategoryEvent.Edit -> adapter.editCategory(event.category)
|
||||
is CategoryEvent.Remove -> {
|
||||
adapter.removeCategory(event.category)
|
||||
// TODO: Localize
|
||||
Toast.makeText(this, "Category deleted", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.isEmptyStateVisible.observe(this) { isVisible ->
|
||||
tvEmptyState.visibility = if (isVisible) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
// Listeners
|
||||
btnBack.setOnClickListener { finish() }
|
||||
btnAddCategory.setOnClickListener { handleCreateCategory() }
|
||||
|
||||
// Load initial categories
|
||||
viewModel.fetchCategories()
|
||||
}
|
||||
|
||||
private fun initializeViews() {
|
||||
rvCategories = findViewById(R.id.rvCategories)
|
||||
btnBack = findViewById(R.id.btnBack)
|
||||
btnAddCategory = findViewById(R.id.btnAddCategory)
|
||||
tvEmptyState = findViewById(R.id.tvEmptyState)
|
||||
|
||||
btnBack.setOnClickListener { finish() }
|
||||
btnAddCategory.setOnClickListener { handleCreateCategory() }
|
||||
|
||||
adapter = CategoryAdapter(
|
||||
mutableListOf(),
|
||||
{ category, _pos -> viewModel.deleteCategory(category.id) },
|
||||
::handleEditCategory
|
||||
)
|
||||
rvCategories.layoutManager = LinearLayoutManager(this)
|
||||
|
||||
lifecycleScope.launch {
|
||||
categories = fetchCategories().toMutableList()
|
||||
|
||||
val adapter = CategoryAdapter(categories, ::handleDeleteCategory, ::handleEditCategory)
|
||||
|
||||
rvCategories.adapter = adapter
|
||||
updateEmptyState()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all categories for the current user from the backend.
|
||||
*/
|
||||
private suspend fun fetchCategories(): List<CategoryResponse> {
|
||||
val userId = (application as MyApplication).tokenManager.userId
|
||||
?: run {
|
||||
finish()
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
return withContext(Dispatchers.IO) {
|
||||
RetrofitClient.categoryService.userCategories(userId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A callback function triggered by the Category Adapter, when the user
|
||||
* clicks on the delete button.
|
||||
*/
|
||||
private fun handleDeleteCategory(category: CategoryResponse, position: Int) {
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
RetrofitClient.categoryService.deleteCategory(category.id)
|
||||
}
|
||||
// Remove category from the adapter and update the UI
|
||||
(rvCategories.adapter as CategoryAdapter).removeCategoryAt(position)
|
||||
Toast.makeText(this@CategoriesActivity, "Category deleted", Toast.LENGTH_SHORT).show()
|
||||
updateEmptyState()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -133,30 +126,4 @@ class CategoriesActivity : AppCompatActivity() {
|
|||
}
|
||||
createActivityLauncher.launch(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Used as a callback, triggered when the CreateCategory Activity returns a result.
|
||||
*
|
||||
* The returned value (the new / edited category data) is passed over as a parameter.
|
||||
*/
|
||||
private fun categoryCreateReply(category: CategoryResponse, mode: CategoryMode) {
|
||||
val adapter = (rvCategories.adapter as CategoryAdapter)
|
||||
when (mode) {
|
||||
CategoryMode.CREATE -> {
|
||||
adapter.addCategory(category)
|
||||
updateEmptyState()
|
||||
}
|
||||
CategoryMode.EDIT -> { adapter.editCategory(category) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a helper function to toggle the visibility of the empty state message
|
||||
* when no categories are available.
|
||||
*
|
||||
* This should be called whenever a category was removed or added.
|
||||
*/
|
||||
private fun updateEmptyState() {
|
||||
tvEmptyState.visibility = if (categories.isEmpty()) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.p_vacho.neat_calendar.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
@ -43,11 +44,10 @@ class CategoryAdapter(
|
|||
override fun getItemCount(): Int = categories.size
|
||||
|
||||
/**
|
||||
* Remove a category from the list and notify the adapter.
|
||||
*
|
||||
* Call this after the onDeleteClick callback deletes the category from the backend API.
|
||||
* Remove an existing category from the list and notify the adapter.
|
||||
*/
|
||||
fun removeCategoryAt(position: Int) {
|
||||
fun removeCategory(category: CategoryResponse) {
|
||||
val position = categories.indexOfFirst { it.id == category.id }
|
||||
categories.removeAt(position)
|
||||
notifyItemRemoved(position)
|
||||
|
||||
|
@ -68,12 +68,24 @@ class CategoryAdapter(
|
|||
|
||||
/**
|
||||
* Edit an existing category, updating it in the UI.
|
||||
*
|
||||
* Call this after the onEditClick callback edits the category from the backend API.
|
||||
*/
|
||||
fun editCategory(category: CategoryResponse) {
|
||||
val position = categories.indexOfFirst { it.id == category.id }
|
||||
categories[position] = category
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the entire list of categories to a new one.
|
||||
*
|
||||
* This is mainly useful for delayed initialization (after data is fetched).
|
||||
*/
|
||||
fun setCategories(newCategories: List<CategoryResponse>) {
|
||||
categories.clear()
|
||||
categories.addAll(newCategories)
|
||||
|
||||
// We can't be more efficient here, we're changing the whole data set
|
||||
@Suppress("NotifyDataSetChanged")
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
package com.p_vacho.neat_calendar.viewmodels
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.p_vacho.neat_calendar.MyApplication
|
||||
import com.p_vacho.neat_calendar.api.RetrofitClient
|
||||
import com.p_vacho.neat_calendar.api.models.CategoryResponse
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
sealed class CategoryEvent {
|
||||
data class Init(val categories: List<CategoryResponse>) : CategoryEvent()
|
||||
data class Add(val category: CategoryResponse) : CategoryEvent()
|
||||
data class Edit(val position: Int, val category: CategoryResponse) : CategoryEvent()
|
||||
data class Remove(val position: Int, val category: CategoryResponse) : CategoryEvent()
|
||||
}
|
||||
|
||||
class CategoriesViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private val categories = mutableListOf<CategoryResponse>()
|
||||
|
||||
private val _isEmptyStateVisible = MutableLiveData<Boolean>()
|
||||
val isEmptyStateVisible: LiveData<Boolean> = _isEmptyStateVisible
|
||||
|
||||
private val _categoryEvents = MutableLiveData<CategoryEvent>()
|
||||
val categoryEvents: LiveData<CategoryEvent> = _categoryEvents
|
||||
|
||||
init {
|
||||
// Initialize empty state visibility to true as the default
|
||||
updateEmptyStateVisibility()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all categories for the current user from the backend.
|
||||
*
|
||||
* This function should always be called after initialization.
|
||||
*/
|
||||
fun fetchCategories() {
|
||||
viewModelScope.launch {
|
||||
val userId = (getApplication<MyApplication>()).tokenManager.userId ?: return@launch
|
||||
val fetchedCategories = withContext(Dispatchers.IO) {
|
||||
RetrofitClient.categoryService.userCategories(userId)
|
||||
}
|
||||
categories.clear()
|
||||
categories.addAll(fetchedCategories)
|
||||
_categoryEvents.value = CategoryEvent.Init(fetchedCategories)
|
||||
updateEmptyStateVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a specific category.
|
||||
*
|
||||
* This will perform an API call, removing the category from the backend.
|
||||
*/
|
||||
fun deleteCategory(categoryId: String) {
|
||||
val index = categories.indexOfFirst { it.id == categoryId }
|
||||
if (index == -1) throw IllegalStateException("Attempted to delete an unknown category; id: $categoryId")
|
||||
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
RetrofitClient.categoryService.deleteCategory(categoryId)
|
||||
}
|
||||
|
||||
val removedCategory = categories.removeAt(index)
|
||||
_categoryEvents.value = CategoryEvent.Remove(index, removedCategory)
|
||||
updateEmptyStateVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the categories list with a new/updated category data.
|
||||
*/
|
||||
fun handleCategoryAddOrEdit(category: CategoryResponse, mode: CategoryMode) {
|
||||
when (mode) {
|
||||
CategoryMode.CREATE -> {
|
||||
categories.add(category)
|
||||
_categoryEvents.value = CategoryEvent.Add(category)
|
||||
updateEmptyStateVisibility()
|
||||
}
|
||||
|
||||
CategoryMode.EDIT -> {
|
||||
val index = categories.indexOfFirst { it.id == category.id }
|
||||
if (index != -1) {
|
||||
categories[index] = category
|
||||
_categoryEvents.value = CategoryEvent.Edit(index, category)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the empty state visibility based on the categories list.
|
||||
*/
|
||||
private fun updateEmptyStateVisibility() {
|
||||
_isEmptyStateVisible.value = categories.isEmpty()
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue