async_wallpaper 3.2.0 copy "async_wallpaper: ^3.2.0" to clipboard
async_wallpaper: ^3.2.0 copied to clipboard

Set wallpapers on Android asynchronously. Also supports video live wallpapers.

example/lib/main.dart

import 'dart:typed_data';

import 'package:async_wallpaper/async_wallpaper.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart';

const List<int> _demoPngBytes = <int>[
  137,
  80,
  78,
  71,
  13,
  10,
  26,
  10,
  0,
  0,
  0,
  13,
  73,
  72,
  68,
  82,
  0,
  0,
  0,
  1,
  0,
  0,
  0,
  1,
  8,
  6,
  0,
  0,
  0,
  31,
  21,
  196,
  137,
  0,
  0,
  0,
  10,
  73,
  68,
  65,
  84,
  120,
  156,
  99,
  96,
  0,
  0,
  0,
  2,
  0,
  1,
  229,
  39,
  212,
  162,
  0,
  0,
  0,
  0,
  73,
  69,
  78,
  68,
  174,
  66,
  96,
  130,
];

/// A deliberately small GLSL ES 1.00 shader that needs no textures or input.
///
/// It only uses the standard `v_uv` and `u_time` values supplied by the
/// plugin's OpenGL renderer, keeping the example safe to run on capable
/// devices without taking arbitrary shader text from the user.
const String _safeOpenGlFragmentShader = '''
precision mediump float;
varying vec2 v_uv;
uniform float u_time;

void main() {
  float pulse = 0.5 + 0.5 * sin(u_time);
  gl_FragColor = vec4(v_uv.x, v_uv.y, pulse, 1.0);
}
''';

/// Small seam that keeps the example's widgets testable without a platform
/// channel or a package-owned notification mechanism.
abstract interface class WallpaperDemoApi {
  Future<WallpaperCapabilities> getCapabilities();

  Future<WallpaperOperationResult> applyWallpaper(
    StaticWallpaperRequest request,
  );

  Future<WallpaperOperationResult> setVideoWallpaper(
    VideoWallpaperRequest request,
  );

  Future<WallpaperOperationResult> openLiveWallpaperPreview(
    VideoWallpaperRequest request,
  );

  Future<WallpaperOperationResult> setOpenGlLiveWallpaper(
    OpenGlLiveWallpaperRequest request,
  );
}

class AsyncWallpaperDemoApi implements WallpaperDemoApi {
  const AsyncWallpaperDemoApi();

  @override
  Future<WallpaperCapabilities> getCapabilities() =>
      AsyncWallpaper.getCapabilities();

  @override
  Future<WallpaperOperationResult> applyWallpaper(
    StaticWallpaperRequest request,
  ) => AsyncWallpaper.applyWallpaper(request);

  @override
  Future<WallpaperOperationResult> setVideoWallpaper(
    VideoWallpaperRequest request,
  ) => AsyncWallpaper.setVideoWallpaper(request);

  @override
  Future<WallpaperOperationResult> openLiveWallpaperPreview(
    VideoWallpaperRequest request,
  ) => AsyncWallpaper.openLiveWallpaperPreview(request);

  @override
  Future<WallpaperOperationResult> setOpenGlLiveWallpaper(
    OpenGlLiveWallpaperRequest request,
  ) => AsyncWallpaper.setOpenGlLiveWallpaper(request);
}

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key, this.api = const AsyncWallpaperDemoApi()});

  final WallpaperDemoApi api;

  @override
  Widget build(BuildContext context) {
    return DynamicColorBuilder(
      builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
        final ColorScheme scheme =
            lightDynamic ?? ColorScheme.fromSeed(seedColor: Colors.blue);
        return MaterialApp(
          title: 'Async Wallpaper Example',
          theme: ThemeData(colorScheme: scheme, useMaterial3: true),
          home: HomePage(api: api),
        );
      },
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key, this.api = const AsyncWallpaperDemoApi()});

  final WallpaperDemoApi api;

  @override
  State<HomePage> createState() => _HomePageState();
}

enum _DemoSourceKind { url, filePath, contentUri, bytes }

class _HomePageState extends State<HomePage> {
  final TextEditingController _urlController = TextEditingController(
    text: 'https://images.unsplash.com/photo-1635593701810-3156162e184f',
  );
  final TextEditingController _fileController = TextEditingController(
    text: '/storage/emulated/0/Download/wallpaper.jpg',
  );
  final TextEditingController _contentUriController = TextEditingController(
    text: 'content://media/external/images/media/1',
  );

