Merge pull request #1 from Iconica-Development/feature/first_release

Basic layout
This commit is contained in:
FlutterJoey 2022-10-25 16:33:07 +02:00 committed by GitHub
commit a2e2be34e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 400 additions and 84 deletions

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_address_form/flutter_address_form.dart';
void main() {
runApp(const MyApp());
@ -24,92 +25,88 @@ class MyApp extends StatelessWidget {
// is not restarted.
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
home: AddressFormExample(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
class AddressFormExample extends StatelessWidget {
AddressFormExample({super.key});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// final RegExp zipcodeRegExp = RegExp(r'^[1-9][0-9]{3}\s?[a-zA-Z]{2}$');
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
final _addressController = AddressController(
zipCodeValidator: (text) {
if (text != null) {
if (text.isEmpty) {
return 'Can\'t be empty';
}
if (!RegExp(r'^[1-9][0-9]{3}\s?[a-zA-Z]{2}$').hasMatch(text)) {
return 'Invalid zipcode';
}
}
return null;
},
housenumberValidator: (text) {
if (text != null) {
if (text.isEmpty) {
return 'Can\'t be empty';
}
if (text.length >= 3 || int.tryParse(text) == null) {
return 'Invalid number';
}
}
return null;
},
suffixValidator: (text) {
if (text != null) {
if (text.isNotEmpty && RegExp(r'/^[a-z]*$/').hasMatch(text)) {
return 'Invalid prefix';
}
}
return null;
},
streetValidator: (text) {
if (text != null) {
if (text.isEmpty) {
return 'Can\'t be empty';
}
}
return null;
},
cityValidator: (text) {
if (text != null) {
if (text.isEmpty) {
return 'Can\'t be empty';
}
}
return null;
},
onAutoComplete: (address) {
return address;
},
);
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
title: const Text('Address Form'),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Invoke "debug painting" (press "p" in the console, choose the
// "Toggle Debug Paint" action from the Flutter Inspector in Android
// Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
// to see the wireframe for each widget.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
body: Column(
children: [
AddressForm(
onSubmit: (value) => value,
controller: _addressController,
),
ElevatedButton(
onPressed: () {
_addressController.validate();
},
child: const Text('Validate'),
)
],
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}

View file

@ -55,6 +55,13 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_address_form:
dependency: "direct main"
description:
path: ".."
relative: true
source: path
version: "0.0.1"
flutter_lints:
dependency: "direct dev"
description:
@ -158,3 +165,4 @@ packages:
version: "2.1.2"
sdks:
dart: ">=2.18.2 <3.0.0"
flutter: ">=1.17.0"

View file

@ -32,6 +32,9 @@ dependencies:
flutter:
sdk: flutter
flutter_address_form:
path: ../
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.

View file

@ -1,7 +1,4 @@
library flutter_address_form;
/// A Calculator.
class Calculator {
/// Returns [value] plus 1.
int addOne(int value) => value + 1;
}
export 'src/address_form.dart';
export 'src/models/address_model.dart';

214
lib/src/address_form.dart Normal file
View file

@ -0,0 +1,214 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_address_form/src/models/address_model.dart';
/// A widget that creates a form with different address fields widgets.
/// Returns a `AddressModel` Object from a `AddressController`.
class AddressForm extends StatefulWidget {
AddressForm({
super.key,
AddressController? controller,
this.zipCodeDecoration = const InputDecoration(label: Text('Zipcode')),
this.housenumberDecoration =
const InputDecoration(label: Text('Housenumber')),
this.suffixDecoration = const InputDecoration(label: Text('Suffix')),
this.streetDecoration = const InputDecoration(label: Text('Street')),
this.cityDecoration = const InputDecoration(label: Text('City')),
required this.onSubmit,
}) {
_addressController = controller ??
AddressController(
onAutoComplete: (model) => model,
zipCodeValidator: (text) => null,
cityValidator: (text) => null,
housenumberValidator: (text) => null,
streetValidator: (text) => null,
suffixValidator: (text) => null,
);
}
final InputDecoration zipCodeDecoration;
final InputDecoration housenumberDecoration;
final InputDecoration suffixDecoration;
final InputDecoration streetDecoration;
final InputDecoration cityDecoration;
final ValueChanged<String> onSubmit;
/// Controls the `AddressModel`
late final AddressController _addressController;
@override
State<AddressForm> createState() => _AddressFormState();
}
class _AddressFormState extends State<AddressForm> {
@override
Widget build(BuildContext context) {
return Form(
key: widget._addressController._formKey,
child: Column(
children: [
AddressFormTextField(
validator: widget._addressController.zipCodeValidator,
controller: widget._addressController._zipcodeController,
fieldDecoration: widget.zipCodeDecoration,
),
Row(
children: [
Expanded(
child: AddressFormTextField(
validator: widget._addressController.housenumberValidator,
controller: widget._addressController._housenumberController,
fieldDecoration: widget.housenumberDecoration,
),
),
Expanded(
child: AddressFormTextField(
validator: widget._addressController.suffixValidator,
controller: widget._addressController._suffixController,
fieldDecoration: widget.suffixDecoration,
),
),
],
),
AddressFormTextField(
validator: widget._addressController.streetValidator,
controller: widget._addressController._streetController,
fieldDecoration: widget.streetDecoration,
),
AddressFormTextField(
validator: widget._addressController.cityValidator,
controller: widget._addressController._cityController,
fieldDecoration: widget.cityDecoration,
),
],
),
);
}
}
class AddressFormTextField extends StatelessWidget {
const AddressFormTextField({
super.key,
required this.fieldDecoration,
required this.controller,
this.validator,
});
final TextEditingController controller;
final InputDecoration fieldDecoration;
final String? Function(String?)? validator;
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.all(10),
child: TextFormField(
controller: controller,
decoration: fieldDecoration,
validator: validator,
),
);
}
}
class AddressController extends ChangeNotifier {
/// An optional value to initialize the form field to, or null otherwise.
final AddressModel? initialValue;
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
/// When the form changes, the function passes the current `AddressModel` as an argument and gives the possibility to manipulate and return a `AddressModel`.
final FutureOr<AddressModel> Function(AddressModel)? onAutoComplete;
AddressController(
{this.initialValue,
this.onAutoComplete,
this.zipCodeValidator,
this.housenumberValidator,
this.suffixValidator,
this.streetValidator,
this.cityValidator}) {
_model = initialValue ??
const AddressModel(
zipcode: null,
street: null,
housenumber: null,
suffix: null,
city: null,
);
_zipcodeController.addListener(_update);
_streetController.addListener(_update);
_housenumberController.addListener(_update);
_suffixController.addListener(_update);
_cityController.addListener(_update);
}
late AddressModel _model;
final String? Function(String?)? zipCodeValidator;
final String? Function(String?)? housenumberValidator;
final String? Function(String?)? suffixValidator;
final String? Function(String?)? streetValidator;
final String? Function(String?)? cityValidator;
late final _zipcodeController =
TextEditingController(text: initialValue?.zipcode);
late final _streetController =
TextEditingController(text: initialValue?.street);
late final _housenumberController =
TextEditingController(text: initialValue?.housenumber);
late final _suffixController =
TextEditingController(text: initialValue?.suffix);
late final _cityController = TextEditingController(text: initialValue?.city);
AddressModel get model => _model;
bool validate() {
assert(_formKey.currentState != null,
'Validation can only be used if a FormKey is attached to the current Form');
return _formKey.currentState!.validate();
}
void _update() async {
AddressModel updatedModel = _model.copyWith(
zipcode: _zipcodeController.text,
street: _streetController.text,
housenumber: _housenumberController.text,
suffix: _suffixController.text,
city: _cityController.text);
_model = await onAutoComplete?.call(updatedModel) ?? updatedModel;
if (_model.zipcode != updatedModel.zipcode) {
_zipcodeController.text = _model.zipcode ?? '';
}
if (_model.street != updatedModel.street) {
_streetController.text = _model.street ?? '';
}
if (_model.housenumber != updatedModel.housenumber) {
_housenumberController.text = _model.housenumber ?? '';
}
if (_model.suffix != updatedModel.suffix) {
_suffixController.text = _model.suffix ?? '';
}
if (_model.city != updatedModel.city) {
_cityController.text = _model.city ?? '';
}
notifyListeners();
}
@override
void dispose() {
super.dispose();
_zipcodeController.dispose();
_streetController.dispose();
_housenumberController.dispose();
_suffixController.dispose();
_cityController.dispose();
}
}

View file

@ -0,0 +1,33 @@
import 'package:flutter/material.dart' show immutable;
@immutable
class AddressModel {
const AddressModel({
this.zipcode,
this.street,
this.housenumber,
this.suffix,
this.city,
});
final String? zipcode;
final String? street;
final String? housenumber;
final String? suffix;
final String? city;
AddressModel copyWith({
String? zipcode,
String? street,
String? housenumber,
String? suffix,
String? city,
}) =>
AddressModel(
zipcode: zipcode ?? this.zipcode,
street: street ?? this.street,
housenumber: housenumber ?? this.housenumber,
suffix: suffix ?? this.suffix,
city: city ?? this.city,
);
}

View file

@ -1,7 +1,6 @@
name: flutter_address_form
description: A new Flutter package project.
version: 0.0.1
homepage:
environment:
sdk: '>=2.18.2 <3.0.0'

View file

@ -1,12 +1,77 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_address_form/flutter_address_form.dart';
void main() {
test('adds one to input values', () {
final calculator = Calculator();
expect(calculator.addOne(2), 3);
expect(calculator.addOne(-7), -6);
expect(calculator.addOne(0), 1);
late RegExp zipcodeRegExp;
setUp(() {
// Testing with Dutch ZipCode
zipcodeRegExp = RegExp(r'^[1-9][0-9]{3}\s?[a-zA-Z]{2}$');
});
testWidgets('Render App with AddressForm Widget', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(),
body: AddressForm(
onSubmit: (value) => value,
controller: AddressController(
zipCodeValidator: (text) {
if (text != null) {
if (text.isEmpty) {
return 'Can\'t be empty';
}
if (!zipcodeRegExp.hasMatch(text)) {
return 'Invalid zipcode';
}
}
return null;
},
housenumberValidator: (text) {
if (text != null) {
if (text.isEmpty) {
return 'Can\'t be empty';
}
if (text.length >= 3 || int.tryParse(text) == null) {
return 'Invalid number';
}
}
return null;
},
suffixValidator: (text) {
if (text != null) {
if (text.isNotEmpty && RegExp(r'/^[a-z]*$/').hasMatch(text)) {
return 'Invalid prefix';
}
}
return null;
},
streetValidator: (text) {
if (text != null) {
if (text.isEmpty) {
return 'Can\'t be empty';
}
}
return null;
},
cityValidator: (text) {
if (text != null) {
if (text.isEmpty) {
return 'Can\'t be empty';
}
}
return null;
},
onAutoComplete: (address) {
return address;
},
),
),
),
),
);
await tester.pump();
});
}