lilt: Lightweight Interactive Learning Tool

Topic 1

Introduction to Kotlin

Introduction

So far on your course you have been introduced to the fundamentals of programming using Python. You have learnt about variables, loops, conditional statements, arrays and functions as well as object-oriented programming. As you saw last year, object-oriented (OO) programming involves writing code to represent real world objects such as a student, a car, players and enemies in games, or graphical user interface elements such as windows and buttons. As you will hopefully see in this module, once you have learnt it, OO programming leads to more readable, more maintainable code.

Object-oriented programming can be used in many languages, such as Python, JavaScript, Java, or C++. An increasingly popular OO language, however, is Kotlin, and we will use Kotlin in this module. This is for a couple of reasons: Kotlin allows you to use contemporary programming styles easily, and Kotlin is the main language for Android development (which many of you will do in Semester 2).

Furthermore, with Kotlin, you can develop applications using a mix of Kotlin and Java, a very commonly-used object-oriented language. Both Kotlin and Java allow you to develop cross-platform applications: you can write a Kotlin/Java program on Windows and the same program will work on other operating systems such as Linux or Mac OS X.

When developing applications, we can use compiled or interpreted approaches, or a mix of the two.

  • A compiled language is a language which is converted from source code into machine code for the machine's CPU, in an executable format understandable by the machine's operating system. This conversion is done by a piece of software known as a compiler. For example, on a Windows/Intel system, Intel machine code instructions would be stored in a .EXE file. On a Linux/Intel system, Intel machine code instructions would be stored in a Linux executable file, which has a different format to an .EXE but still contains Intel instructions. On a smartphone with an ARM processor, ARM machine code instructions would be generated. C++ is an example of a compiled language. You write C++ code and then compile it into machine code for the target CPU and operating system. Machine code is a binary format: a series of bytes which mean something to a CPU but cannot be read in a text editor. Kotlin can also be used as a pure compiled language via Kotlin/Native.
  • An interpreted language is a language in which each line of code is read by a piece of software known as an interpreter at run time, i.e. when you run the program. Interpreted languages are slower than compiled languages. With a compiled language, you run the executable which contains machine code, and machine code is directly understood by the CPU. With interpreted languages, the process of interpreting each line of code at run time is a slow one in comparison to the CPU executing instructions that it understands directly. On the other hand, interpreted languages will run on any machine upon which an interpreter is installed, so they are more portable. They are easier to debug, as you can simply edit the code: with compiled languages, you need to compile again, and if the machine code generated by the compiler contains certain types of errors (e.g. trying to add an element to an array outside its bounds) there is the risk that the computer will crash.
  • A third approach, commonly used with Kotlin, uses bytecode and a virtual machine. This does not exactly fit into either category, but instead adopts a hybrid approach. You compile a program, but it is not compiled into native machine code. Instead, it is compiled into a format known as bytecode which can be run on a virtual machine: a piece of software which interprets the bytecode. Thus it is a combined compiled/interpreted approach: the original source code is compiled into bytecode, and the bytecode is then interpreted by the virtual machine. Thus it combines the advantages of the two approaches:

    • The bytecode will run on any device upon which the virtual machine is installed, thus it is cross-platform;
    • Because it is a compact, machine-friendly format, bytecode can be read by virtual machines faster than interpreters in pure interpreted languages can read text files of source code.

    A commonly-encountered virtual machine is the Java Virtual Machine (JVM), named after the Java programming language. This is used by both Java and Kotlin. The JVM is part of the Java Runtime Environment (JRE). The result is that any computer with the JRE installed can read bytecode for the Java Virtual Machine. So you can distribute such bytecode to Windows, Linux or Mac systems, and as long as they have the JRE installed, they will all be able to run your program.

    This is also the reason why you can mix Java and Kotlin in one application. You can write part of the application in Java and part in Kotlin, and because both compile to Java bytecode, the application will work as a complete whole.

The diagram below compares a standard compiled language and a language making use of bytecode.
Fully compiled language vs. JVM language

Key features of Kotlin

Kotlin, developed by JetBrains (the developers of PyCharm and IntelliJ IDEA) is based on the Java programming language but also contains features of Python and JavaScript. With Kotlin you can develop object-oriented applications, and we will be focusing mostly on object-oriented development in this module. However it also allows you to use other development styles, such as the increasingly-popular functional programming.

Some features of Kotlin include:

  • Null safety which prevents unintended use of null variables (equivalent to None in Python)
  • Type inference: type of variables can be inferred by what they are initialised to, so you don't need to explicitly declare the data type
  • Lambda functions: anonymous functions which can be passed as arguments to other functions (similar to arrow functions in JavaScript)
  • Data classes: concisely create classes which represent data and do not need methods
  • Extension functions: add a single function (method) to an existing class without needing to subclass it
  • Coroutines: a lightweight approach to multi-tasking

Basic Kotlin coding

Hello World in Kotlin

  • Here is Hello World in Kotlin:

  • Note that in Kotlin, we define a main function (fun is the Kotlin keyword to indicate a function). The main function is called main() and is the entry point of the application. If you do not provide a main() function, the Kotlin runtime environment will be unable to work out where your program starts. You may be familiar with this concept if you have used languages such as C or C++ in the past.
  • println() prints to the console, just like print() in Python

Data types in Kotlin

A difference between Kotlin and Python is that Kotlin is a strongly typed language. What does this mean? Whereas in Python, a variable can contain different data types, in Kotlin a variable can contain only one data type throughout its lifetime. By data type, we mean integer, floating-point, string and so on. Kotlin has the following data types (see the documentation).

  • Boolean: represents a boolean, i.e. true or false, value. Expressions return boolean values. Note that in Java, unlike in Python, true and false are lower case.
  • Byte: represents a single byte of data. Can hold the values 0 to 255.
  • Char: represents individual characters such as 'a', '!' or '$'. You will have met the ASCII character set where the values between 0 and 127 represent single characters (letters, numbers and punctuation from American English). An extended version, covering values 0 to 255, accounts for Western European characters (French, German etc as well as currency symbols such as the pound and the euro). However, the Char data type in Kotlin is designed to hold characters from international character sets such as Greek, Cyrillic (used in many Eastern European languages), Chinese or Arabic and therefore Char occupies two bytes in Kotlin. The two-byte system for representing characters is called Unicode.
  • Short: represents smaller whole numbers (16 bits or two bytes) in situations where memory is limited.
  • Int: represents an integer (a whole number). Occupies four bytes (32 bits). Can hold positive and negative values, and therefore can store values from -231 to +231-1 (31 bits for the number, 1 bit for the sign).
  • Long: represents larger whole numbers. Occupies eight bytes (64 bits). Again can hold positive and negative values, and therefore can store values from -263 to +263-1. Occupies two bytes (16 bits). Again can hold positive and negative values, and therefore can store values from -215 to +215-1 (-65536 to +65535)* Float: represents floating-point numbers. Occupies four bytes (32 bits). Recommended where memory is short but due to the relatively low precision, not recommended for applications such as scientific and financial data.
  • Double: represents double-precision floating-point numbers. Occupies eight bytes (64 bits). Recommended for operations where high precision is vital, such as scientific and financial data.
  • String and Arrays (of various types)

An important feature of Kotlin is that all data types are objects, even integers. We will look at the consequences of this later.

Variables example

  • This example shows some aspects of how variables work in Kotlin:

  • Note how we use val for immutable (unchanging) variables, and var for mutable variables (those which can change)
  • Note how the type of a variable can be inferred by what value we assign it to, in all variables above apart from "a", we do not declare the type
  • Nonetheless the variables are strongly typed. Once you have declared a variable, you can only store data of that type in the variable, unlike Python. So, we cannot place a String in the variable c in the above example.
  • See here for details on Kotlin data types

Loops in Kotlin

  • The program below shows some examples of basic loops:

  • Note the syntax 1..10. This is called a range expression. It returns a Range object representing the range of numbers 1 to 10.
  • Note also the difference between the first two examples; until is a function which returns a Range from the initial number up to, but not including, the final number
    • So the second loop will only count from 1 to 9
  • We also have the standard while loop too, which is very like the equivalent in JavaScript
  • See here for full documentation on loops

Conditional statements in Kotlin

  • The program below shows some examples of conditional statements in Kotlin:

  • Note how if/else is similar to JavaScript
  • The when statement is specific to Kotlin though is similar to switch in other languages.
  • Note the use of else inside when to handle unmatched conditions (e.g. here, if the grade is not A, B, C, D or F)

Conditional expressions

  • An important difference in Kotlin compared to many other languages is that conditionals can be used as expressions, i.e. they can return a value which can be assigned to a variable
  • eg.

  • Note in this example how we store the result of the if statement in the variable msg
    • ...so that msg wil contain either You invented Linux or You didn't invent Linux, depending on the name typed in
  • Also notice how the when statement similarly returns a value, i.e. "First" when the grade is "A", "2.1" when the grade is "B", etc.

Basic arrays, including for-in and if-in

  • Arrays are available in Kotlin, like most other languages
  • The in keyword in Kotlin allows us to do loops with arrays and easily work out if a value is in an array: this is the same idea as in Python.

  • Note the use of the if statement as an expression again (if(lang in langs)...)
  • Next time we will cover some additional features of arrays, and lists

Nullability

  • In Kotlin, the value null is used to indicate "no value", rather like None in Python
  • A common error in programming is to unintentionally use a null variable, because you expect it to contain something
  • One of the really useful features of Kotlin is null safety
  • With Kotlin, you can declare a variable to either be nullable or non-nullable
  • Non-nullable variables will produce a compiler error if you attempt to store 'null' in them