  _DemoSourceKind _sourceKind = _DemoSourceKind.url;
  WallpaperTarget _target = WallpaperTarget.both;
  WallpaperScaleMode _scaleMode = WallpaperScaleMode.centerCrop;
  WallpaperApplyStrategy _strategy = WallpaperApplyStrategy.automatic;
  String _status = 'Ready. Choose a source and an action.';
  String? _activeAction;
  WallpaperCapabilities? _capabilities;
  WallpaperOperationResult? _staticResult;
  WallpaperOperationResult? _videoPreparationResult;
  WallpaperOperationResult? _videoPreviewResult;
  WallpaperOperationResult? _openGlResult;

  bool get _isBusy => _activeAction != null;

  @override
  void dispose() {
    _urlController.dispose();
    _fileController.dispose();
    _contentUriController.dispose();
    super.dispose();
  }

  Future<void> _loadCapabilities() async {
    if (_isBusy) {
      return;
    }

    setState(() {
      _activeAction = 'capabilities';
      _status = 'Loading wallpaper capabilities…';
    });

    try {
      final WallpaperCapabilities capabilities = await widget.api
          .getCapabilities();
      if (!mounted) {
        return;
      }
      setState(() {
        _capabilities = capabilities;
        _status = 'Capabilities loaded for ${capabilities.manufacturer}.';
      });
    } catch (error) {
      if (!mounted) {
        return;
      }
      setState(() {
        _status = 'Could not load capabilities: $error';
      });
    } finally {
      if (mounted) {
        setState(() => _activeAction = null);
      }
    }
  }

  Future<void> _runOperation({
    required String action,
    required String startingStatus,
    required Future<WallpaperOperationResult> Function() operation,
    required void Function(WallpaperOperationResult result) storeResult,
  }) async {
    if (_isBusy) {
      return;
    }

    setState(() {
      _activeAction = action;
      _status = startingStatus;
    });

    try {
      final WallpaperOperationResult result = await operation();
      if (!mounted) {
        return;
      }
      setState(() {
        storeResult(result);
        _status = '$action: ${_resultSummary(result)}';
      });
    } catch (error) {
      if (!mounted) {
        return;
      }
      setState(() {
        _status = '$action failed: $error';
      });
    } finally {
      if (mounted) {
        setState(() => _activeAction = null);
      }
    }
  }

  Future<void> _applyStaticWallpaper() {
    return _runOperation(
      action: 'Apply static wallpaper',
      startingStatus: 'Applying static wallpaper…',
      operation: () => widget.api.applyWallpaper(
        StaticWallpaperRequest(
          source: _selectedSource(),
          target: _target,
          scaleMode: _scaleMode,
          strategy: _strategy,
        ),
      ),
      storeResult: (WallpaperOperationResult result) {
        _staticResult = result;
      },
    );
  }

  Future<void> _prepareVideoWallpaper() {
    return _runOperation(
      action: 'Prepare video wallpaper',
      startingStatus: 'Preparing video wallpaper…',
      operation: () => widget.api.setVideoWallpaper(_videoRequest()),
      storeResult: (WallpaperOperationResult result) {
        _videoPreparationResult = result;
      },
    );
  }

  Future<void> _openVideoPreview() {
    return _runOperation(
      action: 'Open live wallpaper preview',
      startingStatus: 'Opening live wallpaper preview…',
      operation: () => widget.api.openLiveWallpaperPreview(_videoRequest()),
      storeResult: (WallpaperOperationResult result) {
        _videoPreviewResult = result;
      },
    );
  }

  Future<void> _applyOpenGlWallpaper() {
    return _runOperation(
      action: 'OpenGL live wallpaper',
      startingStatus: 'Opening the OpenGL live wallpaper flow…',
      operation: () => widget.api.setOpenGlLiveWallpaper(
        OpenGlLiveWallpaperRequest(
          fragmentShader: _safeOpenGlFragmentShader,
          target: _target,
          frameRate: 30,
        ),
      ),
      storeResult: (WallpaperOperationResult result) {
        _openGlResult = result;
      },
    );
  }

  VideoWallpaperRequest _videoRequest() {
    return VideoWallpaperRequest(
      source: _selectedSource(),
      target: _target,
      scaleMode: _scaleMode,
    );
  }

