15

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()
Marvin
  • 1,726
  • 1
  • 17
  • 25

5 Answers5

6

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()
Kirill Rakhman
  • 42,195
  • 18
  • 124
  • 148
  • 1
    the lateinit property could be used to keep it non-null as long as you're sure it's going to get initialized before it gets used. – eski Dec 11 '15 at 18:31
  • I think that's the wrong approach for an alert dialog. You would leak the dialog after it's closed. – Kirill Rakhman Dec 12 '15 at 19:43
  • I think the reference can be recycled once the enclosing scope can be recycled. I'm not sure nullability would have anything to do with it unless I'm wrong? – eski Dec 13 '15 at 20:16
  • 2
    Are you talking about properties or local variables? Because variables cannot have the `lateinit` modifier. – Kirill Rakhman Dec 13 '15 at 20:33
  • Good news here: Kotlin supports lateinit on local variables since 1.2 :-) – Maciej Jastrzebski Oct 17 '18 at 19:36
2

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()
Nevin Chen
  • 1,815
  • 16
  • 19
  • I used this method to work around, but it's a bit overelaborate to define an id in an xml resource then use it in DSL. – Marvin Dec 14 '15 at 01:28
  • 5
    yes, but you can generate ids straight in your code ```@IdRes val DIALOG_ITEM_TEXT = View.generateViewId()``` also, you can use a handy findVIewById shortcut, something like: ```inline fun View.byId(@IdRes id: Int): T = findViewById(id) as T``` – Adel Nizamuddin Dec 15 '15 at 22:33
  • @AdelNizamutdinov `View.generateViewId()` needs API 17, while my minimum sdk is 16 – HendraWD Nov 12 '17 at 08:59
  • @HendraWD it's time for minSdk 19 :) but seriously, make your own function `generateViewId()` then, that would guarantee unique values – Adel Nizamuddin Nov 13 '17 at 09:13
1

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}" }
}
voddan
  • 31,956
  • 8
  • 77
  • 87
  • It's almost perfect, but this way the code structure does not keep the same as the view structure, readability impacted. – Marvin Dec 14 '15 at 09:01
  • I myself usually brake the code into smaller peaces and put them into global variables. Then combine them somewhere else. – voddan Dec 14 '15 at 09:07
1

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)
Antek
  • 721
  • 1
  • 4
  • 27
0

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))