📜 ⬆️ ⬇️

What I learned by converting a project to Kotlin using Android Studio

To my great joy, I finally had the opportunity to work with the popular Kotlin language - convert a simple Java application using the Convert Java File to Kotlin tool from Android Studio. I tried the language and would like to talk about my experience.

I quickly became convinced that this tool converts most of the classes into Java almost flawlessly. But in some places I had to clean up the code behind it, and in the process I learned a few new keywords!

Below I will share my observations. Before we begin, I note: if at some point you want to take a look at what is happening “under the hood”, Android Studio allows you to track all the processes; just go to the panels in the following path: Tools → Kotlin → Show Kotlin Bytecode.
')

Constant long


I did not even notice the first change at first - it was so insignificant. Magically, the converter replaced the long constant in one of the classes with an int and converted it back to long with each call. Brr!

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

Good news: The constant is still recognized by the val keyword.

The bad news: many processes were accompanied by unnecessary transformations. I expected that the security of types in Kotlin will be at a higher level, that everything will be implemented better there. Maybe I overestimated how smart this converter is?

The solution turned out to be simple: you just needed to add an “L” at the end of the variable declaration (something like in Java).

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

Better late than never


One of the main advantages of Kotlin is null security , which eliminates the threat of null references. This is accomplished using a type system that distinguishes between non-null and non-null references. In most cases, you prefer the links that do not allow null values, with which there is no risk of facing NPE (Null Pointer Exceptions). However, in some situations, null links can be useful, for example, when initializing from an onClick () event, such as AsyncTask.

There are several ways to work with zero links:

  1. Good old if statements that check properties for zero references before giving access to them (Java should have taught you to them).
  2. A cool Safe Call Operator (syntax?.) That checks for zero values ​​in the background for you. If the object is a null reference, then it returns zero (not NPE). No more annoying if statements!
  3. Forcible return of NPE with the help of the operator !! .. In this case, you are actually writing a familiar Java code and you need to return to the first step.

Determining exactly which pattern to stop in order to ensure null security is not an easy task, so the converter chooses the simplest solution by default (the third one), allowing the developer to cope with the problem in the best way for his case.

I understood that allowing the Kotlin code to throw out the null pointer exception somehow contradicts the advantages of this language, and began to dig deeper in the hope of finding a solution that would be better than the existing ones.

So I discovered the powerful lateinit keyword. Using lateinit in Kotlin, you can initialize non-zero properties after calling the constructor, which gives you the opportunity to move away from zero properties altogether.

This means that I get all the advantages of the second approach without the need to prescribe an additional "?.". I simply treat the methods as if they are in principle never null, without wasting time on routine checks and using the syntax I’m used to.

Using lateinit is an easy way to remove operators !!! from the Kotlin code. If you are interested in other tips on how to get rid of them and make the code more accurate, I recommend the post David Vávra .

Internal and his inner world


Since I was converting from class to class, I wondered how the already converted classes would interact with those that still remain in Java. I read that Kotlin is perfectly compatible with Java , so, logically, everything should work without visible changes.

I had a public method in one fragment, which was converted to the internal function in Kotlin. In Java, he had no access modifiers , and, accordingly, he was package private.

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

The converter noticed the absence of access modifiers and decided that the method should be visible only within the module / package, using the internal keyword to set the visibility parameters .

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

What does this new keyword mean? Looking into the decompiled bitcode, we will immediately see that the method name from setErrorContent () has become setErrorContent $ production_sources_for_module_app ().

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

Good news: in other Kotlin classes, it’s enough to know the original name of the method.

 mErrorFragment.setErrorContent() 

Kotlin will translate it into the generated name. If you look at the decompiled code again, you can see how the translation was made.

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

Thus, Kotlin understands the changes in the names on their own. What about the rest of the Java classes?

You cannot call the errorFragment.setErrorContent () method from the Java class, because this “internal” method does not actually exist (since the name has changed).

The setErrorContent () method is now invisible for classes in Java, as can be seen in the API and in the Intellisense window in Android Studio. So you have to use the generated (and very cumbersome) method name.


Despite the fact that Java and Kotlin usually interact without problems, when calling the Kotlin classes from Java classes, unforeseen difficulties may arise with the keyword internal. If you plan to move to Kotlin in stages, keep this in mind.

Companion difficulties


Kotlin does not allow public static variables and methods that are so typical of Java. Instead, it offers a concept such as a companion object , which is responsible for the behavior of static objects and interfaces in Java.

