Lessons learned while converting to Kotlin with Android Studio

Lessons learned while converting to Kotlin with Android Studio

Advancing your Kotlin conversion with Android Studio

I was excited to finally try the popular language Kotlin while converting a simple app from Java using the Convert Java File to Kotlin tool in Android Studio. I gave it try, and this is my story of converting this project.

I quickly found out that the tool in Android Studio converts a majority of my Java classes perfectly. There were a few places that needed to be cleaned up and I learned a few new keywords from the conversion!

I will share these insights with you:

  • ints became longs
  • lateinit keyword
  • internal keyword
  • Companion Object
  • Cleaning up ranges
  • Lazy loading
  • Destructuring

Before we get started, if you ever question what actually happens underneath the covers in your Kotlin code, you can examine it in Android Studio by going to Tools → Kotlin → Show Kotlin Bytecode.

Longing for longs

I almost missed the first change because it was so small. The converter magically changed a long constant in one of my classes to an int and cast it back to a long every time it was used. Yikes!

companion object {
    private val TIMER_DELAY = 3000
}
//...
handler.postDelayed({
    //...
}, TIMER_DELAY.toLong())

The good news? It recognized the constant by using the val keyword.

The bad news? There was unnecessary casting throughout my activity where I was expecting Kotlin’s type safety to be more advanced than it is.

I thought Kotlin’s type safety was smarter than this. Maybe the converter isn’t smart enough yet?

Fixing this was simple; I just needed to add an “L” to the end of the variable declaration (similar to Java).

companion object {
    private val TIMER_DELAY = 3000L
}
//...
handler.postDelayed({
    //...
}, TIMER_DELAY)

Better Late than Never

One of Kotlin’s biggest features, null safety, aims to eliminate the danger of null references. This is done by a type system that distinguishes between references that can hold null (nullable references) and those that can not (non-null references).

Most the time you want your references to be non-null, so you don’t run into NPE (Null Pointer Exceptions).

However, in some cases, a null reference can be helpful, for example, initializing variables from an onClick() event such as an AsyncTask. There are several ways you can deal with null references:

  1. Good old if statements checking for null reference before accessing a property (you should be used to those from writing Java).
  2. A cool Safe Call Operator (syntax ?.) which performs a null check in the background for you. If the object is a null reference, it returns a null (not a null pointer exception). No more of those pesky if statements.
  3. Force the return of a NullPointerException using the the !! operator. So you are basically writing familiar Java code and need to do step 1.

Knowing exactly which pattern to enforce null-safety is not trivial, so the converter defaults to the simplest solution (option 3) and lets the developer handle the null-safety the best way for their use case.

I realized letting my Kotlin code throw a null pointer exception is counter-intuitive to the benefits of the language, so I delved deeper to see if there was something even better than what I had already found.

I discovered the powerful keyword lateinit. Using lateinit, Kotlin allows you to initialize non-null properties after the constructor has been called, so I am moving away from null properties all together.

That means I get the benefits of option 2 above (?. syntax) without having to write the extra “?.”. I just handle methods as if they are never null with no boilerplate checks and the same syntax I am used to using.

Using lateinit is an easy way to remove the !! operators from your Kotlin code. For more tips on how to remove the !! operator and cleanup your code, check out David Vávra’s post.

The Internals of internal

Since I was converting one class at a time, I was interested in how the converted Kotlin classes would work with remaining Java classes.

I read Kotlin has complete Java interoperability, so I would expect it to work with no visible change.

There was a public method within a fragment that converted into an internal function in Kotlin. In Java, that method didn’t have any access modifiers, so it was package private.

public class ErrorFragment extends Fragment {
    void setErrorContent() {
        //...
    }
}

The converter recognized there were no access modifiers and thought it only should be visible within the module/package by applying the internal keyword for access visibility.

class ErrorFragment : Fragment() {
    internal fun setErrorContent() {
        //...
    }
}

What does this new keyword mean? Looking into the decompiled bytecode, we quickly see that the method name changes from setErrorContent() to setErrorContent$production_sources_for_module_app().

public final void setErrorContent$production_sources_for_module_app() {
   //...
}

The good news is that in other Kotlin classes, we only need to know the original method name.

mErrorFragment.setErrorContent()

Kotlin will translate to the generated name under the covers for us. Looking again at the decompiled code we see the translation.

// Accesses the ErrorFragment instance and invokes the actual method
ErrorActivity.access$getMErrorFragment$p(ErrorActivity.this)
    .setErrorContent$production_sources_for_module_app();

The change in method name is handled by Kotlin for us but what about other Java classes?

From a Java class, you can’t call errorFragment.setErrorContent(), since that “internal” method does not really exist (method name has changed).

The setErrorContent() method is no longer visible to Java classes as we can see from the api in the intellisense box from Android Studio, so you have to use the generated (clunky) method name.

Even though Kotlin and Java work well together, there can be some unexpected behavior around the internal keyword when calling Kotlin classes from a Java class.

If you plan to migrate to Kotlin in phases, just keep that in mind.

Companion Can Complicate

Kotlin does not allow public static variables/methods that we are typically used to in Java. Instead it has the concept of a companion object that handles the behavior of statics and interfaces in Java.

If you create a constant in a Java class and convert it to Kotlin, the converter does not realize the static final variable was intended to be used as a constant which can lead to weird interoperability between Kotlin and Java.

When you want a constant in Java, you create a static final variable:

public class DetailsActivity extends Activity {
    public static final String SHARED_ELEMENT_NAME = "hero";
    public static final String MOVIE = "Movie";

    //...

}

After the conversion, you will see that they end up in a companion class.

class DetailsActivity : Activity() {

    companion object {
        val SHARED_ELEMENT_NAME = "hero"
        val MOVIE = "Movie"
    }

    //...
}

This works as you would expect when used by other Kotlin classes.

val intent = Intent(context, DetailsActivity::class.java)
intent.putExtra(DetailsActivity.MOVIE, item)

However, because Kotlin converts the constant into its own companion class, access to your constants from a Java class is not intuitive.

intent.putExtra(DetailsActivity.Companion.getMOVIE(), item)

When we decompile the Kotlin class, we see that our constants become private and are exposed via a companion wrapper class.

public final class DetailsActivity extends Activity {
   @NotNull
   private static final String SHARED_ELEMENT_NAME = "hero";
   @NotNull
   private static final String MOVIE = "Movie";
   public static final DetailsActivity.Companion Companion = new DetailsActivity.Companion((DefaultConstructorMarker)null);
   //...

   public static final class Companion {
      @NotNull
      public final String getSHARED_ELEMENT_NAME() {
         return DetailsActivity.SHARED_ELEMENT_NAME;
      }

      @NotNull
      public final String getMOVIE() {
         return DetailsActivity.MOVIE;
      }

      private Companion() {
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}

This makes our Java code more complex than it needs to be.

The good news is that we can fix some of these issues, and get the behavior we want, by using the const keyword in the companion class.

class DetailsActivity : Activity() {

    companion object {
        const val SHARED_ELEMENT_NAME = "hero"
        const val MOVIE = "Movie"
    }

