How to reference other views in Anko DSL? - android

I'm using Anko in my Android project, but I don't know how can it reference the child views I created in the DSL when the referenced view is not at the same level where I reference it.
The following code works:
alert {
customView {
val input = textInputLayout {
editText {
hint = "Name"
textColor =resources.getColor(R.color.highlight)
}
}
positiveButton("OK") { "${input.editText.text}" }
}
}.show()
but the following code does not work:
alert {
customView {
val vertical = verticalLayout {
textView {
text = "Edit device name"
textColor = resources.getColor(R.color.highlight)
textSize = 24F
}
val input = textInputLayout {
editText {
hint = "Name"
textColor = resources.getColor(R.color.highlight)
}
}
}
positiveButton("OK") { "${vertical.input.editText.text}" } // Cannot resolve "input"
}
}.show()

As I see it there are two ways. The super hacky way would be to declare the positive button within the textInputLayout block. This is possible because you can access all outer scopes from within any nested scope and the positiveButton method is declared in the alert scope:
alert {
customView {
verticalLayout {
textInputLayout {
val editText = editText {
hint = "Name"
}
positiveButton("OK") { toast("${editText.text}") }
}
}
}
}.show()
The less hacky way would be to declare a variable that can be accessed from both scopes. However you would need to make it nullable since you can't initialize it immediately:
alert {
var editText: EditText? = null
customView {
verticalLayout {
textInputLayout {
editText = editText {
hint = "Name"
}
}
}
}
positiveButton("OK") { toast("${editText!!.text}") }
}.show()

I propose using findViewById()
alert {
customView {
val vertical = verticalLayout {
textView {
text = "Edit device name"
textSize = 24F
}
val input = textInputLayout {
editText {
id = R.id.my_id_resource // put your id here
hint = "Name"
}
}
}
positiveButton("OK") { "${(vertical.findViewById(R.id.my_id_resource) as? EditText)?.text}" }
}
}.show()

You can always elevate a view, passing the context vertical manually:
customView {
val vertical = verticalLayout {
textView {
text = "Edit device name"
textColor = resources.getColor(R.color.highlight)
textSize = 24F
}
}
val input = /*here:*/ vertical.textInputLayout {
editText {
hint = "Name"
textColor = resources.getColor(R.color.highlight)
}
}
positiveButton("OK") { "${input.editText.text}" }
}

I usually declare a view as a property in a class with lateinit modifier; this way it's not nullable and most of views are declared in one place, improving readability:
lateinit var toolbar: Toolbar
...
appBarLayout {
toolbar = toolbar {}.lparams(width = matchParent, height = matchParent)
}.lparams(width = matchParent)
...
setSupportActionBar(toolbar)

Probably the best way is to use Android IDs for the elements you need to reference later, and the find<T : View>(Int) : T function. This allows you to reference them from anywhere, as long as the view still exists, and you have access to the application/activity scope.
See the Anko Documentation for details
Example case: Dynamically adding buttons to an existing view
verticalLayout {
id = R.id.button_container
}
//note that the code below here may be executed anywhere after the above in your onCreate function
//verticalLayout is a Anko subclass of LinearLayout, so using the android class is valid.
val buttonContainer = find<LinearLayout>(R.id.button_container)
val containerContext = AnkoContext.Companion.create(ctx, buttonContainer)
val button = ctx.button {
text = "click me"
onClick = { toast("created with IDs!") }
}
buttonContainer.addView(button.createView(containerContext, buttonContainer))

Related

Show / Hide ComposeView in XML - Android, Kotlin, Jetpack Compose, Interoperability

