refactor: logger service and remove dynamic

This commit is contained in:
shenlong-tanwen 2025-04-20 00:17:43 +05:30
parent 854ea13d6a
commit 44151c9a0c
14 changed files with 121 additions and 145 deletions

View File

@ -6,9 +6,11 @@ abstract interface class IStoreRepository implements IDatabaseRepository {
Future<T?> tryGet<T>(StoreKey<T> key); Future<T?> tryGet<T>(StoreKey<T> key);
Future<List<StoreDto<Object>>> getAll();
Stream<T?> watch<T>(StoreKey<T> key); Stream<T?> watch<T>(StoreKey<T> key);
Stream<StoreUpdateEvent> watchAll(); Stream<StoreDto<Object>> watchAll();
Future<bool> update<T>(StoreKey<T> key, T value); Future<bool> update<T>(StoreKey<T> key, T value);

View File

@ -75,23 +75,23 @@ enum StoreKey<T> {
Type get type => T; Type get type => T;
} }
class StoreUpdateEvent<T> { class StoreDto<T> {
final StoreKey<T> key; final StoreKey<T> key;
final T? value; final T? value;
const StoreUpdateEvent(this.key, this.value); const StoreDto(this.key, this.value);
@override @override
String toString() { String toString() {
return ''' return '''
StoreUpdateEvent: { StoreDto: {
key: $key, key: $key,
value: ${value ?? '<NA>'}, value: ${value ?? '<NA>'},
}'''; }''';
} }
@override @override
bool operator ==(covariant StoreUpdateEvent<T> other) { bool operator ==(covariant StoreDto<T> other) {
if (identical(this, other)) return true; if (identical(this, other)) return true;
return other.key == key && other.value == value; return other.key == key && other.value == value;

View File

@ -2,8 +2,7 @@ import 'package:openapi/api.dart';
class SyncEvent { class SyncEvent {
final SyncEntityType type; final SyncEntityType type;
// ignore: avoid-dynamic final Object data;
final dynamic data;
final String ack; final String ack;
const SyncEvent({required this.type, required this.data, required this.ack}); const SyncEvent({required this.type, required this.data, required this.ack});

View File

@ -8,6 +8,11 @@ import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
/// Service responsible for handling application logging.
///
/// It listens to Dart's [Logger.root], buffers logs in memory (optionally),
/// writes them to a persistent [ILogRepository], and manages log levels
/// via [IStoreRepository]
class LogService { class LogService {
final ILogRepository _logRepository; final ILogRepository _logRepository;
final IStoreRepository _storeRepository; final IStoreRepository _storeRepository;
@ -18,19 +23,11 @@ class LogService {
/// This is useful when logging in quick succession, as it increases performance /// This is useful when logging in quick succession, as it increases performance
/// and reduces NAND wear. However, it may cause the logs to be lost in case of a crash / in isolates. /// and reduces NAND wear. However, it may cause the logs to be lost in case of a crash / in isolates.
final bool _shouldBuffer; final bool _shouldBuffer;
Timer? _flushTimer; Timer? _flushTimer;
late final StreamSubscription<LogRecord> _logSubscription; late final StreamSubscription<LogRecord> _logSubscription;
LogService._(
this._logRepository,
this._storeRepository,
this._shouldBuffer,
) {
// Listen to log messages and write them to the database
_logSubscription = Logger.root.onRecord.listen(_writeLogToDatabase);
}
static LogService? _instance; static LogService? _instance;
static LogService get I { static LogService get I {
if (_instance == null) { if (_instance == null) {
@ -44,10 +41,7 @@ class LogService {
required IStoreRepository storeRepository, required IStoreRepository storeRepository,
bool shouldBuffer = true, bool shouldBuffer = true,
}) async { }) async {
if (_instance != null) { _instance ??= await create(
return _instance!;
}
_instance = await create(
logRepository: logRepository, logRepository: logRepository,
storeRepository: storeRepository, storeRepository: storeRepository,
shouldBuffer: shouldBuffer, shouldBuffer: shouldBuffer,
@ -61,55 +55,28 @@ class LogService {
bool shouldBuffer = true, bool shouldBuffer = true,
}) async { }) async {
final instance = LogService._(logRepository, storeRepository, shouldBuffer); final instance = LogService._(logRepository, storeRepository, shouldBuffer);
// Truncate logs to 250
await logRepository.truncate(limit: kLogTruncateLimit); await logRepository.truncate(limit: kLogTruncateLimit);
// Get log level from store final level = await instance._storeRepository.tryGet(StoreKey.logLevel) ??
final level = await instance._storeRepository.tryGet(StoreKey.logLevel); LogLevel.info.index;
if (level != null) { Logger.root.level = Level.LEVELS.elementAtOrNull(level) ?? Level.INFO;
Logger.root.level = Level.LEVELS.elementAtOrNull(level) ?? Level.INFO;
}
return instance; return instance;
} }
Future<void> setlogLevel(LogLevel level) async { LogService._(
await _storeRepository.insert(StoreKey.logLevel, level.index); this._logRepository,
Logger.root.level = level.toLevel(); this._storeRepository,
this._shouldBuffer,
) {
_logSubscription = Logger.root.onRecord.listen(_handleLogRecord);
} }
Future<List<LogMessage>> getMessages() async { void _handleLogRecord(LogRecord r) {
final logsFromDb = await _logRepository.getAll();
if (_msgBuffer.isNotEmpty) {
return [..._msgBuffer.reversed, ...logsFromDb];
}
return logsFromDb;
}
Future<void> clearLogs() async {
_flushTimer?.cancel();
_flushTimer = null;
_msgBuffer.clear();
await _logRepository.deleteAll();
}
/// Flush pending log messages to persistent storage
void flush() {
if (_flushTimer == null) {
return;
}
_flushTimer!.cancel();
// TODO: Rename enable this after moving to sqlite - #16504
// await _flushBufferToDatabase();
}
Future<void> dispose() {
_flushTimer?.cancel();
_logSubscription.cancel();
return _flushBufferToDatabase();
}
void _writeLogToDatabase(LogRecord r) {
if (kDebugMode) { if (kDebugMode) {
debugPrint('[${r.level.name}] [${r.time}] ${r.message}'); debugPrint(
'[${r.level.name}] [${r.time}] [${r.loggerName}] ${r.message}'
'${r.error == null ? '' : '\nError: ${r.error}'}'
'${r.stackTrace == null ? '' : '\nStack: ${r.stackTrace}'}',
);
} }
final record = LogMessage( final record = LogMessage(
@ -125,14 +92,44 @@ class LogService {
_msgBuffer.add(record); _msgBuffer.add(record);
_flushTimer ??= Timer( _flushTimer ??= Timer(
const Duration(seconds: 5), const Duration(seconds: 5),
() => unawaited(_flushBufferToDatabase()), () => unawaited(flushBuffer()),
); );
} else { } else {
unawaited(_logRepository.insert(record)); unawaited(_logRepository.insert(record));
} }
} }
Future<void> _flushBufferToDatabase() async { Future<void> setLogLevel(LogLevel level) async {
await _storeRepository.insert(StoreKey.logLevel, level.index);
Logger.root.level = level.toLevel();
}
Future<List<LogMessage>> getMessages() async {
final logsFromDb = await _logRepository.getAll();
return [..._msgBuffer.reversed, ...logsFromDb];
}
Future<void> clearLogs() async {
_flushTimer?.cancel();
_flushTimer = null;
_msgBuffer.clear();
await _logRepository.deleteAll();
}
void flush() {
_flushTimer?.cancel();
// TODO: Rename enable this after moving to sqlite - #16504
// await _flushBufferToDatabase();
}
Future<void> dispose() {
_flushTimer?.cancel();
_logSubscription.cancel();
return flushBuffer();
}
// TOOD: Move this to private once Isar is removed
Future<void> flushBuffer() async {
_flushTimer = null; _flushTimer = null;
final buffer = [..._msgBuffer]; final buffer = [..._msgBuffer];
_msgBuffer.clear(); _msgBuffer.clear();

View File

@ -3,15 +3,17 @@ import 'dart:async';
import 'package:immich_mobile/domain/interfaces/store.interface.dart'; import 'package:immich_mobile/domain/interfaces/store.interface.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
/// Provides access to a persistent key-value store with an in-memory cache.
/// Listens for repository changes to keep the cache updated.
class StoreService { class StoreService {
final IStoreRepository _storeRepository; final IStoreRepository _storeRepository;
final Map<int, dynamic> _cache = {}; /// In-memory cache. Keys are [StoreKey.id]
late final StreamSubscription<StoreUpdateEvent> _storeUpdateSubscription; final Map<int, Object?> _cache = {};
late final StreamSubscription<StoreDto> _storeUpdateSubscription;
StoreService._({ StoreService._({required IStoreRepository storeRepository})
required IStoreRepository storeRepository, : _storeRepository = storeRepository;
}) : _storeRepository = storeRepository;
// TODO: Temporary typedef to make minimal changes. Remove this and make the presentation layer access store through a provider // TODO: Temporary typedef to make minimal changes. Remove this and make the presentation layer access store through a provider
static StoreService? _instance; static StoreService? _instance;
@ -23,7 +25,6 @@ class StoreService {
} }
// TODO: Replace the implementation with the one from create after removing the typedef // TODO: Replace the implementation with the one from create after removing the typedef
/// Initializes the store with the given [storeRepository]
static Future<StoreService> init({ static Future<StoreService> init({
required IStoreRepository storeRepository, required IStoreRepository storeRepository,
}) async { }) async {
@ -31,7 +32,6 @@ class StoreService {
return _instance!; return _instance!;
} }
/// Initializes the store with the given [storeRepository]
static Future<StoreService> create({ static Future<StoreService> create({
required IStoreRepository storeRepository, required IStoreRepository storeRepository,
}) async { }) async {
@ -41,16 +41,14 @@ class StoreService {
return instance; return instance;
} }
/// Fills the cache with the values from the DB
Future<void> _populateCache() async { Future<void> _populateCache() async {
for (StoreKey key in StoreKey.values) { final storeValues = await _storeRepository.getAll();
final storeValue = await _storeRepository.tryGet(key); for (StoreDto storeValue in storeValues) {
_cache[key.id] = storeValue; _cache[storeValue.key.id] = storeValue.value;
} }
} }
/// Listens for changes in the DB and updates the cache StreamSubscription<StoreDto> _listenForChange() =>
StreamSubscription<StoreUpdateEvent> _listenForChange() =>
_storeRepository.watchAll().listen((event) { _storeRepository.watchAll().listen((event) {
_cache[event.key.id] = event.value; _cache[event.key.id] = event.value;
}); });
@ -61,11 +59,11 @@ class StoreService {
_cache.clear(); _cache.clear();
} }
/// Returns the stored value for the given key (possibly null) /// Returns the cached value for [key], or `null`
T? tryGet<T>(StoreKey<T> key) => _cache[key.id]; T? tryGet<T>(StoreKey<T> key) => _cache[key.id] as T?;
/// Returns the stored value for the given key or if null the [defaultValue] /// Returns the stored value for [key] or [defaultValue].
/// Throws a [StoreKeyNotFoundException] if both are null /// Throws [StoreKeyNotFoundException] if value and [defaultValue] are null.
T get<T>(StoreKey<T> key, [T? defaultValue]) { T get<T>(StoreKey<T> key, [T? defaultValue]) {
final value = tryGet(key) ?? defaultValue; final value = tryGet(key) ?? defaultValue;
if (value == null) { if (value == null) {
@ -74,23 +72,23 @@ class StoreService {
return value; return value;
} }
/// Asynchronously stores the value in the Store /// Stores the [value] for the [key]. Skips write if value hasn't changed.
Future<void> put<U extends StoreKey<T>, T>(U key, T value) async { Future<void> put<U extends StoreKey<T>, T>(U key, T value) async {
if (_cache[key.id] == value) return; if (_cache[key.id] == value) return;
await _storeRepository.insert(key, value); await _storeRepository.insert(key, value);
_cache[key.id] = value; _cache[key.id] = value;
} }
/// Watches a specific key for changes /// Returns a stream that emits the value for [key] on change.
Stream<T?> watch<T>(StoreKey<T> key) => _storeRepository.watch(key); Stream<T?> watch<T>(StoreKey<T> key) => _storeRepository.watch(key);
/// Removes the value asynchronously from the Store /// Removes the value for [key]
Future<void> delete<T>(StoreKey<T> key) async { Future<void> delete<T>(StoreKey<T> key) async {
await _storeRepository.delete(key); await _storeRepository.delete(key);
_cache.remove(key.id); _cache.remove(key.id);
} }
/// Clears all values from this store (cache and DB) /// Clears all values from thw store (cache and DB)
Future<void> clear() async { Future<void> clear() async {
await _storeRepository.deleteAll(); await _storeRepository.deleteAll();
_cache.clear(); _cache.clear();

View File

@ -1,5 +1,3 @@
// ignore_for_file: avoid-passing-async-when-sync-expected
import 'dart:async'; import 'dart:async';
import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart'; import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart';
@ -59,8 +57,7 @@ class SyncStreamService {
Future<void> _handleSyncData( Future<void> _handleSyncData(
SyncEntityType type, SyncEntityType type,
// ignore: avoid-dynamic Iterable<Object> data,
Iterable<dynamic> data,
) async { ) async {
_logger.fine("Processing sync data for $type of length ${data.length}"); _logger.fine("Processing sync data for $type of length ${data.length}");
// ignore: prefer-switch-expression // ignore: prefer-switch-expression

View File

@ -1,5 +1,3 @@
// ignore_for_file: avoid-passing-async-when-sync-expected
import 'dart:async'; import 'dart:async';
import 'package:immich_mobile/providers/infrastructure/sync_stream.provider.dart'; import 'package:immich_mobile/providers/infrastructure/sync_stream.provider.dart';
@ -31,9 +29,9 @@ class BackgroundSyncManager {
_syncTask = runInIsolateGentle( _syncTask = runInIsolateGentle(
computation: (ref) => ref.read(syncStreamServiceProvider).sync(), computation: (ref) => ref.read(syncStreamServiceProvider).sync(),
); );
_syncTask!.whenComplete(() {
return _syncTask!.whenComplete(() {
_syncTask = null; _syncTask = null;
}); });
return _syncTask!.future;
} }
} }

View File

@ -22,7 +22,7 @@ class IsarStoreRepository extends IsarDatabaseRepository
} }
@override @override
Stream<StoreUpdateEvent> watchAll() { Stream<StoreDto<Object>> watchAll() {
return _db.storeValues return _db.storeValues
.filter() .filter()
.anyOf(validStoreKeys, (query, id) => query.idEqualTo(id)) .anyOf(validStoreKeys, (query, id) => query.idEqualTo(id))
@ -71,10 +71,11 @@ class IsarStoreRepository extends IsarDatabaseRepository
.asyncMap((e) async => e == null ? null : await _toValue(key, e)); .asyncMap((e) async => e == null ? null : await _toValue(key, e));
} }
Future<StoreUpdateEvent> _toUpdateEvent(StoreValue entity) async { Future<StoreDto<Object>> _toUpdateEvent(StoreValue entity) async {
final key = StoreKey.values.firstWhere((e) => e.id == entity.id); final key = StoreKey.values.firstWhere((e) => e.id == entity.id)
as StoreKey<Object>;
final value = await _toValue(key, entity); final value = await _toValue(key, entity);
return StoreUpdateEvent(key, value); return StoreDto(key, value);
} }
Future<T?> _toValue<T>(StoreKey<T> key, StoreValue entity) async => Future<T?> _toValue<T>(StoreKey<T> key, StoreValue entity) async =>
@ -107,4 +108,13 @@ class IsarStoreRepository extends IsarDatabaseRepository
}; };
return StoreValue(key.id, intValue: intValue, strValue: strValue); return StoreValue(key.id, intValue: intValue, strValue: strValue);
} }
@override
Future<List<StoreDto<Object>>> getAll() async {
final entities = await _db.storeValues
.filter()
.anyOf(validStoreKeys, (query, id) => query.idEqualTo(id))
.findAll();
return Future.wait(entities.map((e) => _toUpdateEvent(e)).toList());
}
} }

View File

@ -25,7 +25,6 @@ class SyncApiRepository implements ISyncApiRepository {
int batchSize = kSyncEventBatchSize, int batchSize = kSyncEventBatchSize,
http.Client? httpClient, http.Client? httpClient,
}) async { }) async {
// ignore: avoid-unused-assignment
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
final client = httpClient ?? http.Client(); final client = httpClient ?? http.Client();
final endpoint = "${_api.apiClient.basePath}/sync/stream"; final endpoint = "${_api.apiClient.basePath}/sync/stream";
@ -65,8 +64,7 @@ class SyncApiRepository implements ISyncApiRepository {
} }
try { try {
final response = final response = await client.send(request);
await client.send(request).timeout(const Duration(seconds: 20));
if (response.statusCode != 200) { if (response.statusCode != 200) {
final errorBody = await response.stream.bytesToString(); final errorBody = await response.stream.bytesToString();
@ -133,8 +131,7 @@ class SyncApiRepository implements ISyncApiRepository {
} }
} }
// ignore: avoid-dynamic const _kResponseMap = <SyncEntityType, Function(Object)>{
const _kResponseMap = <SyncEntityType, Function(dynamic)>{
SyncEntityType.userV1: SyncUserV1.fromJson, SyncEntityType.userV1: SyncUserV1.fromJson,
SyncEntityType.userDeleteV1: SyncUserDeleteV1.fromJson, SyncEntityType.userDeleteV1: SyncUserDeleteV1.fromJson,
SyncEntityType.partnerV1: SyncPartnerV1.fromJson, SyncEntityType.partnerV1: SyncPartnerV1.fromJson,

View File

@ -3,6 +3,7 @@ import 'dart:ui';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/services/log.service.dart';
import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart'; import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
@ -58,9 +59,7 @@ Cancelable<T?> runInIsolateGentle<T>({
stack, stack,
); );
} finally { } finally {
// Wait for the logs to flush await LogService.I.flushBuffer();
await Future.delayed(const Duration(seconds: 2));
// Always close the new db connection on Isolate end
ref.read(driftProvider).close(); ref.read(driftProvider).close();
ref.read(isarProvider).close(); ref.read(isarProvider).close();
} }

View File

@ -41,7 +41,7 @@ class AdvancedSettings extends HookConsumerWidget {
useValueChanged( useValueChanged(
levelId.value, levelId.value,
(_, __) => (_, __) =>
LogService.I.setlogLevel(Level.LEVELS[levelId.value].toLogLevel()), LogService.I.setLogLevel(Level.LEVELS[levelId.value].toLogLevel()),
); );
Future<bool> checkAndroidVersion() async { Future<bool> checkAndroidVersion() async {

View File

@ -74,7 +74,7 @@ void main() {
setUp(() async { setUp(() async {
when(() => mockStoreRepo.insert<int>(StoreKey.logLevel, any())) when(() => mockStoreRepo.insert<int>(StoreKey.logLevel, any()))
.thenAnswer((_) async => true); .thenAnswer((_) async => true);
await sut.setlogLevel(LogLevel.shout); await sut.setLogLevel(LogLevel.shout);
}); });
test('Updates the log level in store', () { test('Updates the log level in store', () {

View File

@ -1,5 +1,3 @@
// ignore_for_file: avoid-dynamic
import 'dart:async'; import 'dart:async';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@ -18,10 +16,10 @@ final _kBackupFailedSince = DateTime.utc(2023);
void main() { void main() {
late StoreService sut; late StoreService sut;
late IStoreRepository mockStoreRepo; late IStoreRepository mockStoreRepo;
late StreamController<StoreUpdateEvent> controller; late StreamController<StoreDto<Object>> controller;
setUp(() async { setUp(() async {
controller = StreamController<StoreUpdateEvent>.broadcast(); controller = StreamController<StoreDto<Object>>.broadcast();
mockStoreRepo = MockStoreRepository(); mockStoreRepo = MockStoreRepository();
// For generics, we need to provide fallback to each concrete type to avoid runtime errors // For generics, we need to provide fallback to each concrete type to avoid runtime errors
registerFallbackValue(StoreKey.accessToken); registerFallbackValue(StoreKey.accessToken);
@ -29,18 +27,14 @@ void main() {
registerFallbackValue(StoreKey.backgroundBackup); registerFallbackValue(StoreKey.backgroundBackup);
registerFallbackValue(StoreKey.backupFailedSince); registerFallbackValue(StoreKey.backupFailedSince);
when(() => mockStoreRepo.tryGet(any<StoreKey<dynamic>>())) when(() => mockStoreRepo.getAll()).thenAnswer(
.thenAnswer((invocation) async { (_) async => [
final key = invocation.positionalArguments.firstOrNull as StoreKey; const StoreDto(StoreKey.accessToken, _kAccessToken),
return switch (key) { const StoreDto(StoreKey.backgroundBackup, _kBackgroundBackup),
StoreKey.accessToken => _kAccessToken, const StoreDto(StoreKey.groupAssetsBy, _kGroupAssetsBy),
StoreKey.backgroundBackup => _kBackgroundBackup, StoreDto(StoreKey.backupFailedSince, _kBackupFailedSince),
StoreKey.groupAssetsBy => _kGroupAssetsBy, ],
StoreKey.backupFailedSince => _kBackupFailedSince, );
// ignore: avoid-wildcard-cases-with-enums
_ => null,
};
});
when(() => mockStoreRepo.watchAll()).thenAnswer((_) => controller.stream); when(() => mockStoreRepo.watchAll()).thenAnswer((_) => controller.stream);
sut = await StoreService.create(storeRepository: mockStoreRepo); sut = await StoreService.create(storeRepository: mockStoreRepo);
@ -53,8 +47,7 @@ void main() {
group("Store Service Init:", () { group("Store Service Init:", () {
test('Populates the internal cache on init', () { test('Populates the internal cache on init', () {
verify(() => mockStoreRepo.tryGet(any<StoreKey<dynamic>>())) verify(() => mockStoreRepo.getAll()).called(1);
.called(equals(StoreKey.values.length));
expect(sut.tryGet(StoreKey.accessToken), _kAccessToken); expect(sut.tryGet(StoreKey.accessToken), _kAccessToken);
expect(sut.tryGet(StoreKey.backgroundBackup), _kBackgroundBackup); expect(sut.tryGet(StoreKey.backgroundBackup), _kBackgroundBackup);
expect(sut.tryGet(StoreKey.groupAssetsBy), _kGroupAssetsBy); expect(sut.tryGet(StoreKey.groupAssetsBy), _kGroupAssetsBy);
@ -64,8 +57,7 @@ void main() {
}); });
test('Listens to stream of store updates', () async { test('Listens to stream of store updates', () async {
final event = final event = StoreDto(StoreKey.accessToken, _kAccessToken.toUpperCase());
StoreUpdateEvent(StoreKey.accessToken, _kAccessToken.toUpperCase());
controller.add(event); controller.add(event);
await pumpEventQueue(); await pumpEventQueue();

View File

@ -1,5 +1,3 @@
// ignore_for_file: avoid-dynamic
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/interfaces/store.interface.dart'; import 'package:immich_mobile/domain/interfaces/store.interface.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
@ -146,32 +144,21 @@ void main() {
expectLater( expectLater(
stream, stream,
emitsInAnyOrder([ emitsInAnyOrder([
emits(const StoreDto<Object>(StoreKey.version, _kTestVersion)),
emits( emits(
const StoreUpdateEvent<dynamic>(StoreKey.version, _kTestVersion), StoreDto<Object>(StoreKey.backupFailedSince, _kTestBackupFailed),
), ),
emits( emits(
StoreUpdateEvent<dynamic>( const StoreDto<Object>(StoreKey.accessToken, _kTestAccessToken),
StoreKey.backupFailedSince,
_kTestBackupFailed,
),
), ),
emits( emits(
const StoreUpdateEvent<dynamic>( const StoreDto<Object>(
StoreKey.accessToken,
_kTestAccessToken,
),
),
emits(
const StoreUpdateEvent<dynamic>(
StoreKey.colorfulInterface, StoreKey.colorfulInterface,
_kTestColorfulInterface, _kTestColorfulInterface,
), ),
), ),
emits( emits(
const StoreUpdateEvent<dynamic>( const StoreDto<Object>(StoreKey.version, _kTestVersion + 10),
StoreKey.version,
_kTestVersion + 10,
),
), ),
]), ]),
); );