FXMLLoader explained - model-view-controller

I am using javafx combined with FXML.
I want to apply the MVC pattern. For that I want my Model.java class to be the model, which launches the View.fxml and the controller of that view would be viewController.java.
I need to bring Model.java and Controller.java to communicate at some point. So let's say ViewController.java looks this way:
public class ViewController implements Initializable {
private String parameter = "hello";
#FXML
private Label label;
#FXML
private Accordion acccord;
public String getParemeter() {
return this.parameter;
}
#Override
public void initialize(URL url, ResourceBundle rb) {
// TODO
}
}
ViewController has a private string and its own methods.
And Model.java :
public class Model extends Application {
#Override
public void start(Stage stage) throws Exception {
FXMLLoader loader = new FXMLLoader(getClass().getResource("View.fxml") );
Parent root = loader.load(); // Here the View is loaded and the Contoller is created along.
loader.getController(); // ?
//Parent root = FXMLLoader.load(getClass().getResource("FXMLDocument.fxml"));
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
How can I access the ViewContoller parameters / methods (such as getPamareter() ) ?
I tried to get the controller with loader.getController() but it returns a generic type, what should I do with it, provided it has something to do with it? I went to the oracle documentation but I am not quite sure to understan, does getController() return an instance of my ViewController.java ?
HOw can I access the Model from the ViewController?
For instance a buton is triggered, the vieController would update a value in Model.java.

Related

Spring data jpa with java FX

I'm using Spring JPA with OpenJFX. It's this project JavaFX-weaver, simply adding spring-boot-start-data-jpa inside pom.
However my starting time of Spring JPA is 15-20s and the UI will not show until spring is initalized. When users will start the application it takes a lot of time, every time!
As a workaround i tried to create a simply java fx application without Spring (using this demo here) and then starting there in the main method the main method from spring over a button (see example bellow). That will start spring, but dependencies and properties are not laoded.
Do you know a good way to practice that case ? Every help is welcome.
Thank you
AppBootstrap (Java + OpenJFX)
public class AppBootstrap extends Application {
#Override
public void start(Stage primaryStage) {
Button btn = new Button();
// start spring jpa main method
btn.setOnAction(event -> App.main(new String[]{""}));
StackPane root = new StackPane();
root.getChildren().add(btn);
primaryStage.setScene(new Scene(root, 300, 250));
primaryStage.show();
}
public static void main(String[] args) {
launch();
}
}
App (Spring JPA + javafx-weaver)
#SpringBootApplication
public class App {
public static void main(String[] args) {
Application.launch(SpringbootJavaFxApplication.class, args);
}
}
Startup of an JPA powered Application increases load time for ApplicationContext. While you can make things faster by not checking or creating a database scheme, e.g. by setting hibernate.hbm2ddl.auto=none, this is not the best option.
It is by design that the primary stage is shown after the ApplicationContext is loaded, since it should be able to be dependency injected.
The best practice I recommend is using a splash screen while loading the ApplicationContext. It's a bit tricky, since you have separate Threads, but roughly it looks like this:
Create a splash window
public class Splash {
private static final int SPLASH_WIDTH = 200;
private static final int SPLASH_HEIGHT = 200;
private final Parent parent;
private final Stage stage;
public Splash() {
this.stage = new Stage();
stage.setWidth(SPLASH_WIDTH);
stage.setHeight(SPLASH_HEIGHT);
Label progressText = new Label("Application loading ...");
VBox splashLayout = new VBox();
splashLayout.setAlignment(Pos.CENTER);
splashLayout.getChildren().addAll(progressText);
progressText.setAlignment(Pos.CENTER);
splashLayout.setStyle(
"-fx-padding: 5; " +
"-fx-background-color: white; " +
"-fx-border-width:5; " +
"-fx-border-color: white;"
);
splashLayout.setEffect(new DropShadow());
this.parent = splashLayout;
}
public void show() {
Scene splashScene = new Scene(parent);
stage.initStyle(StageStyle.UNDECORATED);
final Rectangle2D bounds = Screen.getPrimary().getBounds();
stage.setScene(splashScene);
stage.setX(bounds.getMinX() + bounds.getWidth() / 2 - SPLASH_WIDTH / 2.0);
stage.setY(bounds.getMinY() + bounds.getHeight() / 2 - SPLASH_HEIGHT / 2.0);
stage.show();
}
public void hide() {
stage.toFront();
FadeTransition fadeSplash = new FadeTransition(Duration.seconds(0.3), parent);
fadeSplash.setFromValue(1.0);
fadeSplash.setToValue(0.0);
fadeSplash.setOnFinished(actionEvent -> stage.hide());
fadeSplash.play();
}
}
Initialize Application
public class SpringbootJavaFxApplication extends Application {
private ConfigurableApplicationContext context;
class ApplicationContextLoader extends Task<Void> {
private final Stage primaryStage;
ApplicationContextLoader(Stage primaryStage) {
this.primaryStage = primaryStage;
}
#Override
protected Void call() {
ApplicationContextInitializer<GenericApplicationContext> initializer =
context -> {
context.registerBean(Application.class, () -> SpringbootJavaFxApplication.this);
context.registerBean(Stage.class, () -> primaryStage);
context.registerBean(Parameters.class,
SpringbootJavaFxApplication.this::getParameters); // for demonstration, not really needed
};
SpringbootJavaFxApplication.this.context = new SpringApplicationBuilder()
.sources(JavaFxSpringbootDemo.class)
.initializers(initializer)
.run(getParameters().getRaw().toArray(new String[0]));
return null;
}
}
#Override
public void start(Stage primaryStage) {
var splash = new Splash();
splash.show();
final ApplicationContextLoader applicationContextLoader = new ApplicationContextLoader(primaryStage);
applicationContextLoader.stateProperty().addListener((observableValue, oldState, newState) -> {
if (newState == Worker.State.SUCCEEDED) {
context.publishEvent(new StageReadyEvent(primaryStage));
splash.hide();
}
});
new Thread(applicationContextLoader).start();
}
#Override
public void stop() {
this.context.close();
Platform.exit();
}
}

Toast is shown every time when device is rotate

In my Android app I use AAC.
Here my activity:
public class AddTraderActivity extends AppCompatActivity {
AddTraderViewModel addTraderViewModel;
private static final String TAG = AddTraderActivity.class.getName();
#Override
protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AddTraderActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.add_trader_activity);
binding.setHandler(this);
init();
}
private void init() {
ViewModelProvider viewViewModelProvider = ViewModelProviders.of(this);
addTraderViewModel = viewViewModelProvider.get(AddTraderViewModel.class);
Observer<String> () {
#Override
public void onChanged (String message){
Debug.d(TAG, "onChanged: message = " + message);
Toast.makeText(AddTraderActivity.this, message, Toast.LENGTH_LONG).show();
}
});
}
public void onClickStart() {
EditText baseEditText = findViewById(R.id.baseEditText);
EditText quoteEditText = findViewById(R.id.quoteEditText);
addTraderViewModel.doClickStart(baseEditText.getText().toString(), quoteEditText.getText().toString());
}
}
Here my ViewModel:
public class AddTraderViewModel extends AndroidViewModel {
private MutableLiveData<String> messageLiveData = new MutableLiveData<>();
private static final String TAG = AddTraderViewModel.class.getName();
public AddTraderViewModel(#NonNull Application application) {
super(application);
}
public void doClickStart(String base, String quote) {
Debug.d(TAG, "doClickStart: ");
if (base.trim().isEmpty() || quote.trim().isEmpty()) {
String message = getApplication().getApplicationContext().getString(R.string.please_input_all_fields);
messageLiveData.setValue(message);
return;
}
}
public LiveData<String> getMessageLiveData() {
return messageLiveData;
}
}
So when I click on button on Activity call method onClickStart()
If any fields is empty the show toast. In the activity call method:
onChanged (String message)
Nice. It's work fine.
But the problem is, when I rotate the device in the activity method onChanged(String message) is called AGAIN and as result show toast. This happened on every rotation.
Why?
This is the expected behaviour. If you want to avoid this you must set message = "" and keep an empty check before showing the toast.
A better way to use it is something like Event Wrapper or SingleLiveEvent
Highly recommend you to read this article. This explains why you are facing this and what are your options in detail.

