Use mount extension for litho integration
Summary: Initial implementation of Litho extensions using mount extension. After mount is called on the main thread and we traverse the hierachy. In future we can use mount extensions to construct a sparse tree rather than sending everything every time. Scroll is handled with a native UI scroll listener for each litho view. This may break if the litho view is not a direct child of the scroll view. Reviewed By: mihaelao Differential Revision: D40021840 fbshipit-source-id: b09086a7a16660225885620609009dddf5b90d3b
This commit is contained in:
committed by
Facebook GitHub Bot
parent
7c3e28272b
commit
1aacc51d12
@@ -20,6 +20,7 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
compileOnly deps.lithoAnnotations
|
compileOnly deps.lithoAnnotations
|
||||||
implementation project(':android')
|
implementation project(':android')
|
||||||
|
implementation deps.kotlinCoroutinesAndroid
|
||||||
implementation deps.lithoCore
|
implementation deps.lithoCore
|
||||||
api deps.lithoEditorCore
|
api deps.lithoEditorCore
|
||||||
api(deps.lithoEditorFlipper) {
|
api(deps.lithoEditorFlipper) {
|
||||||
|
|||||||
@@ -8,38 +8,92 @@
|
|||||||
package com.facebook.flipper.plugins.uidebugger.litho
|
package com.facebook.flipper.plugins.uidebugger.litho
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.ViewTreeObserver
|
||||||
import com.facebook.flipper.plugins.uidebugger.LogTag
|
import com.facebook.flipper.plugins.uidebugger.LogTag
|
||||||
import com.facebook.flipper.plugins.uidebugger.core.Context
|
import com.facebook.flipper.plugins.uidebugger.core.Context
|
||||||
import com.facebook.flipper.plugins.uidebugger.descriptors.nodeId
|
import com.facebook.flipper.plugins.uidebugger.descriptors.nodeId
|
||||||
import com.facebook.flipper.plugins.uidebugger.observers.TreeObserver
|
import com.facebook.flipper.plugins.uidebugger.observers.TreeObserver
|
||||||
import com.facebook.flipper.plugins.uidebugger.observers.TreeObserverBuilder
|
import com.facebook.flipper.plugins.uidebugger.observers.TreeObserverBuilder
|
||||||
|
import com.facebook.flipper.plugins.uidebugger.scheduler.throttleLatest
|
||||||
import com.facebook.litho.LithoView
|
import com.facebook.litho.LithoView
|
||||||
|
import com.facebook.rendercore.extensions.ExtensionState
|
||||||
|
import com.facebook.rendercore.extensions.MountExtension
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* There are 2 ways a litho view can update:
|
||||||
|
* 1. a view was added / updated / removed through a mount ,we use the mount extension to capture
|
||||||
|
* these
|
||||||
|
* 2. The user scrolled. This does not cause a mount to the litho view but it may cause new
|
||||||
|
* components to mount as they come on screen On the native side we capture scrolls as it causes the
|
||||||
|
* draw listener to first but but the layout traversal would stop once it sees the lithoview.
|
||||||
|
*
|
||||||
|
* Therefore we need a way to capture the changes in the position of views in a litho view hierarchy
|
||||||
|
* as they are scrolled. A property that seems to hold for litho is if there is a scrolling view in
|
||||||
|
* the heierachy, its direct children are lithoview.
|
||||||
|
*
|
||||||
|
* Given that we are observing a litho view in this class for mount extension we can also attach a
|
||||||
|
* on scroll changed listener to it to be notified by android when it is scrolled. We just need to
|
||||||
|
* then update the bounds for this view as nothing else has changed. If this scroll does lead to a
|
||||||
|
* mount this will be picked up by the mount extension
|
||||||
|
*/
|
||||||
class LithoViewTreeObserver(val context: Context) : TreeObserver<LithoView>() {
|
class LithoViewTreeObserver(val context: Context) : TreeObserver<LithoView>() {
|
||||||
|
|
||||||
override val type = "Litho"
|
override val type = "Litho"
|
||||||
|
private val throttleTimeMs = 500L
|
||||||
|
|
||||||
private var nodeRef: LithoView? = null
|
private val waitScope = CoroutineScope(Dispatchers.IO)
|
||||||
|
private val mainScope = CoroutineScope(Dispatchers.Main)
|
||||||
|
var nodeRef: LithoView? = null
|
||||||
|
var onScrollChangedListener: ViewTreeObserver.OnScrollChangedListener? = null
|
||||||
|
|
||||||
override fun subscribe(node: Any) {
|
override fun subscribe(node: Any) {
|
||||||
|
|
||||||
Log.i(LogTag, "Subscribing to litho view ${node.nodeId()}")
|
Log.d(LogTag, "Subscribing to litho view ${node.nodeId()}")
|
||||||
|
|
||||||
nodeRef = node as LithoView
|
nodeRef = node as LithoView
|
||||||
|
|
||||||
val listener: (view: LithoView) -> Unit = { processUpdate(context, node) }
|
val lithoDebuggerExtension = LithoDebuggerExtension(this)
|
||||||
node.setOnDirtyMountListener(listener)
|
node.registerUIDebugger(lithoDebuggerExtension)
|
||||||
|
|
||||||
listener(node)
|
val throttledUpdate =
|
||||||
|
throttleLatest<Any>(throttleTimeMs, waitScope, mainScope) { node ->
|
||||||
|
// todo only send bounds for the view rather than the entire hierachy
|
||||||
|
processUpdate(context, node)
|
||||||
|
}
|
||||||
|
|
||||||
|
onScrollChangedListener = ViewTreeObserver.OnScrollChangedListener({ throttledUpdate(node) })
|
||||||
|
node.viewTreeObserver.addOnScrollChangedListener(onScrollChangedListener)
|
||||||
|
|
||||||
|
// we have already missed the first mount so we trigger it manually on subscribe
|
||||||
|
processUpdate(context, node)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun unsubscribe() {
|
override fun unsubscribe() {
|
||||||
Log.i(LogTag, "Unsubscribing from litho view")
|
Log.d(LogTag, "Unsubscribing from litho view ${nodeRef?.nodeId()}")
|
||||||
nodeRef?.setOnDirtyMountListener(null)
|
nodeRef?.viewTreeObserver?.removeOnScrollChangedListener(onScrollChangedListener)
|
||||||
|
nodeRef?.unregisterUIDebugger()
|
||||||
nodeRef = null
|
nodeRef = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class LithoDebuggerExtension(val observer: LithoViewTreeObserver) : MountExtension<Void?, Void?>() {
|
||||||
|
|
||||||
|
override fun createState(): Void? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The call guaranteed to be called after new layout mounted completely on the main thread.
|
||||||
|
* mounting includes adding updating or removing views from the heriachy
|
||||||
|
*/
|
||||||
|
override fun afterMount(state: ExtensionState<Void?>) {
|
||||||
|
Log.i(LogTag, "After mount called for litho view ${observer.nodeRef?.nodeId()}")
|
||||||
|
// todo sparse update
|
||||||
|
observer.processUpdate(observer.context, state.rootHost as Any)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
object LithoViewTreeObserverBuilder : TreeObserverBuilder<LithoView> {
|
object LithoViewTreeObserverBuilder : TreeObserverBuilder<LithoView> {
|
||||||
override fun canBuildFor(node: Any): Boolean {
|
override fun canBuildFor(node: Any): Boolean {
|
||||||
return node is LithoView
|
return node is LithoView
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
#
|
#
|
||||||
# This source code is licensed under the MIT license found in the
|
# This source code is licensed under the MIT license found in the
|
||||||
# LICENSE file in the root directory of this source tree.
|
# LICENSE file in the root directory of this source tree.
|
||||||
|
|
||||||
# POM publishing constants
|
# POM publishing constants
|
||||||
VERSION_NAME=0.171.2-SNAPSHOT
|
VERSION_NAME=0.171.2-SNAPSHOT
|
||||||
GROUP=com.facebook.flipper
|
GROUP=com.facebook.flipper
|
||||||
@@ -17,18 +16,15 @@ POM_LICENCE_DIST=repo
|
|||||||
POM_DEVELOPER_ID=facebook
|
POM_DEVELOPER_ID=facebook
|
||||||
POM_DEVELOPER_NAME=facebook
|
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.41.1
|
LITHO_VERSION=0.43.0
|
||||||
ANDROIDX_VERSION=1.3.0
|
ANDROIDX_VERSION=1.3.0
|
||||||
KOTLIN_VERSION=1.6.20
|
KOTLIN_VERSION=1.6.20
|
||||||
|
|
||||||
# Gradle internals
|
# Gradle internals
|
||||||
org.gradle.internal.repository.max.retries=10
|
org.gradle.internal.repository.max.retries=10
|
||||||
org.gradle.internal.repository.initial.backoff=1250
|
org.gradle.internal.repository.initial.backoff=1250
|
||||||
org.gradle.jvmargs=-Xmx2g -Xms512m -XX:MaxPermSize=1024m -XX:+CMSClassUnloadingEnabled
|
org.gradle.jvmargs=-Xmx2g -Xms512m -XX:MaxPermSize=1024m -XX:+CMSClassUnloadingEnabled
|
||||||
systemProp.org.gradle.internal.http.connectionTimeout=120000
|
systemProp.org.gradle.internal.http.connectionTimeout=120000
|
||||||
systemProp.org.gradle.internal.http.socketTimeout=120000
|
systemProp.org.gradle.internal.http.socketTimeout=120000
|
||||||
|
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
|||||||
Reference in New Issue
Block a user