Creating a News Feed list using Facebook Litho’s Sections API

In my previous post I have explained the basics of Litho. In this post let’s create a complex list of items using the Sections API in Litho.

With the basics in mind let’s create a complex RecyclerView showing some fake news feeds with different view types, nested horizontal scrolling, pull-to-refresh and lazy loading 15 items in a batch.

Library dependencies

I am using Android Studio 3.0 and Litho version 0.11.0. Here are the dependencies to be added.

Litho dependencies:

implementation 'com.facebook.litho:litho-core:0.12.0'
implementation 'com.facebook.litho:litho-widget:0.12.0'
compileOnly 'com.facebook.litho:litho-annotations:0.12.0'
annotationProcessor 'com.facebook.litho:litho-processor:0.12.0'

Sections dependencies:

implementation 'com.facebook.litho:litho-sections-core:0.12.0'
implementation 'com.facebook.litho:litho-sections-widget:0.12.0'
compileOnly 'com.facebook.litho:litho-sections-annotations:0.12.0'
annotationProcessor 'com.facebook.litho:litho-sections-processor:0.12.0'

SoLoader:

// Since Litho uses Yoga, to load native code it uses SoLoader
implementation 'com.facebook.soloader:soloader:0.2.0'

We will use the new Sections API to create the list.

Sections API

According to the official docs, Sections are built on top of Litho to provide a declarative and composable API for writing highly-optimized list surfaces. While Litho Components are used for displaying pieces of UI, Sections are a way of structuring the data and translating it into Litho Components.

So that means each data item that backs the UI is represented by a Section which is translated to the UI using Litho Components.

Litho provides two built-in sections namely


OK. Now let’s start building our news feed app.

Data Types

Based on the data we will be rendering three types of items in the list:

  1. News feed with single image
  2. News feed with multiple scrollable images
  3. Ad feed showing an advertisement

Feed Data

Before starting let’s see how our feeds data will look like. I have created two model classes for the feeds. For the purpose of this tutorial I have used static values instead of calling any API for news feed.

This is how the model classes look like:

1. FeedData.java – describes the title, description, an array of photos, and other data of the news feed.

public class FeedData {
  public String title;
  public String description;
  public int[] photos;

  public boolean like;

  public int likeCount;
  public int commentCount;
}

2. Feed.java – describes the type of feed, the id of the feed and the feed data itself.

public class Feed {

  public enum FeedType {NEWS_FEED, PHOTO_FEED, AD_FEED}

  public int id;
  public FeedType type;
  public FeedData data;

  @Override
  public String toString() {
    return "Feed [ id: " + id + ", type: " + type + "]";
  }
}

Here the enum describes what type of feed it is, whether it is a news feed with single image, or a news feed with nested horizontal scrolling images, or a advertisement feed.

Next let us declare the UI for each item of the feed. This will show how our news feed items should look like.

Let’s display each item inside a Card component. This card component will show a title, an image or a list of horizontally scrolling images, and a description.

Remember that we will create a Spec class and Litho generates the actual code for the component. We do this by annotating the Spec class with relevant annotation. For our Card component, since it is a layout, we use the @LayoutSpec annotation.

This is how our LayoutSpec class for the Card looks like:

CardElementSpec.java

@LayoutSpec
public class CardElementSpec {

  @OnCreateLayout
  static Component onCreateLayout(
    ComponentContext c,
    @Prop int id,
    @Prop Feed.FeedType type,
    @Prop String title,
    @Prop String description,
    @Prop int[] imageRes) {

    Component titleComp = Text.create(c, 0, R.style.TextAppearance_AppCompat_Title)
      .text(title)
      .marginDip(YogaEdge.TOP, 16)
      .marginDip(YogaEdge.BOTTOM, 8)
      .marginDip(YogaEdge.HORIZONTAL, 8)
      .typeface(Typeface.DEFAULT_BOLD)
      .textColor(Color.BLACK)
      .build();

    Component descComp = Text.create(c)
      .text(description)
      .maxLines(4)
      .ellipsize(TextUtils.TruncateAt.END)
      .textSizeSp(17)
      .paddingDip(YogaEdge.BOTTOM, 8)
      .marginDip(YogaEdge.VERTICAL, 16)
      .marginDip(YogaEdge.HORIZONTAL, 8)
      .build();

    return Column.create(c)
      .child(titleComp)
      .child((type == Feed.FeedType.NEWS_FEED || type == Feed.FeedType.AD_FEED) ?
        getImageComp(c, imageRes[0]) : getRecyclerComp(c, imageRes))
      .child(descComp)
      .build();

  }