Adding Custom Dialog to Javafx with Spring DI

I have a gui built with javafx the controllers are loaded from fxml and created as Beans with spring so I can access my model. But that is predefined in fxml and loaded at start. Now I would like to load components, defined in fxml at runtime, but I could not yet find a working example, and no matter how I try it doesn't work.
So my question:
How can I create a custom Dialog (or any custom component) in runtime , that is loaded from .fxml and is aware of (Spring application) context?
Edit
So it loads but some fields are not initialized.
This is my custom DialogPane,
#Controller
#Scope("prototype")
public class NewProgramDialogPane extends DialogPane implements Initializable {
public static final ButtonType buttonTypeOk = new ButtonType("Create", ButtonBar.ButtonData.OK_DONE);
public static final ButtonType buttonTypeCancel = new ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
public TextField nameField;
public TextField data1Field;
public TextField data2Field;
public RegexValidator requiredField1;
public RequiredField requiredField2;
public RequiredField requiredField3;
public ErrorLabel duplicateProjectErrorLabel;
private SimpleBooleanProperty match = new SimpleBooleanProperty(false);
#Autowired
MainService mainService;
public NewProgramDialogPane() {
URL url = getClass().getClassLoader().getResource("com/akos/fxml/NewProgramDialog.fxml");
FXMLLoader loader = new FXMLLoader();
loader.setLocation(url);
loader.setRoot(this);
try {
loader.load();
} catch (IOException e) {
e.printStackTrace();
}
}
#Override
public void initialize(URL location, ResourceBundle resources) {
this.lookupButton(buttonTypeOk).addEventHandler(ActionEvent.ACTION, event -> {
if (!validate()) {
event.consume();
}
});
duplicateProjectErrorLabel.visibleProperty().bind(match);
}
public boolean validate() {
requiredField1.eval();
requiredField2.eval();
requiredField3.eval();
match.set(mainService.getPrograms().stream().anyMatch(
program -> program != null && program.getName().equals(nameField.getText())));
return !match.get() &&
!requiredField1.getHasErrors() &&
!requiredField2.getHasErrors() &&
!requiredField3.getHasErrors();
}
}
And when I try to read the nameField, it is null.
public class NewProgramDialog extends Dialog<Program> {
public NewProgramDialog() {
this.setDialogPane(new NewProgramDialogPane());
this.setTitle("New program");
this.initModality(Modality.APPLICATION_MODAL);
this.initStyle(StageStyle.DECORATED);
this.setResultConverter(param -> {
if (param == NewProgramDialogPane.buttonTypeOk) {
int x = 0;
return new Program(((NewProgramDialogPane) getDialogPane()).nameField.getText());
}
return null;
});
}
}
Define your custom dialog using the custom component FXML pattern; then just expose the custom component as a (prototype-scoped) spring bean.

