Skip to content

Template

Templates are an abstraction between Presenters and Renderers and represent the root of the presenter and renderer tree. Practically, a template is one particular type of BaseModel that hosts other models (a container of models). However, instead of using a weak type like List<BaseModel>, a template carries semantics about what content should be rendered, how many UI layers there are and where each individual model should be displayed.

Templates are app specific and not shared, because each app may use a different layering mechanism for individual screen configurations. An example template definition could look like this:

sealed interface SampleAppTemplate : Template {

  data class FullScreenTemplate(
    val model: BaseModel,
  ) : SampleAppTemplate

  data class ListDetailTemplate(
    val list: BaseModel,
    val detail: BaseModel,
  ) : SampleAppTemplate
}
Sample

A similar hierarchy is implemented in the sample application.

The Template interface extends BaseModel and each app must come with its own TemplatePresenter and TemplateRenderer. Both are implemented the same way as other presenters and renderers would be implemented. The responsibility of the TemplatePresenter is to wrap another presenter and wrap its models within a Template, e.g.

@Inject
class SampleAppTemplatePresenter(
  @Assisted private val rootPresenter: MoleculePresenter<Unit, *>,
) : MoleculePresenter<Unit, SampleAppTemplate> {
  @Composable
  override fun present(input: Unit): SampleAppTemplate {
    return returningCompositionLocalProvider {
      rootPresenter.present(Unit).toTemplate {
        SampleAppTemplate.FullScreenTemplate(it)
      }
    }
  }
}
Sample

The sample app has a similar implementation.

The wrapped presenter can override which Template to use by implementing ModelDelegate, e.g.

data class Model(
  ...
) : BaseModel, ModelDelegate {
  override fun delegate(): BaseModel = ListDetailTemplate(...)
}
Sample

The sample app makes use of this mechanism in the user page, where it the layout is split between a list presenter / renderer and detail presenter / renderer.

data class Model(
  val listModel: BaseModel,
  val detailModel: BaseModel
) : BaseModel, ModelDelegate {
  override fun delegate(): BaseModel {
    return SampleAppTemplate.ListDetailTemplate(listModel, detailModel)
  }
}

The TemplateRenderer receives the specific Template, lays out necessary containers and renders individual models in these layers. The renderer often injects RendererFactory to create renderers for the models, e.g.

@Inject
@ContributesRenderer
class ComposeSampleAppTemplateRenderer(
  private val rendererFactory: RendererFactory
) : ComposeRenderer<SampleAppTemplate>() {

  @Composable
  override fun Compose(model: SampleAppTemplate) {
    when (model) {
      is SampleAppTemplate.FullScreenTemplate -> FullScreen(model)
      is SampleAppTemplate.ListDetailTemplate -> ListDetail(model)
    }
  }

  @Composable
  private fun FullScreen(template: SampleAppTemplate.FullScreenTemplate) {
    val renderer = rendererFactory.getComposeRenderer(template.model)
    renderer.renderCompose(template.model)
  }

  @Composable
  private fun ListDetail(template: SampleAppTemplate.ListDetailTemplate) {
    Row {
      Column {
        rendererFactory.getComposeRenderer(template.list).renderCompose(template.list)
      }
      Column {
        rendererFactory.getComposeRenderer(template.detail).renderCompose(template.detail)
      }
    }
  }
}

Consuming Templates

On the API level Templates are regular Models, with a regular Presenter and Renderer. Therefore, they require no special treatment and the regular RendererFactory can be used:

fun mainViewController(rootScopeProvider: RootScopeProvider): UIViewController =
  ComposeUIViewController {
    val factory = remember { ComposeRendererFactory(rootScopeProvider = rootScopeProvider) }

    val templatePresenter = remember {
      val component = rootScopeProvider.rootScope.diComponent<ViewControllerComponent>()
      component.factory.createSampleAppTemplatePresenter(component.navigationPresenter)
    }

    val template = templatePresenter.present(Unit)
    factory.getComposeRenderer(template).renderCompose(template)
  }

@ContributesTo(AppScope::class)
interface ViewControllerComponent {
  val factory: SampleAppTemplatePresenter.Factory
  val navigationPresenter: NavigationPresenter
}

Unidirectional dataflow

Templates complete the circle in our unidirectional dataflow pattern:

Unidirectional dataflow

This diagram summarizes how models from child presenters bubble up ultimately to the template presenter. The template presenter wraps the models in a template, which is then handed off the rendering pipeline. RendererFactory finds the right renderers for the template and models and the content will be shown on screen by individual renderers. The circle repeats either when a renderer invokes a callback from the model and sends the event back to the presenter or another state change occurs within the the presenter tree.