Site icon AppTractor

Объединяем Compose и View: бесшовное взаимодействие с помощью CompositionLocal

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

Источник

Exit mobile version