Sunday, 4 July 2021

Android Java to Kotlin migration by example, part 1

Well, I've recently started to catch up with Kotlin programming language, which replaces Java as main Android development language.

This post will be a first in the series - in this one I'll simply "translate" some Java code to Kotlin, in future ones I'll elaborate more on Kotlin's syntax (making them something like tutorial, I hope).

I've created a simple (one activity) app - in Java at first - which takes the text from an EditText, saves it to internal data, allows to read it and allows to add it as a note to the notification area. There are also some unnecessary (in this context) things, like a constant - it will be our excuse to show some Kotlin's syntax.

In the beginning, let's create a simple layout:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android=
"http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Button
android:id="@+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="160dp"
android:layout_marginBottom="32dp"
android:onClick="onAddNotificationButtonClick"
android:text="Add notification"
app:layout_constraintBottom_toTopOf="@+id/textView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.486"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.171" />

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="392dp"
android:onClick="onSaveClick"
android:text="Save"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.281"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="392dp"
android:onClick="onLoadClick"
android:text="Load"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.758"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Button
android:id="@+id/button5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="76dp"
android:onClick="onButtonOpenClick"
android:text="Open"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />

<EditText
android:id="@+id/editTextTextMultiLine"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="90dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="111dp"
android:ems="10"
android:gravity="start|top"
android:inputType="textMultiLine"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button4" />

</androidx.constraintlayout.widget.ConstraintLayout>

... and save it as activity_main.xml in the res/layout .

In layout editor it looks like this:


It has a button, which adds a notification to the tray, then a multiline edit-text allowing us to input the notification's content, a textView which I forgot to use ;), and then three buttons:

- the Save button causes textView content to be saved in internal storage (persistantly)

- the Load button reads data from internal storage and shows them in the textView

- the Open button opens another Activity... I'll exlain in a moment.

Let's begin with creating the typical Android Activity class and initializing some textView and multiline edit text. To be specific - I'll create two separate activities, but they're be exactly the same (even using the same layout). One will be in Java, another one - in Kotlin. The Open button in each of them will open the other one: Java -> Kotlin -> Java...

JAVA:

public class MainActivity extends AppCompatActivity {

TextView tv;
EditText editTextTextMultiline;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = findViewById(R.id.textView);
editTextTextMultiline = findViewById(R.id.editTextTextMultiLine);
}

KOTLIN:

class SecondActivity : AppCompatActivity() {

// the default visibility modifier is public:
private lateinit var tv: TextView;
private lateinit var editTextMultiline: EditText;

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
tv = findViewById(R.id.textView);
editTextMultiline = findViewById(R.id.editTextTextMultiLine)
}

Ok, so let's go:

- in Kotlin, the default visibility modifier is public (package-private in Java). A bit strange if you're used to most of the object-oriented languages, but doesn't seem to be a problem. BTW Android Studio marks the functions it 'thinks' could be private and allows us to change the visibility automatically.

- the variable declaration is done using the keyword var.

- you can't just declare variable without initialing it OR stating explicitly it's to be initialized later by using the lateinit keyword.

- the type declaration syntax is bit funny, looks Pascal-like to me. 

override becomes modifier instead of annotation

- the nullable parameter has to be expliitly stated as nullable (Bundle?, not Bundle type)

- semicolons after instructions are optional (unnecessary, in fact, I'll try to avoid them later)

- function declaration looks different (more about this in a moment)

Ok, now let's declare a variable, a constant, a simple function and a function called by the button in activity (chosen in a layout xml with onClick):

Java:

int someNumber = 0;

final float PI = 3.1415f;

int getIncrementedNumber(int number){
return number+1;
}
public void onChangeTextClick(View v) {
someNumber = getIncrementedNumber(someNumber);
tv.setText(String.format(Locale.getDefault(), "%d", someNumber));
}

Kotlin:

var someNumber = 0;

val PI = 3.1415f;

private fun getIncrementedNumber(number: Int): Int{
return number+1
}

fun onChangeTextClick(v: View) {
someNumber = getIncrementedNumber(someNumber)
tv.text = String.format(Locale.getDefault(), "%d", someNumber)
}

- constant is declared using val keyword

- function is declared using fun keyword, the returned type for function is added after as a last part of its prototype (: Int{ part). Analogous to variable declaration. If function does not return anythong (void-like), the returned type part is omitted.

- getters and setters are refered to like field names, not like methods (no parenthesis).

Overall, code in Kotlin seems to be a bit more concise.

Now let's do the most basic thing in Android: open another activity. The Kotlin one will open Java activity, the Java activity will open Kotlin one :)

Java:

public void onButtonOpenClick(View v) {
Intent intent = new Intent(this, SecondActivity.class);
startActivity(intent);
}

Kotlin:

fun onButtonOpenClick(v: View) {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
}

In Kotlin:

- using val instead of explicit class name save us some space

- constructor is called without the new keyword

- reference to class looks a bit different. 

Now something longer. Let's create a method for creating a notification channel. It will work just on Android 8.0+ for now (the system changed then):

Java:

private void createNotificationChannel() {
// Create the NotificationChannel, but only on API 26+ because
// the NotificationChannel class is new and not in the support library
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = "chanName";
String description = "chanDesc";
int importance = NotificationManager.IMPORTANCE_DEFAULT;
NotificationChannel channel = new NotificationChannel(
"someChannelId432", name, importance);
channel.setDescription(description);

NotificationManager notificationManager =
getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}
}

Kotlin:

private fun createNotificationChannel() {
// Create the NotificationChannel, but only on API 26+ because
// the NotificationChannel class is new and not in the support library
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val name = "chanName"
val description = "chanDesc"
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel = NotificationChannel(
"someChannelId4234", name, importance)
channel.description = description

val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE)
as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}

