discussions-and-proposals
discussions-and-proposals copied to clipboard
[Android] Discussion: Support for Jetpack Compose
Status as of April 2024
You should be able to use Compose with React Native, but it does not play nice with react-native-screens
The update to Gradle 7+ in RN 0.66+ means that we can now use Jetpack Compose to build React-like declarative UIs natively on Android. It's completely separate from the traditional View
system but there are interoperability APIs to bridge the two worlds.
Thanks to AbstractComposeView
, I was able to implement a basic proof-of-concept (edited for brevity):
class MyViewManager : SimpleViewManager<MyView>() {
override fun createViewInstance(reactContext: ThemedReactContext) = MyView(reactContext)
@ReactProp(name = "displayText")
fun displayText(view: MyView, displayText: String) {
view.displayText = displayText
}
}
class MyView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
) : AbstractComposeView(context, attrs) {
var displayText by mutableStateOf("")
@Composable
override fun Content() {
Text(displayText)
}
}
However!
Release builds crash with an IllegalStateException: ViewTreeLifecycleOwner not found ...
when trying to display the Compose UI content. I haven't been able to nail down the exact reason why this only occurs in release builds, but I was able to figure out two workarounds:
-
Update
androidx.appcompat:appcompat
to version1.3.1+
, discovered via this StackOverflow post and a few others; ~~unfortunately this involved forking~~ RN since it still uses version 1.0.2 which is from 2018 (!!!). Compose depends on some lifecycle and saved-state logic inandroidx.activity.ComponentActivity
, whereasReactActivity
currently ends up extending from the same-name-but-different-packageandroidx.core.app.ComponentActivity
. -
Manually shim the missing logic using
ViewTreeLifecycleOwner
andViewTreeSavedStateRegistryOwner
; thankfullyReactActivity
already implementsLifecycleOwner
via the olderandroidx.core.app.ComponentActivity
, but you do need to addandroidx.savedstate:savedstate-ktx
version1.1.0+
to your dependencies:
abstract class MyReactActivityDelegate(
activity: ReactActivity,
mainComponentName: String?,
) : ReactActivityDelegate(activity, mainComponentName) {
private val shim = SavedStateRegistryOwnerShim(activity)
@CallSuper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
shim.onCreate(savedInstanceState)
}
private class SavedStateRegistryOwnerShim(
private val activity: AppCompatActivity,
) : LifecycleOwner by activity, SavedStateRegistryOwner {
private val controller = SavedStateRegistryController.create(this)
override fun getSavedStateRegistry() = controller.savedStateRegistry
fun onCreate(savedState: Bundle?) {
activity.window.decorView.rootView.let { root ->
ViewTreeLifecycleOwner.set(root, this)
ViewTreeSavedStateRegistryOwner.set(root, this)
}
controller.performRestore(savedState)
}
}
}
class MyActivity : ReactActivity() {
override fun createReactActivityDelegate() {
return object : MyReactActivityDelegate(this, mainComponentName) { ... }
}
}
As far as I can tell, both approaches prevent the crash and Compose seems to function as expected. However:
-
Option 1 ~~involves forking RN and~~ I'm not sure how the change may affect RN overall. I also assume that Compose will eventually require newer and newer AndroidX dependencies, so it would be nice to have them "officially" updated.
-
Option 2 is a kludge that may need to be periodically updated to match the AndroidX implementation, and I have no idea if it even fully implements all of the plumbing that Compose expects under the hood.
So, any chance we could get androidx.appcompat:appcompat
updated to at least 1.3.1
?
Or better yet, some kind of official support for Compose? Perhaps a SimpleComposeViewManager
that directly accepts @Composable
content?
Thanks for opening this discussion @thevoiceless
I'd like to loop in @ShikaSD and @mdvacca as they have relevant context on RN <=> Compose.
I think that we would like to add a sample components inside RN-Tester that uses a Composable
to render. Potentially also adding a Guide to the website would be beneficial so that others could follow your approach.
As for the specific problem you point out:
Update androidx.appcompat:appcompat to version 1.3.1+
This seems like the way to go. You don't need to fork react-native, as specifying a dependency on androidx.appcompat:appcompat:1.3.+
inside your app build.gradle
will force Gradle to pickup that dependency instead of the one provided by RN (1.0.2).
Specifically there was a crash reported that prevented the adoption of AppCompat 1.4.x, which was addressed a couple of months ago so you should be fine using AppCompat till 1.4.x.
That being said, it's actually a good call to update the version of AppCompat used by ReactNative to a newer one. I saw that some work was already done here: https://github.com/facebook/react-native/pull/31620 so let's try to land that or a similar PR - cc @dulmandakh if you have bandwidth or if anyone else wants to pick this up, I'll be happy to review it.
Sounds good! I'm happy to help/contribute any way I can.
You don't need to fork react-native
Ah, looks like you're right; I'm using appcompat-resources
in the project where I noticed the issue, not the full appcompat
- my bad 😅
Thanks for looking into the appcompat issue!
Hey, really happy to see someone trying Compose with React Native! Me and @mdvacca did a few related experiments in October last year, and it should be totally possible to use it with the legacy renderer the way you described, I think.
We were investigating Compose support with Fabric renderer, at the same time, and some of the required functionality is not really supported from Compose side (e.g. measure in background). I hacked around it for the sake of experiment, but not sure it can be considered "production-ready" yet with Fabric.
For now, we don't plan any official support for Compose (because of performance and feature related concerns), but will keep an eye on new developments/community feedback.
I also would love to hear about your experience if you manage to use a Compose-based view in your project :)
@ShikaSD I seem to recall seeing Fabric-related stuff in the stacktraces while investigating the appcompat issue; am I imagining things, or is it already enabled on Android?
or is it already enabled on Android?
It shouldn't be enabled by default, unless you specifically did it. Instructions on how to enable it are here: https://github.com/facebook/react-native-website/pull/2879
Ah I definitely haven't done any of that, but might take a stab at it. Thanks!
You might see Fabric related things in the stack traces even on legacy renderer because we adapted most of the Android native components from legacy renderer to work with both :)
For now, we don't plan any official support for Compose
@ShikaSD can you elaborate on this? Should I avoid investing any effort in using Compose if Fabric won't support it? Compose seems to be the future of UI on android, but Fabric seems to the future of UI in RN...
Should I avoid investing any effort in using Compose
I think not, frankly the opposite :) My comment had more cautionary intention in case you are adopting Fabric right now. Compose support is just not ideal at the moment and it is going to be rough around the edges for some time :)
I think not, frankly the opposite
Awesome, I was hoping you'd say that! I'm working on proving out using Compose in various parts of my current project, so I'll keep you all posted with anything else that I find.
On that note: It seems to "just work" in DialogFragment
s with the updated appcompat as well 🙂
I believe I've found another issue:
I'm not 100% sure if this is the scenario to reproduce, but I have a component like
const Foo = () => {
const [loading, setLoading] = React.useState(true)
if (loading) {
return (
<View><ActivityIndicator /></View>
)
}
return (
<View><MyView /></View>
)
}
where MyView
is implemented as shown in my original post (i.e. SimpleViewManager
returning an AbstractComposeView
). However, when switching from the "loading" UI to the "loaded" UI, it appears that NativeViewHierarchyManager.updateLayout()
is invoking
viewToUpdate.measure(
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY));
before viewToUpdate
(in this case, my MyView
) is attached to the window. This crashes with
java.lang.IllegalStateException: Cannot locate windowRecomposer; View ... is not attached to a window
because AbstractComposeView.onMeasure()
calls ensureCompositionCreated()
which calls resolveParentCompositionContext()
. Unfortunately its onMeasure()
is final
so I can't simply add a check to no-op if not attached; the only workaround I've found is to extract some of the logic from View.createLifecycleAwareViewTreeRecomposer(): Recomposer
declared in androidx.compose.ui.platform.WindowRecomposer.android.kt
:
class MyView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
) : AbstractComposeView(context, attrs) {
init {
val currentThreadContext = AndroidUiDispatcher.CurrentThread
val pausableClock = currentThreadContext[MonotonicFrameClock]?.let {
PausableMonotonicFrameClock(it).apply { pause() }
}
val contextWithClock = currentThreadContext + (pausableClock ?: EmptyCoroutineContext)
val recomposer = Recomposer(contextWithClock)
setParentCompositionContext(recomposer)
}
override fun onAttachedToWindow() {
setParentCompositionContext(null)
super.onAttachedToWindow()
}
@Composable
override fun Content() {
// ...
}
}
which ensures that some kind of parent composition context exists.....but I'm not exactly confident that this is "correct" 😅
Yep, that's the issue we encountered with Fabric as well. I had to commit a few reflection crimes to get around that limitation, but maybe it is possible to just not measure the view until it is attached?
I don't remember the details on how legacy renderer works here, but maybe you could measure the view to the size of the parent (or just 0) and then resize when it is attached.
If the only problem is the absense of composition context, it should be possible to extract it from parent view/activity context, and the set it similarly to the way you worked around this limitation (without resetting the composition context).
Compose attaches the recomposer to the view tag of android.R.id.content
, IIRC, so if you can get hold on the activity, you can:
class MyView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
) : AbstractComposeView(context, attrs) {
init {
val activity = getActivity(context) // you probably want to unwrap it in case it is ContextWrapper
val compositionContext = activity.findViewById(android.R.id.content)!!.compositionContext
setParentCompositionContext(compositionContext)
}
override fun onAttachedToWindow() {
// I don't think there's any need to re-attach here, as recomposer is created once per window.
super.onAttachedToWindow()
}
@Composable
override fun Content() {
// ...
}
}
This should work, albeit still very hacky :)
Exported our internal proof of concept from October last year: https://github.com/facebook/react-native/pull/32871
It tries to change a <Switch />
component to use Compose, supporting a subset of original props and events. All of it with Fabric only though, but seems like there are less issues with legacy renderer so far :)
cc @mdvacca
maybe it is possible to just not measure the view until it is attached
Unless I'm missing something, it looks like AbstractComposeView
specifically forbids that by marking onMeasure()
as final
- it does expose an open fun internalOnMeasure()
but that's not called until after ensureCompositionCreated()
. It seems that any measurement workaround might have to be on the RN side of things - perhaps some kind of marker interface for special behavior? Just spitballing here...
sealed interface SpecialBehavior {
interface MeasureOnlyWhenAttached : SpecialBehavior
// ...
}
and then NativeViewHierarchyManager.updateLayout()
would check if (view !is SpecialBehavior.MeasureOnlyWhenAttached)
?
This should work, albeit still very hacky
Still looks better than my approach - I'll give it a shot!
Exported our internal proof of concept
Awesome, I'll take a look, thanks!
Compose attaches the recomposer to the view tag of android.R.id.content, IIRC, so if you can get hold on the activity, you can ...
I believe it actually uses the first child of android.R.id.content
, and that strategy only works if another ComposeView
has already been attached at some point (might even need to still be attached) which allows you to use findViewTreeCompositionContext(). The compositionContext
is only assigned inside createAndInstallWindowRecomposer()
invoked by the getter for windowRecomposer
, and that getter is what checks isAttachedToWindow
.
Unfortunately, createAndInstallWindowRecomposer()
is internal
and uses @DelicateCoroutinesApi
, and WindowRecomposerFactory.LifecycleAware
is marked as @InternalComposeUiApi
- I'm guessing that's why you had to use reflection?
So it seems the options are:
- Reflection to invoke
createAndInstallWindowRecomposer()
- Opt-in to
@InternalComposeUiApi
and copy implementation details directly from Compose - Do the stuff you did in https://github.com/facebook/react-native/pull/32871
....OR, since ReactRootView
is a FrameLayout
...
- Throw a stub
ComposeView
into the view hierarchy, and use a base class that ensures thecompositionContext
exists
class MyActivity : ReactActivity() {
override fun createReactActivityDelegate() {
return object : ReactActivityDelegate(this, mainComponentName) {
override fun createRootView() = createMyRootView(context)
.apply { addView(StubComposeView(context) }
}
}
}
class StubComposeView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
) : AbstractComposeView(context, attrs) {
init { isVisible = false }
@Composable
override fun Content() {}
}
abstract class MyBaseComposeView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
) : AbstractComposeView(context, attrs) {
private val activity: Activity?
get() {
var candidate = context
while (candidate !is Activity) {
when (candidate) {
is ContextWrapper -> candidate = candidate.baseContext
else -> break
}
}
return candidate as? Activity
}
private val activityCompositionContext: CompositionContext?
get() = (activity?.findViewById<ViewGroup>(android.R.id.content))
?.children
?.mapNotNull { it.findViewTreeCompositionContext() }
?.firstOrNull()
init {
if (compositionContext == null) {
compositionContext = findViewTreeCompositionContext() ?: activityCompositionContext
}
}
}
https://github.com/facebook/react-native/pull/32871 seems closer to the "best" approach since you're integrating more with the Compose internals, but IMO this approach seems like a good tradeoff between effort and technical correctness without relying (too much) on internal details.
Thoughts?
@thevoiceless Ah, interesting, I missed the point where they did it lazily, which makes a lot of sense, tbh
I guess for me calling the createAndInstallWindowRecomposer
sounds like a good plan for the time being. Note that you don't really need reflection to access internal
methods, as those methods are effectively public
in Java 😅
It is still a hack, but feels like the best choice (in my opinion) until Compose UI team gives us a better way out of those guardrails,
Crafty! I'd totally forgotten about using Java to get around Kotlin's internal
- that does appear to work as well 👍
I have run into another issue, although I think it's a bug with Compose instead of RN:
I'm using react-navigation
to progress through screens A
-> B
-> C
, pushing each onto the stack; B
displays a native Compose view using the strategy from my initial post. Everything works as expected when progressing forward, but the Compose view does not appear on screen after pressing the "back" button to return to B
from C
.
Observations:
- According to Flipper, the
ComposeView
is still present in the view hierarchy, attached,View.VISIBLE
, and the correct size (width
,measuredWidth
,height
,measuredHeight
)- The view does not show in the Android Studio layout inspector after returning to
B
, but it did show before navigating toC
- The view does not show in the Android Studio layout inspector after returning to
- The view's
Modifier.clickable(...)
logic is not triggered when tapping where the it allegedly is - The view gets attached (and its composition created), measured, and laid out as expected when first navigating to
B
- With the default
ViewCompositionStrategy
, the composition is disposed when navigating toC
. The composition is then recreated when the view is re-attached upon returning toB
, but the view is not re-measured nor re-laid-out- I can recreate the initial sequence of events on re-attach by manually invoking
measure(...)
andlayout(...)
with the view's existing values, but the view does not appear
- I can recreate the initial sequence of events on re-attach by manually invoking
- With
ViewCompositionStrategy.DisposeOnLifecycleDestroyed
, the composition is not disposed when navigating toC
and that same composition is used when re-attaching inB
, but the view does not appear- Same as above with
measure(...)
andlayout(...)
- Same as above with
-
invalidate()
,requestLayout()
, andforceLayout()
have no effect
That all seems to rule out an issue with layout or measurement, so I started investigating Compose itself...
I tried updating values in the state:
var someValue by mutableStateOf(0)
override fun onAttachedToWindow() {
super.onAttachedToWindow()
postDelayed({
log("increment")
someValue = someValue + 1
}, 1000)
}
@Composable
override fun Content() {
SideEffect { log("composition value $someValue") }
// ...
}
It behaved as expected when first navigating to B
, but indicated that recomposition was not happening after returning from C
.
This led me deep into the guts of Compose, and I think I've narrowed it down to a bug in WrappedComposition and the interaction between owner.setOnViewTreeOwnersAvailable { ... }
and its addedToLifecycle
member. react-navigation
uses Fragments under the hood by attaching/detaching them as you push/pop screens, which I believe is breaking some lifecycle-observation logic. Specifically:
- When navigating to
B
, initial composition invokesAbstractComposeView.setContent()
which creates anAndroidComposeView
and uses it as theowner
for theWrappedComposition
, then invokes itssetContent()
method -
WrappedComposition.setContent()
callsowner.setOnViewTreeOwnersAvailable { ... }
; none are immediately available because this is the first time setting everything up - Eventually the
AndroidComposeView
is attached to the window and itsViewTreeOwners
becomes available;addedToLifecycle
isnull
so it grabs thelifecycle
(i.e.LifecycleRegistry
) from thelifecycleOwner
(i.e.FragmentViewLifecycleOwner
) and observes it.- At this point,
addedToLifecycle.currentState
isINITIALIZED
- At this point,
- When the lifecycle reaches the
CREATED
state,setContent()
is invoked again which ends up calling into the "real"setContent()
for the composition.- End result: The UI displays as expected
- When navigating to
C
, detachingB
triggersLifecycle.Event.ON_DESTROY
and the composition is disposed but the sameAndroidComposeView
instance is kept around- At this point,
addedToLifecycle.currentState
isDESTROYED
- At this point,
- When returning to
B
, the fragment is re-attached which sets up the composition again. However,setOnViewTreeOwnersAvailable { .. }
returns immediately with the previous value because it's using the sameAndroidComposeView
as before - so it assignsaddedToLifecycle
and observes it without checkingcurrentState
, which isDESTROYED
! - The
AndroidComposeView
is eventually re-attached, once again triggering thesetOnViewTreeOwnersAvailable { ... }
callback with new data, butaddedToLifecycle
has already been assigned! So instead it checks iflifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)
, but at this point the lifecycle is onlyINITIALIZED
. We never end up observing the correct lifecycle, so the logic ends there.- End result:
setContent()
never gets called again
- End result:
The issue still exists if you use ViewCompositionStrategy.DisposeOnLifecycleDestroyed
with the activity; it doesn't invoke disposeComposition()
on our AbstractComposeView
, but internally the WrappedComposition
still disposes itself when the fragment lifecycle is destroyed. AbstractComposeView
only recreates the composition if it's null
, even if if it's not-null-but-disposed. I'm not entirely sure why the ViewCompositionStrategy
only seems to affect our AbstractComposeView
....seems like a pretty big foot-gun if it doesn't actually affect any internals.
Thoughts?
I'm not 100% confident in my explanation; it seems like it'd be an obvious issue in any app using Compose with multiple fragments 🤔
Great investigation! I think you are on the right track for the most of it, but I am not sure if the bug is in Compose view here.
When navigating to C, detaching B triggers Lifecycle.Event.ON_DESTROY and the composition is disposed but the same AndroidComposeView instance is kept around
I would expect the React surface to be destroyed in this case as well, did you track what keeps the view around? Assuming that composition is disposed and fragment is destroyed, view should be deleted as well.
I did not determine why the AndroidComposeView
is staying around - AFAICT the only thing holding on to it is the AbstractComposeView
itself, which adds it to itself in setContent()
. The fragment isn't destroyed, only detached and re-attached, but that should result in destroying the view per the docs/SO.
Unfortunately I had to timebox my efforts to yesterday, so I don't think I'll be able to investigate this any further and will have to backtrack on using Compose for now 😞
I'm not sure if it's helpful, but this was my debugger output:
A
-> B
MyView@124075016 onAttachedToWindow
MyView@124075016 create composition
MyView@124075016 setContent
addView AndroidComposeView@144268683 to MyView@124075016
AndroidComposeView@144268683 doSetContent
setContent: WrappedComposition@139419240 setOnViewTreeOwnersAvailable for owner AndroidComposeView@144268683
setOnViewTreeOwnersAvailable: vto not available yet
MyView@124075016 composition is now WrappedComposition@139419240
---
AndroidComposeView@144268683 onAttachedToWindow
vto now available
AndroidComposeView@144268683 vto callback ViewTreeOwners@151281446
vto callback: lifecycle is LifecycleRegistry@155076455 in state INITIALIZED, lifecycleOwner is FragmentViewLifecycleOwner@166721812
vto callback: WrappedComposition@139419240 observe lifecycle LifecycleRegistry@155076455 in state INITIALIZED
---
LifecycleRegistry@155076455 CREATED, source FragmentViewLifecycleOwner@166721812, calling setContent
setContent: WrappedComposition@139419240 setOnViewTreeOwnersAvailable for owner AndroidComposeView@144268683
setOnViewTreeOwnersAvailable: vto immediately available
AndroidComposeView@144268683 vto callback ViewTreeOwners@151281446
vto callback: lifecycle is LifecycleRegistry@155076455 in state CREATED, lifecycleOwner is FragmentViewLifecycleOwner@166721812
vto callback: WrappedComposition@139419240 already observing lifecycle LifecycleRegistry@155076455 in state CREATED, this lifecycle is LifecycleRegistry@155076455 in state CREATED
vto callback: CompositionImpl@239808920 'real' setContent
B
-> C
AndroidComposeView@144268683 onDetachedFromWindow
MyView@124075016 onDetachedFromWindow
MyView@124075016 disposeComposition
WrappedComposition@139419240 dispose
CompositionImpl@239808920 dispose
C
-> B
MyView@124075016 onAttachedToWindow
MyView@124075016 create composition
MyView@124075016 setContent
AndroidComposeView@144268683 doSetContent
setContent: WrappedComposition@163236301 setOnViewTreeOwnersAvailable for owner AndroidComposeView@144268683
setOnViewTreeOwnersAvailable: vto immediately available
AndroidComposeView@144268683 vto callback ViewTreeOwners@151281446
vto callback: lifecycle is LifecycleRegistry@155076455 in state DESTROYED, lifecycleOwner is FragmentViewLifecycleOwner@166721812
vto callback: WrappedComposition@163236301 observe lifecycle LifecycleRegistry@155076455 in state DESTROYED
---
MyView@124075016 composition is now WrappedComposition@163236301
---
AndroidComposeView@144268683 onAttachedToWindow
vto now available
AndroidComposeView@144268683 vto callback ViewTreeOwners@261264002
vto callback: lifecycle is LifecycleRegistry@211299731 in state INITIALIZED, lifecycleOwner is FragmentViewLifecycleOwner@224420304
vto callback: WrappedComposition@163236301 already observing lifecycle LifecycleRegistry@155076455 in state DESTROYED, this lifecycle is LifecycleRegistry@211299731 in state INITIALIZED
B
-> A
AndroidComposeView@144268683 onDetachedFromWindow
MyView@124075016 onDetachedFromWindow
MyView@124075016 disposeComposition
WrappedComposition@163236301 dispose
CompositionImpl@230184436 dispose
And FWIW this is using react-native-screens in conjunction with react-navigation
Edit: Just a guess, but perhaps related to https://github.com/software-mansion/react-native-screens/issues/843#issuecomment-832034119
we cannot destroy and then make new views by restoring the state of the
Fragment
, since each view has itsreactTag
etc...we do not recreate the views of theFragment
, but rather callremove
on the them when they become invisible and thenadd
them back on theScreen
becoming visible with the sameScreen
attached to it.
I have a slight recollection about Fragment attach
/detach
methods and lifecycle, so maybe that's the issue that you are observing. It is still weird to me that view is not recreated after lifecycle is set to DESTROYED
state, as that kinda indicates that Fragment was... destroyed?
detach
documentation (https://developer.android.com/reference/androidx/fragment/app/FragmentTransaction?hl=en#detach(androidx.fragment.app.Fragment) suggests that view hierarchy should be destroyed as well, so I'd guess react-native-screens
are doing something fishy to work around Fragment-RN incompatibility.
Had a bit of time to mess with this some more; unsurprisingly, that bug did not magically fix itself while I was away 😅
I've pushed a minimum reproducible example to https://github.com/thevoiceless/RN-Compose-Playground
It doesn't include any of the workarounds/modifications discussed above, just a barebones RN app created with --template react-native-template-typescript
using @react-navigation/native-stack
to move between screens. I also tried using @react-navigation/stack
but it made no difference (branch: non-native-stack
). I'm not super surprised since (I think) they both still use react-native-screens
under the hood.
https://user-images.githubusercontent.com/1574103/181108249-84b6dbd4-b455-40e9-b292-648238767187.mov
It appears the lifecycle/composition workarounds discussed earlier in this thread are no longer necessary as of:
- RN 0.68
- Compose compiler 1.3.0-rc01
- Compose artifacts 1.2.0
- Appcompat 1.4.2
- Kotlin 1.7.10
I still see the disappearing issue with react-native-screens
, but at least it's a step in the right direction!
@thevoiceless @ShikaSD Pretty cool research done by you folks 🚀
I decided to find some answers to why RN-Screens won't work with compose view nicely. So here are the results:
When embedding a compose view in SimpleViewManager, while using react-native-screens, it doesn't play nicely when resuming the compose view.
I tried to look for the issues and based on this answer, I decided to do the same. But I couldn't do it in SimpleViewManager, so I had to use fragment to be able to listen to the life cycle methods.
When I embedded the compose view in a fragment, I didn't had to use that stackoverflow answer and just using fragments, solved the issue.
However there were some layout issue, which have been covered now.
We now have no issues with react-native-screens and compose view layouts fine.
Here's a PR to the playground @thevoiceless shared.
https://user-images.githubusercontent.com/47336142/228035357-d90447ae-4ce2-4ebf-a1c4-98554def2354.mp4
@hurali97 Interesting! It seems a bit heavy-handed to host an entire Fragment just to show the composable, but good to know there's a workaround 🤔
Hi all, just wanted to report I am trying to do the same thing using AbstractComposeView in a Fabric component.
My only relevant dependencies are: RN: 0.71.6
And in my Fabric component's build.gradle: androidx.compose.ui:ui: 1.4.3
I still get
java.lang.IllegalStateException: Cannot locate windowRecomposer; View com.bsmlibrary.FabricComposable{54bed7b V.E...... ......I. 0,0-0,0 #1ba} is not attached to a window
@thevoiceless it sounded like you said this issue should go away in the new versions of RN and the compose dependency, but it's still happening for me :(
Here's what my AbstractComposable looks like:
import android.util.AttributeSet
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.AbstractComposeView
import com.example.components.MyView
import com.example.ui.theme.CustomTheme
// We create our custom UI component extending from AbstractComposeView
class FabricComposable @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AbstractComposeView(context, attrs, defStyleAttr) {
// The Content function works as a Composable function so we can now define our Compose UI components to render.
@Composable
override fun Content() {
CustomTheme() {
}
MyView()
}
}
Has anyone found a way to make this work? Am I missing a dependency? Am I missing a piece of code in the AbstractComposeView?