Component Overview¶
A component is just a normal class that encapsulates some logic and possibly another (child) components. Every component has its own lifecycle, which is automatically managed by Decompose. So everything encapsulated by a component is scoped. Please head to the Lifecycle documentation page for more information.
UI is optional and is pluggable from outside of components. Components do not depend on UI, the UI depends on components.
Component hierarchy¶
Pluggable UI hierarchy¶
Typical component structure¶
ComponentContext¶
Each component has an associated ComponentContext
which implements the following interfaces:
LifecycleOwner
, provided by Essenty library, so each component has its own lifecycleStateKeeperOwner
, provided by Essenty library, so you can preserve any state during configuration changes and/or process deathInstanceKeeperOwner
, provided by Essenty library, so you can retain arbitrary object instances in your components (like with AndroidX ViewModels)BackHandlerOwner
, provided by Essenty library, so each component can handle back button events
So if a component requires any of the above features, just pass the ComponentContext
via the component's constructor. You can use the delegation pattern to add the ComponentContext
to this
scope:
class Counter(
componentContext: ComponentContext
) : ComponentContext by componentContext {
// The rest of the code
}
Root ComponentContext¶
When instantiating a root component, the ComponentContext
should be created manually. There is DefaultComponentContext which is the default implementation class of the ComponentContext
.
Warning
The root ComponentContext
and the root component should be always created on the UI thread.
Root ComponentContext in Android¶
Decompose provides a few handy helper functions for creating the root ComponentContext
in Android. The preferred way is to create the root ComponentContext
in an Activity
or a Fragment
.
Root ComponentContext in Activity¶
For this case Decompose provides defaultComponentContext()
extension function, which can be called in scope of an Activity
.
Root ComponentContext in Fragment¶
The defaultComponentContext()
extension function can not be used in a Fragment. This is because the Fragment
class does not implement the OnBackPressedDispatcherOwner
interface, and so by default can't handle back button events. It is advised to use the Android-specific DefaultComponentContext(AndroidLifecycle, SavedStateRegistry?, ViewModelStore?, OnBackPressedDispatcher?)
factory function, and supply all the arguments manually. The first three arguments (AndroidLifecycle
, SavedStateRegistry
and ViewModelStore
) can be obtained directly from Fragment
. However the last argument OnBackPressedDispatcher
- can not. If you don't need to handle back button events in your Decompose components, then you can just ignore this argument. Otherwise, a manual solution is required.
Warning
Don't take any argument values from the hosting Activity
(e.g. requireActivity().onBackPressedDispatcher
), as it may produce memory leaks.
Here is an example with using Decompose in a DialogFragment
.
class MyFragment : DialogFragment() {
// Create custom OnBackPressedDispatcher
private val onBackPressedDispatcher = OnBackPressedDispatcher(::dismiss)
private lateinit var root: RootComponent
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
root =
DefaultRootComponent(
DefaultComponentContext(
lifecycle = lifecycle,
savedStateRegistry = savedStateRegistry,
viewModelStore = viewModelStore,
onBackPressedDispatcher = onBackPressedDispatcher,
)
)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
object : Dialog(requireContext(), theme) {
override fun onBackPressed() {
onBackPressedDispatcher.onBackPressed()
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
// Start Compose here
}
Root ComponentContext in Jetpack/JetBrains Compose¶
It is advised to not create the root ComponentContext
(and a root component) directly in a Composable
function. Compositions may be performed in a background thread, which may break things. The preferred way is to create the root component on the UI thread outside of Compose.
Warning
If you can't avoid creating the root component in a Composable
function, please make sure you use remember
. This will prevent the root component and its ComponentContext
from being recreated on each composition.
Android with Compose¶
Prefer creating the root ComponentContext
(and a root component) before starting Compose, e.g. in an Activity
or a Fragment
.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Create the root component before starting Compose
val root = DefaultRootComponent(componentContext = defaultComponentContext())
// Start Compose
setContent {
// The rest of the code
}
}
}
JVM/Desktop with Compose¶
Make sure you always create the root component on the UI thread. Please refer to samples for an example of runOnUiThread function .
fun main() {
// Create the root component on the UI thread before starting Compose
val root = runOnUiThread { DefaultRootComponent(componentContext = DefaultComponentContext(...)) }
// Start Compose
application {
// The rest of the code
}
}
Other platforms with Compose¶
Prefer creating the root ComponentContext
(and a root component) before starting Compose, e.g. in directly in the main
function.
fun main() {
// Create the root component before starting Compose.
// Make sure that this happens on the UI thread.
val root = DefaultRootComponent(componentContext = DefaultComponentContext(...))
// Start Compose
application {
// The rest of the code
}
}
Value and MutableValue state holders¶
Value - is a multiplatform way to expose streams of states. It contains the value
property, which always returns the current state. It also provides the ability to observe state changes via subscribe
/unsubscribe
methods. There is MutableValue which is a mutable variant of Value
. Since Value
is a class (not an interface) with a generic type parameter, it can be used to expose state streams to ObjC/Swift.
Using Value
is not mandatory, you can use any other state holders, e.g. StateFlow, State, Observable, LiveData, etc.
If you are using Jetpack/JetBrains Compose, Value
can be observed in Composable functions using one of the Compose extension modules.
Warning
Value
is not thread-safe, it should be accessed only from the main thread.
Why not StateFlow?¶
Decompose uses Value
to avoid dependency on Kotlin coroutines. One may prefer using Reaktive, RxJava, etc. instead of coroutines. It also provides better interoperability with ObjC/Swift and simplifies testing. Feel free to convert Value
to StateFlow
or any other state holder if you need it.
Examples¶
Simplest Component Example¶
Here is an example of simple Counter component:
class Counter {
private val _state = MutableValue(State())
val state: Value<State> = _state
fun increment() {
_state.reduce { it.copy(count = it.count + 1) }
}
data class State(val count: Int = 0)
}
Jetpack/JetBrains Compose UI Example¶
@Composable
fun CounterUi(counter: Counter) {
val state by counter.state.subscribeAsState()
Column {
Text(text = state.count.toString())
Button(onClick = counter::increment) {
Text("Increment")
}
}
}
SwiftUI Example¶
struct CounterView: View {
private let counter: Counter
@ObservedObject
private var state: ObservableValue<CounterState>
init(_ counter: Counter) {
self.counter = counter
self.state = ObservableValue(counter.state)
}
var body: some View {
VStack(spacing: 8) {
Text(self.state.value.text)
Button(action: self.counter.increment, label: { Text("Increment") })
}
}
}
What is ObservableValue?¶
ObservableValue is a wrapper around Value
that makes it compatible with SwiftUI. It is a simple class that conforms to ObservableObject
protocol. Unfortunately it does not look possible to publish utils for SwiftUI as a library or framework, so it has to be copied to your project.