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:async';
import 'dart:io'; 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 'package:workmanager/workmanager.dart';
import '../Tasks/generate_transactions_task.dart'; import '../Tasks/workers.dart';
import '../Tasks/task.dart'; import '../Tasks/workmanager_workers.dart';
import 'port_controller.dart';
/// Erstellt Hintergrundtasks und führt diese aus /// Erstellt Hintergrundtasks und führt diese aus
class BackgroundTaskController { class BackgroundTaskController {
/// Erstellt eine neue Instanz dieser Klasse /// Erstellt eine neue Instanz dieser Klasse
BackgroundTaskController() { BackgroundTaskController() {
if (Platform.isAndroid) { if (!kIsWeb && Platform.isAndroid) {
unawaited(Workmanager().initialize(callbackDispatcher)); unawaited(Workmanager().initialize(callbackDispatcher));
unawaited( unawaited(
Workmanager().registerPeriodicTask( Workmanager().registerPeriodicTask(
@@ -25,47 +24,12 @@ class BackgroundTaskController {
); );
} else { } else {
unawaited( unawaited(
Isolate.run(() { IsolateManager.runFunction(runTask, {
unawaited(_runTask( 'taskName': 'generate_transactions',
GenerateTransactionsTask(), 'initialDelayMinutes': 1,
const Duration(minutes: 1), 'frequencyMinutes': 30,
const Duration(minutes: 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:isolate';
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.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 /// main und anderen Isolates
class PortController { class PortController {
/// Gibt eine Instanz dieser Klasse zurück /// Gibt eine Instanz dieser Klasse zurück
factory PortController() => _instance; factory PortController() => _instance;
PortController._internal() { PortController._internal() {
if (ServicesBinding.rootIsolateToken != null) { if (!kIsWeb && ServicesBinding.rootIsolateToken != null) {
_registerRootIsolateTokenSender(); _registerRootIsolateTokenSender();
} }
} }
static final PortController _instance = PortController._internal(); static final PortController _instance = PortController._internal();
final Logger _logger = Logger();
/// Fügt einen Port mit [name] zum NameServer hinzu /// Fügt einen Port mit [name] zum NameServer hinzu
void addPort(final SendPort sendPort, final String name) { void addPort(final SendPort sendPort, final String name) {
IsolateNameServer.removePortNameMapping(name);
IsolateNameServer.registerPortWithName(sendPort, name); IsolateNameServer.registerPortWithName(sendPort, name);
} }
@@ -29,6 +33,8 @@ class PortController {
/// Gibt das [RootIsolateToken] der main-Isolate zurück /// Gibt das [RootIsolateToken] der main-Isolate zurück
Future<RootIsolateToken?> getRootIsolateToken() async { Future<RootIsolateToken?> getRootIsolateToken() async {
_logger.d('Trying to retrieve RootIsolateToken...');
final ReceivePort receivePort = ReceivePort(); final ReceivePort receivePort = ReceivePort();
final SendPort? rootPort = IsolateNameServer.lookupPortByName( final SendPort? rootPort = IsolateNameServer.lookupPortByName(
@@ -36,21 +42,26 @@ class PortController {
); );
if (rootPort == null) { if (rootPort == null) {
_logger.e("Couldn't get Port from IsolateNameServer!");
receivePort.close(); receivePort.close();
return null; return null;
} }
_logger.i('Sending communication attempt...');
rootPort.send(receivePort.sendPort); rootPort.send(receivePort.sendPort);
try { try {
final dynamic message = await receivePort.first; final dynamic message = await receivePort.first;
if (message is RootIsolateToken) { if (message is RootIsolateToken) {
_logger.i('Got RootIsolateToken, returning...');
return message; return message;
} }
_logger.w("Couldn't get RootIsolateToken!");
return null; return null;
} finally { } finally {
_logger.i('Closing receivePort...');
receivePort.close(); receivePort.close();
} }
} }
@@ -58,6 +69,8 @@ class PortController {
void _registerRootIsolateTokenSender() { void _registerRootIsolateTokenSender() {
final ReceivePort receivePort = ReceivePort() final ReceivePort receivePort = ReceivePort()
..listen((final value) { ..listen((final value) {
_logger.d('Received Message with $value');
if (value is SendPort) { if (value is SendPort) {
value.send(ServicesBinding.rootIsolateToken); value.send(ServicesBinding.rootIsolateToken);
} }

View File

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

View File

@@ -577,8 +577,7 @@ class RecurringTransaction extends DataClass
String toString() { String toString() {
return (StringBuffer('RecurringTransaction(') return (StringBuffer('RecurringTransaction(')
..write('id: $id, ') ..write('id: $id, ')
..write('name: $name, ') ..write('name: $name, ')..write('startDate: $startDate, ')..write(
..write('startDate: $startDate, ')..write(
'timeFrame: $timeFrame, ')..write('amount: $amount, ')..write( 'timeFrame: $timeFrame, ')..write('amount: $amount, ')..write(
'accountId: $accountId, ')..write('updatedAt: $updatedAt') 'accountId: $accountId, ')..write('updatedAt: $updatedAt')
..write(')')) ..write(')'))
@@ -702,8 +701,7 @@ class RecurringTransactionsCompanion
String toString() { String toString() {
return (StringBuffer('RecurringTransactionsCompanion(') return (StringBuffer('RecurringTransactionsCompanion(')
..write('id: $id, ') ..write('id: $id, ')
..write('name: $name, ') ..write('name: $name, ')..write('startDate: $startDate, ')..write(
..write('startDate: $startDate, ')..write(
'timeFrame: $timeFrame, ')..write('amount: $amount, ')..write( 'timeFrame: $timeFrame, ')..write('amount: $amount, ')..write(
'accountId: $accountId, ')..write('updatedAt: $updatedAt') 'accountId: $accountId, ')..write('updatedAt: $updatedAt')
..write(')')) ..write(')'))
@@ -1076,11 +1074,14 @@ class Transaction extends DataClass implements Insertable<Transaction> {
@override @override
String toString() { String toString() {
return (StringBuffer('Transaction(') return (StringBuffer('Transaction(')
..write('id: $id, ')..write('name: $name, ')..write( ..write('id: $id, ')
'date: $date, ')..write('amount: $amount, ')..write( ..write('name: $name, ')
'checked: $checked, ')..write('accountId: $accountId, ')..write( ..write('date: $date, ')
'recurringTransactionId: $recurringTransactionId, ')..write( ..write('amount: $amount, ')
'updatedAt: $updatedAt') ..write('checked: $checked, ')
..write('accountId: $accountId, ')
..write('recurringTransactionId: $recurringTransactionId, ')
..write('updatedAt: $updatedAt')
..write(')')) ..write(')'))
.toString(); .toString();
} }
@@ -1220,11 +1221,15 @@ class TransactionsCompanion extends UpdateCompanion<Transaction> {
@override @override
String toString() { String toString() {
return (StringBuffer('TransactionsCompanion(') return (StringBuffer('TransactionsCompanion(')
..write('id: $id, ')..write('name: $name, ')..write( ..write('id: $id, ')
'date: $date, ')..write('amount: $amount, ')..write( ..write('name: $name, ')
'checked: $checked, ')..write('accountId: $accountId, ')..write( ..write('date: $date, ')
'recurringTransactionId: $recurringTransactionId, ')..write( ..write('amount: $amount, ')
'updatedAt: $updatedAt')..write(')')) ..write('checked: $checked, ')
..write('accountId: $accountId, ')
..write('recurringTransactionId: $recurringTransactionId, ')
..write('updatedAt: $updatedAt')
..write(')'))
.toString(); .toString();
} }
} }
@@ -1233,9 +1238,7 @@ class $SyncLogTable extends SyncLog with TableInfo<$SyncLogTable, SyncLogData> {
@override @override
final GeneratedDatabase attachedDatabase; final GeneratedDatabase attachedDatabase;
final String? _alias; final String? _alias;
$SyncLogTable(this.attachedDatabase, [this._alias]); $SyncLogTable(this.attachedDatabase, [this._alias]);
static const VerificationMeta _idMeta = const VerificationMeta('id'); static const VerificationMeta _idMeta = const VerificationMeta('id');
@override @override
late final GeneratedColumn<int> id = GeneratedColumn<int>( late final GeneratedColumn<int> id = GeneratedColumn<int>(
@@ -1282,19 +1285,16 @@ class $SyncLogTable extends SyncLog with TableInfo<$SyncLogTable, SyncLogData> {
requiredDuringInsert: false, requiredDuringInsert: false,
defaultValue: currentDateAndTime, defaultValue: currentDateAndTime,
); );
@override @override
List<GeneratedColumn> get $columns => [id, type, description, updatedAt]; List<GeneratedColumn> get $columns => [id, type, description, updatedAt];
@override @override
String get aliasedName => _alias ?? actualTableName; String get aliasedName => _alias ?? actualTableName;
@override @override
String get actualTableName => $name; String get actualTableName => $name;
static const String $name = 'sync_log'; static const String $name = 'sync_log';
@override @override
VerificationContext validateIntegrity(Insertable<SyncLogData> instance, { VerificationContext validateIntegrity(
Insertable<SyncLogData> instance, {
bool isInserting = false, bool isInserting = false,
}) { }) {
final context = VerificationContext(); final context = VerificationContext();
@@ -1322,7 +1322,6 @@ class $SyncLogTable extends SyncLog with TableInfo<$SyncLogTable, SyncLogData> {
@override @override
Set<GeneratedColumn> get $primaryKey => {id}; Set<GeneratedColumn> get $primaryKey => {id};
@override @override
SyncLogData map(Map<String, dynamic> data, {String? tablePrefix}) { SyncLogData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
@@ -1369,14 +1368,12 @@ class SyncLogData extends DataClass implements Insertable<SyncLogData> {
/// Wann dieser SyncLog das letzte mal geupdated wurde /// Wann dieser SyncLog das letzte mal geupdated wurde
final DateTime updatedAt; final DateTime updatedAt;
const SyncLogData({ const SyncLogData({
required this.id, required this.id,
required this.type, required this.type,
required this.description, required this.description,
required this.updatedAt, required this.updatedAt,
}); });
@override @override
Map<String, Expression> toColumns(bool nullToAbsent) { Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{}; 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, ValueSerializer? serializer,
}) { }) {
serializer ??= driftRuntimeOptions.defaultSerializer; serializer ??= driftRuntimeOptions.defaultSerializer;
@@ -1411,7 +1409,6 @@ class SyncLogData extends DataClass implements Insertable<SyncLogData> {
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']), updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
); );
} }
@override @override
Map<String, dynamic> toJson({ValueSerializer? serializer}) { Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer; serializer ??= driftRuntimeOptions.defaultSerializer;
@@ -1428,14 +1425,12 @@ class SyncLogData extends DataClass implements Insertable<SyncLogData> {
SyncLogTypeEnum? type, SyncLogTypeEnum? type,
String? description, String? description,
DateTime? updatedAt, DateTime? updatedAt,
}) => }) => SyncLogData(
SyncLogData(
id: id ?? this.id, id: id ?? this.id,
type: type ?? this.type, type: type ?? this.type,
description: description ?? this.description, description: description ?? this.description,
updatedAt: updatedAt ?? this.updatedAt, updatedAt: updatedAt ?? this.updatedAt,
); );
SyncLogData copyWithCompanion(SyncLogCompanion data) { SyncLogData copyWithCompanion(SyncLogCompanion data) {
return SyncLogData( return SyncLogData(
id: data.id.present ? data.id.value : this.id, id: data.id.present ? data.id.value : this.id,
@@ -1450,15 +1445,16 @@ class SyncLogData extends DataClass implements Insertable<SyncLogData> {
@override @override
String toString() { String toString() {
return (StringBuffer('SyncLogData(') return (StringBuffer('SyncLogData(')
..write('id: $id, ')..write('type: $type, ')..write( ..write('id: $id, ')
'description: $description, ')..write('updatedAt: $updatedAt')..write( ..write('type: $type, ')
')')) ..write('description: $description, ')
..write('updatedAt: $updatedAt')
..write(')'))
.toString(); .toString();
} }
@override @override
int get hashCode => Object.hash(id, type, description, updatedAt); int get hashCode => Object.hash(id, type, description, updatedAt);
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
@@ -1474,21 +1470,18 @@ class SyncLogCompanion extends UpdateCompanion<SyncLogData> {
final Value<SyncLogTypeEnum> type; final Value<SyncLogTypeEnum> type;
final Value<String> description; final Value<String> description;
final Value<DateTime> updatedAt; final Value<DateTime> updatedAt;
const SyncLogCompanion({ const SyncLogCompanion({
this.id = const Value.absent(), this.id = const Value.absent(),
this.type = const Value.absent(), this.type = const Value.absent(),
this.description = const Value.absent(), this.description = const Value.absent(),
this.updatedAt = const Value.absent(), this.updatedAt = const Value.absent(),
}); });
SyncLogCompanion.insert({ SyncLogCompanion.insert({
this.id = const Value.absent(), this.id = const Value.absent(),
required SyncLogTypeEnum type, required SyncLogTypeEnum type,
this.description = const Value.absent(), this.description = const Value.absent(),
this.updatedAt = const Value.absent(), this.updatedAt = const Value.absent(),
}) : type = Value(type); }) : type = Value(type);
static Insertable<SyncLogData> custom({ static Insertable<SyncLogData> custom({
Expression<int>? id, Expression<int>? id,
Expression<int>? type, Expression<int>? type,
@@ -1540,8 +1533,10 @@ class SyncLogCompanion extends UpdateCompanion<SyncLogData> {
@override @override
String toString() { String toString() {
return (StringBuffer('SyncLogCompanion(') return (StringBuffer('SyncLogCompanion(')
..write('id: $id, ')..write('type: $type, ')..write( ..write('id: $id, ')
'description: $description, ')..write('updatedAt: $updatedAt') ..write('type: $type, ')
..write('description: $description, ')
..write('updatedAt: $updatedAt')
..write(')')) ..write(')'))
.toString(); .toString();
} }
@@ -2905,7 +2900,6 @@ class $$SyncLogTableFilterComposer
super.$addJoinBuilderToRootComposer, super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer, super.$removeJoinBuilderFromRootComposer,
}); });
ColumnFilters<int> get id => $composableBuilder( ColumnFilters<int> get id => $composableBuilder(
column: $table.id, column: $table.id,
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
@@ -2937,7 +2931,6 @@ class $$SyncLogTableOrderingComposer
super.$addJoinBuilderToRootComposer, super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer, super.$removeJoinBuilderFromRootComposer,
}); });
ColumnOrderings<int> get id => $composableBuilder( ColumnOrderings<int> get id => $composableBuilder(
column: $table.id, column: $table.id,
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
@@ -2968,7 +2961,6 @@ class $$SyncLogTableAnnotationComposer
super.$addJoinBuilderToRootComposer, super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer, super.$removeJoinBuilderFromRootComposer,
}); });
GeneratedColumn<int> get id => GeneratedColumn<int> get id =>
$composableBuilder(column: $table.id, builder: (column) => column); $composableBuilder(column: $table.id, builder: (column) => column);
@@ -3069,7 +3061,6 @@ class $AppDatabaseManager {
$$RecurringTransactionsTableTableManager(_db, _db.recurringTransactions); $$RecurringTransactionsTableTableManager(_db, _db.recurringTransactions);
$$TransactionsTableTableManager get transactions => $$TransactionsTableTableManager get transactions =>
$$TransactionsTableTableManager(_db, _db.transactions); $$TransactionsTableTableManager(_db, _db.transactions);
$$SyncLogTableTableManager get syncLog => $$SyncLogTableTableManager get syncLog =>
$$SyncLogTableTableManager(_db, _db.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:drift/drift.dart';
import 'package:logger/logger.dart';
import '../Entities/drift_database.dart'; import '../Entities/drift_database.dart';
import '../Entities/time_frame_enum.dart'; import '../Entities/time_frame_enum.dart';
@@ -13,12 +14,18 @@ class GenerateTransactionsTask extends Task {
final RecurringTransactionRepository _recurringTransactionRepository = final RecurringTransactionRepository _recurringTransactionRepository =
RecurringTransactionRepository(); RecurringTransactionRepository();
final Logger _logger = Logger();
@override @override
Future<bool> execute() async { Future<bool> execute() async {
_logger.i('Generating Transactions of recurring Transactions...');
final List<RecurringTransaction> recurringTransactions = final List<RecurringTransaction> recurringTransactions =
await _recurringTransactionRepository.findBy(); await _recurringTransactionRepository.findBy();
for (final recurringTransaction in recurringTransactions) { for (final recurringTransaction in recurringTransactions) {
_logger.i('Generating Transactions of $recurringTransaction...');
final List<Transaction> transactions = await _transactionRepository final List<Transaction> transactions = await _transactionRepository
.findBy( .findBy(
recurringTransaction: recurringTransaction, recurringTransaction: recurringTransaction,
@@ -43,6 +50,7 @@ class GenerateTransactionsTask extends Task {
} }
if (DateTime.now().compareTo(nextTransactionDate) <= 0) { if (DateTime.now().compareTo(nextTransactionDate) <= 0) {
// TODO: Nicht mit NOW, sondern Ende dieses Monats Vergleichen
final TransactionsCompanion transaction = TransactionsCompanion( final TransactionsCompanion transaction = TransactionsCompanion(
name: Value(recurringTransaction.name), name: Value(recurringTransaction.name),
date: Value(nextTransactionDate), date: Value(nextTransactionDate),
@@ -52,10 +60,15 @@ class GenerateTransactionsTask extends Task {
recurringTransactionId: Value(recurringTransaction.id), recurringTransactionId: Value(recurringTransaction.id),
); );
_logger.i(
'Adding transaction ${transaction.name} on ${transaction.date}',
);
await _transactionRepository.add(transaction); await _transactionRepository.add(transaction);
} }
} }
_logger.i('Generating transactions completed.');
return true; 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 'package:routemaster/routemaster.dart';
import 'Controller/background_task_controller.dart'; import 'Controller/background_task_controller.dart';
import 'Controller/port_controller.dart';
import 'Services/navigation_service.dart'; import 'Services/navigation_service.dart';
import 'Services/router_service.dart'; import 'Services/router_service.dart';
import 'Services/theme_service.dart'; import 'Services/theme_service.dart';
@@ -10,6 +11,7 @@ import 'Services/theme_service.dart';
void main() { void main() {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
PortController();
BackgroundTaskController(); BackgroundTaskController();
runApp( runApp(

15465
web/runTask.js Normal file

File diff suppressed because one or more lines are too long