ViewModel not cleared on DialogFragment onDestroy - viewmodel

Lifecycle v 2.3.1
Short Story: ViewModel is not cleared when DialogFragment onDestroy is called. That's it.
In my MainActivity, I instantiate and show a DialogFragment on button press:
private fun showConfirmDialog(message: String) {
MyConfirmDialog.getInstance(message)
.show(supportFragmentManager, MyConfirmDialog.TAG)
}
my dialog fragment:
class MyConfirmDialog: DialogFragment() {
companion object {
val TAG:String = MyConfirmDialog::class.java.simpleName
const val ARGS_MESSAGE = "message"
fun getInstance(message:String): MyConfirmDialog {
val dialog = MyConfirmDialog()
dialog.message = message
val b = Bundle()
b.putString(ARGS_MESSAGE, message)
dialog.arguments = b
return dialog
}
}
private lateinit var message:String
private val vm: MyConfirmDialogViewModel by activityViewModels()
private lateinit var binding: MyConfirmDialogBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
message = requireArguments().getString(ARGS_MESSAGE, "")
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = MyConfirmDialogBinding.inflate(inflater, container, false)
with(binding) {
btnDone.setOnClickListener {
vm.doTheThing(message)
}
btnCancel.setOnClickListener {
dismiss()
}
}
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
/**
* Observer immediately fires on second fresh instance of this dialog.
* regardless of whether observed with this or viewLifecycleOwner. Thus before
* it can even be shown, this second unique dialog is immediately dismissed.
* The observer shouldn't receive an update until after btnDone is clicked.
*/
vm.doTheThing.observe(viewLifecycleOwner, {
Log.e(TAG, "observed doTheThing")
dismiss()
})
}
override fun onDestroy() {
super.onDestroy()
Log.e(TAG, "onDestroy")
//VM NOT CLEARED
}
}
I call showConfirmDialog a second time from my MainActivity, a second dialog is instantiated, but the VM is still happily sitting resident and NOT cleared, despite the first dialog confirming onDestroy having been called.
Thus is appears I can never use a DialogFragment with a ViewModel, if said dialog will be instantiated more than once.
This feels like a bug in Android lifecycle 2.3.1

I think I found my own answer. I was, in fact, instantiating the VM like this:
private val vm: MyConfirmDialogViewModel by activityViewModels()
Had to change to:
private val vm: MyConfirmDialogViewModel by viewModels()
Apparently, the choice of delegate is absolutely critical. Using the first delegate (by activityViewModels()) was automatically scoping the VM lifetime to the lifetime of the Activity instead of the Dialog. Thus even when the dialog was destroyed the VM was not cleared.
I thought scoping the VM's lifecycle was handled when registering the observer (e.g. observe(this, {}) vs. observe(viewLifecycleOwner, {})), but apparently the choice of these two targets has only to do with whether you intend to override onCreateDialog. To me, it's a confusing mess.
It would certainly be a better world if I didn't have to know absolutely everything about lifecycles and all their intricacies just to use the SDK properly.

Related

How to handle back button when at the starting destination of the navigation component

