How to databind livedata object (android) - android-databinding

Today I've some question about mvvm and databinding on android,
I'm trying to bind object properties on view.
I've an Object (Model) with some properties, by example :
public String name;
public String title;
public int value;
I've a ViewModel with livedata like this :
MutableLiveData<Object> _obj = new MutableLiveData<>();
public LiveData<Object> obj = _obj;
And, at last, I've a view like this :
<layout>
<data>
<variable
name="viewModel">
type="com.sample.app.viewmodels.MainViewModel" />
</data>
<LinearLayout
... >
<TextView
android:text:="#{viewModel.obj.name}"
.../>
</LinearLayout>
</layout>
I saw that we can do that in a video from "Android Developers" about "LiveData" : https://youtu.be/OMcDk2_4LSk?t=102
She says that its possible in Android studio on 3.1+ versions.
But this is not working for me.

For this to work, your model class must extend BaseObservable class from databinding library. And you have to call notifyChange() on each setter method like this:
public class Object extends BaseObservable {
public String name;
public String title;
public int value;
public void setName(String name) {
this.name = name;
notifyChange();
}
public void setTitle(String title) {
this.title = title;
notifyChange();
}
public void setValue(int value) {
this.value = value;
notifyChange();
}
}

Related

LiveData update values when field of object change

Today I'm trying to use a LiveData (MutableLiveData) object to have some dynamics values. (in MVVM pattern)
I used a model object like this:
public class Object {
private String name;
private float internalvalue;
private float in1;
private float out1;
private float out2;
public Object(String name, float internalvalue){
this.name = name;
this.internalvalue = internalvalue;
}
public float getOut1(){
return this.out1;
}
public float getOut2(){
return this.out2;
}
public void setIn1(float in1){
this.in1 = in1;
}
private void performSomething(float internalvalue, float in1){
SubClassSingleton.performSomething(internalvalue, in1, new SubClassSingletonListener() {
#Override
public void onSuccess(float out1, float out2){
this.out1 = out1;
this.out2 = out2;
}
});
}
}
I use a ViewModel like this:
public class MainViewModel {
public MutableLiveData<Object> obj;
public MainViewModel(){
this.obj = new MutableLiveData<>();
this.obj.postValue(new Object("Name", 50.0f);
}
}
In MainFragment:
public class MainFragment extends Fragment {
#Nullable
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
MainViewModel viewModel = ViewModelProviders.of(this).get(MainViewModel.class);
MainFragmentBinding binding = MainFragmentBinding.inflate(getLayoutInflater());
binding.setViewModel(viewModel);
binding.setLifecycleOwner(this);
return binding.getRoot();
}
}
And in View:
<layout ...
<data>
<variable
name="viewModel"
type="app.example.MainViewModel"/>
</data>
<LinearLayout
...>
<EditText
android:text="#={viewModel.obj.in1}"
.../>
<TextView
android:text="#{viewModel.obj.out1}"
.../>
<TextView
android:text="#{viewModel.obj.out2}"
.../>
</LinearLayout>
</layout>
I'd like to update my view when the values (out1, out2) of my model are updated (when the calculation is performed).
How can I do this ?
Try this:
You need to make your model class extend BaseObservable.
public class Object extends BaseObservable {
private String name;
private float internalvalue;
private float in1;
private float out1;
private float out2;
public Object(String name, float internalvalue) {
this.name = name;
this.internalvalue = internalvalue;
}
public float getOut1() {
return this.out1;
}
public void setOut1(float out1) {
this.out1 = out1;
notifyChange();
}
public float getOut2() {
return this.out2;
}
public void setOut2(float out2) {
this.out2 = out2;
notifyChange();
}
public void setIn1(float in1) {
this.in1 = in1;
}
private void performSomething(float internalvalue, float in1) {
SubClassSingleton.performSomething(internalvalue, in1, new SubClassSingletonListener() {
#Override
public void onSuccess(float out1, float out2) {
setOut1(out1);
setOut2(out2);
}
});
}
}
And in View, you cannot bind float directly to view. It has to be string, so bind like this:
<layout ...
<data>
<variable
name="viewModel"
type="app.example.MainViewModel"/>
</data>
<LinearLayout
...>
<EditText
android:text='#={""+viewModel.obj.in1}'
.../>
<TextView
android:text='#{""+viewModel.obj.out1}'
.../>
<TextView
android:text='#{""+viewModel.obj.out2}'
.../>
</LinearLayout>
</layout>

