What is the proper way to handle show and hide UI, without having to force rebuild layout everytime?

I have a bunch of UI gameobjects with nested child gameobjects that are set active and inactive depending on the dynamic content. What I find is that I always have to call LayoutRebuilder.ForceRebuildLayoutImmediate so that the layout is updated correctly after the hierarchy initialization is completed.

It’s very tedious, but it was at least working, until I noticed there is some unexpected side effect. When there are 2 sibling gameobjects X and Y, if X undergoing explicit ForceRebuildLayoutImmediate, and the other Y does not need any, the Y’s layout get updated too, and the scrollrect/scrollbar in Y gets reset. I think the problem is that both X and Y share a parent Horizontal Layout Group.

I want to redesign the UI structure so that I don’t have to keep calling ForceRebuildLayoutImmediate and suffer from side effects. Is there any one with experience in dealing with this quirking behavior?

Thanks.

This is what I am using at the moment

public class UIBase : MonoBehaviour {

        public UnityEvent LayoutRebuildEvent;


        public virtual void Init(object args) {
          
            // at this point, this gameobject is inactive

            // ... init content
            // show some gameobject and hide some game objects
            // m_gameObjectA.SetActive(true);
            // m_gameObjectB.SetActive(false); etc.
        }

        public void Show() {
            this.gameObject.SetActive(true);
        }

        void OnEnable() {
            // this is triggered by Unity, due to this.gameObject.SetActive(true)
            StartCoroutine(_OnEnable());
        }

        IEnumerator _OnEnable() {
            yield return new WaitForEndOfFrame();

            // bubble the layout event up to the parent
            // parent will then call ForceRebuildLayoutImmediate() on children then itself (sequence is important)
            // which will propogate the force layout up the tree from grand-grand-child to grand-child to child to parent etc.
            RequestLayoutRebuild();
        }

        public virtual void ForceRebuildLayoutImmediate() {
            if(this.gameObject.activeSelf) {
                LayoutRebuilder.ForceRebuildLayoutImmediate((RectTransform)transform);
            }
        }

        protected void RequestLayoutRebuild() {
            LayoutRebuildEvent?.Invoke();
        }
    }

After some many more hours of debugging, I realised that when a TextMeshProGUI.text changes dynamically while the UGUI is active on the screen, e.g. replenishing health via update event, and… IF the direct parent of TextMeshProGUI is a LayoutGroup e.g., VerticalLayoutGroup, then it will cause a propagation of LayoutRebuild throughout entire Canvas, and cause a ScrollRect, even one that is remotely away and deeply nested, in separate gameobject from the TextMeshProGUI, to reset its scroll position.

The solution is to make all dynamic text labels into subchild under another container gameobject that does not have any layout group.

BUT my question remains, i.e. if we need to ForceRebuildLayout, how to prevent the scrollrect scroll position from resetting? I want to keep the scroll position especially when ForceRebuildLayout is not call on itself or any of its children.

EDIT:
After few more hours of debugging. I think I finally understand how it works.

TLDR: Use empty gameobject or gameobects without LayoutGroups to break the flow of the Layout recalculations that inevitably reset your scrollrect and other UGUI element unexpectedly.

I was so misled by other posts advising to use LayoutGroups all over the place. It cannot be more wrong.

#1 TextMeshProGUI text changes will trigger layout rebuild. If layout rebuild is not intended, wrap it in an empty gameobject. Use the empty game object’s to RectTransform to permanantly reserve a screen real estate for your text label so that the the layout rebuilding will not propogate to its parent, up to the canvas, and back down to other remote game objects within the same canvas.

#2 When ever gameobject.setActive() is called on a gameobject with LayoutGroup, i.e. VerticalLayoutGroup, Horizontal Layout Group, etc., LayoutRebuild is trigger. When you are hiding and showing entire UI component, like a dialog box, you don’t want to trigger entire layout tree rebuilds with setActive(). Again, wrap your top level gameobject with an empty gameobject. The empty gameobject “reserves” the screen space for your underlying dialog box so that no additional lay calculation is needed before displaying your dialog box.