  private static Component getImageComp(ComponentContext c, int imageRes) {
    return Image.create(c)
      .drawableRes(imageRes)
      .widthPercent(100)
      .heightDip(200)
      .scaleType(ImageView.ScaleType.CENTER_CROP)
      .build();
  }

  private static Component getRecyclerComp(ComponentContext c, int[] imageRes) {
    return RecyclerCollectionComponent.create(c)
      .heightDip(200)
      .itemDecoration(new ImageItemDecoration())
      .recyclerConfiguration(new ListRecyclerConfiguration<>(LinearLayoutManager.HORIZONTAL, false))
      .section(
        DataDiffSection.<Integer>create(new SectionContext(c))
          .data(CardElementSpec.getImageArray(imageRes))
          .renderEventHandler(CardElement.onRenderImages(c))
          .build()
      )
      .build();
  }

  @OnEvent(RenderEvent.class)
  static RenderInfo onRenderImages(final ComponentContext c, @FromEvent Integer model) {
    return ComponentRenderInfo.create()
      .component(
        Image.create(c)
          .drawableRes(model)
          .widthPercent(100)
          .heightDip(200)
          .scaleType(ImageView.ScaleType.CENTER_CROP)
          .build()
      )
      .build();
  }

  private static List<Integer> getImageArray(int[] imageRes) {
    List<Integer> images = new ArrayList<>(imageRes.length);
    for (int image : imageRes) {
      images.add(image);
    }
    return images;
  }

}

In the onCreateLayout function we create two Text Components for displaying the title and description of the feed.

Finally we return the layout using a Column component to wrap the title, description and images and display them vertically. Note how we display images here. We check the type of the feed and if it is of type NEWS_FEED or AD_FEED then we call getImageComp() function to display a single image component. Else it is of type PHOTO_FEED and so we call getRecyclerComp()function passing the array of images.

The  getRecyclerComp() function returns a RecyclerCollectionComponent. This Component renders a RecyclerView backed by the Sections data. Here recyclerConfiguration() is used to describe the orientation of the items which is horizontal. Next we provide the images using the DataDiffSection.

DataDiffSection is a built in class part of Sections API for backing the items with data. It is also used to find the changes in a list of data using DiffUtils in a background thread. We can use this to render a horizontally scrolling list of images.

DataDiffSection requires two methods namely data() and renderEventHandler(). The data() is used to pass the data i.e., an array of images. The renderEventHandler() is used to describe the UI that needs to be rendered for each item.

The renderEventHandler() takes a function annotated with RenderEvent as parameter. Take a look at the onRenderImages() function which is annotated with RenderEvent . It takes two params. One is the context and another one is the data of the item which is the image here. This is got from the RenderEvent using the @FromEvent annotation.

So that’s it for our Card Component.

And below is how our Card component will look like for single image news feed and multi-image news feed.

 

So we have declared how our UI will look for each news feed.

Next we are going to display all the three types of cards in a list. For this we need a RecyclerCollectionComponent. Let’s create it in our MainActivity.

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);

  final ComponentContext c = new ComponentContext(this);
  final Component component = RecyclerCollectionComponent.create(c)
    .section(ListSection.create(new SectionContext(c)).build())
    .build();

  final LithoView lithoView = LithoView.create(c, component);
  setContentView(lithoView);

}

Here ListSection is the Section that we will create to display the news feeds. We will create a Spec class called ListSectionSpec. We will create the news feed such that the news feed data is fetched in batches of 15. So when we reach the last item we will show a progress bar and fetch the next batch of items.

Since we need to display both the news feed and progress bar at the end, let’s annotate the ListSectionSpec class with GroupSectionSpec. This class is used to describe different sections of a list. Let’s annotate the ListSectionSpec with GroupSectionSpec.

There are few methods that we need to override in this class.

First let’s override a method annotated with @OnCreateInitialState. Here we will describe the initial state of the list by setting the initial data and other states. For our case, we need initial batch of news feed data. So let’s have two integers start and count that denote the starting index and the size of the batch of data respectively. Feeds are denoted by a List List<Feed>. Finally a boolean isFetching to denote whether the fetching of next batch is going on now or not, which is false initially.

@OnCreateInitialState
static void createInitialState(
  final SectionContext c,
  StateValue<List<Feed>> feeds,
  StateValue<Integer> start,
  StateValue<Integer> count,
  StateValue<Boolean> isFetching
) {
  start.set(0);
  count.set(15);
  feeds.set(new DataService().getData(0, 15).feeds);
  isFetching.set(false);
}