I am adding a ComposeView to my XML layout. The ComposeView is a widget that will stay hidden until a button is clicked (button is in XML).
I have tried a number of things, but the way the app is setup my ComposeView widget does not recompose after the the button click.
Code
DetailsWidget is where the onClickListener is set for each widget
class DetailsWidget : ListingDetailsWidget {
...
override fun buildView(inflater: LayoutInflater) : View? {
...
val expandButton = baseView.findViewById<View>(R.id.btn_expand)
val buttonContainer = baseView.findViewById<View>(R.id.expand_button_container)
val tableContainerComposeview = baseView.findViewById<View>(R.id.table_container_compose_view) <- I added this line to existing code
tableView.post {
if (!tableView.isExpanded) {
buttonContainer.visibility = View.VISIBLE
expandButton.setOnClickListener {
tableView.isExpanded = true
buttonContainer.visibility = View.GONE
DetailsWidgetViewModel().onTableViewExpanded() <- I added this line to existing code
}
}
}
return baseView
}
}
DetailsWidgetViewModel
class DetailsWidgetViewModel : ViewModel() {
private val _showDetailsWidget = MutableStateFlow(false)
val showDetailsWidget: StateFlow<Boolean> = _showDetailsWidget.asStateFlow()
fun onTableViewExpanded() {
_showDetailsWidget.value = true
}
}
JetpackComposeWidget
#Parcelize
#Serializable
data class JetpackComposeWidgetData(
...
) : ListingDetailsWidget() {
override fun buildView(inflater: LayoutInflater): View {
val binding = ViewDetailWidgetDetailsBinding.inflate(inflater, null, false)
val view = binding.root
binding.tableContainerComposeView.setContent {
JetpackComposeWidget(data = this)
}
return view
}
}
#Composable
fun JetpackComposeWidget(data: JetpackComposeWidgetData) {
val showDetailsWidgetState: Boolean by DetailsWidgetViewModel().showDetailsWidget.collectAsState()
if (showDetailsWidgetState) {
LdpWidgetTheme {
Surface(
color = MaterialTheme.colors.surface,
modifier = Modifier.clickable { DetailsWidgetViewModel().onTableViewExpanded() }
) {
Column(
Modifier
.padding(8.dp),
) {
data.title?.let {
Text(
text = it,
style = MaterialTheme.typography.h4,
)
}
data.sections.mapIndexed { index, section -> Section(section, index == 0) }
}
}
}
}
}
Is there a better way to show / hide a ComposeView in XML?
What I've tried:
Add to ComposeView XML android:onClick='onTableViewExpanded'
Add to ComposeView XML android:visibility:"gone" then setting the ComposeView to View.VISIBLE in the setOnClickListener code block
Call an event listener on button click
Recompose on button click using _showDetailsWidget.copy(value = true)
Live Data and Mutable State instead of MutableStateFlow

Using style in AndroidView Compose

How can we use style property inside AndroidView in Compose? The below code snippet doesn't work.
AndroidView(factory = { context ->
Button(context).apply {
text = "Turn On"
style = "#style/button_style"
}
})
"#style/button_style" can only be used inside XML.
When you set styles/colors/drawables and other resources form code, you need to use R.style.button_style instead.
Also android android.widget.Button doesn't have style parameter, but you can pass it with a context into the initializer, like this:
AndroidView(factory = { context ->
val style = R.style.GreenText
android.widget.Button(ContextThemeWrapper(context, style), null, style).apply {
text = "Turn On"
}
})
UPDATE: here's a more detailed example of AndroidView usage. You can set initial values during factory, and you can update your view state depending on Compose state variables inside update block
var flag by remember { mutableStateOf(false) }
Switch(checked = flag, onCheckedChange = { flag = !flag })
AndroidView(
factory = { context ->
val style = R.style.GreenText
android.widget.Button(ContextThemeWrapper(context, style), null, style).apply {
text = "Turn On"
// set initial text color from resources
setTextColor(ContextCompat.getColor(context, R.color.black))
}
},
update = { button ->
// set dynamic color while updating
// depending on a state variable
button.setTextColor((if (flag) Color.Red else Color.Black).toArgb())
}
)

Custom Rating Bar using Anko

I've been exploring android development using Anko and Kotlin and had some trouble with the ratingbar, namely it size. I've tried to make it smaller using a custom style but themedRatingBar doesn't seem to work. So I've opted to make a custom ratingbar instead. I can't seem to make it work that way I want it to in that when I set it this way in the main activity:
starRatingView{
setRating(3)
}
It does not output a rating of 3 and instead will output the default rating, which is zero.
class StarRatingView: _LinearLayout {
lateinit var imageViewStars: List<ImageView>
private var starNum: Float = 0f
private var starSize: Int = 5
constructor(context: Context): super(context) {
initializeView()
}
fun initializeView() {
with(this) {
linearLayout {
relativeLayout {
linearLayout {
for (i in 1..starSize)
imageView(R.drawable.ratingbar_empty)
}
linearLayout {
for (i in 0..Math.round(starNum)) {
imageView(R.drawable.ratingbar_filled)
}
}
}
}
}
}
fun setSize(starSize: Int){
this.starSize = starSize
}
fun setRating(starNum: Float){
this.starNum = starNum
}
}
Above is the code that I use to create the custom RatinBar. Trying to avoid using XMLs as much as possible and use Anko instead.
if I can help here is a possible solution to show a custom RatingBar in a custom Alert using Anko library and Kotlin code...
var rateGave: String? = null
alert {
title = "Rate your experience"
customView {
linearLayout {
ratingBar {
numStars = 5 //here is to define the number of stars you want
rating = 4f //starting rate to show as the alert pop up
setOnRatingBarChangeListener { ratingBar, rating, fromUser ->
rateGave = rating.toString()
}
}
}
}
positiveButton("Rate") { rate() }
}.show()
}
fun rate() {
println(rateGave) //now it's just printing out the rate to show that it's working fine, but in this function you can mange all the operations you need with the rating value
}
Hope it can be usefull.
Have a nice day :)

