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
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()andSearchForSongs()take a callback function as a parameter. ForAddSong(), this is of typeAs we saw last week, this represents a function which takes a
Songas a parameter and returns nothing. So the callback function passed in toAddSongmust also be a function which takes aSongand returns nothing. Likewise, forSearchForSongs(), the parameter is of typeso 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()andSearchForSongs()perform an action when the user clicks the button. In the former, we create aSongobject using the contents of the two text fields (also stored as state) and then call the callback which was passed in, supplying theSongas an argument. Similarly, inSearchForSongs()we call the callback, supplying the search term (a string) as an argument. - In our
App, when we create our two child composablesAddSongandSearchForSong, 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 withinAppthey 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 theSnapshotStateListto the list of matching songs. Instead, we have to clear theSnapshotStateListand then useaddAll()to add all the matching songs to it. DisplaySongstakes a plainListas an argument, and displays the list using this. To obtain a plainListfrom aSnapshotStateListwe use thetoList()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
dropDownVisibleis used to determine whether the dropdown is currently visible or not. - There is a standard
Button. When the user clicks this, thedropDownVisiblestate variable toggles between true and false - this will result in the dropdown becoming visible or invisible. - We then specify the actual
DropdownMenuitself. This takes as arguments:expanded: This determines whether to "expand" the dropdown menu by making it visible. This is set equal to ourdropDownVisiblestate 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 setsdropDownVisibletofalse.
- The final argument of
DropdownMenuis a lambda containing its child elements, as always in Compose. We use aforEachto loop through each item in the listitemsand create aDropdownMenuItemfor each one. This takes atextargument (the text of the dropdown) and anonClickevent handler. For the latter, we call the callback we passed into ourDropdownListcomposable, passing in the current item as an argument. This can then be retrieved in theonItemSelectedcallback.
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.
- Create an
Appcomposable containing aSnapshotStateListofStudentobjects (see the discussion inmutableStateListOf(), above) - Create a series of
TextFields within yourAppcomposable to allow the user to enter the student ID, name, course and mark. There should also be a button, which, when clicked, creates aStudentobject and adds it to the list. You'll need three state variables for the current ID, name and course (see the discussion onTextFieldslast week). - Create a separate composable called
StudentListand 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 theAppcomposable. - Try it out; if it works, the
StudentListcomposable should update each time the user enters a new student. - 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 theAppvia a callback. InApp, write the callback as a lambda function so that the Student parameter is added to the list of students. - Now try rewriting the application using the
Universityclass. 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
Appshould contain aUniversityobject as a regular variable - not a state variable. TheUniversityitself will never change, just the student list within it. - The
Appshould maintain a state variable containing the current list of students to be displayed. This should be aSnapshotStateList. 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()andaddAll(), e.g.:Implement search-by-course functionality by adding a new composable
SearchByCoursewhich 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 theAppusing a callback. Then, in the lambda function in theAppused 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.
- The
- Allow the user to choose between undergraduate and masters, using a
DropdownMenuattached 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.