- Migrating an Android project to Kotlin
- Ben Weiss
- The Nuggets translation Project
- Permanent link to this article: github.com/xitu/gold-m…
- Translator: wilsonandusa
- Proofreader: Phxnirvana, Zhiw
Not long ago we opened source Topeka, a small Android test application. The program is tested using Integration tests and Unit Tests, and is itself written entirely in Java. At least it used to be…
What is the name of that island off st. Petersburg?
Google officially announced support for the Kotlin programming language at its developer conference in 2017. Since then, I’ve been porting Java code, learning Kotlin in the process.
The port was not technically necessary, the application itself was quite stable, and it was mostly to satisfy my curiosity. Topeka became a vehicle for me to learn a new language.
You can look at it if you’re curiousGitHub source code.
Currently the Kotlin code is on a separate branch, but we plan to merge it into the main code at some point in the future.
This article covers some of the key points I’ve discovered in migrating code, as well as some useful tips for Developing new languages for Android.
It looks the same
๐ Key points
- Kotlin is an interesting and powerful language
- More tests will put you at ease
- Platforms are rarely limited
The first step in porting to Kotlin
It’s not as simple as Bad Android Advice suggests, but it’s a good starting point.
Steps 1 and 2 are really useful for learning Kotlin well.
The third step, however, would depend on my own fate.
The actual steps for Topeka are as follows:
- Learn Kotlin’s basic grammar
- Familiarize yourself with the language by using Koan
- โงโK to ensure that (the converted file) will still pass the tests one by one
- Modify the Kotlin file to make it more language friendly
- Repeat step 4 until you and the person reviewing your code are satisfied
- Completed and handed in
interoperability
It’s wise to take things one step at a time. When Kotlin compiles to Java bytecode, the two languages are interoperable. And since two languages can coexist on the same project, you don’t need to port all your code to another language. But if that’s what you want to do, it makes sense to do it repeatedly, so you can keep the project as stable as possible as you migrate the code, and learn something in the process.
Do more tests and you’ll feel better
The benefits of using unit and integration tests together are numerous. In the vast majority of cases, these tests are used to ensure that the current change does not break existing functionality.
I chose to start with a less complex data class. I have been using these classes throughout the project and their complexity is relatively low. This makes these classes an ideal starting point for learning a new language.
After porting some of the code by using the Kotlin codebase that comes with Android Studio, I started executing and passing the tests until I finally ported the tests themselves to Kotlin code.
Without testing, I would need to manually test the functionality that might be affected after each rewrite. Automated tests made it easier for me to migrate the code.
So this is one more reason to do it if you haven’t tested your application properly. ๐
The generated code doesn’t look great every time!!
After completing the migration code, which was almost automated at first, I started to learn the Kotlin Code Style Guide. This shows me that there is still a long way to go.
Overall, the code generator works well. Although many language features and styles are not used in the transition, translating languages is inherently tricky and can be better done this way, especially if the language contains many features or can be expressed in different ways.
If you want to learn more about Kotlin converters, Benjamin Baxter writes about some of his own experiences:
โผ ๏ธ โ
I find that automatic conversion produces a lot of, right? And!!!!! . These symbols are used to define nullable values and to ensure that they are not null. Instead, they cause null-pointer exceptions. I can’t help but think of an apt quote
“Excessive use of exclamation marks,” he said, shaking his head, “is a sign of psychological disorder.” – Terry Pratchett
It won’t be null in most cases, so we don’t need to use null checks. There is also no need to initialize all the values directly through a constructor. Lateinit or delegate can be used instead of the initial process.
However, these methods are not panacea:
Sometimes variables are null.
I’m going to have to redefine view as nullable.
In other cases you still have to check for null. If there is a supportActionBar, *supportActionBar*? SetDisplayShowTitleEnabled (false) will perform a question mark after the code. This means fewer if condition declarations based on null checks. ๐ฅ
It is very convenient to use the stdlib function directly on non-null values:
toolbarBack? .let { it.scaleX = 0f it.scaleY = 0f }Copy the code
Using it on a large scale…
It becomes more and more linguistic
Because we can constantly rewrite the generated code to make it more language-friendly through feedback from reviewers. This makes the code more concise and improves readability. These characteristics prove that Kotlin is a powerful language,
Let’s take a look at a few examples I’ve come across.
Reading less isn’t necessarily a bad thing
Let’s take the getView from the Adapter as an example:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (null == convertView) {
convertView = createView(parent);
}
bindView(convertView);
return convertView;
}Copy the code
In Java getView
override fun getView(position: Int, convertView: View? , parent: ViewGroup) = (convertView ? : createView(parent)).also {bindView(it) }Copy the code
The Kotlin getView
These two pieces of code are doing the same thing:
First check if convertView is null and then createView(…) Create a new view inside, or return convertView. Also call bindView(…) at the end .
Both ends of the code are pretty clean, but I was really surprised to get down from eight lines of code to just two.
Data classes are amazing ๐ฆ
To further illustrate Kotlin’s simplicity, using data classes can easily avoid lengthy code:
public class Player {
private final String mFirstName;
private final String mLastInitial;
private final Avatar mAvatar;
public Player(String firstName, String lastInitial, Avatar avatar) {
mFirstName = firstName;
mLastInitial = lastInitial;
mAvatar = avatar;
}
public String getFirstName() {
return mFirstName;
}
public String getLastInitial() {
return mLastInitial;
}
public Avatar getAvatar() {
return mAvatar;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if(o == null || getClass() ! = o.getClass()) {return false;
}
Player player = (Player) o;
if(mAvatar ! = player.mAvatar) {return false;
}
if(! mFirstName.equals(player.mFirstName)) {return false;
}
if(! mLastInitial.equals(player.mLastInitial)) {return false;
}
return true;
}
@Override
public int hashCode() {
int result = mFirstName.hashCode();
result = 31 * result + mLastInitial.hashCode();
result = 31 * result + mAvatar.hashCode();
returnresult; }}Copy the code
Let’s see how to write this code with Kotlin:
data class Player( val firstName: String? , val lastInitial: String? , val avatar: Avatar?)Copy the code
Yes, fifty-five lines of code are missing without sacrificing functionality. That’s the magic of data classes.
Extended functionality
Here’s what traditional Android developers might find strange. Kotlin allows you to create your own DSL within a given scope.
How does it work
Sometimes we pass Boolean via Parcel in Topeka. The Android framework’s API does not directly support this feature. When one begins to realize the function must be called a function class functions such as ParcelableHelper writeBoolean (parcel, value). If Kotlin is used, the extension function can solve the previous problem:
Import android.os.Parcel /** * writes a Boolean value to [Parcel]. * @param toWrite is the value to be written. */ fun Parcel.writeBoolean(toWrite: Boolean) = writeByte(if (toWrite) 1 else0) /** * get Boolean values from [Parcel]. */ fun Parcel.readBoolean() = 1 == this.readByte()Copy the code
Once you’ve written the code above, you can call parcel. WriteBoolean (value) and parcel. ReadBoolean () directly as part of the framework. It would be hard to tell the difference if Android Studio didn’t use different highlighting to distinguish extension functions.
Extension functions can improve code readability. Let’s look at another example: replacing a Fragment in a View’s hierarchy.
Using Java, the code looks like this:
getSupportFragmentManager().beginTransaction()
.replace(R.id.quiz_fragment_container, myFragment)
.commit();Copy the code
These lines of code are actually pretty good. But every time the Fragment is replaced you have to write those lines again, or create a function in another Utils class.
With Kotlin, when we need to replace a Fragment in a FragmentActivity, we simply call the replaceFragment(R.i C. container, MyFragment()) with the following code:
fun FragmentActivity.replaceFragment(@IdRes id: Int, fragment: Fragment) {
supportFragmentManager.beginTransaction().replace(id, fragment).commit()
}Copy the code
Replacing the Fragment is a single line of code
Less form, more function
Higher-order functions blow my mind. Yes, I know this isn’t a new concept, but it might be for some traditional Android developers. I’ve heard of such functions before and seen them written, but I’ve never used them in my own code.
Several times in Topeka I relied on OnLayoutChangeListener to implement the injection behavior. Without Kotlin, doing so would generate an anonymous class with duplicate code.
After migrating the code, just call the following code: view.onlayoutchange {myAction()}
The code is encapsulated in the following extension function:
/** * Inline fun view. onLayoutChange(crossSinLine action: () -> Unit) { addOnLayoutChangeListener(object : View.OnLayoutChangeListener { override fun onLayoutChange(v: View, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) { removeOnLayoutChangeListener(this) action() } }) }Copy the code
Use higher-order functions to reduce boilerplate code
Another example shows that the same functionality can be applied to database operations:
inline fun SQLiteDatabase.transact(operation: SQLiteDatabase.() -> Unit) {
try {
beginTransaction()
operation()
setTransactionSuccessful()
} finally {
endTransaction()
}
}Copy the code
Less form, more function
When this is done, the API user can do all of the above simply by calling db.transact {operation()}.
Update via Twitter: You can pass functions in operation() and use the database directly by using SQLiteDatabase.() instead of (). ๐ฅ
I don’t have to tell you that.
Using higher-order and extension functions improves the readability of your project while eliminating long code, improving performance, and omitting details.
To explore
So far I’ve been talking about code specifications and development conventions, and I haven’t mentioned any practical experience with Android development.
This is mainly because I don’t know the language very well, or I haven’t put much effort into collecting and publishing about it. Maybe it’s because I haven’t come across this kind of situation yet, but there seem to be quite a few platform-specific language styles. If you know of this, please add it in the comments section.
The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, React, front-end, back-end, product, design and other fields. If you want to see more high-quality translation, please continue to pay attention to the Project, official Weibo, Zhihu column.