diff --git a/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/GXTemplateEngine.kt b/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/GXTemplateEngine.kt index 0af85ca9b..dd40a7ed7 100644 --- a/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/GXTemplateEngine.kt +++ b/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/GXTemplateEngine.kt @@ -634,9 +634,13 @@ class GXTemplateEngine { // val size = Size(gxTemplateContext.size.width, gxTemplateContext.size.height) - GXNodeUtils.computeNodeTreeByPrepareView(gxRootNode, size) + GXNodeUtils.computeNodeTreeByPrepareView(gxTemplateContext, gxRootNode, size) gxRootNode.stretchNode.layoutByPrepareView?.let { - GXGlobalCache.instance.putLayoutForPrepareView(gxTemplateContext.templateItem, it) + GXGlobalCache.instance.putLayoutForPrepareView( + gxTemplateContext, + gxTemplateContext.templateItem, + it + ) GXNodeUtils.composeGXNodeByCreateView(gxRootNode, it) } } @@ -770,14 +774,20 @@ class GXTemplateEngine { if (gxTemplateContext.isReuseRootNode) { if (GXLog.isLog()) { - GXLog.e("reuse root node, skip bindDataOnlyNodeTree") + GXLog.e( + gxTemplateContext.tag, + "traceId=${gxTemplateContext.traceId} tag=internalBindDataOnlyNodeTree reuse root node, skip bindDataOnlyNodeTree" + ) } gxTemplateContext.isReuseRootNode = false return } if (GXLog.isLog()) { - GXLog.e("internalBindDataOnlyNodeTree gxMeasureSize=${gxMeasureSize} gxTemplateItem=${gxTemplateContext.templateItem}") + GXLog.e( + gxTemplateContext.tag, + "traceId=${gxTemplateContext.traceId} tag=internalBindDataOnlyNodeTree gxMeasureSize=${gxMeasureSize} gxTemplateItem=${gxTemplateContext.templateItem}" + ) } gxTemplateContext.templateData = gxTemplateData diff --git a/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/context/GXTemplateContext.kt b/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/context/GXTemplateContext.kt index 90f91eac1..762a755bd 100644 --- a/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/context/GXTemplateContext.kt +++ b/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/context/GXTemplateContext.kt @@ -26,6 +26,7 @@ import com.alibaba.gaiax.render.node.text.GXDirtyText import com.alibaba.gaiax.render.view.GXIContainer import com.alibaba.gaiax.render.view.GXIRootView import com.alibaba.gaiax.template.GXTemplateInfo +import java.util.UUID import java.util.concurrent.CopyOnWriteArraySet /** @@ -56,6 +57,10 @@ class GXTemplateContext private constructor( var visualTemplateNode: GXTemplateNode? = null ) { + var traceId: String? = UUID.randomUUID().toString() + + var tag: String? = "" + /** * 视图的尺寸是否发生了变化,如果发生了变化,那么缓存需要被清空,UI需要被重建。 * diff --git a/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/render/GXRenderImpl.kt b/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/render/GXRenderImpl.kt index 136be75cf..de7304899 100644 --- a/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/render/GXRenderImpl.kt +++ b/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/render/GXRenderImpl.kt @@ -26,6 +26,7 @@ import com.alibaba.gaiax.render.view.GXIRootView import com.alibaba.gaiax.render.view.GXViewTreeCreator import com.alibaba.gaiax.render.view.GXViewTreeUpdate import com.alibaba.gaiax.utils.GXGlobalCache +import com.alibaba.gaiax.utils.GXLog /** * @suppress @@ -37,15 +38,30 @@ class GXRenderImpl { val rootNode = GXNodeTreePrepare.create(gxTemplateContext) gxTemplateContext.rootNode = rootNode rootNode.stretchNode.layoutByPrepareView?.let { - GXGlobalCache.instance.putLayoutForPrepareView(gxTemplateContext.templateItem, it) + GXGlobalCache.instance.putLayoutForPrepareView( + gxTemplateContext, + gxTemplateContext.templateItem, + it + ) } rootNode.release() } fun createViewOnlyNodeTree(gxTemplateContext: GXTemplateContext): GXNode { val rootLayout = - GXGlobalCache.instance.getLayoutForPrepareView(gxTemplateContext.templateItem) + GXGlobalCache.instance.getLayoutForPrepareView( + gxTemplateContext, + gxTemplateContext.templateItem + ) ?: throw IllegalArgumentException("root layout is null") + + if (GXLog.isLog()) { + GXLog.e( + gxTemplateContext.tag, + "traceId=${gxTemplateContext.traceId} tag=createViewOnlyNodeTree rootLayout=${rootLayout}" + ) + } + // Create a virtual node tree val rootNode = GXNodeTreeCreator.create(gxTemplateContext, rootLayout) gxTemplateContext.rootNode = rootNode @@ -67,6 +83,13 @@ class GXRenderImpl { } fun bindViewDataOnlyNodeTree(gxTemplateContext: GXTemplateContext) { + if (GXLog.isLog()) { + GXLog.e( + gxTemplateContext.tag, + "traceId=${gxTemplateContext.traceId} tag=bindViewDataOnlyNodeTree" + ) + } + // Resetting the Template Status gxTemplateContext.isDirty = false @@ -75,6 +98,13 @@ class GXRenderImpl { } fun bindViewDataOnlyViewTree(gxTemplateContext: GXTemplateContext) { + if (GXLog.isLog()) { + GXLog.e( + gxTemplateContext.tag, + "traceId=${gxTemplateContext.traceId} tag=bindViewDataOnlyViewTree" + ) + } + val rootNode = gxTemplateContext.rootNode ?: throw IllegalArgumentException("RootNode is null(bindViewDataOnlyViewTree) gxTemplateContext = $gxTemplateContext") @@ -86,6 +116,13 @@ class GXRenderImpl { } fun resetViewDataOnlyViewTree(gxTemplateContext: GXTemplateContext) { + if (GXLog.isLog()) { + GXLog.e( + gxTemplateContext.tag, + "traceId=${gxTemplateContext.traceId} tag=resetViewDataOnlyViewTree" + ) + } + GXNodeTreeUpdate.resetView(gxTemplateContext) } diff --git a/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/render/node/GXNodeTreeCreator.kt b/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/render/node/GXNodeTreeCreator.kt index ef5a8f0e6..bb03e364f 100644 --- a/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/render/node/GXNodeTreeCreator.kt +++ b/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/render/node/GXNodeTreeCreator.kt @@ -21,6 +21,7 @@ import com.alibaba.gaiax.GXTemplateEngine import com.alibaba.gaiax.context.GXTemplateContext import com.alibaba.gaiax.template.GXLayer import com.alibaba.gaiax.template.GXTemplateInfo +import com.alibaba.gaiax.utils.GXLog /** * 用于创建虚拟节点树 diff --git a/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/render/node/GXNodeTreePrepare.kt b/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/render/node/GXNodeTreePrepare.kt index 2b4a55cdc..9db1a39ef 100644 --- a/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/render/node/GXNodeTreePrepare.kt +++ b/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/render/node/GXNodeTreePrepare.kt @@ -38,6 +38,7 @@ object GXNodeTreePrepare { ) rootNode.isRoot = true GXNodeUtils.computeNodeTreeByPrepareView( + gxTemplateContext, rootNode, Size(gxTemplateContext.size.width, gxTemplateContext.size.height) ) return rootNode diff --git a/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/render/node/GXNodeTreeUpdate.kt b/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/render/node/GXNodeTreeUpdate.kt index 882b5e08a..41a82c44c 100644 --- a/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/render/node/GXNodeTreeUpdate.kt +++ b/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/render/node/GXNodeTreeUpdate.kt @@ -32,7 +32,7 @@ import com.alibaba.gaiax.context.GXTemplateContext import com.alibaba.gaiax.render.node.text.GXDirtyText import com.alibaba.gaiax.render.node.text.GXFitContentUtils import com.alibaba.gaiax.render.node.text.GXHighLightUtil -import com.alibaba.gaiax.render.view.* +import com.alibaba.gaiax.render.view.GXIViewBindData import com.alibaba.gaiax.render.view.basic.GXIImageView import com.alibaba.gaiax.render.view.basic.GXProgressView import com.alibaba.gaiax.render.view.basic.GXText @@ -41,6 +41,19 @@ import com.alibaba.gaiax.render.view.container.GXContainer import com.alibaba.gaiax.render.view.container.GXContainerViewAdapter import com.alibaba.gaiax.render.view.container.slider.GXSliderView import com.alibaba.gaiax.render.view.container.slider.GXSliderViewAdapter +import com.alibaba.gaiax.render.view.setBackgroundColorAndBackgroundImageWithRadius +import com.alibaba.gaiax.render.view.setDisplay +import com.alibaba.gaiax.render.view.setGridContainerDirection +import com.alibaba.gaiax.render.view.setGridContainerItemSpacingAndRowSpacing +import com.alibaba.gaiax.render.view.setHidden +import com.alibaba.gaiax.render.view.setHorizontalScrollContainerLineSpacing +import com.alibaba.gaiax.render.view.setOpacity +import com.alibaba.gaiax.render.view.setOverflow +import com.alibaba.gaiax.render.view.setRoundCornerRadiusAndRoundCornerBorder +import com.alibaba.gaiax.render.view.setScrollContainerDirection +import com.alibaba.gaiax.render.view.setScrollContainerPadding +import com.alibaba.gaiax.render.view.setSpanSizeLookup +import com.alibaba.gaiax.render.view.setVerticalScrollContainerLineSpacing import com.alibaba.gaiax.template.GXCss import com.alibaba.gaiax.template.GXLayer import com.alibaba.gaiax.template.GXStyle @@ -50,6 +63,7 @@ import com.alibaba.gaiax.template.animation.GXLottieAnimation import com.alibaba.gaiax.template.animation.GXPropAnimationSet import com.alibaba.gaiax.template.factory.GXExpressionFactory import com.alibaba.gaiax.template.utils.GXTemplateUtils +import com.alibaba.gaiax.utils.GXLog /** * @suppress @@ -57,6 +71,13 @@ import com.alibaba.gaiax.template.utils.GXTemplateUtils object GXNodeTreeUpdate { fun buildNodeLayout(gxTemplateContext: GXTemplateContext) { + if (GXLog.isLog()) { + GXLog.e( + gxTemplateContext.tag, + "traceId=${gxTemplateContext.traceId} tag=buildNodeLayout" + ) + } + val rootNode = gxTemplateContext.rootNode ?: throw IllegalArgumentException("RootNode is null(buildNodeLayout)") val templateData = @@ -96,12 +117,19 @@ object GXNodeTreeUpdate { templateData: JSONObject, size: Size ) { + if (GXLog.isLog()) { + GXLog.e( + gxTemplateContext.tag, + "traceId=${gxTemplateContext.traceId} tag=updateNodeTreeLayout" + ) + } + // 更新布局 updateNodeTreeLayout(gxTemplateContext, gxNode, templateData) // 计算布局 if (gxTemplateContext.isDirty) { - GXNodeUtils.computeNodeTreeByBindData(gxNode, size) + GXNodeUtils.computeNodeTreeByBindData(gxTemplateContext, gxNode, size) } } @@ -109,6 +137,14 @@ object GXNodeTreeUpdate { gxTemplateContext: GXTemplateContext, rootNode: GXNode, size: Size ) { if (gxTemplateContext.dirtyTexts?.isNotEmpty() == true) { + + if (GXLog.isLog()) { + GXLog.e( + gxTemplateContext.tag, + "traceId=${gxTemplateContext.traceId} tag=updateNodeTreeLayoutByDirtyText" + ) + } + var isTextDirty = false gxTemplateContext.dirtyTexts?.forEach { val isDirty = updateTextLayoutByFitContentByDirtyText( @@ -122,7 +158,7 @@ object GXNodeTreeUpdate { } gxTemplateContext.dirtyTexts?.clear() if (isTextDirty) { - GXNodeUtils.computeNodeTreeByBindData(rootNode, size) + GXNodeUtils.computeNodeTreeByBindData(gxTemplateContext, rootNode, size) } } } @@ -320,6 +356,13 @@ object GXNodeTreeUpdate { gxFlexBox.sizeForDimension?.height = it isDirty = true } + + if (GXLog.isLog()) { + GXLog.e( + gxTemplateContext.tag, + "traceId=${gxTemplateContext.traceId} tag=updateContainerLayout containerSize=${containerSize}" + ) + } } } else if (gxNode.isGridType()) { @@ -1033,21 +1076,26 @@ object GXNodeTreeUpdate { gxNode.isCustomViewType() -> bindCustom( view, gxNode.templateNode, templateData ) + gxNode.isTextType() -> bindText( gxTemplateContext, view, css, layer, gxNode.templateNode, templateData ) + gxNode.isRichTextType() -> bindRichText( gxTemplateContext, view, css, layer, gxNode.templateNode, templateData ) + gxNode.isIconFontType() -> bindIconFont(view, gxNode.templateNode, templateData) gxNode.isImageType() -> bindImage(view, gxNode.templateNode, templateData) gxNode.isProgressType() -> bindProgress(view, gxNode.templateNode, templateData) gxNode.isScrollType() || gxNode.isGridType() -> bindScrollAndGrid( gxTemplateContext, view, gxNode, gxNode.templateNode, templateData ) + gxNode.isSliderType() -> bindSlider( gxTemplateContext, view, gxNode, gxNode.templateNode, templateData ) + gxNode.isViewType() || gxNode.isGaiaTemplateType() -> bindView( view, gxNode.templateNode, templateData ) diff --git a/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/render/node/GXNodeUtils.kt b/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/render/node/GXNodeUtils.kt index cf97d80aa..aaf50cf17 100644 --- a/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/render/node/GXNodeUtils.kt +++ b/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/render/node/GXNodeUtils.kt @@ -23,9 +23,18 @@ import com.alibaba.fastjson.JSONArray import com.alibaba.fastjson.JSONObject import com.alibaba.gaiax.GXRegisterCenter import com.alibaba.gaiax.GXTemplateEngine -import com.alibaba.gaiax.context.* +import com.alibaba.gaiax.context.GXTemplateContext +import com.alibaba.gaiax.context.getLayoutForScroll +import com.alibaba.gaiax.context.getMaxHeightLayoutForScroll +import com.alibaba.gaiax.context.getMinHeightLayoutForScroll +import com.alibaba.gaiax.context.initLayoutForScroll +import com.alibaba.gaiax.context.initNodeForScroll +import com.alibaba.gaiax.context.isExistForScroll +import com.alibaba.gaiax.context.putLayoutForScroll +import com.alibaba.gaiax.context.putNodeForScroll import com.alibaba.gaiax.template.GXTemplateKey import com.alibaba.gaiax.utils.GXGlobalCache +import com.alibaba.gaiax.utils.GXLog import com.alibaba.gaiax.utils.getStringExt import kotlin.math.ceil import kotlin.math.max @@ -40,11 +49,21 @@ object GXNodeUtils { internal const val ITEM_CONFIG = "${GXTemplateKey.GAIAX_DATABINDING_ITEM_TYPE}.${GXTemplateKey.GAIAX_DATABINDING_ITEM_TYPE_CONFIG}" - fun computeNodeTreeByBindData(gxNode: GXNode, size: Size) { + fun computeNodeTreeByBindData( + gxTemplateContext: GXTemplateContext, + gxNode: GXNode, + size: Size + ) { val stretchNode = gxNode.stretchNode.node ?: throw IllegalArgumentException("stretch node is null, please check!") val layout = stretchNode.safeComputeLayout(size) composeStretchNodeByBindData(gxNode, layout) + if (GXLog.isLog()) { + GXLog.e( + gxTemplateContext.tag, + "traceId=${gxTemplateContext.traceId} tag=computeNodeTreeByBindData layout=${layout}" + ) + } } private fun composeStretchNodeByBindData(gxNode: GXNode, layout: Layout) { @@ -57,11 +76,21 @@ object GXNodeUtils { } } - fun computeNodeTreeByPrepareView(gxNode: GXNode, size: Size) { + fun computeNodeTreeByPrepareView( + gxTemplateContext: GXTemplateContext, + gxNode: GXNode, + size: Size + ) { val stretchNode = gxNode.stretchNode.node ?: throw IllegalArgumentException("stretch node is null, please check!") val layout = stretchNode.safeComputeLayout(size) composeStretchNodeByPrepareView(gxNode, layout) + if (GXLog.isLog()) { + GXLog.e( + gxTemplateContext.tag, + "traceId=${gxTemplateContext.traceId} tag=computeNodeTreeByPrepareView layout=${layout}" + ) + } } private fun composeStretchNodeByPrepareView(gxNode: GXNode, layout: Layout) { @@ -109,7 +138,10 @@ object GXNodeUtils { // Improve: 如果之前计算过,并且内容高度都一样,那么直接使用缓存计算。在横滑容器数据量较大的情况下,会节省一些时间。 if (GXGlobalCache.instance.isExistForTemplateItem(itemTemplateItem)) { - val itemLayout = GXGlobalCache.instance.getLayoutForTemplateItem(itemTemplateItem) + val itemLayout = GXGlobalCache.instance.getLayoutForTemplateItem( + gxTemplateContext, + itemTemplateItem + ) return computeScrollContainerSize( gxNode, itemLayout, gxContainerData ) @@ -136,7 +168,11 @@ object GXNodeUtils { if (maxItemLayout != null && minItemLayout != null && maxItemLayout.height == minItemLayout.height) { // 如果相同,代表没有不一样的高度,下次可以只计算一次 - GXGlobalCache.instance.putLayoutForTemplateItem(itemTemplateItem, maxItemLayout) + GXGlobalCache.instance.putLayoutForTemplateItem( + gxTemplateContext, + itemTemplateItem, + maxItemLayout + ) } return computeScrollContainerSize( @@ -173,9 +209,11 @@ object GXNodeUtils { gxNode.isScrollType() -> computeScrollItemContainerSize( gxTemplateContext, gxNode, itemPosition, itemData ) + gxNode.isGridType() -> computeGridItemContainerSize( gxTemplateContext, gxNode, itemData, itemPosition ) + else -> null } } @@ -556,10 +594,12 @@ object GXNodeUtils { gxGridConfig.isVertical -> { Size(containerWidth - (padding.left + padding.right), null) } + gxGridConfig.isHorizontal -> { // TODO: Grid横向处理不支持,此种情况暂时不做处理,很少见 Size(null, null) } + else -> { Size(null, null) } @@ -630,10 +670,12 @@ object GXNodeUtils { Size(width, null) } + gxGridConfig.isHorizontal -> { // TODO: Grid横向处理不支持,此种情况暂时不做处理,很少见 Size(null, null) } + else -> { Size(null, null) } @@ -649,6 +691,7 @@ object GXNodeUtils { return Size(it * nodeWith.value, null) } } + else -> { gxTemplateContext.size.width?.let { return Size(it, null) diff --git a/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/utils/GXGlobalCache.kt b/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/utils/GXGlobalCache.kt index c933c33a9..0aa76c4b8 100644 --- a/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/utils/GXGlobalCache.kt +++ b/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/utils/GXGlobalCache.kt @@ -2,17 +2,33 @@ package com.alibaba.gaiax.utils import app.visly.stretch.Layout import com.alibaba.gaiax.GXTemplateEngine -import com.alibaba.gaiax.render.node.GXNode +import com.alibaba.gaiax.context.GXTemplateContext class GXGlobalCache { - fun putLayoutForPrepareView(key: GXTemplateEngine.GXTemplateItem, value: Layout) { + fun putLayoutForPrepareView( + gxTemplateContext: GXTemplateContext, + key: GXTemplateEngine.GXTemplateItem, + value: Layout + ) { layoutForPrepareView[key.key()] = value if (GXLog.isLog()) { - GXLog.e("putLayoutForPrepareView key=${key.hashCode()} value=$value") + GXLog.e( + gxTemplateContext.tag, + "traceId=${gxTemplateContext.traceId} tag=putLayoutForPrepareView key=${key.hashCode()} value=$value" + ) } } - fun getLayoutForPrepareView(key: GXTemplateEngine.GXTemplateItem): Layout? { + fun getLayoutForPrepareView( + gxTemplateContext: GXTemplateContext, + key: GXTemplateEngine.GXTemplateItem + ): Layout? { + if (GXLog.isLog()) { + GXLog.e( + gxTemplateContext.tag, + "traceId=${gxTemplateContext.traceId} tag=getLayoutForPrepareView key=${key.hashCode()}" + ) + } return layoutForPrepareView[key.key()] } @@ -36,15 +52,35 @@ class GXGlobalCache { layoutForPrepareView.clear() } - fun putLayoutForTemplateItem(key: GXTemplateEngine.GXTemplateItem, value: Layout) { + fun putLayoutForTemplateItem( + gxTemplateContext: GXTemplateContext, + key: GXTemplateEngine.GXTemplateItem, + value: Layout + ) { layoutForTemplateItem[key.key()] = value + if (GXLog.isLog()) { + GXLog.e( + gxTemplateContext.tag, + "traceId=${gxTemplateContext.traceId} tag=putLayoutForTemplateItem key=${key.key()} value=${value}" + ) + } } - fun getLayoutForTemplateItem(key: GXTemplateEngine.GXTemplateItem): Layout? { - return layoutForTemplateItem[key.key()] + fun getLayoutForTemplateItem( + gxTemplateContext: GXTemplateContext, + key: GXTemplateEngine.GXTemplateItem + ): Layout? { + val value = layoutForTemplateItem[key.key()] + if (GXLog.isLog()) { + GXLog.e( + gxTemplateContext.tag, + "traceId=${gxTemplateContext.traceId} tag=getLayoutForTemplateItem key=${key.key()} value=${value}" + ) + } + return value } - fun isExistForTemplateItem(key: GXTemplateEngine.GXTemplateItem): Boolean { + fun isExistForTemplateItem(key: GXTemplateEngine.GXTemplateItem): Boolean { return layoutForTemplateItem.containsKey(key.key()) } diff --git a/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/utils/GXLog.kt b/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/utils/GXLog.kt index 7501480a9..e39ed3b16 100644 --- a/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/utils/GXLog.kt +++ b/GaiaXAndroid/src/main/kotlin/com/alibaba/gaiax/utils/GXLog.kt @@ -26,7 +26,7 @@ object GXLog { private const val TAG = "Alibaba-GaiaX" fun e(tag: String?, msg: String?) { - longE(tag ?: TAG, msg ?: "") + longE(if (tag.isNullOrEmpty()) TAG else tag, msg ?: "") } fun e(msg: String?) {