Post
The blog is a work-in-progress started on Oct 8 '25, expected to last a few weeks

OSA Demo: Grid Layout with Async Item Download

Implement efficient grid layouts with OSA. This demo covers horizontal grids and asynchronous item loading for optimal performance.

Contents

  1. Description
  2. Scene
  3. Code
  4. Summary
  5. Common scenarios

  6. Description

Although this manual refers to the demo scene in the title, this is also the place where you’ll find general information about how to set up grids, and how OSA Grids work.

OSA’s Grids are different from Lists because they need a different recycling process, but that’s only visible at the intermediary level. The base code in OSA is still used for this purpose, and the GridAdapter is the middle-man making it possible to have a grid-looking list. OSA still sees a list of “items”, which are actually Cell groups. GridAdapter creates these Cell groups for you automatically and it adds either a HorizontalLayoutGroup or VerticalLayoutGroup to them & sets everything up behind the scenes so you’ll only need to provide the minimum necessary info, like the Cell prefab and whether a fixed number of cells per row/column should be used or not. The Cell group can have other LayoutGroup types, for advanced cases (see PackedGridExample), but that’s out of scope here.

For now, forget about the Cell groups. We’ll only refer to the actual Cells, since you’re only interacting with them, not with the groups. So if you see the term “Items”, know that I’m referring to “Cells”.

This example contains: grid, horizontal layout and asynchronous downloading of items. We’ll mostly explore the grid.

The Grid’s limitation is that you can’t insert or remove items at a specified index without refreshing the entire view. This is not that scary as it sounds: if you want to insert an item at #4, it’ll update all (visible) items, not only the newly added one, which in almost all cases doesn’t make any visual difference, but it’s worth knowing.

  1. Scene

[Screenshot - see Unity Asset Store documentation for visual guide]

Same general structure as for a list ScrollView:

[Screenshot - see Unity Asset Store documentation for visual guide]

What’s different is the prefab:

[Screenshot - see Unity Asset Store documentation for visual guide]

It requires a LayoutElement component (remember each cell was contained in a LayoutGroup?), where you specify the desired dimensions. You can also use flexible/min properties, but most of the time you’ll have fixed width/height.

It also requires a Views child under which you’ll put the actual views of your cell. We won’t go into why this is needed. You can also put a RectMask2D on the Views in case you want its children clipped.

There’s a nice script that can set either preferred height or width for you, in case your prefab scales with the screen’s size, but you want it to have a certain aspect ratio - AspectRatioFitterByLayoutElement. Using Unity’s AspectRatioFitter doesn’t work with a LayoutGroup parent.

The prefab’s size is used to initially calculate the number of cells per group and the width (height for vertical grids) of the group. Groups all have the same size, but this can be combined with the Content Size Fitter example to achieve more complex scenarios.

Alongside the properties exposed for regular lists, here’s what the GridAdapter exposes:

[Screenshot - see Unity Asset Store documentation for visual guide]

Most important property being the Max Cells Per Group, which is pretty straightforward. Leave to -1 to have a variable number of cells per group (column/row), based on the available space. All the important properties have tooltips, so just hover over them to get more details. The Snapper script just provides snapping behavior - you can ignore it, as it’s not essential.

In this scene, there’s also an edge dragger which is just a utility script to allow you to drag the grid’s edge so you can see the number of cells per group changing live. This is also not an essential component, so ignore it.

Another thing to ignore is the ContentVisual game object under the Viewport, which allows a parallax image scrolling effect.

Also the scrollbar may appear too stuffed - compared to the standard scrollbar we use with OSA, it contains an additional button to turn on/off the Snapper script. You guessed it - you can ignore this one also.

  1. Code

The GridSceneEntry ties some buttons in the Drawer to the adapter. The most important event is the OnItemCountChangeRequested, which is called when you press “Set count”. Here, we’re just setting a new item count and the LazyDataHelper notifies the adapter about that change. The Code section of the CSF manual explains the LazyDataHelper a bit more in depth.

Now open the GridExample script and let’s take a look.

Fields

Ignore the freezeContentEndEdgeOnCountChange field for now.

We’re using a Pool for the images we’ll asynchronously download, to save bandwidth. The most used type of pool is LRU - the least accessed images are discarded in favor of the latest accessed, based on the capacity.

Inner classes

BasicModel is used to store per-item data: a title and a URL for the image.

The class MyCellViewsHolder derives from CellViewsHolder. This is required for grids. You can see it has the same CollectViews() method that’s called on initialization so you can retrieve your views. Ignoring the other views which are not essential, we have the image where to load our downloaded textures and the title Text.

OSA overrides

GridExample.Start():

The LazyData needs to be initialized before the adapter. We’re doing it before calling base.Start(). Same thing with the image pool.

GridExample.Refresh():

