◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/App Dev
◇ App Dev/2026-05-28Intermediate

Adding Slideshow and Page Jump to My Android Wallpaper Apps

Implementation notes from adding slideshow, a page-jump slider, and an ad-free option to the Android editions of Beautiful 4K/HDR Wallpapers and Ukiyo-e Wallpapers.

Android55RecyclerView3ViewPager2indie developer43wallpaper app22

Two requests had been turning up in user reviews for years. One was "let me lie in bed and have the wallpapers advance on their own," and the other was "there are too many wallpapers, I cannot reach the one I want." This week I shipped an update for the Android editions of Beautiful 4K/HDR Wallpapers and Ukiyo-e Wallpapers that adds a slideshow and a page-jump slider to address both. The same release also introduces an ad-free option, which is another long-standing request. The code itself is not exotic, but as an indie developer who has been running wallpaper apps since 2014, I want to leave a record of the moments where I hesitated and what I chose.

Threading auto-advance through the existing RecyclerView

A slideshow that "moves to the next wallpaper every three seconds" sounds trivial, but rolling your own Thread tends to leak across rotations and background transitions. I decided to keep the underlying RecyclerView that ViewPager2 drives, and orchestrate everything from a UI-thread Handler.

private val mainHandler = Handler(Looper.getMainLooper())
private val advanceRunnable = object : Runnable {
    override fun run() {
        val next = viewPager.currentItem + 1
        if (next < adapter.itemCount) {
            viewPager.setCurrentItem(next, true)  // smoothScroll = true
        } else {
            viewPager.setCurrentItem(0, true)     // loop back to the start
        }
        mainHandler.postDelayed(this, INTERVAL_MS)
    }
}
 
fun startSlideshow() {
    mainHandler.removeCallbacks(advanceRunnable)
    mainHandler.postDelayed(advanceRunnable, INTERVAL_MS)
}
 
fun stopSlideshow() {
    mainHandler.removeCallbacks(advanceRunnable)
}

The detail that mattered was catching "tap to stop" through ViewPager2.OnPageChangeCallback#onPageScrollStateChanged rather than onTouchEvent. When the user swipes manually, I want auto-advance and manual scroll never to fight each other, so I call stopSlideshow() the moment SCROLL_STATE_DRAGGING is observed. My first attempt only listened for taps, and it produced a bug where inertial scrolling kept advancing pages after the user had clearly tried to stop the show.

Page jump as a two-layer SeekBar with a lightweight preview

I wanted users to reach a specific image out of a hundred thousand with a single slider. The naive approach—calling viewPager.setCurrentItem(progress, false) straight from OnSeekBarChangeListener—runs layout every frame during the drag and stutters badly.

seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
    override fun onProgressChanged(sb: SeekBar?, progress: Int, fromUser: Boolean) {
        if (!fromUser) return
        previewLabel.text = "${progress + 1} / ${adapter.itemCount}"  // lightweight preview
    }
    override fun onStartTrackingTouch(sb: SeekBar?) {
        stopSlideshow()  // pause auto-advance while the user is scrubbing
    }
    override fun onStopTrackingTouch(sb: SeekBar?) {
        viewPager.setCurrentItem(sb?.progress ?: 0, false)  // commit only on release
    }
})

While the user drags, I only update a text label that reads "current / total." The real setCurrentItem call happens once, the moment the finger lifts. Even at a hundred-thousand-item scale the scrub stays smooth, and Glide's cancellation and reload only fire once at the final destination, which is friendlier to both memory and bandwidth.

How this interacts with the ad-free option

The "let me hide the ads" request has come in for years, so I bundled it into this release. Internally, the ad-free state is the OR of two independent sources of truth: permanent purchase or subscription on one side, and a time-bounded ad-free state earned by watching a rewarded ad on the other. The combined value is exposed as Globals.SHOW_AD. During an active slideshow, an interstitial would shatter the experience, so I added a guard that suppresses background interstitial calls while auto-advance is running.

A while back I had ad gating logic nested inside other gating logic, and the implicit priority order made debugging genuinely painful. Learning from that, I now keep the slideshow flag, ad-free flag, and ad counters as parallel, independent variables. When the state machine is flat, you can later trace "who suppressed whom" purely from logs.

Lock, rotation, and background transitions

The unglamorous fix that mattered most was: always call stopSlideshow() in onPause, and never auto-resume in onResume. If the slideshow keeps running while the device is locked, the user unlocks to find that several images have silently flown past.

For rotation, I save both the current page index and the "slideshow is running" flag in a ViewModel, and double up with onSaveInstanceState. Carrying both feels redundant, but at the cumulative download scale my wallpaper apps operate at—north of fifty million across the family—you see both clean configuration changes and full-Activity crash recoveries with non-trivial frequency, so the belt-and-suspenders setup earns its keep.

What I am still working on

The next items on my list are letting users adjust the slideshow interval from the Settings screen, adding a shortcut that sets the wallpaper directly from the lock screen, and back-porting the same machinery to the iOS edition. I grew up around grandfathers who were temple carpenters and treated "moving your hands" as a form of devotion, and that has stayed with me—I am still not at peace with some of the original release decisions in these apps. This update is one of the steps I am taking, slowly, to tidy them up.

If anything feels off while you use it, please leave a note in the Google Play review and I will reply by hand. There are behaviors only real devices reveal, and I plan to keep responding one review at a time. Thank you for reading.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

◇ App Dev2026-05-29
Two Weeks of Maintenance After v2.1.0 — Running Crashlytics Triage Through Claude in Chrome
Notes from operating Beautiful 4K/HDR Wallpapers v2.1.0 and Ukiyo-e Wallpapers v1.8.0 through phased rollout. Defensive RecyclerView copies, Glide desugaring, drawable-nodpi placement, and what I now hand off to Claude in Chrome every morning.
◇ App Dev2026-10-15
When Only EAS Build Fails with "No matching variant", Check Your Local android/build First
Gradle passes on your machine, but the Android build on EAS stops at "No matching variant of project". Reading the reports on GitHub, I traced how a Windows-generated autolinking cache can reach the Linux builder, and wrote a small script that checks for it before you queue a build.
◇ App Dev2026-09-23
It Crashed, but the Reviews Say "It Freezes" — When an Android Crash Leaves a Blank Screen Behind
Your crash reports show exceptions, yet users only ever say the app freezes. Here is why expo-updates plus the New Architecture can leave an empty Android screen behind, and the order I now use: a way out first, a bad-exit marker second, root cause last.