From Punched Cards to Prompts
AndroidIntroduction When computer programming was young, code was punched into cards. That is, holes were punched into a piece of cardboard in a format...
You start the development sprint full of energy, but the ancient curse of Java bogs you down and you realize you are in for a marathon.
“Is it safe?” the massive code base keeps asking you.
“Is it safe?” forcing you to check whether your variables are null.
“Is it safe?” the sadistic voice is relentless.
“It’s so safe you won’t believe it!” you utter, but you are not sure anymore.
Java does not protect you from the “billion dollar mistake” – the null pointer is lurking everywhere. Every reference can potentially be null. How can anyone be safe in Java?
Many Android developers find refuge in Kotlin, a modern programming language that eliminates a lot of pain and suffering. Less boilerplate, more expressive code and, yes, it is null safe – if you choose the path of avoiding torture.
Still, Kotlin has to live in the world where Java was king, and on Android the Activity lifecycle further complicates things. Consider, for example, storing a reference to a view in a property. This is a common practice because Android developers try to avoid repeated calls to findViewById
.
Ideally, an object’s properties would all be defined at the time it is created, but, because for Activities and Fragments object creation is separate from view loading, the properties intended to store views must start out uninitialized.
This post explores several approaches to handling properties that reference views:
lateinit
by lazy
The simplest way to reference a view in a property is to use a nullable type.
var showAnswerButton: Button? = null
Since all variables must be initialized, null
is assigned to showAnswerButton
. Later, in Activity.onCreate
(or Fragment.onCreateView
), it will be assigned again, this time the value that we actually want it to have.
showAnswerButton = findViewById(R.id.showAnswerButton)
When using a nullable type, the ?.
or !!
operators have to be used to access the nullable variable. Using ?.
avoids a crash by returning null
should showAnswerButton
be null
for some reason.
showAnswerButton?.setOnClickListener { /* */ }
This is equivalent to the following code in Java:
if (showAnswerButton != null) {
showAnswerButton.setOnClickListener(/* */);
}
The !!
operator would cause a crash in the following code if showAnswerButton
were null:
showAnswerButton!!.setOnClickListener { /* */ }
Using these operators at least make it obvious that showAnswerButton
is nullable. This is not as easy to spot in Java:
showAnswerButton.setOnClickListener(/* */);
There is a better alternative: lateinit
.
lateinit var questionTextView: TextView
Using lateinit
, the initial value does not need to be assigned. Furthermore, at the use sites the questionTextView
is not a nullable type, so ?.
and !!
are not used. However, we have to be careful to assign our lateinit var
a value before we use it. Otherwise, a lateinit
property acts as if we performed !!
: it will crash the app on a null
value.
You could also create a property with a custom getter:
val anotherTextView: TextView
get() = findViewById(R.id.another_text_view)
This approach has a big drawback, each time the property is accessed, the findViewById
method is called. Furthermore, for Fragments, you have to use the !!
operator on the view
property. Bang-bang – any illusion of null safety is gone.
A property defined via by lazy
is initialized using the supplied lambda upon first use, unless a value had been previously assigned.
val nameTextView by lazy { view!!.findViewById<TextView>(R.id.nameTextView) }
This approach will cause a crash if nameTextView
is accessed before setContentView
in an Activity. It is even trickier in Fragments, because this code would cause a crash inside onCreateView
even after the view is inflated.
That’s because the view
property of the Fragment is not set until after onCreateView
completes, and it is referenced in the lazy variable’s initializer. It is possible to use lazy properties in onViewCreated
.
Using a lazily initialized property on a retained fragment causes a memory leak, since the property holds a reference to the old view.
Unlike other languages, lazy
in Kotlin is not a language feature, but a property delegate implemented in the standard library. Thus, it is possible to draw on it as inspiration and perform a mind experiment. Would it be possible to resolve the memory leak and the lack of lifecycle-awareness of the by lazy
approach?
Android Architecture Components includes support for lifecycle awareness.
The cityTextView
property is defined using LifecycleAwareLazy
, which takes in a Lifecycle
instance and clears out the value when the ON_STOP
event occurs. This ensures that the value is initialized again.
val cityTextView by lifecycleAwareLazy(lifecycle) { view!!.findViewById<TextView>(R.id.cityTextView) }
This creates an interesting curiosity: while cityTextView
has been defined as a val
, by virtue of the property delegate it can still change, as if it were a var
. In fact, properties that have a delegate cannot even be declared with a var
.
The stateTextView
is defined using LifecycleAwareFindView
, which takes a Fragment that also happens to implement the LifecycleOwner
interface and the view ID.
val stateTextView: TextView by findView(this, R.id.stateTextView)
These two property delegates solve one problem, but not completely.
They still contain a memory leak.
Lazy is a good fit for properties that may or may not be accessed.
If we never access them, we avoid computing their initial value.
They may work for Activities, as long as they are not accessed before setContentView
is called. They are not a great fit for referencing views in a Fragment, because the common pattern of configuring views inside onCreateView
would cause a crash. They can be used if view configuration is done in onViewCreated
.
With Activities or Fragments, it makes more sense to use lateinit
for their properties, especially the ones referencing views. While we don’t control the lifecycle, we know when those properties will be properly initialized. The downside is we have to ensure they are initialized in the appropriate lifecycle methods.
LifecycleAwareLazy
and LifecycleAwareFindView
implementationslazy
gotchasIntroduction When computer programming was young, code was punched into cards. That is, holes were punched into a piece of cardboard in a format...
Jetpack Compose is a declarative framework for building native Android UI recommended by Google. To simplify and accelerate UI development, the framework turns the...
Big Nerd Ranch is chock-full of incredibly talented people. Today, we’re starting a series, Tell Our BNR Story, where folks within our industry share...