Here we’re setting the GridAdapter._CellsCount field to the number of items we have before calling base. It may not be obvious why it’s required to do this. Because you’re the one choosing how to store the data, the GridAdapter can’t know how many items there are when calling a Refresh(), since this method doesn’t take the number of items, as ResetItems() does - if your data set has changed, _CellsCount needs to be kept up to date. This is only needed for the Refresh() method.

GridExample.OnCellViewsHolderCreated():

As opposed to the list implementations where we’ll create the views holder for an item, the GridAdapter creates it for us and we’re notified about this to initialize any custom stuff. In this case, we’re just letting the RemoteImageBehaviour on the item know what pool instance to use for the images it downloads. This pool is shared among all items.

GridExample.UpdateCellViewsHolder():

The central part of the adapter - binding the data to the views. Here we’re retrieving the item’s model using viewsHolder.ItemIndex, setting the title to “Loading”, covering the image with a plain white overlay, then beginning to load the image in the RemoteImageBehaviour. We’re specifying a callback for when the download is done, after which we change the title from “Loading” to the actual title and uncover the white overlay. IsRequestStillValid() method checks whether the image is still needed at the moment it finishes downloading - since this is something happening in the future, we can’t know what happens in between: maybe the item count changed, maybe the views holder was reused to display another image etc.

GridExample.OnDestroy():

Just clearing the image pool (otherwise we’ll have memory leaks) and calling base.

GridExample.IsRequestStillValid():

As said above, this checks whether a completed download request is still needed or it was abandoned. Code comments are included for each individual check inside this method, so check them out.

GridExample.CreateRandomModel():

As it says, it just creates a model with a title and a random image URL. This method is passed to the LazyDataHelper’s constructor in GridExample.Start(), which it uses to create models on demand. A good example may be when reading files on disk, having their paths: you’ll only read them when they’re needed, not all at once, especially if there are thousands. When loading all models at once is possible without performance loss, SimpleDataHelper is preferred, which is just a wrapper around a simple list of models, which notifies the adapter on every change you do (Insert/Remove/Reset).

GridExample.ImageDestroyer():

This one is passed to the image pool’s constructor which it calls when new images need to be stored in the pool and the maximum capacity is reached, which requires the oldest image in the pool to be destroyed to make room.

  1. Summary

Cells are always contained (at runtime) in a LayoutGroup corresponding to either a row (for vertical ScrollViews) or a column (for horizontal ScrollViews), so a LayoutElement is needed on the cell prefab, and you specify its size through it.

The cell prefab must have a View child where you put your actual views.

We need an image pool for the downloaded images. We create it in Start(), where we’re also creating the LazyDataHelper which provides us with random models on demand.

The GridAdatper notifies us via OnCellViewsHolderCreated() when a cell view is created so we can further initialize our custom view-related stuff

When a cell is becoming visible, the GridAdapter calls UpdateCellViewsHolder() so we’ll update the views from the model. Asynchronously loading an image requires an additional step: checking at the end of the download if the image is still needed by that views holder.

The image pool is cleared when the adapter is destroyed, to free the downloaded textures from memory.

  1. Common Scenarios

  2. Fixed aspect ratios for cells, but filling available space and with a consistent spacing horizontally and/or vertically

[Screenshot - see Unity Asset Store documentation for visual guide]

This solution is available for v5.0.1 and up.

For this, we take a vertical grid as an example (the “Select and delete” demo was used, but it works with any).

After everything is set up, the default behavior is having MaxCellsPerGroup set to -1, which means cells’ width will remain constant, and OSA will fill the available space horizontally with cells (number of cells in a row will be OSAWidth/CellPrefabWidth).

You’ll need to change this to whatever works for you, for example 3.

Then enable Grid.PreserveCellAspectRatio in the inspector.

The way it works is getting the aspect ratio of the prefab’s RectTransform (width/height) and applying the same aspect ratio for the instantiated cells, after their width is changed to fill up the available horizontal space in the viewport. This also takes spacing/padding into account, and it works similarly for horizontal grids.

At runtime, this changes the instantiated cells’ LayoutElement so that it has a preferredWidth exactly as needed to fill the viewport and to have a flexibleHeight of 1, to fill the Cell group’s height (which is automatically calculated at initialization).

Because the values you set in the LayoutElement are overridden, a warning is displayed if OSA detects any non-default values present

this post is licensed under cc by 4.0 by the author.

© Forbidden Byte. some rights reserved.

other searches include optimised scrollview adapter, optimized scroll view adapter, optimized scrollvew adapter, optimzed scrollview adapter, optmized scrollview adapter, optimized scrolview adapter, optimized scrollview adaptor, optimized scrollveiw adapter, optimized scrollview adpater, osa unity, mediaplayer8, media player 8, mediaplayer8 unity, media player unity, scene objects query, sceneobjectsquery, scene object query, soq unity, unity video player plugin, unity scene query tool, scroll rect optimizer, unity list view, unity grid view