/** * Copyright (c) 2017-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.litho; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import android.support.v4.util.Pools; import com.facebook.litho.ComponentLifecycle.StateContainer; import com.facebook.infer.annotation.ThreadSafe; import static com.facebook.litho.ComponentLifecycle.StateUpdate; /** * Holds information about the current State of the components in a Component Tree. */ public class StateHandler { private static final int INITIAL_STATE_UPDATE_LIST_CAPACITY = 4; private static final int INITIAL_MAP_CAPACITY = 4; private static final int POOL_CAPACITY = 10; private static final Pools.SynchronizedPool<List<StateUpdate>> sStateUpdatesListPool = new Pools.SynchronizedPool<>(POOL_CAPACITY); private static final Pools.SynchronizedPool<Map<String, List<StateUpdate>>> sPendingStateUpdatesMapPool = new Pools.SynchronizedPool<>(POOL_CAPACITY); private static final Pools.SynchronizedPool<Map<String, StateContainer>> sStateContainersMapPool = new Pools.SynchronizedPool<>(POOL_CAPACITY); private static final Pools.SynchronizedPool<Set<String>> sKnownGlobalKeysSetPool = new Pools.SynchronizedPool<>(POOL_CAPACITY); /** * List of state updates that will be applied during the next layout pass. */ private Map<String, List<StateUpdate>> mPendingStateUpdates; /** * Maps a component key to a component object that retains the current state values for that key. */ public Map<String, StateContainer> mStateContainers; private Set<String> mKnownGlobalKeys; void init(StateHandler stateHandler) { if (stateHandler == null) { return; } copyPendingStateUpdatesMap(stateHandler.getPendingStateUpdates()); copyCurrentStateContainers(stateHandler.getStateContainers()); } public static StateHandler acquireNewInstance(StateHandler stateHandler) { return ComponentsPools.acquireStateHandler(stateHandler); } public boolean isEmpty() { return mStateContainers == null || mStateContainers.isEmpty(); } /** * Adds a state update to the list of the state updates that will be applied for the given * component key during the next layout pass. * @param key the global key of the component * @param stateUpdate the state update to apply to the component */ void queueStateUpdate(String key, StateUpdate stateUpdate) { maybeInitPendingUpdates(); List<StateUpdate> pendingStateUpdatesForKey = mPendingStateUpdates.get(key); if (pendingStateUpdatesForKey == null) { pendingStateUpdatesForKey = StateHandler.acquireStateUpdatesList(); mPendingStateUpdates.put(key, pendingStateUpdatesForKey); } pendingStateUpdatesForKey.add(stateUpdate); } /** * Sets the initial value for a state or transfers the previous state value to the new component, * then applies all the states updates that have been enqueued for the new component's global key. * Assumed thread-safe because the one write is before all the reads. * @param component the new component */ @ThreadSafe(enableChecks = false) void applyStateUpdatesForComponent(Component component) { maybeInitStateContainers(); maybeInitKnownGlobalKeys(); final ComponentLifecycle lifecycle = component.getLifecycle(); if (!lifecycle.hasState()) { return; } final StateContainer previousStateContainer; final String key = component.getGlobalKey(); final StateContainer currentStateContainer = mStateContainers.get(key); if (mKnownGlobalKeys.contains(key)) { // We found two components with the same global key. throw new RuntimeException( "Cannot set State for " + component.getSimpleName() + ", found another Component with the same key"); } mKnownGlobalKeys.add(key); if (currentStateContainer != null) { lifecycle.transferState( component.getScopedContext(), currentStateContainer, component); previousStateContainer = currentStateContainer; } else { lifecycle.createInitialState(component.getScopedContext(), component); previousStateContainer = component.getStateContainer(); } final List<StateUpdate> stateUpdatesForKey = mPendingStateUpdates == null ? null : mPendingStateUpdates.get(key); // If there are no state updates pending for this component, simply store its current state. if (stateUpdatesForKey != null) { for (StateUpdate update : stateUpdatesForKey) { update.updateState(previousStateContainer, component); } } mStateContainers.put(key, component.getStateContainer()); } /** * Removes a list of state updates that have been applied from the pending state updates list and * updates the map of current components with the given components. * @param stateHandler state handler that was used to apply state updates in a layout pass */ void commit(StateHandler stateHandler) { clearStateUpdates(stateHandler.getPendingStateUpdates()); updateCurrentComponentsWithState(stateHandler.getStateContainers()); } private void clearStateUpdates(Map<String, List<StateUpdate>> appliedStateUpdates) { if (appliedStateUpdates == null || mPendingStateUpdates == null || mPendingStateUpdates.isEmpty()) { return; } for (String key : appliedStateUpdates.keySet()) { final List<StateUpdate> pendingStateUpdatesForKey = mPendingStateUpdates.get(key); if (pendingStateUpdatesForKey == null) { continue; } final List<StateUpdate> appliedStateUpdatesForKey = appliedStateUpdates.get(key); if (pendingStateUpdatesForKey.size() == appliedStateUpdatesForKey.size()) { mPendingStateUpdates.remove(key); releaseStateUpdatesList(pendingStateUpdatesForKey); } else { pendingStateUpdatesForKey.removeAll(appliedStateUpdatesForKey); } } } private void updateCurrentComponentsWithState( Map<String, StateContainer> updatedStateContainers) { if (updatedStateContainers == null || updatedStateContainers.isEmpty()) { return; } maybeInitStateContainers(); mStateContainers.putAll(updatedStateContainers); } void release() { if (mPendingStateUpdates != null) { mPendingStateUpdates.clear(); sPendingStateUpdatesMapPool.release(mPendingStateUpdates); mPendingStateUpdates = null; } if (mStateContainers != null) { mStateContainers.clear(); sStateContainersMapPool.release(mStateContainers); mStateContainers = null; } if (mKnownGlobalKeys != null) { mKnownGlobalKeys.clear(); sKnownGlobalKeysSetPool.release(mKnownGlobalKeys); mKnownGlobalKeys = null; } } private static List<StateUpdate> acquireStateUpdatesList() { return acquireStateUpdatesList(null); } private static List<StateUpdate> acquireStateUpdatesList(List<StateUpdate> copyFrom) { List<StateUpdate> list = sStateUpdatesListPool.acquire(); if (list == null) { list = new ArrayList<>( copyFrom == null ? INITIAL_STATE_UPDATE_LIST_CAPACITY : copyFrom.size()); } if (copyFrom != null) { list.addAll(copyFrom); } return list; } private static void releaseStateUpdatesList(List<StateUpdate> list) { list.clear(); sStateUpdatesListPool.release(list); } Map<String, StateContainer> getStateContainers() { return mStateContainers; } Map<String, List<StateUpdate>> getPendingStateUpdates() { return mPendingStateUpdates; } /** * @return copy the information from the given map of state updates into the map of pending state * updates. */ private void copyPendingStateUpdatesMap( Map<String, List<StateUpdate>> pendingStateUpdates) { if (pendingStateUpdates == null || pendingStateUpdates.isEmpty()) { return; } maybeInitPendingUpdates(); for (String key : pendingStateUpdates.keySet()) { mPendingStateUpdates.put(key, acquireStateUpdatesList(pendingStateUpdates.get(key))); } } /** * @return copy the list of given state containers into the map that holds the current * state containers of components. */ private void copyCurrentStateContainers(Map<String, StateContainer> stateContainers) { if (stateContainers == null || stateContainers.isEmpty()) { return; } maybeInitStateContainers(); for (String key : stateContainers.keySet()) { mStateContainers.put(key, stateContainers.get(key)); } } private void maybeInitStateContainers() { if (mStateContainers == null) { mStateContainers = sStateContainersMapPool.acquire(); if (mStateContainers == null) { mStateContainers = new HashMap<>(INITIAL_MAP_CAPACITY); } } } private void maybeInitPendingUpdates() { if (mPendingStateUpdates == null) { mPendingStateUpdates = sPendingStateUpdatesMapPool.acquire(); if (mPendingStateUpdates == null) { mPendingStateUpdates = new HashMap<>(INITIAL_MAP_CAPACITY); } } } private void maybeInitKnownGlobalKeys() { if (mKnownGlobalKeys == null) { mKnownGlobalKeys = sKnownGlobalKeysSetPool.acquire(); if (mKnownGlobalKeys == null) { mKnownGlobalKeys = new HashSet<>(INITIAL_MAP_CAPACITY); } } } }