  WallpaperSource _selectedSource() {
    switch (_sourceKind) {
      case _DemoSourceKind.url:
        return WallpaperSource.url(_requiredInput(_urlController.text, 'URL'));
      case _DemoSourceKind.filePath:
        return WallpaperSource.filePath(
          _requiredInput(_fileController.text, 'File path'),
        );
      case _DemoSourceKind.contentUri:
        return WallpaperSource.contentUri(
          _requiredInput(_contentUriController.text, 'Content URI'),
        );
      case _DemoSourceKind.bytes:
        return WallpaperSource.bytes(Uint8List.fromList(_demoPngBytes));
    }
  }

  String _requiredInput(String value, String label) {
    final String trimmed = value.trim();
    if (trimmed.isEmpty) {
      throw ArgumentError('$label cannot be empty.');
    }
    return trimmed;
  }

  String _resultSummary(WallpaperOperationResult result) {
    final List<String> details = <String>[result.status.name];
    if (result.fallbackUsed) {
      details.add(
        'fallback ${result.fallbackStrategy?.name ?? 'strategy'} used',
      );
    }
    if (result.errorCode != null) {
      details.add(result.errorCode!);
    }
    if (result.errorMessage != null) {
      details.add(result.errorMessage!);
    }
    return details.join(' — ');
  }

