How can I add Jetpack Compose & xml in the same activity? An example would be perfect.
If you want to use a Compose in your XML file, you can add this to your layout file:
<androidx.compose.ui.platform.ComposeView
android:id="#+id/my_composable"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
and then, set the content:
findViewById<ComposeView>(R.id.my_composable).setContent {
MaterialTheme {
Surface {
Text(text = "Hello!")
}
}
}
If you want the opposite, i.e. to use an XML file in your compose, you can use this:
AndroidView(
factory = { context ->
val view = LayoutInflater.from(context).inflate(R.layout.my_layout, null, false)
val textView = view.findViewById<TextView>(R.id.text)
// do whatever you want...
view // return the view
},
update = { view ->
// Update the view
}
)
If you want to provide your composable like a regular View (with the ability to specify its attributes in XML), subclass from AbstractComposeView.
#Composable
fun MyComposable(title: String) {
Text(title)
}
// Do not forget these two imports for the delegation (by) to work
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
class MyCustomView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : AbstractComposeView(context, attrs, defStyle) {
var myProperty by mutableStateOf("A string")
init {
// See the footnote
context.withStyledAttributes(attrs, R.styleable.MyStyleable) {
myProperty = getString(R.styleable.MyStyleable_myAttribute)
}
}
// The important part
#Composable override fun Content() {
MyComposable(title = myProperty)
}
}
And this is how you would use it just like a regular View:
<my.package.name.MyCustomView
android:id="#+id/myView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:myAttribute="Helloooooooooo!" />
Thanks to ProAndroidDev for this article.
Footnote
To define your own custom attributes for your view, see this post.
Also, make sure to use -ktx version of the AndroidX Core library to be able to access useful Kotlin extension functions like Context::withStyledAttributes:
implementation("androidx.core:core-ktx:1.6.0")
https://developer.android.com/jetpack/compose/interop?hl=en
To embed an XML layout, use the AndroidViewBinding API, which is provided by the androidx.compose.ui:ui-viewbinding library. To do this, your project must enable view binding.
AndroidView, like many other built-in composables, takes a Modifier parameter that can be used, for example, to set its position in the parent composable.
#Composable
fun AndroidViewBindingExample() {
AndroidViewBinding(ExampleLayoutBinding::inflate) {
exampleView.setBackgroundColor(Color.GRAY)
}
}
Updated : When you want to use XML file in compose function
AndroidView(
factory = { context ->
val view = LayoutInflater.from(context).inflate(R.layout.test_layout, null, false)
val edittext= view.findViewById<EditText>(R.id.edittext)
view
},
update = { }
)
Related
I am trying to mix Jetpack compose with some legacy code that we have. Was hoping that it would be an easy fix since this is a part of the app that is rarely used. The problem at hand is that i am trying to add legacy view that has databinding to an already made compose view
The View
#SuppressLint("ViewConstructor")
class TimeAndDateScroller #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0,
timePickerViewModel: TimeAndDatePickerViewModel,
) : LinearLayout(context, attrs, defStyle) {
var binding: ViewTimePickerBinding? = null
init {
binding = ViewTimePickerBinding.inflate(LayoutInflater.from(context), this, true).apply {
this.viewModel = timePickerViewModel
}
}
}
Compose View
AndroidView(
factory = {
TimeAndDateScroller(it, timePickerViewModel = viewModel).apply {
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
}
}, update = {
}
)
ViewModel and XML
The view model is passed down correctly as far as i understand. But the values inside the view model is not triggering the listening xml view
val isPickerEnabled: LiveData<Boolean> = selectedOption
.map {
it != TimeParamType.NOW
}
.asLiveData(Dispatchers.Main)
The value above is found in the view model. But the corresponding xml listener is never triggered
android:alpha="#{viewModel.isPickerEnabled() ? 1f : 0.4f}"
In the viewModel you would normally add:
val isPickerEnabledValue = ObservableField<Boolean>()
init {
isPickerEnabledValue.set(false)
}
Then this will be updated based on the live data in the activity/fragment:
vm.isPickerEnabledLiveData.observe(this, isEnabled ->
vm.isPickerEnabledValue.set(isEnabled)
}
)
And then in the xml it would be:
android:alpha="#{viewModel.isPickerEnabledValue ? 1f : 0.4f}"
So since this is hybrid with compose, instead of the ObservableField we would use:
var isPickerEnabledValue: State<Boolean> = stateOf(false)
and then in Compose you could use:
AndroidView(
factory = {
viewModel.isPickerEnabledValue = viewModel.isPickerEnabledLiveData.observeAsState()
The key is you can't get data out of live data directly, but have to observer it in some way, while ObservableField or State can be used directly.
We are working with 5 people on a project.
I have custom TextView component in Android project.
Some of my team friends are using Android Textview (or AppCompatTextView) directly. I want to make it mandatory to use the text view that I created as a custom TextView.
How do I do this? I look forward to your help, thank you.
While coding guidelines and code reviews should catch those issues. You could also create a custom lint check and force your builds to fail on lint errors.
Something like this:
class TextViewDetector : ResourceXmlDetector() {
override fun getApplicableElements(): Collection<String>? {
return listOf(
"android.widget.TextView", "androidx.appcompat.widget.AppCompatTextView"
)
}
override fun visitElement(context: XmlContext, element: Element) {
context.report(
ISSUE, element, context.getLocation(element),
"Do not use TextView"
)
}
companion object {
val ISSUE: Issue = Issue.create(
"id",
"Do not use TextView",
"Use custom view",
CORRECTNESS, 6, Severity.ERROR,
Implementation(TextViewDetector::class.java, RESOURCE_FILE_SCOPE)
)
}
}
There is a guide, an example repository from google and an extensive api guide on how to write custom lint checks.
You can create your own ViewInflater
class MyViewInflater {
fun createView(
parent: View?, name: String?, context: Context,
attrs: AttributeSet, inheritContext: Boolean,
readAndroidTheme: Boolean, readAppTheme: Boolean, wrapContext: Boolean
): View {
// ...
val view: View = when (name) {
"TextView",
"androidx.appcompat.widget.AppCompatTextView",
"com.google.android.material.textview.MaterialTextView" -> createMyTextView(context, attrs)
//other views
}
//...
return view
}
fun createMyTextView(context: Context, attrs: AttributeSet) = MyTextView(context, attrs)
}
and install it in your app theme
<style name="Theme.MyAppTheme" parent="Theme.SomeAppCompatParentTheme">
<item name="viewInflaterClass">package.MyViewInflater</item>
</style>
It will return your View for all tags you specify
See AppCompatViewInflater
There's no technical way to do this. The answer is coding guidelines and code reviews.
Per https://developer.android.com/jetpack/compose/interop/interop-apis , ComposeView and AbstractComposeView should facilitate interoperability between Compose and existing XML based apps.
I've had some success when deploying to a device, but XML previews that include Compose elements aren't working for me.As a simplified example, consider:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="state"
type="ObservableState" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TestAtom
android:layout_width="match_parent"
android:layout_height="match_parent"
app:state="#{state}" />
</LinearLayout>
Making use of the following custom view file:
data class ObservableState(val text: ObservableField<String> = ObservableField("Uninitialized"))
data class State(val text: MutableState<String> = mutableStateOf(String()))
class TestAtom
#JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : AbstractComposeView(context, attrs, defStyleAttr) {
val state by mutableStateOf(State())
fun setState(observableState: ObservableState?) {
state.text.value = observableState?.text?.get() ?: "null"
}
#Composable
override fun Content() {
if (isInEditMode) {
val text = remember { mutableStateOf("Hello Edit Mode Preview!") }
TestAtomCompose(State(text = text))
} else {
TestAtomCompose(state)
}
}
}
#Composable
fun TestAtomCompose(state: State) {
Text(text = "Text: " + state.text.value, modifier = Modifier.wrapContentSize())
}
#Preview(name = "Test", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO)
#Composable
fun PreviewTestCompose() {
val text = remember { mutableStateOf("Hello World!") }
TestAtomCompose(State(text = text))
}
I have tried many variations and iterations on this, however none successfully allowed a Compose-based custom view to render in an XML layout preview. (the #Compose preview works as expected) The above example results in a preview render error: java.lang.IllegalStateException: ViewTreeLifecycleOwner not found from android.widget.LinearLayout.
1) Has anyone found a tactic to allow complex Compose-based custom views to render in XML preview panes?
2) Is there a way to transmit XML preview pane selection options, like theme and light/dark mode, to an embedded ComposeView to update its preview?
3) Is there a way to assign specific sample data from XML to a ComposeView to cause it to render differently in the preview? (similar to tools: functionality)
In order to preview in XML design a custom ComposableView that extend from AbstractComposeView you will need to tell to your view to use a custom recomposer.
AfzalivE from GitHub provide a gist about that:
https://gist.github.com/AfzalivE/43dcef66d7ae234ea6afd62e6d0d2d37
Copy this file in your project, then override the onAttachToWindow method with the following:
override fun onAttachedToWindow() {
if (isInEditMode) {
setupEditMode()
}
super.onAttachedToWindow()
}
FYI: Google is working on a fix about this issue:
https://issuetracker.google.com/u/1/issues/187339385?pli=1
I know it's possible to use a custom view instead of a basic map marker in Google Maps doing what's described in this answer. I was curious if it was possible to achieve a similar affect with Compose?
Since ComposeView is a final class, couldn't extend that directly so I was thinking of having a FrameLayout that could add it as a child. Although this seems to be causing a race condition since composables draw slightly differently from normal android Views.
class MapMarkerView : FrameLayout {
constructor(context: Context) : super(context) {
initView()
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
initView()
}
private fun initView() {
val composeView = ComposeView(context).apply {
layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
}
composeView.setContent {
// this runs asynchronously making the the bitmap generation not include composable at runtime?
Text(text = "2345", modifier = Modifier.background(Color.Green, RoundedCornerShape(4.dp)))
}
addView(composeView)
}
}
Most articles I have read have some callback function for generating a bitmap like this which wouldn't necessarily work in this use case where each view is generated dynamically and needed to be converted to a bitmap right away for the map.
The MapMarkerView you are attempting to create is an indirect child of ViewGroup. ComposeView is also an indirect child of ViewGroup which means you can directly substitute a ComposeView in places where you intend to use this class. The ComposeView class is not meant to be subclassed. The only thing you really need to be concerned with as a developer is its setContent method.
val composeView: View = ComposeView(context).apply{
setContent {
Text(text = "2345", modifier = Modifier.background(Color.Green, RoundedCornerShape(4.dp)))
}
}
Now you can replace the MapMarkerView with this composeView in your code or even use it at any location where a View or any of it's subclasses is required.
I'm trying to use Robinhood Spark in my app but my entire UI is built using jetpack compose. I'm going through these docs but it's only mentioned how you can use XML resource files like String, Dimensions, Colors, Images, Vector drawbles, Icons, and Fonts. I don't know which one Spark falls under. Looking at the XML code it's a LinearLayout but I can't see how that falls under any of the resource types I've mentioned prior. So for now I'm going through non-compose documentations to see if there is some kind of class or method I can use but would appreciate some feedback and help.
It is as nglauber mentioned, but you would need to define layout parameters that will fit into your compose view as well.
More about it https://foso.github.io/Jetpack-Compose-Playground/viewinterop/androidview/
AndroidView(factory = { ctx ->
val view = LayoutInflater.from(ctx)
.inflate(R.layout.activity_sample, null, false)
.apply {
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
}
// Use your view as usual...
view
}, update = {
// here update your view or get element from it and update
})
You can use AndroidView to load your XML file.
AndroidView(
factory = { context: Context ->
val view = LayoutInflater.from(context)
.inflate(R.layout.your_layout, null, false)
// Use your view as usual...
view // return the view
},
update = { view ->
// Update view if needed
}
)