Skip to content

Commit

Permalink
Introduce AttachDetachHandler to execute @OnAttached/@OnDetached dele…
Browse files Browse the repository at this point in the history
…gate methods

Summary:
This is the first step for the destroy callback for components([quip](https://fb.quip.com/0V2RAPImcgYI)). Introduce `AttachDetachHandler` for executing OnAttached / OnDetached delegate methods for components in the ComponentTree.

1. `OnAttached` methods are called for components when LayoutState is calculated;
2. `OnDetached` methods are called when components are removed from the ComponentTree by `ComponentTree#setRoot()`, or the tree are released by `ComponentTree#release()`.

Reviewed By: marco-cova

Differential Revision: D14184234

fbshipit-source-id: 4f37597e26f863d72a85888cc7f7d560f55f573d
  • Loading branch information
Jing-Wei Wu authored and facebook-github-bot committed Apr 14, 2019
1 parent 98ec18a commit 51b8d43
Show file tree
Hide file tree
Showing 9 changed files with 864 additions and 6 deletions.
131 changes: 131 additions & 0 deletions litho-core/src/main/java/com/facebook/litho/AttachDetachHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* Copyright 2019-present Facebook, Inc.
*
* 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;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.facebook.infer.annotation.ThreadSafe;
import com.facebook.litho.annotations.OnAttached;
import com.facebook.litho.annotations.OnDetached;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.concurrent.GuardedBy;

/**
* A handler stores components that have implemented {@link OnAttached} or {@link OnDetached}
* delegate methods. {@link OnAttached} method is called when a component is attached to the {@link
* ComponentTree}, in contrast, {@link OnDetached} method is called when a component is detached
* from the tree.
*/
@ThreadSafe
public class AttachDetachHandler {

/** A container stores components whose {@link OnAttached} methods are already executed. */
@GuardedBy("this")
@Nullable
private Map<String, Component> mAttached;

/**
* Execute {@link OnAttached} method for components in the set of given attachable minus {@link
* #mAttached}; execute {@link OnDetached} method for components in the set of {@link #mAttached}
* minus given attachable.
*
* @param attachable contains components that have implemented {@link OnAttached} or {@link
* OnDetached} delegate methods.
*/
void onAttached(@Nullable Map<String, Component> attachable) {
@Nullable final Map<String, Component> toAttach;
@Nullable final Map<String, Component> toDetach;
synchronized (this) {
toAttach = composeAttach(attachable, mAttached);
toDetach = composeDetach(attachable, mAttached);

if (attachable != null) {
mAttached = new HashMap<>(attachable);
} else {
mAttached = null;
}
}

if (toDetach != null) {
for (Component component : toDetach.values()) {
component.onDetached(component.getScopedContext());
}
}

if (toAttach != null) {
for (Component component : toAttach.values()) {
component.onAttached(component.getScopedContext());
}
}
}

/**
* Execute {@link OnDetached} callbacks for components stored in {@link #mAttached}, this method
* should be called when releasing a {@link ComponentTree}.
*/
void onDetached() {
final List<Component> toDetach;
synchronized (this) {
if (mAttached == null) {
return;
}
toDetach = new ArrayList<>(mAttached.values());
mAttached.clear();
}

for (int i = 0, size = toDetach.size(); i < size; i++) {
final Component component = toDetach.get(i);
component.onDetached(component.getScopedContext());
}
}

@GuardedBy("this")
@Nullable
private static Map<String, Component> composeAttach(
@Nullable Map<String, Component> attachable, @Nullable Map<String, Component> attached) {
Map<String, Component> toAttach = null;
if (attachable != null) {
toAttach = new HashMap<>(attachable);
if (attached != null) {
toAttach.keySet().removeAll(attached.keySet());
}
}
return toAttach;
}

@GuardedBy("this")
@Nullable
private static Map<String, Component> composeDetach(
@Nullable Map<String, Component> attachable, @Nullable Map<String, Component> attached) {
Map<String, Component> toDetach = null;
if (attached != null) {
toDetach = new HashMap<>(attached);
if (attachable != null) {
toDetach.keySet().removeAll(attachable.keySet());
}
}
return toDetach;
}

@VisibleForTesting
@Nullable
Map<String, Component> getAttached() {
return mAttached;
}
}
33 changes: 33 additions & 0 deletions litho-core/src/main/java/com/facebook/litho/ComponentContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ public interface YogaNodeFactory {
/** TODO: (T38237241) remove the usage of the key handler post the nested tree experiment */
private final @Nullable KeyHandler mKeyHandler;

private @Nullable volatile AttachDetachHandler mAttachDetachHandler;

private String mNoStateUpdatesMethod;

// Hold a reference to the component which scope we are currently within.
Expand Down Expand Up @@ -127,6 +129,16 @@ public ComponentContext(
@Nullable KeyHandler keyHandler,
@Nullable TreeProps treeProps,
@Nullable ComponentTree.LayoutStateFuture layoutStateFuture) {
this(context, stateHandler, null, keyHandler, treeProps, layoutStateFuture);
}

public ComponentContext(
ComponentContext context,
@Nullable StateHandler stateHandler,
@Nullable AttachDetachHandler attachDetachHandler,
@Nullable KeyHandler keyHandler,
@Nullable TreeProps treeProps,
@Nullable ComponentTree.LayoutStateFuture layoutStateFuture) {

mContext = context.getAndroidContext();
mResourceCache = context.mResourceCache;
Expand All @@ -142,6 +154,8 @@ public ComponentContext(
mYogaNodeFactory = context.mYogaNodeFactory;

mStateHandler = stateHandler != null ? stateHandler : context.mStateHandler;
mAttachDetachHandler =
attachDetachHandler != null ? attachDetachHandler : context.mAttachDetachHandler;
mKeyHandler = keyHandler != null ? keyHandler : context.mKeyHandler;
mTreeProps = treeProps != null ? treeProps : context.mTreeProps;
mLayoutStateFuture = layoutStateFuture == null ? context.mLayoutStateFuture : layoutStateFuture;
Expand Down Expand Up @@ -190,6 +204,7 @@ public ComponentContext(ComponentContext context) {
this(
context,
context.mStateHandler,
context.mAttachDetachHandler,
context.mKeyHandler,
context.mTreeProps,
context.mLayoutStateFuture);
Expand Down Expand Up @@ -464,6 +479,24 @@ StateHandler getStateHandler() {
return mStateHandler;
}

AttachDetachHandler getOrCreateAttachDetachHandler() {
AttachDetachHandler localAttachDetachHandler = mAttachDetachHandler;
if (localAttachDetachHandler == null) {
synchronized (this) {
localAttachDetachHandler = mAttachDetachHandler;
if (localAttachDetachHandler == null) {
mAttachDetachHandler = localAttachDetachHandler = new AttachDetachHandler();
}
}
}
return localAttachDetachHandler;
}

@Nullable
AttachDetachHandler getAttachDetachHandler() {
return mAttachDetachHandler;
}

@Nullable
KeyHandler getKeyHandler() {
return mKeyHandler;
Expand Down
30 changes: 28 additions & 2 deletions litho-core/src/main/java/com/facebook/litho/ComponentTree.java
Original file line number Diff line number Diff line change
Expand Up @@ -958,9 +958,11 @@ void measure(int widthSpec, int heightSpec, int[] measureOutput, boolean forceLa
}

final List<Component> components;
@Nullable final Map<String, Component> attachables;
synchronized (this) {
final StateHandler layoutStateStateHandler = localLayoutState.consumeStateHandler();
components = new ArrayList<>(localLayoutState.getComponents());
attachables = localLayoutState.consumeAttachables();
if (layoutStateStateHandler != null) {
mStateHandler.commit(layoutStateStateHandler, mNestedTreeResolutionExperimentEnabled);
}
Expand All @@ -969,6 +971,13 @@ void measure(int widthSpec, int heightSpec, int[] measureOutput, boolean forceLa
mMainThreadLayoutState = localLayoutState;
}

final AttachDetachHandler attachDetachHandler = mContext.getAttachDetachHandler();
if (attachDetachHandler != null) {
attachDetachHandler.onAttached(attachables);
} else if (attachables != null) {
mContext.getOrCreateAttachDetachHandler().onAttached(attachables);
}

bindEventAndTriggerHandlers(components);

// We need to force remount on layout
Expand Down Expand Up @@ -1755,6 +1764,7 @@ private void calculateLayout(
}

List<Component> components = null;
@Nullable Map<String, Component> attachables = null;

final boolean noCompatibleComponent;
int rootWidth = 0;
Expand Down Expand Up @@ -1784,14 +1794,24 @@ private void calculateLayout(
components = new ArrayList<>(localLayoutState.getComponents());
localLayoutState.clearComponents();

attachables = localLayoutState.consumeAttachables();

// Set the new layout state.
mBackgroundLayoutState = localLayoutState;
layoutStateUpdated = true;
}
}

if (noCompatibleComponent && mMeasureListener != null) {
mMeasureListener.onSetRootAndSizeSpec(rootWidth, rootHeight);
if (noCompatibleComponent) {
if (mMeasureListener != null) {
mMeasureListener.onSetRootAndSizeSpec(rootWidth, rootHeight);
}
final AttachDetachHandler attachDetachHandler = mContext.getAttachDetachHandler();
if (attachDetachHandler != null) {
attachDetachHandler.onAttached(attachables);
} else if (attachables != null) {
mContext.getOrCreateAttachDetachHandler().onAttached(attachables);
}
}

if (components != null) {
Expand Down Expand Up @@ -1903,6 +1923,12 @@ public void release() {
synchronized (mEventTriggersContainer) {
clearUnusedTriggerHandlers();
}

final AttachDetachHandler attachDetachHandler = mContext.getAttachDetachHandler();
if (attachDetachHandler != null) {
// Execute detached callbacks if necessary.
attachDetachHandler.onDetached();
}
}

@GuardedBy("this")
Expand Down
16 changes: 16 additions & 0 deletions litho-core/src/main/java/com/facebook/litho/LayoutState.java
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,9 @@ public int compare(LayoutOutput lhs, LayoutOutput rhs) {

@Nullable WorkingRangeContainer mWorkingRangeContainer;

/** A container stores components whose OnAttached delegate methods are about to be executed. */
@Nullable private Map<String, Component> mAttachableContainer;

LayoutState(ComponentContext context) {
mContext = context;
mId = sIdGenerator.getAndIncrement();
Expand Down Expand Up @@ -966,6 +969,12 @@ private static void collectResults(
if (delegate.getScopedContext() != null
&& delegate.getScopedContext().getComponentTree() != null) {
layoutState.mComponents.add(delegate);
if (delegate.hasAttachDetachCallback()) {
if (layoutState.mAttachableContainer == null) {
layoutState.mAttachableContainer = new HashMap<>();
}
layoutState.mAttachableContainer.put(delegate.getGlobalKey(), delegate);
}
}
if (delegate.getGlobalKey() != null) {
layoutState.mComponentKeyToBounds.put(delegate.getGlobalKey(), copyRect);
Expand Down Expand Up @@ -1040,6 +1049,13 @@ void clearComponents() {
mComponents.clear();
}

@Nullable
Map<String, Component> consumeAttachables() {
@Nullable Map<String, Component> tmp = mAttachableContainer;
mAttachableContainer = null;
return tmp;
}

private static void calculateAndSetHostOutputIdAndUpdateState(
InternalNode node,
LayoutOutput hostOutput,
Expand Down
Loading

0 comments on commit 51b8d43

Please sign in to comment.