Semi‑Automatic Declarative Show Exposure Tracking for Android Activities and Fragments
This article explains the differences between PV and show exposure points, introduces a PvTracker interface for semi‑automatic page‑view reporting, and presents a comprehensive solution that uses global activity lifecycle callbacks, fragment visibility detection, and Kotlin extension methods to achieve fully automated show event reporting in Android applications.
The article begins by distinguishing two types of exposure points in Android: PV (Page View) , which represents page‑level display and is reported when leaving a page, and show , which can represent any UI element becoming visible and is reported at the moment of display.
To automate PV reporting, a third‑party library provides a public interface PvTracker that defines methods for generating an event ID, extra parameters, and optional reporting control. An example implementation in an AvatarActivity shows how to override getPvEventId() and getPvExtra() to supply the required data.
public interface PvTracker {
String getPvEventId();
Bundle getPvExtra();
default boolean shouldReport() { return true; }
default String getUniqueKey() { return null; }
} class AvatarActivity : BaseActivity, PvTracker {
override fun getPvEventId() = "avatar.pv"
override fun getPvExtra() = Bundle()
}A global PvLifeCycleCallback registers as an Application.ActivityLifecycleCallbacks implementation. It listens to onActivityResumed and onActivityPaused , retrieves the event ID from the activity (if it implements PvTracker ), and triggers visibility start or end via PvManager .
class PvLifeCycleCallback implements Application.ActivityLifecycleCallbacks {
@Override public void onActivityResumed(Activity activity) {
String eventId = getEventId(activity);
if (!TextUtils.isEmpty(eventId)) onActivityVisibleChanged(activity, true);
}
@Override public void onActivityPaused(Activity activity) {
String eventId = getEventId(activity);
if (!TextUtils.isEmpty(eventId)) onActivityVisibleChanged(activity, false);
}
private void onActivityVisibleChanged(Activity activity, boolean isVisible) { /* ... */ }
}For fragments, the article notes that the native lifecycle does not always reflect view visibility, especially when using FragmentTransaction.show()/hide() or ViewPager. To solve this, a custom visibility detection extension View.onVisibilityChange is introduced, which monitors global layout, scroll, focus, and hierarchy changes to determine when a view becomes visible or hidden.
fun View.onVisibilityChange(viewGroups: List
= emptyList(), needScrollListener: Boolean = true, block: (view: View, isVisible: Boolean) -> Unit) { /* implementation */ }A BaseFragment class uses this extension in onViewCreated to forward visibility changes to an abstract method onFragmentVisibilityChange . Sub‑classes enable detection by overriding the detectVisibility property.
abstract class BaseFragment : Fragment() {
abstract val detectVisibility: Boolean
open fun onFragmentVisibilityChange(show: Boolean) {}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (detectVisibility) view.onVisibilityChange { _, isVisible -> onFragmentVisibilityChange(isVisible) }
}
}An ExposureParam interface is defined for any page that wants to report a show event, providing the event ID, extra parameters, and a force flag.
interface ExposureParam {
val eventId: String
fun getExtra(): Map
= emptyMap()
fun isForce(): Boolean = false
}The PageVisibilityListener implements Application.ActivityLifecycleCallbacks and registers a FragmentManager.FragmentLifecycleCallbacks . It invokes a single callback onPageVisibilityChange(page, isVisible) for both activities and fragments that implement ExposureParam .
class PageVisibilityListener : Application.ActivityLifecycleCallbacks {
var onPageVisibilityChange: ((page: Any, isVisible: Boolean) -> Unit)? = null
private val fragmentLifecycleCallbacks by lazy {
object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentViewCreated(fm: FragmentManager, f: Fragment, v: View, savedInstanceState: Bundle?) {
if (f is ExposureParam) {
v.onVisibilityChange { _, isVisible -> onPageVisibilityChange?.invoke(f, isVisible) }
}
}
}
}
// Activity callbacks call onPageVisibilityChange when the activity implements ExposureParam
}Finally, the custom listener is registered in the MyApplication class. When a page becomes visible, the listener casts it to ExposureParam and calls ReportUtil.reportShow with the generated parameters, achieving fully automated show reporting without manual triggers.
class MyApplication : Application() {
private val fragmentVisibilityListener by lazy {
PageVisibilityListener().apply {
onPageVisibilityChange = { page, isVisible ->
if (isVisible) (page as? ExposureParam)?.also { param ->
ReportUtil.reportShow(param.isForce(), param.eventId, param.getExtra())
}
}
}
}
override fun onCreate() {
super.onCreate()
registerActivityLifecycleCallbacks(fragmentVisibilityListener)
}
}Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.