feat: Added linter

This commit is contained in:
Jacques 2023-11-29 11:54:25 +01:00
parent 36125f4174
commit 59e96159f0
27 changed files with 487 additions and 477 deletions

View file

@ -1,4 +0,0 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View file

@ -1,29 +1,9 @@
# This file configures the analyzer, which statically analyzes Dart code to include: package:flutter_iconica_analysis/analysis_options.yaml
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps, # Possible to overwrite the rules from the package
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
analyzer:
exclude:
linter: linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules: rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View file

@ -14,15 +14,13 @@ class MyApp extends StatelessWidget {
const MyApp({super.key}); const MyApp({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => MaterialApp(
return MaterialApp( title: 'Flutter Demo',
title: 'Flutter Demo', theme: ThemeData(
theme: ThemeData( primarySwatch: Colors.blue,
primarySwatch: Colors.blue, ),
), home: const MyHomePage(),
home: const MyHomePage(), );
);
}
} }
class MyHomePage extends StatefulWidget { class MyHomePage extends StatefulWidget {
@ -37,55 +35,51 @@ class _MyHomePageState extends State<MyHomePage> {
IntroductionService(SharedPreferencesIntroductionDataProvider()); IntroductionService(SharedPreferencesIntroductionDataProvider());
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => Scaffold(
return Scaffold( body: Introduction(
body: Introduction( options: IntroductionOptions(
options: IntroductionOptions( pages: [
pages: [ IntroductionPage(
IntroductionPage( title: const Text('First page'),
title: const Text('First page'), text: const Text('Wow a page'),
text: const Text('Wow a page'), graphic: const FlutterLogo(),
graphic: const FlutterLogo(), ),
IntroductionPage(
title: const Text('Second page'),
text: const Text('Another page'),
graphic: const FlutterLogo(),
),
IntroductionPage(
title: const Text('Third page'),
text: const Text('The final page of this app'),
graphic: const FlutterLogo(),
),
],
introductionTranslations: const IntroductionTranslations(
skipButton: 'Skip it!',
nextButton: 'Previous',
previousButton: 'Next',
finishButton: 'To the app!',
), ),
IntroductionPage( tapEnabled: true,
title: const Text('Second page'), displayMode: IntroductionDisplayMode.multiPageHorizontal,
text: const Text('Another page'), buttonMode: IntroductionScreenButtonMode.text,
graphic: const FlutterLogo(), indicatorMode: IndicatorMode.dash,
), skippable: true,
IntroductionPage( buttonBuilder: (context, onPressed, child, type) =>
title: const Text('Third page'), ElevatedButton(onPressed: onPressed, child: child),
text: const Text('The final page of this app'),
graphic: const FlutterLogo(),
),
],
introductionTranslations: const IntroductionTranslations(
skipButton: 'Skip it!',
nextButton: 'Previous',
previousButton: 'Next',
finishButton: 'To the app!',
), ),
tapEnabled: true, service: service,
displayMode: IntroductionDisplayMode.multiPageHorizontal, navigateTo: () async {
buttonMode: IntroductionScreenButtonMode.text, await Navigator.of(context).push(
indicatorMode: IndicatorMode.dash, MaterialPageRoute(
skippable: true, builder: (context) => const Home(),
buttonBuilder: (context, onPressed, child, type) => ),
ElevatedButton(onPressed: onPressed, child: child), );
},
child: const Home(),
), ),
service: service, );
navigateTo: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return const Home();
},
),
);
},
child: const Home(),
),
);
}
} }
class Home extends StatelessWidget { class Home extends StatelessWidget {
@ -94,7 +88,5 @@ class Home extends StatelessWidget {
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => Container();
return Container();
}
} }

View file

@ -26,6 +26,10 @@ dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^2.0.0 flutter_lints: ^2.0.0
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 6.0.0
flutter: flutter:
uses-material-design: true uses-material-design: true

View file

@ -2,6 +2,7 @@
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import 'package:example/main.dart';
// This is a basic Flutter widget test. // This is a basic Flutter widget test.
// //
// To perform an interaction with a widget in your test, use the WidgetTester // To perform an interaction with a widget in your test, use the WidgetTester
@ -12,8 +13,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:example/main.dart';
void main() { void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async { testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame. // Build our app and trigger a frame.

View file

@ -2,14 +2,12 @@
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
library flutter_introduction;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_introduction_service/flutter_introduction_service.dart'; import 'package:flutter_introduction_service/flutter_introduction_service.dart';
import 'package:flutter_introduction_widget/flutter_introduction_widget.dart'; import 'package:flutter_introduction_widget/flutter_introduction_widget.dart';
export 'package:flutter_introduction_widget/flutter_introduction_widget.dart';
export 'package:flutter_introduction_service/flutter_introduction_service.dart'; export 'package:flutter_introduction_service/flutter_introduction_service.dart';
export 'package:flutter_introduction_widget/flutter_introduction_widget.dart';
class Introduction extends StatefulWidget { class Introduction extends StatefulWidget {
const Introduction({ const Introduction({
@ -21,7 +19,7 @@ class Introduction extends StatefulWidget {
super.key, super.key,
}); });
final Function navigateTo; final VoidCallback navigateTo;
final IntroductionService? service; final IntroductionService? service;
final IntroductionOptions options; final IntroductionOptions options;
final ScrollPhysics? physics; final ScrollPhysics? physics;
@ -45,30 +43,29 @@ class _IntroductionState extends State<Introduction> {
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => FutureBuilder(
return FutureBuilder( // ignore: discarded_futures
future: _service.shouldShow(), future: _service.shouldShow(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.data == null || snapshot.data!) { if (snapshot.data == null || snapshot.data!) {
return IntroductionScreen( return IntroductionScreen(
options: widget.options, options: widget.options,
onComplete: () async { onComplete: () async {
_service.onComplete(); await _service.onComplete();
widget.navigateTo();
},
physics: widget.physics,
onSkip: () async {
await _service.onComplete();
widget.navigateTo();
},
);
} else {
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.navigateTo(); widget.navigateTo();
}, });
physics: widget.physics, return widget.child ?? const CircularProgressIndicator();
onSkip: () async { }
_service.onComplete(); },
widget.navigateTo(); );
},
);
} else {
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.navigateTo();
});
return widget.child ?? const CircularProgressIndicator();
}
},
);
}
} }

View file

@ -25,5 +25,9 @@ dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^2.0.0 flutter_lints: ^2.0.0
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 6.0.0
flutter: flutter:

View file

@ -1,4 +1,9 @@
include: package:flutter_lints/flutter.yaml include: package:flutter_iconica_analysis/analysis_options.yaml
# Additional information about this file can be found at # Possible to overwrite the rules from the package
# https://dart.dev/guides/language/analysis-options
analyzer:
exclude:
linter:
rules:

View file

@ -19,7 +19,7 @@ abstract class IntroductionInterface extends DataInterface {
_instance = instance; _instance = instance;
} }
Future<void> setCompleted([bool value = true]); Future<void> setCompleted({bool value = true});
Future<bool> shouldShow(); Future<bool> shouldShow();
} }

View file

@ -10,12 +10,10 @@ class LocalIntroductionDataProvider extends IntroductionInterface {
bool hasViewed = false; bool hasViewed = false;
@override @override
Future<void> setCompleted([bool value = true]) async { Future<void> setCompleted({bool value = true}) async {
hasViewed = value; hasViewed = value;
} }
@override @override
Future<bool> shouldShow() async { Future<bool> shouldShow() async => hasViewed;
return hasViewed;
}
} }

View file

@ -19,5 +19,9 @@ dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^2.0.0 flutter_lints: ^2.0.0
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 6.0.0
flutter: flutter:

View file

@ -1,4 +1,9 @@
include: package:flutter_lints/flutter.yaml include: package:flutter_iconica_analysis/analysis_options.yaml
# Additional information about this file can be found at # Possible to overwrite the rules from the package
# https://dart.dev/guides/language/analysis-options
analyzer:
exclude:
linter:
rules:

View file

@ -2,7 +2,6 @@
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
library flutter_introduction_service; export 'package:flutter_introduction_interface/flutter_introduction_interface.dart';
export './src/introduction_service.dart'; export './src/introduction_service.dart';
export 'package:flutter_introduction_interface/flutter_introduction_interface.dart';

View file

@ -10,15 +10,9 @@ class IntroductionService {
late final IntroductionInterface _dataProvider; late final IntroductionInterface _dataProvider;
Future<void> onSkip() { Future<void> onSkip() => _dataProvider.setCompleted(value: true);
return _dataProvider.setCompleted(true);
}
Future<void> onComplete() { Future<void> onComplete() => _dataProvider.setCompleted(value: true);
return _dataProvider.setCompleted(true);
}
Future<bool> shouldShow() { Future<bool> shouldShow() => _dataProvider.shouldShow();
return _dataProvider.shouldShow();
}
} }

View file

@ -20,5 +20,9 @@ dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^2.0.0 flutter_lints: ^2.0.0
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 6.0.0
flutter: flutter:

View file

@ -1,4 +1,9 @@
include: package:flutter_lints/flutter.yaml include: package:flutter_iconica_analysis/analysis_options.yaml
# Additional information about this file can be found at # Possible to overwrite the rules from the package
# https://dart.dev/guides/language/analysis-options
analyzer:
exclude:
linter:
rules:

View file

@ -2,8 +2,6 @@
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
library flutter_introduction_shared_preferences;
import 'package:flutter_introduction_interface/flutter_introduction_interface.dart'; import 'package:flutter_introduction_interface/flutter_introduction_interface.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@ -13,18 +11,18 @@ class SharedPreferencesIntroductionDataProvider extends IntroductionInterface {
SharedPreferences? _prefs; SharedPreferences? _prefs;
String key = '_completedIntroduction'; String key = '_completedIntroduction';
_writeKeyValue(String key, bool value) async { Future<void> _writeKeyValue(String key, bool value) async {
_prefs!.setBool(key, value); await _prefs!.setBool(key, value);
} }
_init() async { Future<void> _init() async {
_prefs ??= await SharedPreferences.getInstance(); _prefs ??= await SharedPreferences.getInstance();
} }
@override @override
Future<void> setCompleted([bool value = true]) async { Future<void> setCompleted({bool value = true}) async {
await _init(); await _init();
_writeKeyValue(key, value); await _writeKeyValue(key, value);
} }
@override @override

View file

@ -21,5 +21,9 @@ dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^2.0.0 flutter_lints: ^2.0.0
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 6.0.0
flutter: flutter:

View file

@ -1,15 +1,9 @@
include: package:flutter_lints/flutter.yaml include: package:flutter_iconica_analysis/analysis_options.yaml
analyzer:
errors:
todo: ignore
exclude: [lib/generated_plugin_registrant.dart]
linter:
# https://dart.dev/tools/linter-rules#lints
rules:
# pub rules
depend_on_referenced_packages: true
secure_pubspec_urls: false
sort_pub_dependencies: false
# Additional information about this file can be found at # Possible to overwrite the rules from the package
# https://dart.dev/guides/language/analysis-options
analyzer:
exclude:
linter:
rules:

View file

@ -34,12 +34,6 @@ enum IntroductionButtonType {
} }
class IntroductionPage { class IntroductionPage {
final Widget? title;
final Widget? text;
final Widget? graphic;
final BoxDecoration? decoration;
final IntroductionLayoutStyle? layoutStyle;
/// Creates an introduction page with data used in the introduction screen for /// Creates an introduction page with data used in the introduction screen for
/// each page. /// each page.
/// ///
@ -55,83 +49,14 @@ class IntroductionPage {
this.decoration, this.decoration,
this.layoutStyle, this.layoutStyle,
}); });
final Widget? title;
final Widget? text;
final Widget? graphic;
final BoxDecoration? decoration;
final IntroductionLayoutStyle? layoutStyle;
} }
class IntroductionOptions { class IntroductionOptions {
/// Determine when the introduction screens needs to be shown.
/// [IntroductionScreenMode.showNever] To disable introduction screens.
/// [IntroductionScreenMode.showAlways] To always show the introduction screens on startup.
/// [IntroductionScreenMode.showOnce] To only show the introduction screens once on startup.
final IntroductionScreenMode mode;
/// List of introduction pages to set the text, icons or images for the introduction screens.
final List<IntroductionPage> pages;
/// Determines whether the user can tap the screen to go to the next introduction screen.
final bool tapEnabled;
/// Determines what kind of buttons are used to navigate to the next introduction screen.
/// Introduction screens can always be navigated by swiping (or tapping if [tapEnabled] is enabled).
/// [IntroductionScreenButtonMode.text] Use text buttons (text can be set by setting the translation key or using the default appshell translations).
/// [IntroductionScreenButtonMode.icon] Use icon buttons (icons can be changed by providing a icon library)
/// [IntroductionScreenButtonMode.disabled] Disable buttons.
final IntroductionScreenButtonMode buttonMode;
/// Determines the position of the provided images or icons that are set using [pages].
/// Every introduction page provided with a image or icon will use the same layout setting.
/// [IntroductionLayoutStyle.imageCenter] Image/icon will be at the center of the introduction page.
/// [IntroductionLayoutStyle.imageTop] Image/icon will be at the top of the introduction page.
/// [IntroductionLayoutStyle.imageBottom] Image/icon will be at the bottom of the introduction page.
final IntroductionLayoutStyle layoutStyle;
/// Determines the style of the page indicator shown at the bottom on the introduction pages.
/// [IndicatorMode.dot] Shows a dot for each page.
/// [IndicatorMode.dash] Shows a dash for each page.
/// [IndicatorMode.custom] calls indicatorBuilder for the indicator
final IndicatorMode indicatorMode;
/// Builder that is called when [indicatorMode] is set to [IndicatorMode.custom]
final Widget Function(
BuildContext,
PageController,
int,
int,
)? indicatorBuilder;
/// Determines whether the user can skip the introduction pages using a button
/// in the header
final bool skippable;
/// Determines whether the introduction screens should be shown in a single
final TextAlign textAlign;
/// [IntroductionDisplayMode.multiPageHorizontal] Configured introduction pages will be shown on seperate screens and can be navigated using using buttons (if enabled) or swiping.
/// !Unimplemented! [IntroductionDisplayMode.singleScrollablePageVertical] All configured introduction pages will be shown on a single scrollable page.
///
final IntroductionDisplayMode displayMode;
/// When [IntroductionDisplayMode.multiPageHorizontal] is selected multiple controlMode can be selected.
/// [IntroductionControlMode.previousNextButton] shows two buttons at the bottom of the screen to return or proceed. The skip button is placed at the top left of the screen.
/// [IntroductionControlMode.singleButton] contains one button at the bottom of the screen to proceed. Underneath is clickable text to skip if the current page is the first page. If the current page is any different it return to the previous screen.
///
final IntroductionControlMode controlMode;
/// A builder that can be used to replace the default text buttons when
/// [IntroductionScreenButtonMode.text] is provided to [buttonMode]
final Widget Function(
BuildContext, VoidCallback, Widget, IntroductionButtonType)?
buttonBuilder;
/// The translations for all buttons on the introductionpages
///
/// See [IntroductionTranslations] for more information
/// The following buttons have a translation:
/// - Skip
/// - Next
/// - Previous
/// - Finish
final IntroductionTranslations introductionTranslations;
const IntroductionOptions({ const IntroductionOptions({
this.introductionTranslations = const IntroductionTranslations(), this.introductionTranslations = const IntroductionTranslations(),
this.indicatorMode = IndicatorMode.dash, this.indicatorMode = IndicatorMode.dash,
@ -153,6 +78,120 @@ class IntroductionOptions {
'make sure to define indicatorBuilder', 'make sure to define indicatorBuilder',
); );
/// Determine when the introduction screens needs to be shown.
///
/// [IntroductionScreenMode.showNever] To disable introduction screens.
///
/// [IntroductionScreenMode.showAlways] To always show the introduction
/// screens on startup.
///
/// [IntroductionScreenMode.showOnce] To only show the introduction screens
/// once on startup.
final IntroductionScreenMode mode;
/// List of introduction pages to set the text, icons or images for the
/// introduction screens.
final List<IntroductionPage> pages;
/// Determines whether the user can tap the screen to go to the next
/// introduction screen.
final bool tapEnabled;
/// Determines what kind of buttons are used to navigate to the next
/// introduction screen.
/// Introduction screens can always be navigated by swiping (or tapping if
/// [tapEnabled] is enabled).
///
/// [IntroductionScreenButtonMode.text] Use text buttons (text can be set by
/// setting the translation key or using the default appshell translations).
///
/// [IntroductionScreenButtonMode.icon] Use icon buttons (icons can be
/// changed by providing a icon library)
///
/// [IntroductionScreenButtonMode.disabled] Disable buttons.
final IntroductionScreenButtonMode buttonMode;
/// Determines the position of the provided images or icons that are set
/// using [pages].
/// Every introduction page provided with a image or icon will use the same
/// layout setting.
///
/// [IntroductionLayoutStyle.imageCenter] Image/icon will be at the center of the introduction page.
///
/// [IntroductionLayoutStyle.imageTop] Image/icon will be at the top of the introduction page.
///
/// [IntroductionLayoutStyle.imageBottom] Image/icon will be at the bottom of the introduction page.
final IntroductionLayoutStyle layoutStyle;
/// Determines the style of the page indicator shown at the bottom on the
/// introduction pages.
///
/// [IndicatorMode.dot] Shows a dot for each page.
///
/// [IndicatorMode.dash] Shows a dash for each page.
///
/// [IndicatorMode.custom] calls indicatorBuilder for the indicator
final IndicatorMode indicatorMode;
/// Builder that is called when [indicatorMode] is set
/// to [IndicatorMode.custom]
final Widget Function(
BuildContext,
PageController,
int,
int,
)? indicatorBuilder;
/// Determines whether the user can skip the introduction pages using a button
/// in the header
final bool skippable;
/// Determines whether the introduction screens should be shown in a single
final TextAlign textAlign;
/// [IntroductionDisplayMode.multiPageHorizontal] Configured introduction
/// pages will be shown on seperate screens and can be navigated using using
/// buttons (if enabled) or swiping.
///
/// !Unimplemented! [IntroductionDisplayMode.singleScrollablePageVertical]
/// All configured introduction pages will be shown on a single scrollable
/// page.
///
final IntroductionDisplayMode displayMode;
/// When [IntroductionDisplayMode.multiPageHorizontal] is selected multiple
/// controlMode can be selected.
///
/// [IntroductionControlMode.previousNextButton] shows two buttons at the
/// bottom of the screen to return or proceed. The skip button is placed at
/// the top left of the screen.
///
/// [IntroductionControlMode.singleButton] contains one button at the bottom
/// of the screen to proceed. Underneath is clickable text to skip if the
/// current page is the first page. If the current page is any different it
/// return to the previous screen.
///
final IntroductionControlMode controlMode;
/// A builder that can be used to replace the default text buttons when
/// [IntroductionScreenButtonMode.text] is provided to [buttonMode]
final Widget Function(
BuildContext,
VoidCallback,
Widget,
IntroductionButtonType,
)? buttonBuilder;
/// The translations for all buttons on the introductionpages
///
/// See [IntroductionTranslations] for more information
/// The following buttons have a translation:
/// - Skip
/// - Next
/// - Previous
/// - Finish
final IntroductionTranslations introductionTranslations;
IntroductionOptions copyWith({ IntroductionOptions copyWith({
IntroductionScreenMode? mode, IntroductionScreenMode? mode,
List<IntroductionPage>? pages, List<IntroductionPage>? pages,
@ -173,37 +212,35 @@ class IntroductionOptions {
Widget Function(BuildContext, VoidCallback, Widget, IntroductionButtonType)? Widget Function(BuildContext, VoidCallback, Widget, IntroductionButtonType)?
buttonBuilder, buttonBuilder,
IntroductionTranslations? introductionTranslations, IntroductionTranslations? introductionTranslations,
}) { }) =>
return IntroductionOptions( IntroductionOptions(
mode: mode ?? this.mode, mode: mode ?? this.mode,
pages: pages ?? this.pages, pages: pages ?? this.pages,
tapEnabled: tapEnabled ?? this.tapEnabled, tapEnabled: tapEnabled ?? this.tapEnabled,
buttonMode: buttonMode ?? this.buttonMode, buttonMode: buttonMode ?? this.buttonMode,
layoutStyle: layoutStyle ?? this.layoutStyle, layoutStyle: layoutStyle ?? this.layoutStyle,
indicatorMode: indicatorMode ?? this.indicatorMode, indicatorMode: indicatorMode ?? this.indicatorMode,
indicatorBuilder: indicatorBuilder ?? this.indicatorBuilder, indicatorBuilder: indicatorBuilder ?? this.indicatorBuilder,
skippable: skippable ?? this.skippable, skippable: skippable ?? this.skippable,
textAlign: textAlign ?? this.textAlign, textAlign: textAlign ?? this.textAlign,
displayMode: displayMode ?? this.displayMode, displayMode: displayMode ?? this.displayMode,
controlMode: controlMode ?? this.controlMode, controlMode: controlMode ?? this.controlMode,
buttonBuilder: buttonBuilder ?? this.buttonBuilder, buttonBuilder: buttonBuilder ?? this.buttonBuilder,
introductionTranslations: introductionTranslations:
introductionTranslations ?? this.introductionTranslations, introductionTranslations ?? this.introductionTranslations,
); );
}
} }
/// ///
class IntroductionTranslations { class IntroductionTranslations {
final String skipButton;
final String nextButton;
final String previousButton;
final String finishButton;
const IntroductionTranslations({ const IntroductionTranslations({
this.skipButton = 'skip', this.skipButton = 'skip',
this.nextButton = 'next', this.nextButton = 'next',
this.previousButton = 'previous', this.previousButton = 'previous',
this.finishButton = 'finish', this.finishButton = 'finish',
}); });
final String skipButton;
final String nextButton;
final String previousButton;
final String finishButton;
} }

View file

@ -3,23 +3,22 @@
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_introduction_widget/src/config/introduction.dart';
import 'package:flutter_introduction_widget/src/types/page_introduction.dart'; import 'package:flutter_introduction_widget/src/types/page_introduction.dart';
import 'package:flutter_introduction_widget/src/types/single_introduction.dart'; import 'package:flutter_introduction_widget/src/types/single_introduction.dart';
import 'config/introduction.dart';
const kAnimationDuration = Duration(milliseconds: 300); const kAnimationDuration = Duration(milliseconds: 300);
class IntroductionScreen extends StatelessWidget { class IntroductionScreen extends StatelessWidget {
const IntroductionScreen({ const IntroductionScreen({
Key? key,
required this.options, required this.options,
required this.onComplete, required this.onComplete,
super.key,
this.physics, this.physics,
this.onNext, this.onNext,
this.onPrevious, this.onPrevious,
this.onSkip, this.onSkip,
}) : super(key: key); });
/// The options used to build the introduction screen /// The options used to build the introduction screen
final IntroductionOptions options; final IntroductionOptions options;
@ -40,28 +39,26 @@ class IntroductionScreen extends StatelessWidget {
final void Function(IntroductionPage)? onPrevious; final void Function(IntroductionPage)? onPrevious;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => Scaffold(
return Scaffold( backgroundColor: Colors.transparent,
backgroundColor: Colors.transparent, body: Builder(
body: Builder( builder: (context) {
builder: (context) { switch (options.displayMode) {
switch (options.displayMode) { case IntroductionDisplayMode.multiPageHorizontal:
case IntroductionDisplayMode.multiPageHorizontal: return MultiPageIntroductionScreen(
return MultiPageIntroductionScreen( onComplete: onComplete,
onComplete: onComplete, physics: physics,
physics: physics, onSkip: onSkip,
onSkip: onSkip, onPrevious: onPrevious,
onPrevious: onPrevious, onNext: onNext,
onNext: onNext, options: options,
options: options, );
); case IntroductionDisplayMode.singleScrollablePageVertical:
case IntroductionDisplayMode.singleScrollablePageVertical: return SingleIntroductionPage(
return SingleIntroductionPage( options: options,
options: options, );
); }
} },
}, ),
), );
);
}
} }

View file

@ -2,13 +2,14 @@
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart'; import 'dart:async';
import 'package:flutter_introduction_widget/src/introduction.dart';
import '../config/introduction.dart'; import 'package:flutter/material.dart';
import '../widgets/background.dart'; import 'package:flutter_introduction_widget/src/config/introduction.dart';
import '../widgets/indicator.dart'; import 'package:flutter_introduction_widget/src/introduction.dart';
import '../widgets/page_content.dart'; import 'package:flutter_introduction_widget/src/widgets/background.dart';
import 'package:flutter_introduction_widget/src/widgets/indicator.dart';
import 'package:flutter_introduction_widget/src/widgets/page_content.dart';
class MultiPageIntroductionScreen extends StatefulWidget { class MultiPageIntroductionScreen extends StatefulWidget {
const MultiPageIntroductionScreen({ const MultiPageIntroductionScreen({
@ -18,8 +19,8 @@ class MultiPageIntroductionScreen extends StatefulWidget {
this.onNext, this.onNext,
this.onPrevious, this.onPrevious,
this.onSkip, this.onSkip,
Key? key, super.key,
}) : super(key: key); });
final VoidCallback onComplete; final VoidCallback onComplete;
final VoidCallback? onSkip; final VoidCallback? onSkip;
@ -71,7 +72,7 @@ class _MultiPageIntroductionScreenState
} }
// add bouncing scroll physics support // add bouncing scroll physics support
if (notification is ScrollUpdateNotification) { if (notification is ScrollUpdateNotification) {
final offset = notification.metrics.pixels; var offset = notification.metrics.pixels;
if (offset > notification.metrics.maxScrollExtent + 8) { if (offset > notification.metrics.maxScrollExtent + 8) {
widget.onComplete.call(); widget.onComplete.call();
} }
@ -83,17 +84,15 @@ class _MultiPageIntroductionScreenState
physics: widget.physics, physics: widget.physics,
children: List.generate( children: List.generate(
pages.length, pages.length,
(index) { (index) => ExplainerPage(
return ExplainerPage( onTap: () {
onTap: () { widget.onNext?.call(pages[_currentPage.value]);
widget.onNext?.call(pages[_currentPage.value]); },
}, controller: _controller,
controller: _controller, page: pages[index],
page: pages[index], options: widget.options,
options: widget.options, index: index,
index: index, ),
);
},
), ),
), ),
), ),
@ -109,25 +108,22 @@ class _MultiPageIntroductionScreenState
height: 64, height: 64,
child: AnimatedBuilder( child: AnimatedBuilder(
animation: _controller, animation: _controller,
builder: (context, _) { builder: (context, _) => Row(
return Row( mainAxisAlignment: MainAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end, children: [
children: [ if (widget.options.skippable && !_isLast(pages)) ...[
if (widget.options.skippable && TextButton(
!_isLast(pages)) ...[ onPressed: widget.onComplete,
TextButton( child: Text(translations.skipButton),
onPressed: widget.onComplete, ),
child: Text(translations.skipButton),
),
],
], ],
); ],
}, ),
), ),
), ),
), ),
] else ...[ ] else ...[
const SizedBox.shrink() const SizedBox.shrink(),
], ],
Align( Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
@ -190,23 +186,22 @@ class _MultiPageIntroductionScreenState
), ),
), ),
AnimatedBuilder( AnimatedBuilder(
animation: _controller, animation: _controller,
builder: (context, _) { builder: (context, _) => IntroductionIconButtons(
return IntroductionIconButtons( controller: _controller,
controller: _controller, next: _isNext(pages),
next: _isNext(pages), previous: _isPrevious,
previous: _isPrevious, last: _isLast(pages),
last: _isLast(pages), options: widget.options,
options: widget.options, onFinish: widget.onComplete,
onFinish: widget.onComplete, onNext: () {
onNext: () { widget.onNext?.call(pages[_currentPage.value]);
widget.onNext?.call(pages[_currentPage.value]); },
}, onPrevious: () {
onPrevious: () { widget.onNext?.call(pages[_currentPage.value]);
widget.onNext?.call(pages[_currentPage.value]); },
}, ),
); ),
}),
], ],
); );
} }
@ -227,8 +222,8 @@ class ExplainerPage extends StatelessWidget {
required this.index, required this.index,
required this.controller, required this.controller,
this.onTap, this.onTap,
Key? key, super.key,
}) : super(key: key); });
final IntroductionPage page; final IntroductionPage page;
final IntroductionOptions options; final IntroductionOptions options;
@ -297,8 +292,8 @@ class IntroductionTwoButtons extends StatelessWidget {
required this.onFinish, required this.onFinish,
required this.onNext, required this.onNext,
required this.onPrevious, required this.onPrevious,
Key? key, super.key,
}) : super(key: key); });
final IntroductionOptions options; final IntroductionOptions options;
final PageController controller; final PageController controller;
@ -310,8 +305,8 @@ class IntroductionTwoButtons extends StatelessWidget {
final bool next; final bool next;
final bool last; final bool last;
void _previous() { Future<void> _previous() async {
controller.previousPage( await controller.previousPage(
duration: kAnimationDuration, duration: kAnimationDuration,
curve: Curves.easeInOut, curve: Curves.easeInOut,
); );
@ -402,8 +397,8 @@ class IntroductionTwoButtons extends StatelessWidget {
); );
} }
_next() { Future<void> _next() async {
controller.nextPage( await controller.nextPage(
duration: kAnimationDuration, duration: kAnimationDuration,
curve: Curves.easeInOut, curve: Curves.easeInOut,
); );
@ -421,8 +416,8 @@ class IntroductionOneButton extends StatelessWidget {
required this.onFinish, required this.onFinish,
required this.onNext, required this.onNext,
required this.onPrevious, required this.onPrevious,
Key? key, super.key,
}) : super(key: key); });
final IntroductionOptions options; final IntroductionOptions options;
final PageController controller; final PageController controller;
@ -434,8 +429,8 @@ class IntroductionOneButton extends StatelessWidget {
final bool next; final bool next;
final bool last; final bool last;
void _previous() { Future<void> _previous() async {
controller.previousPage( await controller.previousPage(
duration: kAnimationDuration, duration: kAnimationDuration,
curve: Curves.easeInOut, curve: Curves.easeInOut,
); );
@ -508,8 +503,8 @@ class IntroductionOneButton extends StatelessWidget {
); );
} }
_next() { Future<void> _next() async {
controller.nextPage( await controller.nextPage(
duration: kAnimationDuration, duration: kAnimationDuration,
curve: Curves.easeInOut, curve: Curves.easeInOut,
); );
@ -527,8 +522,8 @@ class IntroductionIconButtons extends StatelessWidget {
required this.onFinish, required this.onFinish,
required this.onNext, required this.onNext,
required this.onPrevious, required this.onPrevious,
Key? key, super.key,
}) : super(key: key); });
final IntroductionOptions options; final IntroductionOptions options;
final PageController controller; final PageController controller;
@ -540,8 +535,8 @@ class IntroductionIconButtons extends StatelessWidget {
final bool next; final bool next;
final bool last; final bool last;
void _previous() { Future<void> _previous() async {
controller.previousPage( await controller.previousPage(
duration: kAnimationDuration, duration: kAnimationDuration,
curve: Curves.easeInOut, curve: Curves.easeInOut,
); );
@ -549,39 +544,37 @@ class IntroductionIconButtons extends StatelessWidget {
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => Center(
return Center( child: Row(
child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
children: [ if (options.buttonMode == IntroductionScreenButtonMode.icon) ...[
if (options.buttonMode == IntroductionScreenButtonMode.icon) ...[ if (previous) ...[
if (previous) ...[ IconButton(
iconSize: 70,
onPressed: _previous,
icon: const Icon(Icons.chevron_left),
),
] else
const SizedBox.shrink(),
IconButton( IconButton(
iconSize: 70, iconSize: 70,
onPressed: _previous, onPressed: () {
icon: const Icon(Icons.chevron_left), if (next) {
unawaited(_next());
} else {
onFinish?.call();
}
},
icon: const Icon(Icons.chevron_right),
), ),
] else ],
const SizedBox.shrink(),
IconButton(
iconSize: 70,
onPressed: () {
if (next) {
_next();
} else {
onFinish?.call();
}
},
icon: const Icon(Icons.chevron_right),
),
], ],
], ),
), );
);
}
_next() { Future<void> _next() async {
controller.nextPage( await controller.nextPage(
duration: kAnimationDuration, duration: kAnimationDuration,
curve: Curves.easeInOut, curve: Curves.easeInOut,
); );

View file

@ -4,13 +4,13 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import '../config/introduction.dart'; import 'package:flutter_introduction_widget/src/config/introduction.dart';
class SingleIntroductionPage extends StatelessWidget { class SingleIntroductionPage extends StatelessWidget {
const SingleIntroductionPage({ const SingleIntroductionPage({
Key? key,
required this.options, required this.options,
}) : super(key: key); super.key,
});
final IntroductionOptions options; final IntroductionOptions options;

View file

@ -6,10 +6,10 @@ import 'package:flutter/material.dart';
class Background extends StatelessWidget { class Background extends StatelessWidget {
const Background({ const Background({
this.background,
required this.child, required this.child,
Key? key, this.background,
}) : super(key: key); super.key,
});
final BoxDecoration? background; final BoxDecoration? background;
final Widget child; final Widget child;

View file

@ -2,13 +2,13 @@
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_introduction_widget/src/config/introduction.dart';
import 'package:flutter_introduction_widget/src/introduction.dart'; import 'package:flutter_introduction_widget/src/introduction.dart';
import '../config/introduction.dart';
class Indicator extends StatelessWidget { class Indicator extends StatelessWidget {
const Indicator({ const Indicator({
required this.mode, required this.mode,
@ -16,12 +16,12 @@ class Indicator extends StatelessWidget {
required this.count, required this.count,
required this.index, required this.index,
required this.indicatorBuilder, required this.indicatorBuilder,
Key? key, super.key,
}) : assert( }) : assert(
!(mode == IndicatorMode.custom && indicatorBuilder == null), !(mode == IndicatorMode.custom && indicatorBuilder == null),
"When a custom indicator is used the indicatorBuilder must be provided", 'When a custom indicator is used the indicatorBuilder '
), 'must be provided',
super(key: key); );
final IndicatorMode mode; final IndicatorMode mode;
final PageController controller; final PageController controller;
@ -36,7 +36,7 @@ class Indicator extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); var theme = Theme.of(context);
switch (mode) { switch (mode) {
case IndicatorMode.custom: case IndicatorMode.custom:
return indicatorBuilder!.call(context, controller, index, count); return indicatorBuilder!.call(context, controller, index, count);
@ -47,10 +47,12 @@ class Indicator extends StatelessWidget {
dotcolor: theme.colorScheme.secondary, dotcolor: theme.colorScheme.secondary,
itemCount: count, itemCount: count,
onPageSelected: (int page) { onPageSelected: (int page) {
controller.animateToPage( unawaited(
page, controller.animateToPage(
duration: kAnimationDuration, page,
curve: Curves.easeInOut, duration: kAnimationDuration,
curve: Curves.easeInOut,
),
); );
}, },
); );
@ -60,10 +62,12 @@ class Indicator extends StatelessWidget {
selectedColor: theme.colorScheme.primary, selectedColor: theme.colorScheme.primary,
itemCount: count, itemCount: count,
onPageSelected: (int page) { onPageSelected: (int page) {
controller.animateToPage( unawaited(
page, controller.animateToPage(
duration: kAnimationDuration, page,
curve: Curves.easeInOut, duration: kAnimationDuration,
curve: Curves.easeInOut,
),
); );
}, },
); );
@ -72,76 +76,73 @@ class Indicator extends StatelessWidget {
} }
class DashIndicator extends AnimatedWidget { class DashIndicator extends AnimatedWidget {
const DashIndicator({
required this.controller,
required this.selectedColor,
required this.itemCount,
required this.onPageSelected,
this.color = Colors.white,
super.key,
}) : super(listenable: controller);
final PageController controller; final PageController controller;
final Color color; final Color color;
final Color selectedColor; final Color selectedColor;
final int itemCount; final int itemCount;
final Function(int) onPageSelected; final Function(int) onPageSelected;
const DashIndicator(
{required this.controller,
this.color = Colors.white,
required this.selectedColor,
required this.itemCount,
required this.onPageSelected,
Key? key})
: super(listenable: controller, key: key);
int _getPage() { int _getPage() {
try { try {
return controller.page?.round() ?? 0; return controller.page?.round() ?? 0;
} catch (_) { } on Exception catch (_) {
return 0; return 0;
} }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
int index = _getPage(); var index = _getPage();
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
for (int i = 0; i < itemCount; i++) ...[ for (int i = 0; i < itemCount; i++) ...[
buildDash(i, index == i), buildDash(i, selected: index == i),
], ],
], ],
); );
} }
Widget buildDash(int index, bool selected) { Widget buildDash(int index, {required bool selected}) => SizedBox(
return SizedBox( width: 20,
width: 20, child: Center(
child: Center( child: Material(
child: Material( color: selected ? color : color.withAlpha(125),
color: selected ? color : color.withAlpha(125), type: MaterialType.card,
type: MaterialType.card, child: Container(
child: Container( decoration: BoxDecoration(
decoration: BoxDecoration( borderRadius: BorderRadius.circular(2.5),
borderRadius: BorderRadius.circular(2.5), ),
), width: 16,
width: 16, height: 5,
height: 5, child: InkWell(
child: InkWell( onTap: () => onPageSelected.call(index),
onTap: () => onPageSelected.call(index), ),
), ),
), ),
), ),
), );
);
}
} }
/// An indicator showing the currently selected page of a PageController /// An indicator showing the currently selected page of a PageController
class DotsIndicator extends AnimatedWidget { class DotsIndicator extends AnimatedWidget {
const DotsIndicator({ const DotsIndicator({
this.color = Colors.white,
required this.controller, required this.controller,
this.color = Colors.white,
this.dotcolor = Colors.green, this.dotcolor = Colors.green,
this.itemCount, this.itemCount,
this.onPageSelected, this.onPageSelected,
Key? key, super.key,
}) : super( }) : super(
listenable: controller, listenable: controller,
key: key,
); );
/// The PageController that this DotsIndicator is representing. /// The PageController that this DotsIndicator is representing.
@ -169,14 +170,14 @@ class DotsIndicator extends AnimatedWidget {
static const double _kDotSpacing = 12.0; static const double _kDotSpacing = 12.0;
Widget _buildDot(int index) { Widget _buildDot(int index) {
double selectedness = Curves.easeOut.transform( var selectedness = Curves.easeOut.transform(
max( max(
0.0, 0.0,
1.0 - 1.0 -
((controller.page ?? controller.initialPage).round() - index).abs(), ((controller.page ?? controller.initialPage).round() - index).abs(),
), ),
); );
double zoom = 1.0 + (_kMaxZoom - 1.0) * selectedness; var zoom = 1.0 + (_kMaxZoom - 1.0) * selectedness;
return SizedBox( return SizedBox(
width: _kDotSpacing, width: _kDotSpacing,
@ -203,10 +204,8 @@ class DotsIndicator extends AnimatedWidget {
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => Row(
return Row( mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, children: List<Widget>.generate(itemCount!, _buildDot),
children: List<Widget>.generate(itemCount!, _buildDot), );
);
}
} }

View file

@ -4,7 +4,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../config/introduction.dart'; import 'package:flutter_introduction_widget/src/config/introduction.dart';
class IntroductionPageContent extends StatelessWidget { class IntroductionPageContent extends StatelessWidget {
const IntroductionPageContent({ const IntroductionPageContent({
@ -13,8 +13,8 @@ class IntroductionPageContent extends StatelessWidget {
required this.graphic, required this.graphic,
required this.layoutStyle, required this.layoutStyle,
required this.onTap, required this.onTap,
Key? key, super.key,
}) : super(key: key); });
final Widget? title; final Widget? title;
final Widget? text; final Widget? text;
@ -23,24 +23,22 @@ class IntroductionPageContent extends StatelessWidget {
final VoidCallback onTap; final VoidCallback onTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => GestureDetector(
return GestureDetector( onTap: onTap,
onTap: onTap, child: Column(
child: Column( children: [
children: [ if (graphic != null &&
if (graphic != null && layoutStyle == IntroductionLayoutStyle.imageTop)
layoutStyle == IntroductionLayoutStyle.imageTop) graphic!,
graphic!, if (title != null) title!,
if (title != null) title!, if (graphic != null &&
if (graphic != null && layoutStyle == IntroductionLayoutStyle.imageCenter)
layoutStyle == IntroductionLayoutStyle.imageCenter) graphic!,
graphic!, if (text != null) text!,
if (text != null) text!, if (graphic != null &&
if (graphic != null && layoutStyle == IntroductionLayoutStyle.imageBottom)
layoutStyle == IntroductionLayoutStyle.imageBottom) graphic!,
graphic!, ],
], ),
), );
);
}
} }

View file

@ -15,5 +15,9 @@ dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^2.0.0 flutter_lints: ^2.0.0
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 6.0.0
flutter: flutter: