List Adapters
A ListAdapter
is a tool in the game's UI system that allows you to display a list of widgets based on a common data model. For example, the System Settings category list and the settings themselves are both ListAdapter
s.
You can use a ListAdapter
to display any kind of data inside any kind of widget deriving from ContainerWidget
. If you are familiar with Optimized ScrollView Adapter, the API is very similar. However, a ListAdapter
doesn't need to be a scroll view.
Creating your own ListAdapter
Let's create a ListAdapter
that can display a list of cracked user credentials.
First, create the data model itself
public struct CrackedPassword
{
public string FullName;
public string UserName;
public string Password;
}
And its list - it can be anything implementing Ienumerable<CrackedPassword>
:
var data = new CrackedPassword[] {
new CrackedPassword { FullName = "Brodie Robertson", UserName = "brodieonlinux", Password = "wayland shill" },
new CrackedPassword { FullName = "Ritchie Frodomar", UserName = "ritchie", Password = "strong and complicated password" },
new CrackedPassword { FullName = "TheEvilSkeleton", UserName = "tesk", Password = "bottleofwine" }
};
Next, create a Widget
class that can display a CrackedPassword
:
public sealed class CrackedPasswordView : Widget
{
private readonly StackPanel stack = new StackPanel();
private readonly TextWidget fullName = new TextWidget();
private readonly TextWidget username = new TextWidget();
private readonly TextWidget password = new TextWidget();
public CrackedPasswordView()
{
Children.Add(stack);
stack.ChildWidgets.Add(fullName);
stack.ChildWidgets.Add(username);
stack.ChildWidgets.Add(password);
}
public void UpdateView(CrackedPassword data)
{
fullName.Text = data.FullName;
username.Text = data.UserName;
password.Text = data.Password;
}
}
The important part is the UpdateModel(CrackedPassword)
method - this is what you will call when it's time to update a widget with new data.
Now that we've defined the widget, it's time to define its ViewHolder
. A ViewHolder
acts as the "slot" for the widget type we just created, within the ListAdapter
. There will be a ViewHolder
for each element in the data source.
public sealed class CrackedPasswordViewHolder : ViewHolder
{
private readonly CrackedPasswordView view = new();
public CrackedPasswordViewHolder(int itemIndex, Box root) : base(itemIndex, root)
{
root.Content = view;
}
public void UpdateView(CrackedPassword data)
{
view.UpdateView(data);
}
}
Most ViewHolder
objects will look identical to the code above. All a ViewHolder
must do is create an instance of the view widget, assign it as the Content
of a "root" widget, and expose any API of the view widget needed by your ListAdapter
class.
We now have everything we need to create the ListAdapter
itself. We will display our data inside a StackPanel
.
public sealed class CrackedPasswordList : ListAdapter<StackPanel, CrackedPasswordViewHolder>
{
}
The first type parameter of ListAdapter
declares the type of ContainerWidget
we want to display all of our list items in, in this case StackPanel
. The second parameter declares the ViewHolder
-deriving type we just created.
We must override two abstract methods to tell the ListAdapter
how to interact with our data - TViewHolder CreateViewHolder(int, Box)
and void pdateViewHolder(TViewHolder)
.
The CreateViewHolder
method just constructs a new ViewHolder
instance of the required type and returns it. This method is called when a new view widget needs to be created. We are given the item index and root widget of the new view, so we can just pass them to the constructor of CrackedPasswordViewHolder
.
public override CrackedPasswordViewHolder CreateViewHolder(int itemIndex, Box rootWidget)
{
return new CrackedPasswordViewHolder(itemIndex, rootWidget);
}
UpdateViewHolder()
is called every time the data of a list item has changed. This method receives the item's view holder, and must retrieve the new data and update the view with it.
protected override void UpdateViewHolder(CrackedPasswordViewHolder viewHolder)
{
CrackedPassword item = items[viewHolder.ItemIndex];
viewHolder.UpdateView(item);
}
The only problem we need to solve now is telling ListAdapter
when our data changes, and being able to access it. This is what the DataHelper<T>
class is for.
In the CrackedPasswordListAdapter
, add a readonly field items
of type DataHelper<CrackedPassword>
and construct it.
private readonly DataHelper<CrackedPassword> items;
public CrackedPasswordList()
{
items = new DataHelper<CrackedPassword>(this);
}
We can now populate the ListAdapter with data by calling items.SetItems()
.
var data = new CrackedPassword[] {
new CrackedPassword { FullName = "Brodie Robertson", UserName = "brodieonlinux", Password = "wayland shill" },
new CrackedPassword { FullName = "Ritchie Frodomar", UserName = "ritchie", Password = "strong and complicated password" },
new CrackedPassword { FullName = "TheEvilSkeleton", UserName = "tesk", Password = "bottleofwine" }
};
items.SetItems(data);
Examples of ListAdapter used in-game
System Settings
- the sidebar listing all settings categories
- the scroll view of the active category, where all of its settings widgets are shown
Desktop
- the Info Panel (mission objectives and notifications)
- the Dock (icon groups)
- the Application Launcher (the grid view of icons)
That's by no means exhaustive, ListAdapter is used everywhere.
Performance concerns
ListAdapter tries its best to keep performance issues at bay, but it needs to work with what it's given. You should keep a few things in mind.
Widget recycling
When items are removed from a ListADapter, their view widgets are recycled. When new items are added to the list adapter, widgets are pulled from the recycle bin first. However, this does not extend to the child of the view widget. This is because ListAdapter can't guess how your custom widget handles state or memory.
If you'd like to allow ListADapter
to recycle your custom widget, you will need to do some extra work.
Inside your ViewHolder
constructor, replace:
view = new CrackedPasswordView();
// WITH
view = RecycleBin.Get<CrackedPasswordView>().GetWidget();
This allows you to retrieve your CrackedPasswordView
instance from the recycle bin. If there are no recyclable instances, a new instance will be created. This means you can only use the recycle bin if your widget has a public parameterless constructor.
Next, inside your ViewHolder
class, add:
public void Recycle()
{
// NOTE: You cannot recycle a toplevel widget OR a widget that's added as a child to another.
Root.Content = null;
RecycleBin.Get<CrackedPasswordView>().Recycle(view);
}
This method puts your CrackedPasswordView
instance in the recycle bin. You call this method when a list item is being removed from a ListAdapter
. To get that working, override the following method in your ListAdapter
class.
protected override void BeforeRemoveItem(CrackedPasswordViewHolder holder)
{
holder.Recycle();
}
⚠️ Warning
Before recycling a widget, make sure you remove it from its parent and unbind all event/callback delegates. You do not need to release any unmanaged resources, since the idea is to re-use them when the widget itself is re-used. Recycling is not the same as disposal.
*️⃣ Note
The Recycle Bin is a shared resource. When calling
RecycleBin.Get<T>()
, you are retrieving the shared recycle bin for that exact type of widget. You are not retrieving a new recycle bin instance unless that widget type has never been recycled before.
Layout and geometry updates
When data in a ListAdapter
changes, so too shall its widget layout and geometry. Only notify ListAdapter
of data changes when you know the data has changed.