Pros/cons of Android view access strategies

I know this seems to keep coming up, but I’ve yet to come across a really thorough objective comparison of all the available ways of assigning Android views from inflated layouts. I’ve got a team that I’m just now starting to bring up to speed with Kotlin and I’d like to present a preferred approach before we go too far down one path, rather than ending up with several different approaches for the same thing in the same codebase. I’ll try to list the options I’ve considered and the pros/cons I’ve found so far. We do not currently use Butterknife or any other approach other than findViewById() in our Java code.

Most of the discussions I find seem to be mostly just personal preference and don’t really present much of a case for or against specific approaches, but there are certainly tradeoffs. Maybe the performance differences and object allocation differences between the approaches are so relatively minor that it really does come down to a matter of personal preference.

Nullable vars and findViewById()

private var textView: TextView? = null
…
textView = findViewById(R.id.text_view) as TextView

Pros:

  • probably the most straightforward for a developer coming from Java
    Cons:
  • view properties still need to be vars
  • usages all need to deal with null
  • findViewById() feels like it’s boilerplate
  • not very concise code - need to declare property in one place and initialize it elsewhere

lateinit vars and findViewById()

private lateinit var textView: TextView
…
textView = findViewById(R.id.text_view) as TextView

Pros:

  • still fairly understandable, no hidden code
  • no need for usages to deal with null - they can all assume non-null
    Cons:
  • view properties still need to be vars
  • still needs findViewById() which feels like boilerplate
  • not very concise code - need to declare property in one place and initialize it elsewhere

Delegates.notNull() and findViewById()

private var textView: TextView by Delegates.notNull()
…
textView = findViewById(R.id.text_view) as TextView

Pros:

  • no need for usages to deal with null - they can all assume non-null
    Cons:
  • view properties still need to be vars
  • still needs findViewById()
  • not very concise code - need to declare property in one place and initialize it elsewhere
  • extra objects and indirection needed for the delegates

Lazy properties that use findViewById()

private val textView: TextView by lazy { findViewById(R.id.text_view) }

Pros:

  • view properties can be val rather than var
  • the findViewById() calls are not executed until the view is referenced
    Cons:
  • thread safety overhead unless using lazy(LazyThreadSafetyMode.NONE)
  • unable to clear cached views in a Fragment’s onDestroyView()
  • extra objects and indirection needed for the delegates

Butterknife with lateinit vars

@BindView(R2.id.text_view)
internal lateinit var textView: TextView

Pros:

  • the generated code should be efficient
  • has the ability to clear cached views in a Fragment’s onDestroyView()
    Cons:
  • view properties still need to be vars
  • view properties can’t be private
  • remembering to use R2 rather than R in library modules is mildly frustrating

Kotterknife

private val textView: TextView by bindView(R.id.text_view)

Pros:

  • view properties can be val rather than var
  • view properties can be private
  • has the ability to clear cached views in a Fragment’s onDestroyView() (if you apply one of the patches that adds this support)
  • avoids some of the thread safety overhead in comparison with lazy properties
    Cons:
  • extra objects and indirection needed for the delegates

Kotlin Android Extensions

(no code sample, just get the correct imports added and start using the synthetic generated properties)

Pros:

  • has the ability to clear cached views in a Fragment’s onDestroyView(), and even does so automatically
  • don’t need member properties (either val or var) for the views
  • first-party (Kotlin/JetBrains) support
    Cons:
  • seems a little too magic for some people (I can’t really figure out why)

Android data binding

binding = FragmentLayoutBinding.inflate(inflater, container, false)
...
binding.textView.text = "Hello"

Pros:

  • don’t need member properties (either val or var) for the views
  • first-party (Google) support
    Cons:
  • adds additional dependencies to the project
  • seems like maybe overkill if not using any other features of data binding
4 Likes

Personally my preferences are for data binding if appropriate and lateinit vars and findViewById otherwise. I don’t really like as matter of taste the automagic of the kotlin android extensions or the use of delegates (which will be separate class instances).
In general I like databinding also as a way to handle the connection between data and it’s display. I like it being quite efficient under water (faster binding views than multiple findViewById). I would never want the view value to be nullable, so lateinit is a good solution.

I don’t really like the notNull delegate as it is impossible to interrogate it’s state so it’s not usable for lazy initialization based on passed in parameters (where lazy doesn’t work either).

