diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..8f8d1a9 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,87 @@ +name: Flutter Workflow + +on: + push: + branches-ignore: + - main +jobs: + # Check app for format and lint exceptions (fail fast) + check: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Install Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.3.0' + channel: 'stable' + - run: flutter --version + - run: make format + - run: make clean + - run: make lint + # Check code quality + code_quality: + needs: check + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Install Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.3.0' + channel: 'stable' + - run: export PATH="$PATH":"$HOME/.pub-cache/bin" + - run: flutter pub global activate dart_code_metrics + - run: metrics lib -r codeclimate > gh-code-quality-report.json + - name: Upload Code Quality Report + uses: actions/upload-artifact@v3 + with: + name: gh-code-quality-report + path: gh-code-quality-report.json + # Run tests and report quality + test: + needs: check + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Install Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.3.0' + channel: 'stable' + - run: export PATH="$PATH":"$HOME/.pub-cache/bin" + - run: flutter pub global activate junitreport + - run: flutter test --machine --coverage | tojunit -o report.xml + - name: Upload Report + uses: actions/upload-artifact@v3 + with: + name: report + path: report.xml + # Build the app to check if release builds still work + apk: + needs: [check, test, code_quality] + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Setup Java + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '11' + - name: Install Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.3.0' + channel: 'stable' + - run: make build-android-apk + - name: 'Upload APK' + uses: actions/upload-artifact@v3 + with: + name: apk + path: build-output/app.apk + retention-days: 2 + \ No newline at end of file diff --git a/.gitignore b/.gitignore index 24476c5..3387775 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + + +.build-output \ No newline at end of file diff --git a/build-output/.gitkeep b/build-output/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/lib/main.dart b/lib/main.dart index 9274c45..82e9561 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,13 +1,17 @@ import 'package:counter_workshop/src/app.dart'; +import 'package:counter_workshop/src/core/logger/app_logger.dart'; import 'package:counter_workshop/src/features/counter/data/datasources/remote/src/mock/counter_fake.api.dart'; import 'package:counter_workshop/src/features/counter/data/repositories/counter.repository.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:intl/intl_standalone.dart' if (dart.library.html) 'package:intl/intl_browser.dart'; +import 'package:logging/logging.dart'; Future main() async { Intl.systemLocale = await findSystemLocale(); + AppLogger(level: Level.WARNING); final CounterRepository counterRepository = CounterRepository(counterApi: CounterFakeApi()); + runApp( App( counterRepository: counterRepository, diff --git a/lib/src/core/logger/app_logger.dart b/lib/src/core/logger/app_logger.dart new file mode 100644 index 0000000..eb19c5a --- /dev/null +++ b/lib/src/core/logger/app_logger.dart @@ -0,0 +1,26 @@ +import 'package:counter_workshop/src/core/logger/appenders.dart'; +import 'package:counter_workshop/src/core/logger/log_formatters.dart'; +import 'package:flutter/foundation.dart'; +import 'package:logging/logging.dart'; + +final appLogger = Logger('CounterWorkshop'); + +class AppLogger { + // ignore: unused_field + static late AppLogger _singleton; + + factory AppLogger({Level level = Level.ALL}) => _singleton = AppLogger._initLogger(level); + + AppLogger._initLogger(Level level) { + recordStackTraceAtLevel = level; + appLogger.info('App Started'); + if (kDebugMode) { + ConsoleLogAppender(formatter: const ConsoleLogFormatter()).attachToLogger(Logger.root); + FileLogAppender(formatter: const FileLogFormatter()).attachToLogger(Logger.root); + } + + if (kReleaseMode) { + FileLogAppender(formatter: const FileLogFormatter()).attachToLogger(Logger.root); + } + } +} diff --git a/lib/src/core/logger/appenders.dart b/lib/src/core/logger/appenders.dart new file mode 100644 index 0000000..58053ee --- /dev/null +++ b/lib/src/core/logger/appenders.dart @@ -0,0 +1,50 @@ +import 'dart:developer'; + +import 'package:counter_workshop/src/core/logger/log_formatters.dart'; +import 'package:logging/logging.dart'; +import 'package:logging_appenders/logging_appenders.dart'; + +class ConsoleLogAppender extends BaseLogAppender { + void Function(Object line)? printer; + + ConsoleLogAppender({LogRecordFormatter? formatter}) : super(formatter ?? defaultDebugConsoleLogFormatter()); + + ConsoleLogAppender setupLogging({Level level = Level.ALL, Level stderrLevel = Level.OFF}) { + Logger.root.clearListeners(); + Logger.root.level = level; + + return defaultLogAppender(stderrLevel: stderrLevel)..attachToLogger(Logger.root); + } + + @override + void handle(LogRecord record) { + log(formatter.format(record)); + } +} + +ConsoleLogAppender defaultLogAppender({LogRecordFormatter? formatter, Level? stderrLevel}) { + return ConsoleLogAppender(formatter: formatter); +} + +class FileLogAppender extends RotatingFileAppender { + void Function(Object line)? printer; + + FileLogAppender({LogRecordFormatter? formatter}) + : super(baseFilePath: '/counter_workshop_logs', formatter: formatter); + + FileLogAppender setupLogging({Level level = Level.ALL, Level stderrLevel = Level.OFF}) { + Logger.root.clearListeners(); + Logger.root.level = level; + + return fileLogAppender(stderrLevel: stderrLevel)..attachToLogger(Logger.root); + } + + @override + void handle(LogRecord record) { + //log(formatter.format(record)); + } +} + +FileLogAppender fileLogAppender({LogRecordFormatter? formatter, Level? stderrLevel}) { + return FileLogAppender(formatter: formatter); +} diff --git a/lib/src/core/logger/log_formatters.dart b/lib/src/core/logger/log_formatters.dart new file mode 100644 index 0000000..d79adc0 --- /dev/null +++ b/lib/src/core/logger/log_formatters.dart @@ -0,0 +1,84 @@ +import 'package:logging/logging.dart'; +import 'package:logging_appenders/logging_appenders.dart'; + +LogRecordFormatter defaultDebugConsoleLogFormatter() => const DefaultLogRecordFormatter(); + +LogRecordFormatter defaultFileLogFormatter() => const DefaultLogRecordFormatter(); + +class ConsoleLogFormatter extends LogRecordFormatter { + const ConsoleLogFormatter(); + + @override + StringBuffer formatToStringBuffer(LogRecord rec, StringBuffer sb) { + String outputColor = FormatterHelper.defineOutputColor(rec.level); + sb.write( + '$outputColor [${rec.time}] [${rec.level.name}] [${rec.zone}] ' + '[${rec.loggerName}] - \x1B[34m${rec.message}', + ); + + if (rec.error != null) { + sb.writeln(); + sb.write('### ${rec.error?.runtimeType}: '); + sb.write(rec.error); + } + final stackTrace = rec.stackTrace ?? (rec.error is Error ? (rec.error as Error).stackTrace : null); + if (stackTrace != null) { + sb.writeln(); + sb.write(stackTrace); + } + sb.write('\x1B[0m'); + return sb; + } +} + +class FileLogFormatter extends LogRecordFormatter { + const FileLogFormatter(); + + @override + StringBuffer formatToStringBuffer(LogRecord rec, StringBuffer sb) { + sb.write( + '[${rec.time}] [${rec.level.name}] [${rec.zone}] ' + '[${rec.loggerName}] - ${rec.message}', + ); + + if (rec.error != null) { + sb.writeln(); + sb.write('### ${rec.error?.runtimeType}: '); + sb.write(rec.error); + } + final stackTrace = rec.stackTrace ?? (rec.error is Error ? (rec.error as Error).stackTrace : null); + if (stackTrace != null) { + sb.writeln(); + sb.write(stackTrace); + } + return sb; + } +} + +class FormatterHelper { + FormatterHelper._(); + + static String defineOutputColor(Level level) { + String outputColor = ''; + switch (level.name) { + case 'ALL': + case 'FINEST': + case 'FINER': + case 'FINE': + case 'CONFIG': + outputColor = '\x1B[37m'; + break; + case 'INFO': + case 'WARNING': + outputColor = '\x1B[33m'; + break; + case 'SEVERE': + case 'SHOUT': + outputColor = '\x1B[31m'; + break; + default: + outputColor = '\x1B[37m'; + } + return outputColor; + } +} diff --git a/lib/src/core/routing/router.dart b/lib/src/core/routing/router.dart index d75556a..46d5e82 100644 --- a/lib/src/core/routing/router.dart +++ b/lib/src/core/routing/router.dart @@ -6,7 +6,7 @@ import 'package:go_router/go_router.dart'; final router = GoRouter( urlPathStrategy: UrlPathStrategy.path, - debugLogDiagnostics: true, + debugLogDiagnostics: false, // Logs werden durch den Logger übernommen initialLocation: '/counters', routes: [ GoRoute( diff --git a/lib/src/features/counter/data/repositories/counter.repository.dart b/lib/src/features/counter/data/repositories/counter.repository.dart index 0927222..677a7db 100644 --- a/lib/src/features/counter/data/repositories/counter.repository.dart +++ b/lib/src/features/counter/data/repositories/counter.repository.dart @@ -1,11 +1,11 @@ import 'dart:async'; +import 'package:counter_workshop/src/core/logger/app_logger.dart'; import 'package:counter_workshop/src/features/counter/data/datasources/remote/converters/counter_request.converter.dart'; import 'package:counter_workshop/src/features/counter/data/datasources/remote/counter.api.dart'; import 'package:counter_workshop/src/features/counter/data/datasources/remote/converters/counter_response.converter.dart'; import 'package:counter_workshop/src/features/counter/data/datasources/remote/dtos/counter_response.dto.dart'; import 'package:counter_workshop/src/features/counter/domain/model/counter.model.dart'; -import 'dart:developer'; import 'package:counter_workshop/src/features/counter/domain/repository/counter.repository_interface.dart'; @@ -16,7 +16,7 @@ class CounterRepository implements CounterRepositoryInterface { @override Future> getCounterList() async { - log('retriving counter list'); + appLogger.info('retriving counter list'); final List response = await counterApi.fetchAll(); // map result to model @@ -33,7 +33,7 @@ class CounterRepository implements CounterRepositoryInterface { @override Future createCounter({required CounterModel counterModel}) async { - log('creating new counter with name ${counterModel.name}'); + appLogger.info('creating new counter with name ${counterModel.name}'); // map model to dto final dto = CounterRequestConverter().toDto(counterModel); @@ -47,7 +47,7 @@ class CounterRepository implements CounterRepositoryInterface { @override Future updateCounter({required String id, required CounterModel counterModel}) async { - log('updating counter: $id with value: $counterModel'); + appLogger.info('updating counter: $id with value: $counterModel'); // map model to dto final dto = CounterResponseConverter().toDto(counterModel); @@ -58,7 +58,7 @@ class CounterRepository implements CounterRepositoryInterface { @override Future deleteCounter({required String id}) async { - log('deleting counter: $id'); + appLogger.info('deleting counter: $id'); await counterApi.deleteCounter(id); } } diff --git a/lib/src/features/counter/presentation/edit/bloc/edit_counter.bloc.dart b/lib/src/features/counter/presentation/edit/bloc/edit_counter.bloc.dart index 9c196fe..24a7717 100644 --- a/lib/src/features/counter/presentation/edit/bloc/edit_counter.bloc.dart +++ b/lib/src/features/counter/presentation/edit/bloc/edit_counter.bloc.dart @@ -1,9 +1,8 @@ import 'dart:async'; - +import 'package:counter_workshop/src/core/logger/app_logger.dart'; import 'package:counter_workshop/src/features/counter/data/repositories/counter.repository.dart'; import 'package:counter_workshop/src/features/counter/presentation/edit/bloc/edit_counter.event.dart'; import 'package:counter_workshop/src/features/counter/presentation/edit/bloc/edit_counter.state.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class EditCounterBloc extends Bloc { @@ -26,14 +25,14 @@ class EditCounterBloc extends Bloc { } Future _onIncrement(CounterIncrementPressed event, Emitter emit) async { - debugPrint('INCREMENT: ${event.counterModel.toString()}'); + appLogger.info('INCREMENT: ${event.counterModel.toString()}'); final newCounterModel = event.counterModel.copyWith(value: event.counterModel.value + 1); emit(EditCounterData(newCounterModel)); await counterRepository.updateCounter(id: event.counterModel.id, counterModel: newCounterModel); } Future _onDecrement(CounterDecrementPressed event, Emitter emit) async { - debugPrint('DECREMENT: ${event.counterModel.toString()}'); + appLogger.info('DECREMENT: ${event.counterModel.toString()}'); if (event.counterModel.value == 0) { return; diff --git a/lib/src/features/counter/presentation/edit/view/edit_counter.page.dart b/lib/src/features/counter/presentation/edit/view/edit_counter.page.dart index 361dadb..11e7d69 100644 --- a/lib/src/features/counter/presentation/edit/view/edit_counter.page.dart +++ b/lib/src/features/counter/presentation/edit/view/edit_counter.page.dart @@ -1,5 +1,4 @@ -import 'dart:developer'; - +import 'package:counter_workshop/src/core/logger/app_logger.dart'; import 'package:counter_workshop/src/core/widgets/custom_loading_indicator.widget.dart'; import 'package:counter_workshop/src/core/widgets/error_message.widget.dart'; import 'package:counter_workshop/src/features/counter/data/repositories/counter.repository.dart'; @@ -65,7 +64,7 @@ class CounterView extends StatelessWidget { listener: (context, state) { if (state is EditCounterData) { // Calling DashboardBloc (MasterPage) from EditCounterBloc (DetailPage) - log('EditBlocListener: ${state.counterModel.value}'); + appLogger.info('EditBlocListener: ${state.counterModel.value}'); final dashboardBloc = context.read(); dashboardBloc.add(FetchCounterList()); } diff --git a/pubspec.lock b/pubspec.lock index 9112bda..ee0b0a8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -162,6 +162,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.2.3" + dio: + dependency: transitive + description: + name: dio + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.6" equatable: dependency: "direct main" description: @@ -309,12 +316,19 @@ packages: source: hosted version: "2.0.0" logging: - dependency: transitive + dependency: "direct main" description: name: logging url: "https://pub.dartlang.org" source: hosted version: "1.0.2" + logging_appenders: + dependency: "direct main" + description: + name: logging_appenders + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" matcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a998cab..48e9e70 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,6 +19,8 @@ dependencies: http: ^0.13.5 flutter_bloc: ^8.1.1 go_router: ^4.4.1 + logging: ^1.0.2 + logging_appenders: ^1.0.0 dev_dependencies: flutter_test: diff --git a/test/golden/goldens/macos/counter_grid_phone.png b/test/golden/goldens/macos/counter_grid_phone.png new file mode 100644 index 0000000..7e7452c Binary files /dev/null and b/test/golden/goldens/macos/counter_grid_phone.png differ diff --git a/test/golden/goldens/macos/counter_grid_tablet.png b/test/golden/goldens/macos/counter_grid_tablet.png new file mode 100644 index 0000000..c7886e6 Binary files /dev/null and b/test/golden/goldens/macos/counter_grid_tablet.png differ