Accessing views from the Activity with Anko
Asked Answered
M

4

6

I know I can use an id attribute with Anko to identify a view:

class MainActivityUI : AnkoComponent<MainActivity> {

    override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {
        frameLayout {
            textView {
                id = R.id.text
            }
        }
    }

}

Then obtain it in the Activity using the find() function (or by using Kotlin Android Extensions):

class MainActivity : AppCompatActivity() {

    private val textView by lazy {
        find<TextView>(R.id.text)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        MainActivityUI().setContentView(this)

        textView.text = "Hello World"
    }

}

But I feel like I am missing something; the only place the README mentions the find function or Kotlin Android Extensions is in the section titled Supporting Existing Code:

You don't have to rewrite all your UI with Anko. You can keep your old classes written in Java. Moreover, if you still want (or have) to write a Kotlin activity class and inflate an XML layout for some reason, you can use View properties, which would make things easier:

// Same as findViewById(), simpler to use
val name = find<TextView>(R.id.name)
name.hint = "Enter your name"
name.onClick { /*do something*/ }

You can make your code even more compact by using Kotlin Android Extensions.

Which makes it seem like the find function is only meant for supporting "old" XML code.

So my question is this; is using an id along with the find function the correct way of accessing a View from the Activity using Anko? Is there a more "Anko" way of handling this? Or am I missing some other benefit of Anko that makes accessing the View from the Activity irrelevant?


And a second related question; if this is the correct way of accessing a View from the Activity, is there a way of creating an id resource (i.e. "@+id/") from within an AnkoComponent? Rather than creating each id in the ids.xml file.

Ment answered 7/12, 2016 at 14:42 Comment(0)
I
9

So, why still use XML id to locate the View? since we already use the Anko instead of the XML.

In my opinion, we can store the view elements inside the AnkoComponent instead of the find view's id method. Check the code blow:

class MainActivityUI : AnkoComponent<MainActivity> {

    lateinit var txtView: TextView

    override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {
        frameLayout {
            txtView = textView {
                id = R.id.text // the id here is useless, we can delete this line.
            }
        }
    }

}

class MainActivity : AppCompatActivity() {

    lateinit var mainUI : MainActivityUI

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        mainUI = MainActivityUI()
        mainUI.setContentView(this)

        mainUI.txtView.text = "Hello World"
    }

}
Irradiant answered 7/12, 2016 at 15:47 Comment(1)
I did come across this as well; but I did notice one significant drawback. If, for example, you wanted a reusable portion of a layout (the <include> tag in XML), from what I understand, you must create a separate AnkoComponent, and use it as a view in the other component. In this case, each View member would be attached to the component it resides within; instead of the entire view. In this case, you would have to use say mainUI.toolbarUI.toolbar to access a reused view; whereas find() can access any view within the hierarchy. – Ment
C
3

Do not use id to identify views with Anko DSL! It is unnecessary and useless because Anko was designed to get rid off XML layouts. Instead use this pattern:

class ActivityMain : AppCompatActivity() {

    var mTextView: TextView  // put it here to be accessible everywhere

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ActivityMainUI().setContentView(this)
    }

    fun yourClassMethod() {
        // So this is am example how to get the textView 
        // defined in your Anko DSL class (not by id!):
        mTextView.text = "bla-bla-bla"  
    }

}

class ActivityMainUI : AnkoComponent<ActivityMain> {

    override fun createView(ui: AnkoContext<ActivityMain>) = with(ui) {

        // your fancy interface with Anko DSL:
        verticalLayout {
            owner.mTextView = textView
        }
    }
}

Please note the UI class definition:

class ActivityMainUI : AnkoComponent<ActivityMain> {

If you put there your activity class name in brackets then all its public variables become accessible via owner in UI class body so you can assing them there.
But you may put AppCompatActivity easily and make some universal class which might be cloned. In this case use lateinit var mTextView : TextView in the body of UI class as described in Jacob's answer here.

Canaday answered 13/10, 2017 at 10:33 Comment(1)
πŸ™ for showing use of owner. After 5 hours of reading about Anko (and admittedly being a Kotlin newbie), this is the very first mention of it I've found. – Mannered
O
1

I believe that, as you can add behavior to your Anko files, you don't have to instantiate your views in the activity at all.

That can be really cool, because you can separate the view layer even more. All the code that acts in your views can be inserted in the Anko files. So all you have to do is to call your activity's methods from the Anko and not instantiate any view.

But if you need to instantiate any view... you can use Kotlin Android Extensions in your activity.

Exemple:

Code in your activity:

seekBar.setOnSeekBarChangeListener(object: OnSeekBarChangeListener {
    override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
        // Something
    }
    override fun onStartTrackingTouch(seekBar: SeekBar?) {
        // Just an empty method
    }
    override fun onStopTrackingTouch(seekBar: SeekBar) {
        // Another empty method
    }
})