  String _targetOutcome(
    String targetLabel,
    WallpaperTargetResult? targetResult,
  ) {
    if (targetResult == null) {
      return '$targetLabel: not reported';
    }
    final List<String> details = <String>[targetResult.status.name];
    if (targetResult.errorCode != null) {
      details.add(targetResult.errorCode!);
    }
    if (targetResult.errorMessage != null) {
      details.add(targetResult.errorMessage!);
    }
    return '$targetLabel: ${details.join(' — ')}';
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Async Wallpaper 3.2 example')),
      body: SafeArea(
        child: ListView(
          padding: const EdgeInsets.all(16),
          children: <Widget>[
            _statusCard(context),
            const SizedBox(height: 16),
            _sourceAndStaticControls(context),
            const SizedBox(height: 16),
            _capabilitiesCard(context),
            const SizedBox(height: 16),
            _videoCard(context),
            const SizedBox(height: 16),
            _openGlCard(context),
          ],
        ),
      ),
    );
  }

  Widget _statusCard(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Semantics(
          liveRegion: true,
          label: 'Operation status: $_status',
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text('Status', style: Theme.of(context).textTheme.titleMedium),
              const SizedBox(height: 8),
              Row(
                children: <Widget>[
                  if (_isBusy)
                    const Padding(
                      padding: EdgeInsets.only(right: 12),
                      child: SizedBox.square(
                        dimension: 18,
                        child: CircularProgressIndicator(strokeWidth: 2),
                      ),
                    ),
                  Expanded(child: Text(_status, key: const Key('status-text'))),
                ],
              ),
              if (_isBusy) ...<Widget>[
                const SizedBox(height: 8),
                Text('One action runs at a time: $_activeAction.'),
              ],
            ],
          ),
        ),
      ),
    );
  }

  Widget _sourceAndStaticControls(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text(
              'Static wallpaper',
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const SizedBox(height: 8),
            const Text(
              'This demo uses controlled values instead of a file-picker dependency. '
              'Replace the text with a source from your app.',
            ),
            const SizedBox(height: 16),
            DropdownButtonFormField<_DemoSourceKind>(
              key: const Key('source-selector'),
              initialValue: _sourceKind,
              decoration: const InputDecoration(labelText: 'Source type'),
              items: _DemoSourceKind.values
                  .map(
                    (_DemoSourceKind kind) => DropdownMenuItem<_DemoSourceKind>(
                      value: kind,
                      child: Text(_sourceKindLabel(kind)),
                    ),
                  )
                  .toList(growable: false),
              onChanged: _isBusy
                  ? null
                  : (_DemoSourceKind? value) {
                      if (value != null) {
                        setState(() => _sourceKind = value);
                      }
                    },
            ),
            const SizedBox(height: 12),
            _sourceInput(),
            const SizedBox(height: 12),
            DropdownButtonFormField<WallpaperTarget>(
              key: const Key('target-selector'),
              initialValue: _target,
              decoration: const InputDecoration(labelText: 'Requested target'),
              items: WallpaperTarget.values
                  .map(
                    (WallpaperTarget target) =>
                        DropdownMenuItem<WallpaperTarget>(
                          value: target,
                          child: Text(_enumLabel(target.name)),
                        ),
                  )
                  .toList(growable: false),
              onChanged: _isBusy
                  ? null
                  : (WallpaperTarget? value) {
                      if (value != null) {
                        setState(() => _target = value);
                      }
                    },
            ),
            const SizedBox(height: 12),
            DropdownButtonFormField<WallpaperScaleMode>(
              key: const Key('scale-selector'),
              initialValue: _scaleMode,
              decoration: const InputDecoration(labelText: 'Scale mode'),
              items: WallpaperScaleMode.values
                  .map(
                    (WallpaperScaleMode mode) =>
                        DropdownMenuItem<WallpaperScaleMode>(
                          value: mode,
                          child: Text(_enumLabel(mode.name)),
                        ),
                  )
                  .toList(growable: false),
              onChanged: _isBusy
                  ? null
                  : (WallpaperScaleMode? value) {
                      if (value != null) {
                        setState(() => _scaleMode = value);
                      }
                    },
            ),
            const SizedBox(height: 12),
            DropdownButtonFormField<WallpaperApplyStrategy>(
              key: const Key('strategy-selector'),
              initialValue: _strategy,
              decoration: const InputDecoration(labelText: 'Apply strategy'),
              items: WallpaperApplyStrategy.values
                  .map(
                    (WallpaperApplyStrategy strategy) =>
                        DropdownMenuItem<WallpaperApplyStrategy>(
                          value: strategy,
                          child: Text(_enumLabel(strategy.name)),
                        ),
                  )
                  .toList(growable: false),
              onChanged: _isBusy
                  ? null
                  : (WallpaperApplyStrategy? value) {
                      if (value != null) {
                        setState(() => _strategy = value);
                      }
                    },
            ),
            FilledButton.icon(
              key: const Key('apply-static-button'),
              onPressed: _isBusy ? null : _applyStaticWallpaper,
              icon: const Icon(Icons.wallpaper_outlined),
              label: const Text('Apply static wallpaper'),
            ),
            const SizedBox(height: 16),
            _staticOutcome(context),
          ],
        ),
      ),
    );
  }

  Widget _sourceInput() {
    switch (_sourceKind) {
      case _DemoSourceKind.url:
        return TextField(
          key: const Key('url-source-input'),
          controller: _urlController,
          enabled: !_isBusy,
          keyboardType: TextInputType.url,
          decoration: const InputDecoration(
            labelText: 'HTTPS image or video URL',
            helperText: 'Use an HTTPS URL that your app is allowed to fetch.',
          ),
        );
      case _DemoSourceKind.filePath:
        return TextField(
          key: const Key('file-source-input'),
          controller: _fileController,
          enabled: !_isBusy,
          decoration: const InputDecoration(
            labelText: 'Local file path',
            helperText: 'Pass the path returned by your own picker or cache.',
          ),
        );
      case _DemoSourceKind.contentUri:
        return TextField(
          key: const Key('uri-source-input'),
          controller: _contentUriController,
          enabled: !_isBusy,
          keyboardType: TextInputType.url,
          decoration: const InputDecoration(
            labelText: 'Android content URI',
            helperText:
                'Persist access in your app if the URI outlives a session.',
          ),
        );
      case _DemoSourceKind.bytes:
        return const DecoratedBox(
          decoration: BoxDecoration(color: Color(0x11000000)),
          child: Padding(
            padding: EdgeInsets.all(12),
            child: Text(
              'Embedded bytes: a bundled 1×1 PNG is used for this demo. '
              'Replace WallpaperSource.bytes(...) with your image bytes.',
            ),
          ),
        );
    }
  }

  Widget _staticOutcome(BuildContext context) {
    final WallpaperOperationResult? result = _staticResult;
    return _resultPanel(
      context,
      title: 'Static outcome',
      result: result,
      children: <Widget>[
        Text(_targetOutcome('Home', result?.home)),
        Text(_targetOutcome('Lock', result?.lock)),
      ],
    );
  }

  Widget _capabilitiesCard(BuildContext context) {
    final WallpaperCapabilities? capabilities = _capabilities;
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text('Capabilities', style: Theme.of(context).textTheme.titleLarge),
            const SizedBox(height: 8),
            if (capabilities == null)
              const Text(
                'Not loaded yet. Check before exposing optional flows.',
              )
            else ...<Widget>[
              Text('Manufacturer: ${capabilities.manufacturer}'),
              Text('SDK: ${capabilities.sdkInt}'),
              Text(
                'Static: ${_supportLabel(capabilities.supportsStaticWallpaper)} · '
                'Video: ${_supportLabel(capabilities.supportsLiveWallpaper)} · '
                'OpenGL: ${_supportLabel(capabilities.supportsOpenGlLiveWallpaper)}',
              ),
              Text(
                'Home: ${_supportLabel(capabilities.supportsHomeWallpaper)} · '
                'Lock: ${_supportLabel(capabilities.supportsLockWallpaper)} · '
                'Both: ${_supportLabel(capabilities.supportsBothWallpapers)}',
              ),
              Text(
                'System picker: ${_supportLabel(capabilities.hasSystemWallpaperPicker)} · '
                'Foreground required: ${capabilities.requiresForeground ? 'yes' : 'no'}',
              ),
              if (capabilities.openGlVersion != null)
                Text('OpenGL: ${capabilities.openGlVersion}'),
              if (capabilities.openGlRenderer != null)
                Text('Renderer: ${capabilities.openGlRenderer}'),
            ],
            const SizedBox(height: 12),
            OutlinedButton.icon(
              key: const Key('refresh-capabilities-button'),
              onPressed: _isBusy ? null : _loadCapabilities,
              icon: const Icon(Icons.refresh),
              label: const Text('Refresh capabilities'),
            ),
          ],
        ),
      ),
    );
  }

  Widget _videoCard(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text(
              'Video live wallpaper',
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const SizedBox(height: 8),
            const Text(
              'Prepare validates and stages the selected source. Preview opens '
              'Android’s live-wallpaper UI; it is not proof that the user applied it.',
            ),
            const SizedBox(height: 12),
            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: <Widget>[
                FilledButton.tonalIcon(
                  key: const Key('prepare-video-button'),
                  onPressed: _isBusy ? null : _prepareVideoWallpaper,
                  icon: const Icon(Icons.movie_creation_outlined),
                  label: const Text('Prepare video'),
                ),
                FilledButton.tonalIcon(
                  key: const Key('preview-video-button'),
                  onPressed: _isBusy ? null : _openVideoPreview,
                  icon: const Icon(Icons.preview_outlined),
                  label: const Text('Open live preview'),
                ),
              ],
            ),
            const SizedBox(height: 12),
            Text(
              'Video preparation: '
              '${_videoPreparationResult?.status.name ?? 'not run'}',
            ),
            Text(
              'Video preview: ${_videoPreviewResult?.status.name ?? 'not run'}',
            ),
          ],
        ),
      ),
    );
  }

  Widget _openGlCard(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text(
              'OpenGL live wallpaper',
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const SizedBox(height: 8),
            const Text(
              'Uses a bundled GLSL ES 1.00 color-pulse shader at 30 FPS. '
              'The example does not execute user-supplied shader text or download textures.',
            ),
            const SizedBox(height: 12),
            FilledButton.tonalIcon(
              key: const Key('apply-opengl-button'),
              onPressed: _isBusy ? null : _applyOpenGlWallpaper,
              icon: const Icon(Icons.animation_outlined),
              label: const Text('Open OpenGL wallpaper flow'),
            ),
            const SizedBox(height: 12),
            _resultPanel(
              context,
              title: 'OpenGL outcome',
              result: _openGlResult,
            ),
          ],
        ),
      ),
    );
  }

  Widget _resultPanel(
    BuildContext context, {
    required String title,
    required WallpaperOperationResult? result,
    List<Widget> children = const <Widget>[],
  }) {
    return DecoratedBox(
      decoration: BoxDecoration(
        color: Theme.of(context).colorScheme.surfaceContainerHighest,
        borderRadius: BorderRadius.circular(12),
      ),
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text(title, style: Theme.of(context).textTheme.titleSmall),
            const SizedBox(height: 4),
            Text(
              'Status: ${result == null ? 'not run' : _resultSummary(result)}',
            ),
            ...children,
          ],
        ),
      ),
    );
  }

  static String _sourceKindLabel(_DemoSourceKind kind) {
    switch (kind) {
      case _DemoSourceKind.url:
        return 'URL';
      case _DemoSourceKind.filePath:
        return 'File path';
      case _DemoSourceKind.contentUri:
        return 'Content URI';
      case _DemoSourceKind.bytes:
        return 'Embedded bytes';
    }
  }

  static String _enumLabel(String value) {
    return value.replaceAllMapped(
      RegExp(r'([A-Z])'),
      (Match match) => ' ${match.group(1)!.toLowerCase()}',
    );
  }

  static String _supportLabel(bool supported) {
    return supported ? 'supported' : 'unavailable';
  }
}
90
likes
160
points
927
downloads

Documentation

API reference

Publisher

verified publisherhashstudios.dev

Weekly Downloads

Set wallpapers on Android asynchronously. Also supports video live wallpapers.

Homepage
Repository (GitHub)

License

MIT (license)

Dependencies

flutter

More

Packages that depend on async_wallpaper

Packages that implement async_wallpaper