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; | package com.majinnaibu.monstercards; | ||||||
|  |  | ||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
|  | import android.view.MenuItem; | ||||||
|  |  | ||||||
|  | import androidx.annotation.NonNull; | ||||||
| import androidx.appcompat.app.AppCompatActivity; | import androidx.appcompat.app.AppCompatActivity; | ||||||
| import androidx.navigation.NavController; | import androidx.navigation.NavController; | ||||||
| import androidx.navigation.Navigation; |  | ||||||
| import androidx.navigation.fragment.NavHostFragment; | import androidx.navigation.fragment.NavHostFragment; | ||||||
| import androidx.navigation.ui.AppBarConfiguration; | import androidx.navigation.ui.AppBarConfiguration; | ||||||
| import androidx.navigation.ui.NavigationUI; | import androidx.navigation.ui.NavigationUI; | ||||||
| @@ -12,13 +13,19 @@ import androidx.navigation.ui.NavigationUI; | |||||||
| import com.google.android.material.bottomnavigation.BottomNavigationView; | import com.google.android.material.bottomnavigation.BottomNavigationView; | ||||||
| import com.majinnaibu.monstercards.init.AppCenterInitializer; | import com.majinnaibu.monstercards.init.AppCenterInitializer; | ||||||
| import com.majinnaibu.monstercards.init.FlipperInitializer; | import com.majinnaibu.monstercards.init.FlipperInitializer; | ||||||
|  | import com.majinnaibu.monstercards.utils.Logger; | ||||||
|  |  | ||||||
| public class MainActivity extends AppCompatActivity { | public class MainActivity extends AppCompatActivity { | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public boolean onSupportNavigateUp() { |     public boolean onOptionsItemSelected(@NonNull MenuItem item) { | ||||||
|         NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment); |         if (item.getItemId() == android.R.id.home) { | ||||||
|         return navController.navigateUp() || super.onSupportNavigateUp(); |             Logger.logDebug("Home selected (in MainActivity)"); | ||||||
|  |             getOnBackPressedDispatcher().onBackPressed(); | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return super.onOptionsItemSelected(item); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @SuppressWarnings("ConstantConditions") |     @SuppressWarnings("ConstantConditions") | ||||||
| @@ -44,5 +51,4 @@ public class MainActivity extends AppCompatActivity { | |||||||
|         NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration); |         NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration); | ||||||
|         NavigationUI.setupWithNavController(navView, navController); |         NavigationUI.setupWithNavController(navView, navController); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -4,6 +4,7 @@ package com.majinnaibu.monstercards.data; | |||||||
| import androidx.room.Dao; | import androidx.room.Dao; | ||||||
| import androidx.room.Delete; | import androidx.room.Delete; | ||||||
| import androidx.room.Insert; | import androidx.room.Insert; | ||||||
|  | import androidx.room.OnConflictStrategy; | ||||||
| import androidx.room.Query; | import androidx.room.Query; | ||||||
|  |  | ||||||
| import com.majinnaibu.monstercards.models.Monster; | import com.majinnaibu.monstercards.models.Monster; | ||||||
| @@ -30,6 +31,9 @@ public interface MonsterDAO { | |||||||
|     @Insert |     @Insert | ||||||
|     Completable insertAll(Monster... monsters); |     Completable insertAll(Monster... monsters); | ||||||
|  |  | ||||||
|  |     @Insert(onConflict = OnConflictStrategy.REPLACE) | ||||||
|  |     Completable save(Monster... monsters); | ||||||
|  |  | ||||||
|     @Delete |     @Delete | ||||||
|     Completable delete(Monster monster); |     Completable delete(Monster monster); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -63,4 +63,9 @@ public class MonsterRepository { | |||||||
|         return result; |         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.view.ViewGroup; | ||||||
| import android.widget.TextView; | import android.widget.TextView; | ||||||
|  |  | ||||||
|  | import androidx.activity.OnBackPressedCallback; | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
|  | import androidx.appcompat.app.AlertDialog; | ||||||
| import androidx.lifecycle.ViewModelProvider; | import androidx.lifecycle.ViewModelProvider; | ||||||
| import androidx.navigation.NavBackStackEntry; | import androidx.navigation.NavBackStackEntry; | ||||||
| import androidx.navigation.NavController; | import androidx.navigation.NavController; | ||||||
| import androidx.navigation.NavDirections; | import androidx.navigation.NavDirections; | ||||||
| import androidx.navigation.Navigation; | import androidx.navigation.Navigation; | ||||||
|  |  | ||||||
|  | import com.google.android.material.snackbar.Snackbar; | ||||||
| import com.majinnaibu.monstercards.R; | import com.majinnaibu.monstercards.R; | ||||||
| import com.majinnaibu.monstercards.data.MonsterRepository; | import com.majinnaibu.monstercards.data.MonsterRepository; | ||||||
| import com.majinnaibu.monstercards.models.Monster; | 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.ui.monster.MonsterDetailFragmentArgs; | ||||||
| import com.majinnaibu.monstercards.utils.Logger; | import com.majinnaibu.monstercards.utils.Logger; | ||||||
|  |  | ||||||
|  | import java.util.Objects; | ||||||
| import java.util.UUID; | 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.observers.DisposableSingleObserver; | ||||||
|  | import io.reactivex.rxjava3.schedulers.Schedulers; | ||||||
|  |  | ||||||
|  | @SuppressWarnings("FieldCanBeLocal") | ||||||
| public class EditMonsterFragment extends MCFragment { | public class EditMonsterFragment extends MCFragment { | ||||||
|  |  | ||||||
|     private EditMonsterViewModel mViewModel; |     private EditMonsterViewModel mViewModel; | ||||||
|     private ViewHolder mHolder; |     private ViewHolder mHolder; | ||||||
|  |  | ||||||
|     public static EditMonsterFragment newInstance() { |  | ||||||
|         return new EditMonsterFragment(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, |     public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, | ||||||
|                              @Nullable Bundle savedInstanceState) { |                              @Nullable Bundle savedInstanceState) { | ||||||
| @@ -49,20 +53,20 @@ public class EditMonsterFragment extends MCFragment { | |||||||
|  |  | ||||||
|         View root = inflater.inflate(R.layout.fragment_edit_monster, container, false); |         View root = inflater.inflate(R.layout.fragment_edit_monster, container, false); | ||||||
|         mHolder = new ViewHolder(root); |         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. |         // 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() |             repository.getMonster(monsterId).toObservable() | ||||||
|                     .firstOrError() |                     .firstOrError() | ||||||
|                     .subscribe(new DisposableSingleObserver<Monster>() { |                     .subscribe(new DisposableSingleObserver<Monster>() { | ||||||
|                         @Override |                         @Override | ||||||
|                         public void onSuccess(@io.reactivex.rxjava3.annotations.NonNull Monster monster) { |                         public void onSuccess(@io.reactivex.rxjava3.annotations.NonNull Monster monster) { | ||||||
|                             Logger.logDebug(String.format("Monster loaded: %s", monster.name)); |  | ||||||
|                             mViewModel.setHasLoaded(true); |                             mViewModel.setHasLoaded(true); | ||||||
|                             mViewModel.setHasError(false); |                             mViewModel.setHasError(false); | ||||||
|                             mViewModel.copyFromMonster(monster); |                             mViewModel.copyFromMonster(monster); | ||||||
|                             requireAppCompatActivity().getSupportActionBar().setTitle(getString(R.string.title_edit_monster, monster.name)); |                             setTitle(getString(R.string.title_edit_monster, monster.name)); | ||||||
|                             dispose(); |                             dispose(); | ||||||
|                         } |                         } | ||||||
|  |  | ||||||
| @@ -77,20 +81,57 @@ public class EditMonsterFragment extends MCFragment { | |||||||
|                     }); |                     }); | ||||||
|         } |         } | ||||||
|         mHolder.basicInfoButton.setOnClickListener(v -> { |         mHolder.basicInfoButton.setOnClickListener(v -> { | ||||||
|             // TODO: Navigate to the EditBasicInfo fragment |  | ||||||
|             Logger.logDebug("Basic Info clicked"); |  | ||||||
|             NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditBasicInfoFragment(); |             NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditBasicInfoFragment(); | ||||||
|             View view = getView(); |             View view = getView(); | ||||||
|             assert view != null; |             assert view != null; | ||||||
|             Navigation.findNavController(view).navigate(action); |             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 |                                             @Override | ||||||
|     public void onActivityCreated(@Nullable Bundle savedInstanceState) { |                                             public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) { | ||||||
|         super.onActivityCreated(savedInstanceState); |                                                 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 { |     private static class ViewHolder { | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ import com.majinnaibu.monstercards.models.Monster; | |||||||
|  |  | ||||||
| import java.util.UUID; | import java.util.UUID; | ||||||
|  |  | ||||||
|  | @SuppressWarnings({"ConstantConditions", "unused"}) | ||||||
| public class EditMonsterViewModel extends ViewModel { | public class EditMonsterViewModel extends ViewModel { | ||||||
|     private final MutableLiveData<String> mName; |     private final MutableLiveData<String> mName; | ||||||
|     private final MutableLiveData<UUID> mMonsterId; |     private final MutableLiveData<UUID> mMonsterId; | ||||||
| @@ -20,6 +21,7 @@ public class EditMonsterViewModel extends ViewModel { | |||||||
|     private final MutableLiveData<String> mSubtype; |     private final MutableLiveData<String> mSubtype; | ||||||
|     private final MutableLiveData<String> mAlignment; |     private final MutableLiveData<String> mAlignment; | ||||||
|     private final MutableLiveData<String> mCustomHitPoints; |     private final MutableLiveData<String> mCustomHitPoints; | ||||||
|  |     private final MutableLiveData<Boolean> mHasChanges; | ||||||
|  |  | ||||||
|     public EditMonsterViewModel() { |     public EditMonsterViewModel() { | ||||||
|  |  | ||||||
| @@ -33,6 +35,8 @@ public class EditMonsterViewModel extends ViewModel { | |||||||
|         mSubtype = new MutableLiveData<>(""); |         mSubtype = new MutableLiveData<>(""); | ||||||
|         mAlignment = new MutableLiveData<>(""); |         mAlignment = new MutableLiveData<>(""); | ||||||
|         mCustomHitPoints = 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) { |     public void copyFromMonster(Monster monster) { | ||||||
| @@ -44,6 +48,7 @@ public class EditMonsterViewModel extends ViewModel { | |||||||
|         mSubtype.setValue(monster.subtype); |         mSubtype.setValue(monster.subtype); | ||||||
|         mAlignment.setValue(monster.alignment); |         mAlignment.setValue(monster.alignment); | ||||||
|         mCustomHitPoints.setValue(monster.customHPDescription); |         mCustomHitPoints.setValue(monster.customHPDescription); | ||||||
|  |         mHasChanges.setValue(false); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public LiveData<String> getName() { |     public LiveData<String> getName() { | ||||||
| @@ -52,9 +57,9 @@ public class EditMonsterViewModel extends ViewModel { | |||||||
|  |  | ||||||
|     public void setName(@NonNull String name) { |     public void setName(@NonNull String name) { | ||||||
|         mName.setValue(name); |         mName.setValue(name); | ||||||
|  |         mHasChanges.setValue(true); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     public LiveData<UUID> getMonsterId() { |     public LiveData<UUID> getMonsterId() { | ||||||
|         return mMonsterId; |         return mMonsterId; | ||||||
|     } |     } | ||||||
| @@ -97,6 +102,7 @@ public class EditMonsterViewModel extends ViewModel { | |||||||
|  |  | ||||||
|     public void setSize(@NonNull String size) { |     public void setSize(@NonNull String size) { | ||||||
|         mSize.setValue(size); |         mSize.setValue(size); | ||||||
|  |         mHasChanges.setValue(true); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public LiveData<String> getType() { |     public LiveData<String> getType() { | ||||||
| @@ -105,6 +111,7 @@ public class EditMonsterViewModel extends ViewModel { | |||||||
|  |  | ||||||
|     public void setType(@NonNull String type) { |     public void setType(@NonNull String type) { | ||||||
|         mType.setValue(type); |         mType.setValue(type); | ||||||
|  |         mHasChanges.setValue(true); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public LiveData<String> getSubtype() { |     public LiveData<String> getSubtype() { | ||||||
| @@ -113,6 +120,7 @@ public class EditMonsterViewModel extends ViewModel { | |||||||
|  |  | ||||||
|     public void setSubtype(@NonNull String subType) { |     public void setSubtype(@NonNull String subType) { | ||||||
|         mSubtype.setValue(subType); |         mSubtype.setValue(subType); | ||||||
|  |         mHasChanges.setValue(true); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public LiveData<String> getAlignment() { |     public LiveData<String> getAlignment() { | ||||||
| @@ -121,6 +129,7 @@ public class EditMonsterViewModel extends ViewModel { | |||||||
|  |  | ||||||
|     public void setAlignment(@NonNull String alignment) { |     public void setAlignment(@NonNull String alignment) { | ||||||
|         mAlignment.setValue(alignment); |         mAlignment.setValue(alignment); | ||||||
|  |         mHasChanges.setValue(true); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public LiveData<String> getCustomHitPoints() { |     public LiveData<String> getCustomHitPoints() { | ||||||
| @@ -129,5 +138,32 @@ public class EditMonsterViewModel extends ViewModel { | |||||||
|  |  | ||||||
|     public void setCustomHitPoints(String customHitPoints) { |     public void setCustomHitPoints(String customHitPoints) { | ||||||
|         mCustomHitPoints.setValue(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