Code in Anko:

seekBar {
    onSeekBarChangeListener {
        onProgressChanged { seekBar, progress, fromUser ->
            // Something
        }
    }
}

Now the code is in AnkoComponent. No need to instantiate the view.

Conclusion:

It's a more 'Anko' way to program if you put all your view logic in the AnkoComponents, not in your activities.

Edit:

As an exemple of a code where you don't have to instantiate a view:

In your Anko:

var networkFeedback : TextView = null

    override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {
            frameLayout {
                textView {
                    id = R.id.text2
                    networkFeedback = this
                    onClick {
                        ui.owner.doSomething(2, this)
                    }
                }
            }
        }

fun networkFeedback(text: String){
       networkFeedback.text = text
}

In your activity:

class MainActivity : AppCompatActivity() {

    overriding fun onCreate{
            [...]
            val mainUi = AnkoUi()
            // some dynamic task...
            mainUi.networkFeedback("lalala")
     }

    fun doSomething(number: Int, callback: TextView){
            //Some network or database task goes here!

            //And then, if the operation was successful

            callback.text = "Something has changed..."
        }

This is a very different approach. I'm not so sure if I like it or not, but this is a whole different discussion...

Overtax answered 7/12, 2016 at 16:57 Comment(8)
I do know about Kotlin Android Extensions; but as I mentioned in my question, the Supporting Existing Code section of the README seems to suggest that both of these methods (Kotlin Android Extensions and the find function) are only necessary when supporting XML code. – Ment
Sorry. My bad. Check the edited answer. Maybe it helps – Overtax
I know Anko also makes it easy to add a listener for a UI event, such as a click or a seek bar change; but what if the event is not a UI event? As a simple example, how would you use this to set the text in a TextView based on a dynamic value (i.e. something from a database or a network result) rather than a static one? – Ment
You should only add code for UI events in Anko, otherwise you are going to mix the view and the control layer and the project is going to be a mess. But, yes, you can change the text of a TextView (or any other UI element) based on a dynamic value. Check my edited answer for an exemple. – Overtax
I don't think this solution really solves the issue. You cannot expect that a view should only be updated when it is clicked on. What if the view needs to be refreshed by a network call as soon as the application is opened? In this case you cannot call doSomething(), because you do not have a reference to the TextView. Or an even simpler example; how would you call doSomething() from another view (such as a SwipeRefreshLayout)? – Ment
And the networkFeedback function essentially raises the same issue as the answer by @Jacob. – Ment
I am not assuming that a view is only updated when it is clicked on, you can see that in the method networkFeedback. I think it solves the issue in the @Irradiant answer. You don't need to access view inside view like: LinearLayout.RelativeLayout.CardView.TextView.... All you have to do is call a method like: updateThatTextViewInsideCardView() (Not exactly with this name hahaha) for you UI feedback. With this logic you can call doSomething() inside another view. If you separate all the view logic in the AnkoComponents, your MVC architecture is going to get a lot better (smaller controllers). – Overtax
I don't think you understood my comment; mainUI and toolbarUI are not separate views they are each a separate AnkoComponent. The purpose of this would be to create a single ToolbarUI component that you can reuse in multiple components. In this case, to access the toolbar (or a function referencing the toolbar), you would have to either call mainUI.toolbarUI.toolbar or add a function in the mainUI class that references the toolbar from its ToolbarUI; such as mainUI.getToolbar(). But this is just unnecessary boilerplate that using an id avoids. – Ment
S
1

To generalize the question a bit: How can one make an AnkoComponent that is encapsulated, can be used from the DSL and can have its data programmatically set after creation?

Here is how I did it using the View.tag:

class MyComponent: AnkoComponent<Context> {
    lateinit var innerds: TextView
    override fun createView(ui: AnkoContext<Context>): View {
        val field = with(ui) {
            linearLayout {
                innerds = complexView("hi")
            }
        }
        field.setTag(this) // store the component in the View
        return field
    }

    fun setData(o:SomeObject) { innerds.setStuff(o.getStuff()) }
}

inline fun ViewManager.myComponent(theme: Int = 0) = myComponent(theme) {}
inline fun ViewManager.myComponent(theme: Int = 0, init: MyComponent.() -> Unit) = 
    ankoView({ MyComponent(it) }, theme, init)

// And then you can use it anywhere the Anko DSL is used.
class SomeUser : AnkoComponent<Context>
{
    lateinit var instance:View 
    override fun createView(ui: AnkoContext<Context>): View {
        linearLayout {
            instance = myComponent {}
        }
    }
    fun onDataChange(o:SomeObject) {
        (instance.Tag as MyComponent).setData(o)
    }
}

}

Scaffold answered 5/7, 2018 at 18:21 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.