With the release of Android back in 2008, the go-to language for app development has always been Java – however, that’s beginning to change. At Google IO 2017, it was announced that Android would be receiving “first class support” for Kotlin. Since then, the language has been slowly ramping up to becoming the preferred language for development on the Android platform.

In the following article I’m going to give you some ways you can refactor/rewrite your existing Java app code to Kotlin, while also pointing out the advantages Kotlin gives us.

But what about Java?

I still feel like Java is worth knowing, since a lot of the Android APIs, as well as their documentation, are written in it. If you’re just starting Android development, I would reccommend developing at least an intermediate level of Java knowledge before moving on to Kotlin. If you’d like an in-depth guide to getting started with Android development, you can read an article I wrote detailing how to become an Android developer in under a year.

I should also mention that Kotlin is interoperable with Java. For this reason, you aren’t forced to choose one or the other. You’re able to call Java code with Kotlin and vise versa. So, don’t feel like you’re wasting time just because you’re learning Java.

Refactoring Java to Kotlin

I’m going to present some Java classes from my own personal projects, with the Kotlin counterpart following. The advantages Kotlin presents should be evident from simply looking at the code, but I’ll be sure to go through each.

SharedPreferences

public class SharedPrefsManagerImpl implements SharedPrefsManager { private static final String KEY_SOUND_ENABLED = "key_sound_enabled"; private static final String KEY_SECOND_COUNT = "key_second_count"; private final SharedPreferences preferences; public SharedPrefsManagerImpl(Context context) { this.preferences = PreferenceManager.getDefaultSharedPreferences(context); } @Override public boolean isSoundEnabled() { return preferences.getBoolean(KEY_SOUND_ENABLED, true); } @Override public void setSoundEnabled(boolean enabled) { preferences.edit().putBoolean(KEY_SOUND_ENABLED, enabled).apply(); } @Override public int getStartingCountdownSeconds() { return preferences.getInt(KEY_SECOND_COUNT, 30); } @Override public void changeStartingCountdown() { switch (getStartingCountdownSeconds()){ case 30: setSavedSecondCount(10); break; case 10: setSavedSecondCount(5); break; case 5: setSavedSecondCount(30); break; } } private void setSavedSecondCount(int secondCount){ preferences.edit().putInt(KEY_SECOND_COUNT, secondCount).apply(); } }

Here’s an implementation of an interface used for managing a few SharedPreferences values. The class is quite straight-forward, as far as preference managers go. Let’s take a look at the same class in Kotlin.

class SharedPrefsManagerImpl(context: Context?) : SharedPrefsManager { companion object { private const val KEY_SOUND_ENABLED = "key_sound_enabled" private const val KEY_SECOND_COUNT = "key_second_count" } private val preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) override var isSoundEnabled: Boolean get() = preferences.getBoolean(KEY_SOUND_ENABLED, true) set(value) = preferences.edit { putBoolean(KEY_SOUND_ENABLED, value) } override var savedCountdownSeconds: Int get() = preferences.getInt(KEY_SECOND_COUNT, 30) set(value) = preferences.edit { putInt(KEY_SECOND_COUNT, value)} override fun changeStartingCountdown() { when (savedCountdownSeconds) { 30 -> savedCountdownSeconds = 10 10 -> savedCountdownSeconds = 5 5 -> savedCountdownSeconds = 30 } } }

Immediately, you can already see the amount of code we need is less than that of the Java class. Not only is it less code, but it is much more concise and easier to read. You will notice we also take advantage of a few Kotlin specific features. Since we’re allowed to define our own custom setters and getters directly on class properties, we can go ahead and remove the individual set and get methods that we had in our Java code. In our changeStartingCountdown() function, we use a when expression – it is the Kotlin equivalent of a switch statement. Furthermore, we use a companion object to define our constants in. You can read more on companion objects here. Finally, we take advantage of an extension function when calling edit() on our SharedPreferences. In my project, this function comes from the Android ktx library.

Fragment

