ASCE1885 is the author of advanced Android. Small tight circle: Android advanced advanced, see this article

The original link: https://savvyapps.com/blog/kotlin-tips-android-development.

Savvy Apps started using Kotlin in a new Android project in late 2016, just as Kotlin 1.0.4 was released. Initially, we had the opportunity to try Kotlin on a smaller project, but when we tried it, we found that it was easy to separate functionality from business logic using extension functions, and it saved us development time, so we felt it would be an advanced language selection. Since then, we have created several Android apps using Kotlin, as well as some internal Kotlin libraries.

To build on our Kotlin experience, we decided to put together some of the most useful and favorite development tips from all of Savvy’s Android development teams. Before reading these suggestions, you should check out the official Kotlin documentation 1 and explore the language for yourself at try.kotlinlang.org2. Since these tips are specifically for Kotlin’s development on Android, you should also have experience developing on the Android SDK. You should also be familiar with the Kotlin plugin provided by Kotlin’s creator JetBrains and using Kotlin3 in Android Studio.

Note that these suggestions are sorted based on how familiar you are with Kotlin, so you can easily skip the ones you don’t need to read depending on your skill level.

Primary Suggestions

Lazy loading

Lazy loading has several benefits. First, it speeds up the startup of the application because the load time is delayed until variables are accessed. This feature is especially useful for developing Android applications, as opposed to developing server-side applications using Kotlin. For Android apps, we want to reduce app startup time so that users can see the content of the app more quickly, rather than waiting for the launch page to load.

Second, such lazy loading also has higher memory efficiency because we only load resources into memory when it is called. On a mobile platform like Android, memory usage is very important. Because mobile resources are shared and limited. For example, if you’re creating a shopping application where users might just browse your items, you can lazily load the purchase related apis:

val purchasingApi: PurchasingApi by lazy {

val retrofit: Retrofit = Retrofit.Builder()

.baseUrl(API_URL)

.addConverterFactory(MoshiConverterFactory.create())

.build()

retrofit.create(PurchasingApi::class.java)

}

By using lazy loading like this, your app will not load PurchasingApi if the user does not intend a purchase to occur in the app at all, and thus does not consume resources that it might otherwise consume.

Lazy loading is also a good way to encapsulate initialization logic:

// bounds is created as soon as the first call to bounds is made

val bounds: RectF by lazy {

RectF(0f, 0f, width.toFloat(), height.toFloat())

}

Only when the bounds variable is first referenced will RectF be created using the current width and height values of the View, so we don’t have to explicitly create RectF first and then set it to the bounds.

Custom Getters/Setters

Kotlin’s custom getters and Setters use the structure of the Model class, but specify custom behavior to get and set field values. When using a custom Model class for some framework such as the Parse SDK, the values we retrieve are not stored in local variables of the class instance, but are stored and retrieved in some custom way, such as from JSON. By using custom getters and setters, we can simplify the definition of access methods:

@ParseClassName("Book")

class Book : ParseObject() {



// getString() and put() are methods that come from ParseObject

var name: String

get() = getString("name")

set(value) = put("name", value)



var author: String

get() = getString("author")

set(value) = put("author", value)

}

Accessing the fields defined above looks similar to the traditional way of accessing the Model class:

val book = api.getBook()

textAuthor.text = book.author

Now, if you need to change the data source for your Model class from the Parse SDK to another data source, you may only need to make one change in your code.

Lambdas

Lambdas support functional programming while reducing the total number of lines of code in source files. Although currently in Android development Using the Java language) you can use lambdas expressions, but you either need to introduce Retrolambda4 into your project or change the configuration at project build time, but Kotlin goes a step further and supports lambdas without these additional operations at all.

For example, when using the lambdas representation, onClickListener is used as follows:

button.setOnClickListener { view ->

startDetailActivity()

}

It can even support return values:

toolbar.setOnLongClickListener { 

showContextMenu()

true

}

The Android SDK has many scenarios where a listener is set up or a single method needs to be implemented, where the lambdas representation can be useful.

Data classes

Data classes simplify class definition by automatically adding equals(), hashCode(), copy(), and toString() methods to classes. It clearly defines the intent of the Model class and what it should contain, while separating pure data from business logic.

Let’s look at the definition of a data class with an example:

data class User(val name: String, val age: Int)

As simple as that, the class works without adding any more definitions. If you use the data class with Gson or a similar JSON parsing library, you can create the default build method using the default values as follows:

// Example with Gson's @SerializedName annotation

data class User(

@SerializedName("name") val name: String = "",

@SerializedName("age") val age: Int = 0

)

