Skip to content

Conversation

@rjrjr
Copy link
Collaborator

@rjrjr rjrjr commented Oct 2, 2020

Builds the simpler, more flexible scheme for modals proposed in #195. Only
AlertDialog takes advantage of it so far, and only the hellobackbutton and
helloworkflow samples are ported.

closes #195

  • We introduce the ViewRendering and ModalRendering interfaces,
    similar in sprit to AnyScreen in Swift.

  • ViewRegistry changes to be able to hold a collection of more arbitrary entry types, not just ViewFactory.

    • Its buildView method is deprecated.
    • Once we delete that method, we should be able to move ViewRegistry over to the
      non-Android, core UI module.
  • There are three implementations of ViewRegistry.Entry

    • Our old friend ViewFactory<T: Any>, now deprecated.
    • ViewBuilder<T: ViewRendering>, which replaces ViewFactory
    • DialogBuilder<T: ModalRendering>
  • Extension method ViewRendering.buildView() replaces ViewRegistry.buildView(). We also introduce ModalRendering.buildDialog().

    • These methods find the appropriate builder in ViewRegistry, based on the concrete type of the rendering, just like ViewRegistry.buildView() did.

    • The new ability of ViewRegistry to hold arbitrary entries means that we can add as many rendering:view-machinery pairings as we want to. Maybe cleans up Compose integration? (@zach-klippenstein )

  • The abstract ModalContainer and its subclasses (AlertContainer, ModalViewContainer, our internal ones) are all deprecated in favor of a single final class: ModalContainerView, which is able to display any kind of ModalRendering associated with a DialogBuilder implementation.

  • LayoutRunner<T: Any> is deprecated, replaced by ViewRunner<T: ViewRendering>. That's going to be a tedious migration for existing LayoutRunner implementations, but it avoids a huge breaking change —
    the new mechanism works in parallel with the deprecated one.

  • WorkflowViewStub.display(rendering: ViewRendering) replaces WorkflowViewStub.update(rendering: Any), now deprecated. But the latter is able to delegate to the former so incremental migration is possible.

api(Dependencies.Kotlin.Stdlib.jdk6)

implementation(Dependencies.AndroidX.activity)
implementation(Dependencies.AndroidX.appcompat)
Copy link
Collaborator Author

@rjrjr rjrjr Oct 2, 2020

Choose a reason for hiding this comment

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

Uh-oh. Did we move this out of ui-core to keep @Armaxis happy? Is that still a concern?

I think I can avoid it, but it'll be a bit tedious.

Copy link
Member

Choose a reason for hiding this comment

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

No longer a concern, with the amount of things we need these day, appcompat is imminent.
Bring it back. Sorry for bothering you remove it in first place :/

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yay! Glad I asked.

