Adds confirmation when going up from the edit monster screen to save, cancel, or discard changes.

This commit is contained in:
2021-05-27 19:16:05 -07:00
parent ced5560dd0
commit f41b8d0065
5 changed files with 113 additions and 21 deletions

View File

@@ -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);
} }
} }

View File

@@ -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);
} }

View File

@@ -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;
}
} }

View File

@@ -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 {

View File

@@ -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;
} }
} }