lilt: Lightweight Interactive Learning Tool

Topic 8

Further Compose

Further introductory Compose topics

Modifiers

We can modify the appearance of UI elements with modifiers which allow you to control such things as padding, borders, etc. Modifiers are optional in some cases but compulsory in others. Modifiers are compulsory in the case of the Spacer, which is an element used to provide space between other elements. Here is an example with a Spacer:

This creates a spacer with a height of 32 density independent pixels (see above).

Other modifiers allow us to specify padding (the space between the border of a UI element and its content) or an element's border. For example, this surrounds a GreetingComponent component with a 2 dp wide blue border and with padding of 16dp between the border and the content. Note how Modifier contains many methods to modify different aspects of the element, and note how they can all be chained together.

Our GreetingComponent would now need to take a modifier as a parameter. This would then be passed onto the Column which sets the layout within the greeting component.

Implementing Lists of Data

You may be able to figure out how to create a list of data. Think about how you would create a Compose application which implements a shopping list. It should contain a TextField allowing the user to enter an item, a Button which when pressed adds the item to the list, and then, below that, the shopping list itself should be displayed, with each item on a separate line. There should also be a "Clear" button which clears the list. How might you implement this?

Creating Lists

You would implement a list by storing a list of data in state. You might think you could do something like this:

Note how we have a text field which allows you to enter a shopping list item, which is stored in the currentItem state variable. When the button is clicked, the current item is added to the list. As it's a mutable list, you probably expect it to work.

Exercise 1

This exercise asks you to consider the code above.
  • Do you think the code will work as expected? Explain your answer.
Submission disabled now you have completed or the notes have been made public.

Answer to exercise 1

It does not work as expected. The reason is that composables are only re-rendered if the state variables change. Here, when we add a new item to the list, the list becomes one element longer but the actual list variable is the same variable, referring to the same location in memory.

To trigger a re-render when a new list item is added, we can use SnapshotStateList to store a list in state, using mutableStateListOf() rather than mutableStateOf(). This will automatically trigger a state change and thus a recomposition if you change the list (e.g. add new elements).

This is an example of using a SnapshotStateList:

Lazy Lists

One issue with a long list of items is that by default, a long list consisting of a series of Text items in a Column will not be memory efficient. Why? Let's say there are 100 items in the list, but only 10 are visible on the screen at any time. The items off the screen are still being rendered, even though they are invisible. This is clearly inefficient.

We can solve this problem through the use of lazy columns. The LazyColumn is designed to hold a series of items (i.e. a list) but is implemented with memory optimisation so that only for the items currently visible are rendered.

Creating a LazyColumn is quite straightforward, you place it in the appropriate place in your layout and then specify a lambda to control how it works. This lambda takes an object of type LazyListScope as its single parameter and this object includes an items method to specify a list of items. For example:

Note how items() takes the list of items to render as its first parameter and another lambda as its last parameter. This lambda specifies how each item in the list of data should be transformed into a Compose element. So here, each item in the list is transformed to a Text element containing its details.

Callback functions as composable parameters

Imagine a situation in which you are handling multiple items of data, such as multiple songs or multiple student records. You might want to store the data in state in the top-level App composable. However, what if, for reusability purposes, you have separate, child composables for adding an item of data, and searching for data? How can the child composables access the list stored in state if it's stored in the App?

The answer is to use a callback. A callback is a reference to a function which can be called at some future point in time. What we do is we pass in this function reference into our child composables - and then call them as soon as the user has entered the required information (e.g. a search term or a new item) - probably when the user clicks a button.

We looked at how to pass functions into other functions as parameters in Topic 6 and now we will see a real-world use for it. Imagine this is part of an application to allow users to search for music:

Note the following:

  • Note how both AddSong() and SearchForSongs() take a callback function as a parameter. For AddSong(), this is of type

    As we saw last week, this represents a function which takes a Song as a parameter and returns nothing. So the callback function passed in to AddSong must also be a function which takes a Song and returns nothing. Likewise, for SearchForSongs(), the parameter is of type

    so must take a string as a parameter and return nothing. In both cases, lambda functions will be acceptable to pass in.

  • Note how both AddSong() and SearchForSongs() perform an action when the user clicks the button. In the former, we create a Song object using the contents of the two text fields (also stored as state) and then call the callback which was passed in, supplying the Song as an argument. Similarly, in SearchForSongs() we call the callback, supplying the search term (a string) as an argument.
  • In our App, when we create our two child composables AddSong and SearchForSong, note how we supply a lambda function for the callback argument. As a result, the lambda function passed in will be called when the user presses the buttons in the two child composables. As the lambdas are within App they have access to the list stored in state, and so can change it or call its methods.
  • When we search for a song we get back a list of matching songs. But because the songs are an ordinary list, not a SnapshotStateList, we cannot simply set the SnapshotStateList to the list of matching songs. Instead, we have to clear the SnapshotStateList and then use addAll() to add all the matching songs to it.
  • DisplaySongs takes a plain List as an argument, and displays the list using this. To obtain a plain List from a SnapshotStateList we use the toList() method.

