added helper class for bulk delete and mad pets have bulk delete

This commit is contained in:
Alex
2026-04-07 15:13:15 -06:00
parent aa30efd3b6
commit 9eaf64c7a9
12 changed files with 476 additions and 185 deletions

View File

@@ -10,16 +10,16 @@ import androidx.recyclerview.widget.RecyclerView;
import com.example.petstoremobile.databinding.ItemInventoryBinding;
import com.example.petstoremobile.dtos.InventoryDTO;
import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.SelectionHelper;
import java.util.ArrayList;
import java.util.List;
public class InventoryAdapter extends RecyclerView.Adapter<InventoryAdapter.InventoryViewHolder> {
public class InventoryAdapter extends RecyclerView.Adapter<InventoryAdapter.InventoryViewHolder> implements BulkDeleteHandler.SelectableAdapter {
private final List<InventoryDTO> inventoryList;
private final OnInventoryClickListener clickListener;
private final List<Long> selectedIds = new ArrayList<>();
private boolean selectionMode = false;
private final SelectionHelper selectionHelper;
public interface OnInventoryClickListener {
void onInventoryClick(int position);
@@ -30,6 +30,27 @@ public class InventoryAdapter extends RecyclerView.Adapter<InventoryAdapter.Inve
public InventoryAdapter(List<InventoryDTO> inventoryList, OnInventoryClickListener clickListener) {
this.inventoryList = inventoryList;
this.clickListener = clickListener;
this.selectionHelper = new SelectionHelper(new SelectionHelper.SelectionListener() {
@Override
public void onSelectionChanged(int count) {
clickListener.onSelectionChanged(count);
}
@Override
public void onSelectionModeToggle(boolean selectionMode) {
notifyDataSetChanged();
}
});
}
@Override
public List<Long> getSelectedIds() {
return selectionHelper.getSelectedIds();
}
@Override
public void clearSelection() {
selectionHelper.clearSelection();
}
public static class InventoryViewHolder extends RecyclerView.ViewHolder {
@@ -71,64 +92,31 @@ public class InventoryAdapter extends RecyclerView.Adapter<InventoryAdapter.Inve
}
// Bulk delete selection mode
if (selectionMode) {
if (selectionHelper.isInSelectionMode()) {
binding.cbSelectInventory.setVisibility(View.VISIBLE);
binding.cbSelectInventory.setChecked(inv.getInventoryId() != null
&& selectedIds.contains(inv.getInventoryId()));
binding.cbSelectInventory.setChecked(selectionHelper.isSelected(inv.getInventoryId()));
} else {
binding.cbSelectInventory.setVisibility(View.GONE);
binding.cbSelectInventory.setChecked(false);
}
holder.itemView.setOnClickListener(v -> {
if (selectionMode) {
toggleSelection(inv.getInventoryId(), binding.cbSelectInventory);
if (selectionHelper.isInSelectionMode()) {
selectionHelper.toggleSelection(inv.getInventoryId());
notifyItemChanged(position);
} else {
clickListener.onInventoryClick(holder.getAdapterPosition());
}
});
holder.itemView.setOnLongClickListener(v -> {
if (!selectionMode) {
selectionMode = true;
toggleSelection(inv.getInventoryId(), binding.cbSelectInventory);
notifyDataSetChanged();
if (!selectionHelper.isInSelectionMode()) {
selectionHelper.startSelection(inv.getInventoryId());
}
return true;
});
}
private void toggleSelection(Long id, android.widget.CheckBox checkBox) {
if (id == null)
return;
if (selectedIds.contains(id)) {
selectedIds.remove(id);
checkBox.setChecked(false);
} else {
selectedIds.add(id);
checkBox.setChecked(true);
}
clickListener.onSelectionChanged(selectedIds.size());
if (selectedIds.isEmpty()) {
selectionMode = false;
notifyDataSetChanged();
}
}
public List<Long> getSelectedIds() {
return new ArrayList<>(selectedIds);
}
public void clearSelection() {
selectedIds.clear();
selectionMode = false;
notifyDataSetChanged();
}
public boolean isInSelectionMode() {
return selectionMode;
}
@Override
public int getItemCount() {
return inventoryList.size();

View File

@@ -2,6 +2,7 @@ package com.example.petstoremobile.adapters;
import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
@@ -10,25 +11,41 @@ import com.example.petstoremobile.R;
import com.example.petstoremobile.api.PetApi;
import com.example.petstoremobile.databinding.ItemPetBinding;
import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.GlideUtils;
import com.example.petstoremobile.utils.SelectionHelper;
import java.util.List;
public class PetAdapter extends RecyclerView.Adapter<PetAdapter.PetViewHolder> {
public class PetAdapter extends RecyclerView.Adapter<PetAdapter.PetViewHolder> implements BulkDeleteHandler.SelectableAdapter {
private List<PetDTO> petList;
private OnPetClickListener petClickListener;
private String baseUrl;
private String token;
private final SelectionHelper selectionHelper;
// Interface for pet click on recycler view
public interface OnPetClickListener {
void onPetClick(int position);
void onSelectionChanged(int selectedCount);
}
//Constructor
public PetAdapter(List<PetDTO> petList, OnPetClickListener petClickListener) {
this.petList = petList;
this.petClickListener = petClickListener;
this.selectionHelper = new SelectionHelper(new SelectionHelper.SelectionListener() {
@Override
public void onSelectionChanged(int count) {
petClickListener.onSelectionChanged(count);
}
@Override
public void onSelectionModeToggle(boolean selectionMode) {
notifyDataSetChanged();
}
});
}
public void setBaseUrl(String baseUrl) {
@@ -39,6 +56,16 @@ public class PetAdapter extends RecyclerView.Adapter<PetAdapter.PetViewHolder> {
this.token = token;
}
@Override
public List<Long> getSelectedIds() {
return selectionHelper.getSelectedIds();
}
@Override
public void clearSelection() {
selectionHelper.clearSelection();
}
// Get the controls of each row in recycler view
public static class PetViewHolder extends RecyclerView.ViewHolder {
private final ItemPetBinding binding;
@@ -91,8 +118,31 @@ public class PetAdapter extends RecyclerView.Adapter<PetAdapter.PetViewHolder> {
binding.ivPetProfile.setImageResource(R.drawable.placeholder);
}
// Bulk delete selection mode
if (selectionHelper.isInSelectionMode()) {
binding.cbSelectPet.setVisibility(View.VISIBLE);
binding.cbSelectPet.setChecked(selectionHelper.isSelected(pet.getPetId()));
} else {
binding.cbSelectPet.setVisibility(View.GONE);
binding.cbSelectPet.setChecked(false);
}
//when a row is clicked, open the detail view
holder.itemView.setOnClickListener(v -> petClickListener.onPetClick(position));
holder.itemView.setOnClickListener(v -> {
if (selectionHelper.isInSelectionMode()) {
selectionHelper.toggleSelection(pet.getPetId());
notifyItemChanged(position);
} else {
petClickListener.onPetClick(position);
}
});
holder.itemView.setOnLongClickListener(v -> {
if (!selectionHelper.isInSelectionMode()) {
selectionHelper.startSelection(pet.getPetId());
}
return true;
});
}
@Override

View File

@@ -1,5 +1,6 @@
package com.example.petstoremobile.api;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.PetDTO;
@@ -8,6 +9,7 @@ import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.DELETE;
import retrofit2.http.GET;
import retrofit2.http.HTTP;
import retrofit2.http.Multipart;
import retrofit2.http.POST;
import retrofit2.http.PUT;
@@ -48,6 +50,10 @@ public interface PetApi {
@DELETE("api/v1/pets/{id}")
Call<Void> deletePet(@Path("id") Long id);
// Bulk delete pets
@HTTP(method = "DELETE", path = "api/v1/pets", hasBody = true)
Call<Void> bulkDeletePets(@Body BulkDeleteRequest request);
// Upload pet image
@Multipart
@POST("api/v1/pets/{id}/image")

View File

@@ -24,6 +24,7 @@ import com.example.petstoremobile.databinding.FragmentInventoryBinding;
import com.example.petstoremobile.dtos.InventoryDTO;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.viewmodels.InventoryViewModel;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
@@ -44,6 +45,7 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
private List<StoreDTO> storeList = new ArrayList<>();
private InventoryAdapter adapter;
private InventoryViewModel viewModel;
private BulkDeleteHandler bulkDeleteHandler;
// Pagination
private int currentPage = 0;
@@ -72,6 +74,7 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
setupStoreFilter();
setupSwipeRefresh();
setupFilterToggle();
setupBulkDelete();
loadInventory(true);
loadStoreData();
@@ -87,11 +90,25 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
}
});
binding.btnBulkDelete.setOnClickListener(v -> confirmBulkDelete());
return binding.getRoot();
}
private void setupBulkDelete() {
bulkDeleteHandler = new BulkDeleteHandler(
this,
binding.layoutBulkDelete,
binding.tvSelectionCount,
binding.btnBulkDelete,
new BulkDeleteHandler.SelectableAdapter() {
@Override public List<Long> getSelectedIds() { return adapter.getSelectedIds(); }
@Override public void clearSelection() { adapter.clearSelection(); }
},
"inventory item",
viewModel::bulkDeleteInventory,
() -> loadInventory(true)
);
}
@Override
public void onDestroyView() {
super.onDestroyView();
@@ -243,50 +260,6 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
});
}
/**
* Displays a confirmation dialog before performing a bulk deletion of selected items.
*/
private void confirmBulkDelete() {
List<Long> ids = adapter.getSelectedIds();
if (ids.isEmpty())
return;
new androidx.appcompat.app.AlertDialog.Builder(requireContext())
.setTitle("Delete " + ids.size() + " item(s)?")
.setMessage("This cannot be undone.")
.setPositiveButton("Delete", (d, w) -> bulkDelete(ids))
.setNegativeButton("Cancel", null)
.show();
}
/**
* Executes the bulk deletion of inventory items through the ViewModel.
*/
private void bulkDelete(List<Long> ids) {
viewModel.bulkDeleteInventory(ids).observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status != Resource.Status.LOADING) {
if (resource.status == Resource.Status.SUCCESS) {
adapter.clearSelection();
hideBulkDeleteBar();
loadInventory(true);
Toast.makeText(getContext(), ids.size() + " item(s) deleted", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
}
}
});
}
/**
* Hides the bulk deletion UI bar.
*/
private void hideBulkDeleteBar() {
if (binding != null) {
binding.btnBulkDelete.setVisibility(View.GONE);
binding.tvSelectionCount.setVisibility(View.GONE);
}
}
/**
* Navigates to the inventory detail screen for a specific item or to add a new one.
*/
@@ -322,12 +295,8 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
*/
@Override
public void onSelectionChanged(int selectedCount) {
if (selectedCount > 0) {
binding.btnBulkDelete.setVisibility(View.VISIBLE);
binding.tvSelectionCount.setVisibility(View.VISIBLE);
binding.tvSelectionCount.setText(selectedCount + " selected");
} else {
hideBulkDeleteBar();
if (bulkDeleteHandler != null) {
bulkDeleteHandler.onSelectionChanged(selectedCount);
}
}
}

View File

@@ -25,6 +25,7 @@ import com.example.petstoremobile.databinding.FragmentPetBinding;
import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.viewmodels.PetViewModel;
@@ -46,6 +47,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
private PetAdapter adapter;
private PetViewModel viewModel;
private StoreViewModel storeViewModel;
private BulkDeleteHandler bulkDeleteHandler;
@Inject @Named("baseUrl") String baseUrl;
@@ -74,6 +76,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
setupStoreFilter();
setupSwipeRefresh();
setupFilterToggle();
setupBulkDelete();
binding.fabAddPet.setOnClickListener(v -> openPetDetails());
@@ -90,6 +93,22 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
return binding.getRoot();
}
private void setupBulkDelete() {
bulkDeleteHandler = new BulkDeleteHandler(
this,
binding.layoutBulkDelete,
binding.tvSelectionCount,
binding.btnBulkDelete,
new BulkDeleteHandler.SelectableAdapter() {
@Override public List<Long> getSelectedIds() { return adapter.getSelectedIds(); }
@Override public void clearSelection() { adapter.clearSelection(); }
},
"pet",
viewModel::bulkDeletePets,
this::loadPetData
);
}
@Override
public void onDestroyView() {
super.onDestroyView();
@@ -231,6 +250,13 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
openPetProfile(position);
}
@Override
public void onSelectionChanged(int selectedCount) {
if (bulkDeleteHandler != null) {
bulkDeleteHandler.onSelectionChanged(selectedCount);
}
}
/**
* Fetches pet data from the server with all active filters.
*/

View File

@@ -3,6 +3,7 @@ package com.example.petstoremobile.repositories;
import androidx.lifecycle.LiveData;
import com.example.petstoremobile.api.PetApi;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.utils.Resource;
@@ -57,6 +58,13 @@ public class PetRepository extends BaseRepository {
return executeCall(petApi.deletePet(id));
}
/**
* Sends a request to the API to delete multiple pet records.
*/
public LiveData<Resource<Void>> bulkDeletePets(BulkDeleteRequest request) {
return executeCall(petApi.bulkDeletePets(request));
}
/**
* Uploads an image file for a specific pet via the API.
*/

View File

@@ -0,0 +1,108 @@
package com.example.petstoremobile.utils;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.LiveData;
import java.util.List;
/**
* A helper class to handle the UI and logic for bulk deletion across different fragments.
*/
public class BulkDeleteHandler {
/**
* Interface that adapters must implement to support bulk selection.
*/
public interface SelectableAdapter {
List<Long> getSelectedIds();
void clearSelection();
}
/**
* Functional interface for the API call execution.
*/
public interface BulkDeleteOperation {
LiveData<Resource<Void>> execute(List<Long> ids);
}
private final Fragment fragment;
private final View layoutBar;
private final TextView tvCount;
private final SelectableAdapter adapter;
private final BulkDeleteOperation operation;
private final Runnable onSuccess;
private final String itemName;
public BulkDeleteHandler(Fragment fragment,
View layoutBar,
TextView tvCount,
Button btnDelete,
SelectableAdapter adapter,
String itemName,
BulkDeleteOperation operation,
Runnable onSuccess) {
this.fragment = fragment;
this.layoutBar = layoutBar;
this.tvCount = tvCount;
this.adapter = adapter;
this.operation = operation;
this.onSuccess = onSuccess;
this.itemName = itemName;
btnDelete.setOnClickListener(v -> confirmDelete());
}
/**
* Updates the UI when the selection count changes.
*/
public void onSelectionChanged(int selectedCount) {
if (selectedCount > 0) {
layoutBar.setVisibility(View.VISIBLE);
tvCount.setText(selectedCount + " selected");
} else {
hideBar();
}
}
/**
* Hides the bulk delete bar and resets state.
*/
public void hideBar() {
if (layoutBar != null) {
layoutBar.setVisibility(View.GONE);
}
}
/**
* Shows the confirmation dialog.
*/
private void confirmDelete() {
List<Long> ids = adapter.getSelectedIds();
if (ids.isEmpty()) return;
DialogUtils.showBulkDeleteConfirmDialog(fragment.requireContext(), ids.size(), () -> performDelete(ids));
}
/**
* Executes the deletion via the provided operation.
*/
private void performDelete(List<Long> ids) {
operation.execute(ids).observe(fragment.getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status != Resource.Status.LOADING) {
if (resource.status == Resource.Status.SUCCESS) {
adapter.clearSelection();
hideBar();
onSuccess.run();
Toast.makeText(fragment.getContext(), ids.size() + " " + itemName + "(s) deleted", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(fragment.getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
}
}
});
}
}

View File

@@ -34,6 +34,18 @@ public class DialogUtils {
showConfirmDialog(context, "Delete " + itemName + "?", "Are you sure you want to delete this " + itemName.toLowerCase() + "? This action cannot be undone.", callback);
}
/**
* Shows a confirmation dialog with specific "Delete" and "Cancel" buttons.
*/
public static void showBulkDeleteConfirmDialog(Context context, int count, DialogCallback callback) {
new AlertDialog.Builder(context)
.setTitle("Delete " + count + " item(s)?")
.setMessage("This cannot be undone.")
.setPositiveButton("Delete", (dialog, which) -> callback.onConfirm())
.setNegativeButton("Cancel", null)
.show();
}
/**
* Shows a simple information or error dialog with an "OK" button.
*/

View File

@@ -0,0 +1,66 @@
package com.example.petstoremobile.utils;
import java.util.ArrayList;
import java.util.List;
/**
* Helper class to manage selection state in Adapters for bulk operations.
*/
public class SelectionHelper {
private final List<Long> selectedIds = new ArrayList<>();
private boolean selectionMode = false;
private final SelectionListener listener;
public interface SelectionListener {
void onSelectionChanged(int count);
void onSelectionModeToggle(boolean selectionMode);
}
public SelectionHelper(SelectionListener listener) {
this.listener = listener;
}
public void toggleSelection(Long id) {
if (id == null) return;
if (selectedIds.contains(id)) {
selectedIds.remove(id);
} else {
selectedIds.add(id);
}
listener.onSelectionChanged(selectedIds.size());
if (selectedIds.isEmpty() && selectionMode) {
selectionMode = false;
listener.onSelectionModeToggle(false);
}
}
public void startSelection(Long id) {
selectionMode = true;
selectedIds.add(id);
listener.onSelectionChanged(selectedIds.size());
listener.onSelectionModeToggle(true);
}
public boolean isSelected(Long id) {
return selectedIds.contains(id);
}
public boolean isInSelectionMode() {
return selectionMode;
}
public List<Long> getSelectedIds() {
return new ArrayList<>(selectedIds);
}
public void clearSelection() {
selectedIds.clear();
selectionMode = false;
listener.onSelectionChanged(0);
listener.onSelectionModeToggle(false);
}
}

View File

@@ -3,11 +3,14 @@ package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.repositories.PetRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@@ -57,6 +60,13 @@ public class PetViewModel extends ViewModel {
return repository.deletePet(id);
}
/**
* Deletes multiple pet records.
*/
public LiveData<Resource<Void>> bulkDeletePets(List<Long> ids) {
return repository.bulkDeletePets(new BulkDeleteRequest(ids));
}
/**
* Uploads an image for a specific pet.
*/

View File

@@ -132,6 +132,37 @@
</LinearLayout>
<LinearLayout
android:id="@+id/layoutBulkDelete"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:background="@color/primary_medium"
android:paddingStart="16dp"
android:paddingEnd="8dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:visibility="gone">
<TextView
android:id="@+id/tvSelectionCount"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="0 selected"
android:textColor="@color/white"/>
<Button
android:id="@+id/btnBulkDelete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Delete Selected"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"/>
</LinearLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshPet"
android:layout_width="match_parent"

View File

@@ -3,12 +3,27 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:background="@color/white">
android:background="@color/white"
android:gravity="center_vertical">
<CheckBox
android:id="@+id/cbSelectPet"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:visibility="gone"
android:clickable="false"
android:focusable="false"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
@@ -116,3 +131,5 @@
android:layout_marginTop="12dp"/>
</LinearLayout>
</LinearLayout>