Set the filter

When we use apis, we often have to deal with collections. In most cases you want to filter or modify the contents of the collection. By using Kotlin’s collection filtering capabilities, we can make our code cleaner and more concise. We can more easily express what we want to include in the result list with the following collection filter code:

val users = api.getUsers()

// we only want to show the active users in one list

val activeUsersNames = items.filter {

it.active // the "it" variable is the parameter for single parameter lamdba functions

}

adapter.setUsers(activeUsers)

The ability to implement collection filtering using Kotlin’s built-in methods is very similar to that of other functional programming languages, such as Java 8’s Streams or Swift’s collection types. Being able to implement collection filtering in a uniform way helps us reach a better and faster consensus when discussing with team members, such as how to display the right elements in a list.

Object expression

Object expressions allow strict singleton definitions, so they are not a problem for a class that can be instantiated. Object expressions also ensure that you don’t need to store a singleton in a class like Application or as a static class variable.

For example, if I have a utility class that has a static thread-dependent method that I want to access anywhere in the application:

package com.savvyapps.example.util



import android.os.Handler

import android.os.Looper



// notice that this is object instead of class

object ThreadUtil {



fun onMainThread(runnable: Runnable) {

val mainHandler = Handler(Looper.getMainLooper())

mainHandler.post(runnable)

}

}

ThreadUtil can then be called as if it were a static class method:

ThreadUtil.onMainThread(runnable)

This means that you no longer need to declare private constructors or have to specify where to store static instances. Object expressions are essentially the first citizens of the Kotlin language. Similarly, we can use object expressions instead of anonymous inner classes, as follows:

viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {

override fun onPageScrollStateChanged(state: Int) {}



override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}



override fun onPageSelected(position: Int) {

bindUser(position)

}

});

Both methods are essentially the same; they create a singleton of a class by declaring an object.

The Companion object

At first glance, Kotlin seems to lack static variables and methods. In a sense, Kotlin doesn’t have these concepts, but it does have the concept of companion objects. These Companion objects are singleton objects within a class that contain methods and variables that you might want to access statically. You can define constants and methods in companion objects, similar to static variables and methods in Java. With this, you can adopt the newInstance pattern in fragments.

Let’s look at the simplest form of a Companion object:

class User {

companion object {

const val DEFAULT_USER_AGE = 30

}

}



// later, accessed like you would a static variable:

user.age = User.DEFAULT_USER_AGE

In Android, we typically use static methods and variables to create static factories for fragments and Activity Intents, as shown below:

class ViewUserActivity : AppCompatActivity() {



companion object {



const val KEY_USER = "user"



fun intent(context: Context, user: User): Intent {

val intent = Intent(context, ViewUserActivity::class.java)

intent.putExtra(KEY_USER, user)

return intent

}

}



override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

setContentView(R.layout.activity_cooking)



val user = intent.getParcelableExtra<User>(KEY_USER)

/ /...

}

}

The Intent is created in a manner similar to what you see in Java:

val intent = ViewUserActivity.intent(context, user)

startActivity(intent)

This pattern is good because it reduces the likelihood that the Intent or Fragment will lose the user’s information or whatever else it needs to display. Companion objects are a way of maintaining some form of static access in Kotlin, and should therefore be treated accordingly.

Global constants

Kotlin allows developers to define constants that can be accessed anywhere throughout the application. In general, constants should be scoped as little as possible, but this is a good way to do this when you need globally scoped constants. You don’t need to implement a constant class:

package com.savvyapps.example



import android.support.annotation.StringDef



// Note that this is not a class, or an object

const val PRESENTATION_MODE_PRESENTING = "presenting"

const val PRESENTATION_MODE_EDITING = "editing"

These global constants can be accessed anywhere in the project:

import com.savvyapps.example.PRESENTATION_MODE_EDITING



val currentPresentationMode = PRESENTATION_MODE_EDITING

Keep in mind that constants should be as scoped as possible to reduce code complexity. If you have a constant value that is only relevant to the User class, you should define that constant in the Companion object of the User class, rather than through global constants.

Optional Parameters

Optional arguments make method calls more flexible because we don’t have to pass null or default values. This is especially useful when defining animations.

For example, if you want to define a method for fade-out animation of views throughout your application, but you only need to specify the duration of the animation in special scenarios, you can define this method as follows:

fun View.fadeOut(duration: Long = 500): ViewPropertyAnimator {

return animate()

Alpha (0.0 f)

.setDuration(duration)

}

The usage method is as follows:

icon.fadeOut() // fade out with default time (500)