Updating UI using ViewModel and DataBinding

I am trying to learn ViewModel in android, in my first phase of learning I am trying to update UI (TextView) by using ViewModel and DataBinding. In ViewModel, I have an AsyncTask callback and it will invoke REST API call. I am getting the response from API call but the value in textview is not getting updated.
my ViewModel class:
public class ViewModelData extends ViewModel {
private MutableLiveData<UserData> users;
public LiveData<UserData> getUsers() {
if (users == null) {
users = new MutableLiveData<UserData>();
loadUsers();
}
return users;
}
public void loadUsers() {
ListTask listTask =new ListTask (taskHandler);
listTask .execute();
}
public Handler taskHandler= new Handler() {
#Override
public void handleMessage(Message msg) {
UserData userData = (UserData) msg.obj;
users.setValue(userData);
}
};
}
and my MainActivity class:
public class MainActivity extends AppCompatActivity implements LifecycleOwner {
private LifecycleRegistry mLifecycleRegistry;
private TextView fName;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
fName = (TextView)findViewById(R.id.text_name);
mLifecycleRegistry = new LifecycleRegistry(this);
mLifecycleRegistry.markState(Lifecycle.State.CREATED);
ViewModelData model = ViewModelProviders.of(this).get(ViewModelData.class);
model.getUsers().observe(this, new Observer<UserData>() {
#Override
public void onChanged(#Nullable UserData userData) {
Log.d("data"," = - - - - ="+userData.getFirstName());
}
});
}
#Override
public Lifecycle getLifecycle() {
return mLifecycleRegistry;
}
}
and my data class:
public class UserData extends BaseObservable{
private String firstName ;
#Bindable
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
notifyPropertyChanged(BR.firstName);
}
}
and layout file
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="android.view.View" />
<variable name="data" type="com.cgi.viewmodelexample.UserData"/>
</data>
<RelativeLayout
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="#dimen/activity_vertical_margin"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:paddingTop="#dimen/activity_vertical_margin"
tools:context="com.cgi.viewmodelexample.MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{data.firstName}"
android:id="#+id/text_name"/>
</RelativeLayout>
</layout>
I suggest to follow next basic principles:
don't overload data objects by business or presentation logic
only view model required to obtain data in presentation layer
view model should expose only ready to use data to presentation layer
(optional) background task should expose LiveData to deliver data
Implementation notes:
firstName is read only on view
lastName is editable on view
loadUser() is not threadsafe
we have error message when call save() method until data is not loaded
Don't overload data objects by business or presentation logic
Suppose, we have UserData object with first and last name. So, getters it's (usually) all what we need:
public class UserData {
private String firstName;
private String lastName;
public UserData(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
}
Only view model required to obtain data in presentation
To follow this suggestion we should to use only view model in data binding layout:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
tools:context="com.example.vmtestapplication.MainActivity">
<data>
<import type="android.view.View" />
<!-- Only view model required -->
<variable
name="vm"
type="com.example.vmtestapplication.UserDataViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:orientation="vertical">
<!-- Primitive error message -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{vm.error}"
android:visibility="#{vm.error == null ? View.GONE : View.VISIBLE}"/>
<!-- Read only field (only `#`) -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{vm.firstName}" />
<!-- Two-way data binding (`#=`) -->
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#={vm.lastName}" />
</LinearLayout>
</layout>
Note: you can use a few view models in one layout, but not raw data
View model should expose only ready to use data to presentation
This mean, you shouldn't to expose complex data objects (UserData in our case) directly from view model. Preferable to expose privative types which view can use as-is. In example below we don't need to hold UserData object because it used only to loading grouped data. We, probably, need to create UserData to save it but it depends on your repository implementation.
public class UserDataViewModel extends ViewModel {
private ListTask loadTask;
private final MutableLiveData<String> firstName = new MediatorLiveData<>();
private final MutableLiveData<String> lastName = new MediatorLiveData<>();
private final MutableLiveData<String> error = new MutableLiveData<>();
/**
* Expose LiveData if you do not use two-way data binding
*/
public LiveData<String> getFirstName() {
return firstName;
}
/**
* Expose MutableLiveData to use two-way data binding
*/
public MutableLiveData<String> getLastName() {
return lastName;
}
public LiveData<String> getError() {
return error;
}
#MainThread
public void loadUser(String userId) {
// cancel previous running task
cancelLoadTask();
loadTask = new ListTask();
Observer<UserData> observer = new Observer<UserData>() {
#Override
public void onChanged(#Nullable UserData userData) {
// transform and deliver data to observers
firstName.setValue(userData == null? null : userData.getFirstName());
lastName.setValue(userData == null? null : userData.getLastName());
// remove subscription on complete
loadTask.getUserData().removeObserver(this);
}
};
// it can be replaced to observe() if LifeCycleOwner is passed as argument
loadTask.getUserData().observeForever(observer);
// start loading task
loadTask.execute(userId);
}
public void save() {
// clear previous error message
error.setValue(null);
String fName = firstName.getValue(), lName = lastName.getValue();
// validate data (in background)
if (fName == null || lName == null) {
error.setValue("Opps! Data is invalid");
return;
}
// create and save object
UserData newData = new UserData(fName, lName);
// ...
}
#Override
protected void onCleared() {
super.onCleared();
cancelLoadTask();
}
private void cancelLoadTask() {
if (loadTask != null)
loadTask.cancel(true);
loadTask = null;
}
}
Background task should expose LiveData to deliver data
public class ListTask extends AsyncTask<String, Void, UserData> {
private final MutableLiveData<UserData> data= new MediatorLiveData<>();
public LiveData<UserData> getUserData() {
return data;
}
#Override
protected void onPostExecute(UserData userData) {
data.setValue(userData);
}
#Override
protected UserData doInBackground(String[] userId) {
// some id validations
return loadRemoiteUser(userId[0]);
}
}
MainActivity.java
public class MainActivity extends AppCompatActivity {
private UserDataViewModel viewModel;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// get view model
viewModel = ViewModelProviders.of(this).get(UserDataViewModel.class);
// create binding
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
// set view model to data binding
binding.setVm(viewModel);
// don't forget to set LifecycleOwner to data binding
binding.setLifecycleOwner(this);
// start user loading (if necessary)
viewModel.loadUser("user_id");
// ...
}
}
PS: try to use RxJava library instead of AsyncTask to perform background work.
You'll require to notify observer when you set value like this :
public class UserData extends BaseObservable{
private String firstName ;
#Bindable
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
notifyPropertyChanged(BR.firstName) // call like this
}
}
If you want binding layout to work then you have to set your view in binding way. Also set data in binding class.
public class MainActivity extends AppCompatActivity implements LifecycleOwner {
ActivityMainBinding binding;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
...
ViewModelData model = ViewModelProviders.of(this).get(ViewModelData.class);
...
binding.setData(model.getUsers());
}
}

