Merge pull request #1 from Iconica-Development/v0.0.1

V0.0.1
This commit is contained in:
Gorter-dev 2022-10-04 16:14:46 +02:00 committed by GitHub
commit 839ea1a41f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 4075 additions and 153 deletions

View file

@ -1,3 +1,3 @@
## 0.0.1 ## 0.0.1 - September 29th 2022
* TODO: Describe initial release. - Initial release

View file

@ -1,39 +1,60 @@
<!-- # Flutter Form
This README describes the package. If you publish this package to pub.dev,
this README's contents appear on the landing page for your package.
For information about how to write a good package README, see the guide for Flutter Form is a package you can use to create a single or multi page form with premade or custom inputfields.
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
For general information about developing packages, see the Dart guide for
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
and the Flutter guide for
[developing packages and plugins](https://flutter.dev/developing-packages).
-->
TODO: Put a short description of the package here that helps potential users
know whether this package might be useful for them.
## Features ## Features
TODO: List what your package can do. Maybe include images, gifs, or videos. - Single or multi page form with the ability to define the navigational buttons.
- A handfull premade fields with their own controllers.
- Full posibilty to create custom inputfields and controllers which can be used along side the premade fields and controllers.
- A checkpage where the end user can check his answers and jump back to the page of an inputfield to change his answer without going through the whole form.
- The look of the checkpage answers can be set own desire.
## Getting started ## Setup
TODO: List prerequisites and provide or point to information on how to To use this package, add `flutter_form` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels).
start using the package.
## Usage ## How To Use
TODO: Include short and useful examples for package users. Add longer examples See the [Example Code](example/lib/form_example.dart) for an example on how to use this package.
to `/example` folder.
```dart WARNING Make sure to define your FlutterFormInputControllers above your Flutter Form and not inside each page. This prevents that the used controllers differ from the registered ones.
const like = 'sample';
```
## Additional information Flutter Form has two paramaters: options and formController. Each of these parameters' own parameters will be explained in tabels below.
TODO: Tell users more about the package: where to find more information, how to Options:
contribute to the package, how to file issues, what response they can expect
from the package authors, and more. | Parameter | Explaination |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| checkPage | If this is set the form will feature a checkpage at the end so the end user can verify and alter his answers. |
| nextButton | The button which is put in the stack of the Form. An onTap has to be implemented and should call to the FormController. Standard call is autoNextStep(). |
| backButton | Same as the nextButton. A widget that is put in the stack of the Form. An onTap has to be implemented and should call to the FormController. Standard call is previousStep(). |
| onFinised | The callback that will be called when the last page is finished. If checkPage is enabled this will call after the checkPage is passed. |
| onNext | The callback that is called when the user finishes a page. PageNumber is also provided. |
FormController:
| Parameter | Explaination |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| getFormPageControllers() | The getter to get all FormPageControllers. This should not be needed/called. |
| setFormPageControllers() | The setter for the FormPageControllers. This shoudl not be needed/called. |
| disableCheckPages() | This should be called when the user goes back to a page where the user alters an answer that alters the rest of the form. |
| autoNextStep() | This should be called under the nextButton of the FormOptions if no special actions are required. |
| previousStep() | This should be called under the backButton of the FormOptions. |
| jumpToPage() | A way to jump to a different page if desired. |
| validateAndSaveCurretnStep() | Calling the validate, and possibly save, for the current step. Returns the result of the validate. |
| getCurrentStepResults() | Get the result of the current step. Mostly called after validateAndSaveCurrentStep return true. |
| nextStep() | Called to go to the next step. This is does not do anything else like autoNextStep does do. |
| finishForm() | Calls the onFinished of the form options. |
## Issues
Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_form/pulls) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl).
## Want to contribute
If you would like to contribute to the plugin (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](../CONTRIBUTING.md) and send us your [pull request](URL TO PULL REQUEST TAB IN REPO).
## Author
`flutter-form` for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at <support@iconica.nl>

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

View file

@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:flutter_form/flutter_form.dart';
import 'package:form_example/template_page.dart';
class AgePage extends StatefulWidget {
const AgePage({
required this.inputController,
super.key,
});
final FlutterFormInputNumberPickerController inputController;
@override
State<AgePage> createState() => _AgePageState();
}
class _AgePageState extends State<AgePage> {
@override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
var fontSize = size.height / 40;
return TemplatePage(
size: size,
fontSize: fontSize,
title: "What is your age?",
pageNumber: 1,
amountOfPages: 3,
flutterFormWidgets: [
FlutterFormInputNumberPicker(
minValue: 12,
maxValue: 120,
controller: widget.inputController,
),
],
);
}
}

View file

@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
import 'package:flutter_form/flutter_form.dart';
import 'package:form_example/template_page.dart';
class CarouselPage extends StatefulWidget {
const CarouselPage({
required this.inputController,
required this.cars,
super.key,
});
final FlutterFormInputCarouselController inputController;
final List<Map<String, dynamic>> cars;
@override
State<CarouselPage> createState() => _CarouselPageState();
}
class _CarouselPageState extends State<CarouselPage> {
@override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
var fontSize = size.height / 40;
return TemplatePage(
size: size,
fontSize: fontSize,
title: "What's your favorite car?",
pageNumber: 3,
amountOfPages: 3,
flutterFormWidgets: [
FlutterFormInputCarousel(
controller: widget.inputController, items: getCars())
],
);
}
List<Widget> getCars() {
return widget.cars.map((car) {
return Builder(
builder: (BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Container(
width: MediaQuery.of(context).size.width,
decoration: const BoxDecoration(
color: Color(0xFFD8D8D8),
borderRadius: BorderRadius.all(
Radius.circular(10),
),
),
child: Image.asset('assets/images/${car['title']}.png'),
),
),
const SizedBox(
height: 14,
),
Text(
car["title"],
style: const TextStyle(
fontWeight: FontWeight.w900,
fontSize: 20,
),
),
const SizedBox(
height: 5,
),
Text(
car["description"],
style: const TextStyle(fontSize: 16),
),
],
);
},
);
}).toList();
}
}

View file

@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:flutter_form/flutter_form.dart';
class CheckPageExample {
CheckPage showCheckpage(
BuildContext context,
Size size,
double fontSize,
String checkPageText,
) {
return CheckPage(
title: Container(
margin: const EdgeInsets.only(
top: 70,
bottom: 10,
),
padding: const EdgeInsets.symmetric(horizontal: 40),
child: const Text(
"Check answers",
style: TextStyle(
fontSize: 25,
fontWeight: FontWeight.w900,
),
),
),
inputCheckWidget:
(String title, String? description, Function onPressed) {
return GestureDetector(
onTap: () async {
await onPressed();
},
child: Container(
width: MediaQuery.of(context).size.width * 0.9,
padding: const EdgeInsets.only(
top: 18,
bottom: 16,
right: 18,
left: 27,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: const Color(0xFF000000).withOpacity(0.20),
blurRadius: 5,
),
],
),
child: Column(
children: [
Row(
children: [
Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: const Color(0xFFD8D8D8),
borderRadius: BorderRadius.circular(5),
),
),
const SizedBox(
width: 16,
),
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w900,
fontSize: 20,
),
),
const Spacer(),
const Icon(Icons.arrow_forward),
],
),
if (description != null)
const SizedBox(
height: 9,
),
if (description != null)
Text(
description,
style: const TextStyle(fontSize: 16),
)
],
),
),
);
},
);
}
}

View file

@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:flutter_form/flutter_form.dart';
import 'package:form_example/template_page.dart';
class NamePage extends StatefulWidget {
const NamePage({
required this.firstNameController,
required this.lastNameController,
required this.showLastName,
super.key,
});
final FlutterFormInputPlainTextController firstNameController;
final FlutterFormInputPlainTextController lastNameController;
final bool showLastName;
@override
State<NamePage> createState() => _NamePageState();
}
class _NamePageState extends State<NamePage> {
@override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
var fontSize = size.height / 40;
return TemplatePage(
size: size,
fontSize: fontSize,
pageNumber: 2,
amountOfPages: 3,
title: "Please enter your name",
flutterFormWidgets: [
Padding(
padding: const EdgeInsets.fromLTRB(40, 0, 40, 40),
child: FlutterFormInputPlainText(
label: const Text("First Name"),
controller: widget.firstNameController,
),
),
if (widget.showLastName)
Padding(
padding: const EdgeInsets.fromLTRB(40, 0, 40, 0),
child: FlutterFormInputPlainText(
label: const Text("Last Name"),
controller: widget.lastNameController,
),
),
],
);
}
}

View file

@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
class ThanksPage extends StatelessWidget {
const ThanksPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
"Thanks for filling in the form!",
style: TextStyle(fontSize: 20),
),
const SizedBox(
height: 20,
),
ElevatedButton(
onPressed: () => Navigator.of(context).pushNamed('/'),
child: const Text("Next"))
],
),
),
);
}
}

View file

@ -0,0 +1,201 @@
import 'package:flutter/material.dart';
import 'package:flutter_form/flutter_form.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:form_example/example_pages/age_page.dart';
import 'package:form_example/example_pages/carousel_page.dart';
import 'package:form_example/example_pages/check_page.dart';
import 'package:form_example/example_pages/name_page.dart';
class FormExample extends ConsumerStatefulWidget {
const FormExample({Key? key}) : super(key: key);
@override
ConsumerState<ConsumerStatefulWidget> createState() => _FormExampleState();
}
class _FormExampleState extends ConsumerState<FormExample> {
final FlutterFormController formController = FlutterFormController();
final String checkPageText = "All entered info: ";
final ageInputController = FlutterFormInputNumberPickerController(
id: "age",
checkPageTitle: (dynamic amount) {
return "Age: $amount years";
},
);
late final FlutterFormInputCarouselController carouselInputController;
final List<Map<String, dynamic>> cars = [
{
"title": "Mercedes",
"description": "Mercedes is a car",
},
{
"title": "BMW",
"description": "BMW is a car",
},
{
"title": "Mazda",
'description': "Mazda is a car",
},
];
FlutterFormInputPlainTextController firstNameController =
FlutterFormInputPlainTextController(
mandatory: true,
id: "firstName",
checkPageTitle: (dynamic firstName) {
return "First Name: $firstName";
},
);
FlutterFormInputPlainTextController lastNameController =
FlutterFormInputPlainTextController(
mandatory: true,
id: "lastName",
checkPageTitle: (dynamic lastName) {
return "Last Name: $lastName";
},
);
@override
void initState() {
super.initState();
carouselInputController = FlutterFormInputCarouselController(
id: 'carCarousel',
checkPageTitle: (dynamic index) {
return cars[index]["title"];
},
checkPageDescription: (dynamic index) {
return cars[index]["description"];
},
);
}
bool showLastName = true;
@override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
var fontSize = size.height / 40;
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
body: Center(
child: FlutterForm(
formController: formController,
options: FlutterFormOptions(
onFinished: (Map<int, Map<String, dynamic>> results) {
debugPrint("Final full results: $results");
Navigator.of(context).pushNamed('/thanks');
},
onNext: (int pageNumber, Map<String, dynamic> results) {
debugPrint("Results page $pageNumber: $results");
if (pageNumber == 0) {
if (results['age'] >= 18) {
if (showLastName == false) {
showLastName = true;
formController.disableCheckingPages();
}
} else {
if (showLastName == true) {
showLastName = false;
formController.disableCheckingPages();
}
}
setState(() {});
}
},
nextButton: (int pageNumber, bool checkingPages) {
return Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: EdgeInsets.only(
bottom: size.height * 0.05,
),
child: SizedBox(
height: size.height * 0.07,
width: size.width * 0.7,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
backgroundColor: Colors.black,
textStyle: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
onPressed: () async {
await formController.autoNextStep();
},
child: Text(checkingPages ? "Save" : "Next Page"),
),
),
),
);
},
backButton: (int pageNumber, bool checkingPages, int pageAmount) {
if (pageNumber != 0) {
if (!checkingPages || pageNumber >= pageAmount) {
return Align(
alignment: Alignment.topLeft,
child: Container(
margin: EdgeInsets.only(
top: size.height * 0.045,
left: size.width * 0.07,
),
width: size.width * 0.08,
height: size.width * 0.08,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(90),
color: const Color(0xFFD8D8D8).withOpacity(0.50),
),
child: IconButton(
padding: EdgeInsets.zero,
splashRadius: size.width * 0.06,
onPressed: () {
formController.previousStep();
},
icon: const Icon(Icons.chevron_left),
)),
);
}
}
return Container();
},
pages: [
FlutterFormPage(
child: AgePage(
inputController: ageInputController,
),
),
FlutterFormPage(
child: NamePage(
firstNameController: firstNameController,
lastNameController: lastNameController,
showLastName: showLastName,
),
),
FlutterFormPage(
child: CarouselPage(
inputController: carouselInputController,
cars: cars,
),
),
],
checkPage: CheckPageExample()
.showCheckpage(context, size, fontSize, checkPageText),
),
),
),
),
);
}
}

