MAD Topic 5 : Box Layouts; Kotlin Getters and Setters; Introduction to Delegates

Important! This week has been deliberately kept "light" in order to allow you to catch up. We will cover box layouts in Jetpack Compose, and one or two outstanding general Kotlin topics.

Box Layouts

Imagine you want to display a map above a row containing text fields and a button to set the location. If you think about it, this isn't so easy. If you specify the modifier for the map to be Modifier.fillMaxSize(), it will occupy the whole of the screen and there will be no room for the row. If you add the row first, it will appear on top of the screen.

What we need is another type of layout which allows us to specify exactly where each composable will be positioned, so we can position a composable at the top, the bottom, the left or the right. The Box composable allows us to do this.

In a Box, we can specify the alignment of the composables within the box. Here is an example:

Box(Modifier.fillMaxSize()) {
    Button(
        onClick = { }, modifier =
            Modifier.align(Alignment.BottomStart)
        ) { Text("Bottom Start") }

    Button(
        onClick = { }, modifier =
            Modifier.align(Alignment.BottomCenter)
        ) { Text("Bottom") }

    Button(
        onClick = { }, modifier =
            Modifier.align(Alignment.BottomEnd)
    ) { Text("Bottom End") }


    Button(
        onClick = { }, modifier =
            Modifier.align(Alignment.CenterStart)
    ) { Text("Centre Start") }

    Button(
        onClick = { }, modifier =
            Modifier.align(Alignment.Center)
    ) { Text("Centre") }

    Button(
        onClick = { }, modifier =
            Modifier.align(Alignment.CenterEnd)
    ) { Text("Centre End") }

    Button(
        onClick = { }, modifier =
            Modifier.align(Alignment.TopStart)
    ) { Text("Top Start") }

    Button(
        onClick = { }, modifier =
            Modifier.align(Alignment.TopCenter)
    ) { Text("Top") }

    Button(
        onClick = { }, modifier =
            Modifier.align(Alignment.TopEnd)
    ) { Text("Top End") }
}

This layout would look something like this:
Box Layout
Note how we use the align() method of our Modifier for each button, to specify where it is aligned within the box. The possible values include:

BoxWithConstraints

The BoxWithConstraints is an enhanced Box which allows us to obtain the Box's width and height from within it. Why is this useful? It makes it easier to create layouts which adapt to the screen size. For example, in our mapping application we might want to set the height of the row containing the text fields and buttons to 80dp. How big must the map be in this case? You want it to occupy the remainder of the screen. To calculate this, you subtract the height of the row (80dp) from the total height. Inside a BoxWithConstraints, you can use:

These give a value in dp so you can subtract the height of the controls row, also in dp, to obtain a height in dp for the map. You can then call Modifier.height() to set the height of the map to this calculated height.

Further general Kotlin features

We will use this opportunity to introduce one or two further general Kotlin features which you may find useful.

Making our code more concise with Kotlin getters and setters

Kotlin features a more concise and flexible approach to getters and setters compared to Java. Rather than using explicit getter and setter methods, Kotlin's approach is to define properties (attributes) and then define how each property can be read and written.

For example, imagine a Time class in which you wanted to ensure that incorrect times could not be set. What you could do is define two properties hours and minutes, but write custom setters which prevent the hours and minutes being set to incorrect values. write access:

class Time  {
    var hours: Int = 0
        set(newHours: Int) {
            if(newHours in 0..23) {
                field = newHours
            }
        }
    var minutes: Int = 0
        set(newMinutes: Int) {
            if(newMinutes in 0..59) {
                field = newMinutes
            }
        }

    override fun toString(): String {
        return "$hours:$minutes"
    }
}

fun main() {
    val t = Time()
    t.hours = 23
    t.minutes = 48
    println(t)
    t.hours = 24
    println(t)
    t.minutes = 60
    println(t)
}
Note how we declare two properties, hours and minutes and give each a default value of 0. However, note how we then define a set() method, which is indented one level from the property. This is a custom setter, which controls how the property can be updated. Each custom setter takes a parameter representing the new value, and then checks it to ensure that it's within the correct range. If it is, we set the underlying field (the actual value of the property) to the new value. Note that field is a keyword in Kotlin, which represents the underlying value of a property.

