Flutter Wallpaper Plus
Production-grade Flutter plugin for setting image, Wallpaper Auto Change, and video (live) wallpapers on Android.
- Set wallpaper from asset, file, or URL
- Run Wallpaper Auto Change playlists from pre-resolved image sources
- Use typed results and structured error codes
- Generate and cache video thumbnails
- Manage cache with size limits + LRU eviction
- Handle OEM target limitations with
getTargetSupportPolicy() - Normalize oversized image sources before wallpaper apply for better stability
- Fixed: Now works on Xiaomi/Redmi/Oppo/Vivo/Realme devices with sequential writes

Support
If you find this plugin useful, please consider supporting development:
Table of Contents
- Platform Support
- Installation
- Android Permissions
- Quick Start
- API Reference
- Target Behavior and OEM Limitations
- Real Device Results
- Host App Lifecycle
- FAQ
- Architecture
- Roadmap
- License
Platform Support
- Flutter:
>=3.3.0 - Dart:
^3.11.0 - Platform: Android only
- Android API: 24+
| Capability | Android Support |
|---|---|
| Static image wallpaper | API 24+ |
| Wallpaper Auto Change (images) | API 24+ |
| Live video wallpaper | API 24+ (device must support live wallpaper feature) |
| Video thumbnail generation | API 24+ |
Installation
Add the package:
dependencies:
flutter_wallpaper_plus: ^1.1.1
Install dependencies:
flutter pub get
Android Permissions
Permissions are declared by the plugin manifest:
<uses-permission android:name="android.permission.SET_WALLPAPER" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
Permission notes:
SET_WALLPAPER,INTERNET, andACCESS_NETWORK_STATEare normal permissions (auto-granted).- Storage permissions are only needed for
WallpaperSource.file(...)when reading outside app-internal storage. WallpaperSource.asset(...)andWallpaperSource.url(...)do not require storage read permission.- Oversized image sources are normalized before apply to reduce memory pressure in the system wallpaper service.
Quick Start
Import once:
import 'package:flutter_wallpaper_plus/flutter_wallpaper_plus.dart';
Set Image Wallpaper
final result = await FlutterWallpaperPlus.setImageWallpaper(
source: WallpaperSource.url('https://example.com/wallpaper.jpg'),
target: WallpaperTarget.home,
);
if (result.success) {
print('Applied: ${result.message}');
} else {
print('Failed: ${result.errorCode.name} - ${result.message}');
}
Notes:
- On Android 12+ (
API 31+), changing the wallpaper may also update the system's Material You / dynamic color palette. - This plugin does not provide a separate
setMaterialYouWallpaper(...)API because that color extraction is handled by Android itself after a normal wallpaper change.
Wallpaper Auto Change
final result = await FlutterWallpaperPlus.startWallpaperAutoChange(
sources: [
WallpaperSource.asset('assets/walls/one.jpg'),
WallpaperSource.url('https://example.com/walls/two.jpg'),
WallpaperSource.file('/storage/emulated/0/Pictures/three.jpg'),
],
target: WallpaperTarget.both,
interval: const Duration(minutes: 30),
);
if (result.success) {
final status = await FlutterWallpaperPlus.getWallpaperAutoChangeStatus();
print('Running: ${status.isRunning}, next index: ${status.nextIndex}');
}
Notes:
- V1 is image-only.
- The minimum supported interval is 1 minute.
- Scheduling is best-effort, not exact to the second. Android background policies can still defer runs slightly.
- All sources are resolved and copied into app-internal storage before scheduling, so future runs do not depend on Flutter asset access or live network fetches.
- Use
applyNextWallpaperNow()for a manual advance andstopWallpaperAutoChange()to cancel the active playlist.
Set Video (Live) Wallpaper
final result = await FlutterWallpaperPlus.setVideoWallpaper(
source: WallpaperSource.url('https://example.com/live.mp4'),
target: WallpaperTarget.home,
enableAudio: false,
loop: true,
goToHome: false,
);
Notes:
- Android opens the system live wallpaper chooser and user confirmation is required.
WallpaperTarget.lockis not supported for live video wallpaper (WallpaperErrorCode.unsupported).- Live wallpaper service runs independently after apply (survives app process death).
- Selected live wallpaper video is persisted in app-internal storage so it remains available even after cache cleanup.
Open Native Wallpaper Chooser
final result = await FlutterWallpaperPlus.openNativeWallpaperChooser(
source: WallpaperSource.url('https://example.com/wallpaper.jpg'),
goToHome: true, // optional best-effort: minimize app before chooser
);
source is required and supports:
WallpaperSource.asset(...)WallpaperSource.file(...)WallpaperSource.url(...)
Source path/URL must be non-empty.
Get Device Target Support Policy
Use this before rendering target options in UI:
final policy = await FlutterWallpaperPlus.getTargetSupportPolicy();
if (!policy.allowImageBoth) {
// Disable "Image -> Both" action in UI
}
Generate Video Thumbnail
final bytes = await FlutterWallpaperPlus.getVideoThumbnail(
source: WallpaperSource.url('https://example.com/video.mp4'),
quality: 50,
cache: true,
);
if (bytes != null) {
// Example: Image.memory(bytes)
}
Cache Management
final size = await FlutterWallpaperPlus.getCacheSize();
print('Cache bytes: $size');
await FlutterWallpaperPlus.setMaxCacheSize(100 * 1024 * 1024); // 100 MB
final clearResult = await FlutterWallpaperPlus.clearCache();
print(clearResult.message);
API Reference
FlutterWallpaperPlus
| Method | Returns | Description |
|---|---|---|
setImageWallpaper(...) |
Future<WallpaperResult> |
Apply static image wallpaper |
startWallpaperAutoChange(...) |
Future<WallpaperResult> |
Start image Auto Change playlist |
stopWallpaperAutoChange() |
Future<WallpaperResult> |
Stop Auto Change and clear active playlist |
getWallpaperAutoChangeStatus() |
Future<WallpaperAutoChangeStatus> |
Read Auto Change status snapshot |
applyNextWallpaperNow() |
Future<WallpaperResult> |
Apply the next Auto Change image immediately |
setVideoWallpaper(...) |
Future<WallpaperResult> |
Launch system chooser for live wallpaper |
openNativeWallpaperChooser(...) |
Future<WallpaperResult> |
Open native chooser with provided source (asset/file/url) |
getVideoThumbnail(...) |
Future<Uint8List?> |
Extract video thumbnail bytes |
getTargetSupportPolicy() |
Future<TargetSupportPolicy> |
Get device/OEM target reliability policy |
clearCache() |
Future<WallpaperResult> |
Clear cached media + thumbnails |
getCacheSize() |
Future<int> |
Read cache size in bytes |
setMaxCacheSize(int) |
Future<void> |
Set max cache size (bytes) |
WallpaperSource
| Constructor | Meaning |
|---|---|
WallpaperSource.asset(path) |
Flutter asset declared in pubspec.yaml |
WallpaperSource.file(path) |
Absolute file path on Android device |
WallpaperSource.url(url) |
Remote URL (downloaded and cached) |
WallpaperTarget
| Value | Static image | Live video |
|---|---|---|
home |
Supported | Supported |
lock |
Supported on compatible devices/OEMs | Unsupported by Android public APIs |
both |
Supported on compatible devices/OEMs | System chooser controlled; restricted on some OEMs |
WallpaperResult
| Field | Type | Description |
|---|---|---|
success |
bool |
Operation success flag |
message |
String |
Human-readable outcome |
errorCode |
WallpaperErrorCode |
Structured code for handling |
isError |
bool |
Convenience getter (!success) |
WallpaperAutoChangeStatus
| Field | Type | Description |
|---|---|---|
isRunning |
bool |
Whether Auto Change is active |
intervalMinutes |
int |
Configured interval in minutes |
nextIndex |
int |
Zero-based index of the next image to apply |
totalCount |
int |
Number of prepared wallpapers in the playlist |
nextRunEpochMs |
int |
Next scheduled run time in epoch milliseconds |
target |
WallpaperTarget |
Active target screen(s) |
lastError |
String? |
Last recorded native error, if any |
WallpaperErrorCode
nonesourceNotFounddownloadFailedunsupportedpermissionDeniedapplyFailedvideoErrorthumbnailFailedcacheFailedmanufacturerRestrictionunknown
Target Behavior and OEM Limitations
This section is important for product behavior and user expectations.
Live Wallpaper Targeting Rules
- Live wallpaper always uses Android's native chooser flow.
WallpaperTarget.lockfor live video is unsupported on public Android APIs.- Final behavior for live wallpapers can still be altered by OEM system apps/policies.
Known OEM Restrictions
On some OEM ROMs (commonly Xiaomi/Redmi/Oppo/Vivo/Realme), lock-screen wallpaper behavior may be controlled by system policy (carousel/slideshow/theme engines). In these environments:
- Third-party apps may not reliably force lock wallpaper persistence.
- Lock/both behavior can diverge from what user selected in the system UI.
- Similar behavior is likely on other heavily customized OEM ROMs with lock-screen themes/carousel features.
v1.0.2 Fix: The plugin now uses sequential writes with 500ms delay between home and lock screen wallpaper sets, matching the working approach from async_wallpaper. This significantly improves reliability on restrictive OEMs. The plugin no longer blocks lock/both targets - it attempts them and relies on the sequential write approach for success.
v1.0.4 Fix: Image lock and both targets now retry with a bitmap-based compatibility fallback after the normalized stream apply path. This better matches the smoother lock-screen behavior seen in older async_wallpaper builds on restrictive OEM ROMs.
Real Device Results
Observed behavior from real testing (as of February 24, 2026):
| Device | Image both |
Video both |
Notes |
|---|---|---|---|
| Pixel 6 (emulator) | Works | Works | Smooth and expected behavior |
| Oppo Reno Z | Works (v1.0.2+) | Works | Sequential writes fix |
| Redmi 13C | Works (v1.0.2+) | Works | Sequential writes fix |
v1.0.2 Update: The sequential write approach (500ms delay between home and lock) fixes the previous failures on Oppo and Redmi devices.
Recommended App-Side UX
- Query
getTargetSupportPolicy()before showing target options. - Disable unreliable target choices in your UI.
- Prefer
hometarget by default on restrictive OEMs. - Explain to users when device policy may override lock-screen wallpaper behavior.
Host App Lifecycle
On some devices/ROMs, applying wallpaper can cause the host FlutterActivity
to be torn down and recreated. In Logcat this may look like a "cold restart"
or FlutterEngine detach/reattach cycle even when wallpaper apply itself succeeds.
This is controlled by the host app lifecycle, not just by the plugin. The plugin
cannot force your app to keep its existing FlutterEngine, so if you see this
behavior, make your MainActivity more resilient:
- Use
RenderMode.texture - Reuse a cached
FlutterEngine - Return
falsefromshouldDestroyEngineWithHost() - Destroy the cached engine when the activity is genuinely finishing so an intentional app exit does not reopen the old Dart isolate
Example MainActivity.kt:
package com.example.my_app
import android.content.Context
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.android.RenderMode
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterEngineCache
import io.flutter.embedding.engine.dart.DartExecutor
class MainActivity : FlutterActivity() {
companion object {
private const val ENGINE_ID = "wallpaper_engine"
private fun clearCachedEngine() {
val cache = FlutterEngineCache.getInstance()
val engine = cache.get(ENGINE_ID) ?: return
cache.remove(ENGINE_ID)
engine.destroy()
}
}
override fun getRenderMode(): RenderMode = RenderMode.texture
override fun provideFlutterEngine(context: Context): FlutterEngine {
FlutterEngineCache.getInstance().get(ENGINE_ID)?.let { return it }
val engine = FlutterEngine(context.applicationContext)
engine.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault(),
)
FlutterEngineCache.getInstance().put(ENGINE_ID, engine)
return engine
}
override fun shouldDestroyEngineWithHost(): Boolean = false
override fun onDestroy() {
val shouldClearEngine = isFinishing && !isChangingConfigurations
super.onDestroy()
if (shouldClearEngine) {
clearCachedEngine()
}
}
}
shouldDestroyEngineWithHost() = false keeps the engine alive while Android
recreates the host activity during wallpaper flows. The extra onDestroy()
cleanup prevents the cached engine from surviving a real user exit, so reopening
the app starts fresh instead of resuming the old widget tree.
The full working example used by this package is in
example/android/app/src/main/kotlin/com/example/flutter_wallpaper_plus_example/MainActivity.kt.
FAQ
Does video wallpaper keep playing if the app is closed?
Yes. Playback is hosted in WallpaperService and is independent from your Flutter UI process.
Why does video wallpaper open a system screen?
Android requires user confirmation for live wallpaper selection.
Why does "Home only" sometimes affect lock screen?
Some OEMs mirror lock/home wallpaper behavior unless lock wallpaper is independently controlled by the ROM.
Why does lock wallpaper revert after a few seconds on some devices?
Some ROM features (for example lock-screen slideshow/carousel) can override third-party lock wallpaper shortly after apply.
Why does my Flutter app cold restart after wallpaper apply?
On some devices the wallpaper change flow can recreate the host Android activity.
If your app uses the default FlutterActivity setup, this may look like a full
restart even though the wallpaper apply succeeded. See
Host App Lifecycle for the recommended MainActivity
setup.
Why does my app reopen where the user left off after they closed it?
That happens when you cache a FlutterEngine to survive wallpaper-related
activity recreation, but never destroy it on a genuine app exit. Reuse the
engine during wallpaper flows, then evict and destroy it in onDestroy() when
isFinishing && !isChangingConfigurations is true. See
Host App Lifecycle for a copy-paste example.
Does this update Material You colors on Android 12+?
Usually yes, if the device/ROM refreshes dynamic colors from the new wallpaper. That behavior is controlled by Android, so this plugin applies the wallpaper normally and the system decides whether to regenerate the Material You palette.
Why can Wallpaper Auto Change still drift even with a 1-minute interval?
The plugin schedules the next run with WorkManager after each apply, so Auto Change can work below 15 minutes and still survive app restarts. The tradeoff is that Android treats this as background work, which means timing is best-effort and OEM battery policies can delay some runs.
What formats are supported?
- Video: device codec support via MediaCodec/ExoPlayer (MP4 H.264 recommended for best compatibility)
- Image: device-supported bitmap formats (JPEG/PNG/WebP/BMP, etc.)
Architecture
Flutter (Dart API)
-> MethodChannel (com.flutterwallpaperplus/methods)
-> Kotlin plugin layer
-> ImageWallpaperManager
-> WallpaperAutoChangeStore + Runner + Worker
-> VideoWallpaperService + ExoPlayer
-> ThumbnailGenerator
-> SourceResolver
-> CacheManager (LRU)
-> PermissionHelper
Roadmap
Planned next additions:
- Expand OEM policy coverage and reliability hints for more manufacturers
- Add more end-to-end integration testing across Android OEM variants
License
MIT. See LICENSE.
Libraries
- flutter_wallpaper_plus
- Flutter Wallpaper Plus — Production-grade wallpaper plugin for Android.