View file

@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:form_example/route.dart';
void main() { void main() {
runApp(const FormsExample()); runApp(const ProviderScope(child: FormsExample()));
} }
class FormsExample extends StatelessWidget { class FormsExample extends StatelessWidget {
@ -15,7 +17,22 @@ class FormsExample extends StatelessWidget {
theme: ThemeData( theme: ThemeData(
primarySwatch: Colors.blue, primarySwatch: Colors.blue,
), ),
home: const FormsHomePage(title: 'Flutter Demo Home Page'), home: const FormsHomePage(title: 'Flutter Forms'),
initialRoute: '/',
onGenerateRoute: (settings) {
var routes = getRoutes();
if (routes.containsKey(settings.name)) {
return PageRouteBuilder(
pageBuilder: (_, __, ___) => routes[settings.name]!(context),
settings: settings,
);
} else {
return PageRouteBuilder(
settings: settings,
pageBuilder: (_, __, ___) => const Text('Page not found'),
);
}
},
); );
} }
} }
@ -38,12 +55,10 @@ class _FormsHomePageState extends State<FormsHomePage> {
), ),
body: Center( body: Center(
child: ElevatedButton( child: ElevatedButton(
onPressed: (() => createForm()), onPressed: (() => Navigator.of(context).pushNamed('/form')),
child: const Text('Create form'), child: const Text('Create form'),
), ),
), ),
); );
} }
void createForm() {}
} }

12
example/lib/route.dart Normal file
View file

@ -0,0 +1,12 @@
import 'package:flutter/widgets.dart';
import 'package:form_example/example_pages/thanks_page.dart';
import 'package:form_example/form_example.dart';
import 'package:form_example/main.dart';
Map<String, WidgetBuilder> getRoutes() {
return {
'/': (context) => const FormsExample(),
'/form': (context) => const FormExample(),
'/thanks': (context) => const ThanksPage(),
};
}

View file

@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
class TemplatePage extends StatelessWidget {
const TemplatePage({
super.key,
required this.size,
required this.fontSize,
required this.title,
required this.pageNumber,
required this.amountOfPages,
required this.flutterFormWidgets,
});
final Size size;
final double fontSize;
final String title;
final int pageNumber;
final int amountOfPages;
final List<Widget> flutterFormWidgets;
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: size.width / 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: size.height / 10,
),
Text(
"$pageNumber / $amountOfPages",
style: TextStyle(
fontSize: fontSize,
),
),
SizedBox(
height: size.height / 80,
),
Text(
title,
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.w900,
),
),
],
),
),
),
const Spacer(),
for (var widget in flutterFormWidgets) ...[
widget,
],
const Spacer(
flex: 2,
),
],
);
}
}

View file

@ -36,6 +36,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.16.0" version: "1.16.0"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.2"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@ -55,6 +62,13 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_form:
dependency: "direct main"
description:
path: ".."
relative: true
source: path
version: "0.0.1"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -62,11 +76,30 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.1" version: "2.0.1"
flutter_localizations:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
flutter_riverpod:
dependency: "direct main"
description:
name: flutter_riverpod
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
intl:
dependency: transitive
description:
name: intl
url: "https://pub.dartlang.org"
source: hosted
version: "0.17.0"
lints: lints:
dependency: transitive dependency: transitive
description: description:
@ -74,6 +107,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
localization:
dependency: transitive
description:
name: localization
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@ -102,11 +142,25 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.2" version: "1.8.2"
riverpod:
dependency: transitive
description:
name: riverpod
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.99" version: "0.0.99"
sliding_up_panel:
dependency: transitive
description:
name: sliding_up_panel
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0+1"
source_span: source_span:
dependency: transitive dependency: transitive
description: description:
@ -121,6 +175,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.10.0" version: "1.10.0"
state_notifier:
dependency: transitive
description:
name: state_notifier
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.2+1"
stream_channel: stream_channel:
dependency: transitive dependency: transitive
description: description:
@ -149,6 +210,20 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.12" version: "0.4.12"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.1"
uuid:
dependency: transitive
description:
name: uuid
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.6"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@ -158,3 +233,4 @@ packages:
version: "2.1.2" version: "2.1.2"
sdks: sdks:
dart: ">=2.18.0 <3.0.0" dart: ">=2.18.0 <3.0.0"
flutter: ">=3.0.0"

View file

@ -1,91 +1,30 @@
name: example name: form_example
description: A new Flutter project. description: Form example made with Flutter Form Package.
# The following line prevents the package from being accidentally published to publish_to: 'none'
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1 version: 1.0.0+1
environment: environment:
sdk: '>=2.18.0 <3.0.0' sdk: '>=2.18.0 <3.0.0'
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2 cupertino_icons: ^1.0.2
flutter_riverpod: ^1.0.4
flutter_form:
path: ../
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^2.0.0 flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter: flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true uses-material-design: true
assets:
# To add assets to your application, add an assets section, like this: - assets/images/
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages

View file

@ -1,30 +0,0 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:example/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const FormsExample());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}

View file

@ -1,7 +1,4 @@
library flutter_form; export 'src/form.dart';
export 'src/widgets/input/abstractions.dart';
/// A Calculator. export 'src/widgets/input/input_types/input_types.dart';
class Calculator { export 'utils/form.dart';
/// Returns [value] plus 1.
int addOne(int value) => value + 1;
}

555
lib/src/form.dart Normal file
View file

@ -0,0 +1,555 @@
import 'package:flutter/material.dart';
import 'package:flutter_form/utils/translation_service.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../flutter_form.dart';
import 'utils/form_page_controller.dart';
import 'utils/formstate.dart' as fs;
/// A wrapper for flutters [Form] that can be controlled by a controller and provides multiple pre-defined input types/fields
/// [FlutterForm] also provides multi page forms and a check page for validation.
///
/// A [FlutterFormController] has to be given to control what happens to values and pages within the FlutterForm.
///
/// [FlutterFormOptions] have to be provided to control the appearance of the form.
///
/// WARNING Define your FormInputController above your FlutterForm. Otherwise when rebuild the controller will differ from the registered ones.
/// ``` dart
/// FlutterFormInputEmailController emailController =
/// FlutterFormInputEmailController(id: 'email');
/// FlutterFormInputPasswordController passwordController =
/// FlutterFormInputPasswordController(id: 'password');
///
/// FlutterForm(
/// formController: FlutterFormController,
/// options: FlutterFormOptions(
/// onFinished: (Map<int, Map<String, dynamic>> results) {
/// // print(results);
/// },
/// onNext: (int pageNumber, Map<String, dynamic> results) {
/// // print("Results page $pageNumber: $results");
/// },
/// nextButton: (int pageNumber, bool checkingPages) {
/// return Align(
/// alignment: Alignment.bottomCenter,
/// child: Padding(
/// padding: const EdgeInsets.only(
/// bottom: 25,
/// ),
/// child: ElevatedButton(
/// onPressed: () {
/// FlutterFormController.autoNextStep();
/// },
/// child: Text(checkingPages ? "Save" : "Next Page"),
/// ),
/// ),
/// );
/// },
/// backButton: (int pageNumber, bool checkingPages, int pageAmount) {
/// if (pageNumber != 0) {
/// if (!checkingPages || pageNumber >= pageAmount) {
/// return Align(
/// alignment: Alignment.topLeft,
/// child: IconButton(
/// padding: EdgeInsets.zero,
/// splashRadius: 29,
/// onPressed: () {
/// FlutterFormController.previousStep();
/// },
/// icon: const Icon(Icons.chevron_left),
/// ),
/// );
/// }
/// }
/// return Container();
/// },
/// pages: [
/// FlutterFormPage(
/// child: Column(
/// mainAxisAlignment: MainAxisAlignment.center,
/// children: [
/// Align(
/// alignment: Alignment.centerLeft,
/// child: Padding(
/// padding: const EdgeInsets.symmetric(horizontal: 46),
/// child: Column(
/// crossAxisAlignment: CrossAxisAlignment.start,
/// children: const [
/// SizedBox(
/// height: 60,
/// ),
/// Text(
/// 'Inloggen',
/// style: TextStyle(
/// fontSize: 25,
/// fontWeight: FontWeight.w900,
/// ),
/// ),
/// ],
/// ),
/// ),
/// ),
/// const Spacer(),
/// FlutterFormInputEmail(controller: emailController),
/// const SizedBox(
/// height: 25,
/// ),
/// FlutterFormInputPassword(controller: passwordController),
/// const Spacer(),
/// ],
/// ),
/// ),
/// ],
/// checkPage: CheckPage(
/// title: const Text(
/// "All entered info: ",
/// style: TextStyle(
/// fontSize: 25,
/// fontWeight: FontWeight.w900,
/// ),
/// ),
/// inputCheckWidget:
/// (String title, String? description, Function onPressed) {
/// return GestureDetector(
/// onTap: () async {
/// await onPressed();
/// },
/// child: Container(
/// width: MediaQuery.of(context).size.width * 0.9,
/// padding: const EdgeInsets.only(
/// top: 18,
/// bottom: 16,
/// right: 18,
/// left: 27,
/// ),
/// decoration: BoxDecoration(
/// color: Colors.white,
/// borderRadius: BorderRadius.circular(10),
//// boxShadow: [
/// BoxShadow(
/// color: const Color(0xFF000000).withOpacity(0.20),
/// blurRadius: 5,
/// ),
/// ],
/// ),
/// child: Column(
/// children: [
/// Row(
/// children: [
/// Container(
/// width: 30,
/// height: 30,
/// decoration: BoxDecoration(
/// color: const Color(0xFFD8D8D8),
/// borderRadius: BorderRadius.circular(5),
/// ),
/// ),
/// const SizedBox(
/// width: 16,
/// ),
/// Text(
/// title,
/// style: const TextStyle(
/// fontWeight: FontWeight.w900,
/// fontSize: 20,
/// ),
/// ),
/// const Spacer(),
/// const Icon(Icons.arrow_forward),
/// ],
/// ),
/// if (description != null)
/// const SizedBox(
/// height: 9,
/// ),
/// if (description != null)
/// Text(
/// description,
/// style: const TextStyle(fontSize: 16),
/// )
/// ],
/// ),
/// ),
/// );
/// },
/// mainAxisAlignment: MainAxisAlignment.start,
/// ),
/// ),
/// ),
/// ```
class FlutterForm extends ConsumerStatefulWidget {
const FlutterForm({
Key? key,
required this.options,
required this.formController,
}) : super(key: key);
final FlutterFormOptions options;
final FlutterFormController formController;
@override
ConsumerState<FlutterForm> createState() => _FlutterFormState();
}
class _FlutterFormState extends ConsumerState<FlutterForm> {
late FlutterFormController _formController;
@override
void initState() {
super.initState();
_formController = widget.formController;
_formController.setFlutterFormOptions(widget.options);
List<GlobalKey<FormState>> keys = [];
for (FlutterFormPage _ in widget.options.pages) {
keys.add(GlobalKey<FormState>());
}
_formController.setKeys(keys);
_formController.addListener(() {
setState(() {});
});
List<FlutterFormPageController> controllers = [];
for (int i = 0; i < widget.options.pages.length; i++) {
controllers.add(FlutterFormPageController());
}
_formController.setFormPageControllers(controllers);
}
@override
Widget build(BuildContext context) {
var _ = getTranslator(context, ref);
return Stack(
children: [
PageView(
controller: _formController.getPageController(),
physics: const NeverScrollableScrollPhysics(),
children: [
for (int i = 0; i < widget.options.pages.length; i++) ...[
Form(
key: _formController.getKeys()[i],
child: fs.FormState(
formController: _formController.getFormPageControllers()[i],
child: CustomScrollView(
slivers: [
SliverFillRemaining(
hasScrollBody: false,
child: widget.options.pages[i].child,
),
],
),
),
),
],
if (widget.options.checkPage != null)
Column(
children: [
if (widget.options.checkPage!.title != null)
widget.options.checkPage!.title!,
Expanded(
child: CustomScrollView(
slivers: [
SliverFillRemaining(
hasScrollBody: false,
child: Column(
mainAxisAlignment:
widget.options.checkPage!.mainAxisAlignment,
children: getResultWidgets(),
),
),
],
),
),
],
),
],
),
widget.options.nextButton != null
? widget.options.nextButton!(_formController.getCurrentStep(),
_formController.getCheckpages())
: Align(
alignment: AlignmentDirectional.bottomCenter,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
padding: const EdgeInsets.symmetric(
horizontal: 40, vertical: 15),
textStyle: const TextStyle(
fontSize: 20, fontWeight: FontWeight.bold)),
onPressed: () async {
await _formController.autoNextStep();
},
child: Text(_formController.getCurrentStep() >=
widget.options.pages.length - 1
? "Finish"
: "Next"),
),
),
if (widget.options.backButton != null)
widget.options.backButton!(
_formController.getCurrentStep(),
_formController.getCheckpages(),
widget.options.pages.length,
),
],
);
}
List<Widget> getResultWidgets() {
List<Widget> widgets = [];
_formController.getAllResults().forEach(
(pageNumber, pageResults) {
pageResults.forEach((inputId, inputResult) {
FlutterFormInputController? inputController = _formController
.getFormPageControllers()[pageNumber]
.getController(inputId);
if (inputController != null) {
if (widget.options.checkPage!.inputCheckWidget != null) {
widgets.add(
widget.options.checkPage!.inputCheckWidget!(
inputController.checkPageTitle != null
? inputController.checkPageTitle!(inputController.value)
: inputController.value.toString(),
inputController.checkPageDescription != null
? inputController
.checkPageDescription!(inputController.value)
: null,
() async {
await _formController.jumpToPage(pageNumber);
},
),
);
} else {
widgets.add(
GestureDetector(
onTap: () async {
await _formController.jumpToPage(pageNumber);
},
child: Column(
children: [
Row(
children: [
Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: const Color(0xFFD8D8D8),
borderRadius: BorderRadius.circular(5),
),
),
const SizedBox(
width: 16,
),
Text(
inputController.checkPageTitle != null
? inputController.checkPageTitle!(inputResult)
: inputResult.toString(),
style: const TextStyle(
fontWeight: FontWeight.w900,
fontSize: 20,
),
),
const Spacer(),
const Icon(Icons.arrow_forward),
],
),
if (inputController.checkPageDescription != null)
const SizedBox(
height: 9,
),
if (inputController.checkPageDescription != null)
Text(
inputController.checkPageDescription!(inputResult),
style: const TextStyle(fontSize: 16),
)
],
),
),
);
}
}
widgets.add(
const SizedBox(
height: 25,
),
);
});
},
);
return widgets;
}
}
class FlutterFormController extends ChangeNotifier {
late FlutterFormOptions _options;
int _currentStep = 0;
late List<GlobalKey<FormState>> _keys;
bool _checkingPages = false;
final PageController _pageController = PageController();
late List<FlutterFormPageController> _formPageControllers;
List<FlutterFormPageController> getFormPageControllers() {
return _formPageControllers;
}
setFormPageControllers(List<FlutterFormPageController> controllers) {
_formPageControllers = controllers;
}
disableCheckingPages() {
_checkingPages = false;
for (var controller in _formPageControllers) {
controller.clearControllers();
}
}
Future<void> autoNextStep() async {
if (_currentStep >= _options.pages.length && _options.checkPage != null) {
_options.onFinished(getAllResults());
} else {
if (validateAndSaveCurrentStep()) {
FocusManager.instance.primaryFocus?.unfocus();
_options.onNext(
_currentStep, _formPageControllers[_currentStep].getAllValues());
if (_currentStep >= _options.pages.length - 1 &&
_options.checkPage == null ||
_currentStep >= _options.pages.length &&
_options.checkPage != null) {
_options.onFinished(getAllResults());
} else {
if (_checkingPages) {
_currentStep = _options.pages.length;
notifyListeners();
await _pageController.animateToPage(_currentStep,
duration: const Duration(milliseconds: 250),
curve: Curves.ease);
} else {
_currentStep += 1;
if (_currentStep >= _options.pages.length &&
_options.checkPage != null) {
_checkingPages = true;
}
notifyListeners();
await _pageController.animateToPage(_currentStep,
duration: const Duration(milliseconds: 250),
curve: Curves.ease);
}
}
}
}
}
Future<void> previousStep() async {
_currentStep -= 1;
_checkingPages = false;
notifyListeners();
await _pageController.animateToPage(
_currentStep,
duration: const Duration(milliseconds: 250),
curve: Curves.ease,
);
}
Future<void> jumpToPage(int pageNumber) async {
_currentStep = pageNumber;
notifyListeners();
await _pageController.animateToPage(
_currentStep,
duration: const Duration(milliseconds: 250),
curve: Curves.ease,
);
}
bool validateAndSaveCurrentStep() {
if (_keys[_currentStep].currentState!.validate()) {
_keys[_currentStep].currentState!.save();
return true;
}
return false;
}
Map<String, dynamic> getCurrentStepResults() {
return _formPageControllers[_currentStep].getAllValues();
}
Future<void> nextStep() async {
_currentStep += 1;
if (_currentStep >= _options.pages.length && _options.checkPage != null) {
_checkingPages = true;
}
notifyListeners();
await _pageController.animateToPage(_currentStep,
duration: const Duration(milliseconds: 250), curve: Curves.ease);
}
finishForm() {
_options.onFinished(getAllResults());
}
Map<int, Map<String, dynamic>> getAllResults() {
Map<int, Map<String, dynamic>> allValues = {};
for (var i = 0; i < _options.pages.length; i++) {
allValues.addAll({i: _formPageControllers[i].getAllValues()});
}
return allValues;
}
setFlutterFormOptions(FlutterFormOptions options) {
_options = options;
}
setKeys(List<GlobalKey<FormState>> keys) {
_keys = keys;
}
List<GlobalKey<FormState>> getKeys() {
return _keys;
}
int getCurrentStep() {
return _currentStep;
}
bool getCheckpages() {
return _checkingPages;
}
PageController getPageController() {
return _pageController;
}
}

