/* * Copyright (c) Facebook, Inc. and its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.facebook.litho.widget; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.recyclerview.widget.ListUpdateCallback; import com.facebook.litho.Component; import com.facebook.litho.ComponentContext; import com.facebook.litho.ComponentsReporter; import com.facebook.litho.ComponentsSystrace; import com.facebook.litho.Diff; import java.util.ArrayList; import java.util.List; /** * An implementation of {@link ListUpdateCallback} that generates the relevant {@link Component}s * when an item is inserted/updated. * * <p>The user of this API is expected to provide a ComponentRenderer implementation to build a * Component from a generic model object. */ public class RecyclerBinderUpdateCallback<T> implements ListUpdateCallback { private static final String INCONSISTENT_SIZE = "RecyclerBinderUpdateCallback:InconsistentSize"; public interface ComponentRenderer<T> { RenderInfo render(T t, int idx); } public interface OperationExecutor { void executeOperations(@Nullable ComponentContext c, List<Operation> operations); } private final int mOldDataSize; private final List<? extends T> mPrevData; private final List<? extends T> mNextData; private final List<Operation> mOperations; private final List<ComponentContainer> mPlaceholders; private final List<Diff> mDataHolders; private final ComponentRenderer mComponentRenderer; private final OperationExecutor mOperationExecutor; public RecyclerBinderUpdateCallback( List<? extends T> prevData, List<? extends T> nextData, ComponentRenderer<T> componentRenderer, RecyclerBinder recyclerBinder) { this( prevData, nextData, componentRenderer, new RecyclerBinderOperationExecutor(recyclerBinder)); } public RecyclerBinderUpdateCallback( List<? extends T> prevData, List<? extends T> nextData, ComponentRenderer<T> componentRenderer, OperationExecutor operationExecutor) { mPrevData = prevData; mOldDataSize = prevData != null ? prevData.size() : 0; mNextData = nextData; mComponentRenderer = componentRenderer; mOperationExecutor = operationExecutor; mOperations = new ArrayList<>(); mPlaceholders = new ArrayList<>(); mDataHolders = new ArrayList<>(); for (int i = 0; i < mOldDataSize; i++) { mPlaceholders.add(new ComponentContainer(null, false)); mDataHolders.add(new Diff(mPrevData.get(i), null)); } } @Override public void onInserted(int position, int count) { final List<ComponentContainer> placeholders = new ArrayList<>(count); final List<Diff> dataHolders = new ArrayList<>(count); for (int i = 0; i < count; i++) { final int index = position + i; final ComponentContainer componentContainer = new ComponentContainer(null, true); mPlaceholders.add(index, componentContainer); placeholders.add(componentContainer); final Diff dataHolder = new Diff(null, null); mDataHolders.add(index, dataHolder); dataHolders.add(dataHolder); } mOperations.add(new Operation(Operation.INSERT, position, -1, placeholders, dataHolders)); } @Override public void onRemoved(int position, int count) { final List<Diff> dataHolders = new ArrayList<>(count); for (int i = 0; i < count; i++) { mPlaceholders.remove(position); final Diff dataHolder = mDataHolders.remove(position); dataHolders.add(dataHolder); } mOperations.add(new Operation(Operation.DELETE, position, count, null, dataHolders)); } @Override public void onMoved(int fromPosition, int toPosition) { final List<Diff> dataHolders = new ArrayList<>(1); final ComponentContainer placeholder = mPlaceholders.remove(fromPosition); mPlaceholders.add(toPosition, placeholder); final Diff dataHolder = mDataHolders.remove(fromPosition); dataHolders.add(dataHolder); mDataHolders.add(toPosition, dataHolder); mOperations.add(new Operation(Operation.MOVE, fromPosition, toPosition, null, dataHolders)); } @Override public void onChanged(int position, int count, Object payload) { final List<ComponentContainer> placeholders = new ArrayList<>(); final List<Diff> dataHolders = new ArrayList<>(count); for (int i = 0; i < count; i++) { final int index = position + i; final ComponentContainer placeholder = mPlaceholders.get(index); placeholder.mNeedsComputation = true; placeholders.add(placeholder); dataHolders.add(mDataHolders.get(index)); } mOperations.add(new Operation(Operation.UPDATE, position, -1, placeholders, dataHolders)); } public void applyChangeset(ComponentContext c) { final boolean isTracing = ComponentsSystrace.isTracing(); if (mNextData != null && mNextData.size() != mPlaceholders.size()) { logErrorForInconsistentSize(c); // Clear mPlaceholders and mOperations since they aren't matching with mNextData anymore. mOperations.clear(); mDataHolders.clear(); mPlaceholders.clear(); final List<Diff> prevDataHolders = new ArrayList<>(); for (int i = 0; i < mOldDataSize; i++) { prevDataHolders.add(new Diff(mPrevData.get(i), null)); } mDataHolders.addAll(prevDataHolders); mOperations.add(new Operation(Operation.DELETE, 0, mOldDataSize, null, prevDataHolders)); final int dataSize = mNextData.size(); final List<ComponentContainer> placeholders = new ArrayList<>(dataSize); final List<Diff> dataHolders = new ArrayList<>(dataSize); for (int i = 0; i < dataSize; i++) { final Object model = mNextData.get(i); if (isTracing) { ComponentsSystrace.beginSection("renderInfo:" + getModelName(model)); } final RenderInfo renderInfo = mComponentRenderer.render(model, i); if (isTracing) { ComponentsSystrace.endSection(); } placeholders.add(i, new ComponentContainer(renderInfo, false)); dataHolders.add(new Diff(null, model)); } mPlaceholders.addAll(placeholders); mDataHolders.addAll(dataHolders); mOperations.add(new Operation(Operation.INSERT, 0, -1, placeholders, dataHolders)); } else { for (int i = 0, size = mPlaceholders.size(); i < size; i++) { if (mPlaceholders.get(i).mNeedsComputation) { final Object model = mNextData.get(i); if (isTracing) { ComponentsSystrace.beginSection("renderInfo:" + getModelName(model)); } mPlaceholders.get(i).mRenderInfo = mComponentRenderer.render(model, i); if (isTracing) { ComponentsSystrace.endSection(); } mDataHolders.get(i).setNext(model); } } } if (isTracing) { ComponentsSystrace.beginSection("executeOperations"); } mOperationExecutor.executeOperations(c, mOperations); if (isTracing) { ComponentsSystrace.endSection(); } } private static String getModelName(Object model) { return model instanceof DataDiffModelName ? ((DataDiffModelName) model).getName() : model.getClass().getSimpleName(); } /** Emit a soft error if the size between mPlaceholders and mNextData aren't the same. */ private void logErrorForInconsistentSize(ComponentContext c) { final StringBuilder message = new StringBuilder(); message .append("Inconsistent size between mPlaceholders(") .append(mPlaceholders.size()) .append(") and mNextData(") .append(mNextData.size()) .append("); "); message.append("mOperations: ["); for (int i = 0, size = mOperations.size(); i < size; i++) { final Operation operation = mOperations.get(i); message .append("[type=") .append(operation.getType()) .append(", index=") .append(operation.getIndex()) .append(", toIndex=") .append(operation.getToIndex()); if (operation.mComponentContainers != null) { message.append(", count=").append(operation.mComponentContainers.size()); } message.append("], "); } message.append("]; "); message.append("mNextData: ["); for (int i = 0, size = mNextData.size(); i < size; i++) { message.append("[").append(mNextData.get(i)).append("], "); } message.append("]"); ComponentsReporter.emitMessage( ComponentsReporter.LogLevel.ERROR, INCONSISTENT_SIZE, message.toString()); } @VisibleForTesting List<Operation> getOperations() { return mOperations; } public static class Operation { public static final int INSERT = 0; public static final int UPDATE = 1; public static final int DELETE = 2; public static final int MOVE = 3; private final int mType; private final int mIndex; private final int mToIndex; private final List<ComponentContainer> mComponentContainers; private final List<Diff> mDataContainers; private Operation( int type, int index, int toIndex, List<ComponentContainer> placeholder, List<Diff> dataHolders) { mType = type; mIndex = index; mToIndex = toIndex; mComponentContainers = placeholder; mDataContainers = dataHolders; } public int getType() { return mType; } public int getIndex() { return mIndex; } public int getToIndex() { return mToIndex; } public List<ComponentContainer> getComponentContainers() { return mComponentContainers; } public List<Diff> getDataContainers() { return mDataContainers; } } public static class ComponentContainer { private RenderInfo mRenderInfo; private boolean mNeedsComputation; public ComponentContainer(RenderInfo renderInfo, boolean needsComputation) { mRenderInfo = renderInfo; mNeedsComputation = needsComputation; } public RenderInfo getRenderInfo() { return mRenderInfo; } } }