Android DataBinding in RecyclerView – Profile Screen
1. Prerequisite
This example needs basic knowledge in android data binding. Get started with DataBinding by reading the below tutorial.Read: Android working with DataBinding
2. Creating New Project
1. Create a new project in Android Studio from File ⇒ New Project and select Basic Activity from templates.2. Enable DataBiding in app/build.gradle. Also add the RecyclerView and Glide dependencies and Sync the project.
android { dataBinding { enabled = true } } dependencies { //... implementation 'com.github.bumptech.glide:glide:4.6.1' annotationProcessor 'com.github.bumptech.glide:compiler:4.6.1' implementation 'com.android.support:recyclerview-v7:27.1.0' } |
< uses-permission android:name = "android.permission.INTERNET" /> |
5. Add the below resources to respective strings.xml, dimens.xml and colors.xml
< resources > < string name = "app_name" >Data Binding</ string > < string name = "action_settings" >Settings</ string > < string name = "toolbar_profile" >Profile</ string > < string name = "posts" >POSTS</ string > < string name = "followers" >FOLLOWERS</ string > < string name = "following" >FOLLOWING</ string > </ resources > |
< resources > < dimen name = "fab_margin" >16dp</ dimen > < dimen name = "activity_margin" >16dp</ dimen > < dimen name = "dimen_8dp" >8dp</ dimen > < dimen name = "profile_image" >100dp</ dimen > < dimen name = "fab_profile" >30dp</ dimen > < dimen name = "profile_name" >15dp</ dimen > < dimen name = "profile_about" >13dp</ dimen > < dimen name = "profile_meta" >24dp</ dimen > < dimen name = "profile_meta_label" >10dp</ dimen > </ resources > |
<? xml version = "1.0" encoding = "utf-8" ?> < resources > < color name = "colorPrimary" >#222222</ color > < color name = "colorPrimaryDark" >#111111</ color > < color name = "colorAccent" >#fecb2f</ color > < color name = "profile_meta" >#333</ color > </ resources > |
Below is the final project structure and files required.

7. Create User class under model package. To make this class Observable, extend the class from BaseObservable.
For demonstration, both Observable and ObservableField are used in the same class.
- For variables name, email, profileImage and about., @Bindable annotation is used and notifyPropertyChanged is called upon setting new data
- Variables numberOfPosts, numberOfFollowers, numberOfFollowing are declared as ObservableFields
- @BindingAdapter is used to bind profileImage to ImageView in order to load the image from URL using Glide library.
import android.databinding.BaseObservable; import android.databinding.Bindable; import android.databinding.BindingAdapter; import android.databinding.ObservableField; import android.widget.ImageView; import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; import info.androidhive.databinding.BR; public class User extends BaseObservable { String name; String email; String profileImage; String about; // profile meta fields are ObservableField, will update the UI // whenever a new value is set public ObservableField<Long> numberOfFollowers = new ObservableField<>(); public ObservableField<Long> numberOfPosts = new ObservableField<>(); public ObservableField<Long> numberOfFollowing = new ObservableField<>(); public User() { } @Bindable public String getName() { return name; } public void setName(String name) { this .name = name; notifyPropertyChanged(BR.name); } @Bindable public String getEmail() { return email; } public void setEmail(String email) { this .email = email; notifyPropertyChanged(BR.email); } @BindingAdapter ({ "profileImage" }) public static void loadImage(ImageView view, String imageUrl) { Glide.with(view.getContext()) .load(imageUrl) .apply(RequestOptions.circleCropTransform()) .into(view); // If you consider Picasso, follow the below // Picasso.with(view.getContext()).load(imageUrl).placeholder(R.drawable.placeholder).into(view); } @Bindable public String getProfileImage() { return profileImage; } public void setProfileImage(String profileImage) { this .profileImage = profileImage; notifyPropertyChanged(BR.profileImage); } @Bindable public String getAbout() { return about; } public void setAbout(String about) { this .about = about; notifyPropertyChanged(BR.about); } public ObservableField<Long> getNumberOfFollowers() { return numberOfFollowers; } public ObservableField<Long> getNumberOfPosts() { return numberOfPosts; } public ObservableField<Long> getNumberOfFollowing() { return numberOfFollowing; } } |
import android.databinding.BindingAdapter; import android.widget.ImageView; import com.bumptech.glide.Glide; public class Post { String imageUrl; @BindingAdapter ( "imageUrl" ) public static void loadImage(ImageView view, String imageUrl) { Glide.with(view.getContext()) .load(imageUrl) .into(view); } public String getImageUrl() { return imageUrl; } public void setImageUrl(String imageUrl) { this .imageUrl = imageUrl; } } |
- convertToSuffix() method converts a number to human readable format. For example, 5500L will be converted as 5.5k and 5050890L will be converted as 5.1m.
- We bind this function to TextViews in order to display the posts, followers and following in human readable format.
package info.androidhive.databinding.utils; public class BindingUtils { // Converts the number to K, M suffix // Ex: 5500 will be displayed as 5.5k public static String convertToSuffix( long count) { if (count < 1000 ) return "" + count; int exp = ( int ) (Math.log(count) / Math.log( 1000 )); return String.format( "%.1f%c" , count / Math.pow( 1000 , exp), "kmgtpe" .charAt(exp - 1 )); } } |
package info.androidhive.databinding.utils; import android.graphics.Rect; import android.support.v7.widget.RecyclerView; import android.view.View; public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration { private int spanCount; private int spacing; private boolean includeEdge; public GridSpacingItemDecoration( int spanCount, int spacing, boolean includeEdge) { this .spanCount = spanCount; this .spacing = spacing; this .includeEdge = includeEdge; } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { int position = parent.getChildAdapterPosition(view); int column = position % spanCount; if (includeEdge) { outRect.left = spacing - column * spacing / spanCount; outRect.right = (column + 1 ) * spacing / spanCount; if (position < spanCount) { outRect.top = spacing; } outRect.bottom = spacing; } else { outRect.left = column * spacing / spanCount; outRect.right = spacing - (column + 1 ) * spacing / spanCount; if (position >= spanCount) { outRect.top = spacing; } } } } |
2.1 DataBinding in RecyclerView
Binding a RecyclerView layout is similar to normal binding except few changes in onCreateViewHolder and onBindViewHolder methods.10. Create layout named post_row_item.xml. This layout contains an ImageView to render the image in RecyclerView.
- In this layout, data binding is enabled by keeping the root element as <layout>. The Post model in bound to this layout using <variable> tag.
<? xml version = "1.0" encoding = "utf-8" ?> < layout xmlns:bind = "http://schemas.android.com/apk/res/android" > < data > < variable name = "post" type = "info.androidhive.databinding.model.Post" /> </ data > < android.support.constraint.ConstraintLayout xmlns:android = "http://schemas.android.com/apk/res/android" xmlns:app = "http://schemas.android.com/apk/res-auto" android:layout_width = "match_parent" android:layout_height = "wrap_content" > < ImageView android:id = "@+id/thumbnail" android:layout_width = "0dp" android:layout_height = "0dp" android:scaleType = "centerCrop" bind:imageUrl = "@{post.imageUrl}" app:layout_constraintBottom_toBottomOf = "parent" app:layout_constraintDimensionRatio = "H,1:1" app:layout_constraintLeft_toLeftOf = "parent" app:layout_constraintRight_toRightOf = "parent" app:layout_constraintTop_toTopOf = "parent" /> </ android.support.constraint.ConstraintLayout > </ layout > |
- As the layout name is post_row_item.xml, the generated binding class will be PostRowItemBinding.
- In onCreateViewHolder() method, post_row_item layout is inflated with the help of PostRowItemBinding class.
- holder.binding.setPost() binds the Post model to each row.
package info.androidhive.databinding.view; import android.databinding.DataBindingUtil; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import java.util.List; import info.androidhive.databinding.R; import info.androidhive.databinding.databinding.PostRowItemBinding; import info.androidhive.databinding.model.Post; public class PostsAdapter extends RecyclerView.Adapter<PostsAdapter.MyViewHolder> { private List<Post> postList; private LayoutInflater layoutInflater; private PostsAdapterListener listener; public class MyViewHolder extends RecyclerView.ViewHolder { private final PostRowItemBinding binding; public MyViewHolder( final PostRowItemBinding itemBinding) { super (itemBinding.getRoot()); this .binding = itemBinding; } } public PostsAdapter(List<Post> postList, PostsAdapterListener listener) { this .postList = postList; this .listener = listener; } @Override public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (layoutInflater == null ) { layoutInflater = LayoutInflater.from(parent.getContext()); } PostRowItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.post_row_item, parent, false ); return new MyViewHolder(binding); } @Override public void onBindViewHolder(MyViewHolder holder, final int position) { holder.binding.setPost(postList.get(position)); holder.binding.thumbnail.setOnClickListener( new View.OnClickListener() { @Override public void onClick(View v) { if (listener != null ) { listener.onPostClicked(postList.get(position)); } } }); } @Override public int getItemCount() { return postList.size(); } public interface PostsAdapterListener { void onPostClicked(Post post); } } |
2.2 Building the Profile Screen
Now we have all the files in place. Let’s start building the main interface.12. Open the layout files of main activity i.e activity_main.xml and content_main.xml and enable data-binding by adding <layout>, <data> and <variable> tags.
<? xml version = "1.0" encoding = "utf-8" ?> < layout xmlns:bind = "http://schemas.android.com/apk/res/android" > < data > < variable name = "user" type = "info.androidhive.databinding.model.User" /> </ data > < android.support.design.widget.CoordinatorLayout xmlns:app = "http://schemas.android.com/apk/res-auto" 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 = ".view.MainActivity" > < android.support.design.widget.AppBarLayout android:layout_width = "match_parent" android:layout_height = "wrap_content" app:elevation = "0dp" android:theme = "@style/AppTheme.AppBarOverlay" > < android.support.v7.widget.Toolbar android:id = "@+id/toolbar" android:layout_width = "match_parent" android:layout_height = "?attr/actionBarSize" android:background = "?attr/colorPrimary" app:popupTheme = "@style/AppTheme.PopupOverlay" /> </ android.support.design.widget.AppBarLayout > < include android:id = "@+id/content" layout = "@layout/content_main" bind:user = "@{user}" /> </ android.support.design.widget.CoordinatorLayout > </ layout > |
<? xml version = "1.0" encoding = "utf-8" ?> < layout xmlns:bind = "http://schemas.android.com/apk/res/android" > < data > < import type = "info.androidhive.databinding.utils.BindingUtils" /> < variable name = "user" type = "info.androidhive.databinding.model.User" /> < variable name = "handlers" type = "info.androidhive.databinding.view.MainActivity.MyClickHandlers" /> </ data > < android.support.v4.widget.NestedScrollView 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" android:layout_width = "match_parent" android:layout_height = "match_parent" app:layout_behavior = "@string/appbar_scrolling_view_behavior" > < LinearLayout android:layout_width = "match_parent" android:layout_height = "match_parent" android:focusableInTouchMode = "true" android:orientation = "vertical" tools:context = ".view.MainActivity" tools:showIn = "@layout/activity_main" > < LinearLayout android:layout_width = "match_parent" android:layout_height = "wrap_content" android:background = "@color/colorPrimary" android:orientation = "vertical" android:paddingBottom = "@dimen/activity_margin" android:paddingTop = "@dimen/activity_margin" > < RelativeLayout android:layout_width = "@dimen/profile_image" android:layout_height = "@dimen/profile_image" android:layout_gravity = "center_horizontal" > < ImageView android:id = "@+id/profile_image" android:layout_width = "@dimen/profile_image" android:layout_height = "@dimen/profile_image" android:layout_centerHorizontal = "true" android:onLongClick = "@{handlers::onProfileImageLongPressed}" bind:profileImage = "@{user.profileImage}" /> < android.support.design.widget.FloatingActionButton android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_alignParentBottom = "true" android:layout_alignParentRight = "true" android:onClick = "@{handlers::onProfileFabClicked}" android:src = "@drawable/ic_add_white_24dp" app:fabCustomSize = "@dimen/fab_profile" /> </ RelativeLayout > < TextView android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_gravity = "center_horizontal" android:layout_marginTop = "@dimen/dimen_8dp" android:fontFamily = "sans-serif" android:letterSpacing = "0.1" android:text = "@{user.name}" android:textColor = "@android:color/white" android:textSize = "@dimen/profile_name" android:textStyle = "bold" /> < TextView android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_gravity = "center_horizontal" android:fontFamily = "sans-serif" android:letterSpacing = "0.1" android:text = "@{user.about}" android:textColor = "@android:color/white" android:textSize = "@dimen/profile_about" /> </ LinearLayout > < LinearLayout android:layout_width = "match_parent" android:layout_height = "wrap_content" android:layout_marginBottom = "@dimen/activity_margin" android:layout_marginTop = "@dimen/fab_margin" android:orientation = "horizontal" android:weightSum = "3" > < LinearLayout android:layout_width = "0dp" android:layout_height = "wrap_content" android:layout_weight = "1" android:gravity = "center_horizontal" android:onClick = "@{handlers::onPostsClicked}" android:orientation = "vertical" > < TextView android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:fontFamily = "sans-serif-condensed" android:text = "@{BindingUtils.convertToSuffix(user.numberOfPosts)}" android:textColor = "@color/profile_meta" android:textSize = "24dp" android:textStyle = "normal" /> < TextView android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:text = "@string/posts" android:textSize = "@dimen/profile_meta_label" /> </ LinearLayout > < LinearLayout android:layout_width = "0dp" android:layout_height = "wrap_content" android:layout_weight = "1" android:gravity = "center_horizontal" android:onClick = "@{handlers::onFollowersClicked}" android:orientation = "vertical" > < TextView android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:fontFamily = "sans-serif-condensed" android:text = "@{BindingUtils.convertToSuffix(user.numberOfFollowers)}" android:textColor = "@color/profile_meta" android:textSize = "24dp" /> < TextView android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:text = "@string/followers" android:textSize = "@dimen/profile_meta_label" /> </ LinearLayout > < LinearLayout android:layout_width = "0dp" android:layout_height = "wrap_content" android:layout_weight = "1" android:gravity = "center_horizontal" android:onClick = "@{handlers::onFollowingClicked}" android:orientation = "vertical" > < TextView android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:fontFamily = "sans-serif-condensed" android:text = "@{BindingUtils.convertToSuffix(user.numberOfFollowing)}" android:textColor = "@color/profile_meta" android:textSize = "@dimen/profile_meta" /> < TextView android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:text = "@string/following" android:textSize = "@dimen/profile_meta_label" /> </ LinearLayout > </ LinearLayout > < android.support.v7.widget.RecyclerView android:id = "@+id/recycler_view" android:layout_width = "match_parent" android:layout_height = "match_parent" /> </ LinearLayout > </ android.support.v4.widget.NestedScrollView > </ layout > |
- As the main activity layout name is activity_main, the generated binding class will be ActivityMainBinding.
- renderProfile() renders the user information such as name, description, posts, followers and following count.
- initRecyclerView() initializes the RecyclerView with sample images data.
- MyClickHandlers handles the click events of UI elements. Here, all the binding of click events is done via xml layout only. We don’t explicitly assign anything from activity code.
package info.androidhive.databinding.view; import android.content.Context; import android.content.res.Resources; import android.databinding.DataBindingUtil; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.DefaultItemAnimator; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; import android.util.TypedValue; import android.view.View; import android.widget.Toast; import java.util.ArrayList; import info.androidhive.databinding.R; import info.androidhive.databinding.databinding.ActivityMainBinding; import info.androidhive.databinding.model.Post; import info.androidhive.databinding.model.User; import info.androidhive.databinding.utils.GridSpacingItemDecoration; public class MainActivity extends AppCompatActivity implements PostsAdapter.PostsAdapterListener { private MyClickHandlers handlers; private PostsAdapter mAdapter; private RecyclerView recyclerView; private ActivityMainBinding binding; private User user; @Override protected void onCreate(Bundle savedInstanceState) { super .onCreate(savedInstanceState); binding = DataBindingUtil.setContentView( this , R.layout.activity_main); Toolbar toolbar = binding.toolbar; setSupportActionBar(toolbar); getSupportActionBar().setTitle(R.string.toolbar_profile); getSupportActionBar().setDisplayHomeAsUpEnabled( true ); handlers = new MyClickHandlers( this ); renderProfile(); initRecyclerView(); } /** * Renders RecyclerView with Grid Images in 3 columns */ private void initRecyclerView() { recyclerView = binding.content.recyclerView; recyclerView.setLayoutManager( new GridLayoutManager( this , 3 )); recyclerView.addItemDecoration( new GridSpacingItemDecoration( 3 , dpToPx( 4 ), true )); recyclerView.setItemAnimator( new DefaultItemAnimator()); recyclerView.setNestedScrollingEnabled( false ); mAdapter = new PostsAdapter(getPosts(), this ); recyclerView.setAdapter(mAdapter); } /** * Renders user profile data */ private void renderProfile() { user = new User(); user.setName( "David Attenborough" ); user.setEmail( "david@natgeo.com" ); user.setProfileImage( "https://api.androidhive.info/images/nature/david.jpg" ); user.setAbout( "Naturalist" ); // ObservableField doesn't have setter method, instead will // be called using set() method user.numberOfPosts.set(3400L); user.numberOfFollowers.set(3050890L); user.numberOfFollowing.set(150L); // display user binding.setUser(user); // assign click handlers binding.content.setHandlers(handlers); } private ArrayList<Post> getPosts() { ArrayList<Post> posts = new ArrayList<>(); for ( int i = 1 ; i < 10 ; i++) { Post post = new Post(); post.setImageUrl( "https://api.androidhive.info/images/nature/" + i + ".jpg" ); posts.add(post); } return posts; } @Override public void onPostClicked(Post post) { Toast.makeText(getApplicationContext(), "Post clicked! " + post.getImageUrl(), Toast.LENGTH_SHORT).show(); } public class MyClickHandlers { Context context; public MyClickHandlers(Context context) { this .context = context; } /** * Demonstrating updating bind data * Profile name, number of posts and profile image * will be updated on Fab click */ public void onProfileFabClicked(View view) { user.setName( "Sir David Attenborough" ); user.setProfileImage( "https://api.androidhive.info/images/nature/david1.jpg" ); // updating ObservableField user.numberOfPosts.set(5500L); user.numberOfFollowers.set(5050890L); user.numberOfFollowing.set(180L); } public boolean onProfileImageLongPressed(View view) { Toast.makeText(getApplicationContext(), "Profile image long pressed!" , Toast.LENGTH_LONG).show(); return false ; } public void onFollowersClicked(View view) { Toast.makeText(context, "Followers is clicked!" , Toast.LENGTH_SHORT).show(); } public void onFollowingClicked(View view) { Toast.makeText(context, "Following is clicked!" , Toast.LENGTH_SHORT).show(); } public void onPostsClicked(View view) { Toast.makeText(context, "Posts is clicked!" , Toast.LENGTH_SHORT).show(); } } /** * Converting dp to pixel */ private int dpToPx( int dp) { Resources r = getResources(); return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.getDisplayMetrics())); } } |

Comments
Post a Comment