compose-reels

Introduction: Jetpack Compose library for Instagram Reels/TikTok style vertical video feeds with ExoPlayer pooling, pinch-to-zoom, and infinite scroll
More: Author   ReportBugs   
Tags:

A Jetpack Compose library for creating Instagram Reels / TikTok / YouTube Shorts style short-form video feeds.

Demo

Features

  • Vertical snap scrolling (VerticalPager)
  • Video & Image mixed feed support
  • ExoPlayer with player pooling (memory efficient)
  • Local media support — asset://, file://, content://, android.resource:// in addition to HTTP(S)
  • Disk cache (150 MB LRU) for seamless re-playback of network videos
  • Pinch-to-zoom with spring animation
  • Double-tap gesture detection
  • Long-press to boost playback to fast mode (TikTok-style 2x) with visual indicator
  • Play/Pause & Mute controls
  • Error handling with built-in retry UI (customizable)
  • Analytics callbacks (video start / pause with watch time / completed)
  • Infinite scroll support
  • Lifecycle-aware playback management
  • Fully customizable overlay UI

Installation

Add JitPack repository to your settings.gradle.kts:

dependencyResolutionManagement {
    repositories {
        maven { url = uri("https://jitpack.io") }
    }
}

Add dependency to your module's build.gradle.kts:

dependencies {
    implementation("com.github.manjees:compose-reels:1.1.0")
}

Usage

Basic Usage

data class ReelItem(
    val id: String,
    val videoUrl: String,
    val username: String
)

@Composable
fun ReelsScreen() {
    val items = remember { listOf(
        ReelItem("1", "https://example.com/video1.mp4", "user1"),
        ReelItem("2", "https://example.com/video2.mp4", "user2")
    )}

    val reelsState = rememberReelsState(pageCount = { items.size })

    ComposeReels(
        items = items,
        state = reelsState,
        mediaSource = { item -> MediaSource.Video(item.videoUrl) }
    )
}

With Custom Overlay

ComposeReels(
    items = items,
    state = reelsState,
    config = ReelsConfig(
        autoPlay = true,
        isZoomEnabled = true,
        infiniteScroll = true
    ),
    mediaSource = { item -> MediaSource.Video(item.videoUrl) },
    onDoubleTap = { index, item -> /* Handle like */ },
    onSingleTap = { index, item -> reelsState.togglePlayPause() }
) { item ->
    // Your custom overlay UI
    Box(modifier = Modifier.fillMaxSize()) {
        Text(
            text = "@${item.username}",
            modifier = Modifier
                .align(Alignment.BottomStart)
                .padding(16.dp),
            color = Color.White
        )
    }
}

Mixed Media (Video + Image)

ComposeReels(
    items = items,
    state = reelsState,
    mediaSource = { item ->
        when {
            item.isVideo -> MediaSource.Video(item.url, item.thumbnailUrl)
            else -> MediaSource.Image(item.url)
        }
    }
)

Local Media

Videos and images don't have to come from the network — any URI scheme supported by ExoPlayer and Coil works. Useful for bundled content, downloaded files, or content picked via MediaStore.

// Video bundled in app assets (place file under src/main/assets/)
MediaSource.Video("asset:///clip.mp4")

// Video downloaded to the app's internal storage
MediaSource.Video("file:///${context.filesDir}/downloads/clip.mp4")

// Video via ContentResolver (e.g. from the photo picker)
MediaSource.Video(pickedUri.toString())

// Image bundled in app assets (Coil's asset URI form)
MediaSource.Image("file:///android_asset/photo.jpg")

Error Handling

When a video fails to load, a built-in retry UI is shown and onError is invoked.

ComposeReels(
    items = items,
    state = reelsState,
    mediaSource = { MediaSource.Video(it.videoUrl) },
    onError = { index, item, error ->
        Log.e("Reels", "Failed at $index: ${error.errorCodeName}")
    }
)

Override the retry UI with your own composable via errorContent:

ComposeReels(
    items = items,
    state = reelsState,
    mediaSource = { MediaSource.Video(it.videoUrl) },
    errorContent = { item, error ->
        // `this` is a BoxScope — align, pad, etc. as needed
        Column(
            modifier = Modifier.align(Alignment.Center),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text("Couldn't load video", color = Color.White)
            TextButton(onClick = { /* trigger your own retry flow */ }) {
                Text("Try again")
            }
        }
    }
)

Analytics

Pass a ReelsAnalytics to track playback events. All callbacks are optional.

ComposeReels(
    items = items,
    state = reelsState,
    mediaSource = { MediaSource.Video(it.videoUrl) },
    analytics = ReelsAnalytics(
        onVideoStart = { index ->
            tracker.log("video_start", mapOf("index" to index))
        },
        onVideoPaused = { index, watchTimeMs ->
            tracker.log("video_pause", mapOf("index" to index, "ms" to watchTimeMs))
        },
        onVideoCompleted = { index ->
            tracker.log("video_complete", mapOf("index" to index))
        }
    )
)

Configuration

ReelsConfig(
    autoPlay = true,           // Auto-play videos
    isZoomEnabled = true,      // Enable pinch-to-zoom
    isMuted = false,           // Start muted
    infiniteScroll = false,    // Loop back to first item
    preloadCount = 2,          // Preload N items in both directions
    playerPoolSize = 7,        // Max ExoPlayer instances — defaults to (preloadCount * 2) + 3
    longPressFastPlaybackEnabled = true, // Enable long-press fast playback
    longPressFastPlaybackSpeed = 2f      // Speed multiplier while pressing
)

Note: playerPoolSize must be at least (preloadCount * 2) + 1 to support preloading in both directions plus the current page. The default adds two extra slots of fling-time headroom. Use ReelsConfig.minPoolSizeFor(preloadCount) to read the hard minimum.

ReelsState API

val reelsState = rememberReelsState(pageCount = { items.size })

// Properties
reelsState.currentPage      // Current page index
reelsState.isPlaying        // Playback state
reelsState.isMuted          // Mute state
reelsState.isZoomed         // Zoom state
reelsState.playbackSpeed    // Current playback speed multiplier

// Methods
reelsState.togglePlayPause()
reelsState.toggleMute()
reelsState.play()
reelsState.pause()
reelsState.setPlaybackSpeed(speed)
reelsState.scrollToPage(index)
reelsState.animateScrollToPage(index)

Requirements

  • Min SDK: 24
  • Target SDK: 34+
  • Jetpack Compose

License

Copyright 2024 manjees

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0
Apps
About Me
GitHub: Trinea
Facebook: Dev Tools