Howto implement an Android view model inherited from AndroidViewModel with additional parameter in constructor? - android-architecture-components

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

Related

Two-way binding in Android with data from Room database

I am new to the MVVM architecture in Android, and I have some days with a doubt that I consider basic, but that I can't solve.
I proceed to discuss my problem:
I have an Entity, CustomerView (this entity is created from a DatabaseView):
#DatabaseView("select ... ")
public class CustomerView {
public String cardCode;
public String cardName;
public String cardFName;
...
Then, I have a Dao class:
#Dao
public interface OCRD_DAO {
...
#Query("SELECT * from CustomerView where cardCode= :cardCode")
LiveData<CustomerView> getCustomerViewByCardCode(String cardCode);
...
}
The repository class, makes use of the DAO class:
public LiveData<CustomerView> getCustomer(String cardCode){
return mOcrdDao.getCustomerViewByCardCode(cardCode);
}
The CustomerSheetViewModel class:
public class CustomerSheetViewModel extends BaseObservable {
private Repository mRepository;
public LiveData<CustomerView> mCustomer;
private MutableLiveData<String> _cardName;
#Bindable
public MutableLiveData<String> getCardName(){
return this._cardName;
}
public MutableLiveData<String> setCardName(String value){
// Avoids infinite loops.
if (mCustomer.getValue().cardName != value) {
mCustomer.getValue().cardName = value;
// React to the change.
saveData();
// Notify observers of a new value.
notifyPropertyChanged(BR._cardName);
}
}
public CustomerSheetViewModel (Application application, String cardCode) {
mRepository = new Repository(application);
this.mCustomer = mRepository.getCustomer(cardCode);
//Init MutableLiveData????
this._cardName = this.mCustomer.getValue().cardName;
//Null Exception, because this.mCustomer.getValue() is null
}
}
At this point, my problem occurs: when I initialise the CustomerView object, it is of type LiveData. However, if I want to make use of 2-way binding, I need an object of type MutableLiveData. So, I think I should create the MutableLiveData object with the data extracted from the database (i.e. from the call to the repository). When I try this (e.g. getValue().cardName) a null exception is thrown, since LiveData is asynchronous.
Finally, I could make use of this property in the layout:
android:text="#={customerSheetViewModel.cardName}"
I really appreciate any help, as I can't find any reference to 2-way binding when the data comes from a database read.
Thanks in advance.

JavaFX - Get CheckBox properties from outside GUIController

In JavaFX I want to check if a checkbox is selected and I want to do this using the lookup(#id) method. However this method returns a Node, which doesn't have the isSelected() method.
The code below shows the GUIController and a class Visualize it calls, where the status of the checkbox is read. I added a solution (reading the checkbox properties in GUIController and passing them to Visualize), but this is not how I want to proceed. I whish that the checkbox status is read in Visualize, because there will be many other GUI elements that I need to read so it is more compact to pass on a single object to Visualize instead of a list precomputed in GUIController.
Thank you for suggestions!
GUI Controller:
public class GUIController implements Initializable {
#FXML private AnchorPane RootPane;
#FXML private CheckBox TextCheckBox;
#Override
public void initialize(URL url, ResourceBundle rb) {
Boolean TextCheckBoxSelected = TextCheckBox.isSelected();
Visualize visualizeInstance = new Visualize();
root3D = visualizeInstance.draw(RootPane, TextCheckBoxSelected);
/* ... */
Class called by GUIController:
public class Visualize {
public Visualize() {
//
}
public Group draw(AnchorPane RootPane, Boolean TextCheckBoxSelected) {
System.out.println(RootPane.lookup("#TextCheckBox"));
System.out.println(TextCheckBoxSelected);
/* ... */
Output:
CheckBox[id=TextCheckBox, styleClass=check-box]'Text'
true
If you really want to do it this way, just downcast the result of the lookup:
public class Visualize {
// ...
public Group draw(AnchorPane rootPane) {
CheckBox textCheckBox = (CheckBox) rootPane.lookup("#TextCheckBox");
boolean selected = textCheckBox.isSelected();
// ...
}
}
If you are doing this because you need your Visualize object to respond to changes in the CheckBox's selected state, then consider passing a BooleanProperty instead, which you can observe if you need:
public class Visualize {
private BooleanProperty selectedProperty ;
public Visualize(BooleanProperty selectedProperty) {
this.selectedProperty = selectedProperty ;
// ...
}
// ...
public Group draw() {
boolean selected = selectedProperty.get();
// ...
}
}
and
Visualize visualizeInstance = new Visualize(textCheckBox.selectedProperty());
root3D = visualizeInstance.draw();

How do I persist the value of a property across page-refreshes?

I have created a property in app.xaml ( Public Static List id {get set} )
Can i add Data (Eg: App.id.Add(user.id)) to it.
So that i can take the added data from the page and use it any navigated page (Eg: App.id[3]).
Initializing is the problem when page refreshed the data goes back to App.id[0]
Alternativ you could use the Singleton pattern here, this will ensure that the list is created and only one instance exsist.
In your App.cs file write the following:
private static List<string> _id;
public static List<string> id
{
get
{
if (_id == null)
_id = new List<string>();
return _id;
}
set
{
_id = value;
}
}
Yes you can, public properties defined in app.cs can be reached from all your pages in your app.
in your App.cs
public List<String> id = new List<string>();
// Call this method somewhere so you create some data to use...
// eg in your App() contructor
public void CreateData()
{
id.Add("ID1");
id.Add("ID2");
id.Add("ID3");
id.Add("ID4");
id.Add("ID5");
}
When you need to use it your can get the data from eg. the OnNavigateTo event handler
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
// Sets the data context for the page to have a list of strings
this.DataContext = ((App)Application.Current).id;
// Or you can index to the data directly
var a = ((App)Application.Current).id[2];
}

sharing view model objects in eclipse RCP without OSGi services

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?

Strange Spring #SessionAttributes Behavior

I'm using #SessionAttributes on 2 controllers and am experiencing some very strange behavior. My first controller (ViewController) is simply a view controller that displays JSP pages. The other is a controller that handles Ajax requests (AjaxController). I have a session attribute that is simply an object that has a HashMap as a member. The object is a wrapper around the map. The map is populated from the database and put in the session, which displays fine via the ViewController. However, when I do a delete from the map via an ajax request (AjaxController) and refresh the page, ViewController SOMETIMES shows that the element is removed, yet other times the element is still there. Here's code snippets:
ViewController (the homepage simply displays the contents of the map contained by userSettings
#Controller
#SessionAttributes({"userSettings"})
public class ViewController {
#RequestMapping(value="/", method=RequestMethod.GET)
public String home(ModelMap model) {
UserSettings userSettings = (UserSettings) model.get("userSettings");
String userListenersJson = userSettings.toJson(); // for bootsrtapping the js on the front end
return "views/home";
}
}
AjaxController:
#Controller
#SessionAttributes({"userSettings"})
public class AjaxController {
#RequestMapping(value="/users/listeners/{externalId}", method=RequestMethod.DELETE)
public #ResponseBody
AjaxResponse<?> deleteListener(ModelMap model,
#PathVariable long externalId) {
UserSettings userSettings = (UserSettings) model.get("userSettings");
userSettings.removeSetting(externalId);
return new AjaxResponse<String>(null, true);
}
}
Am I using #SessionAttributes wrong here? Why would this work sometimes and not others? I've also tried putting all of the view and ajax functionality in the same controller and experienced the same behavior.
Thanks for any help!
EDIT:
I've refactored my code a bit to use the UserPrincipal via springsecurity. My understanding is that this object is stored in the session. Regardless, I'm seeing exactly the same behavior.
Here's the UserPrincipal constructor that populates the user settings map. I've set breakpoints here to ensure that the correct listenerDBOs are set - they are, every time. This is the only time the listeners get set from the db into the UserSettings object in CustomUserPrincipal. All other adds/removes are done via the controllers (quick aside: adds never fail... only removes):
public CustomUserPrincipal(UserDBO userDBO) {
// set UserSettings obj
UserSettingsAdapter.addListeners(userDBO.getUserListenerDBOs(), userSettings);
}
The UserSettings object itself:
public class UserSettings implements Serializable {
private static final long serialVersionUID = -1882864351438544088L;
private static final Logger log = Logger.getLogger(UserSettings.class);
private Map<Long, Listener> userListeners = Collections.synchronizedMap(new HashMap<Long, Listener>(1));
// get the listeners as an arraylist
public List<Listener> userListeners() {
return new ArrayList<Listener>(userListeners.values());
}
public Map<Long, Listener> getUserListeners() {
return userListeners;
}
public Listener addListener(Listener listener) {
userListeners.put(listener.getId(), listener);
return listener;
}
// I'm logging here to try and debug the issue. I do see the success
// message each time this function is called
public Listener removeListener(Long id) {
Listener l = userListeners.remove(id);
if (l == null) {
log.info("failed to remove listener with id " + id);
} else {
log.info("successfully removed listener with id " + id);
}
log.info("Resulting map: " + userListeners.toString());
log.info("Map hashcode: " + userListeners.hashCode());
return l;
}
public Listener getListener(long id) {
return userListeners.get(id);
}
}
This is the helper function in the UserSettingsAdapter class that adds to the UserSettings object, called from CustomUserDetails constructor:
public static void addListeners(Set<UserListenerDBO> userListeners, UserSettings userSettings) {
for (UserListenerDBO userListenerDBO : userListeners) {
if (userListenerDBO.isActive()) {
addListener(userListenerDBO, userSettings);
}
}
}
I've also changed the controller code to user the CustomUserPrincipal object instead of #SessionAttributes:
In ViewController:
#RequestMapping(value="/", method=RequestMethod.GET)
public String home(ModelMap model) {
CustomUserPrincipal userPrincipal = authenticationHelpers.getUserPrincipal();
UserSettings userSettings = userPrincipal.getUserSettings();
String userListenersJson = userSettings.toJson();
return "views/home";
}
In AjaxController:
#RequestMapping(value="/users/listeners/{externalId}", method=RequestMethod.DELETE)
public #ResponseBody
AjaxResponse<?> deleteListener(ModelMap model,
#PathVariable long externalId) {
CustomUserPrincipal userPrincipal = authenticationHelpers.getUserPrincipal();
UserSettings userSettings = userPrincipal.getUserSettings();
userSettings.removeListener(externalId);
return new AjaxResponse<String>(null, true);
}
I hope this helps shed some light on the issue!
I ran into a similar problem with #SessionAttributes. A controller had a #SessionAttributes annotation at the class level, and one of the methods handled POST requests, and included an instance of the session-managed object as an argument. This instance was saved to the database, but was re-used by subsequent requests, causing some data corruption. We had to add another method argument of type SessionStatus, and call SessionStatus.setComplete(). This caused the instance to be removed from the session, and prevented reuse and corruption. So try adding a SessionStatus instance to your controllers' handler methods, and invoke setComplete() where appropriate.
EDIT: I accidentally referenced the getter isComplete() in my initial answer; I meant to reference the setter setComplete().
#SessionAttributes is specific to a Controller and is not shared among several Controllers.
Instead, consider using manually session.setAttribute (class HttpSession).
You should have a look here : http://beholdtheapocalypse.blogspot.fr/2013/01/spring-mvc-framework-sessionattributes.html

Resources