Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Return PopResult from OverlayHost.showFullScreenOverlay() #1447

Merged
merged 6 commits into from
Jun 7, 2024

Conversation

aschulz90
Copy link
Contributor

showFullScreenOverlay() now returns the PopResult that was popped by the shown Screen.

I had to change Overlay's generic parameter from Any to Any? to account for the nullable PopResult. I couldn't think of any obvious reason for why overlays shouldn't be able to also return null values. This shouldn't be a breaking change, as the generic parameter is now less restrictive!?

@ZacSweers
Copy link
Collaborator

What's the difference between a full screen overlay and just pushing a new screen onto the back stack? I'm also not a fan of breaking the return type contract of overlays just to support this case

@aschulz90
Copy link
Contributor Author

aschulz90 commented Jun 4, 2024

My usecase is, that I have dialogs that are backed by Screen's, to seaparate their input validation and stuff to presenters. And also get a result after their completion.

Because they are dialogs, I can't just push them onto the backstack, because then the underlying screen wouldn't be visible anymore.

Simplified Example:

data class Group(
    val id: String,
    val name: String,
)

@CommonParcelize
data object CreateGroupDialogScreen: Screen {
    sealed interface Result: PopResult {
        data class Created(
            val group: Group,
        ): Result
        data object Canceled: Result
    }

    data class State(
        val error: String?,
        val eventSink: (Event) -> Unit = {},
    ) : CircuitUiState

    sealed interface Event : CircuitUiEvent {
        data class Submit(
            val name: String,
        ): Event
        data object Cancel: Event
    }
}

@Composable
fun CreateGroupDialogScreenPresenter(
    navigator: Navigator,
): CreateGroupDialogScreen.State {
    var error: String? by remember { mutableStateOf(null) }

    return CreateGroupDialogScreen.State(
        error = error,
    ) {
        when(it) {
            CreateGroupDialogScreen.Event.Cancel -> navigator.pop(CreateGroupDialogScreen.Result.Canceled)
            is CreateGroupDialogScreen.Event.Submit -> {
                if(it.name.isBlank()) {
                    error = "Required"
                    return@State
                }

                navigator.pop(
                    CreateGroupDialogScreen.Result.Created(
                        group = Group(
                            id = "123456",
                            name = it.name,
                        )
                    )
                )
            }
        }
    }
}

@Composable
fun CreateGroupDialog(
    modifier: Modifier,
    state: CreateGroupDialogScreen.State,
) {
    Dialog(
        onDismissRequest = {state.eventSink(CreateGroupDialogScreen.Event.Cancel)},
    ) {
        var name: TextFieldValue by rememberRetained { mutableStateOf(TextFieldValue()) }
        Column(
            modifier = modifier,
        ) {
            TextField(
                value = name,
                onValueChange = {name = it}
            )
            
            state.error?.let { 
                Text(it)
            }
            
            Button(
                onClick = {
                    state.eventSink(CreateGroupDialogScreen.Event.Submit(name = name.text))
                }
            ) {
                Text("Submit")
            }
        }
    }
}

In the parent screen I can then do:

// on button click
scope.launch {
  val result = overlayHost.showFullScreenOverlay(CreateGroupDialogScreen)
  state.eventSink(OtherScreen.Event.CreateGroupResult(result))
}

This of course could also be implemented like this right now with some more code:

var showDialog by rememberRetained { mutableStateOf(false) }

if(showDialog) {
  CircuitContent(
        screen = CreateGroupDialogScreen,
        onNavEvent = {
            when (it) {
                is NavEvent.Pop ->{
                    state.eventSink(OtherScreen.Event.CreateGroupResult(it.result))
                    showDialog = false
                }
                else -> error("not supported")
            }
        },
    )
}

// on button click
{ showDialog  = true }

As for the nullable Overlay return type, I have another usecase. A "Pick-One" dialog that also allows selecting None. The None-choice could simply be mapped to null, instead of having to handle it with wrappers.

Currently not possible:

OverlayEffect {
    val choice = it.show(
        BasicAlertDialogOverlay<List<String>, String?>(
            model = listOf("A", "B", "C"),
            onDismissRequest = { null },
        ) { model, navigator ->
            // navigator.finish("A")
        },
    )
}

Workaround with wrapper:

sealed interface DialogResult {
    data class Value(val value: String) : DialogResult
    data object None : DialogResult
}

OverlayEffect {
    val choice = it.show(
        BasicAlertDialogOverlay<List<String>, DialogResult>(
            model = listOf("A", "B", "C"),
            onDismissRequest = { DialogResult.None },
        ) { model, navigator ->
            // navigator.finish(DialogResult.Value("A"))
        },
    )
}

@ZacSweers
Copy link
Collaborator

ZacSweers commented Jun 4, 2024

It seems like you're just using the Screen input convenience of a full screen overlay to show a complex dialog (which isn't actually full screen), when you should instead just create a dialog overlay that accepts a screen input?

@aschulz90
Copy link
Contributor Author

aschulz90 commented Jun 5, 2024

It's convinient, yes. They are technically fullscreen, because they are modal (fullscreen scrim + dialog). I could also duplicate the showFullScrrenOverlay for my app and implement the small adjustment, except using the wrapper workaround for non-nullable overlay results.
So if the nullable overlay results are not desired, then I will close this PR.

@stagg
Copy link
Collaborator

stagg commented Jun 6, 2024

Not sure want a nullable return from an Overlay as we do want to emphasize the need to return a result.

That said the FullScreenOverlay does have a bit of an oddity in that it's dropping any returned PopResult. So maybe we update the FullScreenOverlay to have a result returned that wraps the optional PopResult?

@ZacSweers
Copy link
Collaborator

I'd be ok with an optional wrapper 👍

@aschulz90
Copy link
Contributor Author

I have updated it to return the PopResult via a wrapper without nullable Overlay results.

Copy link
Collaborator

@stagg stagg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the changes!
Not sure whats up with CI 👀

@stagg stagg enabled auto-merge June 7, 2024 17:13
@stagg stagg disabled auto-merge June 7, 2024 17:13
@stagg stagg merged commit a42869c into slackhq:main Jun 7, 2024
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants