7

Android DataBinding in RecyclerView - Profile Screen

 2 years ago
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.
neoserver,ios ssh client

Architecture Components

Android DataBinding in RecyclerView – Profile Screen

By Ravi Tamada September 25, 2018 46 Comments

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.

DEMO

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

strings.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>
dimens.xml
<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>
colors.xml
<?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.
User.java
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.

Post.java
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.
BindingUtils.java
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.

GridSpacingItemDecoration.java
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.
post_row_item.xml
<?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.
PostsAdapter.java
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.

activity_main.xml
<?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>
content_main.xml
<?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.
MainActivity.java
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("[email protected]");
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");
// 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]


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK