Author: Antonio Leiva
Time: Aug 16, 2017
Links to the original text: https://antonioleiva.com/kotlin-android-extensions/
After the release of Kotlin 1.1.4, the original author decided to update and rewrite the articles for several months based on a series of new features of Kotlin and questions from readers about how to use Kotlin in Fragment and custom view.
In this rewritten article, he covers all the things that KAE (around version 1.1.4) can do. Now you will like to use them in any class (not just activity, fragment or view), including a new annotation to implement Parcelable.
You may be tired of using findViewById to restore Androidview day after day. Maybe you've given up and started using the famous Butterknife library. Then you will like Kotlin Android Extensions.
Kotlin Android Extensions: What is this?
Kotlin Android Extensions is a plug-in for Kotlin that is included in the normal plug-in, which allows viewing to be restored from Activitie s, Fragment s and View s in an amazing seamless manner.
The plug-in will generate additional code that allows you to access the View of the layout XML, just as they are attributes defined in the layout, you can use the name of id.
It also builds a local view cache. So when you first use an attribute, it makes a normal findViewById. Next, View is restored from the cache, so access is faster.
How to use them
Let's see how easy it is to use. I'll take an Activity as the first example:
Integrate Kotlin Android Extensions into our code
Although this plug-in is integrated into a common plug-in (you don't need to install a new plug-in), if you want to use it, you have to add additional applications to the Android module:
1 apply plugin: 'com.android.application' 2 apply plugin: 'kotlin-android' 3 apply plugin: 'kotlin-android-extensions'
All you need to do is start using it.
Restoring View from XML
From this point on, restoring View is as simple as applying your View ID defined in XML directly to your Activity.
Suppose you have an XML like this:
1 <?xml version="1.0" encoding="utf-8"?> 2 <FrameLayout 3 xmlns:android="http://schemas.android.com/apk/res/android" 4 android:layout_width="match_parent" 5 android:layout_height="match_parent"> 6 7 <TextView 8 android:id="@+id/welcomeMessage" 9 android:layout_width="wrap_content" 10 android:layout_height="wrap_content" 11 android:layout_gravity="center" 12 android:text="Hello World!"/> 13 14 </FrameLayout>
As you can see, TextView has a welcomeMessage ID.
In your MainActivity, you just need to write as follows:
1 override fun onCreate(savedInstanceState: Bundle?) { 2 super.onCreate(savedInstanceState) 3 setContentView(R.layout.activity_main) 4 5 welcomeMessage.text = "Hello Kotlin!" 6 }
In order to be able to use it, you need the following import, and IDE can automatically join it. Isn't it easy?
1 import kotlinx.android.synthetic.main.activity_main.*
As mentioned above, the generated code will include the View cache, so if you ask the view again, you don't need another findViewById.
Let's see what's behind it.
The Magic Behind Kotlin Android Extensions
When you start using Kotlin, it's interesting to understand the bytecode generated when you use the features. It will also help you understand what lies behind your decision. Hidden costs.
Under Tools - > Kotlin, there is a powerful function called Show Kotlin Bytecode. If you click on it, you will see the bytecode generated when you compile the open class file.
For most people, bytecode is not very useful, but there is another option: Decompile.
This will display the Java representation of the bytecode generated by Kotlin. So you can learn more or less what Kotlin code you write corresponds to Java code.
Use it in my generated Activity and see the code generated by Kotlin Android Extensions.
Interestingly, this one:
1 private HashMap _$_findViewCache; 2 ... 3 public View _$_findCachedViewById(int var1) { 4 if(this._$_findViewCache == null) { 5 this._$_findViewCache = new HashMap(); 6 } 7 8 View var2 = (View)this._$_findViewCache.get(Integer.valueOf(var1)); 9 if(var2 == null) { 10 var2 = this.findViewById(var1); 11 this._$_findViewCache.put(Integer.valueOf(var1), var2); 12 } 13 14 return var2; 15 } 16 17 public void _$_clearFindViewByIdCache() { 18 if(this._$_findViewCache != null) { 19 this._$_findViewCache.clear(); 20 } 21 22 }
This is the View Cache we are talking about.
When you request a view, you first try to find it in the cache. If it does not exist, it finds it and adds it to the cache. It's simple.
In addition, it adds a caching cleanup function: clearFindViewByIdCache. If you have to rebuild the view, because the old view will no longer be valid, you can use it.
So in this line:
1 welcomeMessage.text = "Hello Kotlin!"
Then it is converted to the following: uuuuuuuuuuu
1 ((TextView)this._$_findCachedViewById(id.welcomeMessage)).setText((CharSequence)"Hello Kotlin!");
Therefore, attributes are not real, and plug-ins do not generate attributes for each view. During compilation, the view cache can be accessed by simply replacing the code, converting it to the correct type and calling the method.
Kotlin Android Extensions of Fragment
This plug-in can also be used for Fragment.
The problem with Fragment is that View can be recreated, while Fragment instances remain valid. What will happen? This means that the views in the cache will no longer be valid.
If we move View to Fragment, let's look at the generated code. This is how I created this simple Fragment, which uses the same XML as the one written above:
1 class Fragment : Fragment() { 2 3 override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 4 return inflater.inflate(R.layout.fragment, container, false) 5 } 6 7 override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { 8 super.onViewCreated(view, savedInstanceState) 9 welcomeMessage.text = "Hello Kotlin!" 10 } 11 }
In onView Created, I changed the text of TextView again. What kind of bytecode is generated?
Everything is the same as in Activity, with only one small difference:
1 // $FF: synthetic method 2 public void onDestroyView() { 3 super.onDestroyView(); 4 this._$_clearFindViewByIdCache(); 5 }
When View is destroyed, this method will call clearFindViewByIdCache, so we are safe!
Kotlin Android extensions for custom View
In a custom View, Kotlin Android extensions are very similar. Suppose we have a View like this:
1 <merge xmlns:android="http://schemas.android.com/apk/res/android" 2 android:orientation="vertical" 3 android:layout_width="match_parent" 4 android:layout_height="match_parent"> 5 6 <ImageView 7 android:id="@+id/itemImage" 8 android:layout_width="match_parent" 9 android:layout_height="200dp"/> 10 11 <TextView 12 android:id="@+id/itemTitle" 13 android:layout_width="match_parent" 14 android:layout_height="wrap_content"/> 15 16 </merge>
I create a very simple custom view and use the new Intent annotated by @JvmOverloads to generate constructors:
1 class CustomView @JvmOverloads constructor( 2 context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 3 ) : LinearLayout(context, attrs, defStyleAttr) { 4 5 init { 6 LayoutInflater.from(context).inflate(R.layout.view_custom, this, true) 7 itemTitle.text = "Hello Kotlin!" 8 } 9 }
In the example above, I changed the text to itemTitle. The generated code should try to find View from the cache. It's no longer meaningful to duplicate all the same code again, but you can see this in the lines that change the text:
1 ((TextView)this._$_findCachedViewById(id.itemTitle)).setText((CharSequence)"Hello Kotlin!");
That is great! In the custom View, we only call findViewById for the first time.
Restore View from another View
The last option Kotlin Android Extensions offers is to use attributes directly from another view.
I use a layout very similar to that in the previous section. Suppose this is an inflate of instances in an Adapter.
With this plug-in, you can also access subview s directly:
1 val itemView = ... 2 itemView.itemImage.setImageResource(R.mipmap.ic_launcher) 3 itemView.itemTitle.text = "My Text"
Although this plug-in will help you fill in import, it is a little different in this respect:
1 import kotlinx.android.synthetic.main.view_item.view.*
Here are a few things you need to know:
- At compile time, you can reference any View from any other View. This means that you can refer to a View that is not a direct child of it. However, when an attempt is made to restore a View that does not exist, execution fails.
- In this case, views are not cached like Activities and Fragment s.
Why is that? Contrary to the previous situation, there is no place for the plug-in to generate the required code for the cache.
If you look at the code again, it is generated by the plug-in when calling attributes from the view, you will see:
1 ((TextView)itemView.findViewById(id.itemTitle)).setText((CharSequence)"My Text");
As you can see, no caching is invoked. If your view is complex and you are using it in an adapter, please note that it may affect performance.
Or you can choose: Kotlin 1.1.4
Kotlin Android Extensions in Version 1.1.4
In Kotlin's new version, Android Extensions has introduced some new interesting features: caching in any class (including ViewHolder interestingly) and a new @Parcelize annotation. There is also a way to customize the generated cache.
Later, we'll see them, but you need to know that these features are not final versions, so you need to add them to build.gradle to start them:
1 androidExtensions { 2 experimental = true 3 }
Use it in ViewHolder (or any custom class)
Now, you can build caches in any class in a simple way. The only requirement is that your class implement the LayoutContainer interface. The interface will provide a view, which is a sub-view looked up by the plug-in. Suppose we have a ViewHolder that maintains the layout view described in the previous example. You just need to do:
1 class ViewHolder(override val containerView: View) : RecyclerView.ViewHolder(containerView), 2 LayoutContainer { 3 4 fun bind(title: String) { 5 itemTitle.text = "Hello Kotlin!" 6 } 7 }
containerView is covered by us from the LayoutContainer interface. But that's what you need.
Then you can access the view directly without using the pre-itemView to access the sub-view.
In addition, if you examine the generated code, you will see that it retrieves views from the cache:
1 ((TextView)this._$_findCachedViewById(id.itemTitle)).setText((CharSequence)"Hello Kotlin!");
Here, I've used it on ViewHolder, but as you can see, it's generic and can be used in any class.
Implementation of Parcelable by Kotlin Android Extension
With the new @Parcelize annotation, you can implement any class in a very simple way Parcelable.
All you need to do is add comments, and the plug-in will do all the complicated work:
1 @Parcelize 2 class Model(val title: String, val amount: Int) : Parcelable
Then, as you know, you can add objects to any intent:
1 val intent = Intent(this, DetailActivity::class.java) 2 intent.putExtra(DetailActivity.EXTRA, model) 3 startActivity(intent)
And at any location (in this case, on the target Activity), restore objects from intent:
1 val model: Model = intent.getParcelableExtra(EXTRA)
Custom build cache
The new feature included in this set of experiments is a new annotation @ContainerOptions. This allows you to build caches in a customized way, or even prevent classes from creating them.
By default, it will use Hashmap, as we saw earlier. However, this can be changed using the SparseArray of the Android framework, which may be more efficient in some cases. If for some reason you don't need a class cache, you can also use this option.
This is its use:
1 @ContainerOptions(CacheImplementation.SPARSE_ARRAY) 2 class MainActivity : AppCompatActivity() { 3 ... 4 }
At present, the existing options are:
1 public enum class CacheImplementation { 2 SPARSE_ARRAY, 3 HASH_MAP, 4 NO_CACHE; 5 6 ... 7 }
conclusion
You've seen how easy Kotlin can handle Android views. With a simple plug-in, we can discard all the terrible code that is restored from the view after inflation. This plug-in will create the required properties for us without any problems.
In addition, Kotlin 1.1.4 adds some interesting features that are very useful when previous plug-ins are not covered.