Wire Editor to Flipper

Summary:
This diff implements the integration between a Litho Editor and Flipper. It does so by converting the Editor format to FlipperObject, and then converting it back from FlipperDynamic.

This conversion works for both `State` and `Prop`. We already provide default implementations for primitive + wrapped types and String, so the functionality should match the existing one.

Reviewed By: passy, Katalune

Differential Revision: D22455220

fbshipit-source-id: f7f633765f3d997ce6de09d2c1277019e72c0802
This commit is contained in:
Paco Estevez Garcia
2020-07-27 05:07:59 -07:00
committed by Facebook GitHub Bot
parent 1784eb78d9
commit 10f9a48540
8 changed files with 146 additions and 58 deletions

View File

@@ -21,6 +21,7 @@ android {
compileOnly deps.lithoAnnotations compileOnly deps.lithoAnnotations
implementation project(':android') implementation project(':android')
implementation deps.lithoCore implementation deps.lithoCore
implementation deps.lithoEditorCore
implementation deps.lithoSectionsDebug implementation deps.lithoSectionsDebug
implementation deps.lithoSectionsCore implementation deps.lithoSectionsCore
implementation deps.lithoWidget implementation deps.lithoWidget

View File

@@ -73,11 +73,7 @@ public class DataUtils {
props.put(f.getName(), InspectorValue.immutable(description)); props.put(f.getName(), InspectorValue.immutable(description));
} }
} else { } else {
if (isTypeMutable(f.getType())) { props.put(f.getName(), FlipperEditor.makeFlipperField(node, f));
props.put(f.getName(), InspectorValue.mutable(f.get(node)));
} else {
props.put(f.getName(), InspectorValue.immutable(f.get(node)));
}
} }
break; break;
} }
@@ -93,7 +89,7 @@ public class DataUtils {
} }
@Nullable @Nullable
static FlipperObject getStateData(Object node, StateContainer stateContainer) throws Exception { static FlipperObject getStateData(StateContainer stateContainer) {
if (stateContainer == null) { if (stateContainer == null) {
return null; return null;
} }
@@ -106,11 +102,7 @@ public class DataUtils {
final State annotation = f.getAnnotation(State.class); final State annotation = f.getAnnotation(State.class);
if (annotation != null) { if (annotation != null) {
if (DataUtils.isTypeMutable(f.getType())) { state.put(f.getName(), FlipperEditor.makeFlipperField(stateContainer, f));
state.put(f.getName(), InspectorValue.mutable(f.get(stateContainer)));
} else {
state.put(f.getName(), InspectorValue.immutable(f.get(stateContainer)));
}
hasState = true; hasState = true;
} }
} }
@@ -118,23 +110,6 @@ public class DataUtils {
return hasState ? state.build() : null; return hasState ? state.build() : null;
} }
static boolean isTypeMutable(Class<?> type) {
if (type == int.class || type == Integer.class) {
return true;
} else if (type == long.class || type == Long.class) {
return true;
} else if (type == float.class || type == Float.class) {
return true;
} else if (type == double.class || type == Double.class) {
return true;
} else if (type == boolean.class || type == Boolean.class) {
return true;
} else if (type.isAssignableFrom(String.class)) {
return true;
}
return false;
}
static com.facebook.flipper.plugins.inspector.InspectorValue fromDrawable(Drawable d) { static com.facebook.flipper.plugins.inspector.InspectorValue fromDrawable(Drawable d) {
int color = 0; int color = 0;
if (d instanceof ColorDrawable) { if (d instanceof ColorDrawable) {

View File

@@ -58,7 +58,7 @@ public class DebugComponentDescriptor extends NodeDescriptor<DebugComponent> {
for (Pair<String[], FlipperDynamic> override : overrides) { for (Pair<String[], FlipperDynamic> override : overrides) {
if (override.first[0].equals("Props")) { if (override.first[0].equals("Props")) {
applyReflectiveOverride(component, override.first[1], override.second); applyReflectiveOverride(component, override.first, override.second);
} }
} }
} }
@@ -72,7 +72,7 @@ public class DebugComponentDescriptor extends NodeDescriptor<DebugComponent> {
for (Pair<String[], FlipperDynamic> override : overrides) { for (Pair<String[], FlipperDynamic> override : overrides) {
if (override.first[0].equals("State")) { if (override.first[0].equals("State")) {
applyReflectiveOverride(stateContainer, override.first[1], override.second); applyReflectiveOverride(stateContainer, override.first, override.second);
} }
} }
} }
@@ -278,8 +278,8 @@ public class DebugComponentDescriptor extends NodeDescriptor<DebugComponent> {
} }
@Nullable @Nullable
private static FlipperObject getStateData(DebugComponent node) throws Exception { private static FlipperObject getStateData(DebugComponent node) {
return DataUtils.getStateData(node, node.getStateContainer()); return DataUtils.getStateData(node.getStateContainer());
} }
@Override @Override
@@ -502,31 +502,13 @@ public class DebugComponentDescriptor extends NodeDescriptor<DebugComponent> {
return YogaEdge.valueOf(s.toUpperCase()); return YogaEdge.valueOf(s.toUpperCase());
} }
private static void applyReflectiveOverride(Object o, String key, FlipperDynamic dynamic) { // The path follows the pattern (Props|State)/field/(field|index)*
private static void applyReflectiveOverride(
Object o, final String[] path, final FlipperDynamic dynamic) {
try { try {
final Field field = o.getClass().getDeclaredField(key); final Field field = o.getClass().getDeclaredField(path[1]);
field.setAccessible(true); FlipperEditor.updateComponent(path, field, o, dynamic);
final Class type = field.getType();
Object value = null;
if (type == int.class || type == Integer.class) {
value = dynamic.asInt();
} else if (type == long.class || type == Long.class) {
value = dynamic.asLong();
} else if (type == float.class || type == Float.class) {
value = dynamic.asFloat();
} else if (type == double.class || type == Double.class) {
value = dynamic.asDouble();
} else if (type == boolean.class || type == Boolean.class) {
value = dynamic.asBoolean();
} else if (type.isAssignableFrom(String.class)) {
value = dynamic.asString();
}
if (value != null) {
field.set(o, value);
}
} catch (Exception ignored) { } catch (Exception ignored) {
} }
} }

