What is best practice to share light weight view models or data context objects in an Eclipse RCP application, since I do not have control over view creation? I will prefer not to use OSGi services or static variables for something this light.
Say I have a MapView that will create an instance of its MapViewController in its constructor.
public class MapView extends ViewPart {
public static final String ID = "myapp.mapview";
MapViewController controller = null;
public MapView() {
LegendContainer legends = new LegendContainer();
MapViewModel viewModel = new MapViewModel();
controller = new MapViewController(null, legends, viewModel);
}
#Override
public void createPartControl(Composite parent) {
// create main map
}
}
MapViewModel has a property called Legends that needs to be accessed by a TableOfContentsView (TocView), which is trivial if TocView is created as child of MapView in MapView.createPartControl()
#Override
public void createPartControl(Composite parent) {
// pass model to child here when creating it
}
If I want TocView and MapView to exist as independent views in the perspective, added to the platform in MapPerspective.createInitialLayout like this:
public class MapPerspective implements IPerspectiveFactory {
#Override
public void createInitialLayout(IPageLayout layout) {
String editorArea = layout.getEditorArea();
layout.setEditorAreaVisible(false);
IFolderLayout main = layout.createFolder("right", IPageLayout.RIGHT, 0.2f, editorArea);
IFolderLayout top = layout.createFolder("top", IPageLayout.TOP, 0.5f, editorArea);
IFolderLayout bottom = layout.createFolder("bottom", IPageLayout.BOTTOM, 0.5f, editorArea);
// how do I share stuff???
main.addView(MapView.ID);
layout.getViewLayout(MapView.ID).setCloseable(false);
top.addView(TableOfContentsView.ID);
layout.getViewLayout(TableOfContentsView.ID).setCloseable(false);
bottom.addView(PropertiesView.ID);
layout.getViewLayout(PropertiesView.ID).setCloseable(false);
How do I pass around the view model or data context that needs to be shared?
Related
I have a fragment which shows images and possible sub-albums of an album. The fragment has a single argument which is the ID of the album to show. The ID is a POD long. For navigation incl. proper navigation history and passing arguments to the fragment, I use the navigation graph and the SafeArgs library.
Clicking on a sub-album navigates to the same fragment with the ID of sub-album to show. The loading of images and sub-albums is done in a view model.
Question: How do I pass the ID of the album as an argument of the constructor to my view model.
Here are some code snippets which might help to understand the problem
The fragment
public class AlbumFragment extends Fragment implements Observer<List<AlbumEntry>> {
private AlbumViewModel viewModel;
private long albumID; /*< The ID of the album which is shown by this instance of the fragment */
public static AlbumFragment newInstance( final long albumID ) {
AlbumFragment fragment = new AlbumFragment();
// The class `AlbumFragmentArgs` is auto-generated by the SafeArgs library
fragment.setArguments(
(new AlbumFragmentArgs.Builder()).setAlbumID( albumID ).build().toBundle()
);
return fragment;
}
#Override
public void onCreate( Bundle savedInstanceState ) {
super.onCreate( savedInstanceState );
albumID = AlbumFragmentArgs.fromBundle( getArguments() ).getAlbumID();
// TODO: We somehow need to pass the ID of the album to the view model
viewModel = new ViewModelProvider( this ).get( AlbumViewModel.class );
}
#Override
public View onCreateView( LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState ) {
// Inflates the view from XML layout file, skipped here
return view;
}
#Override
public void onViewCreated( #NonNull final View view, final Bundle savedInstanceState ) {
// PROBLEM: The list of album entries actually depends on the current album
// The ID of the current album should have been passed when then model was created
LiveData<? extends List<AlbumEntry>> liveAlbumEntries = viewModel.getAlbumEntries();
liveAlbumEntries.observe( this.getViewLifecycleOwner(), this );
}
// Methods for observing live data skipped here
protected void onClick( final View view ) {
// The code how the ID of the target album is obtained from the
// clicked view is left out here
// The variable is targetAlbumID
NavGraphDirections.ActionToAlbumFragment action = NavGraphDirections.actionToAlbumFragment();
action.setAlbumID( targetAlbumID );
Navigation.findNavController( this.getView() ).navigate( action );
}
}
The view model
public class AlbumViewModel extends AndroidViewModel {
private long albumID; /*< The ID of the album which is shown by this instance of the fragment */
public AlbumViewModel( Application application /*, final long albumID */ ) {
super( application );
// TODO: How to write a constructor which gets an Application object and an albumID?
/* this.albumID = albumID */
// Remaining code, esp. initialization of the repository, is left out
}
public LiveData<? extends List<AlbumEntry>> getAlbumEntries() {
// Code left out, but depends on `this.albumID`
}
}
Some final remarks
Of course, it might be possible to not pass the ID of the current album to the view model, but always pass the album ID explicitly as an additional argument to each method of the view model, for example LiveData<? extends List<AlbumEntry>> liveAlbumEntries = viewModel.getAlbumEntries( albumID ).
However, having one instance of the view model for each album ID would be much easier especially with respect to caching. If a single instance of the view model is bound to a particular album ID, the view model only needs to deal with the album entries of this album. If the corresponding fragment for the same album ID ends its life-cycle permanently, the corresponding view model will be destroyed and hence the album entries are removed from memory. If the fragment is only re-created (due to configuration changes) the corresponding view model is kept in memory and so are the album entries.
Contrary, if I only had one application-wide global instance of AlbumViewModel for all albums and all instances of the fragment and if the album ID is passed explicitly to each method of AlbumViewModel I would never know, when a particular fragment is destroyed and when the album entries can be removed from memory.
You should create a ViewModelFactory, like:
public class AlbumViewModelFactory implements ViewModelProvider.Factory {
private Application application;
private long id;
public AlbumViewModelFactory(Application application, long id) {
this.application = application;
this.id = id;
}
#Override
public <T extends ViewModel> T create(Class<T> modelClass) {
return (T) new AlbumViewModel(application, id);
}
}
i have a class that converts drawable image (png) to bitmap. in MainActivity i can do that but when i want do it in other classes i cnnot.
public class BitModel
{
public BitModel()
{
var _bit = BitmapFactory.DecodeResource(Android.Content.Res.Resources, Resource.Drawable.Icon);
}
}
the Android.Content.Res.Resources does not resolve that says Resources is a type which is not valid in the given context
You can override the constructor with a parameter in the model.
public class BitModel
{
public BitModel(Context context)
{
var _bit = BitmapFactory.DecodeResource(context.Resources, Resource.Drawable.Icon);
}
}
And pass the current context as a parameter when you init the model.For example in the MainActivity
BitModel model =new BitModel(context) // the context is the current context in the activity
I am facing an issue with the Vaadin spring annotation #UIScope, defined as follows:
#SpringComponent
#SpringView(name = AdminView.VIEW_NAME)
#UIScope
public class AdminView extends NavigatingView {
...
}
The view is created every time the navigation is opening the view. I would expect that it is created only once, on first time access.
However, if I replace #UIScope with #Scope(UIScopeImpl.VAADIN_UI_SCOPE_NAME) then it works as expected. Did I miss something?
It's related to the order of the #SpringView and #UIScope annotations, as the tutorial and the older wiki page briefly suggest:
// Pay attention to the order of annotations
It's probably related to how and when the annotations are processed. I did not dig that deep into the Vaadin code, but as per the the #SpringView javadoc it puts the view into a view-scope by default. Furthermore, I don't think you require the #SpringComponent annotation because you're already using #SpringView to register it a spring component.
Annotation to be placed on View-classes that should be handled by the SpringViewProvider.
This annotation is also a stereotype annotation, so Spring will automatically detect the annotated classes. By default, this annotation also puts the view into the view scope. You can override this by using another scope annotation, such as the UI scope, on your view class. However, the singleton scope will not work!
In the sample below, you'll find 2 views, the first one with the annotations in the correct order, and the second one with them swapped:
#SpringUI
#SpringViewDisplay
public class MyVaadinUI extends UI implements ViewDisplay {
/* UI */
private Panel springViewDisplay;
#Override
protected void init(VaadinRequest request) {
VerticalLayout mainLayout = new VerticalLayout();
HorizontalLayout buttonLayout = new HorizontalLayout();
springViewDisplay = new Panel();
buttonLayout.addComponent(new Button("1", event -> getNavigator().navigateTo(FirstView.VIEW_NAME)));
buttonLayout.addComponent(new Button("2", event -> getNavigator().navigateTo(SecondView.VIEW_NAME)));
mainLayout.addComponents(buttonLayout, springViewDisplay);
setContent(mainLayout);
}
#Override
public void showView(View view) {
springViewDisplay.setContent((Component) view);
}
/* VIEWS */
#UIScope
#SpringView(name = FirstView.VIEW_NAME)
public static class FirstView extends HorizontalLayout implements View {
public static final String VIEW_NAME = "";
#PostConstruct
private void init() {
System.out.println("Created first view");
addComponent(new Label("First view - " + LocalDateTime.now()));
}
#Override
public void enter(ViewChangeListener.ViewChangeEvent event) {
// no-op
}
}
#SpringView(name = SecondView.VIEW_NAME)
#UIScope
public static class SecondView extends HorizontalLayout implements View {
public static final String VIEW_NAME = "secondView";
#PostConstruct
private void init() {
System.out.println("Created second view");
addComponent(new Label("Second view - " + LocalDateTime.now()));
}
#Override
public void enter(ViewChangeListener.ViewChangeEvent event) {
// no-op
}
}
}
As you'll notice in the animation below, when navigating to the second view a new instance is always created, while navigating to the first one will reuse the initial instance:
I would like to communicate with a FXML controller class at any time, to update information on the screen from the main application or other stages.
Is this possible? I havent found any way to do it.
Static functions could be a way, but they don't have access to the form's controls.
Any ideas?
You can get the controller from the FXMLLoader
FXMLLoader fxmlLoader = new FXMLLoader();
Pane p = fxmlLoader.load(getClass().getResource("foo.fxml").openStream());
FooController fooController = (FooController) fxmlLoader.getController();
store it in your main stage and provide getFooController() getter method.
From other classes or stages, every time when you need to refresh the loaded "foo.fxml" page, ask it from its controller:
getFooController().updatePage(strData);
updatePage() can be something like:
// ...
#FXML private Label lblData;
// ...
public void updatePage(String data){
lblData.setText(data);
}
// ...
in the FooController class.
This way other page users do not bother about page's internal structure like what and where Label lblData is.
Also look the https://stackoverflow.com/a/10718683/682495. In JavaFX 2.2 FXMLLoader is improved.
Just to help clarify the accepted answer and maybe save a bit of time for others that are new to JavaFX:
For a JavaFX FXML Application, NetBeans will auto-generate your start method in the main class as follows:
#Override
public void start(Stage stage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("FXMLDocument.fxml"));
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
Now, all we need to do to have access to the controller class is to change the FXMLLoader load() method from the static implementation to an instantiated implementation and then we can use the instance's method to get the controller, like this:
//Static global variable for the controller (where MyController is the name of your controller class
static MyController myControllerHandle;
#Override
public void start(Stage stage) throws Exception {
//Set up instance instead of using static load() method
FXMLLoader loader = new FXMLLoader(getClass().getResource("FXMLDocument.fxml"));
Parent root = loader.load();
//Now we have access to getController() through the instance... don't forget the type cast
myControllerHandle = (MyController)loader.getController();
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
Another solution is to set the controller from your controller class, like so...
public class Controller implements javafx.fxml.Initializable {
#Override
public void initialize(URL location, ResourceBundle resources) {
// Implementing the Initializable interface means that this method
// will be called when the controller instance is created
App.setController(this);
}
}
This is the solution I prefer to use since the code is somewhat messy to create a fully functional FXMLLoader instance which properly handles local resources etc
#Override
public void start(Stage stage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("/sample.fxml"));
}
versus
#Override
public void start(Stage stage) throws Exception {
URL location = getClass().getResource("/sample.fxml");
FXMLLoader loader = createFXMLLoader(location);
Parent root = loader.load(location.openStream());
}
public FXMLLoader createFXMLLoader(URL location) {
return new FXMLLoader(location, null, new JavaFXBuilderFactory(), null, Charset.forName(FXMLLoader.DEFAULT_CHARSET_NAME));
}
On the object's loading from the Main screen, one way to pass data that I have found and works is to use lookup and then set the data inside an invisible label that I can retrieve later from the controller class. Like this:
Parent root = FXMLLoader.load(me.getClass().getResource("Form.fxml"));
Label lblData = (Label) root.lookup("#lblData");
if (lblData!=null) lblData.setText(strData);
This works, but there must be a better way.
I am a beginner in libgdx and was wondering in what cases you would need to use a constructor when switching screens (examples would be helpful). Is it to save memory? Also, is it better to create instances of all the screens in the main class that extends the game?
Here is an example of instances from https://code.google.com/p/libgdx-users/wiki/ScreenAndGameClasses :
public class MyGame extends Game {
MainMenuScreen mainMenuScreen;
AnotherScreen anotherScreen;
#Override
public void create() {
mainMenuScreen = new MainMenuScreen(this);
anotherScreen = new AnotherScreen(this);
setScreen(mainMenuScreen);
}
}
The constructor is in the next class:
public class MainMenuScreen implements Screen {
MyGame game; // Note it's "MyGame" not "Game"
// constructor to keep a reference to the main Game class
public MainMenuScreen(MyGame game){
this.game = game;
}
...
You should avoid creating all screens in Game class in create() method (you will allocate much memory at once and pointlesly). Create only one screen at a time when do you need It. So e.g. you click button New game in menu and there you call game.setScreen(new NextScreen(this));
You do not have to make constructor with Game parameter - but you will not have reference to the main Game class. Having reference to main game class is good for changing screens, method setScreen(screen).
You need the constructor because your changing to a screen that doesnt extend the Game class which is what u need to call setScreen(); Since ur passing the game class to the constructor you can use it to get back to the screen you were at (or another screen) without creating another class that extends Game
I prefer using a singleton.
Something like
public class MyGame extends Game {
private static MyGame myGame;
public static MyGame getInstance() {
if (myGame == null) {
myGame = new MyGame();
}
return myGame;
}
#Override
public void create() {
setScreen(new MainMenuScreen();
}
}
And the example for desktop main class
public class Main {
public static void main(String[] args) {
LwjglApplicationConfiguration cfg = new LwjglApplicationConfiguration();
cfg.width = 800;
cfg.height = 480;
new LwjglApplication(MyGame.getInstance(), cfg);
}
}
Now whenever you need to change your screen use MyGame.getInstance().setScreen(new ScreenName());