При переходе на Compose наша команда столкнулась с рядом проблем, связанных с совместимостью, поскольку мы интегрировали новые композабл в существующую кодовую базу. Одним из ключевых препятствий было определение того, как вызывать события из Compose и отправлять их основному держателю представления, такому как Фрагмент или Активити.
После некоторых исследований мы нашли эффективное решение с использованием CompositionLocal. Вот как мы его реализовали! 🚀
Что такое CompositionLocal?
Думайте о CompositionLocal как о локализованной системе инъекции зависимостей, встроенной непосредственно в модель композиции Compose. Вы создаете экземпляр CompositionLocal, который выступает в качестве ключа для определенного типа данных. Затем, используя CompositionLocalProvider, вы определяете область видимости в дереве UI и связываете значение с этим ключом CompositionLocal. Любой компонент, находящийся в этой области, может получить доступ к предоставленному значению с помощью свойства CompositionLocal.current, при этом не нужно знать, откуда взялось это значение. Это не только упрощает код, но и делает его более удобным для сопровождения и многократного использования.
Кроме того, значения CompositionLocal могут быть динамическими, вызывая перекомпозицию при их изменении, и они потокобезопасны, что делает их мощным инструментом для управления общим состоянием в вашем пользовательском интерфейсе Compose.
Взаимодействие с CompositionLocal
В следующем коде LocalFeedbackInterop — это staticCompositionLocalOf, который будет содержать реализацию интерфейса FeedbackMessageInterop. Этот интерфейс определяет методы отображения предупреждений, а staticCompositionLocalOf гарантирует, что реализация вряд ли изменится, так как ее нельзя будет пересоздать. Это позволяет любому компоненту в пределах скоупа получить доступ к реализации FeedbackMessageInterop и вызывать алерты без явных зависимостей.
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.staticCompositionLocalOf
val LocalFeedbackInterop: ProvidableCompositionLocal<FeedbackMessageInterop?> = staticCompositionLocalOf { null }
interface FeedbackMessageInterop {
fun showError(message: String)
fun showSuccess(message: String)
fun showInfo(message: String)
}
Из нашего кода Compose мы можем получить экземпляр CompositionLocal для доступа к реализации FeedbackMessageInterop и запустить наш компонент с алертами на основе View, который полагается на состояние ошибки, полученное из ViewModel.
@Composable
fun Content() {
val feedbackHost = LocalFeedbackInterop.current
LaunchedEffect(feedbackHost) {
viewModel.error.collect {
feedbackHost?.showError(
message = "My error message",
)
}
}
}
На стороне Фрагмента или Активити мы можем легко объявить локальную реализацию FeedbackMessageInterop, которая будет предоставлена нашему CompositionLocalProvider для связи с нашим кодом Compose. Вот и все!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
CompositionLocalProvider(
LocalFeedbackInterop provides localFeedback,
) {
TractorTheme {
CurrentScreen()
}
}
}
}
}
private val localFeedback: FeedbackMessageInterop = object : FeedbackMessageInterop {
override fun showError(message: String) {
lifecycleScope.launch {
parentFragment?.view?.findViewById<View>(R.id.rootBottomSheetModalView)?.let { root ->
showErrorFeedbackMessage(root.parent as ViewGroup) {
this.message = message
offsetPositionTop = R.dimen.feedbackOffsetPositionTopSmall
}
}
}
}
}
Возможен и другой вариант: вы можете вызвать код Compose, взаимодействуя с реализацией интерфейса из кода, основанного на представлении! Одним из возможных решений было бы выставление StateFlow или SharedFlow из реализации, предоставленной CompositionLocalProvider, чтобы его можно было получить на стороне Compose.
// On the view side
private val localBottomSheet = object : BottomSheetInterop {
private val rightAction: MutableSharedFlow<Unit> = MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = DROP_OLDEST)
override val onRightActionClicked: SharedFlow<Unit> = rightAction
override fun someFun() {
setOnRightActionClickListener { rightAction.tryEmit(Unit) }
}
}
// On the Compose side
val bottomSheet = LocalBottomSheetInterop.currentOrThrow
LaunchedEffect(bottomSheet) {
bottomSheet.onRightActionClicked.collect {
viewModel.doSomething()
}
}
Изучив возможности CompositionLocal в Compose, мы смогли без проблем улучшить взаимодействие с существующими компонентами на основе представлений. Такой подход не только упростил взаимодействие между Compose и традиционными View, но и ускорил разработку множества экранов в приложении.
Не стесняйтесь писать мне на LinkedIn, если у вас есть вопросы.

