Android DataBinding in RecyclerView - Profile Screen
source link: https://www.androidhive.info/android-databinding-in-recyclerview-profile-screen/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
In my previous article, we have learned the Basics of DataBinding. Today, we are going to put the basics into action by implementing a profile screen using data-binding concepts. The profile screen will have profile details at the top and the below section will have post images in grid format. The grid will be achieved using a RecyclerView implementing the data-binding in adapter class.
Using DataBinding in an adapter class, keeps the code to very minimal as lot of things will be taken care in the layout itself.
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'
}
3. Add INTERNET permission in AndroidManifest.xml as the images needs to be loaded from an URL.
<
uses-permission
android:name
=
"android.permission.INTERNET"
/>
4. Download res.zip and add to your projects res folder. These drawable folders contains plus icon necessary for FAB.
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
>
6. Create three packages named model, utils and view. Once created, move the MainActivity to view package.
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;
}
}
8. Create another class named Post.java under model package. This model class provides data to RecyclerView.
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;
}
}
9. Under utils package, create two classes named BindingUtils.java and GridSpacingItemDecoration.java
- 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
));
}
}
GridSpacingItemDecoration provides spacing between RecyclerView grid elements.
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"
?>
<
data
>
<
variable
name
=
"post"
type
=
"info.androidhive.databinding.model.Post"
/>
</
data
>
<
android.support.constraint.ConstraintLayout
xmlns:android
=
"http://schemas.android.com/apk/res/android"
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
>
11. Create a class named PostsAdapter.java under view package.
- 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"
?>
<
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"
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"
?>
<
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"
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
>
13. Finally open MainActivity.java and do the below modifications.
- 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.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();
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"
);
// 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()));
}
}
Run and test the app once. Make sure device is having internet connection as images will be downloaded from network.
If you have any queries, please post them in below section.
Hi there! I am Founder at androidhive and programming enthusiast. My skills includes Android, iOS, PHP, Ruby on Rails and lot more. If you have any idea that you would want me to develop? Let’s talk: [email protected]
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK