Feat: Stellt den BackgroundManager auf IsolateManager um

This commit is contained in:
2026-01-03 21:53:37 +01:00
parent baef163b68
commit 7b3a1cfac6
11 changed files with 15625 additions and 107 deletions

View File

@@ -1,19 +1,18 @@
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart';
import 'package:isolate_manager/isolate_manager.dart';
import 'package:workmanager/workmanager.dart';
import '../Tasks/generate_transactions_task.dart';
import '../Tasks/task.dart';
import 'port_controller.dart';
import '../Tasks/workers.dart';
import '../Tasks/workmanager_workers.dart';
/// Erstellt Hintergrundtasks und führt diese aus
class BackgroundTaskController {
/// Erstellt eine neue Instanz dieser Klasse
BackgroundTaskController() {
if (Platform.isAndroid) {
if (!kIsWeb && Platform.isAndroid) {
unawaited(Workmanager().initialize(callbackDispatcher));
unawaited(
Workmanager().registerPeriodicTask(
@@ -25,47 +24,12 @@ class BackgroundTaskController {
);
} else {
unawaited(
Isolate.run(() {
unawaited(_runTask(
GenerateTransactionsTask(),
const Duration(minutes: 1),
const Duration(minutes: 30),
));
IsolateManager.runFunction(runTask, {
'taskName': 'generate_transactions',
'initialDelayMinutes': 1,
'frequencyMinutes': 30,
}),
);
}
}
Future<void> _runTask(
final Task task,
final Duration initialDelay,
final Duration frequency,
) async {
await Future.delayed(initialDelay);
final RootIsolateToken? rootIsolateToken = await PortController()
.getRootIsolateToken();
if (rootIsolateToken != null) {
BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);
await GenerateTransactionsTask().execute();
}
await Future.delayed(frequency);
unawaited(_runTask(task, initialDelay, frequency));
}
}
/// Die Funktion wird von Hintergrundtasks ausgerufen, um diese auszuführen
@pragma('vm:entry-point')
void callbackDispatcher() {
Workmanager().executeTask((final task, final inputData) {
switch (task) {
case 'generate_transactions':
return GenerateTransactionsTask().execute();
}
return Future.value(true);
});
}

View File

@@ -2,24 +2,28 @@ import 'dart:async';
import 'dart:isolate';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:logger/logger.dart';
/// Ein PortController mit verschiedenen FUnktionen zur KOmmunikations zwischen
/// Ein PortController mit verschiedenen Funktionen zur Kommunikation zwischen
/// main und anderen Isolates
class PortController {
/// Gibt eine Instanz dieser Klasse zurück
factory PortController() => _instance;
PortController._internal() {
if (ServicesBinding.rootIsolateToken != null) {
if (!kIsWeb && ServicesBinding.rootIsolateToken != null) {
_registerRootIsolateTokenSender();
}
}
static final PortController _instance = PortController._internal();
final Logger _logger = Logger();
/// Fügt einen Port mit [name] zum NameServer hinzu
void addPort(final SendPort sendPort, final String name) {
IsolateNameServer.removePortNameMapping(name);
IsolateNameServer.registerPortWithName(sendPort, name);
}
@@ -29,6 +33,8 @@ class PortController {
/// Gibt das [RootIsolateToken] der main-Isolate zurück
Future<RootIsolateToken?> getRootIsolateToken() async {
_logger.d('Trying to retrieve RootIsolateToken...');
final ReceivePort receivePort = ReceivePort();
final SendPort? rootPort = IsolateNameServer.lookupPortByName(
@@ -36,21 +42,26 @@ class PortController {
);
if (rootPort == null) {
_logger.e("Couldn't get Port from IsolateNameServer!");
receivePort.close();
return null;
}
_logger.i('Sending communication attempt...');
rootPort.send(receivePort.sendPort);
try {
final dynamic message = await receivePort.first;
if (message is RootIsolateToken) {
_logger.i('Got RootIsolateToken, returning...');
return message;
}
_logger.w("Couldn't get RootIsolateToken!");
return null;
} finally {
_logger.i('Closing receivePort...');
receivePort.close();
}
}
@@ -58,6 +69,8 @@ class PortController {
void _registerRootIsolateTokenSender() {
final ReceivePort receivePort = ReceivePort()
..listen((final value) {
_logger.d('Received Message with $value');
if (value is SendPort) {
value.send(ServicesBinding.rootIsolateToken);
}

View File

@@ -102,5 +102,6 @@ class AppDatabase extends _$AppDatabase {
sqlite3Wasm: Uri.parse('sqlite3.wasm'),
driftWorker: Uri.parse('drift_worker.js'),
),
native: const DriftNativeOptions(shareAcrossIsolates: true)
);
}

View File

@@ -577,8 +577,7 @@ class RecurringTransaction extends DataClass
String toString() {
return (StringBuffer('RecurringTransaction(')
..write('id: $id, ')
..write('name: $name, ')
..write('startDate: $startDate, ')..write(
..write('name: $name, ')..write('startDate: $startDate, ')..write(
'timeFrame: $timeFrame, ')..write('amount: $amount, ')..write(
'accountId: $accountId, ')..write('updatedAt: $updatedAt')
..write(')'))
@@ -702,8 +701,7 @@ class RecurringTransactionsCompanion
String toString() {
return (StringBuffer('RecurringTransactionsCompanion(')
..write('id: $id, ')
..write('name: $name, ')
..write('startDate: $startDate, ')..write(
..write('name: $name, ')..write('startDate: $startDate, ')..write(
'timeFrame: $timeFrame, ')..write('amount: $amount, ')..write(
'accountId: $accountId, ')..write('updatedAt: $updatedAt')
..write(')'))
@@ -1076,11 +1074,14 @@ class Transaction extends DataClass implements Insertable<Transaction> {
@override
String toString() {
return (StringBuffer('Transaction(')
..write('id: $id, ')..write('name: $name, ')..write(
'date: $date, ')..write('amount: $amount, ')..write(
'checked: $checked, ')..write('accountId: $accountId, ')..write(
'recurringTransactionId: $recurringTransactionId, ')..write(
'updatedAt: $updatedAt')
..write('id: $id, ')
..write('name: $name, ')
..write('date: $date, ')
..write('amount: $amount, ')
..write('checked: $checked, ')
..write('accountId: $accountId, ')
..write('recurringTransactionId: $recurringTransactionId, ')
..write('updatedAt: $updatedAt')
..write(')'))
.toString();
}
@@ -1220,11 +1221,15 @@ class TransactionsCompanion extends UpdateCompanion<Transaction> {
@override
String toString() {
return (StringBuffer('TransactionsCompanion(')
..write('id: $id, ')..write('name: $name, ')..write(
'date: $date, ')..write('amount: $amount, ')..write(
'checked: $checked, ')..write('accountId: $accountId, ')..write(
'recurringTransactionId: $recurringTransactionId, ')..write(
'updatedAt: $updatedAt')..write(')'))
..write('id: $id, ')
..write('name: $name, ')
..write('date: $date, ')
..write('amount: $amount, ')
..write('checked: $checked, ')
..write('accountId: $accountId, ')
..write('recurringTransactionId: $recurringTransactionId, ')
..write('updatedAt: $updatedAt')
..write(')'))
.toString();
}
}
@@ -1233,9 +1238,7 @@ class $SyncLogTable extends SyncLog with TableInfo<$SyncLogTable, SyncLogData> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
$SyncLogTable(this.attachedDatabase, [this._alias]);
static const VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<int> id = GeneratedColumn<int>(
@@ -1251,13 +1254,13 @@ class $SyncLogTable extends SyncLog with TableInfo<$SyncLogTable, SyncLogData> {
);
@override
late final GeneratedColumnWithTypeConverter<SyncLogTypeEnum, int> type =
GeneratedColumn<int>(
'type',
aliasedName,
false,
type: DriftSqlType.int,
requiredDuringInsert: true,
).withConverter<SyncLogTypeEnum>($SyncLogTable.$convertertype);
GeneratedColumn<int>(
'type',
aliasedName,
false,
type: DriftSqlType.int,
requiredDuringInsert: true,
).withConverter<SyncLogTypeEnum>($SyncLogTable.$convertertype);
static const VerificationMeta _descriptionMeta = const VerificationMeta(
'description',
);
@@ -1282,19 +1285,16 @@ class $SyncLogTable extends SyncLog with TableInfo<$SyncLogTable, SyncLogData> {
requiredDuringInsert: false,
defaultValue: currentDateAndTime,
);
@override
List<GeneratedColumn> get $columns => [id, type, description, updatedAt];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'sync_log';
@override
VerificationContext validateIntegrity(Insertable<SyncLogData> instance, {
VerificationContext validateIntegrity(
Insertable<SyncLogData> instance, {
bool isInserting = false,
}) {
final context = VerificationContext();
@@ -1322,7 +1322,6 @@ class $SyncLogTable extends SyncLog with TableInfo<$SyncLogTable, SyncLogData> {
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
SyncLogData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
@@ -1354,7 +1353,7 @@ class $SyncLogTable extends SyncLog with TableInfo<$SyncLogTable, SyncLogData> {
}
static JsonTypeConverter2<SyncLogTypeEnum, int, int> $convertertype =
const EnumIndexConverter<SyncLogTypeEnum>(SyncLogTypeEnum.values);
const EnumIndexConverter<SyncLogTypeEnum>(SyncLogTypeEnum.values);
}
class SyncLogData extends DataClass implements Insertable<SyncLogData> {
@@ -1369,14 +1368,12 @@ class SyncLogData extends DataClass implements Insertable<SyncLogData> {
/// Wann dieser SyncLog das letzte mal geupdated wurde
final DateTime updatedAt;
const SyncLogData({
required this.id,
required this.type,
required this.description,
required this.updatedAt,
});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
@@ -1398,7 +1395,8 @@ class SyncLogData extends DataClass implements Insertable<SyncLogData> {
);
}
factory SyncLogData.fromJson(Map<String, dynamic> json, {
factory SyncLogData.fromJson(
Map<String, dynamic> json, {
ValueSerializer? serializer,
}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
@@ -1411,7 +1409,6 @@ class SyncLogData extends DataClass implements Insertable<SyncLogData> {
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
@@ -1428,14 +1425,12 @@ class SyncLogData extends DataClass implements Insertable<SyncLogData> {
SyncLogTypeEnum? type,
String? description,
DateTime? updatedAt,
}) =>
SyncLogData(
id: id ?? this.id,
type: type ?? this.type,
description: description ?? this.description,
updatedAt: updatedAt ?? this.updatedAt,
);
}) => SyncLogData(
id: id ?? this.id,
type: type ?? this.type,
description: description ?? this.description,
updatedAt: updatedAt ?? this.updatedAt,
);
SyncLogData copyWithCompanion(SyncLogCompanion data) {
return SyncLogData(
id: data.id.present ? data.id.value : this.id,
@@ -1450,23 +1445,24 @@ class SyncLogData extends DataClass implements Insertable<SyncLogData> {
@override
String toString() {
return (StringBuffer('SyncLogData(')
..write('id: $id, ')..write('type: $type, ')..write(
'description: $description, ')..write('updatedAt: $updatedAt')..write(
')'))
..write('id: $id, ')
..write('type: $type, ')
..write('description: $description, ')
..write('updatedAt: $updatedAt')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, type, description, updatedAt);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is SyncLogData &&
other.id == this.id &&
other.type == this.type &&
other.description == this.description &&
other.updatedAt == this.updatedAt);
(other is SyncLogData &&
other.id == this.id &&
other.type == this.type &&
other.description == this.description &&
other.updatedAt == this.updatedAt);
}
class SyncLogCompanion extends UpdateCompanion<SyncLogData> {
@@ -1474,21 +1470,18 @@ class SyncLogCompanion extends UpdateCompanion<SyncLogData> {
final Value<SyncLogTypeEnum> type;
final Value<String> description;
final Value<DateTime> updatedAt;
const SyncLogCompanion({
this.id = const Value.absent(),
this.type = const Value.absent(),
this.description = const Value.absent(),
this.updatedAt = const Value.absent(),
});
SyncLogCompanion.insert({
this.id = const Value.absent(),
required SyncLogTypeEnum type,
this.description = const Value.absent(),
this.updatedAt = const Value.absent(),
}) : type = Value(type);
static Insertable<SyncLogData> custom({
Expression<int>? id,
Expression<int>? type,
@@ -1540,8 +1533,10 @@ class SyncLogCompanion extends UpdateCompanion<SyncLogData> {
@override
String toString() {
return (StringBuffer('SyncLogCompanion(')
..write('id: $id, ')..write('type: $type, ')..write(
'description: $description, ')..write('updatedAt: $updatedAt')
..write('id: $id, ')
..write('type: $type, ')
..write('description: $description, ')
..write('updatedAt: $updatedAt')
..write(')'))
.toString();
}
@@ -2905,7 +2900,6 @@ class $$SyncLogTableFilterComposer
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnFilters<int> get id => $composableBuilder(
column: $table.id,
builder: (column) => ColumnFilters(column),
@@ -2937,7 +2931,6 @@ class $$SyncLogTableOrderingComposer
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnOrderings<int> get id => $composableBuilder(
column: $table.id,
builder: (column) => ColumnOrderings(column),
@@ -2968,7 +2961,6 @@ class $$SyncLogTableAnnotationComposer
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
GeneratedColumn<int> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
@@ -3069,7 +3061,6 @@ class $AppDatabaseManager {
$$RecurringTransactionsTableTableManager(_db, _db.recurringTransactions);
$$TransactionsTableTableManager get transactions =>
$$TransactionsTableTableManager(_db, _db.transactions);
$$SyncLogTableTableManager get syncLog =>
$$SyncLogTableTableManager(_db, _db.syncLog);
}

View File

@@ -0,0 +1,16 @@
import 'package:flutter/services.dart';
import 'package:logger/logger.dart';
import '../Controller/port_controller.dart';
/// Initialisiert benötigte Services in Background-Isolates
Future<void> initBackground() async {
final Logger logger = Logger()..d('Init Background for Native');
final RootIsolateToken? rootIsolateToken = await PortController()
.getRootIsolateToken();
if (rootIsolateToken != null) {
logger.i('Initialising BackgroundIsolateBinaryMessenger...');
BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);
}
}

View File

@@ -0,0 +1,6 @@
import 'package:logger/logger.dart';
/// Initialisiert benötigte Services in Background-Isolates für Web
Future<void> initBackground() async {
Logger().d('Init Background for Web');
}

View File

@@ -1,4 +1,5 @@
import 'package:drift/drift.dart';
import 'package:logger/logger.dart';
import '../Entities/drift_database.dart';
import '../Entities/time_frame_enum.dart';
@@ -13,12 +14,18 @@ class GenerateTransactionsTask extends Task {
final RecurringTransactionRepository _recurringTransactionRepository =
RecurringTransactionRepository();
final Logger _logger = Logger();
@override
Future<bool> execute() async {
_logger.i('Generating Transactions of recurring Transactions...');
final List<RecurringTransaction> recurringTransactions =
await _recurringTransactionRepository.findBy();
for (final recurringTransaction in recurringTransactions) {
_logger.i('Generating Transactions of $recurringTransaction...');
final List<Transaction> transactions = await _transactionRepository
.findBy(
recurringTransaction: recurringTransaction,
@@ -43,6 +50,7 @@ class GenerateTransactionsTask extends Task {
}
if (DateTime.now().compareTo(nextTransactionDate) <= 0) {
// TODO: Nicht mit NOW, sondern Ende dieses Monats Vergleichen
final TransactionsCompanion transaction = TransactionsCompanion(
name: Value(recurringTransaction.name),
date: Value(nextTransactionDate),
@@ -52,10 +60,15 @@ class GenerateTransactionsTask extends Task {
recurringTransactionId: Value(recurringTransaction.id),
);
_logger.i(
'Adding transaction ${transaction.name} on ${transaction.date}',
);
await _transactionRepository.add(transaction);
}
}
_logger.i('Generating transactions completed.');
return true;
}
}

38
lib/Tasks/workers.dart Normal file
View File

@@ -0,0 +1,38 @@
import 'dart:async';
import 'package:isolate_manager/isolate_manager.dart';
import 'background_init_web.dart' if (dart.library.io) 'background_init.dart';
import 'generate_transactions_task.dart';
@pragma('vm:entry-point')
@isolateManagerWorker
/// Führt eine Hintergrundtask lokal aus
Future<void> runTask(final Map<String, dynamic> params) async {
final String taskName = params['taskName'];
final int initialDelayMinutes = params['initialDelayMinutes'];
final int frequencyMinutes = params['frequencyMinutes'];
await Future.delayed(Duration(minutes: initialDelayMinutes));
await executeTask(taskName, null);
await Future.delayed(Duration(minutes: frequencyMinutes));
unawaited(runTask(params));
}
/// Funktion um Hintergrundaufgaben auszuführen
Future<bool> executeTask(
final String taskName,
final Map<String, dynamic>? inputData,
) async {
await initBackground();
switch (taskName) {
case 'generate_transactions':
return GenerateTransactionsTask().execute();
}
return Future.value(true);
}

View File

@@ -0,0 +1,9 @@
import 'package:workmanager/workmanager.dart';
import 'workers.dart';
/// Die Funktion wird von Hintergrundtasks ausgerufen, um diese auszuführen
@pragma('vm:entry-point')
void callbackDispatcher() {
Workmanager().executeTask(executeTask);
}

View File

@@ -3,6 +3,7 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:routemaster/routemaster.dart';
import 'Controller/background_task_controller.dart';
import 'Controller/port_controller.dart';
import 'Services/navigation_service.dart';
import 'Services/router_service.dart';
import 'Services/theme_service.dart';
@@ -10,6 +11,7 @@ import 'Services/theme_service.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
PortController();
BackgroundTaskController();
runApp(