Creating notificationManager requires casting - to be honest, I don't know why.

Method for saving some string to file (internal storage):

Java:

void saveDataToFile(String filename, String fileContents) {
try {
FileOutputStream fos =
this.getBaseContext().openFileOutput(filename,
Context.MODE_PRIVATE);
fos.write(fileContents.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}

Kotlin:

private fun saveDataToFile(fileName: String, fileContents: String) {
try {
val fos: FileOutputStream =
this.baseContext.openFileOutput(fileName, Context.MODE_PRIVATE)
fos.write(fileContents.toByteArray())
} catch (e: IOException) {
e.printStackTrace()
}
}

(please mind the exception type declaration syntax in catch

... and for reading from file:

Java:

String readFromFile(String filename) {
FileInputStream fis;
StringBuilder stringBuilder = new StringBuilder();
try {
fis = MainActivity.this.openFileInput(filename);

InputStreamReader isr =
new InputStreamReader(fis, StandardCharsets.UTF_8);
BufferedReader reader = new BufferedReader(isr);
String line = reader.readLine();
while (line != null && line!="") {
stringBuilder.append(line);
line = reader.readLine();
if(line!=null)
stringBuilder.append('\n');
}
} catch (IOException e) {
// Error occurred when opening raw file for reading.
}
return stringBuilder.toString();
}

Kotlin:

private fun readFromFile(fileName: String): String {
val fis: FileInputStream?
val stringBuilder = StringBuilder()
try {
fis = (this@SecondActivity).openFileInput(fileName)

val isr = InputStreamReader(fis, StandardCharsets.UTF_8)
val reader = BufferedReader(isr)
var line = reader.readLine()
while (line != null && line != "") {
stringBuilder.append(line)
line = reader.readLine()
if(line!=null)
stringBuilder.append('\n')
}
} catch (e: IOException) {
// Error occurred when opening raw file for reading.
}
return stringBuilder.toString()
}

FileInputStream? has to explicitly declared with ? as nullable. ifwhile and return statements are similar to Java.

Now let's call our methods:

public void onSaveClick(View v) {
saveDataToFile(someFileName, editTextTextMultiline.
getText().toString());
}

public void onLoadClick(View v) {
editTextTextMultiline.
setText(readFromFile(someFileName));
}

becomes:

fun onSaveClick(v: View) {
saveDataToFile(someFileName, editTextMultiline.text.toString())
}

fun onLoadClick(v: View) {
editTextMultiline.setText(readFromFile(someFileName))
}

Finally, let's create the method for adding notification, called when "Add notification" button is pressed.

Java:

    public void onAddNotificationButtonClick(View v) {

createNotificationChannel();
NotificationCompat.Builder builder =
new NotificationCompat.Builder(this,
"someChannelId432")
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentTitle("My notification")
.setContentText("")
.setStyle(new NotificationCompat.BigTextStyle()
.bigText(editTextTextMultiline.getText()))
.setPriority(NotificationCompat.PRIORITY_DEFAULT);

NotificationManagerCompat notificationManager =
NotificationManagerCompat.from(this);
notificationManager.notify(4234, builder.build());
}
} // class end

...and Kotlin:

    fun onAddNotificationButtonClick(v: View) {

createNotificationChannel()
val builder = NotificationCompat.Builder(this,
"someChannelId432")
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentTitle("My kotlinofication")
.setContentText("")
.setStyle(NotificationCompat.BigTextStyle().
bigText(editTextMultiline.text))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)

val notificationManager = NotificationManagerCompat.from(this)
notificationManager.notify(3213, builder.build())
}
} // class end

Again: it's similar. Lack of explicit types declarations (NotificationCompat.Builder builder becomes val builder) no new keyword and lack of semicolons makes the code a bit shorter. Quite readable, in my opinion.

One last thing: manifest, without it updated the second activity couldn't be open. It should look like this:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapplicationjava">

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MyApplicationJava">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".SecondActivity">
</activity>
</application>

</manifest>

That's it for now. In the future posts I'll elaborate more on details, providing more examples of short Java code and corresponsing Kotlin code.

No comments:

Post a Comment

Python crash course part 10: inheritance and polymorphism

In the last part we've shown how to create and use a class in Python. Today we're going to talk about inheritance: wchich means cre...