Non-nullable variables

  • By default, given data types (String, etc.) are non-nullable `* So this code will not compile, beacuse we declare s as a non-nullable String and attempt to assign null to it

Nullable variables

  • What about cases where we want the variable to be nullable?
  • For example, a collection of data which doesn't exist until we open a file and read it in from the file
  • In this case, we explicitly declare the variable as nullable by adding a question-mark ? to the data type
  • So, will this code successfully compile?

The safe-access operator

  • The answer is no, because even though we declared s as a nullable string, we then try to access the length of a String object which is null
  • In Java this type of operation would throw a NullPointerException
  • Kotlin's null-safety forces you to deal with this using the safe-access operator, ?.
  • Here is the previous example, rewritten to use the safe-access operator:

  • Here, due to the safe-access operator, we will only access the length if "s" is not null

Functions in Kotlin

  • The following example shows the use of functions in Kotlin, including parameters and return types:

  • Note the syntax for parameters, in which we specify the data type of each parameter:

    and note how we specify the return type of the function: the type of data which is returned.

  • This illustrates the strongly-typed nature of Kotlin. We must pass in a string and an integer to the function; if we pass anything else the compiler will give an error. Similarly, we must return a double from the function.
  • Note also how we can put function calls in quotes using $, and {} to contain the expression

Classes and Objects in Kotlin

As this is an object-oriented programming module, we will now look at how to create classes and objects in Kotlin. Remember from last year that:

  • a class is a general blueprint, or specification, for what data an entity contains and how it operates (e.g. Cat);
  • an object is a specific example of that class (e.g. a specific cat).

Remember also that classes contain:

  • Properties, also known as attributes, which describe objects of that class. For example, a cat class might have name, age and weight properties;
  • Methods, which describe what objects of that class can do. These are functions within the class. For example, a cat class might have walk(), eat() and meow() methods.

A Cat class

Binnie and Clyde
Here is an example of a Kotlin class representing a Cat:

We could use this in a main() function as follows:

  • Note how we begin the class declaration:

    This is known as the primary constructor. A constructor is used to initialise objects of the current class. It is the equivalent of the initialisation method __init__() in Python. The constructor takes parameters of the name (nameIn), age (ageIn) and weight (weightIn): these are passed into objects from outside the class.

  • Note how we place the constructor parameters (nameIn, ageIn and weightIn) immediately after the class name - this is different to Python in which we create the __init__() function inside the class body
  • Note how we need to declare the class properties (name, age and weight) inside the class. We did not need to do this with Python.
  • Note also the init block. This contains code we want to run when the object is first created. Here, we use the init block to set the properties equal to the constructor parameters
  • The walk() function is a class method, working the same way as methods in Python.
  • The toString() method returns a string representation of our object, containing the current Cat's name, age and weight. This is useful if we need to display the object. For example, when we print objects, such as our two Cats:

    the toString() method is used to figure out how to display the object.

    Note that we precede the toString() method with the keyword override. This explicitly states that we are overriding a method from the superclass, in other words replacing a more general version of the method in the superclass with a more specific version in a subclass. You might be thinking, what is the superclass? No inheritance is declared here. In fact, all Kotlin classes implicitly inherit from the Object class, which is the superclass of all others.

  • Note also that we specify the return type of toString():

Making our class more concise - automatically setting constructor parameters equal to class properties

  • This version of the previous example is considerably more concise:

  • Note how we specify either val or var before each constructor parameter. This automatically makes each parameter an property of the class
  • vals will be immutable, vars will be mutable
  • Thus, unlike the previous version, we do not have to declare the properties inside the class, or use an init block

Data classes - concisely representing complex data structures

  • In many cases, we need to create classes which represent a complex data structure, but do not need methods
  • A good example would be a Point class, to represent a 2D point (with x and y coordinates)
  • In Java, you could do this (note how x and y are public, to avoid the need for getters and setters; an implementation which wanted to make x and y immutable would need to make them private and add getter methods):

  • However, having to create a constructor to initialise the properties to the constructor parameters is a pain
  • In Kotlin, in an extension of the previous example, you can create a Point class with just one line of code:

  • That is it! This will create a Point class, with a two argument constructor (x and y), and two immutable (because of val) properties, also x and y
  • This could be used in a main() function as follows:

  • Much more concise code!

Exercises

1. Hello World.

We are going to be using IntelliJ IDEA. Create a new project in IDEA by selecting New Project. Ensure that you un-select "Add sample code"; this creates auto-generated code but we will be starting from scratch.

Creating an IDEA Kotlin project

The project will be initialised and you will end up with an empty project, as follows:

Empty IDEA project

You then need to add a new file to the project. Do this by right clicking on src and then selecting New Kotlin Class/File, as shown below:

Adding a new Kotlin file

Then select File (not Class) and choose a filename: Main.kt is standard.

Adding a new Kotlin file - specifying filename

You will then end up with an editing area for your source code on the right of the screen, alongside the project structure. Add "hello world" there:

Adding Hello World code

Then, compile and run the Kotlin program by clicking on the green "Run" icon as shown above. The output will appear in the "Run" console output window at the bottom of the screen.

IDEA project: console output

2. Loops and ifs – basic

  • Create a new program, ex2.kt. The program should ask the user to input their name and then should print it 10 times.
  • Modify the program so that it also asks how many times they want to display their name. Hint! You can use the toInt() method of String to convert a string to an integer.

3. Using an array

  • Create a new program which stores an array of your favourite music artists. The program should prompt the user to ask the user to input an artist name. Use a loop to keep prompting the user until they guess one of the correct artists. Hint: you can use while with in to achieve this.

4. when

Write a program which uses a "when" statement to print the grade (A, B, C, D, E, or F) equivalent to a given percentage. Make the 'when' act as an expression, i.e. you should get it to return a value and then print that value. Grades are as follows:

The program should also display "Error - invalid percentage" if the percentage is below 0 or greater than 100. Hint: you can use the "in" keyword with a range (e.g. 1..10) as a condition inside "when".

5. Classes and objects, including using Git branching

This exercise will also allow you to get some initial experience of Git branching.

Git repositories can contain multiple branches, all with the project code in a different state. Why is this important? The main branch of a repository (usually called main or master) represents stable, well-tested code which is production-ready: ready for people to use. Commits to the main branch would normally just be bug-fixes. However, frequently developers wish to add new and experimental features to a project which require extensive testing, and which might break the main branch and make the project unstable and unusable until such testing has been completed. For this reason, developers frequently create additional branches to add these new, experimental features. Often a separate branch for each feature (called a feature branch) is created. Each feature is tested in isolation. Once this has been done, a common strategy is then to merge each feature branch into a development branch (frequently called dev) which will contain all new and experimental features. This can be further tested before similarly merging the code to the main branch.

This typical branching workflow is shown below:
Typical Git branching workflow

Another use of branches, and one which you will make use of in the Group Project, is to allow multiple developers in a team to work independently on a project. Each team member can create their own branch and work on their assigned features independently, without breaking the main branch. Again, individual team member branches can be merged into a dev branch and tested, and when all is working, the dev branch can be merged into the main branch.

A more detailed article on Git branching and merging is available on the Git website. Git merging is a bit more complex than basic branching (due to the possibility of conflicts), so will be considered later in the module.

Setting up a dev branch of the Cat project

  • Login to GitHub and make a fork of this repository containing the cat app above:

  • Change directory to IdeaProjects from within Git Bash:

  • Clone your fork from Git Bash:

    You should see output like that below:
    Cloning a Git repository from Git Bash

  • Change directory into your cloned repository:

  • Create a separate branch called dev. This is a dev branch which will contain new features.

    This will also make the new branch the active (current) branch.

  • Open the project in IntelliJ IDEA, but keep Git Bash open.

Working on the Cat app, committing changes, and pushing to GitHub

  • In IntelliJ, add an additional method to Cat called eat(). The eat() method should add one to the weight of the cat. Ensure you save the work afterwards.
  • Make a commit from Git Bash (we will be using Git Bash to commit and push as it's simpler than via IDEA):

  • Extend the code in the main() to create two Cats with these attributes:

    Make Flathead eat twice, and display Flathead's details. Make Cupra walk four times, and display Cupra's details.

  • Again commit your changes.
  • You'll note that the weight is currently not returned in the toString(). Change the toString() so that the weight is included in the string returned.
  • Commit your changes once more.
  • Finally push your dev branch to GitHub. This will setup a remote dev branch on GitHub which will be separate from the main branch. Note that you will need to login to GitHub to do this. When you enter:

    a login window will pop up, like this:
    Logging into GitHub from Git Bash
    Click "Sign in with your browser."

  • Log in with your browser, as below:
    Logging into GitHub from the browser within Git Bash
  • You will probably then be sent a verification code to your GitHub registered email address. You will see the screen below:
    GitHub login - verification code
    Check your email and enter the code you receive in the text field as directed.
  • Finally you will see this window to confirm the authentication has succeeded:
    GitHub login - authentication succeeded
    As directed, you can now close the browser tab, and Git Bash should report that the push completed successfully.

6. Another object-oriented app

  • Create a completely new project called StudentApp and add a class called Student to represent a student, as well as a main(). Give it the following attributes:

    • id, representing the student's ID (String)
    • name, representing the student's name (String)
    • course, representing the student's course. (String)
    • mark, the student's mark (Double).

    Also give it:

    • A constructor, which initialises the four attributes.
    • toString() should return the details of the student (name, course and mark).
  • Add a loop to your main(). In the loop, read in student details from the keyboard and use them to create a Student object each time the loop runs. Then, still within the loop, display each student. The loop should quit when the user enters quit for the student name.
  • Create a repository on GitHub for the project. Ensure it's completely empty; do not add a .gitignore, README or license.
  • Create a Git repository for the project and push it to GitHub. To do this you will need to initialise a Git repo within the project folder from Git Bash, e.g.

    You should know how to do this from last year; ask me if you get stuck. Note that

    changes your main branch from master to main.