public class PressButtonFragment extends PressButtonFragmentBase { @BindView(R.id.b_red_button) Button bRedButton; @BindView(R.id.tv_timer) TextView tvTimer; @BindView(R.id.tv_press_count) TextView tvPressCount; @BindView(R.id.tv_gameover_amt) TextView tvGameoverAmt; @BindView(R.id.gameover_view) ConstraintLayout gameoverView; @BindView(R.id.main_view) ConstraintLayout mainView; @BindView(R.id.tv_gameover_avg) TextView tvGameoverAvg; Unbinder unbinder; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_press_button, container, false); unbinder = ButterKnife.bind(this, view); return view; } @Override public PressButtonContract.Actions createPresenter() { FragmentComponent component = DaggerFragmentComponent .builder() .schedulerProviderModule(new SchedulerProviderModule(new BaseSchedulerProvider())) .contextModule(new ContextModule(getActivity().getApplicationContext())) .build(); PressButtonPresenter pressButtonPresenter = new PressButtonPresenter(); component.injectPressButtonPresenter(pressButtonPresenter); return pressButtonPresenter; } @Override public void onNewViewStateInstance() { Timber.d("new viewstate"); presenter.reset(); } @Override public void showMainView() { hideGameOverView(); viewState.setPressAmt(0); tvPressCount.setText(String.valueOf(0)); } @Override public void setStartingTime(int milliseconds) { viewState.setStartingTime(milliseconds / 1000); tvTimer.setText(Util.formatTime(milliseconds)); } @Override public void decrementTime(int milliseconds) { tvTimer.setText(Util.formatTime(milliseconds)); } @Override public void incrementPress(int presses) { viewState.setPressAmt(presses); tvPressCount.setText(String.valueOf(presses)); } @Override public void setGameOverMsg(int pressAmt, int initial) { showGameOverView(); tvGameoverAmt.setText(getString(R.string.gameover_pressed_amt, String.valueOf(pressAmt))); tvGameoverAvg.setText(getString(R.string.gameover_pressed_average, Util.getAverage(pressAmt, initial))); } @Override public void onDestroyView() { super.onDestroyView(); unbinder.unbind(); } @OnClick({R.id.b_try_again, R.id.b_red_button, R.id.tv_timer}) public void onViewClicked(View view) { switch (view.getId()) { case R.id.b_try_again: presenter.reset(); break; case R.id.b_red_button: presenter.incrementPress(); break; case R.id.tv_timer: presenter.changeInitialTime(); break; } } @Override public void hideGameOverView() { mainView.setVisibility(View.VISIBLE); gameoverView.setVisibility(View.GONE); } @Override public void showGameOverView() { mainView.setVisibility(View.GONE); gameoverView.setVisibility(View.VISIBLE); } }

Here we have a standard Fragment class with some UI related methods. At the top of the class, you will notice the standard Butterknife annotations used to remove a lot of the findViewById() boilerplate code. I’ve also delegated all my view clicks to a single method, onViewClicked() which then calls the correct method depending on the id of the view. At the time I wrote this class, lambdas, without the use of a 3rd party library, were still unsupported in Android Studio (pre 3.0).

class PressButtonFragment : PressButtonFragmentBase() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { return inflater.inflate(R.layout.fragment_press_button, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) b_try_again.setOnClickListener { presenter.reset() } b_red_button.setOnClickListener { presenter.incrementPress() } tv_timer.setOnClickListener { presenter.changeInitialTime() } } override fun createPresenter(): PressButtonContract.Actions { val component = DaggerFragmentComponent .builder() .schedulerProviderModule(SchedulerProviderModule(BaseSchedulerProvider())) .contextModule(ContextModule(activity?.applicationContext)) .build() val pressButtonPresenter = PressButtonPresenter(component.timerService, component.sharedPrefsManager, component.schedulerProvider) component.injectPressButtonPresenter(pressButtonPresenter) return pressButtonPresenter } override fun onNewViewStateInstance() { Timber.d("new viewstate") presenter.reset() } override fun showMainView() { hideGameOverView() viewState.setPressAmt(0) tv_press_count.text = 0.toString() } override fun setStartingTime(seconds: Int) { viewState.setStartingTime(seconds) tv_timer.text = seconds.secondsToMilliseconds().formatTimeStringMs() } override fun decrementTime(milliseconds: Int) { tv_timer.text = milliseconds.formatTimeStringMs() } override fun incrementPress(presses: Int) { Timber.d(presses.toString()) viewState.setPressAmt(presses) tv_press_count.text = presses.toString() } override fun setTextRunningColour() { tv_timer.setTextColor(ContextCompat.getColor(activity!!.applicationContext, android.R.color.primary_text_dark)) } override fun setGameOverMsg(pressAmt: Int, startingTimeSeconds: Int) { showGameOverView() tv_gameover_amt.text = getString(R.string.gameover_pressed_amt, pressAmt.toString()) tv_gameover_avg.text = getString(R.string.gameover_pressed_average, Extensions.getAverageString(pressAmt, startingTimeSeconds)) } override fun hideGameOverView() { main_view.animateInFrom(gameover_view) tv_timer.setTextColor(ContextCompat.getColor(activity!!.applicationContext, R.color.timerRest)) } override fun showGameOverView() { main_view.visibility = View.GONE gameover_view.visibility = View.VISIBLE gameover_view.alpha = 1f } }

