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.
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.
I have a compound view that I want to create its viewmodel by ViewModelLazy, I need to send the ViewModelStoreOwner of the view to ViewModelLazy but trying to get the ViewModelStoreOwner using ViewTreeViewModelStoreOwner.get(this) always returns null. The compound view itself is a simple view, but I am using it in a recyclerview adapter that resides in a fragment. Right now, I am getting forced to use the parent fragment ViewModelStoreOwner, which is causing all the items in the adapter to have the same viewmodel instance. I searched for an example on how to use ViewTreeViewModelStoreOwner but I can't find one, am I missing something?
Note: I am injecting the viewmodel by dagger-hilt
This example shows how it can be implemented in custom view.
class SummaryView(context: Context, attrs: AttributeSet?) : ConstraintLayout(context, attrs) {
private val viewModel by lazy {
ViewModelProvider(findViewTreeViewModelStoreOwner()!!).get<SummaryViewModel>()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
viewModel.summaryModel.observe(findViewTreeLifecycleOwner()!!, ::populateSummaryView)
}
private fun populateSummaryView(summaryModel: SummaryModel) {
// do stuff
}
}
Found this great example here
1, you need update
androidx.activity to 1.2.0
androidx.fragment to 1.3.0
2, activity or fragment set its ViewModelStore to rootViw via setTag, so any view in activity or fragment's rootView gets the same ViewModelStoreOwner and also same ViewModelStore.
// ComponentActivity line 409
ViewTreeViewModelStoreOwner.set(getWindow().getDecorView(), this)
// ViewTreeViewModelStoreOwner line 50
view.setTag(R.id.view_tree_view_model_store_owner, viewModelStoreOwner);
I have an existing view that extends from ConstraintLayout which looks something like this:
class LandingTemplate: ConstraintLayout {
init {
inflate(context, R.layout.landing_template, this)
// Currently this 'recyclerView' is a kotlin synthetic
recyclerView.run {
// this sets up the recycler view
}
}
I'm familiar with view binding with activities and fragments, but I can't find any documentation around the extends layout case.
My question is, what do I replace that initial inflate call with here?
I'm assuming you have a context available from your constructor and your XML layout's top level tag is <merge>. You can use your binding class's inflate to create and add the child layout.
And since this can all be set up in the constructor, you don't need lateinit var like in the Activity/Fragment examples, and can just use val instead.
class LandingTemplate(context: Context, attrs: AttributeSet): ConstraintLayout(context, attrs) {
private val binding = LandingTemplateBinding.inflate(LayoutInflater.from(context), this)
init {
binding.recyclerView.run {
// this sets up the recycler view
}
}
}
you can get layout inflater like below
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val view = inflater.inflate(R.layout.landing_temple,this,true)
and you must have valid view construct too
LandingTemple(Context) // for creating view programmatically
LandingTemple(Context,AttrributeSet) // to inflate view from xml , and
//the constructor context is one that you use to call `getSystemService
for more information check
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
}
)
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 = { }
)