The main() demonstrates the use of the custom setters. We create a Time object then set its hours and minutes to a sensible value (23 hours and 48 minutes). We then try to set the hours to 24 (invalid) and print the time object, and set the minutes to 60 (invalid) an print the time object again. The output will be as follows:

23:48
23:48
23:48
So you can see that the custom setters, with validation, have prevented the Time object being set to an invalid time.

Private setters

In some circumstances we might want to prevent a property from being updated from outside the class. An example might be a student ID in a Student class, as this should never change. We could just make it a private property, but the outside world would then not be able to read it. What we want is for the outside world to be able to read the student ID, but not change it. We can use a private setter for this:

class Student(studentIdIn: String, var name: String, var course: String) {
    var id = studentIdIn
        private set

    override fun toString(): String {
        return "$studentId: $name, studying $course"
    }
}

fun main() {
    var student = Student("1smitj01", "James Smith", "Web Design")
    student.course = "Software Engineering" // OK
    println(student.id) // OK - can read student ID from outside
    student.id="2smitj02" // cannot modify ID from outside due to private setter
}

Delegated properties

You will have seen this syntax when declaring state variables in Compose:

var name: String by remember { mutableStateOf("") }
What exactly does the by remember mean? It's a Kotlin feature known as delegated properties. Delegated properties, like custom getters and setters, allow you to customise what happens when you retrieve or update a property (attribute) of an object. They do it with a delegate class; see the documentation for details. So delegates are rather like custom getters and setters, but rather than simply customising the getter and setter, you delegate the job of getting and setting a property to an entirely separate class - the delegate - which can give more flexibility in some situations. The by keyword specifies that we will be using a delegate. In the case of state variables, remember is the delegate.

Here is an example:

package com.example.delegates1

import kotlin.reflect.KProperty

class Student (var name: String, var course: String) {
    var mark: Int by MarkDelegate()

    override fun toString(): String {
        return "Student $name on $course with mark $mark"
    }
}

class MarkDelegate(private var controlledProperty: Int = 0) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>) : Int{
        return controlledProperty
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: Int) {
        if(newValue in 0..100) {
           controlledProperty = newValue
        } else {
            println("Cannot set ${property.name} to $newValue for $thisRef")
        }
    }
}


fun main (args: Array) {
    val john = Student("John", "Software Engineering")
    john.mark = 101
    println(john)
    john.mark = 83
    println(john)
    john.mark = -1
    println(john)
}
Note how we are declaring a property mark inside the Student class but declaring it with by MarkDelegate(). This means that the MarkDelegate class will be responsible for handling retrieving the value of, and setting the value of, the mark property. MarkDelegate is thus a delegate. A delegate should contain two methods, getValue() and setValue(), which retrieve and update the associated value. Note also how the delegate class takes the property that it's controlling as a parameter:
class Mark(private var controlledProperty: Int=0)
If you look at getValue(), it's just returning the mark, so no custom behaviour occurs if we simply retrieve the value (e.g. by printing it). However setValue() is more interesting. Note how it take the new value as a parameter (newValue). The setValue() checks that the new value is in the range 0 to 100, and only updates the mark to the value if it is. So in other words, marks below 0 or above 100 will be rejected. So if we try to set the mark to any value less than 0 or greater than 100, it will not be updated. Note that the thisRef parameter in setValue() and getValue() refers to the object that the delegated property belongs to. Also the property parameter represents the property being controlled, and the property's name can be retrieved with property.name. Therefore the output of this program will be as follows:
Cannot set mark to 101 for Student John on course Software Engineering with mark 0
Student John on course Software Engineering with mark 0
Student John on course Software Engineering with mark 83
Cannot set mark to -1 for Student John on course Software Engineering with mark 83
Student John on course Software Engineering with mark 83
When we try to set the mark to 101 or -1, it's rejected. However when we set the mark to 83, it's accepted.

Note that you can pass additional parameters to the delegate, but the first parameter always reference the property that the delegate is controlling.

Exercises

  1. Ensure that you catch up with unfinished work.
  2. Try altering the layout of your mapping application so that the row containing the text fields and buttons to allow the user to enter a latitude and longitude is at the bottom of the screen. The intended layout is as follows:
    Map layout with controls at bottom