fix: feedback userstory

This commit is contained in:
mike doornenbal 2024-05-21 10:34:23 +02:00
parent 55727fa76d
commit 6808ee972d
16 changed files with 265 additions and 253 deletions

View file

@ -1,3 +1,8 @@
## 3.1.0
* Introduction now uses `IntroductionScreenMode` to determine how often the introductions should be shown
* Added `dotSize` and `dotSpacing` to `IntroductionOptions` to allow for customization of the dots for the introduction
## 3.0.0 ## 3.0.0
* Update default styling * Update default styling

View file

@ -56,7 +56,9 @@ class _IntroductionState extends State<Introduction> {
// ignore: discarded_futures // 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! ||
widget.options.mode == IntroductionScreenMode.showAlways) {
return IntroductionScreen( return IntroductionScreen(
options: widget.options, options: widget.options,
onComplete: () async { onComplete: () async {

View file

@ -1,6 +1,6 @@
name: flutter_introduction name: flutter_introduction
description: Combined Package of Flutter Introduction Widget and Flutter Introduction Service description: Combined Package of Flutter Introduction Widget and Flutter Introduction Service
version: 3.0.0 version: 3.1.0
publish_to: none publish_to: none
environment: environment:
@ -13,12 +13,12 @@ dependencies:
flutter_introduction_widget: flutter_introduction_widget:
git: git:
url: https://github.com/Iconica-Development/flutter_introduction url: https://github.com/Iconica-Development/flutter_introduction
ref: 3.0.0 ref: 3.1.0
path: packages/flutter_introduction_widget path: packages/flutter_introduction_widget
flutter_introduction_service: flutter_introduction_service:
git: git:
url: https://github.com/Iconica-Development/flutter_introduction url: https://github.com/Iconica-Development/flutter_introduction
ref: 3.0.0 ref: 3.1.0
path: packages/flutter_introduction_service path: packages/flutter_introduction_service
dev_dependencies: dev_dependencies:

View file

@ -1,6 +1,6 @@
name: flutter_introduction_firebase name: flutter_introduction_firebase
description: Flutter Introduction Page that uses firebase for the pages and some settings description: Flutter Introduction Page that uses firebase for the pages and some settings
version: 3.0.0 version: 3.1.0
publish_to: none publish_to: none
environment: environment:
@ -15,12 +15,12 @@ dependencies:
flutter_introduction_widget: flutter_introduction_widget:
git: git:
url: https://github.com/Iconica-Development/flutter_introduction url: https://github.com/Iconica-Development/flutter_introduction
ref: 3.0.0 ref: 3.1.0
path: packages/flutter_introduction_widget path: packages/flutter_introduction_widget
flutter_introduction_service: flutter_introduction_service:
git: git:
url: https://github.com/Iconica-Development/flutter_introduction url: https://github.com/Iconica-Development/flutter_introduction
ref: 3.0.0 ref: 3.1.0
path: packages/flutter_introduction_service path: packages/flutter_introduction_service
dev_dependencies: dev_dependencies:

View file

@ -1,6 +1,6 @@
name: flutter_introduction_interface name: flutter_introduction_interface
description: A new Flutter package project. description: A new Flutter package project.
version: 3.0.0 version: 3.1.0
publish_to: none publish_to: none
environment: environment:

View file

@ -1,6 +1,6 @@
name: flutter_introduction_service name: flutter_introduction_service
description: A new Flutter package project. description: A new Flutter package project.
version: 3.0.0 version: 3.1.0
publish_to: none publish_to: none
environment: environment:
@ -13,7 +13,7 @@ dependencies:
flutter_introduction_interface: flutter_introduction_interface:
git: git:
url: https://github.com/Iconica-Development/flutter_introduction url: https://github.com/Iconica-Development/flutter_introduction
ref: 3.0.0 ref: 3.1.0
path: packages/flutter_introduction_interface path: packages/flutter_introduction_interface
dev_dependencies: dev_dependencies:

View file

@ -1,6 +1,6 @@
name: flutter_introduction_shared_preferences name: flutter_introduction_shared_preferences
description: A new Flutter package project. description: A new Flutter package project.
version: 3.0.0 version: 3.1.0
publish_to: none publish_to: none
environment: environment:
@ -13,7 +13,7 @@ dependencies:
flutter_introduction_interface: flutter_introduction_interface:
git: git:
url: https://github.com/Iconica-Development/flutter_introduction url: https://github.com/Iconica-Development/flutter_introduction
ref: 3.0.0 ref: 3.1.0
path: packages/flutter_introduction_interface path: packages/flutter_introduction_interface
shared_preferences: any shared_preferences: any

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View file

@ -1,33 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_introduction_widget/flutter_introduction_widget.dart'; import 'package:flutter_introduction_widget/flutter_introduction_widget.dart';
const List<IntroductionPage> defaultIntroductionPages = [ const List<IntroductionPage> defaultIntroductionPages = [
IntroductionPage( IntroductionPage(
decoration: BoxDecoration(
color: Color(0xffFAF9F6),
),
title: Column( title: Column(
children: [ children: [
SizedBox(height: 100), SizedBox(height: 50),
Text( Text(
'welcome to iconinstagram', 'welcome to iconinstagram',
style: TextStyle(
color: Color(0xff71C6D1),
fontSize: 24,
fontWeight: FontWeight.w700,
),
), ),
SizedBox(height: 6), SizedBox(height: 6),
Text(
'Welcome to the world of Instagram, where creativity'
' knows no bounds and connections are made'
' through captivating visuals.',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
),
textAlign: TextAlign.center,
),
], ],
), ),
graphic: Image( graphic: Image(
@ -36,72 +19,46 @@ const List<IntroductionPage> defaultIntroductionPages = [
package: 'flutter_introduction_widget', package: 'flutter_introduction_widget',
), ),
), ),
text: Text(''), text: Text(
'Welcome to the world of Instagram, where creativity'
' knows no bounds and connections are made'
' through captivating visuals.',
textAlign: TextAlign.center,
),
), ),
IntroductionPage( IntroductionPage(
decoration: BoxDecoration(
color: Color(0xffFAF9F6),
),
title: Column( title: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
SizedBox(height: 100), SizedBox(height: 50),
Text( Text(
'discover iconinstagram', 'discover iconinstagram',
style: TextStyle(
color: Color(0xff71C6D1),
fontSize: 24,
fontWeight: FontWeight.w700,
),
), ),
SizedBox(height: 6), SizedBox(height: 6),
Text(
'Dive into the vibrant world of'
' Instagram and discover endless possibilities.'
' From stunning photography to engaging videos,'
' Instagram offers a diverse range of content to explore and enjoy.',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
),
textAlign: TextAlign.center,
),
], ],
), ),
text: Text(
'Dive into the vibrant world of'
' Instagram and discover endless possibilities.'
' From stunning photography to engaging videos,'
' Instagram offers a diverse range of content to explore and enjoy.',
textAlign: TextAlign.center,
),
graphic: Image( graphic: Image(
image: AssetImage( image: AssetImage(
'assets/second.png', 'assets/second.png',
package: 'flutter_introduction_widget', package: 'flutter_introduction_widget',
), ),
), ),
text: Text(''),
), ),
IntroductionPage( IntroductionPage(
decoration: BoxDecoration(
color: Color(0xffFAF9F6),
),
title: Column( title: Column(
children: [ children: [
SizedBox(height: 100), SizedBox(height: 50),
Text( Text(
'elevate your experience', 'elevate your experience',
style: TextStyle(
color: Color(0xff71C6D1),
fontSize: 24,
fontWeight: FontWeight.w700,
),
), ),
SizedBox(height: 6), SizedBox(height: 6),
Text(
'Whether promoting your business, or connecting'
' with friends and family, Instagram provides the'
' tools and platform to make your voice heard.',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
),
textAlign: TextAlign.center,
),
], ],
), ),
graphic: Image( graphic: Image(
@ -110,6 +67,11 @@ const List<IntroductionPage> defaultIntroductionPages = [
package: 'flutter_introduction_widget', package: 'flutter_introduction_widget',
), ),
), ),
text: Text(''), text: Text(
'Whether promoting your business, or connecting'
' with friends and family, Instagram provides the'
' tools and platform to make your voice heard.',
textAlign: TextAlign.center,
),
), ),
]; ];