If you create a constant in a Java class and then convert it to Kotlin, the converter does not recognize that the static final variable should be used as a constant, which can lead to interference with Java and Kotlin compatibility.

When you need a constant in the Java class, 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"; //... } 

As you can see, after converting, they all ended up in a companion class:

 class DetailsActivity : Activity() { companion object { val SHARED_ELEMENT_NAME = "hero" val MOVIE = "Movie" } //... } 

When they are used by other Kotlin classes, everything happens as expected:

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


However, since Kotlin, converting a constant, places it in a companion’s own class, access to such constants from the Java class is not intuitive.

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

Decompiling the class in Kotlin, we can see that the constants have become private and are expanded through the 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(); } } } 

As a result, the code is much more complicated than we would like.

The good news is that we can partially correct the situation and achieve the desired behavior by entering the keyword const in the companion class.

 class DetailsActivity : Activity() { companion object { const val SHARED_ELEMENT_NAME = "hero" const val MOVIE = "Movie" } //... } 

Now, if you look at the decompiled code, we will see our constants! But alas, ultimately we still create an empty companion class.

 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(); } } } 

But access from Java classes occurs in the usual way!

Please note that this method only works for primitives and strings . To learn more about non-primitives, read JvmField and the Kotlin's hidden costs article.


Cycles, and how Kotlin improves them


By default, Kotlin converts loops in the range with 0..N-1 bounds, which makes it difficult to maintain the code, increasing the probability of errors by one .

In my code, for example, there was a nested for loop to add maps to each row — the most common example of a for loop on Android.

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

The conversion took place without any special tricks.

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

The resulting code may seem unusual to Java developers - as if it was written in Ruby or Python.

As Dan Lew writes on his blog, Kotlin’s range function is inclusive by default. However, having familiarized myself with the characteristics of the Kotlin range , I found them very well developed and flexible. We can simplify the code and make it more readable by taking advantage of the opportunities they offer.

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

The until function makes loops non-inclusive and easier to read. You can finally throw all these ridiculous -1 out of your head!

Useful tips for advanced


For the lazy

Sometimes it is useful to lazily load the member variable. Imagine that you have a singleton class that manages a list of data. Each time there is no need to re-create this list, so we often turn to the lazy getter . The pattern is like this:

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

If the converter tries to convert this pattern, the code will not compile, since the list is registered as immutable, while createMovies () has a changeable return type. The compiler will not allow returning a mutable object if the method signature specifies an immutable one.


This is a very powerful pattern for delegating object loading, so Kotlin incorporates a special function, lazy , to simplify loading in a lazy way. With it, the code is compiled.

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

Since the last line is the returned object, now we can create an object that requires less code to load it lazily!

Destructuring

If you had to destructurize arrays or objects in javascript , then declarations on restructuring will seem familiar to you.

In Java, we constantly create and move objects. However, in some cases, we need literally several properties of the object, and it is a pity to take them to variables. If we are talking about a large number of properties, it is easier to access them through a 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()); 

Kotlin, however, offers a powerful destructor declaration that simplifies the process of retrieving object properties, reducing the amount of code needed to assign a separate variable to each property.

 val (_, title, description, _, _, videoUrl) = activity .intent.getSerializableExtra(DetailsActivity.MOVIE) as Movie // Access properties via variables mMediaPlayerGlue.setTitle(title) mMediaPlayerGlue.setArtist(description) mMediaPlayerGlue.setVideoUrl(videoUrl) 


It is not surprising that in the decompiled code the methods we refer to are getters in data classes.

 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 smart enough to simplify the code by destructuring the object. Nevertheless, I would advise you to read about lambda and destructurization . In Java 8, there is a common practice to enclose the parameters of lambda functions in parentheses if there are more than one, but in Kotlin this can be interpreted as destructuring.

Conclusion


Using the tool to convert to Android Studio was for me a great first step in mastering Kotlin. But, having overlooked some sections of the received code, I was forced to begin to delve deeper into this language in order to find more efficient ways to write on it.

It is good that I was warned: after the conversion, the code must be deducted. Otherwise, on Kotlin, I would have got something unintelligible! Although, to be honest, I have no better with Java.

If you want to learn other useful information about Kotlin for beginners, I advise you to read this post and watch the video .

Source: https://habr.com/ru/post/332598/


All Articles