How to group the duplicated statement in .apply in Kotlin?

Here is my code. As you can see the body of apply is exactly the same. Is there a better than use extension function?
contentText?.let {
contentTextView?.apply {
visibility = View.VISIBLE
text = contentText
}
}
titleText?.let {
titleTextView?.apply {
visibility = View.VISIBLE
text = titleText
}
}
Here is my function
private fun setTextAndVisiblity(textView: TextView?, newText: String?): TextView? {
return textView?.apply {
visibility = View.VISIBLE
text = newText
}
}
This is my code when apply function
contentText?.let {
setVisibleText(contentTextView, it)
}
titleText?.let {
setVisibleText(titleTextView, it)
}
I would write the extension function like this:
fun TextView.setVisibleIfTextNotNull(text: CharSequence?) = text?.let {
visibility = View.VISIBLE
this.text = it
}
Usage:
contentTextView?.setVisibleIfTextNotNull(contentText)
titleTextView?.setVisibleIfTextNotNull(titleText)
You can either make it as an nested function or private extension function as you like. The name of the function may not be clear enough to clarify what the function does, you may think of a better one.
An extension function seems like the best choice. If you make the function return this you can use it without apply.
Your other choice would be to create an ordinary function and pass it into also using method references, e.g.
fun setVisibleText(view: View) { }
titleTextView.also(this::setVisibleText)
the cleanest for me is to declare an extension function as:
fun TextView.setVisibleWithText(text: String?){
text ?: return
visibility = View.VISIBLE
setText(text)
}
then calling it as:
myTextView?.setVisibleWithText(myText)
Anyway,remember that an extension functions it just an static util function.
This function below:
fun TextView.setVisibleWithText(text: String){
visibility = View.VISIBLE
setText(text)
}
Will become something like this in java:
class TextViewKt {
public static function setVisibleWithText(#NotNull TextView receiver, #NotNull String text){
receiver.visibility = View.VISIBLE
receiver.setText(text)
}
}
And after you can call it as:
theText?.let { theTextView?.setVisibleWithText(it) }
You can always declare a normal funtion as:
fun setVisibleWithText(textView: TextView, text: String){
textView.visibility = View.VISIBLE
textView.text = text
}
or if you want to make the check inside:
/***
* It makes the textview visible if text is not null (it will stay visible if it was visible before)
**/
fun setVisibleWithText(textView: TextView, text: String?){
text ?: return
textView.visibility = View.VISIBLE
textView.text = text
}

How to use selectableButtonBackground on Anko?

How do I use selectableButtonBackground attribute on a custom View that uses Anko's apply() method inside its constructor like the following structure?
class XPTO(context: Context) : CardView(context) {
init {
this.apply {
// I'd like to invoke selectableButtonBackground here
}
}
I've tried to do context.obtainStyledAttributes(arrayOf(R.attr.selectableItemBackground).toIntArray()).getDrawable(0) but with no success.
I just created an extension function to get the resource ids for attributes.
val Context.selectableItemBackgroundResource: Int get() {
return getResourceIdAttribute(R.attr.selectableItemBackground)
}
fun Context.getResourceIdAttribute(#AttrRes attribute: Int) : Int {
val typedValue = TypedValue()
theme.resolveAttribute(attribute, typedValue, true)
return typedValue.resourceId
}
This way you can also add more attributes if needed. Example to put it in anko:
frameLayout {
textView {
text = "Test"
backgroundResource = selectableItemBackgroundResource
isClickable = true
}
}
Don't forget the isClickable, else you won't see anything when you're clicking the textView
Another way to achieve this with Anko:
val backgroundResource = attr(R.attr.selectableItemBackgroundBorderless).resourceId

Categories

Resources