Lab 2
Introduction
In this lab we will try our first steps with the Kotlin language and add basic interactive behaviour, such as click handling, to our UI.
- Make sure you have an understanding of these Kotlin concepts, discussed during the lecture:
- Mutable and immutable variable declaration (var vs val)
- Null-safety, including safe calls and the !! operator
- Inferred types
Exercise 1: In-code GUI creation
Let's try to define a basic UI Layout programmatically, in Kotlin, without XML.
In MainActivity.kt onCreate():
- Create an instance of the LinearLayout class. The primary constructor expects a Context type argument, you can use the this keyword (which points to the current Activity instance).
- Set it to be vertical by updating the value of its' orientation property to
LinearLayout.VERTICAL
- The expected value is a constant static property of the LinearLayout class. Use Android Studio's autocomplete (ctrl + space) or study the docs to see other constants of LinearLayout.
- In this case, we need to set the orientation property to the value LinearLayout.
- Set it to be vertical by updating the value of its' orientation property to
- Let's add 10 buttons to the LinearLayout using a loop.( Feel free to try out different ways to achieve this with Kotlin, e.g. with for loops or using repeat )
- In the loop, create a new Button instance, and use LinearLayout-s
addView()
function to add it to the layout.
- In the loop, create a new Button instance, and use LinearLayout-s
- Make the Activity display the LinearLayout object instead of the default XML resource (using
setContentView(..)
).- Test to see if the 10 buttons appear.
- Update the loop so that the attributes of each button are modified:
- Set the text of each one to "Button X", where X is an integer representing current iteration number within the loop.
- If you use Kotlin's repeat, you can get the iteration number using the
it
keyword within the loop.
- If you use Kotlin's repeat, you can get the iteration number using the
- Set the background of the buttons to different colours.
- You can update the color with
setBackgroundColor( .. )
, where the argument is a static constant of Color class, e.g. Color.BLUE - Use Kotlin's "when" statement and the iteration number to switch between colours. For example, if you used "repeat" for the loop:
- You can update the color with
repeat (10) { // ... code to create button // "it" refers to the iteration number of the "repeat" statement when (it % 3){ 0 -> // set color 1 1 -> // set color 2 2 -> // set color 3 }
- Set the text of each one to "Button X", where X is an integer representing current iteration number within the loop.
- Try to refactor your code and use Kotlin scope functions (let or apply )
- E.g. if you use apply:
myLinearLayout.addView( TextView(this).apply { // inside apply, "this" refers to the TextView text = "My button's text" } // apply returns the object it was called on )
- Optional task: Try to use colour from your project resources (from colors.xml) instead of the Color class constants. You can do this with
getColor( R.color.some_color_id )
for example.
Exercise 2: ConstraintLayout practice
Change back your MainActivity so that it uses the XML resource activity definition. Create the following ConstraintLayout configuration:
- Divide the UI vertically into 2 (2/3 and 1/3, as shown on picture), using a Guideline in percentage mode
- Note: When you drop a Guideline from the Palette, by default it's not in percentage mode, toggle the mode by clicking on the circle next to the GuideLine, or change the attribute
layout_constraintGuide_XXXX
, where XXXX is "begin"/"end" or "percent".
- Note: When you drop a Guideline from the Palette, by default it's not in percentage mode, toggle the mode by clicking on the circle next to the GuideLine, or change the attribute
- Top should have 1 TextView text
- Vertically centered w.r.t. guideline
- Horizontally constrained to edges of layout
- Add 2 Buttons to the bottom
- Vertically centered
- Horizontally constrained to parent edges
Exercise 3: Click behaviour
- Set up a click handler for both buttons (see below for more details)
- For the 1st button, use the programmatic approach and make the click update the button to show the number of times it has been clicked. Each time it gets clicked, it should also add a word to the TextView (hint: use TextView.append(..) or Kotlin repeat).
- For the 2nd button, use
XML onClick
and make it restart the counter. (Also try clearing the TextView)
You can achieve this based on the below tips:
Getting a reference to a UI element defined in XML
- you can access UI View Objects directly using its ID: e.g. if your XML object has an ID of
myButton
, use the findViewById() method, to get View objects that have been declared in XML resources or created in code.val b:Button = findViewById(R.id.myButton)
Setting click listeners
- To set a click listener from the code, you can use Button.setOnClickListener( .. )
button1.setOnClickListener { view: View? -> Log.i(TAG, "View: ${view?.id} was clicked!") }
- Alternatively, you can specify android:onClick attribute of the XML object definition, setting its value to the name of the method to be called
- The method has to be defined in the Activities' .kt file and accept a single argument of type View:
fun myHandler(view: View) { /** react to click */ }
- The method has to be defined in the Activities' .kt file and accept a single argument of type View:
Exercise 4: Touch listener
Let's add more fine-grained control of button presses using a touch listener. With a touch listener, we can differentiate the moment when the button is pressed down and when it is released.
The basic code for attaching a touch listener looks like this:
my_button.setOnTouchListener({ view, motionEvent -> // do something useful true // return value - whether this listener consumed the event })
In the above, the return value indicates whether or not this listener consumed the event (if you set it false and also have a click listener for the same button, the click listener also gets triggered, whereas if the touch listener consumes the event, the click listener won't get triggered).
the motionEvent
can be used to determine which kind of touch event it is.
Let's handle two kinds of events: DOWN and UP using Kotlin's when construct:
when (motionEvent.action){ MotionEvent.ACTION_UP -> { /** TODO */ } MotionEvent.ACTION_DOWN -> { /** TODO */ } else -> { /** something else happened */} }
- You may see a warning about not calling performClick(), you can ignore it for this lab, but here is a good explanation of the warning.
- Replace the TextView in your ConstraintLayout with a vertical LinearLayout (in the XML layout file).
- Using the onTouchListener, record how long the button was held down in milliseconds (use
System.currentTimeMillis()
) - Whenever the button is released and you have the press duration, add a new TextView to the LinearLayout
- Make the TextView display the duration
- To empty the LinearLayout (for the reset button), you can use
my_layout.removeAllViews()
Exercise 5: Enabling View Binding
It can become bothersome and inefficient to manage UI manipulation using findViewById(..). For this purpose, View Binding is a technique which automatically generates classes representing XML-defined layouts, and provides references to the objects therein.
https://developer.android.com/topic/libraries/view-binding
By default, View Binding isn't enabled. Let's add it to our project:
Update your module level build.gradle
file, by adding the below buildFeatures part:
android { ... buildFeatures { viewBinding true } }
In MainActivity.kt, let's initialize the Binding object. Since we enabled View Binding in the Gradle file, a class is automatically created for each Activity. In our case, the class ActivityMainBinding has been generated . Let's use it in the code:
// Declare variable for storing the binding private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) // binding.root refers to the root Layout ... // we can access UI elements from the binding, e.g. binding.myButton.text = "Button" }
Note: the lateinit
is a special Kotlin keyword which alllows us to have a non-nullable type variable, whose initial value is undefined. We define it later, in onCreate, but thanks to lateinit, the type is : ActivityMainBinding and not : ActivityMainBinding?