icon.fadeOut(1000) // fade out with custom time

Intermediate advice

extension

The beauty of extensions is that they allow you to add functionality to a class without having to inherit it. For example, have you ever wished your Activity had some method, such as hideKeyboard()? Using extensions, you can easily implement this functionality:

fun Activity.hideKeyboard(): Boolean {

val view = currentFocus

view? .let {

val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE)

as InputMethodManager

return inputMethodManager.hideSoftInputFromWindow(view.windowToken,

InputMethodManager.HIDE_NOT_ALWAYS)

}

return false

}

With extensions, you can easily eliminate the need to use utility classes or utility methods, and can really improve the readability of your code. We want to go a step further and use extensions to improve the organization of the code. For example, suppose you have a basic Model class, such as an article. This article might be thought of as a data class that fetches from an API data source:

class Article(val title: String, val numberOfViews: Int, val topic: String)

Suppose you want to calculate the relevance of articles to your users according to some formula. Should you put correlations directly in the data class as variables? One could argue that the Article model class should only hold data retrieved from the API, and that’s it. In this case, extensions can work for you again:

// In another Kotlin file, possibly named ArticleLogic.kt or something similar

fun Article.isArticleRelevant(user: User): Boolean {

return user.favoriteTopics.contains(topic)

}

At this point, the above code simply checks to see if the specified article is in the user’s list of favorite topics. Logically, however, this business logic can also be modified to check other attributes of the user. Because this check logic is separate from the Article Model class, you can modify it with confidence in the purpose of the method and its ability to change.

lateinit

One of Kotlin’s major features is its support for null-pointer safety. Lateinit provides an easy way to implement both null Pointers and variable initialization as required by the Android platform. This is a great language feature, but it still takes some getting used to if you’ve done Java development for a long time. In Kotlin, a field is either immediately assigned or declared nullable:

var total = 0 // declared right away, no possibility of null

var toolbar: Toolbar? = null // could be a toolbar, could be null

This language feature can be frustrating when dealing with Android layouts. As we know, views exist in an Activity or Fragment, and we cannot assign values to them when declared because they must be inflated after the layout is inflate in order to be assigned in onCreate/onCreateView. You can handle this type of problem by checking where your Activity needs a View, but from an empty check handling perspective, these operations are frustrating and unnecessary. Conversely, in Kotlin you can use the Lateinit modifier:

lateinit var toolbar: Toolbar

Developers can now leave the Toolbar unreferenced until it is initialized. This works great when used with a function library like Butter Knife5:

@BindView(R.id.toolbar) lateinit var toolbar: Toolbar



override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

setContentView(R.layout.activity_main)

ButterKnife.bind(this)

// you can now reference toolbar with no problems!

toolbar.setTitle("Hello There")

}

Safe type conversion

Some Android programming specifications require safe type conversions because ordinary type conversions can raise exceptions. For example, a typical way to create a Fragment in an Activity is to first check to see if the Fragment instance already exists using the FragmentManager. If it doesn’t exist, create it and add it to the current Activity. When you first see Kotlin’s cast, you might implement this feature like this:

var feedFragment: FeedFragment? = supportFragmentManager

.findFragmentByTag(TAG_FEED_FRAGMENT) as FeedFragment

This will actually cause a crash. When you use AS, it will attempt to cast the object, which in this case may be NUL, and may raise a null-pointer exception. You need to use as? To replace AS, which means to cast an object and return NULL if the cast fails. Therefore, proper initialization of fragments should look like this:

var feedFragment: FeedFragment? = supportFragmentManager

.findFragmentByTag(TAG_FEED_FRAGMENT) as? FeedFragment

if (feedFragment == null) {

feedFragment = FeedFragment.newInstance()

supportFragmentManager.beginTransaction()

.replace(R.id.root_fragment, feedFragment, TAG_FEED_FRAGMENT)

.commit()

}

Use the let

If the value of the let modified object is not null, the corresponding code block is allowed to execute. This allows developers to avoid null type checks, making the code more readable. For example, in Java, the code is as follows:

if (currentUser ! = null) {

text.setText(currentUser.name)

}

Let is used in Kotlin as follows:

user? .let {

println(it.name)

}

In this example, in addition to making the code more developer-friendly, let also automatically assigns a non-null variable IT to the User instance, which we can continue to use without worrying that it will be null.

isNullOrEmpty | isNullOrBlank

In the process of developing an Android application, we usually need to do a lot of validation. If you’ve ever dealt with this situation without Kotlin, you’ve probably discovered and used the TextUtils class in Android. The use of the TextUtils class looks like this:

