I am learning MVVM with data binding in Android and got stuck on maintaining UI visibility state of items in the recycler view. Each item sell has 2 text view - title(visible) & description(hidden). Onclick of title, I want to show/hide description. To maintain its UI state, I have a boolean field in the POJO (shared with DB as an entity).
OnClick of title, I am trying to set this field. How can I make the change trigger changing the visibility and maintain it during the scroll?
Here is the Github link for the code and below, the code snippet that I want to work.
Following MVVM architecture with repository to hit network and update DB and observe data via LiveData.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="2dp">
<TextView
android:id="#+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#id/title"
android:layout_margin="8dp"
android:text="#{posts.desc}"
android:textSize="14sp"
android:visibility="#{(posts.isDescVisible()) ? View.VISIBLE : View.GONE}"
tools:text="This is an answer provided by the creator to test the UI layout" />
<TextView
android:id="#+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_margin="2dp"
android:ellipsize="end"
android:maxLines="1"
android:onClick="#{()-> posts.setDescVisible(!posts.isDescVisible())}"
android:text="#{posts.title}"
android:textSize="16sp"
android:textStyle="bold"
tools:text="This is a question" />
</RelativeLayout>
<data>
<import type="android.view.View" />
<variable
name="posts"
type="com.tyagiabhinav.loremipsum.model.db.Posts" />
</data>
</layout>
This works well for me
import androidx.databinding.BaseObservable;
public class Posts extends BaseObservable {
private int id;
private final String title;
public final String desc;
private boolean descVisible;
public Posts(String title, String desc) {
this.title = title;
this.desc = desc;
}
public void setId(int id) {
this.id = id;
}
public int getId() {
return id;
}
public String getTitle() {
return title;
}
public String getDesc() {
return desc;
}
public void setDescVisible(boolean descVisible) {
this.descVisible = descVisible;
notifyPropertyChanged(BR.descVisible);
}
#Bindable
public boolean isDescVisible() {
return descVisible;
}
}
And the xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="2dp">
<TextView
android:id="#+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#id/title"
android:layout_margin="8dp"
android:text="#{posts.desc}"
android:textSize="14sp"
***android:visibility="#{posts.descVisible ? View.VISIBLE : View.GONE}"***
tools:text="This is an answer provided by the creator to test the UI layout" />
<TextView
android:id="#+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_margin="2dp"
android:ellipsize="end"
android:maxLines="1"
android:onClick="#{()-> posts.setDescVisible(!posts.descVisible)}"
android:text="#{posts.title}"
android:textSize="16sp"
android:textStyle="bold"
tools:text="This is a question" />
</RelativeLayout>
<data>
<import type="android.view.View" />
<variable
name="posts"
type="com.tyagiabhinav.loremipsum.model.db.Posts" />
</data>
</layout>
Related
I am learning mvvm structure and making an app using mvvm structure and data binding also.
Now, what I want to do is, I wanted to fetch a user from sharedpreference, If I am getting a user successfully then, I would set the name of usr to edittext1. In that case, I want to request focus on edittext2.
How to achieve that using databinding? (in such a way that i don't
have to use activity. The job should be done using view model and xml
only.)
I have tried that using following way.
StartGameViewModel
public class StartGameViewModel extends ViewModel {
public static String TAG="StartGameViewModel";
private Preference<User> preference;
public ObservableField<String> player1Name=new ObservableField<>("");
public ObservableField<String> player2Name=new ObservableField<>("");
public ObservableBoolean shouldRequestFocus=new ObservableBoolean(false);
StartGameViewModel(Preference preference){
this.preference=preference;
}
public void getPreference() {
preference.get(Constants.CURRENT_USER,User.class)
.subscribe(new Observer<User>() {
#Override
public void onSubscribe(Disposable d) {
}
#Override
public void onNext(User user) {
player1Name.set(user.name);
shouldRequestFocus.set(true);
}
#Override
public void onError(Throwable e) {
Log.i(TAG,"user is logged out");
}
#Override
public void onComplete() {
Log.i(TAG,"Completed Preference");
}
});
}
}
activity_start_game.xml
<?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"
tools:context=".view.StartGameActivity">
<data>
<variable
name="startGameViewModel"
type="com.sevenbits.android.mvvmsample.viewmodel.StartGameViewModel" />
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#drawable/splash_bg">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Enter Players Name"
android:textSize="24sp"
android:textColor="#color/white"
app:layout_constraintVertical_bias="0.75"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toTopOf="#+id/input_layout"
app:layout_constraintTop_toTopOf="parent"
/>
<LinearLayout
android:id="#+id/input_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="40dp"
android:layout_marginRight="40dp"
android:background="#color/white"
android:elevation="20dp"
android:orientation="vertical"
android:paddingBottom="40dp"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:paddingTop="40dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.45"
tools:layout_editor_absoluteX="8dp">
<android.support.design.widget.TextInputLayout
android:id="#+id/til_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp">
<android.support.design.widget.TextInputEditText
android:id="#+id/et_player1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Player1"
android:text="#{startGameViewModel.player1Name}"
app:addTextChangedListener="#{startGameViewModel.Player1Watcher}" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:id="#+id/til_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp">
<android.support.design.widget.TextInputEditText
android:id="#+id/et_player2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Player2"
android:text="#{startGameViewModel.player2Name}"
app:addTextChangedListener="#{startGameViewModel.Player2Watcher}" />
</android.support.design.widget.TextInputLayout>
</LinearLayout>
<Button
android:id="#+id/start_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#drawable/bg_button"
android:elevation="20dp"
android:gravity="center"
android:paddingBottom="20dp"
android:paddingEnd="40dp"
android:paddingStart="40dp"
android:paddingTop="20dp"
android:text="Start"
android:textAllCaps="false"
android:textColor="#color/white"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="#+id/input_layout" />
</android.support.constraint.ConstraintLayout>
</layout>
StartGameActivity:
public class StartGameActivity extends AppCompatActivity {
ActivityStartGameBinding activityStartGameBinding;
StartGameViewModel startGameViewModel;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initDataBinding();
setStartGameListener();
}
private void initDataBinding() {
activityStartGameBinding = DataBindingUtil.setContentView(this, R.layout.activity_start_game);
StartGameViewModelFactory startGameViewModelFactory= Injection.provideStartGameViewModelFactory(this);
startGameViewModel = ViewModelProviders.of(this,startGameViewModelFactory).get(StartGameViewModel.class);
activityStartGameBinding.setStartGameViewModel(startGameViewModel);
startGameViewModel.getPreference();
if(startGameViewModel.shouldRequestFocus.get())
findViewById(R.id.et_player2).requestFocus();
}
private void setStartGameListener() {
findViewById(R.id.start_button).setOnClickListener(v -> {
Intent intent=new Intent(StartGameActivity.this,GameActivity.class);
intent.putExtra(Constants.PLAYER1_NAME,startGameViewModel.player1Name.get());
intent.putExtra(Constants.PLAYER2_NAME,startGameViewModel.player2Name.get());
startActivity(intent);
});
}
}
Very simple via binding adapter
#JvmStatic
#BindingAdapter("requestFocus")
fun requestFocus(view: TextView, requestFocus: Boolean) {
if (requestFocus) {
view.isFocusableInTouchMode = true
view.requestFocus()
}
}
<EditText
android:id="#+id/et_company"
android:layout_width="0dp"
android:layout_height="match_parent"
android:imeOptions="actionDone"
android:inputType="textCapSentences"
android:maxLength="32"
android:scrollbars="vertical"
app:requestFocus="#{company.requestFocus}" />
I know this is an old question, but I was not able to solve the issue with the current answer. Posting my answer here:
in XML:
<EditText
android:id="#+id/email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="#dimen/elevation15"
app:requestFocus="#{viewmodel.focus}"/>
And in the viewModel, I had a Boolean variable declared as
val focus = true
And the binding adapter is as follows:
companion object{
#BindingAdapter("requestFocus")
#JvmStatic fun requestFocus(editText: EditText, requestFocus: Boolean){
if(requestFocus){
editText.isFocusableInTouchMode = true
editText.requestFocus()
}
}
}
Basically what is happening here is, Since app:requestFocus is written inside the xml, it will invoke the binding adapter with the focus Boolean, which is given as the value for the attribute for app:requestFocus.
In my case, I didn't want to remove focus, that's why I used Boolean. If it needs to be changed, using a livedata and set the value, and update the binding adapter accordingly
Disclaimer: I am new to MVVM and Databinding, someone please correct me if I am wrong.
I have a model CricketModel
public class CricketModel extends BaseObservable{
private String score;
#Bindable
public String getScore(){
return score
}
public void setScore(String s){
score=s;
notifyPropertyChanged(BR.score);
}
}
When I call API I get JSONArray, I convert them to CricketModel and add it to a CricketModel list. And pass this list to adapter to display them in the Recycler view.
This is my item_cricket.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="cm"
type="com.panasonic.arbohub.cricket.model.CricketModel" />
</data>
<LinearLayout xmlns:app="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<android.support.v7.widget.CardView
android:id="#+id/cv_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="#dimen/dp_8"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginTop="8dp"
card_view:cardCornerRadius="5dp"
card_view:cardElevation="3dp">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/tv_two_s_score"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#id/tv_one_p_score"
android:layout_marginEnd="#dimen/dp_16"
android:layout_marginTop="#dimen/dp_8"
android:layout_toStartOf="#+id/rl_team_two"
android:textSize="#dimen/sp_12"
android:textStyle="bold"
android:text="#{cm.score}"
android:visibility="visible" />
</FrameLayout>
</android.support.v7.widget.CardView>
</LinearLayout>
</layout>
This is my ViewHolder
private class CricketViewHolder extends RecyclerView.ViewHolder {
ItemCricketBinding itemCricketBinding;
public CricketViewHolder(ItemCricketBinding binding) {
super(binding.getRoot());
itemCricketBinding = binding;
}
public void bind(CricketModel cm){
itemCricketBinding.setCm(cm);
itemCricketBinding.executePendingBindings();
}
}
In my CricketAdapter.java
holder.bind(cricketModelList.get(position));
API updates once in 1 mi, I'm replacing the cricketModelList with new data that is coming from the API but data is not reflecting in the UI.
Have followed this- https://medium.com/google-developers/android-data-binding-recyclerview-db7c40d9f0e4
Update:
After api has been called. I added notifyDataSetChanged() onClickOfthe item view, then new data is reflecting. So model has the new data but its not notifying the UI.
you should set "itemCricketBinding.lifecycleOwner" in bind method, not the best practice and I'm not sure about the performance but if you wonder why that not work, That was the reason.
I have RecyclerView ,where each item represents, CheckBox and EditText
when clicks on CheckBox the text of EditText should strike through,
I have ObservableBoolean which is article.complete
I used it in app:checkBoxChangeListener="#{article.complete}"
app:itemComplete="#{article.complete}"
it works unless I scroll RecyclerView, then clicking on CheckBox another item’s text is strike through
#BindingAdapter("itemComplete")
public static void bindItemComplete(EditText itemInput, boolean complete){
itemInput.setPaintFlags(complete ?
(itemInput.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG) : 0);
}
Article.java
public class Article{
public final ObservableBoolean complete = new ObservableBoolean();
}
xml file :
<?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">
<data>
<import type="android.view.View" />
<variable
name="viewModel"
type="se.ica.handla.articles.ArticleListViewModel" />
<variable
name="article"
type="se.ica.handla.models.articles.Article" />
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<EditText
android:id="#+id/editText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
app:itemComplete="#{article.complete}"
/>
<CheckBox
android:id="#+id/checkBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="#={article.complete}" />
</android.support.constraint.ConstraintLayout>
</layout>
As written in my comments, I recommend you to use Two-way-Databinding.
You can completely delete this BindingAdapter:
#BindingAdapter(value = {"checkBoxChangeListener", "article"}, requireAll = false)
public static void bindCheckBox(CheckBox view, final ObservableBoolean checked, Article article) {
if (view.getTag(R.id.binded) == null) {
//Here you are setting the attributes to your *view* and
//decouple it from your article. It does not reference it,
//the properties (isChecked) isnow on the view.
//So when your view gets recycled when you scroll,
//it still has the property you set the last time -
//and not from your current article, which is displayed now in the view.
view.setTag(R.id.binded, true);
view.setOnCheckedChangeListener((buttonView, isChecked) -> checked.set(isChecked));}
}
}
As you already found out, your xml should look like this now, using Two-Way Databinding:
<EditText
android:id="#+id/editText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
app:itemComplete="#{article.complete}"
/>
<CheckBox
android:id="#+id/checkBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="#={article.complete}" //#={} for Two-Way Databinding
/>
I have fragment with RecycleView and Floating Action Button to add new object.
POJO:
public class MockCar extends BaseObservable {
private String name;
private String plateNumber;
public MockCar(String name, String plateNumber) {
this.name = name;
this.plateNumber = plateNumber;
}
#Bindable
public String getName() {
return name;
}
#Bindable
public void setName(String name) {
this.name = name;
notifyPropertyChanged(BR.car);
}
#Bindable
public String getPlateNumber() {
return plateNumber;
}
#Bindable
public void setPlateNumber(String plateNumber) {
this.plateNumber = plateNumber;
notifyPropertyChanged(BR.car);
}
}
item.xml
<?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">
<data>
<variable
name="car"
type="com.igor.touchless.model.MockCar"/>
<variable
name="click"
type="com.igor.touchless.adapter.interfaces.CarClickHandler"/>
</data>
<android.support.v7.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:id="#+id/card_view"
android:layout_width="match_parent"
android:layout_height="72dp"
android:layout_gravity="center"
android:padding="4dp"
android:background="#FFF"
card_view:cardCornerRadius="6dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="#{car.name}"
android:textColor="#color/primary_text"
android:textSize="18sp" />
<TextView
android:id="#+id/plate_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#+id/name"
android:text="#{car.plateNumber}"
android:textColor="#color/secondary_text"
android:textSize="13sp" />
<ImageView
android:id="#+id/button_edit"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentRight="true"
android:background="#color/primary"
android:src="#drawable/ic_create_black_24dp"
app:civ_border_color="#00EEEEEE"
app:civ_border_width="0dp"
app:civ_shadow="false"
app:civ_shadow_color="#008BC34A"
app:civ_shadow_radius="0" />
<ImageView
android:id="#+id/button_remove"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:background="#F44336"
android:src="#drawable/ic_clear_black_24dp" />
</RelativeLayout>
</android.support.v7.widget.CardView>
</layout>
After pressing floating action button I add new item to collection
public void add(MockCar car) {
carList.add(car);
notifyDataSetChanged();
}
The problem is next - when I try to add elements more then can fit in the screen (8th and all next) I can see that recycler view twitches, doesn't matter I am at the beginning of the list or at the end. As far as I understand it updated all views if I update ArrayList or try to change any property of the object. I assume I made something wrong with it notifyPropertyChanged(BR.car), but have no idea how to deal with it.
If I use standard approach everything works as expected.
I just started learning android data binding. Sure somebody has already faced with this problem. Thanks in advance.
I have created a simple login screen using the databinding as described at http://developer.android.com/tools/data-binding/guide.html however I am unable to get the notification of text changed from
the text box or
the button click.
I think for the text box notification, the android team might not have implemented it completely. However, I fail to understand my mistake for the button click handler.
The fragment code looks like
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
FragmentAccountSetupInitialBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_account_setup_initial, container, false);
View view = binding.getRoot();
binding.setAccount(new UserAccount());
return view;
}
The ViewModel for user account is as defined below
public class UserAccount extends BaseObservable {
private String _email;
private String _password;
#Bindable
public String getEmail() {
return _email;
}
public void setEmail(String email) {
if(!TextUtils.equals(_email, email)) {
_email= email;
notifyPropertyChanged(BR.email);
}
}
#Bindable
public String getPassword() {
return _password;
}
public void setPassword(String password) {
if(!TextUtils.equals(_password, password)) {
_password = password;
notifyPropertyChanged(BR.password);
}
}
public void onSignInButtonClick(View view) {
// Sign in now
}
}
And the fragment layout is
<layout
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="account" type="project.namespace.UserAccount"/>
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="56dp"
android:paddingLeft="24dp"
android:paddingRight="24dp">
<!-- Email Label -->
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp">
<android.support.design.widget.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textEmailAddress"
android:hint="#string/email"
android:text="#{account.email}"/>
</android.support.design.widget.TextInputLayout>
<!-- Password Label -->
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp">
<android.support.design.widget.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:hint="#string/password"
android:text="#{account.password}"/>
</android.support.design.widget.TextInputLayout>
<android.support.v7.widget.AppCompatButton
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginBottom="24dp"
android:padding="12dp"
android:text="#string/signin"
android:onClick="#{account.onSignInButtonClick}"/>
</LinearLayout>
</FrameLayout>
</layout>
Update
Moving to android studio 2.1 beta 2 solves the first problem. Updated the layout as follows
<android.support.design.widget.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textEmailAddress"
android:hint="#string/email"
android:text="#={account.email}"/>
Can you try the following for your button?
<android.support.v7.widget.AppCompatButton
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginBottom="24dp"
android:padding="12dp"
android:text="#string/signin"
android:onClick="#{account::onSignInButtonClick}"/>
I get an error on the IDE ('!=', '%'... expected, got ':') but it works when the app is running... The important part is the "::".
Hope this helps you!
As I known,the onClick event seems not work in fragment and I don't why.Try use BindingAdapter annotation to define custom setter.