Property change is not propagated to binding target (UWP)

I have a ViewModel and a Class. They look like this:
//viewmodel
public class MyViewModel : ViewModelBase {
private MyClass myClass;
public MyClass MyClass{
get{
return myClass;
}
set{
this.myClass = value;
base.OnPropertyChanged();
}
}
private string testString;
public string TestString{
get{
return testString;
}
set{
this.testString = value;
base.OnPropertyChanged();
}
}
public MyViewModel(){
this.MyClass = new MyClass();
this.TestString = "blah, blah, blah"
}}
//class
public class MyClass : ViewModelBase{
private string myString;
public string MyString{
get {
return myString
}
set{
this.myString = value;
base.OnPropertyChanged();
}
}
public MyClass (){
this.MyString = "25"; }}
The base class - ViewModelBase implements INotifyChange and contains OnPropertyChanged handler logic.
I have a UserControl where I wish to bind values from MyClass like this:
<TextBlock Text="{x:Bind Path=MyViewModel.MyClass.MyString, Mode=TwoWay}"></TextBlock>
However this does not work. Value is binded in initialisation correctly, but any change in MyViewModel.MyClass.MyString is not reflected in texblock, the text remains the same. The OnPropertyChange is raised, the breakpoint in ViewModelBase is hit with MyString value changed, but it is somehow not propagated to texblock.
Binding on simple value from MyClass works like charm, this textblock is updated, when property is changed:
<TextBlock Text="{x:Bind Path=MyViewModel.TestString, Mode=TwoWay}"></TextBlock>
What am I missing? Why the textblock with binding to "MyViewModel.MyClass.MyString" is not being updated?
I have tested the code you provided and don't think there is any problem with it. Where the problem could be, however, is your Page's code-behind. By any chance, aren't you using this as the property declaration?
public MyViewModel MyViewModel => new MyViewModel();
Because in this case, every access to MyViewModel property evaluates as a new instance of MyViewModel class. In this configuration, you would properly change the property, but the UI would never notice, as you would update the property on a new instance. If you instead use
public MyViewModel MyViewModel { get; } = new MyViewModel();
You will get the correct behavior of creating only one instance when the page is created.
I have the following code:
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
}
public MyViewModel MyViewModel { get; } = new MyViewModel();
private void Button_Click(object sender, RoutedEventArgs e)
{
MyViewModel.MyClass.MyString = "test";
}
}
And my simple ViewModelBase:
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

