Skip to content

Commit

Permalink
refact(Android): allow for different fragment types inside ScreenCont…
Browse files Browse the repository at this point in the history
…ainer (software-mansion#1887)

## Description

This PR is laying out ground work under not-so-distant Android modal &
bottom sheet implementation.

Currently `ScreenContainer<T: ScreenFragment>: ViewGroup` (and thus
`ScreenStack: ScreenContainer<ScreenStackFragment>`) operates on
`ScreenFragment: Fragment` objects, making it impossible to use any
other `Fragment` subclass with the stack (only single inheritance is
possible on JVM).

This PR changes the situation by introducing `ScreenFragmentWrapper`
interface which both:

1. holds the reference to underlaying `Fragment`
2. provides additional API previously directly attached to the
`ScreenFragment`

Also introduced `ScreenStackFragmentWrapper` extends
`ScreenFragmentWrapper` with API previously attached to
`ScreenStackFragment`.

Thereafter by implementing such interface by the fragment subclass
itself we can have both of worlds:

1. Customized fragment behaviour
2. Implementation of all methods required for fragment<->container
communication.

Disclaimer: *I have decided to split "Android modals PR" into few
smaller ones to reduce its size and complexity. I won't use "stack PR"
technique to spare myself whole bunch of rebasing & conflict resolving.*

## Changes

1. `ScreenContainer` is no longer parameterized with `T:
ScreenFragment`. It uses `ScreenFragmentWrapper` as screen-primitve base
type now.
2. Our fragment types `ScreenFragment` & `ScreenStackFragment` now
implement `ScreenFragmentWrapper` & `ScreenStackFragmentWrapper`
interfaces respectively.
3. `ScreenContainer` has access to the fragments it operates on
(fragment API) via `ScreenFragmentWrapper.fragment` property.
4. Our custom methods previously implemented directly on `Fragment`
subclass are exposed via the new interfaces so `ScreenContainer` still
has access to them.

## Test code and steps to reproduce

See that test examples build & CI passes.

I did some manual testing in our example apps & didn't notice any
regression.

## Checklist

- [x] Ensured that CI passes
  • Loading branch information
kkafar authored and ja1ns committed Oct 9, 2024
1 parent 80b9a9a commit e0c0a9a
Show file tree
Hide file tree
Showing 13 changed files with 206 additions and 135 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.swmansion.rnscreens

import androidx.fragment.app.Fragment

interface FragmentHolder {
val fragment: Fragment
}
21 changes: 12 additions & 9 deletions android/src/main/java/com/swmansion/rnscreens/Screen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@ import android.view.ViewGroup
import android.view.WindowManager
import android.webkit.WebView
import androidx.core.view.children
import androidx.fragment.app.Fragment
import com.facebook.react.bridge.GuardedRunnable
import com.facebook.react.bridge.ReactContext
import com.facebook.react.uimanager.UIManagerModule

@SuppressLint("ViewConstructor")
class Screen constructor(context: ReactContext?) : FabricEnabledViewGroup(context) {
val fragment: Fragment?
get() = fragmentWrapper?.fragment

var fragment: ScreenFragment? = null
var container: ScreenContainer<*>? = null
var fragmentWrapper: ScreenFragmentWrapper? = null
var container: ScreenContainer? = null
var activityState: ActivityState? = null
private set
private var mTransitioning = false
Expand Down Expand Up @@ -150,7 +153,7 @@ class Screen constructor(context: ReactContext?) : FabricEnabledViewGroup(contex
else -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}

fragment?.let { ScreenWindowTraits.setOrientation(this, it.tryGetActivity()) }
fragmentWrapper?.let { ScreenWindowTraits.setOrientation(this, it.tryGetActivity()) }
}

// Accepts one of 4 accessibility flags
Expand All @@ -167,7 +170,7 @@ class Screen constructor(context: ReactContext?) : FabricEnabledViewGroup(contex
ScreenWindowTraits.applyDidSetStatusBarAppearance()
}
mStatusBarStyle = statusBarStyle
fragment?.let { ScreenWindowTraits.setStyle(this, it.tryGetActivity(), it.tryGetContext()) }
fragmentWrapper?.let { ScreenWindowTraits.setStyle(this, it.tryGetActivity(), it.tryGetContext()) }
}