    //...
}

Now when we look at the decompiled code, we see our constants!

Sadly, we still end up creating an empty companion class instance. 

public final class DetailsActivity extends Activity {
   @NotNull
   public static final String SHARED_ELEMENT_NAME = "hero";
   @NotNull
   public static final String MOVIE = "Movie";
   public static final DetailsActivity.Companion Companion = new DetailsActivity.Companion((DefaultConstructorMarker)null);
   //...
   public static final class Companion {
      private Companion() {
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}

We can now access our constants as expected in our Java classes!

intent.putExtra(DetailsActivity.MOVIE, item)

Note that this only applies to primitives and Strings. For more information on non-primitives, check out @JvmField and Kotlin’s hidden costs.

Looping was limited until Kotlin improved it

By default, Kotlin converts loops over ranges using 0..N-1 bounds, which can be hard to maintain since there’s a potential of introducing off-by-one error.

For example, in my code, there was a nested for loop that added cards for each row/column in a grid; pretty typical Android for looping.

for (int i = 0; i < NUM_ROWS; i++) {
    //...
    for (int j = 0; j < NUM_COLS; j++) {
        //...
    }
    //...
}

The conversion was pretty straightforward.

for (i in 0..NUM_ROWS - 1) {
    //...
    for (j in 0..NUM_COLS - 1) {
        //...
    }
    //...
}

When converted, the code can look unfamiliar for Java developers, almost like it was written in Ruby or Python.

After reading Dan Lew’s blog, it appeared Kotlin’s range function is inclusive by default. However, when I read about Kotlin’s range features, I saw that their range features are extremely advanced and flexible.

We can simplify the converted code and make it more readable by leveraging some of Kotlin’s cool features.

for (i in 0 until NUM_ROWS) {
    //...
    for (j in 0 until NUM_COLS) {
        //...
    }
    //...
}

The until function makes our loops exclusive and easier to read. No more wondering about that awkward minus one!

Helpful tips for advanced Kotlin-ing

Feeling Kinda Lazy

Sometimes it is beneficial to lazy-load a member variable. Imagine you have a singleton class that manages a list of data. You do not need to create the list each time, so we often find ourselves making a lazy getter. A pattern along the lines of the following.

public static List<Movie> getList() {
    if (list == null) {
        list = createMovies();
    }
    return list;
}

After the converter attempted to convert this pattern, the code would not compile since it made list immutable, yet the return type of createMovies() was mutable. The compiler would not allow a mutable object to be returned when the method signature was expecting an immutable object.

It is a very powerful pattern to delegate loading an object, so Kotlin includes a function, lazy, to facilitate lazy loading. By using the lazy function, the code now compiles.

val list: List<Movie> by lazy {
    createMovies()
}

Since the last line is the returned object, we now can easily create an object that is lazy-loaded with less boilerplate code!

Death and Destruction

If you are familiar with destructing an array or object in javascript, then the destructuring declarations will feel very familiar.

In Java, we use objects and pass them around all the time. However, sometimes we only need a few properties of an object but do not take the time to extract only those properties into variables.

For a large number of properties, it is easier to just access the properties from the getter. For example:

final Movie movie = (Movie) getActivity()
        .getIntent().getSerializableExtra(DetailsActivity.MOVIE);

// Access properties from getters
mMediaPlayerGlue.setTitle(movie.getTitle());
mMediaPlayerGlue.setArtist(movie.getDescription());
mMediaPlayerGlue.setVideoUrl(movie.getVideoUrl());

However, Kotlin provides a powerful destructor declaration that simplifies accessing the properties of an object by shortening the boilerplate code of assigning each property to its own variable.

val (_, title, description, _, _, videoUrl) = activity
        .intent.getSerializableExtra(DetailsActivity.MOVIE) as Movie

// Access properties via variables
mMediaPlayerGlue.setTitle(title)
mMediaPlayerGlue.setArtist(description)
mMediaPlayerGlue.setVideoUrl(videoUrl)

And no surprise, the decompiled code is to be expected, where the componentX() methods reference our getters in our data class.

Serializable var10000 = this.getActivity().getIntent().getSerializableExtra("Movie");
Movie var5 = (Movie)var10000;
String title = var5.component2();
String description = var5.component3();
String videoUrl = var5.component6();

The converter was clever enough to simplify the code by destructuring the model object. However, I recommend reading about lambdas with destruction. In Java 8, it is common to put parameters to a lambda function in parenthesis for more than one parameter. In Kotlin, that could be interpreted as a destructor.

Conclusion

Using the conversion tool in Android Studio was a great starting point in my journey to learning Kotlin. However, seeing the some of the code that was produced has forced me to dig deeper into Kotlin to find better ways to write code.

I am glad I was warned to proofread after the conversion. Otherwise, I would have created some Kotlin code that wasn’t easy to understand! Then again, my Java code is not much better.

猜你喜欢

转载自blog.csdn.net/ultrapro/article/details/73776757