The biggest difference in our Kotlin equivalent class should be immediately apparent. Where are we defining and initializing our views? Thanks to the Kotlin Android Extensions plugin, we no longer have to do that. With synthetic properties, we can access our views by the exact id we give them in our layouts. The plugin is taking care of inflating and caching our views (for configuration changes) under the hood. You may also notice I’ve removed the onViewClicked() method. Instead of a switch statement, I am now explicitly calling the presenter methods in lambdas. However, they look a bit strange don’t they? In Kotlin, if we are writing a lambda that takes one parameter, we can completely omit the use of -> – as you can see in the setOnClickListener() methods.

Model Objects

public class ServerModel implements Comparable<ServerModel> { @SerializedName("id") @Expose private int id; @SerializedName("name") @Expose private String name; @SerializedName("population") @Expose private String population; private int populationLevel; public ServerModel(int id, String name, String population) { this.id = id; this.name = name; this.population = population; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPopulation() { return population; } public void setPopulation(String population) { this.population = population; } public void setPopulationLevel(int level){ this.populationLevel = level; } public int getPopulationLevel(){ switch (population){ case "Low": return 0; case "Medium": return 1; case "High": return 2; case "VeryHigh": return 3; case "Full": return 4; default: return 0; } } public int getPopulationColor(){ switch (population){ case "Low": return Color.parseColor("#C6E0B4"); case "Medium": return Color.parseColor("#A9D08E"); case "High": return Color.parseColor("#FFC000"); case "VeryHigh": return Color.parseColor("#ED7D31"); case "Full": return Color.parseColor("#FF0000"); default: return Color.parseColor("#C6E0B4"); } } public String getIdString(){ return String.valueOf(this.id); } @Override public int compareTo(@NonNull ServerModel serverModel) { return serverModel.getPopulationLevel() - this.getPopulationLevel(); } public static ServerModel getFakeServer(){ return new ServerModel(9999, "TestServer", "null"); } }

Here’s a standard model class I wrote in an older project. Notice the explicit use of writing our setters and getters in the Java code. Let’s see the Kotlin equivalent.

data class ServerModel(@field:SerializedName("id") @field:Expose val id: Int, @field:SerializedName("name") @field:Expose val name: String, @field:SerializedName("population") @field:Expose val population: String) : Comparable<ServerModel> { private val populationLevel: Int get() { return when (population) { "Low" -> 0 "Medium" -> 1 "High" -> 2 "VeryHigh" -> 3 "Full" -> 4 else -> 0 } } val populationColor: Int get() { return when (population) { "Low" -> Color.parseColor("#C6E0B4") "Medium" -> Color.parseColor("#A9D08E") "High" -> Color.parseColor("#FFC000") "VeryHigh" -> Color.parseColor("#ED7D31") "Full" -> Color.parseColor("#FF0000") else -> Color.parseColor("#C6E0B4") } } val idString: String get() = this.id.toString() override fun compareTo(other: ServerModel): Int { return other.populationLevel - this.populationLevel } companion object { val fakeServer: ServerModel get() = ServerModel(9999, "TestServer", "null") } }

As standard in Kotlin, we can omit the use of set and get methods. However, we still define them for custom behaviour as can be seen in populationColor and populationLevel. We also mark this class as a data class in Kotlin. Doing so provides our class with a number of features which can be seen here. It is worth noting that the use of the data keyword should only be reserved for model objects.

Wrapping up

If you’ve been on the fence with switching to Kotlin – I’d highly suggest you to give it a try. The language provides many great advantages over Java that make our lives as developers much easier. A good way to learn would be to refactor your older Java projects. I’d advise you start with the less complex classes and work your way up to the more involved ones such as Fragments, Activities or any code related fetching network data.

Liked the article? Share it!