I've started working with the new navigation component and I'm really digging it! I do have one issue though - How am I supposed to handle the back button when I'm at the starting destination of the graph?
This is the code I'm using now:
findNavController(this, R.id.my_nav_host_fragment)
.navigateUp()
When I'm anywhere on my graph, it's working great, it send me back, but when I'm at the start of it - the app crashes since the backstack is empty.
This all makes sense to me, I'm just not sure how to handle it.
While I can check if the current fragment's ID is the same as the one that I know to be the root of the graph, I'm looking for a more elegant solution like some bool flag of wether or not the current location in the graph is the starting location or not.
Ideas?
I had a similar scenario where I wanted to finish the activity when I was at the start destination and do a regular 'navigateUp' when I was further down the navigation graph. I solved this through a simple extension function:
fun NavController.navigateUpOrFinish(activity: AppCompatActivity): Boolean {
return if (navigateUp()) {
true
} else {
activity.finish()
true
}
}
And then call it like:
override fun onSupportNavigateUp() =
findNavController(R.id.nav_fragment).navigateUpOrFinish(this)
However I was unable to use NavigationUI as this would hide the back arrow whenever I was at the start destination. So instead of:
NavigationUI.setupActionBarWithNavController(this, controller)
I manually controlled the home icon:
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_navigation_back)
Override onBackPressed in your activity and check if the current destination is the start destination or not.
Practically it looks like this:
#Override
public void onBackPressed() {
if (Navigation.findNavController(this,R.id.nav_host_fragment)
.getCurrentDestination().getId() == R.id.your_start_destination) {
// handle back button the way you want here
return;
}
super.onBackPressed();
}
You shouldn't override "onBackPressed", you should override "onSupportNavigateUp" and put there
findNavController(this, R.id.my_nav_host_fragment)
.navigateUp()
From the official documentation:
You will also overwrite AppCompatActivity.onSupportNavigateUp() and call NavController.navigateUp
https://developer.android.com/topic/libraries/architecture/navigation/navigation-implementing
In Jetpack Navigation Component, if you want to perform some operation when fragment is poped then you need to override following functions.
Add OnBackPressedCallback in fragment to run your special operation when back present in system navigation bar at bottom is pressed .
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
//perform your operation and call navigateUp
findNavController().navigateUp()
}
}
requireActivity().onBackPressedDispatcher.addCallback(onBackPressedCallback)
}
Add onOptionsItemMenu in fragment to handle back arrow press present at top left corner within the app.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setHasOptionsMenu(true)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
//perform your operation and call navigateUp
findNavController().navigateUp()
return true
}
return super.onOptionsItemSelected(item)
}
If there is no special code to be run when back is pressed on host fragment then use onSupportNavigateUp in Activity.
override fun onSupportNavigateUp(): Boolean {
if (navController.navigateUp() == false){
//navigateUp() returns false if there are no more fragments to pop
onBackPressed()
}
return navController.navigateUp()
}
Note that onSupportNavigateUp() is not called if the fragment contains onOptionsItemSelected()
As my back button works correctly, and using NavController.navigateUp() crashed on start destination back button. I have changed this code to something like this. Other possibility will be to just check if currentDestination == startDestination.id but I want to close Activity and go back to other Activity.
override fun onSupportNavigateUp() : Boolean {
//return findNavController(R.id.wizard_nav_host_fragment).navigateUp()
onBackPressed()
return true
}
/** in your activity **/
private boolean doubleBackToExitPressedOnce = false;
#RequiresApi(api = Build.VERSION_CODES.M)
#Override
public void onBackPressed() {
int start = Navigation.findNavController(this, R.id.nav_host_fragment).getCurrentDestination().getId();
if (start == R.id.nav_home) {
if (doubleBackToExitPressedOnce) {
super.onBackPressed();
return;
}
this.doubleBackToExitPressedOnce = true;
Toast.makeText(MainActivity.this, "Press back again to exits", Toast.LENGTH_SHORT).show();
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
doubleBackToExitPressedOnce = false;
}
}, 2000);
} else {
super.onBackPressed();
}
}
If you mean the start of your "root" navigation graph (just incase you have nested navigation graphs) then you shouldn't be showing an up button at all, at least according to the navigation principles.
Just call this in your back button Onclick
requireActivity().finish()

Listeners for NavigationUI, set on NavigationViewOptions not working

I'm using mapbox-android-navigation and navigation-ui, version 0.11.1, to display turn-by-turn for routes I'm creating. Everything is working great, except I'm not getting callbacks from the navigation UI.
Specifically, I've set listeners on my NavigationViewOptions object as directed. But the listeners never get called back. Moreover, it appears like the listeners are being ignored if you follow the code into NavigationLauncher#startNavigation
Here is my code to launch turn-by-turn:
private fun launchTurnByTurn() {
val navigationListener = object: NavigationListener {
override fun onNavigationFinished() = Timber.i("onNavigationFinished()")
override fun onNavigationRunning() = Timber.i("onNavigationRunning()")
override fun onCancelNavigation() = Timber.i("onCancelNavigation()")
}
val routeListener = object: RouteListener {
override fun allowRerouteFrom(offRoutePoint: Point?): Boolean {
Timber.i("allowRerouteFrom()")
return true
}
override fun onFailedReroute(errorMessage: String?) = Timber.i("onFailedReroute()")
override fun onRerouteAlong(directionsRoute: DirectionsRoute?) = Timber.i("onRerouteAlong()")
override fun onOffRoute(offRoutePoint: Point?) = Timber.i("TC onOffRoute")
}
val simulateRoute = true
// Create a NavigationViewOptions object to package everything together
val options = NavigationViewOptions.builder()
.directionsRoute(routesMap?.currentRoute)
.shouldSimulateRoute(simulateRoute)
.navigationListener(navigationListener)
.routeListener(routeListener)
.build()
NavigationLauncher.startNavigation(this, options)
}
My question is, should these listeners be called, or has this callback feature just not been implemented yet?
Looks like you're trying to pass NavigationViewOptions into NavigationLauncher.startNavigation when it accepts NavigationLauncherOptions.
See the clarification here: https://github.com/mapbox/mapbox-navigation-android/issues/781#issuecomment-374328736