No converter found capable of converting from type [java.lang.String] to type [org.springframework.data.solr.core.geo.Point]

I am trying to use spring-data-solr in order to access to my Solr instance through my Spring boot application. I have the following bean class:
#SolrDocument(solrCoreName = "associations")
public class Association implements PlusimpleEntityI {
#Id
#Indexed
private String id;
#Indexed
private String name;
#Indexed
private Point location;
#Indexed
private String description;
#Indexed
private Set<String> tags;
#Indexed
private Set<String> topics;
#Indexed
private Set<String> professionals;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Point getLocation() {
return location;
}
public void setLocation(Point location) {
this.location = location;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Set<String> getTags() {
return tags;
}
public void setTags(Set<String> tags) {
this.tags = tags;
}
public Set<String> getTopics() {
return topics;
}
public void setTopics(Set<String> topics) {
this.topics = topics;
}
public Set<String> getProfessionals() {
return professionals;
}
public void setProfessionals(Set<String> professionals) {
this.professionals = professionals;
}
}
I have implemented the following repository in order to access to the related information:
public interface AssociationsRepository extends SolrCrudRepository<Association, String> {
}
I have created a configuration class which looks like the following one:
#Configuration
#EnableSolrRepositories(basePackages = {"com.package.repositories"}, multicoreSupport = true)
public class SolrRepositoryConfig {
#Value("${solr.url}")
private String solrHost;
#Bean
public SolrConverter solrConverter() {
MappingSolrConverter solrConverter = new MappingSolrConverter(new SimpleSolrMappingContext());
solrConverter.setCustomConversions(new CustomConversions(null));
return solrConverter;
}
#Bean
public SolrClientFactory solrClientFactory () throws Exception {
return new MulticoreSolrClientFactory(solrClient());
}
#Bean
public SolrClient solrClient() throws Exception {
return new HttpSolrClient.Builder(solrHost).build();
}
#Bean
public SolrOperations associationsTemplate() throws Exception {
SolrTemplate solrTemplate = new SolrTemplate(solrClient());
solrTemplate.setSolrConverter(solrConverter());
return solrTemplate;
}
}
Unfortunately, when I try to read an association from my Solr instance I got the following error:
org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [java.lang.String] to type [org.springframework.data.solr.core.geo.Point]
I don't understand why it is not able to find a converter if I have explicitly defined it in the solrTemplate() method.
This is my POM definition:
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-solr</artifactId>
<version>2.1.4.RELEASE</version>
</dependency>
Thank you for your help.
EDIT:
I've also tried with different BUILD-RELEASEs but they are highly unstable and I've found a lot of errors using them.
Alessandro, as you can see directly in the GeoConverters class on GitHub, the implemented converters are only for:
org.springframework.data.geo.Point
and not for:
org.springframework.data.solr.core.geo.Point
Simply use this class and you don't even need a custom converter for this. Spring Data for Solr will perform the conversion for you.
I'm using a slightly patched version of the 3.0.0 M4, but I'm pretty sure this solution should apply seamlessly also to your case.

Universal Windows Apps 8.1 data binding issue

Simple two way data binding to a model's property is not working, to reproduce the issue, I have created a new project in Visual Studio 2013 i.e. with Blank App (Universal Apps) template with .NET framework 4.5
Project folders and files
The model
namespace UWP.MVVM.Models
{
public class PersonModel
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
}
The base view model
namespace UWP.MVVM.Core
{
using System.ComponentModel;
using System.Runtime.CompilerServices;
public class VMBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
The INavigable interface
namespace UWP.MVVM.Core
{
#if WINDOWS_PHONE_APP
using Windows.Phone.UI.Input;
#endif
public interface INavigable
{
void Activate(object parameter);
void Deactivate(object parameter);
#if WINDOWS_PHONE_APP
void BackButtonPressed(BackPressedEventArgs e);
#endif
}
}
The main view model
namespace UWP.MVVM.ViewModels
{
using UWP.MVVM.Core;
using UWP.MVVM.Models;
#if WINDOWS_PHONE_APP
using Windows.Phone.UI.Input;
#endif
public class MainViewModel : VMBase, INavigable
{
private PersonModel person;
public MainViewModel()
{
this.Person = new PersonModel();
}
public PersonModel Person
{
get
{
return this.person;
}
set
{
if (value == this.person)
{
return;
}
this.person = value;
this.NotifyPropertyChanged();
}
}
public void Activate(object parameter)
{
this.Person.FirstName = "Gerrard";
}
public void Deactivate(object parameter)
{
}
#if WINDOWS_PHONE_APP
public void BackButtonPressed(BackPressedEventArgs e)
{
}
#endif
}
}
The main page view
<Page
x:Class="UWP.MVVM.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:UWP.MVVM"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:UWP.MVVM.ViewModels"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<!--<Page.DataContext>
<vm:MainViewModel/>
</Page.DataContext>-->
<Grid Margin="24,24">
<TextBox Header="First Name"
Text="{Binding Person.FirstName}"/>
</Grid>
</Page>
The main page code behind
namespace UWP.MVVM
{
using UWP.MVVM.Core;
#if WINDOWS_PHONE_APP
using Windows.Phone.UI.Input;
#endif
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;
using UWP.MVVM.ViewModels;
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
this.NavigationCacheMode = NavigationCacheMode.Required;
this.DataContext = new MainViewModel();
}
/// <summary>
/// Invoked when this page is about to be displayed in a Frame.
/// </summary>
/// <param name="e">Event data that describes how this page was reached.
/// This parameter is typically used to configure the page.</param>
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
var navigableViewModel = this.DataContext as INavigable;
if (navigableViewModel != null)
{
navigableViewModel.Activate(e.Parameter);
}
#if WINDOWS_PHONE_APP
HardwareButtons.BackPressed += HardwareButtons_BackPressed;
#endif
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
var navigableViewModel = this.DataContext as INavigable;
if (navigableViewModel != null)
{
navigableViewModel.Deactivate(e.Parameter);
}
#if WINDOWS_PHONE_APP
HardwareButtons.BackPressed -= HardwareButtons_BackPressed;
#endif
}
#if WINDOWS_PHONE_APP
private void HardwareButtons_BackPressed(object sender, BackPressedEventArgs e)
{
var navigableViewModel = this.DataContext as INavigable;
if (navigableViewModel != null)
{
navigableViewModel.BackButtonPressed(e);
}
}
#endif
}
}
I tried using Mode=TwoWay on the TextBox and it is not working, but when I set the DataContext in xaml instead of the code behind then data binding works even without the Mode=TwoWay property.
I want to set the DataContext in the code behind file as in the real project where I am having this issue, I am using MVVM-light libraries along with its SimpleIoc container, so I want to get the view model instance from SimpleIoc and set the DataContext because the view model dependencies are injected by the SimpleIoc and the code will be a lot cleaner.
The problem is: you only notify the change of PersonModel Person. The ViewModel need to notify the change of the property of PersonModel.
Since you are using MVVM Light, change your Model to:
public class PersonModel : ObservableObject
{
public int Id { get; set; }
string _FirstName = "";
public string FirstName {
get {
return _FirstName;
}
set {
Set(ref _FirstName, value);
}
}
public string LastName { get; set; }
}