View file

@ -63,7 +63,7 @@ class IntroductionOptions {
this.introductionButtonTextstyles = const IntroductionButtonTextstyles(), this.introductionButtonTextstyles = const IntroductionButtonTextstyles(),
this.indicatorMode = IndicatorMode.dot, this.indicatorMode = IndicatorMode.dot,
this.indicatorBuilder, this.indicatorBuilder,
this.layoutStyle = IntroductionLayoutStyle.imageCenter, this.layoutStyle = IntroductionLayoutStyle.imageBottom,
this.pages = defaultIntroductionPages, this.pages = defaultIntroductionPages,
this.buttonMode = IntroductionScreenButtonMode.text, this.buttonMode = IntroductionScreenButtonMode.text,
this.tapEnabled = false, this.tapEnabled = false,
@ -73,6 +73,8 @@ class IntroductionOptions {
this.skippable = false, this.skippable = false,
this.buttonBuilder, this.buttonBuilder,
this.controlMode = IntroductionControlMode.previousNextButton, this.controlMode = IntroductionControlMode.previousNextButton,
this.dotSize = 12,
this.dotSpacing = 24,
}) : assert( }) : assert(
!(identical(indicatorMode, IndicatorMode.custom) && !(identical(indicatorMode, IndicatorMode.custom) &&
indicatorBuilder == null), indicatorBuilder == null),
@ -204,6 +206,12 @@ class IntroductionOptions {
/// - Finish /// - Finish
final IntroductionButtonTextstyles introductionButtonTextstyles; final IntroductionButtonTextstyles introductionButtonTextstyles;
/// The size of the dots in the indicator. Default is 12
final double dotSize;
/// The distance between the center of each dot. Default is 24
final double dotSpacing;
IntroductionOptions copyWith({ IntroductionOptions copyWith({
IntroductionScreenMode? mode, IntroductionScreenMode? mode,
List<IntroductionPage>? pages, List<IntroductionPage>? pages,
@ -248,7 +256,7 @@ class IntroductionOptions {
class IntroductionTranslations { class IntroductionTranslations {
const IntroductionTranslations({ const IntroductionTranslations({
this.skipButton = 'skip', this.skipButton = 'Skip',
this.nextButton = 'Next', this.nextButton = 'Next',
this.previousButton = 'Previous', this.previousButton = 'Previous',
this.finishButton = 'Get Started', this.finishButton = 'Get Started',

View file

@ -155,10 +155,15 @@ class _MultiPageIntroductionScreenState
controller: _controller, controller: _controller,
count: pages.length, count: pages.length,
index: _currentPage.value, index: _currentPage.value,
dotSize: widget.options.dotSize,
dotSpacing: widget.options.dotSpacing,
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.all(32), padding: const EdgeInsets.symmetric(
vertical: 40,
horizontal: 20,
),
child: AnimatedBuilder( child: AnimatedBuilder(
animation: _controller, animation: _controller,
builder: (context, _) { builder: (context, _) {
@ -270,14 +275,14 @@ class ExplainerPage extends StatelessWidget {
title: Padding( title: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32), padding: const EdgeInsets.symmetric(horizontal: 32),
child: DefaultTextStyle( child: DefaultTextStyle(
style: theme.textTheme.displayMedium!, style: theme.textTheme.titleMedium!,
child: page.title ?? Text('introduction.$index.title'), child: page.title ?? Text('introduction.$index.title'),
), ),
), ),
text: Padding( text: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32), padding: const EdgeInsets.symmetric(horizontal: 32),
child: DefaultTextStyle( child: DefaultTextStyle(
style: theme.textTheme.bodyLarge!, style: theme.textTheme.bodyMedium!,
child: page.text ?? Text('introduction.$index.description'), child: page.text ?? Text('introduction.$index.description'),
), ),
), ),
@ -340,116 +345,148 @@ class IntroductionTwoButtons extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
if (options.buttonMode == IntroductionScreenButtonMode.text) ...[ if (options.buttonMode == IntroductionScreenButtonMode.text) ...[
if (previous) ...[ Flexible(
options.buttonBuilder?.call( child: Padding(
context, padding: const EdgeInsets.only(right: 6),
_previous, child: ConstrainedBox(
Text( constraints: const BoxConstraints(
translations.previousButton, maxWidth: 180,
style: options ),
.introductionButtonTextstyles.previousButtonStyle, child: Opacity(
), opacity: previous ? 1 : 0,
IntroductionButtonType.previous, child: IgnorePointer(
) ?? ignoring: !previous,
InkWell( child: options.buttonBuilder?.call(
onTap: _previous, context,
child: Container( _previous,
width: 180, Text(
decoration: BoxDecoration( translations.previousButton,
borderRadius: BorderRadius.circular(20), style: options.introductionButtonTextstyles
border: Border.all( .previousButtonStyle,
color: const Color( ),
0xff979797, IntroductionButtonType.previous,
) ??
InkWell(
onTap: _previous,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: const Color(
0xff979797,
),
),
),
child: Center(
child: Padding(
padding:
const EdgeInsets.symmetric(vertical: 4),
child: Text(
translations.previousButton,
style: options.introductionButtonTextstyles
.previousButtonStyle,
),
),
),
),
), ),
),
),
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Text(
translations.previousButton,
style: options
.introductionButtonTextstyles.previousButtonStyle,
),
),
),
), ),
), ),
] else ),
const SizedBox.shrink(), ),
),
if (next) ...[ if (next) ...[
options.buttonBuilder?.call( Flexible(
context, child: Padding(
_next, padding: const EdgeInsets.only(left: 6),
Text( child: ConstrainedBox(
translations.nextButton, constraints: const BoxConstraints(
style: options.introductionButtonTextstyles.nextButtonStyle, maxWidth: 180,
), ),
IntroductionButtonType.next, child: options.buttonBuilder?.call(
) ?? context,
InkWell( _next,
onTap: _next, Text(
child: Container(
width: 180,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: const Color(
0xff979797,
),
),
),
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Text(
translations.nextButton, translations.nextButton,
style: options style: options
.introductionButtonTextstyles.nextButtonStyle, .introductionButtonTextstyles.nextButtonStyle,
), ),
), IntroductionButtonType.next,
), ) ??
), InkWell(
), onTap: _next,
] else if (last) ...[ child: Container(
options.buttonBuilder?.call( decoration: BoxDecoration(
context, borderRadius: BorderRadius.circular(20),
() { border: Border.all(
onFinish?.call(); color: const Color(
}, 0xff979797,
Text( ),
translations.finishButton, ),
style: ),
options.introductionButtonTextstyles.finishButtonStyle, child: Center(
), child: Padding(
IntroductionButtonType.finish, padding: const EdgeInsets.symmetric(vertical: 4),
) ?? child: Text(
InkWell( translations.nextButton,
onTap: () { style: options.introductionButtonTextstyles
onFinish?.call(); .nextButtonStyle,
}, ),
child: Container( ),
width: 180, ),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: const Color(
0xff979797,
), ),
), ),
), ),
child: Center( ),
child: Padding( ),
padding: const EdgeInsets.symmetric(vertical: 2.0), ] else if (last) ...[
child: Text( Flexible(
child: Padding(
padding: const EdgeInsets.only(left: 6),
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 180,
),
child: options.buttonBuilder?.call(
context,
() {
onFinish?.call();
},
Text(
translations.finishButton, translations.finishButton,
style: options style: options
.introductionButtonTextstyles.finishButtonStyle, .introductionButtonTextstyles.finishButtonStyle,
), ),
IntroductionButtonType.finish,
) ??
InkWell(
onTap: () {
onFinish?.call();
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: const Color(
0xff979797,
),
),
),
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Text(
translations.finishButton,
style: options.introductionButtonTextstyles
.finishButtonStyle,
),
),
),
),
), ),
),
),
), ),
),
),
] else ...[ ] else ...[
const SizedBox.shrink(), const SizedBox.shrink(),
], ],
@ -463,44 +500,51 @@ class IntroductionTwoButtons extends StatelessWidget {
maintainState: true, maintainState: true,
maintainInteractivity: false, maintainInteractivity: false,
child: Align( child: Align(
child: options.buttonBuilder?.call( child: Flexible(
context, child: ConstrainedBox(
() { constraints: const BoxConstraints(
onFinish?.call(); maxWidth: 180,
},
Text(
translations.finishButton,
style: options
.introductionButtonTextstyles.finishButtonStyle,
),
IntroductionButtonType.finish,
) ??
InkWell(
onTap: () {
onFinish?.call();
},
child: Container(
width: 180,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: const Color(
0xff979797,
),
),
),
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Text(
translations.finishButton,
style: options.introductionButtonTextstyles
.finishButtonStyle,
),
),
),
),
), ),
child: options.buttonBuilder?.call(
context,
() {
onFinish?.call();
},
Text(
translations.finishButton,
style: options
.introductionButtonTextstyles.finishButtonStyle,
),
IntroductionButtonType.finish,
) ??
InkWell(
onTap: () {
onFinish?.call();
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: const Color(
0xff979797,
),
),
),
child: Center(
child: Padding(
padding:
const EdgeInsets.symmetric(vertical: 4),
child: Text(
translations.finishButton,
style: options.introductionButtonTextstyles
.finishButtonStyle,
),
),
),
),
),
),
),
), ),
), ),
), ),

