I am developing a sort of monitor which should update a lot of imageviews (fake leds) basing on what i receive from a machine and let the user know the current machine state.
Is there a way to set on the variable a value listener which trigger a method when the value change in which i can update my UI?
Thanks everybody for the help and the comments;
I find out Data Binding was the best option in my case, so after a few attempts i've been able to build a sample project where i've got a an handler which every 1sec increment an integer value by 1.
After that i check if the value is pair or dispair and i set a boolen true or false.
I set up a layout file in which i've got 2 text view and a toggle button. The first text view text is binded to a simple string with my name, the second one is binded to the integer which is incremented each second (a timer) and the Toggle Button is binded to the Boolean.
Here's the Code:
MainActivity
public class MainActivity extends AppCompatActivity {
ActivityMainBinding binding;
android.os.Handler Handler;
User user;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
user = new User("Lorenzo", "Gangemi", 0);
binding.setUser(user);
useHandler();
}
public void useHandler() {
if (Handler != null) {
Handler.postDelayed(RunableHandler, 1000);
Handler.removeCallbacks(RunableHandler);
}
try {
Handler = new Handler();
} catch (Exception e) {
Handler = new Handler(Looper.getMainLooper());
}
Handler.postDelayed(RunableHandler, 1000);
}
private Runnable RunableHandler = new Runnable() {
#Override
public void run() {
user.increment_i();
binding.setUser(user);
useHandler();
}
};
}
User
public class User {
private final String firstName;
private final String lastName;
private boolean pari;
private int i;
public User(String firstName, String lastName, int i) {
this.firstName = firstName;
this.lastName = lastName;
this.i = i;
setPari(checkPari());
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public int getI() {
return i;
}
public void increment_i()
{
i++;
setPari(checkPari());
}
public boolean checkPari()
{
return i%2==0;
}
public boolean isPari() {
return pari;
}
public void setPari(boolean pari) {
this.pari = pari;
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="mdptech.databinding.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{user.firstName}"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{Integer.toString(user.i)}" />
<ToggleButton
android:text="ToggleButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/toggleButton"
android:drawableBottom="#drawable/toggle"
android:background="#android:color/transparent"
android:textOff="OFF"
android:textOn="ON"
android:checked="#{user.pari ? true : false}"/>
</LinearLayout>
</layout>
Everything works fine.
As you can see from the code, in the handler Runnable i call the method for increment the timer, but i have to call binding.setUser(user); too for updating the layout.
Is there a way to make this update happen automatically and not becouse i call a method?
That's not a big problem becouse i can create a separate handler which loops every 100ms (for ex.) and in his runnable call binding.setUser(user);, But it will speed up my work a lot.
Thank you.
Related
I have an activity that shows one text-view in that:
<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">
<data>
<variable
name="Users"
type="com.example.mvvm.model.UserInfoModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.MainActivity">
<TextView
android:id="#+id/tv_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="50dp"
android:text="#{Users.name,default = hello}"/>
</LinearLayout>
</layout>
And my UserInfoModel class:
public class UserInfoModel {
private String name;
public UserInfoModel() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
And in my Activity:
public class MainActivity extends AppCompatActivity {
ActivityMainBinding binding;
UserInfoModel users;
TextView textView;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
textView = binding.tvMain;
users = new UserInfoModel();
users.setName("Stack");
viewAction();
binding.setLifecycleOwner(this);
binding.setUsers(users);
}
}
My Problem is, When I Change name in viewAction method like this:
private void viewAction() {
textView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
users.setName("Change Text");
}
});
}
My UI doesn't change and text-view still showing Stack. I try to use binding.notifyPropertyChanged(id) but it doesn't affect. Where is my mistake? How can I change the data?
thanks for your attention.
You can just make your fields observable in the models.
public class UserInfoModel {
private ObservableField<String> name;
public UserInfoModel() {
this.name = new ObservableField<>();
}
public ObservableField<String> getName() {
return name;
}
public void setName(String name) {
this.name.set(name);
}
}
Actually the problem is that generated binding class doesn't know when property is changed. You should notify it. First, your viewmodel can be inherited from BaseObservable class, then you have to add #Bindable annotation to getters in your viewmodel and call notifyPropertyChanged inside setters with appropriate id from BR class(it's going to be generated for you by databinding library, just add #Bindable annotation. for instance notifyPropertyChanged(BR.name) ). Also you should pay attention how you store "binding" variable in MainActivity class.It can lead to memory leaks. You can investigate that issue. See library like databindingPropertyDelegate.
Something like this
public class UserInfoModel extends BaseObservable{
#Bindable
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
notifyPropertyChanged(BR.name)
}
}
I'm using the following dependencies in build.gradle:
compile 'com.google.firebase:firebase-database:11.4.2'
compile 'com.firebaseui:firebase-ui-database:3.1.0'
compile 'com.firebaseui:firebase-ui-auth:3.1.0'
My goal is to populate a Listview with data from a Firebase Database.
I'm following the guide on Github Firebase
The following method should bind the data to a listview:
private void showMessages() {
Query query = FirebaseDatabase.getInstance()
.getReference("notfication/unread/" + userMailAddress.replace(".", ","))
.orderByKey();
FirebaseListOptions<Message> options = new FirebaseListOptions.Builder<Message>()
.setLayout(R.layout.fbmessage_listitem)//Note: The guide doesn't mention this method, without it an exception is thrown that the layout has to be set.
.setQuery(query, Message.class)
.build();
FirebaseListAdapter<Message> adapter = new FirebaseListAdapter<Message>(options) {
#Override
protected void populateView(View v, Message model, int position) {
TextView tvMessage = v.findViewById(R.id.tv_message);
tvMessage.setText(model.getDateTimeCreated());
}
};
ListView readMessageList = findViewById(R.id.lvReadMessageList);
readMessageList.setAdapter(adapter);
}
This is the layout that contains the TextViews.
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="0.6"
android:orientation="vertical">
<TextView
android:id="#+id/tv_created_on"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:gravity="start"
android:paddingStart="5dp"
android:paddingEnd="0dp"
android:textColor="#android:color/black" />
<TextView
android:id="#+id/tv_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:gravity="start"
android:paddingStart="5dp"
android:paddingEnd="0dp"
android:textColor="#android:color/black" />
</LinearLayout>
<ImageView
android:id="#+id/iv_message_read"
android:layout_width="200dp"
android:layout_height="30dp"
android:layout_weight="0.2"
android:contentDescription="#string/message_read"
android:elevation="2dp"
app:srcCompat="#drawable/android_ok_sign" />
<ImageView
android:id="#+id/iv_delete"
android:layout_width="200dp"
android:layout_height="30dp"
android:layout_weight="0.2"
android:contentDescription="#string/abc_delete"
android:elevation="2dp"
android:src="#drawable/ic_delete_red_48dp" />
The model:
public class Message {
private String message;
private String sender;
private String receiver;
private long timestamp;
private boolean isRead;
private String userMailAddress;
public Message() {
}
public Message(String message, String sender, String receiver, long timestamp, boolean isRead) {
this.message = message;
this.sender = sender;
this.receiver = receiver;
this.timestamp = timestamp;
this.isRead = isRead;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getSender() {
return sender;
}
public void setSender(String sender) {
this.sender = sender;
}
public String getReceiver() {
return receiver;
}
public void setReceiver(String receiver) {
this.receiver = receiver;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
public boolean isRead() {
return isRead;
}
public void setRead(boolean read) {
isRead = read;
}
public String getUserMailAddress() {
return userMailAddress;
}
public void setUserMailAddress(String userMailAddress) {
this.userMailAddress = userMailAddress;
}
public String getDateTimeCreated() {
Calendar calendar = Calendar.getInstance(Locale.getDefault());
calendar.setTimeInMillis(timestamp);
String date = DateFormat.format("dd-MM-yyyy", calendar).toString();
String time = DateFormat.format("HH:mm", calendar).toString();
return date + "\r\n" + time;
}
}
No matter what i try, set a breakpoint or Log a TAG, the method populateView to bind the data from the model to the textfields isn't called.
I use the same layouts in the previous FirebaseUI (still in my production app) and everything is working fine there.
Does anyone know why the override method populateView isn't called?
I had the same problem, fixed it with:
#Override
protected void onStart() {
super.onStart();
mAdapter.startListening();
}
And then
#Override
protected void onStop() {
super.onStop();
mAdapter.stopListening();
}
Works like it did before the update :-)
I had the same problem. I tried the accepted answer, but the code was failing with null pointer exception in the startListening statement (adapter was null !).
After much research, I added .setLifecycleOwner(this) as in the sample code (see below code for example). It worked like a charm!
FirebaseListOptions<Message> options = new FirebaseListOptions.Builder<Message>()
.setLayout(R.layout.fbmessage_listitem)//Note: The guide doesn't mention this method, without it an exception is thrown that the layout has to be set.
.setQuery(query, Message.class)
.setLifecycleOwner(this) //Added this
.build();
Could it be a typo in the query? notifications instead of notification? Try putting a breakpoint in FirebaseListAdapter#onChildChanged() and see if any events come in.
Most likely, it's because you haven't called FirebaseListAdapter#startListening(). Unless you are using Android Architecture Components with FirebaseListOptions#setLifecycleOwner(...), you must manually manage your adapter's lifecycle. This means calling startListening() in onStart() and stopListening() in onStop().
PS: I would recommend checking out the RecyclerView which has better performance than ListView.
I am able to put data into Firebase database, but when I want to retrieve it and display it to ListView, it only displays blank screen.
BaseActivity class:
public class BaseActivity extends AppCompatActivity {
#Bean
OttoBus bus;
#Override
protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
bus.register(this);
}
#Override
protected void onDestroy() {
super.onDestroy();
bus.unregister(this);
}
}
HomeActivity class:
#OptionsMenu(R.menu.signout)
#EActivity(R.layout.activity_home)
public class HomeActivity extends BaseActivity {
private static final int LOGIN_REQUEST_CODE = 42;
#ViewById
ListView listView;
#Bean
ConversationAdapter conversationAdapter;
#Bean
UserDao userDao;
#AfterViews
void init() {
// if no user is logged in
if (FirebaseAuth.getInstance().getCurrentUser() == null) {
LoginActivity_.intent(this).startForResult(LOGIN_REQUEST_CODE);
} else {
userDao.init();
}
listView.setAdapter(conversationAdapter);
}
#OnActivityResult(value = LOGIN_REQUEST_CODE)
void loginSucceeded(int resultCode) {
if (resultCode != RESULT_OK) {
return;
}
userDao.init();
conversationAdapter.resetConversationFlow();
}
#Subscribe
public void usersLoaded(UsersLoadedEvent event) {
final FirebaseUser firebaseUser = FirebaseAuth
.getInstance().getCurrentUser();
if (userDao.userExists(firebaseUser.getUid())) {
userDao.setCurrentUser(userDao.getUserById(firebaseUser.getUid()));
} else {
final User user = new User(firebaseUser.getUid(),
firebaseUser.getDisplayName(),
firebaseUser.getPhotoUrl().toString());
userDao.write(user);
userDao.setCurrentUser(user);
}
}
#ItemClick
void listViewItemClicked(Conversation conversation) {
ConversationActivity_.intent(this)
.conversation(conversation)
.start();
}
/**
* When option item with id signOut is clicked.
*/
#OptionsItem
void signOut() {
FirebaseAuth.getInstance().signOut();
// restart this activity after user is logged out because if there is no user we will start
// login activity
final Intent intent = getIntent();
finish();
startActivity(intent);
}
/**
* Called when button with id=fab is clicked.
*/
#Click
void fab() {
CreateConversationActivity_.intent(this).start();
}
}
activity_home.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.nemus.execomchatworkshop.activity.HomeActivity">
<ListView
android:id="#+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<android.support.design.widget.FloatingActionButton
android:id="#+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_margin="#dimen/large_margin"
android:src="#drawable/ic_add_black_24dp" />
</RelativeLayout>
ConversationItemView class:
#EViewGroup(R.layout.item_view_conversation)
public class ConversationItemView extends LinearLayout {
#ViewById
TextView title;
public ConversationItemView(Context context) {
super(context);
}
public void bind(Conversation conversation) {
title.setText(conversation.getTitle());
}
}
item_view_conversation.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal">
<ImageView
android:id="#+id/conversationPhoto"
android:layout_width="#dimen/image_large_size"
android:layout_height="#dimen/image_large_size"
android:layout_margin="#dimen/medium_margin" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="#dimen/large_margin"
android:orientation="vertical">
<TextView
android:id="#+id/title"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textColor="#color/primary_text"
android:textSize="#dimen/large_text" />
<TextView
android:id="#+id/conversationPreview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textColor="#color/secondary_text"
android:textSize="#dimen/medium_text" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>
ConversationDao class
#EBean(scope = EBean.Scope.Singleton)
public class ConversationDao {
static final String CONVERSATION_TAG = "conversations";
private FirebaseDatabase database = FirebaseDatabase.getInstance();
private List<Conversation> conversations = new ArrayList<>();
private Map<String, Conversation> conversationMap = new HashMap<>();
#Bean
OttoBus bus;
/**
* After this class is injected call this method.
*/
#AfterInject
public void init() {
database.getReference(CONVERSATION_TAG).addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
conversationMap = dataSnapshot.getValue(
new GenericTypeIndicator<Map<String, Conversation>>() {
});
publish();
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
}
public void write(Conversation conversation) {
final DatabaseReference databaseReference =
database.getReference(CONVERSATION_TAG).push();
conversation.setId(databaseReference.getKey()); // set unique key to our conversation
databaseReference.setValue(conversation); // push conversation to database
}
public List<Conversation> getConversations() {
return conversations;
}
private void publish() {
conversations.clear();
if (conversationMap != null) {
conversations.addAll(conversationMap.values());
}
// post to event bus
bus.post(new ConversationsUpdateEvent());
}
ConversationAdapter class:
#EBean
public class ConversationAdapter extends BaseAdapter {
private List<Conversation> conversations = new ArrayList<>();
#RootContext
Context context;
#Bean
ConversationDao conversationDao;
#Bean
OttoBus bus;
/**
* This method is called after this class is injected.
*/
#AfterInject
void init() {
bus.register(this);
}
public void resetConversationFlow() {
conversationDao.init();
}
#Override
public int getCount() {
return conversations.size();
}
#Override
public Conversation getItem(int position) {
return conversations.get(position);
}
#Override
public long getItemId(int position) {
return 0;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
final ConversationItemView conversationItemView;
if (convertView == null) { // if view item is not created
conversationItemView = ConversationItemView_.build(context);
} else { // if view item was already created
conversationItemView = (ConversationItemView) convertView;
}
// bind data to view
conversationItemView.bind(getItem(position));
return conversationItemView;
}
private void setConversations(List<Conversation> conversations) {
this.conversations = conversations;
// notify that data set changed so that the list is refreshed
notifyDataSetChanged();
}
#Subscribe
public void conversationsUpdated(ConversationsUpdateEvent event) {
setConversations(conversationDao.getConversations());
}
}
So, what am I missing here. Why is not displaying conversation title into ListView?
It's difficult to follow allow with all this code at once. You are more likely to get answers if you ask one small question, than one large one. This is what I would recommend for you to figure out how to solve this issue:
Get familiar with the Debugger. It is essential and once you learn it you will become much better at solving any issues and coding in general.
Setup breakpoints to stop after you've successfully retrieved your data. Then confirm that you actually received the data and it is currently stored in the variable you wish to use.
Now that you've confirmed that your data exists, make sure you're setting up the list view correctly.
Firstly, I would recommend you start out with RecylerView, as that is the much newer standard and has much more helpful tutorials online. They are fundamently the same thing but RecylerView is better. Then I would continue to use the debugger to check that my data is being passed in to the RecylerViewAdapter correctly, and that when the data is bounded, it is pointing it to the correct views.
Here is a good tutorial for using the Android studio debugger.
Here is a good tutorial by google for adding lists(Using recylerview)
I'm having some issue with implementing two way binding with an Integer data type.
public class User {
private String firstName;
private String lastName;
private int age;
public User() {}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getFirstName() {
return this.firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getLastName() {
return this.lastName;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return this.age;
}
}
XML:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data class="UserDataBinding">
<variable
name="user"
type="com.databinding.model.User" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="#dimen/activity_horizontal_margin">
<EditText android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#={user.firstName}" />
<EditText android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#={user.lastName}" />
<EditText android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#={user.age}" />
</LinearLayout>
</layout>
Unfortunately, it gives me the error
"Error:(52, 17) Cannot find the getter for attribute 'android:text'
with value type java.lang.Integer on
android.support.design.widget.TextInputEditText. "
If I change the attribute text to
<EditText android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#={Integer.toString(user.age)}" />
then I get the error
"Error:cannot generate view binders java.lang.NullPointerException"
Appreciate any help on this.
UPDATE: It seems there was another error right after the error mentioned above.
cannot generate view binders java.lang.NullPointerException
Not sure why its giving me NPE even though the app hasn't started yet.
Well, six months later but maybe i can help someone.
You can do this simple trick:
android:text="#={`` + mObject.someNumber}"
OBS.: You need at least Android Studio 2.3
android:text="#{String.valueOf(Integer)}"
Somehow I got this to work by using BindingAdapter and InverseBindingAdapter.
public class User {
private String firstName;
private String lastName;
private int age;
public User() {}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getFirstName() {
return this.firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getLastName() {
return this.lastName;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return this.age;
}
#BindingAdapter("android:text")
public static void setText(TextView view, int value) {
view.setText(Integer.toString(value));
}
#InverseBindingAdapter(attribute = "android:text")
public static int getText(TextView view) {
return Integer.parseInt(view.getText().toString());
}
}
Hopefully this will help someone else as well.
I managed to use Integer.toString(...), doing the import, like this:
<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">
<data>
<import type="java.lang.Integer" />
<variable ... />
</data>
The previous answer, along with Roberto Leinardi's comment worked perfectly for me!
I only have to add is that a null-check should be done to Roberto's check:
#BindingAdapter("android:text")
public static void setText(TextView view, int value) {
view.setText(Integer.toString(value));
}
#BindingAdapter("android:text")
public static void setText(TextView view, int value) {
if (view.getText() != null
&& ( !view.getText().toString().isEmpty() )
&& Integer.parseInt(view.getText().toString()) != value) {
view.setText(Integer.toString(value));
}
}
Here is my solution. It's clean and simple. Simply if layout needs String, give it a String instead of int. All you have to do is create a setter and getter with String type and use them to bind to ui while normal setter and getter doing the normal thing!
A complete code !
My POJO class(Mydata.java). getAgeString and setAgeString are the ui methods, doing the conversion. Note that I put #Bindable on getAgeString. so ui will use ageString
package com.databindingnumber;
import android.databinding.BaseObservable;
import android.databinding.Bindable;
public class MyData extends BaseObservable{
private int age;
public int getAge() {
return age;
}
public void setAge(int age) {
if(this.age != age) {
this.age = age;
notifyPropertyChanged(BR.ageString);//NOTE: ui is using ageString !
}
}
#Bindable
public String getAgeString() {
return Integer.toString(age);
}
public void setAgeString(String ageString) {
try {
int val = Integer.parseInt(ageString);
this.setAge(val);
}catch(NumberFormatException ex){
this.setAge(0);//default value
}
}
}
The Layout File(activity_main.xml). use normal two-way binding with #= but use ageString instead of age
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="foo" type="com.databindingnumber.MyData"/>
</data>
<EditText
android:layout_width="100dp"
android:layout_height="wrap_content"
android:inputType="numberSigned"
android:text="#={foo.ageString}" />
</layout>
MainActivity.java file
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.setFoo(new MyData());
}
}
Hope this will help to someone!
The way of using #xdbas's solution
DataBindingConverter.kt
class DataBindingConverters {
companion object {
#InverseMethod("convertIntegerToString")
#JvmStatic
fun convertStringToInteger(value: String): Int? {
if (TextUtils.isEmpty(value) || !TextUtils.isDigitsOnly(value)) {
return null
}
return value.toIntOrNull()
}
#JvmStatic
fun convertIntegerToString(value: Int?): String {
return value?.toString() ?: ""
}
}
}
XML import
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="com.package.DataBindingConverter" />
</data>
.....
Bind to textView
<EditText
...
android:text="#={DataBindingConverter.convertStringToInteger(ViewModel.user.age)}" />
Maybe I should have edited his answer but i don't know if it didn't work for him.
This might help some people who need to get this to work with two way databinding and kotlin.
DataBindingConverter.kt
class DataBindingConverter {
companion object {
#InverseMethod("convertStringToInteger")
#JvmStatic
fun convertIntegerToString(value: String): Int? {
if (TextUtils.isEmpty(value) || !TextUtils.isDigitsOnly(value)) {
return null
}
return value.toIntOrNull()
}
#JvmStatic
fun convertStringToInteger(value: Int?): String {
return value?.toString() ?: ""
}
}
}
import that class in your view
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="com.package.DataBindingConverter" />
</data>
.....
bind it to a textview
<EditText
...
android:text="#={DataBindingConverter.convertStringToInteger(ViewModel.user.age)}" />
Add following in strings.xml:
<resources>
<string name="_int">%d</string>
</resources>
Then you can do:
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{#string/_int(user.age)}" />
I have implemented the new Android data-binding, and after implementing realised that it does not support two-way binding. I have tried to solve this manually but I am struggling to find a good solution to use when binding to an EditText.
In my layout I have this view:
<EditText
android:id="#+id/firstname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapWords|textNoSuggestions"
android:text="#{statement.firstName}"/>
Another view is also showing the results:
<TextView
style="#style/Text.Large"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{statement.firstName}"/>
In my fragment I create the binding like this:
FragmentStatementPersonaliaBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_statement_personalia, container, false);
binding.setStatement(mCurrentStatement);
This works and puts the current value of firstName in the EditText. The problem is how to update the model when the text changes. I tried putting an OnTextChanged-listener on the editText and updating the model. This created a loop killing my app (model-update updates the GUI, which calls textChanged times infinity). Next I tried to only notify when real changes occured like this:
#Bindable
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
boolean changed = !TextUtils.equals(this.firstName, firstName);
this.firstName = firstName;
if(changed) {
notifyPropertyChanged(BR.firstName);
}
}
This worked better, but everytime I write a letter, the GUI is updated and for som reason the edit-cursor is moved to the front.
Any suggestions would be welcome
EDIT 04.05.16:
Android Data binding now supports two way-binding automatically!
Simply replace:
android:text="#{viewModel.address}"
with:
android:text="#={viewModel.address}"
in an EditText for instance and you get two-way binding. Make sure you update to the latest version of Android Studio/gradle/build-tools to enable this.
(PREVIOUS ANSWER):
I tried Bhavdip Pathar's solution, but this failed to update other views I had bound to the same variable. I solved this a different way, by creating my own EditText:
public class BindableEditText extends EditText{
public BindableEditText(Context context) {
super(context);
}
public BindableEditText(Context context, AttributeSet attrs) {
super(context, attrs);
}
public BindableEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
private boolean isInititalized = false;
#Override
public void setText(CharSequence text, BufferType type) {
//Initialization
if(!isInititalized){
super.setText(text, type);
if(type == BufferType.EDITABLE){
isInititalized = true;
}
return;
}
//No change
if(TextUtils.equals(getText(), text)){
return;
}
//Change
int prevCaretPosition = getSelectionEnd();
super.setText(text, type);
setSelection(prevCaretPosition);
}}
With this solution you can update the model any way you want (TextWatcher, OnTextChangedListener etc), and it takes care of the infinite update loop for you. With this solution the model-setter can be implemented simply as:
public void setFirstName(String firstName) {
this.firstName = firstName;
notifyPropertyChanged(BR.firstName);
}
This puts less code in the model-class (you can keep the listeners in your Fragment).
I would appreciate any comments, improvements or other/better solutions to my problem
This is now supported in Android Studio 2.1+ when using the gradle plugin 2.1+
Simply change the EditText's text attribute from #{} to #={} like this:
<EditText
android:id="#+id/firstname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapWords|textNoSuggestions"
android:text="#={statement.firstName}"/>
for more info, see: https://halfthought.wordpress.com/2016/03/23/2-way-data-binding-on-android/
#Gober The android data-binding support the two way binding. Therefore you do not need to make it manually. As you tried by putting the OnTextChanged-listener on the editText. It should update the model.
I tried putting an OnTextChanged-listener on the editText and updating
the model. This created a loop killing my app (model-update updates
the GUI, which calls textChanged times infinity).
It’s worth noting that binding frameworks that implement two-way binding would normally do this check for you…
Here’s the example of modified view model, which does not raise a data binding notification if the change originated in the watcher:
Let’s create a SimpleTextWatcher that only requires only one method to be overridden:
public abstract class SimpleTextWatcher implements TextWatcher {
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
#Override
public void afterTextChanged(Editable s) {
onTextChanged(s.toString());
}
public abstract void onTextChanged(String newValue);
}
Next, in the view model we can create a method that exposes the watcher. The watcher will be configured to pass the changed value of the control to the view model:
#Bindable
public TextWatcher getOnUsernameChanged() {
return new SimpleTextWatcher() {
#Override
public void onTextChanged(String newValue) {
setUsername(newValue);
}
};
}
Finally, in the view we can bind the watcher to the EditText using addTextChangeListener:
<!-- most attributes removed -->
<EditText
android:id="#+id/input_username"
android:addTextChangedListener="#{viewModel.onUsernameChanged}"/>
Here is the implementation of the view Model that resolve the notification infinity.
public class LoginViewModel extends BaseObservable {
private String username;
private String password;
private boolean isInNotification = false;
private Command loginCommand;
public LoginViewModel(){
loginCommand = new Command() {
#Override
public void onExecute() {
Log.d("db", String.format("username=%s;password=%s", username, password));
}
};
}
#Bindable
public String getUsername() {
return this.username;
}
#Bindable
public String getPassword() {
return this.password;
}
public Command getLoginCommand() { return loginCommand; }
public void setUsername(String username) {
this.username = username;
if (!isInNotification)
notifyPropertyChanged(com.petermajor.databinding.BR.username);
}
public void setPassword(String password) {
this.password = password;
if (!isInNotification)
notifyPropertyChanged(com.petermajor.databinding.BR.password);
}
#Bindable
public TextWatcher getOnUsernameChanged() {
return new SimpleTextWatcher() {
#Override
public void onTextChanged(String newValue) {
isInNotification = true;
setUsername(newValue);
isInNotification = false;
}
};
}
#Bindable
public TextWatcher getOnPasswordChanged() {
return new SimpleTextWatcher() {
#Override
public void onTextChanged(String newValue) {
isInNotification = true;
setPassword(newValue);
isInNotification = false;
}
};
}
}
I hope this is what you are looking and sure can help you. Thanks
There is a simpler solution. Just avoid updating field if it hadn't really changed.
#Bindable
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
if(this.firstName.equals(firstName))
return;
this.firstName = firstName;
notifyPropertyChanged(BR.firstName);
}
POJO:
public class User {
public final ObservableField<String> firstName =
new ObservableField<>();
public final ObservableField<String> lastName =
new ObservableField<>();
public User(String firstName, String lastName) {
this.firstName.set(firstName);
this.lastName.set(lastName);
}
public TextWatcherAdapter firstNameWatcher = new TextWatcherAdapter(firstName);
public TextWatcherAdapter lastNameWatcher = new TextWatcherAdapter(lastName);
}
Layout:
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{user.firstName, default=First_NAME}"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{user.lastName, default=LAST_NAME}"/>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/editFirstName"
android:text="#{user.firstNameWatcher.value}"
android:addTextChangedListener="#{user.firstNameWatcher}"/>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/editLastName"
android:text="#{user.lastNameWatcher.value}"
android:addTextChangedListener="#{user.lastNameWatcher}"/>
Watcher:
public class TextWatcherAdapter implements TextWatcher {
public final ObservableField<String> value =
new ObservableField<>();
private final ObservableField<String> field;
private boolean isInEditMode = false;
public TextWatcherAdapter(ObservableField<String> f) {
this.field = f;
field.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback(){
#Override
public void onPropertyChanged(Observable sender, int propertyId) {
if (isInEditMode){
return;
}
value.set(field.get());
}
});
}
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
//
}
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
//
}
#Override public void afterTextChanged(Editable s) {
if (!Objects.equals(field.get(), s.toString())) {
isInEditMode = true;
field.set(s.toString());
isInEditMode = false;
}
}
}
I struggled to find a full example of 2-way databinding. I hope this helps.
The full documentation is here:
https://developer.android.com/topic/libraries/data-binding/index.html
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="item"
type="com.example.abc.twowaydatabinding.Item" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="#+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#={item.name}"
android:textSize="20sp" />
<Switch
android:id="#+id/switch_test"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="#={item.checked}" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="change"
android:onClick="button_onClick"/>
</LinearLayout>
</layout>
Item.java:
import android.databinding.BaseObservable;
import android.databinding.Bindable;
public class Item extends BaseObservable {
private String name;
private Boolean checked;
#Bindable
public String getName() {
return this.name;
}
#Bindable
public Boolean getChecked() {
return this.checked;
}
public void setName(String name) {
this.name = name;
notifyPropertyChanged(BR.name);
}
public void setChecked(Boolean checked) {
this.checked = checked;
notifyPropertyChanged(BR.checked);
}
}
MainActivity.java:
public class MainActivity extends AppCompatActivity {
public Item item;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
item = new Item();
item.setChecked(true);
item.setName("a");
/* By default, a Binding class will be generated based on the name of the layout file,
converting it to Pascal case and suffixing “Binding” to it.
The above layout file was activity_main.xml so the generate class was ActivityMainBinding */
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.setItem(item);
}
public void button_onClick(View v) {
item.setChecked(!item.getChecked());
item.setName(item.getName() + "a");
}
}
build.gradle:
android {
...
dataBinding{
enabled=true
}
}