View file

@ -0,0 +1,36 @@
import 'package:flutter_form/flutter_form.dart';
class FlutterFormPageController {
List<FlutterFormInputController> _controllers = [];
void register(FlutterFormInputController inputController) {
_controllers.add(inputController);
}
clearControllers() {
_controllers = [];
}
bool _isRegisteredById(String id) {
return _controllers.any((element) => (element.id == id));
}
FlutterFormInputController? getController(String key) {
if (_isRegisteredById(key)) {
return _controllers.firstWhere((element) => element.id == key);
}
return null;
}
Map<String, dynamic> getAllValues() {
Map<String, dynamic> values = {};
for (FlutterFormInputController controller in _controllers) {
if (controller.value != null) {
values.addAll({controller.id!: controller.value});
}
}
return values;
}
}

View file

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'form_page_controller.dart';
class FormState extends InheritedWidget {
const FormState({
Key? key,
required Widget child,
required this.formController,
}) : super(key: key, child: child);
final FlutterFormPageController formController;
static FormState of(BuildContext context) {
final FormState? result =
context.dependOnInheritedWidgetOfExactType<FormState>();
assert(result != null, 'No FormStat found in context');
return result!;
}
@override
bool updateShouldNotify(FormState oldWidget) =>
formController != oldWidget.formController;
}

View file

@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '/src/utils/formstate.dart' as fs;
/// Abstract class for the input widgets used in a [FlutterForm].
///
/// The controller [FlutterFormInputController] has to be given to the widget.
/// Which controller is used determines how to value will be handled.
///
/// label is a standard parameter to normally sets the label of the input.
///
/// [registerController] should be called to register the given [controller] to the form page.
abstract class FlutterFormInputWidget extends ConsumerWidget {
const FlutterFormInputWidget({
Key? key,
required this.controller,
this.label,
String? hintText,
}) : super(key: key);
/// The [controller] which determines how the value is handled and how the value is shown on the checkpage.
final FlutterFormInputController controller;
/// [label] is a standard parameter to normally sets the label of the input.
final Widget? label;
/// [registerController] should be called to register the given [controller] to the form page.
registerController(BuildContext context) {
FlutterFormInputController? localController =
fs.FormState.of(context).formController.getController(controller.id!);
if (localController == null) {
fs.FormState.of(context).formController.register(controller);
}
}
}
/// Abstract class for the controller for inputs used in a [FlutterForm].
///
/// The [id] determines the key in the [Map] returned by the [FlutterForm].
///
/// [value] is a way to set a initial value and will be the value when change by the user.
///
/// [mandatory] determines if the input is mandatory.
///
/// [checkPageTitle] is a function where you can transform the value from the input into something representable.
/// This value will be given when defining the check page widgets.
/// If this function is not set, the value will be used as is.
/// Example:
/// ``` dart
/// checkPageTitle: (dynamic amount) {
/// return "$amount persons";
/// },
/// ```
///
/// [checkPageDescription] is the same as checkPageTitle but for the description.
/// If null no description will be shown.
///
/// [onSaved] goes of when the save function is called for the page if [onValidate] return null.
///
/// [onValidate] is used to validate the given input by the user.
abstract class FlutterFormInputController<T> {
/// The [id] determines the key in the [Map] returned by the [FlutterForm].
String? id;
/// [value] is a way to set a initial value and will be the value when change by the user.
T? value;
/// [mandatory] determines if the input is mandatory.
bool mandatory = false;
/// [checkPageTitle] is a function where you can transform the value from the input into something representable.
/// This value will be given when defining the check page widgets.
/// If this function is not set, the value will be used as is.
/// Example:
/// ``` dart
/// checkPageTitle: (dynamic amount) {
/// return "$amount persons";
/// },
/// ```
String Function(T value)? checkPageTitle;
/// [checkPageDescription] is the same as checkPageTitle but for the description.
/// If null no description will be shown.
String Function(T value)? checkPageDescription;
/// [onSaved] goes of when the save function is called for the page if [onValidate] return null.
void onSaved(T value);
/// [onValidate] is used to validate the given input by the user.
String? onValidate(
T value, String Function(String, {List<String>? params}) translator);
}

View file

@ -0,0 +1,146 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'carousel_utils.dart';
import 'carousel_options.dart';
import 'carousel_state.dart';
abstract class CarouselController {
bool get ready;
Future<void> get onReady;
Future<void> nextPage({Duration? duration, Curve? curve});
Future<void> previousPage({Duration? duration, Curve? curve});
void jumpToPage(int page);
Future<void> animateToPage(int page, {Duration? duration, Curve? curve});
void startAutoPlay();
void stopAutoPlay();
factory CarouselController() => CarouselControllerImpl();
}
class CarouselControllerImpl implements CarouselController {
final Completer<void> _readyCompleter = Completer<void>();
CarouselState? _state;
set state(CarouselState? state) {
_state = state;
if (!_readyCompleter.isCompleted) {
_readyCompleter.complete();
}
}
void _setModeController() =>
_state!.changeMode(CarouselPageChangedReason.controller);
@override
bool get ready => _state != null;
@override
Future<void> get onReady => _readyCompleter.future;
/// Animates the controlled [CarouselSlider] to the next page.
///
/// The animation lasts for the given duration and follows the given curve.
/// The returned [Future] resolves when the animation completes.
@override
Future<void> nextPage(
{Duration? duration = const Duration(milliseconds: 300),
Curve? curve = Curves.linear}) async {
final bool isNeedResetTimer = _state!.options.pauseAutoPlayOnManualNavigate;
if (isNeedResetTimer) {
_state!.onResetTimer();
}
_setModeController();
await _state!.pageController!.nextPage(duration: duration!, curve: curve!);
if (isNeedResetTimer) {
_state!.onResumeTimer();
}
}
/// Animates the controlled [CarouselSlider] to the previous page.
///
/// The animation lasts for the given duration and follows the given curve.
/// The returned [Future] resolves when the animation completes.
@override
Future<void> previousPage(
{Duration? duration = const Duration(milliseconds: 300),
Curve? curve = Curves.linear}) async {
final bool isNeedResetTimer = _state!.options.pauseAutoPlayOnManualNavigate;
if (isNeedResetTimer) {
_state!.onResetTimer();
}
_setModeController();
await _state!.pageController!
.previousPage(duration: duration!, curve: curve!);
if (isNeedResetTimer) {
_state!.onResumeTimer();
}
}
/// Changes which page is displayed in the controlled [CarouselSlider].
///
/// Jumps the page position from its current value to the given value,
/// without animation, and without checking if the new value is in range.
@override
void jumpToPage(int page) {
final index = getRealIndex(_state!.pageController!.page!.toInt(),
_state!.realPage - _state!.initialPage, _state!.itemCount);
_setModeController();
final int pageToJump = _state!.pageController!.page!.toInt() + page - index;
return _state!.pageController!.jumpToPage(pageToJump);
}
/// Animates the controlled [CarouselSlider] from the current page to the
/// given page.
///
/// The animation lasts for the given duration and follows the given curve.
/// The returned [Future] resolves when the animation completes.
@override
Future<void> animateToPage(int page,
{Duration? duration = const Duration(milliseconds: 300),
Curve? curve = Curves.linear}) async {
final bool isNeedResetTimer = _state!.options.pauseAutoPlayOnManualNavigate;
if (isNeedResetTimer) {
_state!.onResetTimer();
}
final index = getRealIndex(_state!.pageController!.page!.toInt(),
_state!.realPage - _state!.initialPage, _state!.itemCount);
_setModeController();
await _state!.pageController!.animateToPage(
_state!.pageController!.page!.toInt() + page - index,
duration: duration!,
curve: curve!);
if (isNeedResetTimer) {
_state!.onResumeTimer();
}
}
/// Starts the controlled [CarouselSlider] autoplay.
///
/// The carousel will only autoPlay if the [autoPlay] parameter
/// in [CarouselOptions] is true.
@override
void startAutoPlay() {
_state!.onResumeTimer();
}
/// Stops the controlled [CarouselSlider] from autoplaying.
///
/// This is a more on-demand way of doing this. Use the [autoPlay]
/// parameter in [CarouselOptions] to specify the autoPlay behaviour of the
/// carousel.
@override
void stopAutoPlay() {
_state!.onResetTimer();
}
}

