-
Notifications
You must be signed in to change notification settings - Fork 125
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Android] Discussion: Support for Jetpack Compose #446
Comments
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 As for the specific problem you point out:
This seems like the way to go. You don't need to fork react-native, as specifying a dependency on 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: facebook/react-native#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.
Ah, looks like you're right; I'm using 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? |
It shouldn't be enabled by default, unless you specifically did it. Instructions on how to enable it are here: facebook/react-native-website#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 :) |
@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... |
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 :) |
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 |
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 viewToUpdate.measure(
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)); before
because 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 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: facebook/react-native#32871 cc @mdvacca |
Unless I'm missing something, it looks like sealed interface SpecialBehavior {
interface MeasureOnlyWhenAttached : SpecialBehavior
// ...
} and then
Still looks better than my approach - I'll give it a shot!
Awesome, I'll take a look, thanks! |
I believe it actually uses the first child of Unfortunately, So it seems the options are:
....OR, since
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
}
}
} facebook/react-native#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 |
Crafty! I'd totally forgotten about using Java to get around Kotlin's |
I have run into another issue, although I think it's a bug with Compose instead of RN: I'm using Observations:
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 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
The issue still exists if you use 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.
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 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:
|
And FWIW this is using react-native-screens in conjunction with react-navigation Edit: Just a guess, but perhaps related to software-mansion/react-native-screens#843 (comment)
|
I have a slight recollection about Fragment |
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 Untitled.mov |
It appears the lifecycle/composition workarounds discussed earlier in this thread are no longer necessary as of:
I still see the disappearing issue with |
@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:
Here's a PR to the playground @thevoiceless shared. compose-view.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: And in my Fabric component's build.gradle: I still get
@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? |
also tried @hurali97 's solution but got the following error:
|
@dm20 I haven't done anything with Fabric components yet 😕 |
@thevoiceless thanks for your reply - I see, I'm confused because it looked like you were using the Fabric architecture. I am using Fabric architecture and am running into the "not attached to Window" issue. I wonder if I follow the same architecture as you would I be able to render my component... Which architecture are you following for these native components? Is it the legacy React Native way of setting up a native component? |
Ah I see...You add the package class in MainApplication..Interesting. Still curious how did you learn to do it this way? I haven't seen this in RN docs |
@dm20 I'm doing things the "old" way. @ShikaSD shared a proof-of-concept using Fabric here: #446 (comment) Beyond that, I don't know anything about using it yet (even without Compose) But even if you switch to the old way, you'll run into the issue I showed here: #446 (comment) |
Ok got it @thevoiceless thanks for clarifying. I tried the old way, i.e. linking the package manually, used the exact same ViewManager class, then the same requireComponent usage in RN land. Still getting the "not attached to window" error! I wonder, is it because my app uses the new architecture? i.e. Also, I think the issue is not a problem for me. I just need to display one view with no navigation. Instead of navigation, the view redraws itself as the users advances through a flow Thanks for the fabric example! I'll check it out |
@dm20 Unfortunately I don't get much time to experiment with Compose + RN, so I have not tried the new architecture either 😅 Did you try the workaround from #446 (comment) ? The Compose code may have changed since then so I don't know if you can copy and paste exactly what's in that comment Edit: Actually @ShikaSD specifically called this out as being an issue when using Fabric in #446 (comment) |
@thevoiceless I am trying it right now, only problem is the |
Tried some workarounds getting the current activity from the ThemedReactContext using @ShikaSD suggested technique, still got "not attached to window" error 😢 |
Continuing my investigation from #446 (comment) .... I used a breakpoint to assign Untitled.mov |
I commented about this under the issue above, but wanted to mention it here as well. |
@ShikaSD does that mean this is related to https://issuetracker.google.com/issues/195342734 ? Unfortunately I don't see an obvious way to hook into the lifecycle of fragments created by |
Yeah, most likely it is caused by the same recycling mechanism |
I am using the fabric native component to create abstract composeview and getting the following error at runtime an app gets crashes. error: java.lang.AbstractMethodError: abstract method "void androidx.compose.ui.platform.AbstractComposeView.Content(androidx.compose.runtime.Composer, int)" CustomView.kt
CustomViewManager.kt
CustomViewPackage.kt
|
Thanks for trying Jetpack Compose in React native. Made my R&D much faster. I am working on a Jetpack Compose native component and faced the same issue of For reference versions are: So far I have learned that there are two possible solutions:
But I am looking forward for a better approach that will enable me to have no issue with 'react-native-screens'. |
In ContrainsLayout FilltoContraints not working, check below code. ConstraintLayout (modifier = Modifier.background(color = Color.Black)){
var inputValue : MutableState<String> = remember {
mutableStateOf("")
}
var ans : MutableState<String> = remember{
mutableStateOf("")
}
val card1 = createRef()
val (row1, row2, row3, row4, row5) = createRefs()
Card(
shape = RoundedCornerShape(0.dp,0.dp,20.dp,20.dp),
modifier = Modifier
.constrainAs(card1) {
top.linkTo(parent.top, margin = 0.dp)
start.linkTo(parent.start, margin = 0.dp)
end.linkTo(parent.end, margin = 0.dp)
bottom.linkTo(row1.top, margin = 10.dp)
height = Dimension.fillToConstraints
},colors = CardDefaults.cardColors(
containerColor = Color.DarkGray,
)
) {
TextField(
value = inputValue.value,
onValueChange = { },
label = { Text("", color = Color.White, fontSize = 30.sp) },
modifier = Modifier.fillMaxSize(),
colors = TextFieldDefaults.textFieldColors(textColor = Color.White, containerColor = Color.DarkGray),
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.End),
)
TextField(
value = ans.value,
onValueChange = { },
label = { Text("", color = Color.White, fontSize = 20.sp) },
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
colors = TextFieldDefaults.textFieldColors(textColor = Color.White,containerColor = Color.DarkGray),
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.End)
)
}
Row(modifier = Modifier.constrainAs(row1) {
start.linkTo(parent.start, margin = 0.dp)
end.linkTo(parent.end, margin = 10.dp)
bottom.linkTo(row2.top, margin = 10.dp)
}.background(color = Color.Black).border(0.dp,Color.Transparent), horizontalArrangement = Arrangement.spacedBy(15.dp)){...} |
@thevoiceless - thank you for dropping a detailed paper trail of your journey, specifically regarding the react-native-screens issue you've discussed. I, too, wanted to embed a native ComposeView inside a screen and have ultimately concluded that wrapping it within a Fragment is the best approach moving forward since overriding the nature of the composable lifecycle ultimately forces the composable to recompose, which isn't ideal in all scenarios. So unless react-native-screens starts treating their in house "Screen" fragments like normal fragments, instead of caching the views of fragment, as noted in that Google issue thread you created, our hands are tied. Created an issue on react-native-screens here software-mansion/react-native-screens#2098 That said, Wrapping a simple component in a Fragment comes with a whole different set of issues, specifically layout logistics. As seen here I'm hoping to come back to this thread with a solution that does all of that soon. ™️ Until then, I've come here to drop my award for the dirtiest hack around this problem. This isn't ideal for intense composable, be warned. In the container that hosts the ComposeView, you can force a recomposition of the react component which allows the ComposeView to always recompose by updating the
|
Thank you for creating that issue! I've transitioned away from React Native so this topic has fallen off of my radar. Per their reply, it sounds like they're at least aware of the issue: software-mansion/react-native-screens#2098 (comment) |
Going through the same thing, my component renders in a View but when inside a Modal the app crash |
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):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 forkingRN 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:As far as I can tell, both approaches prevent the crash and Compose seems to function as expected. However:
Option 1
involves forking RN andI'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 least1.3.1
?Or better yet, some kind of official support for Compose? Perhaps a
SimpleComposeViewManager
that directly accepts@Composable
content?The text was updated successfully, but these errors were encountered: