Fragment Lifecycle - Why does this variable persist? - android-fragments

I'm declaring a variable in the scope of my fragment class (LocalDate?):
class HomeFragment : Fragment() {
private var _binding: FragmentMainBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
private val homeViewModel: HomeViewModel by viewModels()
private var selectedDate: LocalDate? = null
The code then assigns a selectedDate based on an onclick listener that I assign in onViewCreated()
The initial state is correctly that selectedDate == null. However, when I select a date and then change my device rotation, and/or navigate back and forth between fragments, the date remains selected. To me this is unexpected behavior - according to this documentation , "Variables" which are defined as "Local variables in the fragment" are destroyed on config change, process death, etc. Why is this variable staying populated?
I'm pretty new to Android dev so probably misunderstanding something.

Related

Escape from a Number TextField in a JavaFX dialog

I've a custom dialog with several UI elements. Some TextFields are for numeric input. This dialog does not close when the escape key is hit and the focus is on any of the numeric text fields. The dialog closes fine when focus is on other TextFields which do not have this custom TextFormatter.
Here's the simplified code:
package application;
import java.text.DecimalFormat;
import java.text.ParsePosition;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
try {
TextField name = new TextField();
HBox hb1 = new HBox();
hb1.getChildren().addAll(new Label("Name: "), name);
TextField id = new TextField();
id.setTextFormatter(getNumberFormatter()); // numbers only
HBox hb2 = new HBox();
hb2.getChildren().addAll(new Label("ID: "), id);
VBox vbox = new VBox();
vbox.getChildren().addAll(hb1, hb2);
Dialog<ButtonType> dialog = new Dialog<>();
dialog.setTitle("Number Escape");
dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
dialog.getDialogPane().setContent(vbox);
Platform.runLater(() -> name.requestFocus());
if (dialog.showAndWait().get() == ButtonType.OK) {
System.out.println("OK: " + name.getText() + id.getText());
} else {
System.out.println("Cancel");
}
} catch (Exception e) {
e.printStackTrace();
}
}
TextFormatter<Number> getNumberFormatter() {
// from https://stackoverflow.com/a/31043122
DecimalFormat format = new DecimalFormat("#");
TextFormatter<Number> tf = new TextFormatter<>(c -> {
if (c.getControlNewText().isEmpty()) {
return c;
}
ParsePosition parsePosition = new ParsePosition(0);
Object object = format.parse(c.getControlNewText(), parsePosition);
if (object == null || parsePosition.getIndex() < c.getControlNewText().length()) {
return null;
} else {
return c;
}
});
return tf;
}
public static void main(String[] args) {
launch(args);
}
}
How do I close the dialog when escape key is hit while focus is on id?
The Problem
Before offering a solution I think it's important, or at least interesting, to understand why having a TextFormatter seems to change the behavior of the Dialog. If this doesn't matter to you, feel free to jump to the end of the answer.
Cancel Buttons
According to the documentation of Button, a cancel button is:
the button that receives a keyboard VK_ESC press, if no other node in the scene consumes it.
The end of that sentence is the important part. The way cancel buttons, as well as default buttons, are implemented is by registering an accelerator with the Scene that the Button belongs to. These accelerators are only invoked if the appropriate KeyEvent bubbles up to the Scene. If the event is consumed before it reaches the Scene, the accelerator is not invoked.
Note: To understand more about event processing in JavaFX, especially terms such as "bubbles" and "consumed", I suggest reading this tutorial.
Dialogs
A Dialog has certain rules regarding how and when it can be closed. These rules are documented here, in the Dialog Closing Rules section. Suffice to say, basically everything depends on which ButtonTypes have been added to the DialogPane. In your example you use one of the predefined types: ButtonType.CANCEL. If you look at the documentation of that field, you'll see:
A pre-defined ButtonType that displays "Cancel" and has a ButtonBar.ButtonData of ButtonBar.ButtonData.CANCEL_CLOSE.
And if you look at the documentation of ButtonData.CANCEL_CLOSE, you'll see:
A tag for the "cancel" or "close" button.
Is cancel button: True
What this means, at least for the default implementation, is that the Button created for said ButtonType.CANCEL will be a cancel button. In other words, the Button will have its cancelButton property set to true. This is what allows one to close a Dialog by pressing the Esc key.
Note: It's the DialogPane#createButton(ButtonType) method that's responsible for creating the appropriate button (and can be overridden for customization). While the return type of that method is Node it is typical, as documented, to return an instance of Button.
The TextFormatter
Every control in (core) JavaFX has three components: the control class, the skin class, and the behavior class. The latter class is responsible for handling user input, such as mouse and key events. In this case, we care about TextInputControlBehavior and TextFieldBehavior; the former is the superclass of the latter.
Note: Unlike the skin classes, which became public API in JavaFX 9, the behavior classes are still private API as of JavaFX 12.0.2. Much of what's described below are implementation details.
The TextInputControlBehavior class registers an EventHandler that reacts to the Esc key being pressed, invoking the cancelEdit(KeyEvent) method of the same class. All the base implementation of this method does is forward the KeyEvent to the TextInputControl's parent, if it has one—resulting in two event dispatching cycles for some unknown (to me) reason. However, the TextFieldBehavior class overrides this method:
#Override
protected void cancelEdit(KeyEvent event) {
TextField textField = getNode();
if (textField.getTextFormatter() != null) {
textField.cancelEdit();
event.consume();
} else {
super.cancelEdit(event);
}
}
As you can see, the presence of a TextFormatter causes the KeyEvent to be unconditionally consumed. This stops the event from reaching the Scene, the cancel button is not fired, and thus the Dialog does not close when the Esc key is pressed while the TextField has the focus. When there is no TextFormatter the super implementation is invoked which, as stated before, simply forwards the event to the parent.
The reason for this behavior is hinted at by the call to TextInputControl#cancelEdit(). That method has a "sister method" in the form of TextInputControl#commitValue(). If you look at the documentation of those two methods, you'll see:
If the field is currently being edited, this call will set text to the last commited value.
And:
Commit the current text and convert it to a value.
Respectively. That doesn't explain much, unfortunately, but if you look at the implementation their purpose becomes clear. A TextFormatter has a value property which is not updated in real time while typing into the TextField. Instead, the value is only updated when it's committed (e.g. by pressing Enter). The reverse is also true; the current text can be reverted to the current value by cancelling the edit (e.g. by pressing Esc).
Note: The conversion between String and an object of arbitrary type is handled by the StringConverter associated with the TextFormatter.
When there's a TextFormatter, the act of cancelling the edit is deemed an event-consuming scenario. This makes sense, I suppose. However, even when there's nothing to cancel the event is still consumed—this doesn't make as much sense to me.
A Solution
One way to fix this is to dig into the internals, using reflection, as is shown in kleopatra's answer. Another option is to add an event filter to the TextField or some ancestor of the TextField that closes the Dialog when the Esc key is pressed.
textField.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode() == KeyCode.ESCAPE) {
event.consume();
dialog.close();
}
});
If you'd like to include the cancel-edit behavior (cancel without closing) then you should only close the Dialog if there's no edit to cancel. Take a look at kleopatra's answer to see how one might determine whether or not a cancel is needed. If there is something to cancel simply don't consume the event and don't close the Dialog. If there isn't anything to cancel then just do the same as the code above (i.e. consume and close).
Is using an event filter the "recommended way"? It's certainly a valid way. JavaFX is event-driven like most, if not all, mainstream UI toolkits. For JavaFX specifically that means reacting to Events or observing Observable[Value]s for invalidations/changes. A framework built "on top of" JavaFX may add its own mechanisms. Since the problem is an event being consumed when we don't want it to be, it is valid to add your own handlers to implement the desired behavior.
The question already has an excellent answer, nothing to add. Just wanted to demonstrate how to tweak the behavior's InputMap to inject/replace our own mappings (as a follow-up to my comment). Beware: it's dirty in reflectively accessing a skin's behavior (private final field) and using internal api (Behavior/InputMap didn't make it into public, yet).
As Slaw pointed out, it's the behavior that prevents the ESCAPE from bubbling up to the cancel button if the TextField has a TextFormatter installed. IMO, it's not misbehaving in that case, just overshooting: the cancel/default buttons should be triggered on ESCAPE/ENTER if and only if no other had used it to change the state of the any input nodes (my somewhat free interpretation of consumed - had done some research on general UX guidelines that I can't find right now, embarassingly ...)
Applied to a form containing both a textField with textFormatter and a cancel button (aka: isCancelButton is true)
if the textField has uncommitted text, a cancel should revert the edit to the most recent committed value and consume the event
if the textField is committed it should let it bubble up to trigger the cancel button
The implementation of cancelEdit in behavior doesn't distinguish between those two states, but always consumes it. The example below implements the expected (by me, at least) behavior. It has
a helper method to decide whether or not is dirty (aka: the textField has an uncommitted edit)
a event handling method that checks for dirtyness, calls cancel and consumes the event only it had been dirty
a configuration method that tweaks the textFields inputMap such that the mapping is replaced by our own.
Note that this is a PoC: doesn't belong into helpers but into a custom skin (at the very least, ideally should be done by the behavior). And it is missing similar support for the ENTER .. which is slightly more involved because it has to take actionHandlers into account (which behavior tries to but fails to achieve)
To test the example:
compile (note: you need to reflectively access a private field, use whatever you have at hand - we all do, don't we) and run
type something into the field
press escape: the field's text is reverted to its initial value
press escape again: the cancel button is triggered
The example code:
public class TextFieldCancelSO extends Application {
/**
* Returns a boolean to indicate whether the given field has uncommitted
* changes.
*
* #param <T> the type of the formatter's value
* #param field the field to analyse
* #return true if the field has a textFormatter with converter and
* uncommitted changes, false otherwise
*/
public static <T> boolean isDirty(TextField field) {
TextFormatter<T> textFormatter = (TextFormatter<T>) field.getTextFormatter();
if (textFormatter == null || textFormatter.getValueConverter() == null) return false;
String fieldText = field.getText();
StringConverter<T> valueConverter = textFormatter.getValueConverter();
String formatterText = valueConverter.toString(textFormatter.getValue());
// todo: handle empty string vs. null value
return !Objects.equals(fieldText, formatterText);
}
/**
* Install a custom keyMapping for ESCAPE in the inputMap of the given field.
* #param field the textField to configure
*/
protected void installCancel(TextField field) {
// Dirty: reflectively access the behavior
// needs --add-exports at compile- and runtime!
// note: FXUtils is a custom helper class not contained in core fx, use your own
// helper or write the field access code as needed.
TextFieldBehavior behavior = (TextFieldBehavior) FXUtils.invokeGetFieldValue(
TextFieldSkin.class, field.getSkin(), "behavior");
// Dirty: internal api/classes
InputMap inputMap = behavior.getInputMap();
KeyBinding binding = new KeyBinding(KeyCode.ESCAPE);
// custom mapping that delegates to helper method
KeyMapping keyMapping = new KeyMapping(binding, e -> {
cancelEdit(field, e);
});
// by default, mappings consume the event - configure not to
keyMapping.setAutoConsume(false);
// remove old
inputMap.getMappings().remove(keyMapping);
// add new
inputMap.getMappings().add(keyMapping);
}
/**
* Custom EventHandler that's mapped to ESCAPE.
*
* #param field the field to handle a cancel for
* #param ev the received keyEvent
*/
protected void cancelEdit(TextField field, KeyEvent ev) {
boolean dirty = isDirty(field);
field.cancelEdit();
if (dirty) {
ev.consume();
}
}
private Parent createContent() {
TextFormatter<String> fieldFormatter = new TextFormatter<>(
TextFormatter.IDENTITY_STRING_CONVERTER, "textField ...");
TextField field = new TextField();
field.setTextFormatter(fieldFormatter);
// listen to skin: behavior is available only after it's set
field.skinProperty().addListener((src, ov, nv) -> {
installCancel(field);
});
// just to see the state of the formatter
Label fieldValue = new Label();
fieldValue.textProperty().bind(fieldFormatter.valueProperty());
// add cancel button
Button cancel = new Button("I'm the cancel");
cancel.setCancelButton(true);
cancel.setOnAction(e -> LOG.info("triggered: " + cancel.getText()));
HBox fields = new HBox(100, field, fieldValue);
BorderPane content = new BorderPane(fields);
content.setBottom(cancel);
return content;
}
#Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent()));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
#SuppressWarnings("unused")
private static final Logger LOG = Logger
.getLogger(TextFieldCancelSO.class.getName());
}

Javafx (Node) getUserDate() or getProperties() returns a none null value and throws NullPointerException

Here is the code that set the user data.
FXMLLoader loader = new FXMLLoader(getClass().getResource(page.toString()));
Parent node = (Parent) loader.load();
loader.getController().setParent(handler);
controllers.put(page.name(), loader.getController());
controller = controllers.get(page.name());
node.setUserData(page.name());
node.getProperties().put("page", page.name());
the variable page is an Enum object, I'm using the name 'page.name()' as the user data
Here is the code that gets the user data:
while (iterator.hasNext()) {
Node node = iterator.next();
String name = node.getProperties().get("page").toString();
System.out.println(name);
....
}
now this what happens:
the System.out.println(name); prints out the user data correctly,
and then I get a NullPointException pointing to
String name = node.getProperties().get("page").toString();
the point is System.out.println(name); that prints the user date
prints it correct.
What's going on here?
Try breaking it down. Code is usually easier to read when you do not chain multiple method calls together.
Node node = iterator.next();
ObservableMap<Object, Object> properties = node.getProperties(); // Swap Object with whatever subclass it is supposed to return
Object nameProperty = properties.get("page"); // Same here
String name = nameProperty.toString();
Then see which line returns the null pointer exception and try searching for the reason behind it. It is likely that either getProperties() or get("page") is in fact returning a null value.

Flex: Help me understand data Binding on getters and setters

Help me understand data Binding
When I create a variable in a class:
[Bindable] private var _name:String;
and then generate getters and setters, I get:
private var _name:String;
[Bindable]
public function get name():String
{
return _name;
}
public function set name(value:String):void
{
_name = value;
}
Why does it generate the tag '[Bindable]' only on the get function?
To me, it would seem that it should be on the set function, as I would want to know when the
value changes, not when the value is just read.
What might help to understand what is going on here is the code that the MXML compiler will generate for you when you make something [Bindable]. The MXML compiler wraps your [Bindable] property in it's own getter/setter. It does this so that the wrapper setter method can dispatch a "propertyChange" event when a new value is set. This event notifies the parties binding to the property that the value has changed.
Getters/setters in Actionscript are considered to be properties of the object (they are not methods of the object). So it doesn't matter whether your annotate the getter or the setter as [Bindable], the generated code does the right thing.
It's worth noting that you can avoid the generated code and optimize the situation by dispatching your own event when your property changes. To do this, your [Bindable] metadata tag needs to include the event name that will be dispatched when the property changes:
private var _name:String;
[Bindable("nameChanged")]
public function get name():String
{
return _name;
}
public function set name(value:String)
{
if (_name == value)
return;
_name = value;
dispatchEvent(new Event("nameChanged"));
}
Because the bindable metadata contains an event string, no extra code is generated. Note, the compiler won't warn you if you forget to dispatch the event from the setter. In fact, you can dispatch your custom binding event from anywhere in your class (this can be useful with functions that are bindable).

Flex event will only fire once

In an AIR application, I have a private variable and a setter:
private var _saveResult
public function set saveResult( result:String ):void
{
_saveResult = result;
dispatchEvent( new resultUpdatedEvent( _saveResult ));
}
The first time that I set "saveResult" the event fires. But it will never fire again unless I restart the application.
If I change the setter to:
public function set saveResult( result:String ):void
{
_saveResult = result;
if ( result != null)
{
dispatchEvent( new resultUpdatedEvent( _saveResult ));
}
}
The problem goes away, I can set the variable many times and the event fires every time.
My question:
Am I doing something wrong here? If not, can anyone explain to me whats happening? If so, what SHOULD I be doing?
Thanks!
It looks like you're constructing your event incorrectly. The first parameter of an Event object should always be a string. So in this case you'd want to always use the same string so you could listen for the event. What does your resultUpdatedEvent class look like? You'll want it to look something like this:
package myEvents
{
import flash.events.Event;
public class PropertyChangeEvent extends Event
{
public static const PROPERTY_CHANGE:String = "propertyChange";
public var result:String = "";
// Public constructor.
public function PropertyChangeEvent (type:String,
result:String="") {
// Call the constructor of the superclass.
super(type);
// Set the new property.
this.result= result;
}
// Override the inherited clone() method.
override public function clone():Event {
return new PropertyChangeEvent(type, result);
}
}
}
That way, when you go to dispatch your event, you can construct the event as follows:
new PropertyChangeEvent(PropertyChangeEvent.PROPERTY_CHANGE, result);
That way, you're listening for the event "PropertyChangeEvent.PROPERTY_CHANGE", which never changes. The problem is now your event listener is probably listening for an event represented by the string saved in result, and obviously, this changes after the first time it's set, so there's no way to assign a listener to it.
For more information about how events work in AS3: http://livedocs.adobe.com/flex/3/html/help.html?content=events_02.html
Per the comments...
There was no event dispatcher problem.
I misdiagnosed the problem, the REAL problem was that if you have a [Bindable] property and you use a setter, and you set it for the current value, flex will ignore it. SO, you have several choices:
1) give the getter and setter different names. Seems like a "bad idea" but it does fix the problem.
2) remove [Bindable] from either the class (my problem) or the property. If the class does not implement IEventDispatcher, you will need to do so. You can simply "extends Sprite" to see it work, but that seems like a "bad idea" as a solution, so I implemented IEventDispatcher per the example at the end of this page: http://livedocs.adobe.com/flash/9.0/ActionScriptLangRefV3/flash/events/IEventDispatcher.html
3) I am sure that there is a way to get around this bug, but I don't actually NEED the class to be [Bindable] so I did not find a work around.