* )
*/
@WorkflowUiExperimentalApi
class BespokeViewBuilder<RenderingT : ViewRendering>(
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not even sure if this is worth providing. Should try just implementing the interface somewhere.

}
Quitting -> {
val dialog = AlertScreen(
val dialog = AlertModalRendering(
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is the punchline. AlertModalRendering is bound in the ViewRegistry to AlertDialogBuilder. We didn't have to make a subclass of ModalContainerView in order to show this particular kind of modal.

contextForNewView: Context,
container: ViewGroup? = null
): View {
val builder = initialViewEnvironment[ViewRegistry].getEntryFor(this::class)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Very tempting to allow the RenderingT itself to implement ViewBuilder

  val builder = (this as? ViewBuilder<RenderingT>) 
      ?: initialViewEnvironment[ViewRegistry].getEntryFor(this::class)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Actually, better to do it in the other order so that ViewRegistry can override default implementations.

  val builder = initialViewEnvironment[ViewRegistry].getEntryFor(this::class)
      ?: (this as? ViewBuilder<RenderingT>)

Copy link
Collaborator

@zach-klippenstein zach-klippenstein Oct 6, 2020

Choose a reason for hiding this comment

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

IIRC the way we talked ourselves out of that was because we didn’t want to encourage screen value types from being coupled to the UI layer. But I think in reality, screen types and view factories are often defined in public-facing, API, android-dependent modules already. And this isn’t requiring that, it merely allows it, which can be super useful for things like the WorkflowEditText wiring, the ComposeRendering PoC, etc. So, I'm on board.

For context on the compose thing, see the comments on square/workflow-kotlin-compose#8.

Builds the simpler, more flexible scheme for modals proposed in #195. Only
AlertDialog takes advantage of it so far, and only the `hellobackbutton` and
`helloworkflow` samples are ported.

closes #195

 * We introduce the `ViewRendering` and `ModalRendering` interfaces,
   similar in sprit to `AnyScreen` in Swift.

 * `ViewRegistry` changes to be able to hold a collection of more arbitrary entry types, not just `ViewFactory`.
    * Its `buildView` method is deprecated.
    * Once we delete that method, we should be able to move `ViewRegistry` over to the
      non-Android, core UI module.

 * There are three implementations of `ViewRegistry.Entry`
    * Our old friend `ViewFactory<T: Any>`, now deprecated.
    * `ViewBuilder<T: ViewRendering>`, which replaces `ViewFactory`
    * `DialogBuilder<T: ModalRendering>`

 * Extension method `ViewRendering.buildView()` replaces `ViewRegistry.buildView()`. We also introduce  `ModalRendering.buildDialog()`.

    * These methods find the appropriate builder in `ViewRegistry`, based on the concrete type of the rendering, just like `ViewRegistry.buildView()` did.

    * The new ability of `ViewRegistry` to hold arbitrary entries means that we can add as many rendering:view-machinery pairings as we want to. Maybe cleans up Compose integration?

 * The abstract `ModalContainer` and its subclasses (`AlertContainer`, `ModalViewContainer`, our internal ones) are all deprecated in favor of a single final class: `ModalContainerView`, which is able to display any kind of `ModalRendering` associated with a `DialogBuilder` implementation.

 * `LayoutRunner<T: Any>` is deprecated, replaced by `ViewRunner<T: ViewRendering>`. That's going to be a tedious migration for existing `LayoutRunner` implementations, but it avoids a huge breaking change —
 the new mechanism works in parallel with the deprecated one.

 * `WorkflowViewStub.display(rendering: ViewRendering)` replaces `WorkflowViewStub.update(rendering: Any)`, now deprecated. But the latter is able to delegate to the former so incremental migration is possible.
@rjrjr rjrjr force-pushed the ray/better-modals-195 branch from f520e16 to 7818bf4 Compare October 2, 2020 21:13
@zach-klippenstein zach-klippenstein added this to the post-v1.0.0 milestone Oct 6, 2020
import kotlinx.android.parcel.Parcelize

@OptIn(WorkflowUiExperimentalApi::class)
private typealias Rendering = ModalContainerViewRendering<*, AlertModalRendering>
Copy link
Contributor

Choose a reason for hiding this comment

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

I've noticed this 'generic type aliasing' pattern with our Actions and now Rendering/Screens for Workflows. I've always felt like it makes things harder to follow rather than easier because I need to constantly look up what the actual types are. What advantage am I missing with this pattern?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Note that this one is private -- it was strictly a convenience to avoid some copy / paste, and arguably a bad choice for the reaons you mentioned. Our trend has been to use fewer and fewer type aliases, especially public ones.

Some of the lingering ones are leftovers from before we had the more concise action {} lambdas.

import android.view.ViewGroup
import kotlin.reflect.KClass

/**
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need some kind of documentation here just for archaeology?

* has never been called.
*/
@WorkflowUiExperimentalApi
fun <RenderingT : ModalRendering> Dialog.getDisplaying(): RenderingT? {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it worth an interface with showRenderingTag and displaying so we can abstract some of these duplicated functions into the same implementation?

Copy link
Contributor

Choose a reason for hiding this comment

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

nvm I see these extension properties are defined below (which of course they are as this is Dialog and View). No clear way to pull out an abstraction as an extension on Dialog and View I guess. 🤔

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess you do almost already have an extension abstraction in ShowRenderingTag - why can't all these other extensions delegate into functions defined on that class itself. That way we wouldn't have the double maintenance. Granted this may not scale much beyond Dialog and View.

Copy link
Contributor

Choose a reason for hiding this comment

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

Except for would it if we introduce Compose?

for ((i, modalRendering) in newScreen.modals.withIndex()) {
updateDialogs +=
if (i < dialogs.size && compatible(dialogs[i].getDisplaying()!!, modalRendering)) {
dialogs[i].apply { display(modalRendering, viewEnvironment) }
Copy link
Contributor

Choose a reason for hiding this comment

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

AFAIK alert dialogs can't be updated in place. Perhaps AlertModalRendering should implement Compatible?

@zach-klippenstein
Copy link
Collaborator

This change seems to require all screen renderings to be completely opinionated about whether they should be a modal or not. Is that true? What happens if a rendering type wants to be usable as either a modal or a non-modal, and so implements both marker interfaces? Do we explicitly allow/forbid that, do we check for it anywhere?

As a straw-view, consider ComposeRendering: This is a generic rendering type that can render itself. It doesn't care if it's in a modal or not, it just defines how to render. Could it implement both marker interfaces?

@rjrjr
Copy link
Collaborator Author

rjrjr commented Nov 17, 2020

This change seems to require all screen renderings to be completely opinionated about whether they should be a modal or not. Is that true? What happens if a rendering type wants to be usable as either a modal or a non-modal, and so implements both marker interfaces? Do we explicitly allow/forbid that, do we check for it anywhere?

As a straw-view, consider ComposeRendering: This is a generic rendering type that can render itself. It doesn't care if it's in a modal or not, it just defines how to render. Could it implement both marker interfaces?

My original thought was that ComposeRendering would be a completely different beast, the fourth implementation of ViewRegistry.Entry. Haven't dug in at all to see if that makes sense. Seems like it should -- it'd identify a rendering that needs some kind of compose-friendly home. Perhaps we'll also have something like data class ComposeContainerView<C: ComposeRendering>(val composed: C): ViewRendering?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Decouple modality from modal visual treatment

6 participants