При переходе на 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, если у вас есть вопросы.