13 minutes read

You already know how to declare views in XML files. You also know that you can declare initial values for the attributes of each view you declare. However, what if you want to initialize or change those attributes during the execution of the app? For example, you may want to initialize a certain text value to the result of some calculation or procedure, like retrieving a username from a database. Or, you may need to set listeners for events, such as button clicks, and react to those.

The simplest and most reliable way to access the views declared in your XML files is using the findViewById method.

Declaring an ID for your view in an XML file

To be able to retrieve a view declared in an XML file, you will first need to set an id attribute to the view you wish to access.


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/someTextViewId"
        tools:text="placeholder text"
        android:textSize="32sp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        android:gravity="center"/>
</LinearLayout>

Above, we have an example with a really simple layout, which includes one TextView. The ID of that TextView is set to someTextViewId with the following attribute declaration: android:id="@+id/someTextViewId.

That is all we will need to do in the XML file. You can set IDs for any of your views, including layouts, for as many views as you want. It is a harmful practice to set multiple views with the same ID, so you should avoid doing that.

Retrieving views to local variables

Now, let us try to access the view we've declared in XML to programmatically make some changes to it. Look at some examples of how that can be achieved:

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

     // val beforeSetContentView: View = findViewById(R.id.someTextViewId)
     // example0, throws error

        setContentView(R.layout.activity_name)
     // must be called before any findViewById call

        val someView: View? = findViewById(R.id.someTextViewId) // example1
        val someTextView = findViewById<TextView>(R.id.someTextViewId)  // example2
        ...
}

The first thing we should acknowledge is that IDs declared in XML will be available as properties of the R.id class. That means we can make a reference to the ID we declared in our XML example with R.id.someTextViewId. If you look at the signature of findViewById, you will see it expects an Int to be passed as an argument. That is because technically, an ID is just an integer number and its value is automatically generated by AAPT (Android asset packaging tool) during compile time.

Now, let's talk about the differences between example0, example1, and example2.

In example0, we will receive the following error: java.lang.NullPointerException: findViewById(R.id.someTextViewId) must not be null.

The reason for that error is that you are only able to retrieve the views that already exist, which won’t happen until setContentView(R.layout.activity_name) is called. The method setContentView is responsible for bringing the views declared in XML into existence by a procedure called inflation, which will be considered in more detail in a future topic. If a view is not found, findViewById will return null. In example0, the NullPointerException is thrown because we were expecting a non-nullable value.

Let’s now look at example1. Here, we are declaring a more generic View type – and declaring it as nullable. This is not how it is usually done, though, for a number of reasons.

The first reason is that you usually won't be calling findViewById with an unexisting ID, since your code won't compile if you reference one, like R.id.someUnexistingViewId. For that reason, you usually don't need to worry about null values being returned from findViewById. Since it is just easier to work with non-nullable values, that is a preferable way. If, for some reason, you are not sure that the view you are looking for is going to be found, then you should declare a nullable type instead; however, that is not the most usual case.

The second reason is that you will usually be willing to use specific methods of a particular view class you've declared. This means it is preferable to declare your variable with that specific class instead of using a more generic class. For that reason, looking at our example, using TextView rather than View would be preferable.

In example2, we have an opposite to example1. That means example2 is the common way of using findViewById to declare local variables. The reasons for that are the same as those already discussed for example1.

An attentive reader might have noticed another difference between example1 and example2. In example1, we are declaring the type on the left side of the assignment, and in example2, we are passing the type information on the right side, inside the angle brackets. Both ways are equally good. You could also pass the information on both sides, but you cannot omit the information on both sides, as that would be a compilation error: Not enough information to infer type variable T.

The reason for this error is that findViewById is a generic function and the Kotlin compiler has to know the type argument for a function at compile time. In example1, the compiler is smart enough to infer the type information the function needs by looking at the type you are declaring for your variable on the left side of the assignment. In example2, we have it the other way around: here the type argument is passed directly to findViewById.

Declaring views as properties

Ok, we have learned how to retrieve our views as local variables. However, we don't want to redeclare variables for each method of our Activity class; besides, it is inconvenient to pass these views around as arguments. It's a better idea to declare properties right in our Activity class to hold those views, but wait, how can we do that if we need to call setContentView(R.layout.activity_name) before being able to call findViewById?

Let's see another code example. Assume that all used views were previously declared in the appropriate XML file.


var someButton: Button? = null    // example3
lateinit var someEditText: EditText  // example4
val someImageView: ImageView by lazy { findViewById(R.id.someImageViewId) } // example5

...

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_name)
        someButton = findViewById(R.id.someButtonId) // example3
        someEditText = findViewById(R.id.someEditTextId) // example4
        someImageView // example5
        ...
}

The code used in example3 is quite straightforward, but it is not common because working with nullable properties is a little less convenient than using non-nullable ones.

The pattern used in example4, with lateinit var, is the most common one you will come across. It is really important here to make sure the property is initialized before being read. Otherwise, you'll get an UninitializedPropertyAccessException and your application will crash. For this reason, it is a good practice to initialize properties declared with lateinit right after calling setContentView in the onCreate method.

If you are the kind of programmer who avoids var as much as you can because you value immutability and the benefits it brings to your code, then you might want to use the pattern from example5. In this case, a property will be initialized when it's first used. To be honest, it brings a lot of overhead for property delegation, access synchronization, and keeping initializer lambda code, while bringing no benefit over lateinit: access before setContentView would still lead to a crash. Thus, this pattern is undesirable and should be avoided.

Other classes that have findViewById

Besides Activity, there are also other classes that contain the findViewById method, like View and Dialog.

It is good to keep in mind that you can find an existing view only if it is contained in the object whose findViewById is called.

The View.findViewById can be useful if you are already holding a reference to some ViewGroup, like GridLayout or CardView, and wish to retrieve a view it contains.

Dialog.findViewById may be useful if you have a dialog with a custom XML layout.

Conclusion

In this topic, we have learned how to use the method findViewById to retrieve views declared in XML files in our Activities. We've learned that we should make sure our views exist before calling findViewById. We have also looked at several possible ways to declare local variables holding views and several ways to declare properties initialized with this method. We've discussed their pros and cons and pointed out most common practices.

Finally, we have also briefly noted that there are other classes besides Activity that also have a method called findViewById with the same general purpose of retrieving views.

30 learners liked this piece of theory. 3 didn't like it. What about you?
Report a typo