if (TextUtils.isEmpty(name)) {

// alert the user!

}

In this example, you will see that if users set their username name to blank, it will pass the above test. IsNullOrEmpty and isNullOrBlank are built into the Kotlin language, eliminating the need to use Textutisl.isString (someString) while providing additional functionality to check for whitespace. You can use both methods when appropriate:

// If we do not care about the possibility of only spaces...

if (number.isNullOrEmpty()) {

// alert the user to fill in their number!

}



// when we need to block the user from inputting only spaces

if (name.isNullOrBlank()) {

// alert the user to fill in their name!

}

Field validation is a common condition for application registration. These built-in methods are great for checksums of fields to alert users to input errors. You can even use extension methods to implement some custom validations, such as validating email addresses:

fun TextInputLayout.isValidForEmail(): Boolean {

val input = editText? .text.toString()

if (input.isNullOrBlank()) {

error = resources.getString(R.string.required)

return false

} else if (emailPattern.matcher(input).matches()) {

error = resources.getString(R.string.invalid_email)

return false

} else {

error = null

return true

}

}

Senior advice

Avoid defining a unique abstract method for the Kotlin class

This tip allows you to use lambdas, which make your code clearer and more concise. For example, when writing code in Java, you often encounter writing listener classes like this:

public interface OnClickListener {



void onClick(View v);

}

One of Kotlin’s great features is that it performs SAM (Single Abstract Method6) transformations for Java classes. ClickListener is used in Java as follows:

textView.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View view) {

// do something

}

});

The Kotlin code can be simplified to:

textView.setOnClickListener { view ->

// do something

}

The same usage does not apply to the SAM interface created in Kotlin. This is determined by language design 7, which can be surprising and frustrating for Kotlin beginners. If the same listener interface were defined in Kotlin, it would look like this:

view.setOnClickListener(object : OnClickListener {

override fun onClick(v: View?) {

// do things

}

})

To reduce the amount of code, you might write a listener in a class like this:

private var onClickListener: ((View) -> Unit)? = null



fun setOnClickListener(listener: (view: View) -> Unit) {

onClickListener = listener

}



// later, to invoke

onClickListener? .invoke(this)

This will allow you to reuse the simple Lambda syntax of SAM’s automatic conversion exercises.

Use coroutines instead of AnsyncTask

Since AsyncTask is cumbersome and tends to leak memory, we prefer coroutines because they improve code readability without causing memory leaks. Learn the basic concepts and usage of coroutines in resource 8. Note that since coroutines are currently an experimental feature in Kotlin 1.1, we still recommend RxJava9 for most asynchronous scenarios.

At the time of writing, the use of coroutines requires the introduction of additional dependency libraries:

The compile "org. Jetbrains. Kotlinx: kotlinx coroutines - android: 0.13"Copy the code

Add the configuration to the gradle.properties file:

kotlin.coroutines=enableCopy the code

By using coroutines, you can write simple, inline asynchronous code and modify the UI directly.

fun bindUser() = launch(UI) {

// Call to API or some other things that takes time

val user = Api.getUser().execute()

// continue doing things with the ui

text.text = user.name

}

conclusion

This is a summary of some of the most useful tips we’ve gleaned since we started developing with Kotlin. A special thanks to my colleague Nathan Hillyer for his contributions. We hope these tips will give you a head start on developing Android with Kotlin. You can also check out Kotlin’s official documentation 10. Jake Wharton, an Android developer at Square, also provides some useful resources on Kotlin, including his talk 11 and Notes 12 on using Kotlin in Android development.

Expect more development suggestions as Kotlin evolves as a programming language.


https://kotlinlang.org/docs/reference/ ↩ [1] [2] https://try.kotlinlang.org/ ↩ [3] https://kotlinlang.org/docs/reference/using-gradle.html#targeting-android ↩ [4] https://github.com/orfjackal/retrolambda ↩ [5] https://github.com/JakeWharton/butterknife ↩ [6] https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions ↩ [7] https://youtrack.jetbrains.com/oauth?state=%2Fissue%2FKT-7770 ↩ [8] https://github.com/Kotlin/kotlinx.coroutines/blob/master/coroutines-guide.md ↩ ↩ [9] https://github.com/ReactiveX/RxJava https://kotlinlang.org/docs/reference/ ↩ [10] [11] https://www.youtube.com/watch?v=A2LukgT2mKc ↩ [12] https://docs.google.com/document/d/1ReS3ep-hjxWA8kZi0YqDbEhCqTt29hG8P44aA9W0DM8/preview ↩