Here each state variable is wrapped with StateValue which provides get and set methods to read and write the new states. Initially the starting index will be 0 and we will fetch a batch of 15 news feeds. So the count will be 15.

I have created a class called DataService where I will fetch the news feed data using the start index of the batch and count, and return them.

Now we need to declare the children that need to be rendered for each item. So let’s override a method annotated with @OnCreateChildren.

@OnCreateChildren
static Children onCreateChildren(
  final SectionContext c,
  @State List<Feed> feeds
) {
  Children.Builder builder = Children.create();
  for (int i = 0; i < feeds.size(); i++) {
    Feed model = feeds.get(i);
    builder
      .child(
        SingleComponentSection.create(c)
          .key("" + model.id)
          .component(
            Card.create(c)
              .content(
                CardElement.create(c)
                  .id(model.id)
                  .type(model.type)
                  .title(model.data.title)
                  .description(model.data.description)
                  .imageRes(model.data.photos)
                  .build()
              )
              .cardBackgroundColor(Color.WHITE)
              .elevationDip(6)
              .build()
          )
      );
  }
  builder.child(
    SingleComponentSection.create(c)
      .component(ProgressLayout.create(c))
      .build()
  );
  return builder.build();
}

Here the children of the GroupSectionSpec is represented by the Children class inside which we pass each child element.

The Children class requires Section as it’s child. So we use the SingleComponentSection to represent each item. We must provide unique key for each Section to tell that each Section is different from others.

Inside the SingleComponentSection I am creating a Card component using Card.create() passing in our CardElement Component. We pass the news feed details to the CardElement component as props.

After adding all the feed items, we pass a ProgressBar as the last item of the list. We create a LayoutSpec for ProgressBar and wrap it with SingleComponentSection and add it as the child to the Children class.

Here is the LayoutSpec for ProgressBar.

@LayoutSpec
public class ProgressLayoutSpec {

  @OnCreateLayout
  static Component onCreateLayout(ComponentContext c) {
    return Column.create(c)
      .child(
        Progress.create(c)
          .widthDip(40)
          .heightDip(40)
          .alignSelf(YogaAlign.CENTER)
          .build()
      )
      .build();
  }

}

Next we can initialize our data fetching services in a method annotated with @OnCreateService.

@OnCreateService
static DataService onCreateService(
  final SectionContext c,
  @State List<Feed> feeds,
  @State int start,
  @State int count
) {
  return new DataService();
}

DataService is where the actual data fetching happens. Let’s look at its code shortly. Before that we need to create two more methods annotated with @OnBindService and @OnUnbindService.

The method annotated with @OnBindService will be called once the service is ready for use. Here we can start a network request or set any listeners.

The method annotated with @OnUnbindService is called after fetching is completed. Here we can reset any listeners and clear unwanted data we previously set in @OnBindService .

I am going to set a listener for identifying when data fetching is completed. These are the methods.

@OnBindService
static void onBindService(final SectionContext c, final DataService service) {
  service.registerLoadingEvent(ListSection.onDataLoaded(c));
}

@OnUnbindService
static void onUnbindService(final SectionContext c, final DataService service) {
  service.unregisterLoadingEvent();
}

Now let’s look at the DataService class. For the purpose of the blogpost, I am not going to call any API for fetching news feeds. Instead I am using some static texts and images for news feed.

Inside my DataService class:

private EventHandler<FeedModel> dataModelEventHandler;

private Random r = new Random();

public void registerLoadingEvent(EventHandler<FeedModel> dataModelEventHandler) {
  this.dataModelEventHandler = dataModelEventHandler;
}

public void unregisterLoadingEvent() {
  this.dataModelEventHandler = null;
}

public void fetch(final int start, final int count) {
  new Handler().postDelayed(new Runnable() {
    @Override
    public void run() {
      FeedModel feedModel = getData(start, count);
      dataModelEventHandler.dispatchEvent(feedModel);
    }
  }, 2000);
}

public void refetch(final int start, final int count) {
  new Handler().postDelayed(new Runnable() {
    @Override
    public void run() {
      FeedModel feedModel = getData(start, count);
      dataModelEventHandler.dispatchEvent(feedModel);
    }
  }, 2000);

}

public FeedModel getData(int start, int count) {
  FeedModel feedModel = new FeedModel();
  feedModel.feeds = new ArrayList<>(count);
  for (int i = start; i < start + count; i++) {
    Feed feed = generateFeed(i);
    feedModel.feeds.add(feed);
  }
  return feedModel;
}