View file

@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'carousel_slider.dart';
class CarouselFormField extends FormField<int> {
CarouselFormField({
Key? key,
required FormFieldSetter<int> onSaved,
required FormFieldValidator<int> validator,
int initialValue = 0,
bool autovalidate = false,
required List<Widget> items,
}) : super(
key: key,
onSaved: onSaved,
validator: validator,
initialValue: initialValue,
builder: (FormFieldState<int> state) {
return CarouselSlider(
options: CarouselOptions(
initialPage: initialValue,
onPageChanged: (index, reason) {
state.didChange(index);
},
height: 425,
aspectRatio: 2.0,
enlargeCenterPage: true,
enableInfiniteScroll: false,
),
items: items.map((Widget item) {
return item;
}).toList(),
);
});
}

View file

@ -0,0 +1,216 @@
import 'package:flutter/material.dart';
enum CarouselPageChangedReason { timed, manual, controller }
enum CenterPageEnlargeStrategy { scale, height }
class CarouselOptions {
/// Set carousel height and overrides any existing [aspectRatio].
final double? height;
/// Aspect ratio is used if no height have been declared.
///
/// Defaults to 16:9 aspect ratio.
final double aspectRatio;
/// The fraction of the viewport that each page should occupy.
///
/// Defaults to 0.8, which means each page fills 80% of the carousel.
final double viewportFraction;
/// The initial page to show when first creating the [CarouselSlider].
///
/// Defaults to 0.
final int initialPage;
///Determines if carousel should loop infinitely or be limited to item length.
///
///Defaults to true, i.e. infinite loop.
final bool enableInfiniteScroll;
/// Reverse the order of items if set to true.
///
/// Defaults to false.
final bool reverse;
/// Enables auto play, sliding one page at a time.
///
/// Use [autoPlayInterval] to determine the frequency of slides.
/// Defaults to false.
final bool autoPlay;
/// Sets Duration to determine the frequency of slides when [autoPlay] is set
/// to true.
/// Defaults to 4 seconds.
final Duration autoPlayInterval;
/// The animation duration between two transitioning pages while in auto
/// playback.
///
/// Defaults to 800 ms.
final Duration autoPlayAnimationDuration;
/// Determines the animation curve physics.
///
/// Defaults to [Curves.fastOutSlowIn].
final Curve autoPlayCurve;
/// Determines if current page should be larger than the side images,
/// creating a feeling of depth in the carousel.
///
/// Defaults to false.
final bool? enlargeCenterPage;
/// The axis along which the page view scrolls.
///
/// Defaults to [Axis.horizontal].
final Axis scrollDirection;
/// Called whenever the page in the center of the viewport changes.
final Function(int index, CarouselPageChangedReason reason)? onPageChanged;
/// Called whenever the carousel is scrolled
final ValueChanged<double?>? onScrolled;
/// How the carousel should respond to user input.
///
/// For example, determines how the items continues to animate after the
/// user stops dragging the page view.
///
/// The physics are modified to snap to page boundaries using
/// [PageScrollPhysics] prior to being used.
///
/// Defaults to matching platform conventions.
final ScrollPhysics? scrollPhysics;
/// Set to false to disable page snapping, useful for custom scroll behavior.
///
/// Default to `true`.
final bool pageSnapping;
/// If `true`, the auto play function will be paused when user is interacting
/// with the carousel, and will be resumed when user finish interacting.
///
/// Default to `true`.
final bool pauseAutoPlayOnTouch;
/// If `true`, the auto play function will be paused when user is calling
/// [PageController]'s [nextPage] or [previousPage] or [animateToPage] method.
/// And after the animation complete, the auto play will be resumed.
///
/// Default to `true`.
final bool pauseAutoPlayOnManualNavigate;
/// If [enableInfiniteScroll] is `false`, and [autoPlay] is `true`, this option
/// decide the carousel should go to the first item when it reach the last item or not.
/// If set to `true`, the auto play will be paused when it reach the last item.
/// If set to `false`, the auto play function will animate to the first item
/// when it was in the last item.
final bool pauseAutoPlayInFiniteScroll;
/// Pass a [PageStorageKey] if you want to keep the pageview's position when
/// it was recreated.
final PageStorageKey? pageViewKey;
/// Use [enlargeStrategy] to determine which method to enlarge the center page.
final CenterPageEnlargeStrategy enlargeStrategy;
/// Whether or not to disable the [Center] widget for each slide.
final bool disableCenter;
/// Whether to add padding to both ends of the list.
/// If this is set to true and [viewportFraction] < 1.0, padding will be added
/// such that the first and last child slivers will be in the center of the 1
/// viewport when scrolled all the way to the start or end, respectively.
///
/// If [viewportFraction] >= 1.0, this property has no effect.
/// This property defaults to true and must not be null.
final bool padEnds;
/// Exposed [clipBehavior] of [PageView]
final Clip clipBehavior;
CarouselOptions({
this.height,
this.aspectRatio = 16 / 9,
this.viewportFraction = 0.8,
this.initialPage = 0,
this.enableInfiniteScroll = true,
this.reverse = false,
this.autoPlay = false,
this.autoPlayInterval = const Duration(seconds: 4),
this.autoPlayAnimationDuration = const Duration(milliseconds: 800),
this.autoPlayCurve = Curves.fastOutSlowIn,
this.enlargeCenterPage = false,
this.onPageChanged,
this.onScrolled,
this.scrollPhysics,
this.pageSnapping = true,
this.scrollDirection = Axis.horizontal,
this.pauseAutoPlayOnTouch = true,
this.pauseAutoPlayOnManualNavigate = true,
this.pauseAutoPlayInFiniteScroll = false,
this.pageViewKey,
this.enlargeStrategy = CenterPageEnlargeStrategy.scale,
this.disableCenter = false,
this.padEnds = true,
this.clipBehavior = Clip.hardEdge,
});
///Generate new [CarouselOptions] based on old ones.
CarouselOptions copyWith(
{double? height,
double? aspectRatio,
double? viewportFraction,
int? initialPage,
bool? enableInfiniteScroll,
bool? reverse,
bool? autoPlay,
Duration? autoPlayInterval,
Duration? autoPlayAnimationDuration,
Curve? autoPlayCurve,
bool? enlargeCenterPage,
Function(int index, CarouselPageChangedReason reason)? onPageChanged,
ValueChanged<double?>? onScrolled,
ScrollPhysics? scrollPhysics,
bool? pageSnapping,
Axis? scrollDirection,
bool? pauseAutoPlayOnTouch,
bool? pauseAutoPlayOnManualNavigate,
bool? pauseAutoPlayInFiniteScroll,
PageStorageKey? pageViewKey,
CenterPageEnlargeStrategy? enlargeStrategy,
bool? disableCenter,
Clip? clipBehavior,
bool? padEnds}) =>
CarouselOptions(
height: height ?? this.height,
aspectRatio: aspectRatio ?? this.aspectRatio,
viewportFraction: viewportFraction ?? this.viewportFraction,
initialPage: initialPage ?? this.initialPage,
enableInfiniteScroll: enableInfiniteScroll ?? this.enableInfiniteScroll,
reverse: reverse ?? this.reverse,
autoPlay: autoPlay ?? this.autoPlay,
autoPlayInterval: autoPlayInterval ?? this.autoPlayInterval,
autoPlayAnimationDuration:
autoPlayAnimationDuration ?? this.autoPlayAnimationDuration,
autoPlayCurve: autoPlayCurve ?? this.autoPlayCurve,
enlargeCenterPage: enlargeCenterPage ?? this.enlargeCenterPage,
onPageChanged: onPageChanged ?? this.onPageChanged,
onScrolled: onScrolled ?? this.onScrolled,
scrollPhysics: scrollPhysics ?? this.scrollPhysics,
pageSnapping: pageSnapping ?? this.pageSnapping,
scrollDirection: scrollDirection ?? this.scrollDirection,
pauseAutoPlayOnTouch: pauseAutoPlayOnTouch ?? this.pauseAutoPlayOnTouch,
pauseAutoPlayOnManualNavigate:
pauseAutoPlayOnManualNavigate ?? this.pauseAutoPlayOnManualNavigate,
pauseAutoPlayInFiniteScroll:
pauseAutoPlayInFiniteScroll ?? this.pauseAutoPlayInFiniteScroll,
pageViewKey: pageViewKey ?? this.pageViewKey,
enlargeStrategy: enlargeStrategy ?? this.enlargeStrategy,
disableCenter: disableCenter ?? this.disableCenter,
clipBehavior: clipBehavior ?? this.clipBehavior,
padEnds: padEnds ?? this.padEnds,
);
}

View file

@ -0,0 +1,354 @@
library carousel_slider;
import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'carousel_state.dart';
import 'carousel_utils.dart';
import 'carousel_controller.dart';
import 'carousel_options.dart';
export 'carousel_controller.dart';
export 'carousel_options.dart';
typedef ExtendedIndexedWidgetBuilder = Widget Function(
BuildContext context, int index, int realIndex);
class CarouselSlider extends StatefulWidget {
/// [CarouselOptions] to create a [CarouselState] with.
final CarouselOptions options;
/// The widgets to be shown in the carousel of default constructor.
final List<Widget>? items;
/// The widget item builder that will be used to build item on demand
/// The third argument is the [PageView]'s real index, can be used to cooperate
/// with Hero.
final ExtendedIndexedWidgetBuilder? itemBuilder;
/// A [MapController], used to control the map.
final CarouselControllerImpl _carouselController;
final int? itemCount;
CarouselSlider(
{required this.items,
required this.options,
CarouselController? carouselController,
Key? key})
: itemBuilder = null,
itemCount = items != null ? items.length : 0,
_carouselController = carouselController != null
? carouselController as CarouselControllerImpl
: CarouselController() as CarouselControllerImpl,
super(key: key);
/// The on demand item builder constructor/
CarouselSlider.builder(
{required this.itemCount,
required this.itemBuilder,
required this.options,
CarouselController? carouselController,
Key? key})
: items = null,
_carouselController = carouselController != null
? carouselController as CarouselControllerImpl
: CarouselController() as CarouselControllerImpl,
super(key: key);
@override
CarouselSliderState createState() => CarouselSliderState();
}
class CarouselSliderState extends State<CarouselSlider>
with TickerProviderStateMixin {
late CarouselControllerImpl carouselController;
Timer? timer;
CarouselOptions get options => widget.options;
CarouselState? carouselState;
PageController? pageController;
/// [mode] is related to why the page is being changed.
CarouselPageChangedReason mode = CarouselPageChangedReason.controller;
CarouselSliderState();
void changeMode(CarouselPageChangedReason mode) {
this.mode = mode;
}
@override
void didUpdateWidget(CarouselSlider oldWidget) {
carouselState!.options = options;
carouselState!.itemCount = widget.itemCount;
/// [pageController] needs to be re-initialized to respond to state changes.
pageController = PageController(
viewportFraction: options.viewportFraction,
initialPage: carouselState!.realPage,
);
carouselState!.pageController = pageController;
/// handle autoplay when state changes
handleAutoPlay();
super.didUpdateWidget(oldWidget);
}
@override
void initState() {
super.initState();
carouselController = widget._carouselController;
carouselState = CarouselState(options, clearTimer, resumeTimer, changeMode);
carouselState!.itemCount = widget.itemCount;
carouselController.state = carouselState;
carouselState!.initialPage = widget.options.initialPage;
carouselState!.realPage = options.enableInfiniteScroll
? carouselState!.realPage + carouselState!.initialPage
: carouselState!.initialPage;
handleAutoPlay();
pageController = PageController(
viewportFraction: options.viewportFraction,
initialPage: carouselState!.realPage,
);
carouselState!.pageController = pageController;
}
Timer? getTimer() {
return widget.options.autoPlay
? Timer.periodic(widget.options.autoPlayInterval, (_) {
final route = ModalRoute.of(context);
if (route?.isCurrent == false) {
return;
}
CarouselPageChangedReason previousReason = mode;
changeMode(CarouselPageChangedReason.timed);
int nextPage = carouselState!.pageController!.page!.round() + 1;
int itemCount = widget.itemCount ?? widget.items!.length;
if (nextPage >= itemCount &&
widget.options.enableInfiniteScroll == false) {
if (widget.options.pauseAutoPlayInFiniteScroll) {
clearTimer();
return;
}
nextPage = 0;
}
carouselState!.pageController!
.animateToPage(nextPage,
duration: widget.options.autoPlayAnimationDuration,
curve: widget.options.autoPlayCurve)
.then((_) => changeMode(previousReason));
})
: null;
}
void clearTimer() {
if (timer != null) {
timer?.cancel();
timer = null;
}
}
void resumeTimer() {
timer ??= getTimer();
}
void handleAutoPlay() {
bool autoPlayEnabled = widget.options.autoPlay;
if (autoPlayEnabled && timer != null) return;
clearTimer();
if (autoPlayEnabled) {
resumeTimer();
}
}
Widget getGestureWrapper(Widget child) {
Widget wrapper;
if (widget.options.height != null) {
wrapper = SizedBox(height: widget.options.height, child: child);
} else {
wrapper =
AspectRatio(aspectRatio: widget.options.aspectRatio, child: child);
}
return RawGestureDetector(
gestures: {
_MultipleGestureRecognizer:
GestureRecognizerFactoryWithHandlers<_MultipleGestureRecognizer>(
() => _MultipleGestureRecognizer(),
(_MultipleGestureRecognizer instance) {
instance.onStart = (_) {
onStart();
};
instance.onDown = (_) {
onPanDown();
};
instance.onEnd = (_) {
onPanUp();
};
instance.onCancel = () {
onPanUp();
};
}),
},
child: NotificationListener(
onNotification: (Notification notification) {
if (widget.options.onScrolled != null &&
notification is ScrollUpdateNotification) {
widget.options.onScrolled!(carouselState!.pageController!.page);
}
return false;
},
child: wrapper,
),
);
}
Widget getCenterWrapper(Widget child) {
if (widget.options.disableCenter) {
return Container(
child: child,
);
}
return Center(child: child);
}
Widget getEnlargeWrapper(Widget? child,
{double? width, double? height, double? scale}) {
if (widget.options.enlargeStrategy == CenterPageEnlargeStrategy.height) {
return SizedBox(width: width, height: height, child: child);
}
return Transform.scale(
scale: scale!,
child: SizedBox(width: width, height: height, child: child));
}
void onStart() {
changeMode(CarouselPageChangedReason.manual);
}
void onPanDown() {
if (widget.options.pauseAutoPlayOnTouch) {
clearTimer();
}
changeMode(CarouselPageChangedReason.manual);
}
void onPanUp() {
if (widget.options.pauseAutoPlayOnTouch) {
resumeTimer();
}
}
@override
void dispose() {
super.dispose();
clearTimer();
}
@override
Widget build(BuildContext context) {
return getGestureWrapper(PageView.builder(
padEnds: widget.options.padEnds,
scrollBehavior: ScrollConfiguration.of(context).copyWith(
scrollbars: false,
overscroll: false,
dragDevices: {PointerDeviceKind.touch, PointerDeviceKind.mouse},
),
clipBehavior: widget.options.clipBehavior,
physics: widget.options.scrollPhysics,
scrollDirection: widget.options.scrollDirection,
pageSnapping: widget.options.pageSnapping,
controller: carouselState!.pageController,
reverse: widget.options.reverse,
itemCount: widget.options.enableInfiniteScroll ? null : widget.itemCount,
key: widget.options.pageViewKey,
onPageChanged: (int index) {
int currentPage = getRealIndex(index + carouselState!.initialPage,
carouselState!.realPage, widget.itemCount);
if (widget.options.onPageChanged != null) {
widget.options.onPageChanged!(currentPage, mode);
}
},
itemBuilder: (BuildContext context, int idx) {
final int index = getRealIndex(idx + carouselState!.initialPage,
carouselState!.realPage, widget.itemCount);
return AnimatedBuilder(
animation: carouselState!.pageController!,
child: (widget.items != null)
? (widget.items!.isNotEmpty ? widget.items![index] : Container())
: widget.itemBuilder!(context, index, idx),
builder: (BuildContext context, child) {
double distortionValue = 1.0;
// if [enlargeCenterPage] is true, we must calculate the carousel item's height
// to display the visual effect
if (widget.options.enlargeCenterPage != null &&
widget.options.enlargeCenterPage == true) {
// [pageController.page] can only be accessed after the first build,
// so in the first build we calculate the [itemOffset] manually
double itemOffset = 0;
var position = carouselState?.pageController?.position;
if (position != null &&
position.hasPixels &&
position.hasContentDimensions) {
var page = carouselState?.pageController?.page;
if (page != null) {
itemOffset = page - idx;
}
} else {
BuildContext storageContext = carouselState!
.pageController!.position.context.storageContext;
final double? previousSavedPosition =
PageStorage.of(storageContext)?.readState(storageContext)
as double?;
if (previousSavedPosition != null) {
itemOffset = previousSavedPosition - idx.toDouble();
} else {
itemOffset =
carouselState!.realPage.toDouble() - idx.toDouble();
}
}
final num distortionRatio =
(1 - (itemOffset.abs() * 0.3)).clamp(0.0, 1.0);
distortionValue =
Curves.easeOut.transform(distortionRatio as double);
}
final double height = widget.options.height ??
MediaQuery.of(context).size.width *
(1 / widget.options.aspectRatio);
if (widget.options.scrollDirection == Axis.horizontal) {
return getCenterWrapper(getEnlargeWrapper(child,
height: distortionValue * height, scale: distortionValue));
} else {
return getCenterWrapper(getEnlargeWrapper(child,
width: distortionValue * MediaQuery.of(context).size.width,
scale: distortionValue));
}
},
);
},
));
}
}
class _MultipleGestureRecognizer extends PanGestureRecognizer {}

View file

@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'carousel_slider.dart';
class CarouselState {
/// The [CarouselOptions] to create this state
CarouselOptions options;
/// [pageController] is created using the properties passed to the constructor
/// and can be used to control the [PageView] it is passed to.
PageController? pageController;
/// The actual index of the [PageView].
///
/// This value can be ignored unless you know the carousel will be scrolled
/// backwards more then 10000 pages.
/// Defaults to 10000 to simulate infinite backwards scrolling.
int realPage = 10000;
/// The initial index of the [PageView] on [CarouselSlider] init.
///
int initialPage = 0;
/// The widgets count that should be shown at carousel
int? itemCount;
/// Will be called when using [pageController] to go to next page or
/// previous page. It will clear the autoPlay timer.
/// Internal use only
Function onResetTimer;
/// Will be called when using pageController to go to next page or
/// previous page. It will restart the autoPlay timer.
/// Internal use only
Function onResumeTimer;
/// The callback to set the Reason Carousel changed
Function(CarouselPageChangedReason) changeMode;
CarouselState(
this.options, this.onResetTimer, this.onResumeTimer, this.changeMode);
}

View file

@ -0,0 +1,23 @@
/// Converts an index of a set size to the corresponding index of a collection of another size
/// as if they were circular.
///
/// Takes a [position] from collection Foo, a [base] from where Foo's index originated
/// and the [length] of a second collection Baa, for which the correlating index is sought.
///
/// For example; We have a Carousel of 10000(simulating infinity) but only 6 images.
/// We need to repeat the images to give the illusion of a never ending stream.
/// By calling [getRealIndex] with position and base we get an offset.
/// This offset modulo our length, 6, will return a number between 0 and 5, which represent the image
/// to be placed in the given position.
int getRealIndex(int position, int base, int? length) {
final int offset = position - base;
return remainder(offset, length);
}
/// Returns the remainder of the modulo operation [input] % [source], and adjust it for
/// negative values.
int remainder(int input, int? source) {
if (source == 0) return 0;
final int result = input % source!;
return result < 0 ? source + result : result;
}

View file

@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:flutter_form/utils/translation_service.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_form/flutter_form.dart';
import 'carousel_form.dart';
/// Input for a carousel of items used in a [FlutterForm].
///
/// [items] will be the [Widget]s to be displayed in the carousel.
///
/// Standard controller is [FlutterFormInputCarouselController].
class FlutterFormInputCarousel extends FlutterFormInputWidget {
const FlutterFormInputCarousel({
Key? key,
required FlutterFormInputController controller,
Widget? label,
required this.items,
}) : super(key: key, controller: controller, label: label);
final List<Widget> items;
@override
Widget build(BuildContext context, WidgetRef ref) {
String Function(String, {List<String>? params}) _ =
getTranslator(context, ref);
super.registerController(context);
return CarouselFormField(
onSaved: (value) => controller.onSaved(value),
validator: (value) => controller.onValidate(value, _),
initialValue: controller.value ?? 0,
items: items,
);
}
}
/// Controller for the carousel used by a [FlutterFormInputWidget] used in a [FlutterForm].
///
/// Mainly used by [FlutterFormInputCarousel].
class FlutterFormInputCarouselController
implements FlutterFormInputController<int> {
FlutterFormInputCarouselController({
required this.id,
this.mandatory = true,
this.value,
this.checkPageTitle,
this.checkPageDescription,
});
@override
String? id;
@override
int? value;
@override
bool mandatory;
@override
String Function(int value)? checkPageTitle;
@override
String Function(int value)? checkPageDescription;
@override
void onSaved(int value) {
this.value = value;
}
@override
String? onValidate(
int value, String Function(String, {List<String>? params}) translator) {
if (mandatory) {}
return null;
}
}

View file

@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:flutter_form/utils/translation_service.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../flutter_form.dart';
/// Input for an email used in a [FlutterForm].
///
/// Standard controller is [FlutterFormInputEmailController].
class FlutterFormInputEmail extends FlutterFormInputWidget {
const FlutterFormInputEmail({
Key? key,
required FlutterFormInputController controller,
Widget? label,
}) : super(
key: key,
controller: controller,
label: label,
);
@override
Widget build(BuildContext context, WidgetRef ref) {
String Function(String, {List<String>? params}) _ =
getTranslator(context, ref);
super.registerController(context);
return TextFormField(
initialValue: controller.value,
onSaved: (value) {
controller.onSaved(value);
},
validator: (value) => controller.onValidate(value, _),
decoration: InputDecoration(
focusColor: Theme.of(context).primaryColor,
label: label ?? const Text("Email"),
),
);
}
}
/// Controller for emails used by a [FlutterFormInputWidget] used in a [FlutterForm].
///
/// Mainly used by [FlutterFormInputEmail].
class FlutterFormInputEmailController
implements FlutterFormInputController<String> {
FlutterFormInputEmailController({
required this.id,
this.mandatory = true,
this.value,
this.checkPageTitle,
this.checkPageDescription,
});
@override
String? id;
@override
String? value;
@override
bool mandatory;
@override
String Function(String value)? checkPageTitle;
@override
String Function(String value)? checkPageDescription;
@override
void onSaved(dynamic value) {
this.value = value;
}
@override
String? onValidate(String? value,
String Function(String, {List<String>? params}) translator) {
if (mandatory) {
if (value == null || value.isEmpty) {
return translator('shell.form.error.empty');
}
if (!RegExp(
r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+")
.hasMatch(value)) {
return translator('shell.form.error.email.notValid');
}
}
return null;
}
}

View file

@ -0,0 +1,112 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'numberpicker.dart';
class DecimalNumberPicker extends StatelessWidget {
final int minValue;
final int maxValue;
final double value;
final ValueChanged<double> onChanged;
final int itemCount;
final double itemHeight;
final double itemWidth;
final Axis axis;
final TextStyle? textStyle;
final TextStyle? selectedTextStyle;
final bool haptics;
final TextMapper? integerTextMapper;
final TextMapper? decimalTextMapper;
final bool integerZeroPad;
/// Decoration to apply to central box where the selected integer value is placed
final Decoration? integerDecoration;
/// Decoration to apply to central box where the selected decimal value is placed
final Decoration? decimalDecoration;
/// Inidcates how many decimal places to show
/// e.g. 0=>[1,2,3...], 1=>[1.0, 1.1, 1.2...] 2=>[1.00, 1.01, 1.02...]
final int decimalPlaces;
const DecimalNumberPicker({
Key? key,
required this.minValue,
required this.maxValue,
required this.value,
required this.onChanged,
this.itemCount = 3,
this.itemHeight = 50,
this.itemWidth = 100,
this.axis = Axis.vertical,
this.textStyle,
this.selectedTextStyle,
this.haptics = false,
this.decimalPlaces = 1,
this.integerTextMapper,
this.decimalTextMapper,
this.integerZeroPad = false,
this.integerDecoration,
this.decimalDecoration,
}) : assert(minValue <= value),
assert(value <= maxValue),
super(key: key);
@override
Widget build(BuildContext context) {
final isMax = value.floor() == maxValue;
final decimalValue = isMax
? 0
: ((value - value.floorToDouble()) * math.pow(10, decimalPlaces))
.round();
final doubleMaxValue = isMax ? 0 : math.pow(10, decimalPlaces).toInt() - 1;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
NumberPicker(
minValue: minValue,
maxValue: maxValue,
value: value.floor(),
onChanged: _onIntChanged,
itemCount: itemCount,
itemHeight: itemHeight,
itemWidth: itemWidth,
textStyle: textStyle,
selectedTextStyle: selectedTextStyle,
haptics: haptics,
zeroPad: integerZeroPad,
textMapper: integerTextMapper,
decoration: integerDecoration,
),
NumberPicker(
minValue: 0,
maxValue: doubleMaxValue,
value: decimalValue,
onChanged: _onDoubleChanged,
itemCount: itemCount,
itemHeight: itemHeight,
itemWidth: itemWidth,
textStyle: textStyle,
selectedTextStyle: selectedTextStyle,
haptics: haptics,
textMapper: decimalTextMapper,
decoration: decimalDecoration,
),
],
);
}
void _onIntChanged(int intValue) {
final newValue =
(value - value.floor() + intValue).clamp(minValue, maxValue);
onChanged(newValue.toDouble());
}
void _onDoubleChanged(int doubleValue) {
final decimalPart = double.parse(
(doubleValue * math.pow(10, -decimalPlaces))
.toStringAsFixed(decimalPlaces));
onChanged(value.floor() + decimalPart);
}
}

