refactor(device_management): migrate background_image to Riverpod

This commit is contained in:
2026-04-22 21:56:07 +02:00
parent a2ef28a1b5
commit fb281caf99
9 changed files with 377 additions and 534 deletions

View File

@@ -1,15 +1,16 @@
import 'package:design_system/design_system.dart';
import 'package:legacy_theme/legacy_theme.dart';
import 'package:device_management/src/features/background_image/presentation/providers/background_image_controller.dart';
import 'package:device_management/src/features/background_image/presentation/providers/background_image_photos_provider.dart';
import 'package:device_management/src/features/remote_connection/domain/entities/picture_entity.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:legacy_device_state/legacy_device_state.dart';
import 'package:legacy_theme/legacy_theme.dart';
import 'package:navigation/navigation.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:utils/utils.dart';
import 'state/background_image_view_model.dart';
import 'state/background_image_view_state.dart';
class BackgroundImageScreen extends ConsumerWidget {
final NavigationContract navigationContract;
@@ -18,45 +19,32 @@ class BackgroundImageScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final primaryColor = context.sfColors.legacyPrimary;
final device = ref.watch(selectedDeviceProvider).value;
final photosAsync = ref.watch(backgroundImagePhotosProvider);
final isSaving = ref
.watch(backgroundImageControllerProvider.select((s) => s.isLoading));
final state = ref.watch(backgroundImageViewModelProvider);
final vm = ref.read(backgroundImageViewModelProvider.notifier);
ref.listen(backgroundImageViewModelProvider.select((s) => s.errorEvent), (
previous,
next,
) {
if (next != null) {
final message = switch (next) {
BackgroundImageErrorEvent.load => context.translate(
I18n.errorBackgroundImageLoad,
),
BackgroundImageErrorEvent.upload => context.translate(
I18n.errorBackgroundImageUpload,
),
BackgroundImageErrorEvent.set => context.translate(
I18n.errorBackgroundImageSet,
),
ref.listen(backgroundImageControllerProvider, (prev, next) async {
if (prev == null || !prev.isLoading || next.isLoading) return;
if (next.hasError) {
final action =
ref.read(backgroundImageControllerProvider.notifier).lastAction;
final key = switch (action) {
BackgroundImageAction.uploaded => I18n.errorBackgroundImageUpload,
BackgroundImageAction.backgroundSet => I18n.errorBackgroundImageSet,
null => I18n.errorGeneric,
};
showTopSnackbar(context, message: message, type: MessageType.error);
}
});
ref.listen(backgroundImageViewModelProvider.select((s) => s.successEvent), (
previous,
next,
) {
if (next != null) {
final message = switch (next) {
BackgroundImageSuccessEvent.uploaded => context.translate(
I18n.backgroundImageUploaded,
),
BackgroundImageSuccessEvent.backgroundSet => context.translate(
I18n.backgroundImageSet,
),
};
showTopSnackbar(context, message: message, type: MessageType.success);
await showErrorDialog(context, key);
return;
}
final action =
ref.read(backgroundImageControllerProvider.notifier).lastAction;
final key = switch (action) {
BackgroundImageAction.uploaded => I18n.backgroundImageUploaded,
BackgroundImageAction.backgroundSet => I18n.backgroundImageSet,
null => I18n.deviceUpdatedSuccess,
};
await showSuccessDialog(context, key);
});
return Scaffold(
@@ -95,11 +83,14 @@ class BackgroundImageScreen extends ConsumerWidget {
shape: BoxShape.circle,
),
child: IconButton(
onPressed: state.isSaving
onPressed: isSaving || device == null
? null
: () async {
if (!await guardDeviceCommand(context, ref)) return;
vm.uploadPhoto();
if (!context.mounted) return;
ref
.read(backgroundImageControllerProvider.notifier)
.uploadPhoto(device: device);
},
icon: Icon(
Icons.add_photo_alternate_outlined,
@@ -113,26 +104,43 @@ class BackgroundImageScreen extends ConsumerWidget {
),
body: SafeArea(
top: false,
child: state.isLoading
? const Center(child: CircularProgressIndicator())
: state.isSaving
? const Center(child: CircularProgressIndicator())
: state.photos.isEmpty
? _EmptyState(
child: photosAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (_, __) => Center(
child: Text(context.translate(I18n.errorBackgroundImageLoad)),
),
data: (photos) {
if (isSaving) {
return const Center(child: CircularProgressIndicator());
}
if (photos.isEmpty) {
return _EmptyState(
onUpload: () async {
if (device == null) return;
if (!await guardDeviceCommand(context, ref)) return;
vm.uploadPhoto();
if (!context.mounted) return;
ref
.read(backgroundImageControllerProvider.notifier)
.uploadPhoto(device: device);
},
primaryColor: primaryColor,
)
: _PhotoGrid(
state: state,
onPhotoTap: (id) async {
if (!await guardDeviceCommand(context, ref)) return;
vm.setAsBackground(id);
},
primaryColor: primaryColor,
),
);
}
return _PhotoGrid(
photos: photos,
currentBackgroundId: device?.backgroundImageId,
primaryColor: primaryColor,
onPhotoTap: (id) async {
if (device == null) return;
if (!await guardDeviceCommand(context, ref)) return;
if (!context.mounted) return;
ref
.read(backgroundImageControllerProvider.notifier)
.setAsBackground(device: device, photoId: id);
},
);
},
),
),
);
}
@@ -176,14 +184,16 @@ class _EmptyState extends StatelessWidget {
}
class _PhotoGrid extends StatelessWidget {
final BackgroundImageViewState state;
final ValueChanged<String> onPhotoTap;
final List<PictureEntity> photos;
final String? currentBackgroundId;
final Color primaryColor;
final ValueChanged<String> onPhotoTap;
const _PhotoGrid({
required this.state,
required this.onPhotoTap,
required this.photos,
required this.currentBackgroundId,
required this.primaryColor,
required this.onPhotoTap,
});
@override
@@ -206,10 +216,10 @@ class _PhotoGrid extends StatelessWidget {
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
itemCount: state.photos.length,
itemCount: photos.length,
itemBuilder: (context, index) {
final photo = state.photos[index];
final isActive = state.currentBackgroundId == photo.id;
final photo = photos[index];
final isActive = currentBackgroundId == photo.id;
return GestureDetector(
onTap: () => onPhotoTap(photo.id),
child: Stack(
@@ -232,7 +242,8 @@ class _PhotoGrid extends StatelessWidget {
child: Icon(
Icons.image_outlined,
size: 48,
color: Theme.of(context).colorScheme.outline,
color:
Theme.of(context).colorScheme.outline,
),
),
),

View File

@@ -0,0 +1,79 @@
import 'dart:async';
import 'package:device_management/src/core/providers/background_image_repository_provider.dart';
import 'package:device_management/src/features/background_image/presentation/providers/background_image_photos_provider.dart';
import 'package:image_picker/image_picker.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:sf_tracking/sf_tracking.dart';
part 'background_image_controller.g.dart';
enum BackgroundImageAction { uploaded, backgroundSet }
class BackgroundImageNoSelectionException implements Exception {
const BackgroundImageNoSelectionException();
}
@riverpod
class BackgroundImageController extends _$BackgroundImageController {
BackgroundImageAction? _lastAction;
BackgroundImageAction? get lastAction => _lastAction;
@override
FutureOr<void> build() {}
Future<void> uploadPhoto({required DeviceEntity device}) async {
final picker = ImagePicker();
final image = await picker.pickImage(
source: ImageSource.gallery,
maxWidth: 800,
imageQuality: 80,
);
if (image == null) return;
_lastAction = BackgroundImageAction.uploaded;
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
final photoId = await ref
.read(backgroundImageRepositoryProvider)
.uploadImage(path: image.path);
await ref
.read(backgroundImageRepositoryProvider)
.setBackgroundImage(deviceId: device.id, photoId: photoId);
await _refreshDevice(device);
ref.invalidate(backgroundImagePhotosProvider);
unawaited(
ref.read(sfTrackingProvider).legacyDeviceBackgroundImageUploaded(),
);
});
}
Future<void> setAsBackground({
required DeviceEntity device,
required String photoId,
}) async {
_lastAction = BackgroundImageAction.backgroundSet;
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
await ref
.read(backgroundImageRepositoryProvider)
.setBackgroundImage(deviceId: device.id, photoId: photoId);
await _refreshDevice(device);
unawaited(
ref.read(sfTrackingProvider).legacyDeviceBackgroundImageChanged(),
);
});
}
Future<void> _refreshDevice(DeviceEntity current) async {
final devices =
await ref.read(sharedDevicesRepositoryProvider).getDevices();
final updated = devices.firstWhere(
(d) => d.identificator == current.identificator,
orElse: () => current,
);
ref.read(selectedDeviceProvider.notifier).setSelectedDevice(updated);
}
}

View File

@@ -0,0 +1,56 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'background_image_controller.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(BackgroundImageController)
const backgroundImageControllerProvider = BackgroundImageControllerProvider._();
final class BackgroundImageControllerProvider
extends $AsyncNotifierProvider<BackgroundImageController, void> {
const BackgroundImageControllerProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'backgroundImageControllerProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$backgroundImageControllerHash();
@$internal
@override
BackgroundImageController create() => BackgroundImageController();
}
String _$backgroundImageControllerHash() =>
r'60faa9520c5534e915321f65d92119220c1ca1e5';
abstract class _$BackgroundImageController extends $AsyncNotifier<void> {
FutureOr<void> build();
@$mustCallSuper
@override
void runBuild() {
build();
final ref = this.ref as $Ref<AsyncValue<void>, void>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<void>, void>,
AsyncValue<void>,
Object?,
Object?
>;
element.handleValue(ref, null);
}
}

View File

@@ -0,0 +1,10 @@
import 'package:device_management/src/core/providers/background_image_repository_provider.dart';
import 'package:device_management/src/features/remote_connection/domain/entities/picture_entity.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'background_image_photos_provider.g.dart';
@riverpod
Future<List<PictureEntity>> backgroundImagePhotos(Ref ref) async {
return ref.read(backgroundImageRepositoryProvider).getPhotos();
}

View File

@@ -0,0 +1,52 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'background_image_photos_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(backgroundImagePhotos)
const backgroundImagePhotosProvider = BackgroundImagePhotosProvider._();
final class BackgroundImagePhotosProvider
extends
$FunctionalProvider<
AsyncValue<List<PictureEntity>>,
List<PictureEntity>,
FutureOr<List<PictureEntity>>
>
with
$FutureModifier<List<PictureEntity>>,
$FutureProvider<List<PictureEntity>> {
const BackgroundImagePhotosProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'backgroundImagePhotosProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$backgroundImagePhotosHash();
@$internal
@override
$FutureProviderElement<List<PictureEntity>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<PictureEntity>> create(Ref ref) {
return backgroundImagePhotos(ref);
}
}
String _$backgroundImagePhotosHash() =>
r'25ebb7f6612d1b95435c4b59f66d035f5281ff8b';

View File

@@ -1,156 +0,0 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:sf_tracking/sf_tracking.dart';
import '../../../../core/domain/repositories/background_image_repository.dart';
import '../../../../core/providers/background_image_repository_provider.dart';
import 'background_image_view_state.dart';
final backgroundImageViewModelProvider =
NotifierProvider.autoDispose<
BackgroundImageViewModel,
BackgroundImageViewState
>(BackgroundImageViewModel.new);
class BackgroundImageViewModel extends Notifier<BackgroundImageViewState> {
late final BackgroundImageRepository _repository;
late final SharedDevicesRepository _devicesRepository;
late final SfTrackingRepository _tracking;
@override
BackgroundImageViewState build() {
_repository = ref.read(backgroundImageRepositoryProvider);
_devicesRepository = ref.read(sharedDevicesRepositoryProvider);
_tracking = ref.read(sfTrackingProvider);
Future.microtask(_load);
return const BackgroundImageViewState();
}
Future<void> _load() async {
try {
final device = ref.read(selectedDeviceProvider).value;
if (device == null) return;
final photos = await _repository.getPhotos();
if (!ref.mounted) return;
state = state.copyWith(
photos: photos,
currentBackgroundId: device.backgroundImageId,
isLoading: false,
);
} catch (e) {
if (!ref.mounted) return;
state = state.copyWith(
isLoading: false,
errorEvent: BackgroundImageErrorEvent.load,
);
}
}
Future<void> reload() async {
state = state.copyWith(isLoading: true, errorEvent: null);
await _load();
}
Future<void> uploadPhoto() async {
if (state.isSaving) return;
final picker = ImagePicker();
final image = await picker.pickImage(
source: ImageSource.gallery,
maxWidth: 800,
imageQuality: 80,
);
if (image == null) return;
final device = ref.read(selectedDeviceProvider).value;
if (device == null) return;
state = state.copyWith(
isSaving: true,
errorEvent: null,
successEvent: null,
);
try {
final photoId = await _repository.uploadImage(path: image.path);
if (!ref.mounted) return;
await _repository.setBackgroundImage(
deviceId: device.id,
photoId: photoId,
);
if (!ref.mounted) return;
await _refreshDevice(device);
if (!ref.mounted) return;
unawaited(_tracking.legacyDeviceBackgroundImageUploaded());
state = state.copyWith(
isSaving: false,
successEvent: BackgroundImageSuccessEvent.uploaded,
);
await reload();
} catch (e) {
if (!ref.mounted) return;
state = state.copyWith(
isSaving: false,
errorEvent: BackgroundImageErrorEvent.upload,
);
}
}
Future<void> setAsBackground(String photoId) async {
final device = ref.read(selectedDeviceProvider).value;
if (device == null) return;
state = state.copyWith(
isSaving: true,
errorEvent: null,
successEvent: null,
);
try {
await _repository.setBackgroundImage(
deviceId: device.id,
photoId: photoId,
);
if (!ref.mounted) return;
await _refreshDevice(device);
if (!ref.mounted) return;
unawaited(_tracking.legacyDeviceBackgroundImageChanged());
state = state.copyWith(
isSaving: false,
successEvent: BackgroundImageSuccessEvent.backgroundSet,
);
await reload();
} catch (e) {
if (!ref.mounted) return;
state = state.copyWith(
isSaving: false,
errorEvent: BackgroundImageErrorEvent.set,
);
}
}
Future<void> _refreshDevice(DeviceEntity currentDevice) async {
final devices = await _devicesRepository.getDevices();
if (!ref.mounted) return;
final updated = devices.firstWhere(
(d) => d.identificator == currentDevice.identificator,
orElse: () => currentDevice,
);
ref.read(selectedDeviceProvider.notifier).setSelectedDevice(updated);
}
}

View File

@@ -1,20 +0,0 @@
import 'package:device_management/src/features/remote_connection/domain/entities/picture_entity.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'background_image_view_state.freezed.dart';
enum BackgroundImageErrorEvent { load, upload, set }
enum BackgroundImageSuccessEvent { uploaded, backgroundSet }
@freezed
abstract class BackgroundImageViewState with _$BackgroundImageViewState {
const factory BackgroundImageViewState({
@Default([]) List<PictureEntity> photos,
@Default(true) bool isLoading,
@Default(false) bool isSaving,
String? currentBackgroundId,
BackgroundImageSuccessEvent? successEvent,
BackgroundImageErrorEvent? errorEvent,
}) = _BackgroundImageViewState;
}

View File

@@ -1,292 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'background_image_view_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$BackgroundImageViewState {
List<PictureEntity> get photos; bool get isLoading; bool get isSaving; String? get currentBackgroundId; BackgroundImageSuccessEvent? get successEvent; BackgroundImageErrorEvent? get errorEvent;
/// Create a copy of BackgroundImageViewState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$BackgroundImageViewStateCopyWith<BackgroundImageViewState> get copyWith => _$BackgroundImageViewStateCopyWithImpl<BackgroundImageViewState>(this as BackgroundImageViewState, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is BackgroundImageViewState&&const DeepCollectionEquality().equals(other.photos, photos)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isSaving, isSaving) || other.isSaving == isSaving)&&(identical(other.currentBackgroundId, currentBackgroundId) || other.currentBackgroundId == currentBackgroundId)&&(identical(other.successEvent, successEvent) || other.successEvent == successEvent)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent));
}
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(photos),isLoading,isSaving,currentBackgroundId,successEvent,errorEvent);
@override
String toString() {
return 'BackgroundImageViewState(photos: $photos, isLoading: $isLoading, isSaving: $isSaving, currentBackgroundId: $currentBackgroundId, successEvent: $successEvent, errorEvent: $errorEvent)';
}
}
/// @nodoc
abstract mixin class $BackgroundImageViewStateCopyWith<$Res> {
factory $BackgroundImageViewStateCopyWith(BackgroundImageViewState value, $Res Function(BackgroundImageViewState) _then) = _$BackgroundImageViewStateCopyWithImpl;
@useResult
$Res call({
List<PictureEntity> photos, bool isLoading, bool isSaving, String? currentBackgroundId, BackgroundImageSuccessEvent? successEvent, BackgroundImageErrorEvent? errorEvent
});
}
/// @nodoc
class _$BackgroundImageViewStateCopyWithImpl<$Res>
implements $BackgroundImageViewStateCopyWith<$Res> {
_$BackgroundImageViewStateCopyWithImpl(this._self, this._then);
final BackgroundImageViewState _self;
final $Res Function(BackgroundImageViewState) _then;
/// Create a copy of BackgroundImageViewState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? photos = null,Object? isLoading = null,Object? isSaving = null,Object? currentBackgroundId = freezed,Object? successEvent = freezed,Object? errorEvent = freezed,}) {
return _then(_self.copyWith(
photos: null == photos ? _self.photos : photos // ignore: cast_nullable_to_non_nullable
as List<PictureEntity>,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,isSaving: null == isSaving ? _self.isSaving : isSaving // ignore: cast_nullable_to_non_nullable
as bool,currentBackgroundId: freezed == currentBackgroundId ? _self.currentBackgroundId : currentBackgroundId // ignore: cast_nullable_to_non_nullable
as String?,successEvent: freezed == successEvent ? _self.successEvent : successEvent // ignore: cast_nullable_to_non_nullable
as BackgroundImageSuccessEvent?,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable
as BackgroundImageErrorEvent?,
));
}
}
/// Adds pattern-matching-related methods to [BackgroundImageViewState].
extension BackgroundImageViewStatePatterns on BackgroundImageViewState {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _BackgroundImageViewState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _BackgroundImageViewState() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _BackgroundImageViewState value) $default,){
final _that = this;
switch (_that) {
case _BackgroundImageViewState():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _BackgroundImageViewState value)? $default,){
final _that = this;
switch (_that) {
case _BackgroundImageViewState() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<PictureEntity> photos, bool isLoading, bool isSaving, String? currentBackgroundId, BackgroundImageSuccessEvent? successEvent, BackgroundImageErrorEvent? errorEvent)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _BackgroundImageViewState() when $default != null:
return $default(_that.photos,_that.isLoading,_that.isSaving,_that.currentBackgroundId,_that.successEvent,_that.errorEvent);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<PictureEntity> photos, bool isLoading, bool isSaving, String? currentBackgroundId, BackgroundImageSuccessEvent? successEvent, BackgroundImageErrorEvent? errorEvent) $default,) {final _that = this;
switch (_that) {
case _BackgroundImageViewState():
return $default(_that.photos,_that.isLoading,_that.isSaving,_that.currentBackgroundId,_that.successEvent,_that.errorEvent);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<PictureEntity> photos, bool isLoading, bool isSaving, String? currentBackgroundId, BackgroundImageSuccessEvent? successEvent, BackgroundImageErrorEvent? errorEvent)? $default,) {final _that = this;
switch (_that) {
case _BackgroundImageViewState() when $default != null:
return $default(_that.photos,_that.isLoading,_that.isSaving,_that.currentBackgroundId,_that.successEvent,_that.errorEvent);case _:
return null;
}
}
}
/// @nodoc
class _BackgroundImageViewState implements BackgroundImageViewState {
const _BackgroundImageViewState({final List<PictureEntity> photos = const [], this.isLoading = true, this.isSaving = false, this.currentBackgroundId, this.successEvent, this.errorEvent}): _photos = photos;
final List<PictureEntity> _photos;
@override@JsonKey() List<PictureEntity> get photos {
if (_photos is EqualUnmodifiableListView) return _photos;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_photos);
}
@override@JsonKey() final bool isLoading;
@override@JsonKey() final bool isSaving;
@override final String? currentBackgroundId;
@override final BackgroundImageSuccessEvent? successEvent;
@override final BackgroundImageErrorEvent? errorEvent;
/// Create a copy of BackgroundImageViewState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$BackgroundImageViewStateCopyWith<_BackgroundImageViewState> get copyWith => __$BackgroundImageViewStateCopyWithImpl<_BackgroundImageViewState>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _BackgroundImageViewState&&const DeepCollectionEquality().equals(other._photos, _photos)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isSaving, isSaving) || other.isSaving == isSaving)&&(identical(other.currentBackgroundId, currentBackgroundId) || other.currentBackgroundId == currentBackgroundId)&&(identical(other.successEvent, successEvent) || other.successEvent == successEvent)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent));
}
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_photos),isLoading,isSaving,currentBackgroundId,successEvent,errorEvent);
@override
String toString() {
return 'BackgroundImageViewState(photos: $photos, isLoading: $isLoading, isSaving: $isSaving, currentBackgroundId: $currentBackgroundId, successEvent: $successEvent, errorEvent: $errorEvent)';
}
}
/// @nodoc
abstract mixin class _$BackgroundImageViewStateCopyWith<$Res> implements $BackgroundImageViewStateCopyWith<$Res> {
factory _$BackgroundImageViewStateCopyWith(_BackgroundImageViewState value, $Res Function(_BackgroundImageViewState) _then) = __$BackgroundImageViewStateCopyWithImpl;
@override @useResult
$Res call({
List<PictureEntity> photos, bool isLoading, bool isSaving, String? currentBackgroundId, BackgroundImageSuccessEvent? successEvent, BackgroundImageErrorEvent? errorEvent
});
}
/// @nodoc
class __$BackgroundImageViewStateCopyWithImpl<$Res>
implements _$BackgroundImageViewStateCopyWith<$Res> {
__$BackgroundImageViewStateCopyWithImpl(this._self, this._then);
final _BackgroundImageViewState _self;
final $Res Function(_BackgroundImageViewState) _then;
/// Create a copy of BackgroundImageViewState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? photos = null,Object? isLoading = null,Object? isSaving = null,Object? currentBackgroundId = freezed,Object? successEvent = freezed,Object? errorEvent = freezed,}) {
return _then(_BackgroundImageViewState(
photos: null == photos ? _self._photos : photos // ignore: cast_nullable_to_non_nullable
as List<PictureEntity>,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,isSaving: null == isSaving ? _self.isSaving : isSaving // ignore: cast_nullable_to_non_nullable
as bool,currentBackgroundId: freezed == currentBackgroundId ? _self.currentBackgroundId : currentBackgroundId // ignore: cast_nullable_to_non_nullable
as String?,successEvent: freezed == successEvent ? _self.successEvent : successEvent // ignore: cast_nullable_to_non_nullable
as BackgroundImageSuccessEvent?,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable
as BackgroundImageErrorEvent?,
));
}
}
// dart format on

View File

@@ -0,0 +1,103 @@
import 'package:device_management/src/core/domain/repositories/background_image_repository.dart';
import 'package:device_management/src/core/providers/background_image_repository_provider.dart';
import 'package:device_management/src/features/background_image/presentation/providers/background_image_controller.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:sf_infrastructure/sf_infrastructure.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:sf_shared/testing.dart';
import 'package:sf_tracking/sf_tracking.dart';
class MockBackgroundImageRepository extends Mock
implements BackgroundImageRepository {}
class MockSharedDevicesRepository extends Mock
implements SharedDevicesRepository {}
const _device = DeviceEntity(
id: 'device-1',
identificator: 'imei-1',
carrierName: 'Watch',
);
void main() {
setUpAll(() {
registerFallbackValue(_device);
});
ProviderContainer buildContainer({
required BackgroundImageRepository repo,
required SharedDevicesRepository devicesRepo,
}) {
return makeContainer(
overrides: [
backgroundImageRepositoryProvider.overrideWithValue(repo),
sharedDevicesRepositoryProvider.overrideWithValue(devicesRepo),
sfTrackingProvider.overrideWithValue(
SfTrackingRepository(clients: const []),
),
],
);
}
group('BackgroundImageController.setAsBackground', () {
test('updates backend and refreshes device on success', () async {
final repo = MockBackgroundImageRepository();
final devicesRepo = MockSharedDevicesRepository();
when(
() => repo.setBackgroundImage(
deviceId: any(named: 'deviceId'),
photoId: any(named: 'photoId'),
),
).thenAnswer((_) async {});
when(() => devicesRepo.getDevices()).thenAnswer((_) async => const []);
final container =
buildContainer(repo: repo, devicesRepo: devicesRepo);
addTearDown(container.dispose);
await container
.read(backgroundImageControllerProvider.notifier)
.setAsBackground(device: _device, photoId: 'photo-1');
expect(
container.read(backgroundImageControllerProvider),
isA<AsyncData<void>>(),
);
expect(
container
.read(backgroundImageControllerProvider.notifier)
.lastAction,
BackgroundImageAction.backgroundSet,
);
verify(
() => repo.setBackgroundImage(deviceId: 'device-1', photoId: 'photo-1'),
).called(1);
});
test('exposes AsyncError when repository fails', () async {
final repo = MockBackgroundImageRepository();
final devicesRepo = MockSharedDevicesRepository();
when(
() => repo.setBackgroundImage(
deviceId: any(named: 'deviceId'),
photoId: any(named: 'photoId'),
),
).thenThrow(const ApiException(message: 'boom', isNetworkError: true));
final container =
buildContainer(repo: repo, devicesRepo: devicesRepo);
addTearDown(container.dispose);
await container
.read(backgroundImageControllerProvider.notifier)
.setAsBackground(device: _device, photoId: 'photo-1');
expect(
container.read(backgroundImageControllerProvider),
isA<AsyncError<void>>(),
);
});
});
}