Flex Type Coercion, it's gone mental I think

I have a class essentially:
public class WindowEvent extends Event
{
public static const WARNEVENT:String = "warnEvent";
public static const TASKREQEVENT:String = "taskRequestEvent";
public static const TASKANNOUNCE:String = "taskAnnounce";
public static const WINDOWCHANGE:String = "windowChange";
public static const ILLEGALPOSITION:String = "illegalPosition";
// insert brevity
}
The first four events were working fine, but I just I added ILLEGALPOSITION and tried this:
// inside Window.as
private function checkDimensions():void {
if(!Window._legalBoundaryEnable)
return;
... var pass:Boolean = Window.legalBoundary.containsRect(
455 this.getBounds(stage));
456 if(!pass) {
457 this.dispatchEvent(new WindowEvent(WindowEvent.ILLEGALPOSITION,
... "Illegal Position etc."));
}
}
So Flex spewed this stack at me when I hit the dispatch method:
TypeError: Error #1034: Type Coercion failed: cannot convert ¬
flex.utils.ui::WindowEvent#511dce41 to flash.events.MouseEvent.
at flash.events::EventDispatcher/dispatchEventFunction()
at flash.events::EventDispatcher/dispatchEvent()
at mx.core::UIComponent/dispatchEvent() ¬
[C:\autobuild\~\UIComponent.as:9298]
at flex.utils.ui::Window/checkDimensions()[C:\Users\~\Window.as:457]
at flex.utils.ui::Window/stageResized()[C:\Users\~\Window.as:220]
As you can see from the trace, Window.as:457 is the last user code line. So WTF is flash.events.EventDispatcher.dispatchEventFunction trying to do with a MouseEvent?
That error normally occurs because a listener you have set up has incorrect event param type. I'm pretty certain that this must be the case here.
Check all the listeners you have set up for that event and make sure the function is
someFunction(event : WindowEvent) : void
Try using another value for ILLEGALPOSITION, "illegalPosition" might be used by Flex itself (or another part of your code) and related to a mouse event. So when that event handler fires, it tries to convert your event to a MouseEvent because it thinks it should be one.

Resources