ViewPagerIndicator not snapping or displaying pager contents all of a sudden

This is a wierd problem.
This will be a terrible question because I have little to no information.
About two days ago I had the ViewPagerAdapter working just fine. I could swipe and it would switch between views as defined by the adapter.
However, all of a sudden (not by itself, I'm sure I did something) the TitlePagerIndicator doesn't snap to the headings and doesn't display any content. By not snapping I mean that if I drag to the left, the title will sit at 3/4 of the screen instead of snapping to the side and displaying the next page (screenshot below).
I have debugged and instantiate item is called and a proper view is returned.
However, when I open the app I'm getting a lot of warnings like these:
VFY: unable to resolve virtual method 3015: Landroid/widget/LinearLayout;.getAlpha ()F
VFY: unable to resolve direct method 3011: Landroid/widget/LinearLayout;. (Landroid/content/Context;Landroid/util/AttributeSet;I)V
VFY: unable to resolve virtual method 2965: Landroid/widget/FrameLayout;.setAlpha (F)V
I'm assuming this is a problem with my imports, but everything compiles just fine, I have the ViewPagerIndicator as a library project, as well as Sherlock.
Here's my adapter code:
public class ViewPagerAdapter extends PagerAdapter implements TitleProvider {
private static String[] titles = new String[] {
"My Klinks",
"Received Klinks"
};
private final Context context;
public ViewPagerAdapter(Context context) {
this.context = context;
}
public String getTitle(int position) {
return titles[position];
}
#Override
public int getCount() {
return titles.length;
}
#Override
public Object instantiateItem(View pager, int position) {
TextView t = new TextView(context);
t.setText("WheeeE");
return t;
}
#Override
public void destroyItem(View pager, int position, Object view) {
((ViewPager) pager).removeView((TextView) view);
}
#Override
public boolean isViewFromObject(View view, Object object) {
return view.equals(object);
}
#Override
public void finishUpdate(View view) {
}
#Override
public void restoreState(Parcelable p, ClassLoader c) {
}
#Override
public Parcelable saveState() {
return null;
}
#Override
public void startUpdate(View view) {
}
}
And here is my activity code:
public void onCreate(Bundle savedInstanceState) {
super.onCreate( savedInstanceState );
setContentView( R.layout.main );
// set up the slidey tabs
ViewPagerAdapter adapter = new ViewPagerAdapter( this );
ViewPager pager = (ViewPager)findViewById( R.id.viewpager );
TitlePageIndicator indicator = (TitlePageIndicator)findViewById( R.id.indicator );
pager.setAdapter( adapter );
indicator.setViewPager( pager );
// set up the action bar
final ActionBar ab = getSupportActionBar();
ab.setBackgroundDrawable(getResources().getDrawable(R.drawable.ad_action_bar_gradient_bak));
}
If someone else gets the same problem:
In instantiateView: don't forget to attach your new View to the ViewPager:
#Override
public Object instantiateItem(View pager, int position) {
TextView t = new TextView(context);
t.setText("WheeeE");
((ViewPager)pager).addView(t);
return t;
}
The current version of instantiateItem gets a ViewGroup instead of a View, the solution should be the same.
Well after a couple days of banging my head against a wall I've come to the conclusion that my ViewPagerAdapter was the problem.
I simply created a dynamic fragment and created a subclass of FragmentPagerAdapter instead and now it works just fine...

Update UI from an AsyncTaskLoader

I've converted my AsyncTask to an AsyncTaskLoader (mostly to deal with configuration changes). I have a TextView I am using as a progress status and was using onProgressUpdate in the AsyncTask to update it. It doesn't look like AsyncTaskLoader has an equivalent, so during loadInBackground (in the AsyncTaskLoader) I'm using this:
getActivity().runOnUiThread(new Runnable() {
public void run() {
((TextView)getActivity().findViewById(R.id.status)).setText("Updating...");
}
});
I am using this in a Fragment, which is why I'm using getActivity(). This work pretty well, except when a configuration change happens, like changing the screen orientation. My AsyncTaskLoader keeps running (which is why I'm using an AsyncTaskLoader), but the runOnUiThread seems to get skipped.
Not sure why it's being skipped or if this is the best way to update the UI from an AsyncTaskLoader.
UPDATE:
I ended up reverting back to an AsyncTask as it seems better suited for UI updates. Wish they could merge what works with an AsyncTask with an AsyncTaskLoader.
It's actually possible. You essentially need to subclass the AsyncTaskloader and implement a publishMessage() method, which will use a Handler to deliver the progress message to any class that implements the ProgressListener (or whatever you want to call it) interface.
Download this for an example: http://www.2shared.com/file/VW68yhZ1/SampleTaskProgressDialogFragme.html (message me if it goes offline) - this was based of http://habrahabr.ru/post/131560/
Emm... you shouldn't be doing this.
because how an anonymous class access parent class Method or Field is by storing an invisible reference to the parent class.
for example you have a Activity:
public class MyActivity
extends Activity
{
public void someFunction() { /* do some work over here */ }
public void someOtherFunction() {
Runnable r = new Runnable() {
#Override
public void run() {
while (true)
someFunction();
}
};
new Thread(r).start(); // use it, for example here just make a thread to run it.
}
}
the compiler will actually generate something like this:
private static class AnonymousRunnable {
private MyActivity parent;
public AnonymousRunnable(MyActivity parent) {
this.parent = parent;
}
#Override
public void run() {
while (true)
parent.someFunction();
}
}
So, when your parent Activity destroys (due to configuration change, for example), and your anonymous class still exists, the whole activity cannot be gc-ed. (because someone still hold a reference.)
THAT BECOMES A MEMORY LEAK AND MAKE YOUR APP GO LIMBO!!!
If it was me, I would implement the "onProgressUpdate()" for loaders like this:
public class MyLoader extends AsyncTaskLoader<Something> {
private Observable mObservable = new Observable();
synchronized void addObserver(Observer observer) {
mObservable.addObserver(observer);
}
synchronized void deleteObserver(Observer observer) {
mObservable.deleteObserver(observer);
}
#Override
public void loadInBackground(CancellationSignal signal)
{
for (int i = 0;i < 100;++i)
mObservable.notifyObservers(new Integer(i));
}
}
And in your Activity class
public class MyActivity extends Activity {
private Observer mObserver = new Observer() {
#Override
public void update(Observable observable, Object data) {
final Integer progress = (Integer) data;
mTextView.post(new Runnable() {
mTextView.setText(data.toString()); // update your progress....
});
}
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreated(savedInstanceState);
MyLoader loader = (MyLoader) getLoaderManager().initLoader(0, null, this);
loader.addObserver(mObserver);
}
#Override
public void onDestroy() {
MyLoader loader = (MyLoader) getLoaderManager().getLoader(0);
if (loader != null)
loader.deleteObserver(mObserver);
super.onDestroy();
}
}
remember to deleteObserver() during onDestroy() is important, this way the loader don't hold a reference to your activity forever. (the loader will probably be held alive during your Application lifecycle...)
Answering my own question, but from what I can tell, AsyncTaskLoader isn't the best to use if you need to update the UI.
In the class in which you implement LoaderManager.LoaderCallback (presumably your Activity), there is an onLoadFinished() method which you must override. This is what is returned when the AsyncTaskLoader has finished loading.
The best method is to use LiveData, 100% Working
Step 1: Add lifecycle dependency or use androidx artifacts as yes during project creation
implementation "androidx.lifecycle:lifecycle-livedata:2.1.0"
Step 2: Create the loader class as follow, in loader create in public method to set the livedata that can be observed from activity or fragment. see the setLiveCount method in my loader class.
package com.androidcodeshop.asynctaskloaderdemo;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.MutableLiveData;
import androidx.loader.content.AsyncTaskLoader;
import java.util.ArrayList;
public class ContactLoader extends AsyncTaskLoader<ArrayList<String>> {
private MutableLiveData<Integer> countLive = new MutableLiveData<>();
synchronized public void setLiveCount(MutableLiveData<Integer> observer) {
countLive = (observer);
}
public ContactLoader(#NonNull Context context) {
super(context);
}
#Nullable
#Override
public ArrayList<String> loadInBackground() {
return loadNamesFromDB();
}
private ArrayList<String> loadNamesFromDB() {
ArrayList<String> names = new ArrayList<>();
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
names.add("Name" + i);
countLive.postValue(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return names;
}
#Override
protected void onStartLoading() {
super.onStartLoading();
forceLoad(); // forcing the loading operation everytime it starts loading
}
}
Step 3: Set the live data from activity and observe the change as follows
package com.androidcodeshop.asynctaskloaderdemo;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.MutableLiveData;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
public class MainActivity extends AppCompatActivity implements
LoaderManager.LoaderCallbacks<ArrayList> {
private ContactAdapter mAdapter;
private ArrayList<String> mNames;
private MutableLiveData<Integer> countLiveData;
private static final String TAG = "MainActivity";
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mNames = new ArrayList<>();
mAdapter = new ContactAdapter(this, mNames);
RecyclerView mRecyclerView = findViewById(R.id.recycler_view);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mRecyclerView.setAdapter(mAdapter);
countLiveData = new MutableLiveData<>();
countLiveData.observe(this, new androidx.lifecycle.Observer<Integer>() {
#Override
public void onChanged(Integer integer) {
Log.d(TAG, "onChanged: " + integer);
Toast.makeText(MainActivity.this, "" +
integer,Toast.LENGTH_SHORT).show();
}
});
// initialize the loader in onCreate of activity
getSupportLoaderManager().initLoader(0, null, this);
// it's deprecated the best way is to use viewmodel and livedata while loading data
}
#NonNull
#Override
public Loader onCreateLoader(int id, #Nullable Bundle args) {
ContactLoader loader = new ContactLoader(this);
loader.setLiveCount(countLiveData);
return loader;
}
#Override
public void onLoadFinished(#NonNull Loader<ArrayList> load, ArrayList data) {
mNames.clear();
mNames.addAll(data);
mAdapter.notifyDataSetChanged();
}
#Override
public void onLoaderReset(#NonNull Loader loader) {
mNames.clear();
}
#Override
protected void onDestroy() {
super.onDestroy();
}
}
Hope this will help you :) happy coding

Resources