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:
Luke De Feo
2022-10-25 07:10:38 -07:00
committed by Facebook GitHub Bot
parent 7c3e28272b
commit 1aacc51d12
3 changed files with 63 additions and 12 deletions

View File

@@ -20,6 +20,7 @@ android {
dependencies {
compileOnly deps.lithoAnnotations
implementation project(':android')
implementation deps.kotlinCoroutinesAndroid
implementation deps.lithoCore
api deps.lithoEditorCore
api(deps.lithoEditorFlipper) {

View File

@@ -8,38 +8,92 @@
package com.facebook.flipper.plugins.uidebugger.litho
import android.util.Log
import android.view.ViewTreeObserver
import com.facebook.flipper.plugins.uidebugger.LogTag
import com.facebook.flipper.plugins.uidebugger.core.Context
import com.facebook.flipper.plugins.uidebugger.descriptors.nodeId
import com.facebook.flipper.plugins.uidebugger.observers.TreeObserver
import com.facebook.flipper.plugins.uidebugger.observers.TreeObserverBuilder
import com.facebook.flipper.plugins.uidebugger.scheduler.throttleLatest
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>() {
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) {
Log.i(LogTag, "Subscribing to litho view ${node.nodeId()}")
Log.d(LogTag, "Subscribing to litho view ${node.nodeId()}")
nodeRef = node as LithoView
val listener: (view: LithoView) -> Unit = { processUpdate(context, node) }
node.setOnDirtyMountListener(listener)
val lithoDebuggerExtension = LithoDebuggerExtension(this)
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() {
Log.i(LogTag, "Unsubscribing from litho view")
nodeRef?.setOnDirtyMountListener(null)
Log.d(LogTag, "Unsubscribing from litho view ${nodeRef?.nodeId()}")
nodeRef?.viewTreeObserver?.removeOnScrollChangedListener(onScrollChangedListener)
nodeRef?.unregisterUIDebugger()
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> {
override fun canBuildFor(node: Any): Boolean {
return node is LithoView

View File

@@ -2,7 +2,6 @@
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
# POM publishing constants
VERSION_NAME=0.171.2-SNAPSHOT
GROUP=com.facebook.flipper
@@ -17,18 +16,15 @@ POM_LICENCE_DIST=repo
POM_DEVELOPER_ID=facebook
POM_DEVELOPER_NAME=facebook
POM_ISSUES_URL=https://github.com/facebook/flipper/issues/
# Shared version numbers
LITHO_VERSION=0.41.1
LITHO_VERSION=0.43.0
ANDROIDX_VERSION=1.3.0
KOTLIN_VERSION=1.6.20
# Gradle internals
org.gradle.internal.repository.max.retries=10
org.gradle.internal.repository.initial.backoff=1250
org.gradle.jvmargs=-Xmx2g -Xms512m -XX:MaxPermSize=1024m -XX:+CMSClassUnloadingEnabled
systemProp.org.gradle.internal.http.connectionTimeout=120000
systemProp.org.gradle.internal.http.socketTimeout=120000
android.useAndroidX=true
android.enableJetifier=true