View file

@ -0,0 +1,362 @@
library infinite_listview;
import 'dart:math' as math;
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
/// Infinite ListView
///
/// ListView that builds its children with to an infinite extent.
///
class InfiniteListView extends StatefulWidget {
/// See [ListView.builder]
const InfiniteListView.builder({
Key? key,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.controller,
this.physics,
this.padding,
this.itemExtent,
required this.itemBuilder,
this.itemCount,
this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true,
this.addSemanticIndexes = true,
this.cacheExtent,
this.anchor = 0.0,
this.dragStartBehavior = DragStartBehavior.start,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
}) : separatorBuilder = null,
super(key: key);
/// See [ListView.separated]
const InfiniteListView.separated({
Key? key,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.controller,
this.physics,
this.padding,
required this.itemBuilder,
required this.separatorBuilder,
this.itemCount,
this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true,
this.addSemanticIndexes = true,
this.cacheExtent,
this.anchor = 0.0,
this.dragStartBehavior = DragStartBehavior.start,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
}) : itemExtent = null,
super(key: key);
/// See: [ScrollView.scrollDirection]
final Axis scrollDirection;
/// See: [ScrollView.reverse]
final bool reverse;
/// See: [ScrollView.controller]
final InfiniteScrollController? controller;
/// See: [ScrollView.physics]
final ScrollPhysics? physics;
/// See: [BoxScrollView.padding]
final EdgeInsets? padding;
/// See: [ListView.builder]
final IndexedWidgetBuilder itemBuilder;
/// See: [ListView.separated]
final IndexedWidgetBuilder? separatorBuilder;
/// See: [SliverChildBuilderDelegate.childCount]
final int? itemCount;
/// See: [ListView.itemExtent]
final double? itemExtent;
/// See: [ScrollView.cacheExtent]
final double? cacheExtent;
/// See: [ScrollView.anchor]
final double anchor;
/// See: [SliverChildBuilderDelegate.addAutomaticKeepAlives]
final bool addAutomaticKeepAlives;
/// See: [SliverChildBuilderDelegate.addRepaintBoundaries]
final bool addRepaintBoundaries;
/// See: [SliverChildBuilderDelegate.addSemanticIndexes]
final bool addSemanticIndexes;
/// See: [ScrollView.dragStartBehavior]
final DragStartBehavior dragStartBehavior;
/// See: [ScrollView.keyboardDismissBehavior]
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
/// See: [ScrollView.restorationId]
final String? restorationId;
/// See: [ScrollView.clipBehavior]
final Clip clipBehavior;
@override
InfiniteListViewState createState() => InfiniteListViewState();
}
class InfiniteListViewState extends State<InfiniteListView> {
InfiniteScrollController? _controller;
InfiniteScrollController get _effectiveController =>
widget.controller ?? _controller!;
@override
void initState() {
super.initState();
if (widget.controller == null) {
_controller = InfiniteScrollController();
}
}
@override
void didUpdateWidget(InfiniteListView oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller == null && oldWidget.controller != null) {
_controller = InfiniteScrollController();
} else if (widget.controller != null && oldWidget.controller == null) {
_controller!.dispose();
_controller = null;
}
}
@override
void dispose() {
_controller?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final List<Widget> slivers = _buildSlivers(context, negative: false);
final List<Widget> negativeSlivers = _buildSlivers(context, negative: true);
final AxisDirection axisDirection = _getDirection(context);
final scrollPhysics =
widget.physics ?? const AlwaysScrollableScrollPhysics();
return Scrollable(
axisDirection: axisDirection,
controller: _effectiveController,
physics: scrollPhysics,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return Builder(builder: (BuildContext context) {
/// Build negative [ScrollPosition] for the negative scrolling [Viewport].
final state = Scrollable.of(context)!;
final negativeOffset = _InfiniteScrollPosition(
physics: scrollPhysics,
context: state,
initialPixels: -offset.pixels,
keepScrollOffset: _effectiveController.keepScrollOffset,
negativeScroll: true,
);
/// Keep the negative scrolling [Viewport] positioned to the [ScrollPosition].
offset.addListener(() {
negativeOffset._forceNegativePixels(offset.pixels);
});
/// Stack the two [Viewport]s on top of each other so they move in sync.
return Stack(
children: <Widget>[
Viewport(
axisDirection: flipAxisDirection(axisDirection),
anchor: 1.0 - widget.anchor,
offset: negativeOffset,
slivers: negativeSlivers,
cacheExtent: widget.cacheExtent,
),
Viewport(
axisDirection: axisDirection,
anchor: widget.anchor,
offset: offset,
slivers: slivers,
cacheExtent: widget.cacheExtent,
),
],
);
});
},
);
}
AxisDirection _getDirection(BuildContext context) {
return getAxisDirectionFromAxisReverseAndDirectionality(
context, widget.scrollDirection, widget.reverse);
}
List<Widget> _buildSlivers(BuildContext context, {bool negative = false}) {
final itemExtent = widget.itemExtent;
final padding = widget.padding ?? EdgeInsets.zero;
return <Widget>[
SliverPadding(
padding: negative
? padding - EdgeInsets.only(bottom: padding.bottom)
: padding - EdgeInsets.only(top: padding.top),
sliver: (itemExtent != null)
? SliverFixedExtentList(
delegate: negative
? negativeChildrenDelegate
: positiveChildrenDelegate,
itemExtent: itemExtent,
)
: SliverList(
delegate: negative
? negativeChildrenDelegate
: positiveChildrenDelegate,
),
)
];
}
SliverChildDelegate get negativeChildrenDelegate {
return SliverChildBuilderDelegate(
(BuildContext context, int index) {
final separatorBuilder = widget.separatorBuilder;
if (separatorBuilder != null) {
final itemIndex = (-1 - index) ~/ 2;
return index.isOdd
? widget.itemBuilder(context, itemIndex)
: separatorBuilder(context, itemIndex);
} else {
return widget.itemBuilder(context, -1 - index);
}
},
childCount: widget.itemCount,
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
addRepaintBoundaries: widget.addRepaintBoundaries,
);
}
SliverChildDelegate get positiveChildrenDelegate {
final separatorBuilder = widget.separatorBuilder;
final itemCount = widget.itemCount;
return SliverChildBuilderDelegate(
(separatorBuilder != null)
? (BuildContext context, int index) {
final itemIndex = index ~/ 2;
return index.isEven
? widget.itemBuilder(context, itemIndex)
: separatorBuilder(context, itemIndex);
}
: widget.itemBuilder,
childCount: separatorBuilder == null
? itemCount
: (itemCount != null ? math.max(0, itemCount * 2 - 1) : null),
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
addRepaintBoundaries: widget.addRepaintBoundaries,
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
.add(EnumProperty<Axis>('scrollDirection', widget.scrollDirection));
properties.add(FlagProperty('reverse',
value: widget.reverse, ifTrue: 'reversed', showName: true));
properties.add(DiagnosticsProperty<ScrollController>(
'controller', widget.controller,
showName: false, defaultValue: null));
properties.add(DiagnosticsProperty<ScrollPhysics>('physics', widget.physics,
showName: false, defaultValue: null));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>(
'padding', widget.padding,
defaultValue: null));
properties.add(
DoubleProperty('itemExtent', widget.itemExtent, defaultValue: null));
properties.add(
DoubleProperty('cacheExtent', widget.cacheExtent, defaultValue: null));
}
}
/// Same as a [ScrollController] except it provides [ScrollPosition] objects with infinite bounds.
class InfiniteScrollController extends ScrollController {
/// Creates a new [InfiniteScrollController]
InfiniteScrollController({
double initialScrollOffset = 0.0,
bool keepScrollOffset = true,
String? debugLabel,
}) : super(
initialScrollOffset: initialScrollOffset,
keepScrollOffset: keepScrollOffset,
debugLabel: debugLabel,
);
@override
ScrollPosition createScrollPosition(ScrollPhysics physics,
ScrollContext context, ScrollPosition? oldPosition) {
return _InfiniteScrollPosition(
physics: physics,
context: context,
initialPixels: initialScrollOffset,
keepScrollOffset: keepScrollOffset,
oldPosition: oldPosition,
debugLabel: debugLabel,
);
}
}
class _InfiniteScrollPosition extends ScrollPositionWithSingleContext {
_InfiniteScrollPosition({
required ScrollPhysics physics,
required ScrollContext context,
double? initialPixels = 0.0,
bool keepScrollOffset = true,
ScrollPosition? oldPosition,
String? debugLabel,
this.negativeScroll = false,
}) : super(
physics: physics,
context: context,
initialPixels: initialPixels,
keepScrollOffset: keepScrollOffset,
oldPosition: oldPosition,
debugLabel: debugLabel,
);
final bool negativeScroll;
void _forceNegativePixels(double value) {
super.forcePixels(-value);
}
@override
void saveScrollOffset() {
if (!negativeScroll) {
super.saveScrollOffset();
}
}
@override
void restoreScrollOffset() {
if (!negativeScroll) {
super.restoreScrollOffset();
}
}
@override
double get minScrollExtent => double.negativeInfinity;
@override
double get maxScrollExtent => double.infinity;
}

View file

@ -0,0 +1,114 @@
import 'package:flutter/material.dart';
import 'package:flutter_form/utils/translation_service.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../../flutter_form.dart';
import 'numberpicker.dart';
/// Input for a number used in a [FlutterForm].
///
/// [minValue] sets the minimal value of the picker.
/// [maxValue] sets the maximal value of the picker.
///
/// Standard controller is [FlutterFormInputNumberPickerController].
class FlutterFormInputNumberPicker extends FlutterFormInputWidget {
const FlutterFormInputNumberPicker({
Key? key,
required FlutterFormInputController controller,
Widget? label,
this.minValue = 0,
this.maxValue = 100,
}) : assert(minValue < maxValue),
super(key: key, controller: controller, label: label);
final int minValue;
final int maxValue;
@override
Widget build(BuildContext context, WidgetRef ref) {
String Function(String, {List<String>? params}) _ =
getTranslator(context, ref);
super.registerController(context);
return NumberPickerFormField(
minValue: minValue,
maxValue: maxValue,
onSaved: (value) {
controller.onSaved(value);
},
validator: (value) => controller.onValidate(value, _),
initialValue: controller.value ?? minValue,
);
}
}
/// Controller for the numberPicker used by a [FlutterFormInputWidget] used in a [FlutterForm].
///
/// Mainly used by [FlutterFormInputNumberPicker].
class NumberPickerFormField extends FormField<int> {
NumberPickerFormField({
Key? key,
required FormFieldSetter<int> onSaved,
required FormFieldValidator<int> validator,
int initialValue = 0,
bool autovalidate = false,
int minValue = 0,
int maxValue = 100,
}) : super(
key: key,
onSaved: onSaved,
validator: validator,
initialValue: initialValue,
builder: (FormFieldState<int> state) {
return NumberPicker(
minValue: minValue,
maxValue: maxValue,
value: initialValue,
onChanged: (int value) {
state.didChange(value);
},
itemHeight: 35,
itemCount: 5,
);
});
}
class FlutterFormInputNumberPickerController
implements FlutterFormInputController<int> {
FlutterFormInputNumberPickerController({
required this.id,
this.mandatory = true,
this.value,
this.checkPageTitle,
this.checkPageDescription,
});
@override
String? id;
@override
int? value;
@override
bool mandatory;
@override
String Function(int value)? checkPageTitle;
@override
String Function(int value)? checkPageDescription;
@override
void onSaved(int value) {
this.value = value;
}
@override
String? onValidate(
int value, String Function(String, {List<String>? params}) translator) {
if (mandatory) {}
return null;
}
}

View file

