<@UHAJKUSTU> How do you think what is the best app...
# decompose
e
@Arkadii Ivanov How do you think what is the best approach to implement the following use case. There is a detail screen, which is quite deep inside the navigation (3 levels of components hierarchy including bottom tabs) and this screen should be full screen media player. In the past it was handy to implement such screen with a separate activity with it’s own sensor behaviour. But it is not much interesting in single-activity compose application using decompose. So I see 2 solutions: 1. Implement this screen on the root level (easy to go fullscreen, because tabs are deeper). But semantically this screen doesn’t belong to the root and is controlled by 3-level component. 2. Implement some weird interface, which can remove any decoration, including top bars and bottom bars and pass it to the component for calling when the fullscreen detail screen is entered and left. This looks more consistent semantically, but doesn’t look elegant in the implementation. Are there any other options I should consider and what would you choose?
a
When you open this screen as an Activity, it's essentially scoped at the system level. We can think about it as a top-most stack. So a semantical equivalent with single Activity is to also scope that screen somewhere at the top-most level. With Decompose we can have more granular levels, so not necessarily root, could be something like "signed-in" scope, etc. Convenience - is another concern. An easiest solution indeed is to put all fullscreen components at that level (e.g. root). This should work fine especially if there are not so many screens. It might make sense to extract the navigation logic to a separate component/class, to make it the only responsibility of the component. You can also try the "Portal" approach (comes from React). I created a sample snippet a while ago. This can be useful if you want to remove the knowledge about the fullscreen components from root, and delegate this to the calling child components instead. Yet scope those components at the root level.
e
Didn’t think about the portal approach, it’s interesting. It is not more complex in implementation/structure than option 2 with “weird delegate”, but much more logical. Takes the best from both options and combines them. And the navigation should also work, because as I understand when user leaves the portal the root component returns to the previous stack element which contains the previous screen with all it’s hierarchy .Thank you! 💪🔥 I will definitely go in this direction.
a
Awesome! Let me know if there are any issues.
👍 1
e
@Arkadii Ivanov I’m trying to implement the portal pattern but I am not sure how to make not a specific but some universal portal, which can show different components. In my app I have a lot of “detail” screens all of which should be fullscreen (without tab navigation). Implementing specific portals for each is a lot of work and it also introduces leak of concerns from the feature modules to the root classes. For example if I make a general portal, it needs some object to be passed along with when portal is created and this object should be able to be rendered from the root composable. It could be a specific component, but in this case it requires that the root will specify all possible portal users. Maybe I would better go with some sealed interface/class here, but Kotlin restricts sealed classes to be inside one module and my features are located in different gradle-modules. Should I go with some core interface providing a method like
@Composable fun render()
? Do you know any better solution?
I don’t like the interface approach, because I will have to add dependency to Compose to the feature modules, which don’t have it right now. I can separate it even further to avoid adding Compose dependency to the feature modules. I can inject some other interface from the root module to the child feature modules and inject that interface to the component which should be inside the portal. And then map that child-interfaces in the root module in the UI to the specific UI-implementation. But it makes the whole architecture too complicated for my taste.
Also I am a bit concerned that the feature looses control over it’s child when it needs something to be opened via portal. For it the screen is just a regular child, added to the stack. but for some reason it has to use some portal instead of usual stack navigation and doesn’t control how this child will be closed. If the child has back button and it passes the UI event to the feature-component, the feature component can’t just pop the stack, it has to ask the portal to do it. Also it looks like feature-component is getting some UI-details mixed into the logic in this case, because if the UI changes and the screen needs to have bottom tabs in the future it will change not only the UI part, but also the feature-component (and even the root component).
a
Sadly, I don't have any solution in my head right now. We all need to experiment sometimes. I will update here if I have something.
e
It looks like I have to change approach and not think about detail screens like a part of the feature-component which is opening it. Probably it is better to forward this event to the root and ask it to open the details screen (without portals, “oficially”). The cons is that the root get more feature-specific responsibilities, but the pros is that it can be done in the same paradigm, as usual. Root will handle back event and return the user to the feature component which opened the details screen.
Now I try to think in the opposite direction and keep the components and it’s hierarchy as UI-independent as possible. And I think I came across interesting solution on only Compose level
I will post it shortly
a
Probably it is better to forward this event to the root and ask it to open the details screen
Exactly! I feel the same.
This would also remove interdependencies between "details" screen, e.g. when Details1 needs to open Details2 and Details2 needs to open Details1.
e
this is the opposite solution. The context: the feature component holds both main and details screens, manages it’s navigation by it’s own.
Copy code
@Composable
fun Root(
    contentRootComponent: ContentRootComponent
) {
    fun showContent(content: @Composable () -> Unit, showBottomNavigation: Boolean) {
        if (showBottomNavigation) {
            BottomNavigationScreen(
                content = content
            )
        } else {
            content()
        }
    }

    Children(
        modifier = modifier.padding(innerpadding),
        stack = contentRootComponent.childStack,
        animation = stackAnimation(fade() + scale()),
    ) {
        when (val child = it.instance) {
            is Child.A -> {
                ChildAContent(::showContent)
            }
            ...
        }
    }
}