Take a look at the fetch and refetch methods. Here I am mocking the network latency using a delayed Handler. Inside it I am calling getData() method which returns a class called FeedModel that I created.

@Event
public class FeedModel {
  public List<Feed> feeds;
}

This FeedModel class is annotated with @Event to notify the listener when the data fetching is completed.

Note that we have a variable dataModelEventHandler of type EventHandler. This is used to dispatch certain events that we can handle. Here since we need to trigger when data has been fetched, we will call dataModelEventHandler.dispatchEvent(feedModel) to notify that the data is fetched successfully.

And inside our ListSectionSpec class we override the listener callback as follows.

@OnEvent(FeedModel.class)
static void onDataLoaded(final SectionContext c, @FromEvent List<Feed> feeds) {
  ListSection.updateData(c, feeds);
  ListSection.setFetching(c, false);
  SectionLifecycle.dispatchLoadingEvent(c, false, LoadingState.SUCCEEDED, null);
}

In this method we get the fetched list of feeds. We then add these new feeds to existing feeds and set the boolean isFetching to false. We also notify that the loading event was completed successfully using the SectionLifecycle method. Since we need to change the current state of the feeds list by adding new feeds we call ListSection.updateData(c, feeds);. Similarly for changing isFetching to false we call ListSection.setFetching(c, false);.

Here is the code for updateData() method.

@OnUpdateState
static void updateData(
  final StateValue<List<Feed>> feeds,
  final StateValue<Integer> start,
  @Param List<Feed> newFeeds
) {
  if (start.get() == 0) {
    feeds.set(newFeeds);
  } else {
    List<Feed> feeds1 = new ArrayList<>();
    feeds1.addAll(feeds.get());
    feeds1.addAll(newFeeds);
    feeds.set(feeds1);
  }
}

Here we check if the value of start is 0. If it is, it means that this is the initial data. Otherwise it is the next batch of feed data. Note that the values of state variables are immutable. So we just set the newFeeds to feeds list if start is 0. Otherwise, we will create a new list and add all the items in newFeeds and feeds to it.

Below is the code for updating isFetching.

@OnUpdateState
static void setFetching(final StateValue<Boolean> isFetching, @Param boolean fetch) {
  isFetching.set(fetch);
}

So far we have completed how the news feeds are displayed. Next let us see how to trigger the fetching of next batch of feeds when we reach the end of scrolling.

So to listen for the scrolling, Sections API provides an annotation @OnViewportChanged . The method annotated with it will be called whenever the list is scrolled. The method takes parameters for the first visible position in the list, last visible position, total count of the items displayed by the list, etc. We can also access our state variables by passing them as parameters. Below is my code.

@OnViewportChanged
static void onViewportChanged(
  SectionContext c,
  int firstVisiblePosition,
  int lastVisiblePosition,
  int firstFullyVisibleIndex,
  int lastFullyVisibleIndex,
  int totalCount,
  DataService service,
  @State List<Feed> feeds,
  @State int start,
  @State int count,
  @State boolean isFetching
) {
  if (totalCount == feeds.size() && !isFetching) {
    ListSection.setFetching(c, true);
    ListSection.updateStartParam(c, feeds.size());
    service.fetch(feeds.size(), count);
  }
}

Here I am checking if we reached the list end. And also checking if fetching is not started already. If condition is true, then it means we are going to start fetching next batch. So we toggle the fetching state by making isFetching as true and also update the start parameter to the current feed lists size (i.e., if initially the feed list size is 15, the next value for start becomes 15). Finally we call the service to fetch the next 15 items.

When we run the app, this is how lazy loading looks like:

lazy-loading-recyclerview

Next let’s see how to implement Pull-To-Refresh. This is very easy. In your RecyclerCollectionComponent there is an option to disable PTR using disablePTR(true). If you didn’t mention it, then by default PTR will be enabled.

To receive PTR event whenever user drags, let’s create a method annotated with @OnRefresh in our ListSectionSpec class.

@OnRefresh
static void onRefresh(
  SectionContext c,
  DataService service,
  @State List<Feed> feeds,
  @State int start,
  @State int count
) {
  ListSection.updateStartParam(c, 0);
  service.refetch(0, 15);
}

Here whenever you pull to refresh, this method gets called. Here you can call the refetch method to refresh the feeds list.

So finally this is how the whole app looks like:

You can also enable click events for each items like I showed you in my previous post on Litho.

The source code of the app that I used for this post on github.

If you found this helpful please like and share it. If you have any questions feel free to comment below.

***

Advertisements

4 thoughts on “Creating a News Feed list using Facebook Litho’s Sections API

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s