@ -0,0 +1,305 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'infinite_listview.dart';
typedef TextMapper = String Function(String numberText);
class NumberPicker extends StatefulWidget {
/// Min value user can pick
final int minValue;
/// Max value user can pick
final int maxValue;
/// Currently selected value
final int value;
/// Called when selected value changes
final ValueChanged<int> onChanged;
/// Specifies how many items should be shown - defaults to 3
final int itemCount;
/// Step between elements. Only for integer datePicker
/// Examples:
/// if step is 100 the following elements may be 100, 200, 300...
/// if min=0, max=6, step=3, then items will be 0, 3 and 6
/// if min=0, max=5, step=3, then items will be 0 and 3.
final int step;
/// height of single item in pixels
final double itemHeight;
/// width of single item in pixels
final double itemWidth;
/// Direction of scrolling
final Axis axis;
/// Style of non-selected numbers. If null, it uses Theme's bodyText2
final TextStyle? textStyle;
/// Style of non-selected numbers. If null, it uses Theme's headline5 with accentColor
final TextStyle? selectedTextStyle;
/// Whether to trigger haptic pulses or not
final bool haptics;
/// Build the text of each item on the picker
final TextMapper? textMapper;
/// Pads displayed integer values up to the length of maxValue
final bool zeroPad;
/// Decoration to apply to central box where the selected value is placed
final Decoration? decoration;
final bool infiniteLoop;
const NumberPicker({
Key? key,
required this.minValue,
required this.maxValue,
required this.value,
required this.onChanged,
this.itemCount = 3,
this.step = 1,
this.itemHeight = 50,
this.itemWidth = 100,
this.axis = Axis.vertical,
this.textStyle,
this.selectedTextStyle,
this.haptics = false,
this.decoration,
this.zeroPad = false,
this.textMapper,
this.infiniteLoop = false,
}) : assert(minValue <= value),
assert(value <= maxValue),
super(key: key);
@override
NumberPickerState createState() => NumberPickerState();
}
class NumberPickerState extends State<NumberPicker> {
late ScrollController _scrollController;
late int value;
@override
void initState() {
super.initState();
value = widget.value;
final initialOffset = (value - widget.minValue) ~/ widget.step * itemExtent;
if (widget.infiniteLoop) {
_scrollController =
InfiniteScrollController(initialScrollOffset: initialOffset);
} else {
_scrollController = ScrollController(initialScrollOffset: initialOffset);
}
_scrollController.addListener(_scrollListener);
}
void _scrollListener() {
var indexOfMiddleElement = (_scrollController.offset / itemExtent).round();
if (widget.infiniteLoop) {
indexOfMiddleElement %= itemCount;
} else {
indexOfMiddleElement = indexOfMiddleElement.clamp(0, itemCount - 1);
}
final intValueInTheMiddle =
_intValueFromIndex(indexOfMiddleElement + additionalItemsOnEachSide);
if (value != intValueInTheMiddle) {
setState(() {
value = intValueInTheMiddle;
});
widget.onChanged(intValueInTheMiddle);
if (widget.haptics) {
HapticFeedback.selectionClick();
}
}
Future.delayed(
const Duration(milliseconds: 100),
() => _maybeCenterValue(),
);
}
@override
void didUpdateWidget(NumberPicker oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.value != value) {
_maybeCenterValue();
}
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
bool get isScrolling => _scrollController.position.isScrollingNotifier.value;
double get itemExtent =>
widget.axis == Axis.vertical ? widget.itemHeight : widget.itemWidth;
int get itemCount => (widget.maxValue - widget.minValue) ~/ widget.step + 1;
int get listItemsCount => itemCount + 2 * additionalItemsOnEachSide;
int get additionalItemsOnEachSide => (widget.itemCount - 1) ~/ 2;
@override
Widget build(BuildContext context) {
return SizedBox(
width: widget.axis == Axis.vertical
? widget.itemWidth
: widget.itemCount * widget.itemWidth,
height: widget.axis == Axis.vertical
? widget.itemCount * widget.itemHeight
: widget.itemHeight,
child: NotificationListener<ScrollEndNotification>(
onNotification: (not) {
if (not.dragDetails?.primaryVelocity == 0) {
Future.microtask(() => _maybeCenterValue());
}
return true;
},
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
child: Stack(
children: [
Center(
child: Container(
width: 300,
height: 45,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
color: const Color(0xFFD8D8D8).withOpacity(0.50),
),
),
),
if (widget.infiniteLoop)
InfiniteListView.builder(
scrollDirection: widget.axis,
controller: _scrollController as InfiniteScrollController,
itemExtent: itemExtent,
itemBuilder: _itemBuilder,
padding: EdgeInsets.zero,
)
else
ListView.builder(
itemCount: listItemsCount,
scrollDirection: widget.axis,
controller: _scrollController,
itemExtent: itemExtent,
itemBuilder: _itemBuilder,
padding: EdgeInsets.zero,
),
_NumberPickerSelectedItemDecoration(
axis: widget.axis,
itemExtent: itemExtent,
decoration: widget.decoration,
),
],
),
),
),
);
}
Widget _itemBuilder(BuildContext context, int index) {
final themeData = Theme.of(context);
final defaultStyle = widget.textStyle ?? themeData.textTheme.bodyText2;
final selectedStyle = widget.selectedTextStyle ??
themeData.textTheme.headline5
?.copyWith(color: themeData.highlightColor);
final valueFromIndex = _intValueFromIndex(index % itemCount);
final isExtra = !widget.infiniteLoop &&
(index < additionalItemsOnEachSide ||
index >= listItemsCount - additionalItemsOnEachSide);
final itemStyle = valueFromIndex == value ? selectedStyle : defaultStyle;
final child = isExtra
? const SizedBox.shrink()
: Text(
_getDisplayedValue(valueFromIndex),
style: itemStyle,
);
return Container(
width: widget.itemWidth,
height: widget.itemHeight,
alignment: Alignment.center,
child: child,
);
}
String _getDisplayedValue(int value) {
final text = widget.zeroPad
? value.toString().padLeft(widget.maxValue.toString().length, '0')
: value.toString();
if (widget.textMapper != null) {
return widget.textMapper!(text);
} else {
return text;
}
}
int _intValueFromIndex(int index) {
index -= additionalItemsOnEachSide;
index %= itemCount;
return widget.minValue + index * widget.step;
}
void _maybeCenterValue() {
if (_scrollController.hasClients && !isScrolling) {
int diff = value - widget.minValue;
int index = diff ~/ widget.step;
if (widget.infiniteLoop) {
final offset = _scrollController.offset + 0.5 * itemExtent;
final cycles = (offset / (itemCount * itemExtent)).floor();
index += cycles * itemCount;
}
_scrollController.animateTo(
index * itemExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
);
}
}
}
class _NumberPickerSelectedItemDecoration extends StatelessWidget {
final Axis axis;
final double itemExtent;
final Decoration? decoration;
const _NumberPickerSelectedItemDecoration({
Key? key,
required this.axis,
required this.itemExtent,
required this.decoration,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: IgnorePointer(
child: Container(
width: isVertical ? double.infinity : itemExtent,
height: isVertical ? itemExtent : double.infinity,
decoration: decoration,
),
),
);
}
bool get isVertical => axis == Axis.vertical;
}

View file

@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:flutter_form/src/widgets/input/input_types/input_password/password.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../../flutter_form.dart';
/// Input for a password used in a [FlutterForm].
///
/// Standard controller is [FlutterFormInputEmailController].
class FlutterFormInputPassword extends FlutterFormInputWidget {
const FlutterFormInputPassword({
Key? key,
required FlutterFormInputController controller,
Widget? label,
}) : super(key: key, controller: controller, label: label);
@override
Widget build(BuildContext context, WidgetRef ref) {
super.registerController(context);
return PasswordTextField(
label: label,
controller: controller,
);
}
}
/// Controller for passwords used by a [FlutterFormInputWidget] used in a [ShellFrom].
///
/// Mainly used by [FlutterFormInputPassword].
class FlutterFormInputPasswordController
implements FlutterFormInputController<String> {
FlutterFormInputPasswordController({
required this.id,
this.mandatory = true,
this.value,
this.checkPageTitle,
this.checkPageDescription,
});
@override
String? id;
@override
String? value;
@override
bool mandatory;
@override
String Function(String value)? checkPageTitle;
@override
String Function(String value)? checkPageDescription;
@override
void onSaved(dynamic value) {
this.value = value;
}
@override
String? onValidate(String? value,
String Function(String, {List<String>? params}) translator) {
if (mandatory) {
if (value == null || value.isEmpty) {
return translator('Field can not be empty');
}
if (value.length < 6) {
return translator('Field should be atleast 6 characters long');
}
}
return null;
}
}

View file

@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:flutter_form/utils/translation_service.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../../flutter_form.dart';
/// Generates a [TextFormField] for passwords. It requires a [FlutterFormInputController]
/// as the [controller] parameter and an optional [Widget] as [label]
class PasswordTextField extends ConsumerStatefulWidget {
final Widget? label;
final FlutterFormInputController controller;
const PasswordTextField({
Key? key,
required this.controller,
this.label,
}) : super(key: key);
@override
ConsumerState<PasswordTextField> createState() => _PasswordTextFieldState();
}
class _PasswordTextFieldState extends ConsumerState<PasswordTextField> {
bool obscured = true;
@override
Widget build(BuildContext context) {
String Function(String, {List<String>? params}) _ =
getTranslator(context, ref);
return TextFormField(
initialValue: widget.controller.value,
obscureText: obscured,
onSaved: (value) => widget.controller.onSaved(value),
validator: (value) => widget.controller.onValidate(value, _),
decoration: InputDecoration(
label: widget.label ?? const Text("Password"),
suffixIcon: IconButton(
onPressed: () {
setState(() {
obscured = !obscured;
});
},
icon: Icon(obscured ? Icons.visibility_off : Icons.visibility),
),
),
);
}
}

View file

@ -0,0 +1,120 @@
import 'package:flutter/material.dart';
import 'package:flutter_form/utils/translation_service.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../flutter_form.dart';
/// Input for plain text input used in a [FlutterForm].
///
/// Standard controller is [FlutterFormInputPlainTextController].
class FlutterFormInputPlainText extends FlutterFormInputWidget {
const FlutterFormInputPlainText({
Key? key,
required FlutterFormInputController controller,
Widget? label,
}) : super(key: key, controller: controller, label: label);
@override
Widget build(BuildContext context, WidgetRef ref) {
String Function(String, {List<String>? params}) _ =
getTranslator(context, ref);
super.registerController(context);
return TextFormField(
initialValue: controller.value,
onSaved: (value) => controller.onSaved(value),
validator: (value) => controller.onValidate(value, _),
decoration: InputDecoration(
label: label ?? const Text("Plain text"),
),
);
}
}
/// Input for an plain text with extra styling used in a [FlutterForm].
///
/// Standard controller is [FlutterFormInputPlainTextController].
class FlutterFormInputPlainTextWhiteWithBorder extends FlutterFormInputWidget {
const FlutterFormInputPlainTextWhiteWithBorder({
Key? key,
required FlutterFormInputController controller,
Widget? label,
this.hint,
}) : super(key: key, controller: controller, label: label);
final String? hint;
@override
Widget build(BuildContext context, WidgetRef ref) {
String Function(String, {List<String>? params}) _ =
getTranslator(context, ref);
super.registerController(context);
return TextFormField(
initialValue: controller.value,
onSaved: (value) => controller.onSaved(value),
validator: (value) => controller.onValidate(value, _),
decoration: InputDecoration(
hintText: hint,
floatingLabelBehavior: FloatingLabelBehavior.never,
isDense: true,
border: const OutlineInputBorder(
borderSide: BorderSide(color: Color(0xFF979797)),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Color(0xFF979797)),
),
fillColor: Colors.white,
filled: true,
),
);
}
}
/// Controller for plain text used by a [FlutterFormInputWidget] used in a [FlutterForm].
///
/// Mainly used by [FlutterFormInputPlainText].
class FlutterFormInputPlainTextController
implements FlutterFormInputController<String> {
FlutterFormInputPlainTextController({
required this.id,
this.mandatory = false,
this.value,
this.checkPageTitle,
this.checkPageDescription,
});
@override
String? id;
@override
String? value;
@override
bool mandatory;
@override
String Function(String value)? checkPageTitle;
@override
String Function(String value)? checkPageDescription;
@override
void onSaved(String value) {
this.value = value;
}
@override
String? onValidate(String? value,
String Function(String, {List<String>? params}) translator) {
if (mandatory) {
if (value == null || value.isEmpty) {
return translator('Field can not be empty');
}
}
return null;
}
}

View file

