A full-screen story viewer for Android (Instagram/Stories-style): progress bars on top, timer-based auto-advance, tap left/right to navigate, pause/mute, image and video support (with an on-disk video cache for instant opening). Available as a View flavor and a Jetpack Compose flavor, sharing the same underlying model and video cache.
The library has no dependency on any specific backend or DI framework: "story viewed" and
"link clicked" events are reported back through the Activity result
(registerForActivityResult), not through direct API calls.
| Module | What it is |
|---|---|
storyviewer-core |
Shared, UI-toolkit-agnostic code: StoryItem, StoryVideoCache, the custom VideoViewNoAudioFocus video view. Not meant to be depended on directly. |
storyviewer |
View-based StoryViewerActivity + XML layouts/theme. |
storyviewer-compose |
Compose StoryViewerScreen composable + a StoryViewerActivity wrapper with the same Intent contract as the View flavor. |
| Feed | Viewer | Video |
|---|---|---|
![]() |
![]() |
![]() |
See the sample module for a runnable, self-contained example (bundled placeholder
images/video, no network access required).
// root build.gradle
repositories {
maven { url 'https://jitpack.io' }
}
// app module's build.gradle — pick one flavor (or both)
dependencies {
implementation 'com.github.btctcn.storyviewer-android:storyviewer:v1.1.0' // View
implementation 'com.github.btctcn.storyviewer-android:storyviewer-compose:v1.1.0' // Compose
}val items = listOf(
StoryItem(id = "1", imageUrl = "https://...", linkUrl = "https://..."),
StoryItem(id = "2", videoUrl = "https://..."),
)
val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val viewedIds = result.data?.getStringArrayListExtra(StoryViewerActivity.EXTRA_VIEWED_IDS)
val clickedLink = result.data?.getStringExtra(StoryViewerActivity.EXTRA_CLICKED_LINK)
// mark viewedIds as viewed in your own model, handle clickedLink yourself
}
launcher.launch(StoryViewerActivity.newIntent(context, items, startIndex = 0))Optionally, prefetch the first N stories' videos in the background (e.g. right after loading the feed) so opening the viewer doesn't wait on network buffering:
StoryVideoCache.getInstance(context, okHttpClient)
.prefetch(videoUrls, onDownloadError = { e -> /* your own analytics, optional */ })The host app can override the Theme.StoryViewer theme (or set its attributes on its own
theme) without touching the library's code or layouts:
<style name="Theme.StoryViewer" parent="Theme.AppCompat.NoActionBar">
<item name="storyViewerIconTintColor">@color/your_accent</item>
<item name="storyViewerWatchButtonBackground">@drawable/your_button_bg</item>
<item name="storyViewerWatchButtonTextColor">@color/your_text_color</item>
<item name="storyViewerFontFamily">@font/your_font</item>
</style>Call StoryViewerScreen directly if your app already uses Compose Navigation:
StoryViewerScreen(
stories = items,
startIndex = 0,
style = StoryViewerStyle.Default.copy(watchButtonTextColor = MyColors.accent),
onFinished = { viewedIds, clickedLink ->
// mark viewedIds as viewed in your own model, handle clickedLink yourself
},
)Or launch the bundled StoryViewerActivity — same Intent/extras contract as the View flavor —
if you're not on Compose Navigation yet:
val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val viewedIds = result.data?.getStringArrayListExtra(StoryViewerActivity.EXTRA_VIEWED_IDS)
val clickedLink = result.data?.getStringExtra(StoryViewerActivity.EXTRA_CLICKED_LINK)
}
launcher.launch(StoryViewerActivity.newIntent(context, items, startIndex = 0))