View file

@ -3,7 +3,6 @@
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import 'dart:async'; import 'dart:async';
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/config/introduction.dart';
@ -16,6 +15,8 @@ class Indicator extends StatelessWidget {
required this.count, required this.count,
required this.index, required this.index,
required this.indicatorBuilder, required this.indicatorBuilder,
required this.dotSize,
required this.dotSpacing,
super.key, super.key,
}) : assert( }) : assert(
!(mode == IndicatorMode.custom && indicatorBuilder == null), !(mode == IndicatorMode.custom && indicatorBuilder == null),
@ -39,6 +40,12 @@ class Indicator extends StatelessWidget {
final Widget Function(BuildContext, PageController, int, int)? final Widget Function(BuildContext, PageController, int, int)?
indicatorBuilder; indicatorBuilder;
/// The size of the dots.
final double dotSize;
/// The distance between the center of each dot.
final double dotSpacing;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var theme = Theme.of(context); var theme = Theme.of(context);
@ -47,9 +54,10 @@ class Indicator extends StatelessWidget {
return indicatorBuilder!.call(context, controller, index, count); return indicatorBuilder!.call(context, controller, index, count);
case IndicatorMode.dot: case IndicatorMode.dot:
return DotsIndicator( return DotsIndicator(
dotSize: dotSize,
dotSpacing: dotSpacing,
controller: controller, controller: controller,
color: theme.colorScheme.primary, color: theme.colorScheme.secondary,
dotcolor: theme.colorScheme.secondary,
itemCount: count, itemCount: count,
onPageSelected: (int page) { onPageSelected: (int page) {
unawaited( unawaited(
@ -153,16 +161,15 @@ class DotsIndicator extends AnimatedWidget {
const DotsIndicator({ const DotsIndicator({
required this.controller, required this.controller,
this.color = Colors.white, this.color = Colors.white,
this.dotcolor = Colors.green,
this.itemCount, this.itemCount,
this.onPageSelected, this.onPageSelected,
this.dotSize = 8.0,
this.dotSpacing = 24.0,
super.key, super.key,
}) : super( }) : super(
listenable: controller, listenable: controller,
); );
/// The PageController that this DotsIndicator is representing.
final Color? dotcolor;
final PageController controller; final PageController controller;
/// The number of items managed by the PageController /// The number of items managed by the PageController
@ -177,47 +184,31 @@ class DotsIndicator extends AnimatedWidget {
final Color color; final Color color;
// The base size of the dots // The base size of the dots
static const double _kDotSize = 4.0; final double dotSize;
final double dotSpacing;
// The increase in the size of the selected dot Widget _buildDot(int index) => SizedBox(
static const double _kMaxZoom = 2.0; width: dotSpacing,
child: Center(
// The distance between the center of each dot child: Material(
static const double _kDotSpacing = 12.0; color:
(((controller.page ?? controller.initialPage).round()) == index
Widget _buildDot(int index) { ? color
var selectedness = Curves.easeOut.transform( : color.withAlpha(125)),
max( type: MaterialType.circle,
0.0, child: Container(
1.0 - decoration: const BoxDecoration(
((controller.page ?? controller.initialPage).round() - index).abs(), shape: BoxShape.circle,
), ),
); width: dotSize,
var zoom = 1.0 + (_kMaxZoom - 1.0) * selectedness; height: dotSize,
child: InkWell(
return SizedBox( onTap: () => onPageSelected!.call(index),
width: _kDotSpacing, ),
child: Center(
child: Material(
color: (((controller.page ?? controller.initialPage).round()) == index
? color
: color.withAlpha(125)),
type: MaterialType.circle,
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(width: 2, color: dotcolor!),
),
width: _kDotSize * 2 * zoom,
height: _kDotSize * 2 * zoom,
child: InkWell(
onTap: () => onPageSelected!.call(index),
), ),
), ),
), ),
), );
);
}
@override @override
Widget build(BuildContext context) => Row( Widget build(BuildContext context) => Row(

View file

@ -1,6 +1,6 @@
name: flutter_introduction_widget name: flutter_introduction_widget
description: Flutter Introduction Widget for showing a list of introduction pages on a single scrollable page or horizontal pageview description: Flutter Introduction Widget for showing a list of introduction pages on a single scrollable page or horizontal pageview
version: 3.0.0 version: 3.1.0
homepage: https://github.com/Iconica-Development/flutter_introduction_widget homepage: https://github.com/Iconica-Development/flutter_introduction_widget
environment: environment:

View file

@ -1,5 +1,5 @@
name: flutter_introduction_workspace name: flutter_introduction_workspace
version: 3.0.0 version: 3.1.0
environment: environment:
sdk: '>=3.1.0 <4.0.0' sdk: '>=3.1.0 <4.0.0'