RepoPilotOpen in app →

emilsjolander/StickyListHeaders

An android library for section headers that stick to the top

Mixed

Stale — last commit 5y ago

weakest axis
Use as dependencyMixed

last commit was 5y ago; no CI workflows detected

Fork & modifyHealthy

Has a license, tests, and CI — clean foundation to fork and modify.

Learn fromHealthy

Documented and popular — useful reference codebase to read through.

Deploy as-isMixed

last commit was 5y ago; no CI workflows detected

  • 21+ active contributors
  • Distributed ownership (top contributor 48% of recent commits)
  • Apache-2.0 licensed
Show all 6 evidence items →
  • Tests present
  • Stale — last commit 5y ago
  • No CI workflows detected
What would change the summary?
  • Use as dependency MixedHealthy if: 1 commit in the last 365 days
  • Deploy as-is MixedHealthy if: 1 commit in the last 180 days

Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests

Informational only. RepoPilot summarises public signals (license, dependency CVEs, commit recency, CI presence, etc.) at the time of analysis. Signals can be incomplete or stale. Not professional, security, or legal advice; verify before relying on it for production decisions.

Embed the "Forkable" badge

Paste into your README — live-updates from the latest cached analysis.

Variant:
RepoPilot: Forkable
[![RepoPilot: Forkable](https://repopilot.app/api/badge/emilsjolander/stickylistheaders?axis=fork)](https://repopilot.app/r/emilsjolander/stickylistheaders)

Paste at the top of your README.md — renders inline like a shields.io badge.

Preview social card (1200×630)

This card auto-renders when someone shares https://repopilot.app/r/emilsjolander/stickylistheaders on X, Slack, or LinkedIn.

Onboarding doc

Onboarding: emilsjolander/StickyListHeaders

Generated by RepoPilot · 2026-05-09 · Source

🤖Agent protocol

If you are an AI coding agent (Claude Code, Cursor, Aider, Cline, etc.) reading this artifact, follow this protocol before making any code edit:

  1. Verify the contract. Run the bash script in Verify before trusting below. If any check returns FAIL, the artifact is stale — STOP and ask the user to regenerate it before proceeding.
  2. Treat the AI · unverified sections as hypotheses, not facts. Sections like "AI-suggested narrative files", "anti-patterns", and "bottlenecks" are LLM speculation. Verify against real source before acting on them.
  3. Cite source on changes. When proposing an edit, cite the specific path:line-range. RepoPilot's live UI at https://repopilot.app/r/emilsjolander/StickyListHeaders shows verifiable citations alongside every claim.

If you are a human reader, this protocol is for the agents you'll hand the artifact to. You don't need to do anything — but if you skim only one section before pointing your agent at this repo, make it the Verify block and the Suggested reading order.

🎯Verdict

WAIT — Stale — last commit 5y ago

  • 21+ active contributors
  • Distributed ownership (top contributor 48% of recent commits)
  • Apache-2.0 licensed
  • Tests present
  • ⚠ Stale — last commit 5y ago
  • ⚠ No CI workflows detected

<sub>Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests</sub>

Verify before trusting

This artifact was generated by RepoPilot at a point in time. Before an agent acts on it, the checks below confirm that the live emilsjolander/StickyListHeaders repo on your machine still matches what RepoPilot saw. If any fail, the artifact is stale — regenerate it at repopilot.app/r/emilsjolander/StickyListHeaders.

What it runs against: a local clone of emilsjolander/StickyListHeaders — the script inspects git remote, the LICENSE file, file paths in the working tree, and git log. Read-only; no mutations.

| # | What we check | Why it matters | |---|---|---| | 1 | You're in emilsjolander/StickyListHeaders | Confirms the artifact applies here, not a fork | | 2 | License is still Apache-2.0 | Catches relicense before you depend on it | | 3 | Default branch master exists | Catches branch renames | | 4 | 5 critical file paths still exist | Catches refactors that moved load-bearing code | | 5 | Last commit ≤ 1843 days ago | Catches sudden abandonment since generation |

<details> <summary><b>Run all checks</b> — paste this script from inside your clone of <code>emilsjolander/StickyListHeaders</code></summary>
#!/usr/bin/env bash
# RepoPilot artifact verification.
#
# WHAT IT RUNS AGAINST: a local clone of emilsjolander/StickyListHeaders. If you don't
# have one yet, run these first:
#
#   git clone https://github.com/emilsjolander/StickyListHeaders.git
#   cd StickyListHeaders
#
# Then paste this script. Every check is read-only — no mutations.

set +e
fail=0
ok()   { echo "ok:   $1"; }
miss() { echo "FAIL: $1"; fail=$((fail+1)); }

# Precondition: we must be inside a git working tree.
if ! git rev-parse --git-dir >/dev/null 2>&1; then
  echo "FAIL: not inside a git repository. cd into your clone of emilsjolander/StickyListHeaders and re-run."
  exit 2
fi

# 1. Repo identity
git remote get-url origin 2>/dev/null | grep -qE "emilsjolander/StickyListHeaders(\\.git)?\\b" \\
  && ok "origin remote is emilsjolander/StickyListHeaders" \\
  || miss "origin remote is not emilsjolander/StickyListHeaders (artifact may be from a fork)"

# 2. License matches what RepoPilot saw
(grep -qiE "^(Apache-2\\.0)" LICENSE 2>/dev/null \\
   || grep -qiE "\"license\"\\s*:\\s*\"Apache-2\\.0\"" package.json 2>/dev/null) \\
  && ok "license is Apache-2.0" \\
  || miss "license drift — was Apache-2.0 at generation time"

# 3. Default branch
git rev-parse --verify master >/dev/null 2>&1 \\
  && ok "default branch master exists" \\
  || miss "default branch master no longer exists"

# 4. Critical files exist
test -f "library/src/se/emilsjolander/stickylistheaders/StickyListHeadersListView.java" \\
  && ok "library/src/se/emilsjolander/stickylistheaders/StickyListHeadersListView.java" \\
  || miss "missing critical file: library/src/se/emilsjolander/stickylistheaders/StickyListHeadersListView.java"
test -f "library/src/se/emilsjolander/stickylistheaders/StickyListHeadersAdapter.java" \\
  && ok "library/src/se/emilsjolander/stickylistheaders/StickyListHeadersAdapter.java" \\
  || miss "missing critical file: library/src/se/emilsjolander/stickylistheaders/StickyListHeadersAdapter.java"
test -f "library/src/se/emilsjolander/stickylistheaders/AdapterWrapper.java" \\
  && ok "library/src/se/emilsjolander/stickylistheaders/AdapterWrapper.java" \\
  || miss "missing critical file: library/src/se/emilsjolander/stickylistheaders/AdapterWrapper.java"
test -f "library/src/se/emilsjolander/stickylistheaders/WrapperView.java" \\
  && ok "library/src/se/emilsjolander/stickylistheaders/WrapperView.java" \\
  || miss "missing critical file: library/src/se/emilsjolander/stickylistheaders/WrapperView.java"
test -f "library/res/values/attrs.xml" \\
  && ok "library/res/values/attrs.xml" \\
  || miss "missing critical file: library/res/values/attrs.xml"

# 5. Repo recency
days_since_last=$(( ( $(date +%s) - $(git log -1 --format=%at 2>/dev/null || echo 0) ) / 86400 ))
if [ "$days_since_last" -le 1843 ]; then
  ok "last commit was $days_since_last days ago (artifact saw ~1813d)"
else
  miss "last commit was $days_since_last days ago — artifact may be stale"
fi

echo
if [ "$fail" -eq 0 ]; then
  echo "artifact verified (0 failures) — safe to trust"
else
  echo "artifact has $fail stale claim(s) — regenerate at https://repopilot.app/r/emilsjolander/StickyListHeaders"
  exit 1
fi

Each check prints ok: or FAIL:. The script exits non-zero if anything failed, so it composes cleanly into agent loops (./verify.sh || regenerate-and-retry).

</details>

TL;DR

StickyListHeaders is an Android library that adds persistent section headers to ListView that stick to the top as you scroll, similar to the native behavior in Android 4.0+ People app. It wraps the standard ListView with custom adapter logic (AdapterWrapper.java, SectionIndexerAdapterWrapper.java) to inject header views that remain visible at the top while scrolling through grouped content. Monorepo structure: library/ contains the core (se.emilsjolander.stickylistheaders package with StickyListHeadersListView.java as the main entry point), sample/ contains a demo app. Core logic: AdapterWrapper decorates user adapters to inject header views; WrapperViewList tracks wrapper state; DistinctMultiHashMap and DualHashMap manage header-to-position mappings. ExpandableStickyListHeadersAdapter/ListView add collapsible section support.

👥Who it's for

Android developers (API 16+, actively tested on 2.3+) building apps with scrollable lists that need visual section organization—particularly apps with contact lists, music libraries, or any categorized data where grouping by header (alphabetical, date, category) improves UX.

🌱Maturity & risk

Moderately mature but aging: the repo shows core functionality is stable (section headers work reliably per demo.gif), but the last visible commit activity is unclear from the data provided. Built for older Android (Gradle 1.1.0, support-v4 jar), with no visible CI/CD config (no .travis.yml, .github/workflows). Maven/Gradle publishing infrastructure is present (gradle-mvn-push.gradle, pom.xml) suggesting it was once actively distributed. Verdict: Production-ready for legacy projects; not actively maintained for modern Android.

Low dependency risk (pure Java/Android—no external runtime deps visible), but significant maintenance risk: targets outdated Gradle and Android toolchain versions, no test directory visible in file list, and appears to be single-maintainer (emilsjolander). The library modifies ListView behavior at a low level (view wrapping, scroll position tracking in WrapperViewList.java), which could conflict with other ListView customizations or break on future Android versions.

Active areas of work

Unknown from file data—no git log, branch info, or recent PR list provided. The presence of both Gradle and legacy build.xml/project.properties suggests transition from old Android build system, but completion status unclear.

🚀Get running

git clone https://github.com/emilsjolander/StickyListHeaders.git && cd StickyListHeaders && ./gradlew build (or gradle build on older systems). Import into Android Studio via File > Import Project, selecting build.gradle.

Daily commands: Build: ./gradlew build. Run sample app: open sample/ in Android Studio and run on emulator/device. The sample/ app demonstrates StickyListHeadersListView with a countries-by-letter dataset.

🗺️Map of the codebase

  • library/src/se/emilsjolander/stickylistheaders/StickyListHeadersListView.java — Core ListView subclass that implements sticky header behavior and scroll listening—the main entry point for the entire library
  • library/src/se/emilsjolander/stickylistheaders/StickyListHeadersAdapter.java — Interface that adapters must implement to provide header-aware data; defines the contract between data and UI
  • library/src/se/emilsjolander/stickylistheaders/AdapterWrapper.java — Wraps user adapters to inject header views and manage dual-view rendering; critical for transforming data into sticky-header layout
  • library/src/se/emilsjolander/stickylistheaders/WrapperView.java — Custom view that combines header and list item into a single unit; handles the visual stickiness mechanism
  • library/res/values/attrs.xml — Defines custom XML attributes (e.g., sticky behavior toggle) that enable library configuration in layout files
  • library/build.gradle — Gradle build configuration that defines library dependencies, versioning, and publishing settings

🛠️How to make changes

Add a new list with sticky headers

  1. Create a custom adapter extending BaseAdapter and implementing StickyListHeadersAdapter interface (sample/src/se/emilsjolander/stickylistheaders/sample/TestBaseAdapter.java)
  2. Implement getHeaderId() to return unique ID per section and getHeaderView() to inflate/populate header views (library/src/se/emilsjolander/stickylistheaders/StickyListHeadersAdapter.java)
  3. In your Activity, declare StickyListHeadersListView in layout and set adapter via setAdapter() (sample/res/layout/main.xml)
  4. Optionally customize sticky behavior via XML attributes (android:stickyHeaders) in your layout (library/res/values/attrs.xml)

Add expandable section support

  1. Extend ExpandableStickyListHeadersAdapter instead of StickyListHeadersAdapter in your custom adapter (library/src/se/emilsjolander/stickylistheaders/ExpandableStickyListHeadersAdapter.java)
  2. Use ExpandableStickyListHeadersListView instead of StickyListHeadersListView in your layout (library/src/se/emilsjolander/stickylistheaders/ExpandableStickyListHeadersListView.java)
  3. Implement getChildrenCount(sectionIndex) to define collapse/expand behavior per section (sample/src/se/emilsjolander/stickylistheaders/sample/ExpandableListTestActivity.java)

Customize header and item view layouts

  1. Create layout XMLs for header (e.g., header.xml) and list item (e.g., test_list_item_layout.xml) (sample/res/layout/header.xml)
  2. In your adapter's getHeaderView(), inflate and bind data to your custom header layout (sample/src/se/emilsjolander/stickylistheaders/sample/TestBaseAdapter.java)
  3. In getView(), inflate and bind data to your custom item layout using standard BaseAdapter pattern (sample/res/layout/test_list_item_layout.xml)

Enable indexed scrolling with section headers

  1. Implement SectionIndexer interface in your adapter alongside StickyListHeadersAdapter (library/src/se/emilsjolander/stickylistheaders/SectionIndexerAdapterWrapper.java)
  2. Implement getSections() to return array of section titles and getPositionForSection() for lookup (library/src/se/emilsjolander/stickylistheaders/SectionIndexerAdapterWrapper.java)
  3. The library automatically wraps SectionIndexer-compliant adapters for fast scrolling via side index (library/src/se/emilsjolander/stickylistheaders/AdapterWrapper.java)

🔧Why these technologies

  • Android ListView subclass — Extends native ListView to minimize API surface and maintain compatibility with standard Android patterns; prioritizes general use cases over special features
  • BaseAdapter wrapper pattern — Allows library to work with any adapter implementation without requiring users to rewrite their data layer; composite pattern enables header injection
  • Custom view composition (WrapperView) — Combines header and item into single view to enable efficient sticky positioning without ScrollView overhead
  • Hash-based caching (DualHashMap) — Caches header positions and views by ID to avoid redundant lookups during rapid scrolling (high-frequency events)

⚖️Trade-offs already made

  • Sticky headers implemented via view repositioning rather than RecyclerView or custom scroll container

    • Why: Maximizes compatibility with existing ListView code and Android 2.3+ devices; simpler mental model for developers
    • Consequence: More complex scroll listener logic; potential jank if header inflation is slow; less efficient than RecyclerView's view pool
  • Adapter interface requires getHeaderId() + getHeaderView() instead of automatic grouping

    • Why: Gives developers explicit control over section logic and header rendering; works with diverse data structures
    • Consequence: More boilerplate for users; requires understanding of header vs. item view distinction
  • Library only adds few public methods; delegates to standard ListView behaviors

    • Why: Keeps API surface minimal and maintainable; avoids scope creep into specialized use cases
    • Consequence: Some advanced features (e.g., section collapsing) require separate ExpandableStickyListHeadersListView subclass

🚫Non-goals (don't propose these)

  • Does not support real-time data updates (e.g., live feeds); designed for static or periodically-refreshed lists
  • Does not provide built-in section filtering or searching; delegates to adapter implementation
  • Does not handle touch events or gesture handling beyond standard ListView; no swipe-to-delete or drag-reorder
  • Does not support nested sections or multi-level hierarchies in base implementation (only via ExpandableStickyListHeadersListView)
  • Does not provide layout manager flexibility like RecyclerView; locked to vertical scrolling ListView paradigm

🪤Traps & gotchas

Adapter interface contract: implementers must correctly implement StickyListHeadersAdapter.getSectionHeaderId(int section)—returning duplicate IDs for different sections breaks header positioning. WrapperViewList assumes View state is managed externally; custom ViewHolder patterns may not work without modification. Scroll performance degrades with very large lists (1000+ items) due to repeated header inflation in AdapterWrapper—no view pooling visible. CheckableWrapperView assumes wrapped items are Checkable; wrapping non-Checkable items silently fails. Android API level: library targets older support levels; modern Android (API 29+) gestures may conflict with sticky header scroll interception.

🏗️Architecture

💡Concepts to learn

  • ListView Adapter Decoration Pattern — AdapterWrapper implements decorator pattern to intercept adapter calls without modifying user code—core technique for injecting header logic
  • Custom ViewGroup Layout & Measurement — WrapperViewList extends ViewGroup and overrides onLayout() to position header Views on top during scroll—requires understanding Android's measure/layout pipeline
  • ListView RecycledView Pooling — ListView reuses Views via getView() callbacks for performance; StickyListHeadersAdapter must return consistent Views per section ID to avoid visual glitches
  • Scroll Position to Content Mapping — DistinctMultiHashMap and scroll listeners must efficiently map vertical scroll offset to header section—used to determine which header should stick at top
  • SectionIndexer Interface — Android's native interface for fast-scrolling; SectionIndexerAdapterWrapper wraps this to enable fast-scroll bars while maintaining sticky headers
  • Z-order & View Elevation — Sticky header View must render above all list items; achieved via ViewGroup draw ordering and layout parameters in CheckableWrapperView
  • nostra13/Android-Universal-Image-Loader — Contemporary library (2010s era) solving a different problem but using similar adapter-wrapping patterns for list optimization
  • jjoe64/GraphView — Android library from same era using custom ViewGroup extension patterns similar to WrapperViewList for visual customization
  • dslmeinte/android-sticky-headers — Competing sticky header library for Android using RecyclerView (modern alternative) instead of ListView
  • google/android-architecture — Google's Android architecture samples—useful reference for updating this library to modern MVVM/LiveData patterns
  • androidx/androidx — Official AndroidX library includes StickyHeadersRecyclerViewAdapter and modern replacement patterns for ListView-based code

🪄PR ideas

To work on one of these in Claude Code or Cursor, paste: Implement the "<title>" PR idea from CLAUDE.md, working through the checklist as the task list.

Add unit tests for StickyListHeadersAdapter and AdapterWrapper

The library has core adapter wrapping logic in AdapterWrapper.java and SectionIndexerAdapterWrapper.java that lack test coverage. These classes handle critical functionality like header positioning and section indexing. Adding unit tests would catch regressions and make the codebase more maintainable for contributors. The sample app exists but doesn't include automated tests.

  • [ ] Create sample/src/se/emilsjolander/stickylistheaders/sample/test/ directory structure
  • [ ] Write unit tests for AdapterWrapper.java covering header wrapping, position mapping, and view type handling
  • [ ] Write unit tests for SectionIndexerAdapterWrapper.java covering section indexing logic
  • [ ] Add junit and mockito dependencies to sample/build.gradle
  • [ ] Ensure tests cover edge cases like empty adapters and single-item lists

Migrate legacy Android build configuration to modern Gradle syntax

The repo contains outdated build artifacts (build.xml, project.properties, pom.xml, gradle-mvn-push.gradle in library/) indicating incomplete migration from Ant/Maven. The gradle.properties and build.gradle files use Android Gradle Plugin 1.1.0 (from 2015). Modernizing the build system will improve build times, enable current Android tooling, and reduce confusion for new contributors.

  • [ ] Update library/build.gradle to use current Android Gradle Plugin (AGP 8.x) and remove deprecated android { } block syntax
  • [ ] Remove or archive legacy files: library/build.xml, library/pom.xml, library/gradle-mvn-push.gradle, library/project.properties
  • [ ] Update sample/build.gradle similarly and remove sample/project.properties and sample/libs/ (replace with gradle dependencies)
  • [ ] Update buildscript in root build.gradle to use current repositories (google(), mavenCentral()) and remove deprecated classpath
  • [ ] Test that library builds and sample app runs on current Android Studio/emulator

Add integration test for ExpandableStickyListHeadersListView in sample app

ExpandableStickyListHeadersListView.java and ExpandableStickyListHeadersAdapter.java exist but ExpandableListTestActivity.java in the sample is not integrated into automated testing. This expandable list functionality is a significant feature variant that should have test coverage to prevent regressions when the core library is modified.

  • [ ] Create sample/src/se/emilsjolander/stickylistheaders/sample/test/ExpandableListTest.java with AndroidTestCase or AndroidJUnit4 instrumentation tests
  • [ ] Write test cases covering: expanding/collapsing groups, header sticky behavior with expanded state, empty groups, group position tracking
  • [ ] Add test dependencies (androidx.test:rules, androidx.test.espresso:espresso-core) to sample/build.gradle
  • [ ] Reference ExpandableListTestActivity.java layout (sample/res/layout/expandable_sample.xml) and TestBaseAdapter.java in test setup
  • [ ] Document how to run instrumentation tests in README.md

🌿Good first issues

  • Add unit tests for DistinctMultiHashMap.java and DualHashMap.java—currently no test/ directory visible, so boundary cases (duplicate section IDs, null positions) are untested.
  • Document the required contract for StickyListHeadersAdapter implementers in library/README or code Javadoc—currently only README snippet shows example, no formal specification of getSectionHeaderId() uniqueness requirement.
  • Upgrade gradle build configuration from 1.1.0 to modern Android Gradle Plugin—update library/build.gradle and sample/build.gradle to support Android API 29+ and remove legacy build.xml/project.properties files.

Top contributors

Click to expand

📝Recent commits

Click to expand
  • cec8d6a — Initialize down event with original down y position. (Emil Sjolander)
  • 6703372 — Revert changes to sample only used for testing (Emil Sjolander)
  • 63a69f5 — Save touchslop in instance variable (Emil Sjolander)
  • 1574bff — Bump version (Emil Sjolander)
  • 33c8a02 — Allow scrolling list on top of header (Emil Sjolander)
  • 814abb7 — bump version (emilsjolander)
  • 49dd986 — Fix gradle issues in sample (emilsjolander)
  • db67ab1 — Merge pull request #376 from asymptotik/top_offset (emilsjolander)
  • c308473 — Merge pull request #353 from seato/seato/issue-352-memory-leaks (emilsjolander)
  • 168bbd5 — Merge pull request #372 from passos/patch-1 (emilsjolander)

🔒Security observations

This Android library project has significant security concerns primarily related to outdated build tools and dependencies. The Gradle plugin version (1.1.0 from 2014) is severely outdated and lacks modern security features. The inclusion of legacy android-support-v4.jar as a committed binary file bypasses dependency management. The project appears to target an outdated Android API level without recent security patches. Immediate action is required to update the build infrastructure, migrate to AndroidX, and modernize the development toolchain. The codebase itself (Java source files) does not appear to have obvious injection vulnerabilities or hardcoded secrets, but cannot be fully assessed without detailed code review of the core library files.

  • High · Outdated Gradle Build Tool — build.gradle. The build.gradle file specifies gradle version 1.1.0 (classpath 'com.android.tools.build:gradle:1.1.0'), which was released in 2014. This version contains multiple known security vulnerabilities and lacks security patches, modern dependency resolution, and build system hardening features. Fix: Update to the latest stable Android Gradle Plugin version (currently 8.x or higher). Run './gradlew wrapper --gradle-version=LATEST' to update the gradle wrapper.
  • High · Legacy Android Support Library — sample/libs/android-support-v4.jar. The sample app includes 'android-support-v4.jar' as a direct JAR file (sample/libs/android-support-v4.jar). Support libraries from this era are no longer maintained and contain known security vulnerabilities. Additionally, JAR files committed to source control bypass dependency management and vulnerability scanning. Fix: Remove the JAR file from the repository and use AndroidX libraries through gradle dependencies instead. Add 'androidx.appcompat:appcompat:1.x.x' to build.gradle and manage versions through dependency management.
  • Medium · Vulnerable Target API Level — library/AndroidManifest.xml, sample/AndroidManifest.xml. Based on the project age and gradle configuration, the target API level is likely significantly outdated. Modern Android requires targeting recent API levels for Google Play Store distribution and includes important security improvements. Fix: Update targetSdkVersion to API 34 or higher. Ensure compileSdkVersion matches the target version. Add required runtime permissions for Android 6.0+.
  • Medium · Missing Gradle Dependency Lock/Verification — build.gradle, library/build.gradle. The build.gradle does not specify exact versions for dependencies or use gradle dependency locking. This allows transitive dependencies to be automatically updated to potentially vulnerable versions without explicit control. Fix: Pin explicit versions for all direct and critical transitive dependencies. Enable gradle dependency locking with 'dependencyLocking { lockAllConfigurations() }' to prevent unexpected updates.
  • Low · Missing ProGuard Configuration — library/proguard-project.txt. While proguard-project.txt exists, there is no evidence of proper configuration for code obfuscation and optimization in the library. This could expose internal logic and class names. Fix: Implement comprehensive ProGuard/R8 rules to obfuscate library code, remove unused code, and optimize builds. Configure proper keep rules for public APIs.

LLM-derived; treat as a starting point, not a security audit.


Generated by RepoPilot. Verdict based on maintenance signals — see the live page for receipts. Re-run on a new commit to refresh.

Mixed signals · emilsjolander/StickyListHeaders — RepoPilot