Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4398

Kotlin Android Extensions

$
0
0
Featured Image

Android Kotlin Extensions

The Kotlin programming language has improved the Android development experience in many ways, especially since the announcement in 2017 of first-party support for the language. Null safety, extensions, lambdas, the powerful Kotlin standard library and many other features all add up to a better and more enjoyable way to make Android apps.

In addition to the Kotlin language, JetBrains also has developed a plugin for Android called the Kotlin Android Extensions, which was created to further ease everyday Android development. The extensions are a Kotlin plugin that every Android developer should be aware of.

In this tutorial, you’ll explore many of the features that come with the extensions, including the following main features:

View binding

View binding enables you to refer to views just like any other variable, by using synthethic properties, without the need to initialize view references by calling the ubiquitous findViewById().

Note: If you have used Butter Knife in the past, you’ll know that it also helps with view binding. However, the binding is done in a different way. Butter Knife requires you to write a variable and annotate it with the identifier of the corresponding view. Also, inside methods like Activity onCreate(), you need to call a method that will bind all those annotated variables at once. Using the Kotlin Android Extensions, you don’t need to annotate any variable. The binding is done the first time you need to access the corresponding view and saved into a cache. More on this later.

LayoutContainer

The LayoutContainer interface allows you to use view binding with views such as the ViewHolder for a RecyclerView.

Parcelize annotation

The @Parcelize annotation saves you from having to write all the boilerplate code related to implementing the Parcelable interface.

Note: This tutorial assumes you have previous experience with developing for Android in Kotlin. If you are unfamiliar with the language, have a look at this tutorial. If you’re beginning with Android, check out some of our Getting Started and other Android tutorials.

Getting started

The project you’ll be working with, YANA (Yet Another Notepad App), is an app to write notes. Use the Download Materials button at the top or bottom of this tutorial to download the starter project.

Once downloaded, open the starter project in Android Studio 3.0.1 or greater, and give it a build and run. Tap the floating action button to add a note, and tap the back button to save a note into your list.

app no notes app note app single note
app urgent note app multiple notes

Taking a look at the project, you see that it consists of two activities:

  • NoteListActivity.kt: The main activity that lists all your existing notes and lets you create or edit one.
  • NoteDetailActivity.kt: This will show an existing note and also can handle a new note.

Setting up Kotlin Android Extensions

Open the build.gradle file for the app module and add the following, just below the ‘kotlin-android’ plugin:

apply plugin: 'kotlin-android-extensions'

That’s all the setup you need to use the plugin! Now you’re ready to start using the extensions.

Note: If you create a new Android app with Kotlin support from scratch, you’ll see that the plugin is already included in the app build.gradle file.

View binding

Open NoteListActivity, and remove the following lines from the onCreate method:

val noteListView: RecyclerView = findViewById(R.id.noteListView)
val addNoteView: View = findViewById(R.id.addNoteView)

Android Studio should notice that it’s missing imports for your two views. You can hit Option-Return on macOS or Alt-Enter on PC to pull in the imports. Make sure to choose the import that starts with kotlinx to use the extensions.

If you check the imports now at the top of the file, you should see the following:

import kotlinx.android.synthetic.main.activity_note_list.*

In order to generate the synthetic properties, to reference the views of the layout, you need to import kotlinx.android.synthetic.main.<layout>.*. In this case, the layout is activity_note_list. The asterisk wild-card at the end means that all possible views will be pulled in from the file. You can also import views individually if you wish, by replacing the asterisk with the view name.

Your `onCreate()` method should now look like this:


override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_note_list)
    
    noteRepository = ...
    
    adapter = ...
    
    // 1
    noteListView.adapter = adapter
    // 2
    addNoteView.setOnClickListener {
      addNote()
    }
  }
  1. To reference the list that shows notes, check the `id` in the activity_note_list.xml file, you’ll find that it’s noteListView. So, you access it using noteListView.
  2. The floating action button has the `id` addNoteView, so you reference it using addNoteView.

It’s important to note here that your synthetic view reference has the same name as the `id` you used in the layout file. Many teams have their own naming conventions on XML identifiers, so you may need to update your convention if you want to stick to camel case on the view references in your code.

Next, open NoteDetailActivity and remove the following view properties:

