RORK LABJP
PLAY — Google Play's target API level 36 requirement took effect yesterday, August 31. From today, new apps and updates must target Android 16VISIBILITY — Apps still on API 35 stay listed but disappear for users on newer Android versions. No error is raised; new installs simply fade, which makes the change easy to missEXTENSION — If you missed the deadline, an extension through November 1, 2026 can be requested in Play Console — best filed alongside a concrete migration planAPPLE — On the Apple side, the event lands September 9 and iOS 27 is reported to ship September 14. Testing generated apps on iOS 27 hardware before release week is time well spentEXPO — Expo released expo-paste-input on August 28, a native module that brings image, GIF, and sticker paste to React Native TextInputEAS — EAS Observe reached general availability on August 20, putting crash and performance monitoring on the same EAS platform as builds and updatesPLAY — Google Play's target API level 36 requirement took effect yesterday, August 31. From today, new apps and updates must target Android 16VISIBILITY — Apps still on API 35 stay listed but disappear for users on newer Android versions. No error is raised; new installs simply fade, which makes the change easy to missEXTENSION — If you missed the deadline, an extension through November 1, 2026 can be requested in Play Console — best filed alongside a concrete migration planAPPLE — On the Apple side, the event lands September 9 and iOS 27 is reported to ship September 14. Testing generated apps on iOS 27 hardware before release week is time well spentEXPO — Expo released expo-paste-input on August 28, a native module that brings image, GIF, and sticker paste to React Native TextInputEAS — EAS Observe reached general availability on August 20, putting crash and performance monitoring on the same EAS platform as builds and updates
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.

Android47RecyclerView3ViewPager2indie developer39wallpaper 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 $15 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.
AI Models2026-05-20
A month of running Rork and Claude on Xcode side by side — where each one ended up fitting
Notes from an indie developer who has shipped wallpaper apps on iOS and Android since 2014. After a month of keeping both Rork and Claude on Xcode open on the same desk, the work that fit each one turned out to be more obvious than I expected.
App Dev2026-08-18
The three places I had to fix before a Rork project actually targeted API level 36
My app.json said targetSdkVersion 36. The value my build actually read was 35. Here is the script that reports the effective value, and how I split my apps between raising, leaving alone, and requesting an extension.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →