Flutter - Quickly achieve half-view exposure statistic 📊

LinXunFeng
5 min readNov 18, 2023

--

Overview

Among the many exposure statistics calculation methods, there is such a special calculation method called half-view exposure statistics. As the name suggests, when the exposed size of the view exceeds 50% of its own size, a statistic need to be triggered and recorded to prevent repeated statistic. The exposure record will be reset when exposed size of the view less than 50%.

It is generally used to count the exposure of advertisement view placed on list page and certain advertisement view on detail page.

Solution

It is quite troublesome to monitor and calculate the display ratio of all current items in real time, so generally we will priority finding and using existing solutions.

I think the first thing that comes to everyone’s mind is Google’s visibility_detector. This package is really easy to use. I also recommend you to use it in regular scenarios because it is really convenient! However, there is one scenario I encountered that it couldn’t handle, that is, when there is SliverPersistentHeader in CustomScrollView, its calculation results will be inaccurate, as shown below.

Pay attention to the blue Middle Sliver view. When it is first blocked by the AppBar, its display ratio is still 1. It does not start to change until it exceeds the top of the screen. When it is completely blocked by the AppBar, the value is 0.58~

However, here I will focus on another solution, which is to use my package (flutter_scrollview_observer) to quickly obtain the item’s own display ratio without the above bugs.

Practical part

Taking ListView as an example, the code is as follows:

final observerController = ListObserverController();

ListViewObserver(
child: _buildListView(),
// Without comparison, the results are returned directly.
triggerOnObserveType: ObserverTriggerOnObserveType.directly,
controller: observerController,
onObserve: (resultModel) {
// Get the data of all items being displayed from the observation results.
final models = resultModel.displayingChildModelList;
// Get all indexes.
final indexList = models.map((e) => e.index).toList();
// Get the display radio of all items.
final displayPercentageList =
models.map((e) => e.displayPercentage).toList();
debugPrint('index -- $indexList -- $displayPercentageList');
},

Yes, it is so simple to get the display radio of all items. Just use the corresponding WidgetObserver to observe the ScrollView.

The observation result will be returned directly every time ScrollView scroll. If you need to get an observation without scrolling, you can call the following method.

observerController.dispatchOnceObserve();

Statistical Logic

Above you can already get the data of items own display ratio, and then you can make a logical judgment on whether to trigger exposure. This is more business-oriented, so I will give my code directly here for participation.

import 'package:scrollview_observer/scrollview_observer.dart';

mixin VisibilityExposureMixin {
// The map that records which items have been exposed.
Map<dynamic, bool> exposureRecordMap = {};

/// Reset the exposure records of all items.
resetExposureRecordMap() {
exposureRecordMap.clear();
}

/// Handle exposure of items in ScrollView
///
/// [resultModel] Observation result (the base class is ObserveModel, pass the value in the onObserve callback, or the value taken out according to the BuildContext in onObserveAll)
/// [toExposeDisplayPercent] When the display ratio of the item exceeds this value, it is considered exposed and recorded, otherwise the exposure record is reset.
/// [recordKeyCallback] Return the key used to record the exposure of the item. If not implemented, use the index.
/// [needExposeCallback] Used to determine whether the item corresponding to the index participates in the exposure calculation logic. If not implemented, the default is true.
/// [toExposeCallback] This callback is called after the exposure conditions are met.
handleExposure({
required dynamic resultModel,
double toExposeDisplayPercent = 0.5,
dynamic Function(int index)? recordKeyCallback,
bool Function(int index)? needExposeCallback,
required Function(int index) toExposeCallback,
}) {
List<ObserveDisplayingChildModelMixin> displayingChildModelList = [];
if (resultModel is ListViewObserveModel) {
displayingChildModelList = resultModel.displayingChildModelList;
} else if (resultModel is GridViewObserveModel) {
displayingChildModelList = resultModel.displayingChildModelList;
}
for (var displayingChildModel in displayingChildModelList) {
final index = displayingChildModel.index;
final recordKey = recordKeyCallback?.call(index) ?? index;
// Let the outside tell us whether the item corresponding to index needs to participate in the exposure calculation logic
final needExpose = needExposeCallback?.call(index) ?? true;
if (!needExpose) continue;
// debugPrint('item : $index - ${displayingChildModel.displayPercentage}');
// Determine whether the item's own display ratio exceeds [toExposeDisplayPercent]
if (displayingChildModel.displayPercentage < toExposeDisplayPercent) {
// The exposure conditions are not currently met, reset the exposure record.
exposureRecordMap[recordKey] = false;
} else {
// The exposure conditions are currently met
final haveExposure = exposureRecordMap[recordKey] ?? false;
if (haveExposure) continue;
toExposeCallback(index);
exposureRecordMap[recordKey] = true;
}
}
}
}

Judgment logic:

  1. After reaching 1/2, exposure statistics are triggered and recorded to prevent multiple requests from being triggered.
  2. Reset the exposure record of the current item when it is less than 1/2

Usage:

Mix in VisibilityExposureMixin

class _VisibilityListViewPageState extends State<VisibilityListViewPage>
with VisibilityExposureMixin {
...
}

Call the handleExposure method in onObserve.

onObserve: (resultModel) {
handleExposure(
resultModel: resultModel,
needExposeCallback: (index) {
// Only the item with index 6 needs to be calculated whether it is exposed.
return index == 6;
},
toExposeCallback: (index) {
// The conditions are currently met and can be reported for exposure.
debugPrint('Exposure -- $index');
},
);
},

Let’s finally take a look at the effect. Pay attention to the red view and console output.

Expose the red item with index 6 in the ListView in the lower left corner.

Expose the purple item with index 6 in the SliverGrid in the lower left corner.

Demo link: visibility_demo

--

--