private lateinit var editNoteView: EditText
private lateinit var lowPriorityView: View
private lateinit var normalPriorityView: View
private lateinit var highPriorityView: View
private lateinit var urgentPriorityView: View
private lateinit var noteCardView: CardView

Also, remove the findViewById calls in onCreate().

The project will not compile at this point because all of those undeclared views. However, if you take a look into the view ids of activity_note_detail.xml and note_priorities_chooser_view.xml, you’ll notice they match the undeclared views you have in the activity. Add the following imports to the top of the file:

import kotlinx.android.synthetic.main.activity_note_detail.*
import kotlinx.android.synthetic.main.note_priorities_chooser_view.*

Now the project should compile again :]

Finally, do similar with NoteListAdapter, and remove the following in NoteViewHolder:

private val noteTextView: TextView = itemView.findViewById(R.id.noteTextView)
private val noteDateView: TextView = itemView.findViewById(R.id.noteDateView)
private val noteCardView: CardView = itemView.findViewById(R.id.noteCardView)

Add the following import to the file:

import kotlinx.android.synthetic.main.note_item.view.*

Note: in a view you have to import kotlinx.android.synthetic.main.<layout>.view.*

And then prepend each referenced view with itemView, like so:

fun bind(note: Note, listener: Listener) {
  itemView.noteTextView.text = note.text
  itemView.noteCardView.setCardBackgroundColor(
    ContextCompat.getColor(itemView.noteCardView.context, note.getPriorityColor()))
  itemView.noteCardView.setOnClickListener {
    listener.onNoteClick(itemView.noteCardView, note)
  }

  itemView.noteDateView.text = sdf.format(Date(note.lastModifed))
}

Build and run the app, and you’ll see that everything is working like before, and you’ve removed all the findViewById() boilerplate. :]

View binding under the hood

I bet you’re curious about how this “magic” works.

Curious about the magic

Fortunately, there is a tool to decompile the code!

Open NoteListActivity and go to Tools > Kotlin > Show Kotlin Bytecode and then press the Decompile button.

Decompile

You’ll find the following method, generated by the plugin:

public View _$_findCachedViewById(int var1) {
  if(this._$_findViewCache == null) {

    this._$_findViewCache = new HashMap();
  }

  View var2 = (View)this._$_findViewCache.get(Integer.valueOf(var1));
  if(var2 == null) {
    var2 = this.findViewById(var1);
    this._$_findViewCache.put(Integer.valueOf(var1), var2);
  }

  return var2;
}

Now check that this method is called whenever you reference a view by right-clicking on it and selecting Find Usages. One example usage is:

RecyclerView var10000 = (RecyclerView)this._$_findCachedViewById(id.noteListView);

_$_findCachedViewById() creates a view cache HashMap, tries to find the cached view, and, if it doesn’t find it, then calls good old findViewById() and saves it to the cache map.

Pretty cool right? :]

Note: You’ll see that a _$_clearFindViewByIdCache was also generated, but the Activity doesn’t call it. This method is only needed when using Fragments, as the Fragment’s onDestroyView() calls it.

Check what the plugin does with the adapter. Open NoteListAdapter and decompile it.

To your surprise, you won’t find the _$_findCachedViewById method. Instead, you’ll find that each time that the bind() method is called, findViewById() is called. This leads to a performance problem (because it will always have to find the views through the hierarchy), the exact problem that a ViewHolder should solve. So, this is not following the ViewHolder pattern!

To avoid this, you could workaround with the following approach:

class NoteViewHolder(itemView: View)
  : RecyclerView.ViewHolder(itemView) {

  private val noteTextView = itemView.noteTextView
  private val noteCardView = itemView.noteCardView
  private val noteDateView = itemView.noteDateView

  private val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault())

  fun bind(note: Note, listener: Listener) {
    noteTextView.text = note.text
    noteCardView.setCardBackgroundColor(
        ContextCompat.getColor(noteCardView.context, note.getPriorityColor()))
    noteCardView.setOnClickListener {
      listener.onNoteClick(noteCardView, note)
    }
    
    noteDateView.text = sdf.format(Date(note.lastModifed))
  }
}

Now, if you decompile, you’ll see that findViewById() is only called when the NoteViewHolder is created, so you’re safe again!

