Adds confirmation when going up from the edit monster screen to save, cancel, or discard changes.
This commit is contained in:
		| @@ -1,10 +1,11 @@ | ||||
| package com.majinnaibu.monstercards; | ||||
|  | ||||
| import android.os.Bundle; | ||||
| import android.view.MenuItem; | ||||
|  | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
| import androidx.navigation.NavController; | ||||
| import androidx.navigation.Navigation; | ||||
| import androidx.navigation.fragment.NavHostFragment; | ||||
| import androidx.navigation.ui.AppBarConfiguration; | ||||
| import androidx.navigation.ui.NavigationUI; | ||||
| @@ -12,13 +13,19 @@ import androidx.navigation.ui.NavigationUI; | ||||
| import com.google.android.material.bottomnavigation.BottomNavigationView; | ||||
| import com.majinnaibu.monstercards.init.AppCenterInitializer; | ||||
| import com.majinnaibu.monstercards.init.FlipperInitializer; | ||||
| import com.majinnaibu.monstercards.utils.Logger; | ||||
|  | ||||
| public class MainActivity extends AppCompatActivity { | ||||
|  | ||||
|     @Override | ||||
|     public boolean onSupportNavigateUp() { | ||||
|         NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment); | ||||
|         return navController.navigateUp() || super.onSupportNavigateUp(); | ||||
|     public boolean onOptionsItemSelected(@NonNull MenuItem item) { | ||||
|         if (item.getItemId() == android.R.id.home) { | ||||
|             Logger.logDebug("Home selected (in MainActivity)"); | ||||
|             getOnBackPressedDispatcher().onBackPressed(); | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         return super.onOptionsItemSelected(item); | ||||
|     } | ||||
|  | ||||
|     @SuppressWarnings("ConstantConditions") | ||||
| @@ -44,5 +51,4 @@ public class MainActivity extends AppCompatActivity { | ||||
|         NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration); | ||||
|         NavigationUI.setupWithNavController(navView, navController); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -4,6 +4,7 @@ package com.majinnaibu.monstercards.data; | ||||
| import androidx.room.Dao; | ||||
| import androidx.room.Delete; | ||||
| import androidx.room.Insert; | ||||
| import androidx.room.OnConflictStrategy; | ||||
| import androidx.room.Query; | ||||
|  | ||||
| import com.majinnaibu.monstercards.models.Monster; | ||||
| @@ -30,6 +31,9 @@ public interface MonsterDAO { | ||||
|     @Insert | ||||
|     Completable insertAll(Monster... monsters); | ||||
|  | ||||
|     @Insert(onConflict = OnConflictStrategy.REPLACE) | ||||
|     Completable save(Monster... monsters); | ||||
|  | ||||
|     @Delete | ||||
|     Completable delete(Monster monster); | ||||
| } | ||||
|   | ||||
| @@ -63,4 +63,9 @@ public class MonsterRepository { | ||||
|         return result; | ||||
|     } | ||||
|  | ||||
|     public Completable saveMonster(Monster monster) { | ||||
|         Completable result = m_db.monsterDAO().save(monster); | ||||
|         result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()); | ||||
|         return result; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -6,14 +6,17 @@ import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.TextView; | ||||
|  | ||||
| import androidx.activity.OnBackPressedCallback; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.appcompat.app.AlertDialog; | ||||
| import androidx.lifecycle.ViewModelProvider; | ||||
| import androidx.navigation.NavBackStackEntry; | ||||
| import androidx.navigation.NavController; | ||||
| import androidx.navigation.NavDirections; | ||||
| import androidx.navigation.Navigation; | ||||
|  | ||||
| import com.google.android.material.snackbar.Snackbar; | ||||
| import com.majinnaibu.monstercards.R; | ||||
| import com.majinnaibu.monstercards.data.MonsterRepository; | ||||
| import com.majinnaibu.monstercards.models.Monster; | ||||
| @@ -21,19 +24,20 @@ import com.majinnaibu.monstercards.ui.MCFragment; | ||||
| import com.majinnaibu.monstercards.ui.monster.MonsterDetailFragmentArgs; | ||||
| import com.majinnaibu.monstercards.utils.Logger; | ||||
|  | ||||
| import java.util.Objects; | ||||
| import java.util.UUID; | ||||
|  | ||||
| import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; | ||||
| import io.reactivex.rxjava3.observers.DisposableCompletableObserver; | ||||
| import io.reactivex.rxjava3.observers.DisposableSingleObserver; | ||||
| import io.reactivex.rxjava3.schedulers.Schedulers; | ||||
|  | ||||
| @SuppressWarnings("FieldCanBeLocal") | ||||
| public class EditMonsterFragment extends MCFragment { | ||||
|  | ||||
|     private EditMonsterViewModel mViewModel; | ||||
|     private ViewHolder mHolder; | ||||
|  | ||||
|     public static EditMonsterFragment newInstance() { | ||||
|         return new EditMonsterFragment(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, | ||||
|                              @Nullable Bundle savedInstanceState) { | ||||
| @@ -49,20 +53,20 @@ public class EditMonsterFragment extends MCFragment { | ||||
|  | ||||
|         View root = inflater.inflate(R.layout.fragment_edit_monster, container, false); | ||||
|         mHolder = new ViewHolder(root); | ||||
|         requireAppCompatActivity().getSupportActionBar().setTitle(getString(R.string.title_edit_monster, getString(R.string.default_monster_name))); | ||||
|  | ||||
|         setTitle(getString(R.string.title_edit_monster, getString(R.string.default_monster_name))); | ||||
|  | ||||
|         // TODO: Show a loading spinner until we have the monster loaded. | ||||
|         if (mViewModel.hasError() || !mViewModel.hasLoaded() || !mViewModel.getMonsterId().getValue().equals(monsterId)) { | ||||
|         if (mViewModel.hasError() || !mViewModel.hasLoaded() || !Objects.equals(mViewModel.getMonsterId().getValue(), monsterId)) { | ||||
|             repository.getMonster(monsterId).toObservable() | ||||
|                     .firstOrError() | ||||
|                     .subscribe(new DisposableSingleObserver<Monster>() { | ||||
|                         @Override | ||||
|                         public void onSuccess(@io.reactivex.rxjava3.annotations.NonNull Monster monster) { | ||||
|                             Logger.logDebug(String.format("Monster loaded: %s", monster.name)); | ||||
|                             mViewModel.setHasLoaded(true); | ||||
|                             mViewModel.setHasError(false); | ||||
|                             mViewModel.copyFromMonster(monster); | ||||
|                             requireAppCompatActivity().getSupportActionBar().setTitle(getString(R.string.title_edit_monster, monster.name)); | ||||
|                             setTitle(getString(R.string.title_edit_monster, monster.name)); | ||||
|                             dispose(); | ||||
|                         } | ||||
|  | ||||
| @@ -77,20 +81,57 @@ public class EditMonsterFragment extends MCFragment { | ||||
|                     }); | ||||
|         } | ||||
|         mHolder.basicInfoButton.setOnClickListener(v -> { | ||||
|             // TODO: Navigate to the EditBasicInfo fragment | ||||
|             Logger.logDebug("Basic Info clicked"); | ||||
|             NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditBasicInfoFragment(); | ||||
|             View view = getView(); | ||||
|             assert view != null; | ||||
|             Navigation.findNavController(view).navigate(action); | ||||
|         }); | ||||
|  | ||||
|         return root; | ||||
|     } | ||||
|         requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) { | ||||
|             @Override | ||||
|             public void handleOnBackPressed() { | ||||
|                 if (mViewModel.hasChanges()) { | ||||
|                     View view = getView(); | ||||
|                     AlertDialog alertDialog = new AlertDialog.Builder(requireContext()).create(); | ||||
|                     alertDialog.setTitle("Unsaved Changes"); | ||||
|                     alertDialog.setMessage("Do you want to save your changes?"); | ||||
|                     alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, "Save", (dialog, id) -> { | ||||
|                         // Save the monster. Navigate up if the save is successful. Show a SnackBar if there was an error. | ||||
|                         getMonsterRepository().saveMonster(mViewModel.buildMonster()) | ||||
|                                 .subscribeOn(Schedulers.io()) | ||||
|                                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                                 .subscribe( | ||||
|                                         new DisposableCompletableObserver() { | ||||
|                                             @Override | ||||
|                                             public void onComplete() { | ||||
|                                                 Navigation.findNavController(requireView()).navigateUp(); | ||||
|                                             } | ||||
|  | ||||
|     @Override | ||||
|     public void onActivityCreated(@Nullable Bundle savedInstanceState) { | ||||
|         super.onActivityCreated(savedInstanceState); | ||||
|                                             @Override | ||||
|                                             public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) { | ||||
|                                                 Logger.logError("Error creating monster", e); | ||||
|                                                 assert view != null; | ||||
|                                                 Snackbar.make(view, getString(R.string.snackbar_failed_to_create_monster), Snackbar.LENGTH_LONG) | ||||
|                                                         .setAction("Action", null).show(); | ||||
|                                             } | ||||
|                                         }); | ||||
|                     }); | ||||
|                     alertDialog.setButton(AlertDialog.BUTTON_NEGATIVE, "Discard", (dialog, id) -> { | ||||
|                         // Navigate up ignoring unsaved changes. | ||||
|                         Navigation.findNavController(requireView()).navigateUp(); | ||||
|                     }); | ||||
|                     alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, "Cancel", (dialog, id) -> { | ||||
|                         // Do nothing. | ||||
|                     }); | ||||
|                     alertDialog.show(); | ||||
|                 } else { | ||||
|                     // No changes so we can safely leave. | ||||
|                     Navigation.findNavController(requireView()).navigateUp(); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         return root; | ||||
|     } | ||||
|  | ||||
|     private static class ViewHolder { | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import com.majinnaibu.monstercards.models.Monster; | ||||
|  | ||||
| import java.util.UUID; | ||||
|  | ||||
| @SuppressWarnings({"ConstantConditions", "unused"}) | ||||
| public class EditMonsterViewModel extends ViewModel { | ||||
|     private final MutableLiveData<String> mName; | ||||
|     private final MutableLiveData<UUID> mMonsterId; | ||||
| @@ -20,6 +21,7 @@ public class EditMonsterViewModel extends ViewModel { | ||||
|     private final MutableLiveData<String> mSubtype; | ||||
|     private final MutableLiveData<String> mAlignment; | ||||
|     private final MutableLiveData<String> mCustomHitPoints; | ||||
|     private final MutableLiveData<Boolean> mHasChanges; | ||||
|  | ||||
|     public EditMonsterViewModel() { | ||||
|  | ||||
| @@ -33,6 +35,8 @@ public class EditMonsterViewModel extends ViewModel { | ||||
|         mSubtype = new MutableLiveData<>(""); | ||||
|         mAlignment = new MutableLiveData<>(""); | ||||
|         mCustomHitPoints = new MutableLiveData<>(""); | ||||
|         // TODO: consider initializing this to true so all new monsters need saving | ||||
|         mHasChanges = new MutableLiveData<>(false); | ||||
|     } | ||||
|  | ||||
|     public void copyFromMonster(Monster monster) { | ||||
| @@ -44,6 +48,7 @@ public class EditMonsterViewModel extends ViewModel { | ||||
|         mSubtype.setValue(monster.subtype); | ||||
|         mAlignment.setValue(monster.alignment); | ||||
|         mCustomHitPoints.setValue(monster.customHPDescription); | ||||
|         mHasChanges.setValue(false); | ||||
|     } | ||||
|  | ||||
|     public LiveData<String> getName() { | ||||
| @@ -52,9 +57,9 @@ public class EditMonsterViewModel extends ViewModel { | ||||
|  | ||||
|     public void setName(@NonNull String name) { | ||||
|         mName.setValue(name); | ||||
|         mHasChanges.setValue(true); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     public LiveData<UUID> getMonsterId() { | ||||
|         return mMonsterId; | ||||
|     } | ||||
| @@ -97,6 +102,7 @@ public class EditMonsterViewModel extends ViewModel { | ||||
|  | ||||
|     public void setSize(@NonNull String size) { | ||||
|         mSize.setValue(size); | ||||
|         mHasChanges.setValue(true); | ||||
|     } | ||||
|  | ||||
|     public LiveData<String> getType() { | ||||
| @@ -105,6 +111,7 @@ public class EditMonsterViewModel extends ViewModel { | ||||
|  | ||||
|     public void setType(@NonNull String type) { | ||||
|         mType.setValue(type); | ||||
|         mHasChanges.setValue(true); | ||||
|     } | ||||
|  | ||||
|     public LiveData<String> getSubtype() { | ||||
| @@ -113,6 +120,7 @@ public class EditMonsterViewModel extends ViewModel { | ||||
|  | ||||
|     public void setSubtype(@NonNull String subType) { | ||||
|         mSubtype.setValue(subType); | ||||
|         mHasChanges.setValue(true); | ||||
|     } | ||||
|  | ||||
|     public LiveData<String> getAlignment() { | ||||
| @@ -121,6 +129,7 @@ public class EditMonsterViewModel extends ViewModel { | ||||
|  | ||||
|     public void setAlignment(@NonNull String alignment) { | ||||
|         mAlignment.setValue(alignment); | ||||
|         mHasChanges.setValue(true); | ||||
|     } | ||||
|  | ||||
|     public LiveData<String> getCustomHitPoints() { | ||||
| @@ -129,5 +138,32 @@ public class EditMonsterViewModel extends ViewModel { | ||||
|  | ||||
|     public void setCustomHitPoints(String customHitPoints) { | ||||
|         mCustomHitPoints.setValue(customHitPoints); | ||||
|         mHasChanges.setValue(true); | ||||
|     } | ||||
|  | ||||
|     public LiveData<Boolean> getHasChanges() { | ||||
|         return mHasChanges; | ||||
|     } | ||||
|  | ||||
|     public void setHasChanges(@NonNull Boolean hasChanges) { | ||||
|         mHasChanges.setValue(hasChanges); | ||||
|     } | ||||
|  | ||||
|     public boolean hasChanges() { | ||||
|         return mHasChanges.getValue(); | ||||
|     } | ||||
|  | ||||
|     public Monster buildMonster() { | ||||
|         Monster monster = new Monster(); | ||||
|  | ||||
|         monster.id = mMonsterId.getValue(); | ||||
|         monster.name = mName.getValue(); | ||||
|         monster.size = mSize.getValue(); | ||||
|         monster.type = mType.getValue(); | ||||
|         monster.subtype = mSubtype.getValue(); | ||||
|         monster.alignment = mAlignment.getValue(); | ||||
|         monster.customHPDescription = mCustomHitPoints.getValue(); | ||||
|  | ||||
|         return monster; | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user