@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:flutter_form/src/widgets/input/input_types/input_slider/slider.dart';
import 'package:flutter_form/utils/translation_service.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../../flutter_form.dart';
/// Input for a number value between two values via a slider. Used in a [FlutterForm].
///
/// Standard controller is [FlutterFormInputSliderController].
class FlutterFormInputSlider extends FlutterFormInputWidget {
const FlutterFormInputSlider({
Key? key,
required FlutterFormInputController controller,
Widget? label,
this.minValue = 0,
this.maxValue = 100,
}) : assert(minValue < maxValue),
super(key: key, controller: controller, label: label);
final int minValue;
final int maxValue;
@override
Widget build(BuildContext context, WidgetRef ref) {
String Function(String, {List<String>? params}) _ =
getTranslator(context, ref);
super.registerController(context);
return SliderFormField(
onSaved: (value) => controller.onSaved(value),
validator: (value) => controller.onValidate(value, _),
);
}
}
/// Controller for slider used by a [FlutterFormInputWidget] used in a [FlutterForm].
///
/// Mainly used by [FlutterFormInputSlider].
class FlutterFormInputSliderController
implements FlutterFormInputController<double> {
FlutterFormInputSliderController({
required this.id,
this.mandatory = true,
this.value,
this.checkPageTitle,
this.checkPageDescription,
});
@override
String? id;
@override
double? value;
@override
bool mandatory;
@override
String Function(double value)? checkPageTitle;
@override
String Function(double value)? checkPageDescription;
@override
void onSaved(double value) {
this.value = value;
}
@override
String? onValidate(double value,
String Function(String, {List<String>? params}) translator) {
if (mandatory) {}
return null;
}
}

View file

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
/// Creates a slider with the given input parameters
class SliderFormField extends FormField<double> {
SliderFormField({
Key? key,
required FormFieldSetter<double> onSaved,
required FormFieldValidator<double> validator,
double initialValue = 0.5,
}) : super(
key: key,
onSaved: onSaved,
validator: validator,
initialValue: initialValue,
builder: (FormFieldState<double> state) {
return Slider(
value: state.value ?? initialValue,
onChanged: (double value) {
state.didChange(value);
},
);
});
}

View file

@ -0,0 +1,6 @@
export 'input_carousel/input_carousel.dart';
export 'input_email.dart';
export 'input_number_picker/input_number_picker.dart';
export 'input_password/input_password.dart';
export 'input_plain_text.dart';
export 'input_slider/input_slider.dart';

72
lib/utils/form.dart Normal file
View file

@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
/// The options used to set parameters to a [FlutterForm].
///
/// The pages determine what pages the pageview will contain via a [List] of [FlutterFormPage]s.
///
/// Using a checkpage gives the ability for the user to check all input values before commiting by [CheckPage].
/// If [checkPage] is null no check page will be shown.
///
/// [nextButton] and [backButton] are both a way to give controls to user.
/// Both are just plain widgets used in a [Stack]. So the widgets can be aligned where ever.
/// The formcontroller of [FlutterForm] should be used to give control to the widgets/buttons.
///
/// [onFinished] and [onNext] are both callbacks which give the users results.
/// [onNext] is called when the user goes to the next page.
/// [onFinished] is called when the form is finished. When checkpage is set [onFinished] is called when the checkpage is finished.
class FlutterFormOptions {
final List<FlutterFormPage> pages;
final CheckPage? checkPage;
final Widget Function(int pageNumber, bool checkingPages)? nextButton;
final Widget Function(int pageNumber, bool checkingPages, int pageAmount)?
backButton;
final void Function(Map<int, Map<String, dynamic>>) onFinished;
final void Function(int pageNumber, Map<String, dynamic>) onNext;
const FlutterFormOptions({
required this.pages,
this.checkPage,
this.nextButton,
this.backButton,
required this.onFinished,
required this.onNext,
});
}
/// The defines every page in a [FlutterForm].
class FlutterFormPage {
final Widget child;
FlutterFormPage({
required this.child,
});
}
/// [CheckPage] is used to set a check page at the end of a [FlutterForm].
/// A [CheckPage] is a page where the user can check all input values before commiting.
///
/// [title] is the widget shown at the top of the page.
///
/// [mainAxisAlignment] is the alignment of the check widgets.
///
/// [inputCheckWidget] determines how every input is represented on the page.
/// [title] is the value given in the input.
/// This input can be modified by setting the [checkPageTitle] of that input controller.
///
/// Same for the [description] but if the description is not set in the input controller no description will be given.
///
/// [onPressed] can be set so that when the user triggers it the user will be sent back to the page including the input.
/// Here the user can modify the input and save it. Afterwards the user will be sent back to the check page.
class CheckPage {
final Widget? title;
final MainAxisAlignment mainAxisAlignment;
final Widget Function(String title, String? description, Function onPressed)?
inputCheckWidget;
const CheckPage({
this.title,
this.inputCheckWidget,
this.mainAxisAlignment = MainAxisAlignment.start,
});
}

6
lib/utils/providers.dart Normal file
View file

@ -0,0 +1,6 @@
import 'package:flutter_form/utils/translation_service.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
/// Provides the [ShellTranslationService]
final translationServiceProvider =
Provider<TranslationService>((ref) => ShellTranslationService());

View file

@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:flutter_form/utils/providers.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
abstract class TranslationService {
TranslationService._();
String translate(
BuildContext context,
String key, {
List<String>? params,
});
String number(double value);
}
typedef Translator = String Function(
String, {
List<String>? params,
});
class ShellTranslationService implements TranslationService {
@override
String number(double value) {
return value.toStringAsFixed(2);
}
@override
String translate(BuildContext context, String key, {List<String>? params}) {
return key;
}
}
Translator getTranslator(BuildContext context, WidgetRef ref) {
try {
var translator = ref.read(translationServiceProvider).translate;
return (
String key, {
List<String>? params,
}) {
return translator(context, key, params: params);
};
} catch (e) {
return (
String key, {
List<String>? params,
}) {
return key;
};
}
}

View file

@ -3,6 +3,8 @@ description: A new Flutter package project.
version: 0.0.1 version: 0.0.1
homepage: homepage:
publish_to: none
environment: environment:
sdk: '>=2.18.0 <3.0.0' sdk: '>=2.18.0 <3.0.0'
flutter: ">=1.17.0" flutter: ">=1.17.0"
@ -10,16 +12,19 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_localizations:
sdk: flutter
flutter_riverpod: ^1.0.4
localization: ^2.1.0
sliding_up_panel: ^2.0.0+1
uuid: ^3.0.6
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^2.0.0 flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter: flutter:
# To add assets to your package, add an assets section, like this: # To add assets to your package, add an assets section, like this:

View file

@ -1,12 +1,278 @@
import 'package:flutter/material.dart';
import 'package:flutter_form/flutter_form.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_form/flutter_form.dart';
void main() { void main() {
test('adds one to input values', () { testWidgets('Normal walk through without check page', (tester) async {
final calculator = Calculator(); FlutterFormController formController = FlutterFormController();
expect(calculator.addOne(2), 3);
expect(calculator.addOne(-7), -6); var testField1Controller = FlutterFormInputPlainTextController(
expect(calculator.addOne(0), 1); id: 'Field1',
);
var testField2Controller = FlutterFormInputPlainTextController(
id: 'Field2',
);
int? onNextPageNumber;
Map<String, dynamic>? onNextResults;
Map<int, Map<String, dynamic>>? onFinishResults;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: FlutterForm(
options: FlutterFormOptions(
nextButton: (pageNumber, checkingPages) {
return Align(
alignment: Alignment.bottomCenter,
child: ElevatedButton(
onPressed: () async {
await formController.autoNextStep();
},
child: Text(pageNumber == 0
? 'next1'
: pageNumber == 1
? 'next2'
: 'finish'),
),
);
},
onFinished: (Map<int, Map<String, dynamic>> results) {
onFinishResults = results;
},
onNext: (int pageNumber, Map<String, dynamic> results) {
onNextPageNumber = pageNumber;
onNextResults = results;
},
pages: [
FlutterFormPage(
child: Center(
child: FlutterFormInputPlainText(
label: const Text('Field1Label'),
controller: testField1Controller,
),
),
),
FlutterFormPage(
child: Center(
child: FlutterFormInputPlainText(
label: const Text('Field2Label'),
controller: testField2Controller,
),
),
),
],
),
formController: formController,
),
),
),
);
await tester.enterText(
find.widgetWithText(TextFormField, 'Field1Label'), 'Field1Input');
await tester.tap(find.widgetWithText(ElevatedButton, 'next1'));
await tester.pumpAndSettle();
expect(0, onNextPageNumber);
expect({'Field1': 'Field1Input'}, onNextResults);
await tester.enterText(
find.widgetWithText(TextFormField, 'Field2Label'), 'Field2Input');
await tester.tap(find.widgetWithText(ElevatedButton, 'next2'));
await tester.pumpAndSettle();
expect(1, onNextPageNumber);
expect({'Field2': 'Field2Input'}, onNextResults);
expect({
0: {'Field1': 'Field1Input'},
1: {'Field2': 'Field2Input'}
}, onFinishResults);
});
testWidgets('Normal walk through with check page', (tester) async {
FlutterFormController formController = FlutterFormController();
var testField1Controller = FlutterFormInputPlainTextController(
id: 'Field1',
);
var testField2Controller = FlutterFormInputPlainTextController(
id: 'Field2',
);
int? onNextPageNumber;
Map<String, dynamic>? onNextResults;
Map<int, Map<String, dynamic>>? onFinishResults;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: FlutterForm(
options: FlutterFormOptions(
checkPage: const CheckPage(),
nextButton: (pageNumber, checkingPages) {
return Align(
alignment: Alignment.bottomCenter,
child: ElevatedButton(
onPressed: () async {
await formController.autoNextStep();
},
child: Text(pageNumber == 0
? 'next1'
: pageNumber == 1
? 'next2'
: 'finish'),
),
);
},
onFinished: (Map<int, Map<String, dynamic>> results) {
onFinishResults = results;
},
onNext: (int pageNumber, Map<String, dynamic> results) {
onNextPageNumber = pageNumber;
onNextResults = results;
},
pages: [
FlutterFormPage(
child: Center(
child: FlutterFormInputPlainText(
label: const Text('Field1Label'),
controller: testField1Controller,
),
),
),
FlutterFormPage(
child: Center(
child: FlutterFormInputPlainText(
label: const Text('Field2Label'),
controller: testField2Controller,
),
),
),
],
),
formController: formController,
),
),
),
);
await tester.enterText(
find.widgetWithText(TextFormField, 'Field1Label'), 'Field1Input');
await tester.tap(find.widgetWithText(ElevatedButton, 'next1'));
await tester.pumpAndSettle();
expect(0, onNextPageNumber);
expect({'Field1': 'Field1Input'}, onNextResults);
await tester.enterText(
find.widgetWithText(TextFormField, 'Field2Label'), 'Field2Input');
await tester.tap(find.widgetWithText(ElevatedButton, 'next2'));
await tester.pumpAndSettle();
expect(1, onNextPageNumber);
expect({'Field2': 'Field2Input'}, onNextResults);
await tester.tap(find.text('Field1Input'));
await tester.pumpAndSettle();
await tester.enterText(
find.widgetWithText(TextFormField, 'Field1Label'), 'Field1Input2');
await tester.tap(find.widgetWithText(ElevatedButton, 'next1'));
await tester.pumpAndSettle();
expect(0, onNextPageNumber);
expect({'Field1': 'Field1Input2'}, onNextResults);
await tester.tap(find.widgetWithText(ElevatedButton, "finish"));
await tester.pumpAndSettle();
expect({
0: {'Field1': 'Field1Input2'},
1: {'Field2': 'Field2Input'}
}, onFinishResults);
});
testWidgets('Wrong input with mandatory validator', (tester) async {
FlutterFormController formController = FlutterFormController();
var testField1Controller = FlutterFormInputPlainTextController(
id: 'Field1',
mandatory: true,
);
int? onNextPageNumber;
Map<String, dynamic>? onNextResults;
Map<int, Map<String, dynamic>>? onFinishResults;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: FlutterForm(
options: FlutterFormOptions(
nextButton: (pageNumber, checkingPages) {
return Align(
alignment: Alignment.bottomCenter,
child: ElevatedButton(
onPressed: () async {
await formController.autoNextStep();
},
child: const Text('finish'),
),
);
},
onFinished: (Map<int, Map<String, dynamic>> results) {
// print('finished results: $results');
onFinishResults = results;
},
onNext: (int pageNumber, Map<String, dynamic> results) {
// print('nextResults: $pageNumber: $results');
onNextPageNumber = pageNumber;
onNextResults = results;
},
pages: [
FlutterFormPage(
child: Center(
child: FlutterFormInputPlainText(
label: const Text('Field1Label'),
controller: testField1Controller,
),
),
),
],
),
formController: formController,
),
),
),
);
await tester.tap(find.widgetWithText(ElevatedButton, 'finish'));
await tester.pumpAndSettle();
expect(null, onNextPageNumber);
expect(null, onNextResults);
final errorMessageFinder = find.text('Field can not be empty');
expect(errorMessageFinder, findsOneWidget);
await tester.enterText(
find.widgetWithText(TextFormField, 'Field1Label'), 'Field1Input');
await tester.tap(find.widgetWithText(ElevatedButton, 'finish'));
await tester.pumpAndSettle();
expect(0, onNextPageNumber);
expect({'Field1': 'Field1Input'}, onNextResults);
expect({
0: {'Field1': 'Field1Input'},
}, onFinishResults);
}); });
} }