airbnb / epoxy

Epoxy is an Android library for building complex screens in a RecyclerView
https://goo.gl/eIK82p
Apache License 2.0
8.5k stars 733 forks source link

EpoxyController.setData() vs. EpoxyController.requestModelBuild() #257

Closed fredo- closed 7 years ago

fredo- commented 7 years ago

Hey guys love the library, great work!

I'm having some problems updating views once they're set up though.

I'm trying to set up a list and change its contents based on state on the UI. I have my callbacks and models working properly, but when I call setData() and the ModelGroups are built and the Models are created, the list doesn't update with the new data.

We tried using the pattern seen in the sample app with the colors in the carousels, and the pattern used in the Epoxy Controller section of the Wiki (this second one does not work and throws an exception that we should be using setData() instead)

Here's the controller.buildModels():

   @Override
    protected void buildModels(SearchViewModel.State state) {
        String cityName = state.getCity().getName();
        header.title("Shows this week in")
                .caption(cityName)
                .callback(callbacks);
        add(header);

        ArrayList<CarouselEventModel_> carouselList = getEventModels(state.getCarouselEventList());

        if (state.getCarouselEventList().size() > 0) {
            Timber.e("carousel size = %s", state.getCarouselEventList().size());
            add(new CarouselModelGroup(new CarouselData("Most Popular in New York", 0, carouselList))
            .id(1));
        }

        if (state.getVerticalEventList().size() > 0) {
            add(new EverythingFilterModelGroup(state.getGenreList(), 20, state.isGenreDrawerVisible(), callbacks)
            .id(2));
            ArrayList<FilterPillModel_> filterButtons = getFilterButtonModels(state.getGenreList(), callbacks);
            if (state.isGenreDrawerVisible()) {
                add(new FilterOptionsModel_()
                        .reset()
                        .id(100)
                        .models(filterButtons));
            }
            ArrayList<ListEventModel_> listList = getListModels(state.getVerticalEventList());
            add(listModel.models(listList));
            add(footerModel.title("That's all!")
                    .notifyText("Notify me when there are new shows to check out:")
                    .feedbackText("Would you mind giving us some feedback?")
                    .yesBtnTxt("Ok, Sure!")
                    .noBtnTxt("Not Really"));
        }
    }

This is how I set up the Models for the List that we're trying to update:

    @NonNull
    private ArrayList<ListEventModel_> getListModels(List<Event> events) {
        ArrayList<ListEventModel_> list = new ArrayList<>();
        for (Event event : events) {
            String dateStr = event.getDates().getStart().getLocalTime();
            String artist = "";
            if (event.get_embedded().getAttractions() != null) {
                artist = event.get_embedded().getAttractions().get(0).getName();
            }
            ListEventModel_ model = new ListEventModel_()
                    .id(event.getId())
                    .imageUrl(getLargeImage(event.getImages()).getUrl())
                    .artistString((artist != null) ? artist : "")
                    .dateTimeString((dateStr != null) ? getTimeString(event) : "")
                    .venueString(event.get_embedded().getVenues().get(0).getName())
                    .priceString("$" + ((event.getPriceRanges() != null)
                            ? event.getPriceRanges().get(0).getMin() : "0"))
                    .clickListener(new OnModelClickListener() {
                        @Override
                        public void onClick(EpoxyModel model, Object parentView, View clickedView, int position) {
                            callbacks.onListEventClicked(position);
                        }
                    });

            list.add(model);
        }
        return list;
    }

The first time we call setModels() everything is drawn correctly, but when we create a different list (with fewer ListEventModels) the views do not change. From the sample app and the documentation, the views in listModel should update when we pass to it a different list of ListEventModels but it's not updating.

Here's the ListModel declaration just in case:

@EpoxyModelClass
public abstract class ListModel extends EpoxyModelWithView<RecyclerView> {
    @EpoxyAttribute
    List<? extends EpoxyModel<?>> models;

    @Override
    public boolean shouldSaveViewState() {
        return true;
    }

    @Override
    protected RecyclerView buildView(ViewGroup parent) {
        RecyclerView recView = new RecyclerView(parent.getContext(), null);
        RecyclerView.LayoutManager layoutManager =
                new LinearLayoutManager(parent.getContext(), LinearLayoutManager.VERTICAL, false);
        recView.setLayoutManager(layoutManager);
        SimpleEpoxyController controller = new SimpleEpoxyController();
        controller.setSpanCount(1);
        recView.setAdapter(controller.getAdapter());
        controller.setModels(models);
        return recView;
    }
}
elihart commented 7 years ago

you're setting the models in buildView, not bind.

buildView should just create the view, bind is where you update data on the view

elihart commented 7 years ago

Look at CarouselModel more carefully in the sample.

I've also been meaning to write up a Carousel sample in the wiki

fredo- commented 7 years ago

Got it working, had to create new class EventList that extends RecyclerView just like the sample app uses Carousel, where the state seems to be being saved.

Here's the updated ListModel:

@EpoxyModelClass
public abstract class ListModel extends EpoxyModelWithView<EventList> {
    @EpoxyAttribute
    List<? extends EpoxyModel<?>> models;

    @Override
    public boolean shouldSaveViewState() {
        return false;
    }

    @Override
    protected EventList buildView(ViewGroup parent) {
        EventList eventList = new EventList(parent.getContext(), null);
        return eventList;
    }

    @Override
    public void bind(EventList eventList) {
        eventList.setModels(models);
    }
}

And here's EventList:

public class EventList extends RecyclerView {
    private final LinearLayoutManager layoutManager;
    private SimpleEpoxyController controller;

    public EventList(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);

        setHasFixedSize(true);
        layoutManager = new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false);
        setLayoutManager(layoutManager);
    }

    public void setModels(List<? extends EpoxyModel<?>> models){
        if (controller == null) {
            controller = new SimpleEpoxyController();
            controller.setSpanCount(1);
            setAdapter(controller.getAdapter());
        }
        controller.setModels(models);
    }
}

Thanks @elihart!!!

elihart commented 7 years ago

nice :) I'd highly recommend also clearing the models in unbind btw to clean up resources. the sample app shows that