My team has been using kotlin_android_extensions for most cases: though “magical” looking, the IDE provides links to the layout targets so it’s clear what you’re referencing. For the cases we can’t use extensions (i.e., layout nodes that are present only in tablet or phone variants of the referenced layout) we are using the lateinit var with findViewById() approach. We also use lateinit var for other view-related tools that can’t be constructed without context.

This approach is working well for us so far. Perhaps later we’ll experiment with Butterknife or Anko, but so far there hasn’t been much of an “itch to scratch.”

Another alternative is to implement your UI in Anko and then you can simply declare properties in the Anko view with no findViewById involved.

A reason I prefer Android Data Binding library vs Kotlin Android Extensions is ironically the better Null Safety the Data Binding API provides.

Kotlin Android Extensions’ synthetic view properties allows for null access before view inflation whereas Data Binding library exposes view properties via the Generated Binding object which is returned from the view inflation itself. Of course if you store the Generated Binding object as a lateinit property (not a Custom View) you have the same problem but at least the problem is constrained to a single property as opposed to multiple view properties.

2 Likes

This is a great write-up, thanks for sharing!

I’d like to add two more strategies, for completeness, perhaps to be used only as a starting point/baseline to evaluate all others.

Strategy #0 is to call findViewById and cast the result with either as? or as anywhere you need to access a view. Obviously, this results in such hard-to-read code that I don’t even see the point in trying to describe its benefits. I’d call this “the naive strategy.”

Strategy #1 would be either

private val textView: TextView?
    get() = findViewById(R.id.text_view) as? TextView

or

private val textView: TextView
    get() = findViewById(R.id.text_view) as TextView

depending on whether or not you would like to crash when findByView returns null. These days, I usually prefer not to crash, and I don’t mind to type textView? instead of textView when I need to reference the text view, so I’d prefer the former – but I am not judging anyone using the latter.

This is a clear improvement over the naive strategy, as it improves code clarity without introducing any drawbacks (that I am aware of…). I’d call this “the baseline strategy”: any other proposal would have to demonstrate how they are better than this.

The obvious (?) problem (?) with the baseline strategy is that it (just like the naive strategy) calls findViewById every time you access the view. I am not convinced at all that this is an actual problem in general (ListView-like use cases are obviously an exception), and I’ve come to regard any attempt to improve on the baseline strategy by way of caching a case of premature optimization.

Case in point, as soon as I learned about the by lazy approach, I made an effort to migrate all of my get() = findViewById code to by lazy – and then I promptly ran into the “unable to clear cached views” problem with FragmentPagerAdapter. One can be excused for thinking at this point “of course this elegant solution is undermined by fragments, fragments suck!”, but if we want to be honest with ourselves then I think we ought to ask: what exactly were we going to gain by caching the views in the first place?

1 Like

@dpisoni I am very much curious, why is that you can’t use extensions in this case? can’t we just use something like view?.{}

Sorry, I don’t mean can’t use kotlin class extensions, I mean can’t use kotlin-android-extensions. And the cases we can’t use kotlin-android-extensions safely are when we have a view ID that appears in a layout but not in its alternatives (e.g., phone/tablet layouts)

@dpisoni thanks for the response but I was actually asking about specifically kotlin-android-extensions. Couldn’t we just access the view that might not exist in the same layout for different device size using text_view.?let {} or simply text_view?. (Execute if not null)

Good question. I’m not sure any more. When I first was using kotlin-android-extensions, I found all sorts of nullability issues… the extension symbols were of a NotNull type, so if the id was missing it would throw an exception since the state was inconsistent (compiled code says not nullable type, value is null.) It didn’t make sense to use ?. since it was not a nullable type. Perhaps this has been fixed now, it’s been over a year since I ran in to this issue first.

@dpisoni ah that’s a valid point. there is no way to know why ? is being used for a specific view, unless of course, we look at the layout files and realize that some views don’t exist in all of the configurations. Something to keep in mind I suppose. Thanks david, appreciate the responses

In contrast the Android Data Binding library’s Generate Binding does appear to provide meaningful compile time contract with respect to nullable and non-null views where Views that are in all layout configurations are annotated with NonNull return while Views that are not in all layout configurations are annotated with Nullable return value.