Загрузка данных
package ru.ozon.seller.composer.screen
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.OnBackPressedCallback
import androidx.compose.runtime.Composable
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment
import ru.ozon.analytics.storage.connectToParent
import ru.ozon.composer.ComposerInterceptor
import ru.ozon.composer.compose.ComposerComposeScreen
import ru.ozon.composer.event.ComposerEvent
import ru.ozon.composer.state.ComposerRequestState
import ru.ozon.composer.state.Loader
import ru.ozon.composer.ui.ComposerScreen
import ru.ozon.composer.ui.ComposerScreenUiContainer
import ru.ozon.composer.ui.FragmentViewModelOwnerProvider
import ru.ozon.composer.ui.OwnerContainer
import ru.ozon.composer.ui.ViewModelOwnerProvider
import ru.ozon.composer.ui.ext.composerAppbar
import ru.ozon.composer.widget.item.ComposerViewItem
import ru.ozon.composer.widget.store.WidgetFactory
import ru.ozon.designsystem.theme.DsTheme
import ru.ozon.seller.composer.configuration.ConfiguratorComposerDependencies
import ru.ozon.seller.composer.configuration.PageConfigurator
import ru.ozon.seller.composer.di.composer.component.ComposerFragmentComponentDependencies
import ru.ozon.seller.composer.di.composer.component.DaggerComposerFragmentComponent
import ru.ozon.seller.composer.navigation.COMPOSER_ARGS
import ru.ozon.seller.composer.navigation.COMPOSER_PAGE
import ru.ozon.seller.composer.navigation.LoadableScreen
import ru.ozon.seller.composer.navigation.TopScrollable
import ru.ozon.seller.composer.navigation.config.ComposerPage
import ru.ozon.seller.composer.utils.SoftInputChangeDelegate
import ru.ozon.seller.composer.utils.serializable
import ru.ozon.seller.composer.utils.whenStarted
import ru.ozon.seller.composer.utils.withArgs
import ru.ozon.seller.composer.viewmodel.ComposerHolderViewModel
import ru.ozon.seller.composer.viewmodel.sharedViewModelStore
import ru.ozon.seller.composer.wrapper.ComposerErrorStateFactoryWrapper
import ru.ozon.seller.composer.wrapper.InitialWidgetsProviderWrapper
import ru.ozon.seller.core.api.abTesting.isDisabled
import ru.ozon.seller.core.api.abTesting.isEnabled
import ru.ozon.seller.growthStream.api.core.MonetizationFeatureFlags
import ru.ozon.seller_app.core.android.api.di.findFeatureDependencies
import ru.ozon.seller_app.core.android.api.system.parcelable
import ru.ozon.seller_app.core.navigation.routers.ScreenRouter
import ru.ozon.uikit.pool.ViewPool
import ru.ozon.uikit.pool.ViewPoolHolder
import ru.ozon.uikit.pool.precreation.PreCreationViewPool
import ru.ozon.uikit.theme.OzonTheme
import ru.ozon.uni.android.uikit.extensions.exhaustive
import ru.ozon.uni.ozi.theme.OziTheme
import javax.inject.Inject
import javax.inject.Provider
class ComposerFragment :
Fragment(),
ComposerComposeScreen,
TopScrollable,
ViewPoolHolder,
LoadableScreen {
companion object {
internal const val CONFIG = "ru.ozon.composer.SCREEN_CONFIG"
internal const val ARG_DISPLAY_MODE = "ARG_DISPLAY_MODE"
fun newInstance(
config: ComposerScreenConfig,
displayMode: DisplayMode? = null,
): ComposerFragment {
return ComposerFragment().withArgs {
putParcelable(CONFIG, config)
putSerializable(ARG_DISPLAY_MODE, displayMode)
}
}
}
@Inject
lateinit var widgets: Set<@JvmSuppressWildcards WidgetFactory>
@Inject
lateinit var configurators: List<@JvmSuppressWildcards PageConfigurator>
@Inject
lateinit var interceptors: Set<@JvmSuppressWildcards ComposerInterceptor>
@Inject
lateinit var router: ScreenRouter
@Inject
lateinit var errorStateFactoryWrapper: ComposerErrorStateFactoryWrapper
@Inject
lateinit var initialWidgetsProviderWrapper: InitialWidgetsProviderWrapper
@Inject
lateinit var viewPoolProvider: Provider<ViewPool>
private val handler by lazy { Handler(Looper.getMainLooper()) }
private val fragmentController: ComposerFragmentController by lazy {
ComposerFragmentController(
fragment = this,
config = config,
ownerContainer = OwnerContainer(this),
viewModelOwnerProvider = FragmentViewModelOwnerProvider(this, sharedViewModelStore()),
widgets = widgets,
displayMode = displayMode(),
interceptors = interceptors,
errorStateFactory = errorStateFactoryWrapper.factory,
initialWidgetsProvider = initialWidgetsProviderWrapper.provider,
)
}
private val ownerContainer: OwnerContainer
get() = fragmentController.ownerContainer
private val viewModelOwnerProvider: ViewModelOwnerProvider
get() = fragmentController.viewModelOwnerProvider
private val composerStore
get() = fragmentController.composerStore
private val controller
get() = fragmentController.controller
lateinit var config: ComposerScreenConfig
lateinit var displayMode: DisplayMode
private var softInputInputChangeDelegate: SoftInputChangeDelegate? = null
private var isFirstLoaded = false
private var host: Host? = null
private var composerScreenUiContainer: ComposerScreenUiContainer? = null
private var prefetchViewPool: ViewPool? = null
private val loading = Runnable {
// если фрагмент не добавлен к родителю, то выполнение runnable нужно игнорировать
if (!isAdded) return@Runnable
// Запускаем инициализационную загрузку данных, только если передана информация для загрузки данных.
// Например при использовании общего ComposerRepository на два ComposerFragment,
// для зависимого экрана ссылки не будет, ComposerRepository проинициализируется основным экраном.
if (!isFirstLoaded) {
isFirstLoaded = true
fragmentController.pagePerformanceTracker.onComposerFirstLoadInitiated()
composerStore.emit(ComposerEvent.ReloadEvent.FirstLoad)
}
}
override fun onAttach(context: Context) {
val composerInitTimeStart = System.nanoTime()
val args = arguments
val composerDeps = requireActivity().findFeatureDependencies<ComposerFragmentComponentDependencies>()
config = args?.parcelable<ComposerScreenConfig>(CONFIG)
?: composerDeps
.getComposerScreenConfigStore()
.config(
ComposerPage(
value = requireNotNull(args?.getString(COMPOSER_PAGE)),
args = args.getString(COMPOSER_ARGS),
),
)
displayMode = args?.serializable(ARG_DISPLAY_MODE)
?: config.bottomSheetConfig?.displayMode
?: DisplayMode.REGULAR
DaggerComposerFragmentComponent
.factory()
.create(
config,
composerDeps,
)
.inject(this)
fragmentController.pagePerformanceTracker.onComposerViewInitStarted(composerInitTimeStart)
super.onAttach(context)
host = (parentFragment ?: activity) as? Host
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val softInputMode = config.softInputMode
if (softInputMode != null) {
softInputInputChangeDelegate = SoftInputChangeDelegate(softInputMode)
.also { it.register(requireActivity(), this) }
}
val references = ConfiguratorComposerDependencies(fragmentController)
configurators.forEach {
it.composerInitialized(references)
it.onRestoreInstanceState(savedInstanceState, viewModelOwnerProvider)
lifecycle.addObserver(it)
}
fragmentController.pagePerformanceTracker.onComposerViewInitEnded()
prefetchViewPool = viewPoolProvider.get()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
fragmentController.pagePerformanceTracker.onComposerViewCreationStarted()
val screenContainer = fragmentController.createComposerScreenUiContainer(inflater, container)
composerScreenUiContainer = screenContainer
return screenContainer.view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val viewModel = ComposerHolderViewModel
.getFromViewModelStoreOwner(router.currentBackStackEntry.value?.viewModelStoreOwner)
viewModel?.setComposer(fragmentController.composer)
host?.onComposerCreated(this, fragmentController)
fragmentController.analyticsScreenStorage.connectToParent(this)
handler.post(loading)
activity?.onBackPressedDispatcher?.addCallback(
viewLifecycleOwner,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
router.back()
}
},
)
fragmentController.pagePerformanceTracker.onComposerViewCreationEnded()
if (MonetizationFeatureFlags.CONSUME_INSETS_BEFORE_COMPOSER.isDisabled()) {
ViewCompat.setOnApplyWindowInsetsListener(view) { view, insets ->
val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime())
view.setPadding(0, 0, 0, imeInsets.bottom)
composerStore.emit(ComposerEvent.ApplyWindowInsets(insets))
ViewCompat.onApplyWindowInsets(view, insets)
}
}
configurators.forEach { it.onViewCreated(view, savedInstanceState) }
}
@Composable
override fun ComposeWidgetWrapper(item: ComposerViewItem, content: @Composable (() -> Unit)) {
if (MonetizationFeatureFlags.COMPOSER_CONFIG_INIT.isEnabled()) {
OziTheme {
DsTheme {
OzonTheme {
content()
}
}
}
} else {
super.ComposeWidgetWrapper(item, content)
}
}
override fun onResume() {
super.onResume()
handler.post(loading)
}
override fun onPause() {
super.onPause()
handler.removeCallbacks(loading)
}
override fun onStop() {
super.onStop()
PreCreationViewPool.clear(this.toString())
}
override fun onDestroyView() {
super.onDestroyView()
composerScreenUiContainer = null
prefetchViewPool = null
configurators.forEach { it.onDestroyView() }
}
override fun onDetach() {
host = null
super.onDetach()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
whenStarted {
configurators.forEach { it.onActivityResult(requestCode, resultCode, data) }
}
}
override fun onSaveInstanceState(outState: Bundle) {
configurators.forEach { it.onSaveInstanceState(outState, viewModelOwnerProvider) }
super.onSaveInstanceState(outState)
}
override fun scrollToTop() {
controller.scrollToPosition(0)
ownerContainer.fragment?.view?.composerAppbar()?.setExpanded(true)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
configurators.forEach { it.onConfigurationChanged(newConfig) }
}
override fun showLoader(type: LoadableScreen.LoaderType) {
val loaderType = when (type) {
LoadableScreen.LoaderType.OVERLAY -> Loader.Type.Overlay()
LoadableScreen.LoaderType.TRANSPARENT -> Loader.Type.Transparent()
}
controller.showLoader(loaderType)
}
override fun hideLoader() {
controller.hideLoader()
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
whenStarted {
configurators.forEach { it.onRequestPermissionsResult(requestCode, permissions, grantResults) }
}
}
fun startLoading(deeplink: String, oneTimePostProcessing: ComposerRequestState.OneTimePostProcessing? = null) {
composerStore.emit(
ComposerEvent.ReloadEvent.Refresh(url = deeplink, postProcessingInfo = oneTimePostProcessing),
)
}
fun startLoading(pageRef: ComposerRequestState.PageRef) {
when (pageRef) {
is ComposerRequestState.PageRef.Deeplink -> composerStore.emit(
ComposerEvent.ReloadEvent.Refresh(url = pageRef.rootUrl),
)
is ComposerRequestState.PageRef.Json -> composerStore.emit(
ComposerEvent.ReloadEvent.Refresh(jsonBody = pageRef.value),
)
}.exhaustive
}
fun scrollToWidget(
widgetComponent: String,
includeToolbarHeight: Boolean = true,
offset: Int = 0,
) {
controller.scrollToWidget(
widgetComponent = widgetComponent,
includeToolbarHeight = includeToolbarHeight,
offset = offset,
)
}
fun scrollToWidgetKey(
widgetKey: Int,
offset: Int = 0,
smooth: Boolean = false,
) {
controller.scrollToWidget(
widgetKey = widgetKey,
offset = offset,
smooth = smooth,
)
}
private fun displayMode() = when (displayMode.ordinal) {
DisplayMode.BOTTOM_SHEET_FULL.ordinal -> ComposerScreen.DisplayMode.BOTTOM_SHEET_FULL
DisplayMode.BOTTOM_SHEET_WRAP.ordinal -> ComposerScreen.DisplayMode.BOTTOM_SHEET_WRAP
else -> ComposerScreen.DisplayMode.REGULAR
}
override fun getViewPool(): ViewPool? {
return prefetchViewPool
}
enum class DisplayMode {
// Это фулл скрин, который поддерживает match_parent у холдера, но перекрывает bottomContainer
REGULAR,
// Шторка отображается по контенту
BOTTOM_SHEET_WRAP,
// Это фулл скрин, который не поддерживает match_parent у холдера, но учитывает bottomContainer
BOTTOM_SHEET_FULL,
}
/**
* Если родительский фрагмент (host) ComposerFragment-а реализует этот метод,
* то он сможет гибче контролировать композер.
*/
interface Host {
/**
* Вызывается когда композер создан.
*/
fun onComposerCreated(fragment: Fragment, composer: ComposerFragmentController) {
}
}
}