View File

@@ -91,8 +91,8 @@ public class DebugSectionDescriptor extends NodeDescriptor<DebugSection> {
return DataUtils.getPropData(section); return DataUtils.getPropData(section);
} }
private static @Nullable FlipperObject getStateData(DebugSection node) throws Exception { private static @Nullable FlipperObject getStateData(DebugSection node) {
return DataUtils.getStateData(node, node.getStateContainer()); return DataUtils.getStateData(node.getStateContainer());
} }
@Override @Override

View File

@@ -0,0 +1,125 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.flipper.plugins.litho;
import com.facebook.flipper.core.FlipperArray;
import com.facebook.flipper.core.FlipperDynamic;
import com.facebook.flipper.core.FlipperObject;
import com.facebook.flipper.core.FlipperValue;
import com.facebook.flipper.plugins.inspector.InspectorValue;
import com.facebook.litho.editor.EditorRegistry;
import com.facebook.litho.editor.model.EditorArray;
import com.facebook.litho.editor.model.EditorBool;
import com.facebook.litho.editor.model.EditorNumber;
import com.facebook.litho.editor.model.EditorShape;
import com.facebook.litho.editor.model.EditorString;
import com.facebook.litho.editor.model.EditorValue;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nullable;
/**
* This class is responsible for making Litho Editor compatible with Flipper.
*
* <p>It provides methods to convert from FlipperDynamic, and to provide the description of a Prop
* or State as a FlipperObject, FlipperArray or FlipperValue.
*/
public class FlipperEditor {
/**
* Uses an editor to create a FlipperObject, FlipperArray or FlipperValue to describe it. If no
* editor is available then it returns the class name.
*/
public static Object makeFlipperField(Object node, Field f) {
Class<?> type = f.getType();
final EditorValue editorValue = EditorRegistry.read(type, f, node);
if (editorValue != null) {
return intoFlipper(editorValue);
} else {
return InspectorValue.immutable(type.toString());
}
}
/**
* Uses an editor to update a field nested in an object path with the value of a FlipperDynamic.
* If no editor is available then it returns null.
*
* <p>The path as defined by Flipper starts with either "Props" or "State" followed by the call
* chain into the object. Fields retain their name, positions in an array use their index.
*/
public static @Nullable Boolean updateComponent(
String[] path, Field field, Object o, FlipperDynamic dynamic) {
Object raw = dynamic.raw();
EditorValue edit;
if (raw instanceof String) {
edit = EditorValue.string((String) raw);
} else if (raw instanceof Number) {
edit = EditorValue.number(((Number) raw));
} else if (raw instanceof Boolean) {
edit = EditorValue.bool((Boolean) raw);
} else {
edit = EditorValue.string(raw.toString());
}
for (int i = path.length - 1; i > 0; i--) {
HashMap<String, EditorValue> content = new HashMap<>();
content.put(path[i], edit);
edit = EditorValue.shape(content);
}
return EditorRegistry.write(field.getType(), field, o, edit);
}
/** Converts into one of FlipperValue, FlipperObject, or FlipperArray */
public static Object intoFlipper(EditorValue editorValue) {
return editorValue.when(
new EditorValue.EditorVisitor<Object>() {
@Override
public Object isShape(EditorShape object) {
FlipperObject.Builder bb = new FlipperObject.Builder();
for (Map.Entry<String, EditorValue> entry : object.value.entrySet()) {
bb.put(entry.getKey(), intoFlipper(entry.getValue()));
}
return bb.build();
}
@Override
public Object isArray(EditorArray array) {
FlipperArray.Builder bb = new FlipperArray.Builder();
for (EditorValue entry : array.value) {
Object flipper = intoFlipper(entry);
if (flipper instanceof FlipperValue) {
bb.put((FlipperValue) flipper);
} else if (flipper instanceof FlipperObject) {
bb.put((FlipperObject) flipper);
} else if (flipper instanceof FlipperArray) {
bb.put((FlipperArray) flipper);
}
}
return bb.build();
}
@Override
public Object isNumber(EditorNumber number) {
return InspectorValue.mutable(number.value);
}
@Override
public Object isString(EditorString string) {
return InspectorValue.mutable(string.value);
}
@Override
public Object isBool(EditorBool bool) {
return InspectorValue.mutable(bool.value);
}
});
}
}

View File

@@ -18,6 +18,10 @@ public class FlipperDynamic {
mObject = object; mObject = object;
} }
public Object raw() {
return mObject;
}
public @Nullable String asString() { public @Nullable String asString() {
if (mObject == null) { if (mObject == null) {
return null; return null;

View File

@@ -72,6 +72,7 @@ ext.deps = [
// Litho // Litho
lithoAnnotations : "com.facebook.litho:litho-annotations:$LITHO_VERSION", lithoAnnotations : "com.facebook.litho:litho-annotations:$LITHO_VERSION",
lithoCore : "com.facebook.litho:litho-core:$LITHO_VERSION", lithoCore : "com.facebook.litho:litho-core:$LITHO_VERSION",
lithoEditorCore : "com.facebook.litho:litho-editor-core:$LITHO_VERSION",
lithoSectionsAnnotations: "com.facebook.litho:litho-sections-annotations:$LITHO_VERSION", lithoSectionsAnnotations: "com.facebook.litho:litho-sections-annotations:$LITHO_VERSION",
lithoSectionsDebug : "com.facebook.litho:litho-sections-debug:$LITHO_VERSION", lithoSectionsDebug : "com.facebook.litho:litho-sections-debug:$LITHO_VERSION",
lithoSectionsCore : "com.facebook.litho:litho-sections-core:$LITHO_VERSION", lithoSectionsCore : "com.facebook.litho:litho-sections-core:$LITHO_VERSION",

View File

@@ -18,7 +18,7 @@ POM_DEVELOPER_NAME=facebook
POM_ISSUES_URL = 'https://github.com/facebook/flipper/issues/' POM_ISSUES_URL = 'https://github.com/facebook/flipper/issues/'
# Shared version numbers # Shared version numbers
LITHO_VERSION=0.36.0 LITHO_VERSION=0.37.1
ANDROIDX_VERSION=1.1.0 ANDROIDX_VERSION=1.1.0
KOTLIN_VERSION=1.3.71 KOTLIN_VERSION=1.3.71