https://kotlinlang.org logo
#compose-desktop
Title
# compose-desktop
o

Olivier Patry

03/06/2023, 7:55 PM
Do you have recommendation on how to architect
Menu
content, state & action (of the
WindowScope
) for different "screens"? Currently, I have a single desktop specific
MyApp
class which is responsible to • provide
main()
• create
application {}
• create main
Window
composable • delegate main window content to a
MyAppContent
composable The
MyAppContent
then handles the navigation logic between "screens" (using #decompose library (but whatever?)) Depending on the navigation state machine, a proper
FooScreen
composable is chosen to display screen content. Each of these "screen" have its own
FooViewModel
to drive logic and UI binding. Now, back to the menu topic. AFAIK, the menu must be managed within the
WindowScope
and thus currently in my main top level
MyApp
. For general purpose menu items like preferences, it's fine but several items are contextual to the screen being displayed. I need to • decide if an item must be displayed or not • decide if the item is enabled or disabled • provide a business logic callback to call when item is clicked (most likely to a
FooViewModel
entity) I'm not sure what would work well in this setup, ideally isolating desktop specific stuff like that outside main screens meant to be reusable at least on Android and maybe more later. If "duplicating" some boilerplate of "screens" would be needed to combine desktop feature and reusing code in other platforms, it's fine though.
d

Dima Avdeev

03/06/2023, 9:00 PM
Compose Multiplatform is a UI and state manager framework. Actions, that you want to add on the menu, - may be a part of the State of your application. State may contain all logic that you need to display on the top level of your application. If you navigate in application, the State may change and actions will change also.
o

Olivier Patry

03/06/2023, 9:11 PM
Would it mean that I should model my menu content (an item with a label, a shortcut, an enabled state and an action callback mainly) and expose it as an observed state in my main app code, right? If so, this would mean to hoist this state into my screens and populate it from there while listening to my
FooViewModel
state, right? Doing that would spread menu logic into reusable screen but it's not necessarily a problem, it wouldn't be coupled to desktop specific stuff if I use my own "menu item data model", only useless elsewhere (or reused another way). So basically I would have something like
Copy code
MyApp {
  val menuState = remember { mutableStateOf(MyMenuDataModel()) }

  Window {
    // interpret menuState/MyMenuDataModel as Menu and Item tree
    Menu {
      Item() {}
    }

    MyAppContent(menuState)
  }
}

@Composable
fun MyAppContent(menuState: MutableState<MyMenuDataModel>) {
  // depending on state machine, choose right screen composable
  
  FooScreen(menuState)
}

@Composable
fun FooScreen(menuState: MutableState<MyMenuDataModel>) {
   val viewModel = ...
   val canDoSomething = viewModel.canDoSomething.collectAsState()

   menuState.update(MyMenuDataModel.FooScreen(..., canDoSomething, ...))

   ...
}
Is this something like that you were suggesting?
d

Dima Avdeev

03/06/2023, 9:13 PM
For example - it's a good first solution.
But there are a lot of permissions to downside hierarchy
I mean, the downside hierarchy may be bigger than only one FooScreen(menuState). You may want to continue use menuState in another parts.
And it's hard to control
Instead of using direct menuState, you can create menu state dynamically, based on the current Application state (Opened screens, selected settings, etc.).
For example, your application may have 2 separate screens: 1 - List of notes, 2 - Edit current note.
You know that on first screen you need actions to create new note
On second screen - you need actions to save or delete note
Copy code
data class AppState(val screen: Screen)
sealed interface Screen {
   object Notes: Screen
   object Edit: Screen
}
And menuItem may look as:
Copy code
val menuItems = listOf(Item("AlwaysMenuItem")) + when(state.screen) {
    is Notes -> listOf(Item("Add note"))
    is Edit -> listOf(Item("Save"), Item("Remove"))
}
And build your menu based on that calculated state
So, the main state is AppState that contains opened Screens, selected options, etc. Menu state is a derived state (or calculated state)
AppState in this sample should be located on the top of Composable hierarchy.
o

Olivier Patry

03/06/2023, 9:28 PM
Yes, I think I get the idea
I'll follow up in that direction, sounds promising, I'll see where it goes more concretely and if it scales
d

Dima Avdeev

03/06/2023, 9:28 PM
It's only one of possible solutions. You can choose that you want.
o

Olivier Patry

03/06/2023, 9:30 PM
I might also have to inject a "parent" menu "key" in my items so that I can aggregate several items from different sources (eg. "AlwaysMenuItem"'s parent and screen item in your example, for things like "Edit" or "File" top menus)
In terms of "injection", do you have an opinion which is the best suited to this use case: • inject the state in each nested composable requiring it • make it accessible globally with
CompositionLocalProvider
-like things
d

Dima Avdeev

03/07/2023, 12:08 PM
I think it is better to: inject the state in each nested composable requiring it
16 Views