@Composable
fun ChildAContent(
    showContentHandler: @Composable (content: @Composable () -> Unit, showBottomNavigation: Boolean) -> Unit
) {
    showContentHandler(
        { ChildAContent() },
        false
    )

}

@Composable
fun ChildAContent(){

}
So the composition is inverted here and the child decides if the parent should surround it with bottom navigation or not.
In this case this becomes implementation detail of the particular screen Compose function, which could be easily changed in any moment and doesn’t affect the component hierarchy.
pros: • feature component knows nothing about parent navigation (bottom navigation), no interfaces should be passed to it, no portals, no callbacks. It just works with own children. • root component also doesn’t know anything about child specific and if it should be part of bottom navigation or not • all screen specific, regarding bottom navigation is moved to the UI layer and to the particular screen implementation and can be changed for each screen just by changing a flag cons: • this tricky code in the root composable (minor IMHO)
@Arkadii Ivanov what do you think?
a
TBH I would prefer the classic decomposition with Bottom Bar. I think it's better to extract bottom bar components into a separate component, instead of having all components in one stack. Also, it's easier to use bringToFront in the bottom bar component.
e
I think I don't follow, sorry. I do use decomposition with bottom bar. The root component has child stack navigation, with bringToFront. The only change here is that usually there is root function, which "draws" bottom bar and adds the child as a content to that bottom bar. In my case it is the same generally, but instead the bottom bar itself is wrapped in a lambda, which is passed inside the child function. And besides it has a flag, which inside that lambda can instead of content inside bottom bar put just content.
a
Well, usually there is a separate function like BottomBarContent that adds a bottom bar under its children shown in a stack. And the root content shows BottomBarContent plus other fullscreen components. Nested stacks look better to me. But if you find your approach better, then go with it. 🙂
e
What do you mean by nested stacks? What is nested?
a
Root has a stack of BottomBarComponent and other fullscreen components. BottomBarContent has another stack of tab components.
e
Got it. The more I think about it the more I suppose that the UI hierarchy is not always the same as logical/semantical/data flow hierarchy. In this case we have a detail screen which logically can belong to the root in some cases and in some cases can be very closely tight to the component inside the tab. So in general being Fullscreen doesn't mean that the screen belongs to the root in the logical/navigation tree. It may belong, but it is not the definitely true. So in general it should be a way to keep the detail screen in the child component and have it fullscreen
If my reasoning is correct, that would mean that the choice where to put the details screen should depend on the navigational/semantical logic, but not the UI restrictions. And it can be both in the root and in the child depending on the particular case
a
So in general being Fullscreen doesn't mean that the screen belongs to the root in the logical/navigation tree
I think it's kinda subjective and it depends. From my point of view, removing the dependency between screens in a stack (a flow of screens) is almost always better. E.g. if your child (tab) screen needs to open a details screen, then actually it doesn't care what the actual screen it should be - inversion of control. But again, the Portal pattern exists exactly because sometimes it might be useful. So use it with care. 🙂
e
I agree about IoC, but you can’t just remove the knowledge about the logical dependency between screens from the app completely. Yes, if there is some component which
may
not know about dependency and we remove that dependency it is always good. But in this case we can’t just remove it completely. If the feature component looses this knowledge, it just moves to some over point of the app. In this case the root, which now decides what to do on some event which should lead to opening of the details screen. So in one place we remove dependency and in another one we put that dependency. We don’t decrease coupling in general, but just remove coupling in one place and add it in another place. That means that we still will need to decide where this coupling is more justified and where less and choose from this decision. Also besides IoC there is a factor of complexity. And if there are 2 places where the dependency/coupling can go we should also consider the resulting complexity as not the main factor but still important, especially if there are no strong reasons why that exact components should not be coupled (we should keep following something like SOLID here I suppose).
a
In this case the root, which now decides what to do on some event which should lead to opening of the details screen.
Yes, and this is the super-power. It is the integration point (the upper level) that decides how to handle the action. If it can't, then it bubbles the decision further up. And it's also the only way to avoid possible circular dependencies between features.
Also besides IoC there is a factor of complexity.
True! I consider it as a trade-off.
👍 1