var isStatusBarHidden: Boolean?
Expand All @@ -177,7 +180,7 @@ class Screen constructor(context: ReactContext?) : FabricEnabledViewGroup(contex
ScreenWindowTraits.applyDidSetStatusBarAppearance()
}
mStatusBarHidden = statusBarHidden
fragment?.let { ScreenWindowTraits.setHidden(this, it.tryGetActivity()) }
fragmentWrapper?.let { ScreenWindowTraits.setHidden(this, it.tryGetActivity()) }
}

var isStatusBarTranslucent: Boolean?
Expand All @@ -187,7 +190,7 @@ class Screen constructor(context: ReactContext?) : FabricEnabledViewGroup(contex
ScreenWindowTraits.applyDidSetStatusBarAppearance()
}
mStatusBarTranslucent = statusBarTranslucent
fragment?.let {
fragmentWrapper?.let {
ScreenWindowTraits.setTranslucent(
this,
it.tryGetActivity(),
Expand All @@ -203,7 +206,7 @@ class Screen constructor(context: ReactContext?) : FabricEnabledViewGroup(contex
ScreenWindowTraits.applyDidSetStatusBarAppearance()
}
mStatusBarColor = statusBarColor
fragment?.let { ScreenWindowTraits.setColor(this, it.tryGetActivity(), it.tryGetContext()) }
fragmentWrapper?.let { ScreenWindowTraits.setColor(this, it.tryGetActivity(), it.tryGetContext()) }
}

var navigationBarColor: Int?
Expand All @@ -213,7 +216,7 @@ class Screen constructor(context: ReactContext?) : FabricEnabledViewGroup(contex
ScreenWindowTraits.applyDidSetNavigationBarAppearance()
}
mNavigationBarColor = navigationBarColor
fragment?.let { ScreenWindowTraits.setNavigationBarColor(this, it.tryGetActivity()) }
fragmentWrapper?.let { ScreenWindowTraits.setNavigationBarColor(this, it.tryGetActivity()) }
}

var isNavigationBarHidden: Boolean?
Expand All @@ -223,7 +226,7 @@ class Screen constructor(context: ReactContext?) : FabricEnabledViewGroup(contex
ScreenWindowTraits.applyDidSetNavigationBarAppearance()
}
mNavigationBarHidden = navigationBarHidden
fragment?.let {
fragmentWrapper?.let {
ScreenWindowTraits.setNavigationBarHidden(
this,
it.tryGetActivity(),
Expand Down
71 changes: 35 additions & 36 deletions android/src/main/java/com/swmansion/rnscreens/ScreenContainer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ import com.facebook.react.modules.core.ChoreographerCompat
import com.facebook.react.modules.core.ReactChoreographer
import com.swmansion.rnscreens.Screen.ActivityState

open class ScreenContainer<T : ScreenFragment>(context: Context?) : ViewGroup(context) {
open class ScreenContainer(context: Context?) : ViewGroup(context) {
@JvmField
protected val mScreenFragments = ArrayList<T>()
protected val mScreenFragments = ArrayList<ScreenFragmentWrapper>()
@JvmField
protected var mFragmentManager: FragmentManager? = null
private var mIsAttached = false
Expand All @@ -34,7 +34,7 @@ open class ScreenContainer<T : ScreenFragment>(context: Context?) : ViewGroup(co
layout(left, top, right, bottom)
}
}
private var mParentScreenFragment: ScreenFragment? = null
private var mParentScreenFragment: ScreenFragmentWrapper? = null

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var i = 0
Expand Down Expand Up @@ -87,14 +87,11 @@ open class ScreenContainer<T : ScreenFragment>(context: Context?) : ViewGroup(co
performUpdatesNow()
}

protected open fun adapt(screen: Screen): T {
@Suppress("UNCHECKED_CAST")
return ScreenFragment(screen) as T
}
protected open fun adapt(screen: Screen): ScreenFragmentWrapper = ScreenFragment(screen)

fun addScreen(screen: Screen, index: Int) {
val fragment = adapt(screen)
screen.fragment = fragment
screen.fragmentWrapper = fragment
mScreenFragments.add(index, fragment)
screen.container = this
onScreenChanged()
Expand All @@ -119,6 +116,8 @@ open class ScreenContainer<T : ScreenFragment>(context: Context?) : ViewGroup(co

fun getScreenAt(index: Int): Screen = mScreenFragments[index].screen

fun getScreenFragmentWrapperAt(index: Int): ScreenFragmentWrapper = mScreenFragments[index]

open val topScreen: Screen?
get() = mScreenFragments.firstOrNull { getActivityState(it) === ActivityState.ON_TOP }?.screen

Expand Down Expand Up @@ -174,10 +173,10 @@ open class ScreenContainer<T : ScreenFragment>(context: Context?) : ViewGroup(co
// Otherwise we expect to connect directly with root view and get root fragment manager
if (parent is Screen) {
checkNotNull(
parent.fragment?.let { screenFragment ->
mParentScreenFragment = screenFragment
screenFragment.registerChildScreenContainer(this)
setFragmentManager(screenFragment.childFragmentManager)
parent.fragmentWrapper?.let { fragmentWrapper ->
mParentScreenFragment = fragmentWrapper
fragmentWrapper.addChildScreenContainer(this)
setFragmentManager(fragmentWrapper.fragment.childFragmentManager)
}
) { "Parent Screen does not have its Fragment attached" }
} else {
Expand All @@ -197,19 +196,19 @@ open class ScreenContainer<T : ScreenFragment>(context: Context?) : ViewGroup(co
.setReorderingAllowed(true)
}

private fun attachScreen(transaction: FragmentTransaction, screenFragment: ScreenFragment) {
transaction.add(id, screenFragment)
private fun attachScreen(transaction: FragmentTransaction, fragment: Fragment) {
transaction.add(id, fragment)
}

private fun detachScreen(transaction: FragmentTransaction, screenFragment: ScreenFragment) {
transaction.remove(screenFragment)
private fun detachScreen(transaction: FragmentTransaction, fragment: Fragment) {
transaction.remove(fragment)
}

private fun getActivityState(screenFragment: ScreenFragment): ActivityState? =
screenFragment.screen.activityState
private fun getActivityState(screenFragmentWrapper: ScreenFragmentWrapper): ActivityState? =
screenFragmentWrapper.screen.activityState

open fun hasScreen(screenFragment: ScreenFragment?): Boolean =
mScreenFragments.contains(screenFragment)
open fun hasScreen(screenFragmentWrapper: ScreenFragmentWrapper?): Boolean =
mScreenFragments.contains(screenFragmentWrapper)

override fun onAttachedToWindow() {
super.onAttachedToWindow()
Expand Down Expand Up @@ -246,7 +245,7 @@ open class ScreenContainer<T : ScreenFragment>(context: Context?) : ViewGroup(co
}
}

mParentScreenFragment?.unregisterChildScreenContainer(this)
mParentScreenFragment?.removeChildScreenContainer(this)
mParentScreenFragment = null

super.onDetachedFromWindow()
Expand Down Expand Up @@ -320,13 +319,13 @@ open class ScreenContainer<T : ScreenFragment>(context: Context?) : ViewGroup(co
"mFragmentManager is null when performing update in ScreenContainer"
}.fragments
)
for (screenFragment in mScreenFragments) {
if (getActivityState(screenFragment) === ActivityState.INACTIVE &&
screenFragment.isAdded
for (fragmentWrapper in mScreenFragments) {
if (getActivityState(fragmentWrapper) === ActivityState.INACTIVE &&
fragmentWrapper.fragment.isAdded
) {
detachScreen(it, screenFragment)
detachScreen(it, fragmentWrapper.fragment)
}
orphaned.remove(screenFragment)
orphaned.remove(fragmentWrapper.fragment)
}
if (orphaned.isNotEmpty()) {
val orphanedAry = orphaned.toTypedArray()
Expand All @@ -344,30 +343,30 @@ open class ScreenContainer<T : ScreenFragment>(context: Context?) : ViewGroup(co

// attach newly activated screens
var addedBefore = false
val pendingFront: ArrayList<T> = ArrayList()
val pendingFront: ArrayList<ScreenFragmentWrapper> = ArrayList()

for (screenFragment in mScreenFragments) {
val activityState = getActivityState(screenFragment)
if (activityState !== ActivityState.INACTIVE && !screenFragment.isAdded) {
for (fragmentWrapper in mScreenFragments) {
val activityState = getActivityState(fragmentWrapper)
if (activityState !== ActivityState.INACTIVE && !fragmentWrapper.fragment.isAdded) {
addedBefore = true
attachScreen(it, screenFragment)
attachScreen(it, fragmentWrapper.fragment)
} else if (activityState !== ActivityState.INACTIVE && addedBefore) {
// we detach the screen and then reattach it later to make it appear on front
detachScreen(it, screenFragment)
pendingFront.add(screenFragment)
detachScreen(it, fragmentWrapper.fragment)
pendingFront.add(fragmentWrapper)
}
screenFragment.screen.setTransitioning(transitioning)
fragmentWrapper.screen.setTransitioning(transitioning)
}

for (screenFragment in pendingFront) {
attachScreen(it, screenFragment)
attachScreen(it, screenFragment.fragment)
}

it.commitNowAllowingStateLoss()
}
}

protected open fun notifyContainerUpdate() {
topScreen?.fragment?.onContainerUpdate()
topScreen?.fragmentWrapper?.onContainerUpdate()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,27 @@ import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewGroupManager

@ReactModule(name = ScreenContainerViewManager.REACT_CLASS)
class ScreenContainerViewManager : ViewGroupManager<ScreenContainer<*>>() {
class ScreenContainerViewManager : ViewGroupManager<ScreenContainer>() {
override fun getName(): String = REACT_CLASS

override fun createViewInstance(reactContext: ThemedReactContext): ScreenContainer<ScreenFragment> = ScreenContainer(reactContext)
override fun createViewInstance(reactContext: ThemedReactContext): ScreenContainer = ScreenContainer(reactContext)

override fun addView(parent: ScreenContainer<*>, child: View, index: Int) {
override fun addView(parent: ScreenContainer, child: View, index: Int) {
require(child is Screen) { "Attempt attach child that is not of type RNScreens" }
parent.addScreen(child, index)
}

override fun removeViewAt(parent: ScreenContainer<*>, index: Int) {
override fun removeViewAt(parent: ScreenContainer, index: Int) {
parent.removeScreenAt(index)
}

override fun removeAllViews(parent: ScreenContainer<*>) {
override fun removeAllViews(parent: ScreenContainer) {
parent.removeAllScreens()
}

override fun getChildCount(parent: ScreenContainer<*>): Int = parent.screenCount
override fun getChildCount(parent: ScreenContainer): Int = parent.screenCount

override fun getChildAt(parent: ScreenContainer<*>, index: Int): View = parent.getScreenAt(index)
override fun getChildAt(parent: ScreenContainer, index: Int): View = parent.getScreenAt(index)

override fun createShadowNodeInstance(context: ReactApplicationContext): LayoutShadowNode = ScreensShadowNode(context)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.swmansion.rnscreens

interface ScreenEventDispatcher {
fun canDispatchLifecycleEvent(event: ScreenFragment.ScreenLifecycleEvent): Boolean
fun updateLastEventDispatched(event: ScreenFragment.ScreenLifecycleEvent)

/**
* Dispatches given screen lifecycle event to JS using screen from given fragment `fragmentWrapper`
*/
fun dispatchLifecycleEvent(event: ScreenFragment.ScreenLifecycleEvent, fragmentWrapper: ScreenFragmentWrapper)

/**
* Dispatches given screen lifecycle event from all non-empty child containers to JS
*/
fun dispatchLifecycleEventInChildContainers(event: ScreenFragment.ScreenLifecycleEvent)

fun dispatchHeaderBackButtonClickedEvent()
fun dispatchTransitionProgressEvent(alpha: Float, closing: Boolean)

// Concrete dispatchers
}
Loading

0 comments on commit e0c0a9a

Please sign in to comment.