Update TextArea while executing processes

i found out that my GUI starts to freeze after 3-4 seconds when I click the "start" button like "no response". When I keep on click the App, it's forced to shut down.
Now I want to prevent this but I got no clue how. Just as far as I know JavaFX runs in a Single Thread, therefore, to update my TextArea while the Methods are executing, I need to run these Methods in another Thread.
I hope someone can help me.
How does my project look like?
I got a FXML, a Controller, a Handler, a Transformer , also a Writer and a Reader class (which are used in the Handler class).
When I click the button, which is bind to a method in the Controller, an instance of Handler is created and this one calls the Reader to read in a text file, transformed to a List of Strings (line by line).
In addition, the lines are getting manipulated. After this, the Writer is used to creat a new file and write the new manipulated lines to this file.
It is also allowed to the user to refer to more than just one file.
What I want is that the textarea shows whenever the reader starts to read a file like "The file ... is being read".
Then append "The file ... is being manipulated" when Transformer comes to action and then
"The file ... is being written" when the new lines are written to the new file.
Here is some code..
Controller:
public class FXMLDocumentController implements Initializable {
#FXML
private TextArea console;
#FXML
private void handleButtonAction(ActionEvent event) {
System.out.println("You clicked me!");
Handler hand = new Handler();
hand.handle(files);
}
#Override
public void initialize(URL url, ResourceBundle rb) {
// TODO
}
}
Handler:
public class Handler {
public void handle(List<String> files) {
for (String s : files) {
List<String> ls = Reader.readFile(s);
Writer.writeFile(Transformer.transform(ls));
}
}
How should I change my code to update the TextArea whenever a file is read, manipulated or written?
Info: I know the class Handler won't compile as I erased the initialization of the List "files" which contains Strings of file paths.
If I left out relevant information, feel free to ask.
I thank you in advance!
You should execute the handle() method in a background thread:
#FXML
private void handleButtonAction(ActionEvent event) {
System.out.println("You clicked me!");
Thread thread = new Thread(() -> {
Handler hand = new Handler();
hand.handle(files);
});
// this line means the background thread will not prevent application exit:
thread.setDaemon(true);
thread.start();
}
If you want to update the text area with the current status, you need to schedule that back on the FX Application Thread using Platform.runLater(). Probably the cleanest way to do this is not to have Platform.runLater() in the Handler class, but define a field in Handler for "consuming" status messages:
public class Handler {
private final Consumer<String> statusMessageProcessor ;
public Handler(Consumer<String> statusMessageProcessor) {
this.statusMessageProcessor = statusMessageProcessor ;
}
// default processor does nothing:
public Handler() {
this(s -> {});
}
public void handle(List<String> files) {
for (String s : files) {
// similarly for other status updates:
statusMessageProcessor.accept("The file "+s+" is being read");
List<String> ls = Reader.readFile(s);
Writer.writeFile(Transformer.transform(ls));
}
}
}
and then
Handler hand = new Handler() ;
can become
Handler hand = new Handler(message ->
Platform.runLater(() -> console.appendText(message + "\n")));
in the button handler method.

How to dismiss a Alert Dialog in Mono for android correctly?

In my application i have a Custom AlertView, which works quite good so far. I can open it the first time, do, what i want to do, and then close it. If i want to open it again, i'll get
java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first
so, here some code:
public Class ReadingTab
{
...
private AlertDialog AD;
...
protected override void OnCreate(Bundle bundle)
{
btnAdd.Click += delegate
{
if (IsNewTask)
{
...
AlertDialog.Builer adb = new AlertDialog.Builer(this);
...
View view = LayoutInflater.Inflate(Resource.Layout.AlertDView15ET15TVvert, null);
adb.setView(view)
}
AD = adb.show();
}
}
}
that would be the rough look of my code.
Inside of btnAdd are two more buttons, and within one of them (btnSafe) i do AD.Dismiss() to close the Alert dialoge, adb.dispose() hasn't done anything.
the first time works fine, but when i call it the secon time, the debugger holds at AD = adb.show(); with the Exception mentioned above.
So what do i have to do, to remove the Dialoge from the parent? i can't find removeView() anywhere.
If you are setting up an AlertView once and then using it in multiple places (especially if you are using the same AlertView across different Activities) then you should consider creating a static AlertDialog class that you can then call from all over the place, passing in the current context as a parameter each time you want to show it. Then when a button is clicked you can simply dismiss the dialog and set the instance to null. Here is a basic example:
internal static class CustomAlertDialog
{
private static AlertDialog _instance;
private const string CANCEL = #"Cancel";
private const string OK = #"OK";
private static EventHandler _handler;
// Static method that creates your dialog instance with the given title, message, and context
public static void Show(string title,
string message,
Context context)
{
if (_instance != null)
{
throw new Exception(#"Cannot have more than one confirmation dialog at once.");
}
var builder = new AlertDialog.Builder(context);
builder.SetTitle(title);
builder.SetMessage(message);
// Set buttons and handle clicks
builder.SetPositiveButton(OK, delegate { /* some action here */ });
builder.SetNegativeButton(CANCEL, delegate { /* some action here */});
// Create a dialog from the builder and show it
_instance = builder.Create();
_instance.SetCancelable(false);
_instance.Show();
}
}
And from your Activity you would call your CustomAlertDialog like this:
CustomAlertDialog.Show(#"This is my title", #"This is my message", this);

javafx - how to disable events fired not from the user

i have a little problem with javafx. i added a change listener like this:
private final ChangeListener<String> pageItemSelected = new ChangeListener<String>()
{
#Override
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue){
pageGotSelected(newValue);
}
};
now to the problem: if i change an page item like this:
guiPageList.setValue(model.getCurrentTargetPage());
the event gets also(as it get by selecting something with the mouse or key) fired. is there a way to disable the event firing or another way?
i need the event only, if the element got selected by the user and not if i change it with the setValue() function...
perhaps consuming the event, but i donĀ“t know what kind of event this would be.
thanks in advance!!!
You can temporarily remove the listener and add it again:
guiPageList.getSelectionModel().selectedItemProperty().removeListener(pageItemSelected);
guiPageList.setValue(model.getCurrentTargetPage());
guiPageList.getSelectionModel().selectedItemProperty().addListener(pageItemSelected);
Alternatively you could decorate the listener with another listener implementation, the code would be something like:
class InvalidationListenerEventBlocker implements InvalidationListener {
InvalidationListener decoratedListener;
boolean block;
public void invalidated(Observable observable) {
if(!block) {
decoratedListener.invalidated(observable);
}
}
}
Add a setter for the block boolean and send the listener in through the constructor. Set block to true to stop events.
This is very old question, but I came to some solution I personally use, that's reusable and does not require storing a reference to the listener (but it needs a reference to the exposing/muffling property thou).
So first the concept: we're going to create lambda (InvalidationListener), that is going to be called only if above mentioned exposing/muffling property is set to true/false. For that we are going to define another functional interface that provides described behavior:
#FunctionalInterface
private interface ManageableInvalidationListener
extends InvalidationListener {
public static InvalidationListener exposing(
BooleanProperty expose,
ManageableInvalidationListener listener) {
return ob -> {
if (expose.get()) {
listener.invalidate(ob);
}
};
}
public static InvalidationListener muffling(
BooleanProperty muffle,
ManageableInvalidationListener listener) {
return ob -> {
if (!muffle.get()) {
listener.invalidated(ob);
}
}
}
public abstract void invalidated(Observable ob);
}
This interface defines two static methods we're going to use in our code. We pass a steering property as first argument (it will tell if listener should be called) and actual implementation to be performed, when it will be called. Please note, that there is no need to extend InvalidationListener, but I'd like to keep ManageableInvalidationListener in sync with InvalidationListener.
So we would call exposing if we need to create a (manageabale) listener that would notify the (invalidation) listener if expose property has value of true. In other case we would create the listener with muffling, if true of the steering property would mean, well, to muffle the notification.
How to use it?
//- Let's make life easier and import expose method statically
import static ManageableInvalidationListener.exposing;
// ...
//- This is the steering property.
BooleanProperty notify = new SimpleBooleanProperty(true);
//- This is our main property with the listener.
ObjectProperty<Foobar> foobar = new SimpleObjectProperty<>();
//- Let's say we are going to notify the listener, if the
// notify property is set to true.
foobar.addListener(exposing(notify, ob -> {
//- Here comes the InvalidListener code.
}));
And then somewhere in the code:
//- Listener will be notified as usual.
foobar.set(new Foobar());
//- Now temporarily disable notifications.
notify.set(false);
//- The listener will not get the notification this time.
foobar.set(new Foobar());
//- Re-enable notifications.
notify.set(true);
Hope this somehow helps. You're free to use the code in this post as pleases you.

Resources