Skip to main content

Adding Your Own Features

Assuming your feature will have a new screen, start by creating a new component interface in the shared module.

interface MyFeatureComponent {

}

Define your feature's Model class by deciding the data that the component needs to load and display to the user. eg. a list of items.

interface MyFeatureComponent {
val model: Value<Model>

data class Model(
val items: List<MyFeatureItem> = emptyList()
)
}

Create the functions that the view can call to notify the component about the user actions.

interface MyFeatureComponent {
...
fun onItemTap(itemId: String)
}

Create the navigation events that the component can emit to it's parent component to move to another screen.

interface MyFeatureComponent {
...
sealed interface Output {
data class NavigateToItemDetail(val itemId: String) : Output()
}
}

Now you can create the Component implementation by implementing the component interface. Remember to always pass in the constructor arguments the componentContext, the onOutput function and all dependencies that will be used by the component (eg. userRepository, analyticsProvider, etc.).

class DefaultMyFeatureComponent(
componentContext: ComponentContext,
private val onOutput: (Output) -> Unit
) : MyFeatureComponent, ComponentContext by componentContext {
override val model = MutableValue(Model())
...
}

Now let's load the data and display it in the view.

You will need to pass the repository that will provide the data to the component. eg. an ItemRepository that will provide the list of items.

class DefaultMyFeatureComponent(
componentContext: ComponentContext,
private val itemRepository: ItemRepository,
private val onOutput: (Output) -> Unit,
) : MyFeatureComponent, ComponentContext by componentContext {
...
}

You can choose to load whenever the component is created or when the component is mounted. For that you need to use the Lifecycle object from the ComponentContext.

You can do this in the init block of the component. We assume that the itemRepository is a suspend function that will return a list of items.

In order to call the suspend function from the init block, we need to wrap the code in a coroutine, so we create a scope that is tied to the component's lifecycle.

class DefaultMyFeatureComponent(
componentContext: ComponentContext,
private val itemRepository: ItemRepository,
private val onOutput: (Output) -> Unit
) : MyFeatureComponent, ComponentContext by componentContext {
private val scope = createCoroutineScope()

init {
lifecycle.doOnResume {
scope.launch {
val items = itemRepository.getItems()
model.value = model.value.copy(items = items)
}
}
}
}

Now you can create the view that will display the data.

Create a new Screen composable in /ui/screens/ that receives the MyFeatureComponent interface as a parameter.

@Composable
fun MyFeatureScreen(component: MyFeatureComponent) {
...
}

Listen to the model updates and display the data in the view.

@Composable
fun MyFeatureScreen(component: MyFeatureComponent) {
val model by component.model.subscribeAsState()
model.items.forEach { item ->
Text(item.name)
}
}

Now you need to add your feature to the RootComponent interface to declare it as a child component in order for the users to be able to navigate to it.

interface RootComponent {
...
sealed interface Child {
data class MyFeature(val component: MyFeatureComponent) : Child
}
}

Then you have to add a new Config in the RootComponent.Config object to declare the route that will be used to navigate to the feature.

@Serializable
private sealed interface Config {
...
data object MyFeature : Config
}

Finally, you need to add a new case to the RootComponent createChild function so Root can instantiate your feature component.

private fun createChild(
config: Config,
componentContext: ComponentContext,
): Child =
when (config) {
...

Config.MyFeature -> Child.MyFeature(
component = DefaultMyFeatureComponent(
componentContext = componentContext,
itemRepository = itemRepository,
onOutput = { output ->
when (output) {
MyFeatureComponent.Output.NavigateToItemDetail -> navigation.pushToFront(Config.ResetPassword)
}
}
)
)
}

Remember to add the itemRepository as a new dependency to the DependencyProvider so it can be provided to the MyFeatureComponent from Root.

interface DependencyProvider {
...
val itemRepository: ItemRepository
}