Selectable dropdown lists

In Compose, a selectable dropdown list can be created via the DropdownMenu composable. This is a general-use composable which can also be used to implement popup menus, but we are going to use it by creating the equivalent of a select box in HTML. A DropdownMenu can be attached to any kind of composable with an onClick event; in our case we are going to attach it to a button. This kind of UI element was also known as a list box or combo box in some older UI libraries.

Here is an example of a composable implementing a dropdown list:

This would be called by passing in a list of strings, and an onItemSelected callback, e.g.

How does it work?

  • A state variable dropDownVisible is used to determine whether the dropdown is currently visible or not.
  • There is a standard Button. When the user clicks this, the dropDownVisible state variable toggles between true and false - this will result in the dropdown becoming visible or invisible.
  • We then specify the actual DropdownMenu itself. This takes as arguments:
    • expanded: This determines whether to "expand" the dropdown menu by making it visible. This is set equal to our dropDownVisible state variable: consequently, changing the value of the state variable will show or hide the dropdown menu as appropriate.
    • onDismissRequest: this runs when the user tries to dismiss the dropdown menu by clicking outside it. By default this does nothing, so if we want to hide the dropdown, we must implement a lambda which sets dropDownVisible to false.
  • The final argument of DropdownMenu is a lambda containing its child elements, as always in Compose. We use a forEach to loop through each item in the list items and create a DropdownMenuItem for each one. This takes a text argument (the text of the dropdown) and an onClick event handler. For the latter, we call the callback we passed into our DropdownList composable, passing in the current item as an argument. This can then be retrieved in the onItemSelected callback.

Exercise

Complete last week's exercise!

First, ensure you have completed all the work from last week, including the login exercise (Question 5).

Tasks

This exercise allows you to add a GUI to the University project from earlier in the module. You can use the University, Student, Undergraduate and Masters classes from the Week 3 pre-prepared solution as part of your code:

but do not use the whole project as it is not a Compose project. Instead, just copy the classes mentioned above into the new Compose project you create for this exercise.

  1. Create an App composable containing a SnapshotStateList of Student objects (see the discussion in mutableStateListOf(), above)
  2. Create a series of TextFields within your App composable to allow the user to enter the student ID, name, course and mark. There should also be a button, which, when clicked, creates a Student object and adds it to the list. You'll need three state variables for the current ID, name and course (see the discussion on TextFields last week).
  3. Create a separate composable called StudentList and in it, display the entire contents of the student list. The composable will need a list of students as a parameter, and you'll need to pass this in from the App composable.
  4. Try it out; if it works, the StudentList composable should update each time the user enters a new student.
  5. Now try moving the "add student" functionality into its own composable, AddStudent. This should contain the three TextFields and the button as well as the three state variables for the ID, name and course. Assume the student is an undergraduate for now. When the button is clicked, it should create a new student and pass it back to the App via a callback. In App, write the callback as a lambda function so that the Student parameter is added to the list of students.
  6. Now try rewriting the application using the University class. Create a copy of your project, so you still have your old code. You will need to make some changes due to the fact that the list of students is contained within the university.
    • The App should contain a University object as a regular variable - not a state variable. The University itself will never change, just the student list within it.
    • The App should maintain a state variable containing the current list of students to be displayed. This should be a SnapshotStateList.
    • When a user adds a student, add it to the university. Then, retrieve the list of students from the university and update the state variable so it contains a copy of this list (you'll need a copy to ensure the state updates). To do this use clear() and addAll(), e.g.:

    • Implement search-by-course functionality by adding a new composable SearchByCourse which should contain a Text label reading "Enter course", a TextField and a Button. When the user clicks the button the course the user entered in the text field should be sent back to the App using a callback. Then, in the lambda function in the App used as the callback, it should find all students on that course and update the list state variable to the results, so that only students on the given course are displayed on the UI.

  7. Allow the user to choose between undergraduate and masters, using a DropdownMenu attached to a button. The easiest way of doing this is to create a separate composable specifically for the dropdown list, as in the example above.