However, there is another approach. You can use the LayoutContainer interface, which will be covered in the following section.

Experimental features

Certain features of the Kotlin Android Extensions have not yet been deemed production ready, and are considered experimental features. These include the LayoutContainer interface and the @Parcelize annotation.

To enable the experimental features, open the app module build.gradle file again and add the following, just below the ‘kotlin-android-extensions’ plugin:

androidExtensions {
  experimental = true
}

LayoutContainer

As you’ve seen, it’s easy to access views with synthethic properties by using the corresponding kotlinx imports. This applies to both activities and fragments.

But, in the case of a ViewHolder (or any class that has a container view), you can implement the LayoutContainer interface to avoid workarounds like the one you used before.

Open again NoteListAdapter and implement the LayoutContainer interface.

// 1
import kotlinx.android.extensions.LayoutContainer
// 2
import kotlinx.android.synthetic.main.note_item.*

...

// 3
class NoteViewHolder(override val containerView: View)
  : RecyclerView.ViewHolder(containerView), LayoutContainer {

  private val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault())

  fun bind(note: Note, listener: Listener) {
    // 4
    noteTextView.text = note.text
    noteCardView.setCardBackgroundColor(
        ContextCompat.getColor(noteCardView.context, note.getPriorityColor()))
    noteCardView.setOnClickListener {
      listener.onNoteClick(noteCardView, note)
    }
    
    noteDateView.text = sdf.format(Date(note.lastModifed))
  }
}
  • Import the LayoutContainer interface.
  • To reference the views of the note_item.xml layout using LayoutContainer you need to import kotlinx.android.synthetic.main.note_item.*
  • Add the interface to NoteViewHolder. To comply with it, you provide a containerView property override in the primary constructor, which then gets passed along to the superclass.
  • Finally, use the properties that reference the views of the layout.

If you decompile this code, you’ll see that it uses _$_findCachedViewById() to access the views.

Build and run the app to see the app working just like before, this time with LayoutContainer.

View caching strategy

You’ve seen that _$_findCachedViewById uses a HashMap by default. The map uses an integer for the key and a view object for the value. You could use a SparseArray for the storage instead.

If you prefer using a SparseArray, you can annotate the Activity/Fragment/ViewHolder with:

@ContainerOptions(cache = CacheImplementation.SPARSE_ARRAY)

If you want to disable the cache, the annotation is:

@ContainerOptions(cache = CacheImplementation.NO_CACHE)

It’s also possible to set a module-level caching strategy by setting the defaultCacheImplementation value in the androidExtensions in the build.gradle file.

@Parcelize

Implementing the Parcelable interface on a custom class allows you to add instances of the class to a parcel, for example, adding them into a Bundle to pass between Android components. There is a fair amount of boilerplate needed to implement Parcelable. Libraries like AutoValue have been created to help with that boilerplate.

The Kotlin Android Extensions have their own way to help you implement Parcelable, using the @Parcelize annotation.

Open the Note class and modify it to the following:

@Parcelize
data class Note(var text: String,

                var priority: Int = 0,
                var lastModifed: Long = Date().time,
                val id: String = UUID.randomUUID().toString()) :
    Parcelable

You’ve removed literally all the code in the body of the class, and replaced it with the single annotation.

Note: Android Studio may highlight the class thinking there’s a compile error. But this is a known bug, and here is the issue. Fear not, you can build and run the project without any problems.

Build and run the app, and all is working as before. Implementing Parcelable is just that simple! Imagine how much time this will save you. :]

Basic Happy

Where To Go From Here?

Congratulations! You’ve just learned the Kotlin Android Extensions, and seen how they let you remove a ton of boilerplate code from your project.

You can download the final version of the project using the Download Materials button at the top or bottom of this tutorial.

Here are some great references to learn more about the development of Kotlin Android Extensions:

Finally, as a separate project from Kotlin Android Extensions, Google has released Android KTX. KTX is not a plugin, but instead another set of extensions to ease Android development. KTX simplifies working with strings, SharedPreferences, and other parts of Android.

Feel free to share your feedback, findings or ask any questions in the comments below or in the forums. I hope you enjoyed this tutorial on the Kotlin Android Extenstions!

The post Kotlin Android Extensions appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4398

Trending Articles