]> gitweb.hamatoma.de Git - flutter_bones.git/commitdiff
Start
authorWinfried Kappeler <winfried.kappeler@infeos.eu>
Mon, 28 Sep 2020 07:25:24 +0000 (09:25 +0200)
committerWinfried Kappeler <winfried.kappeler@infeos.eu>
Mon, 28 Sep 2020 07:25:24 +0000 (09:25 +0200)
29 files changed:
.gitignore [new file with mode: 0644]
.metadata [new file with mode: 0644]
LICENSE [new file with mode: 0644]
README.md [new file with mode: 0644]
lib/app.dart [new file with mode: 0644]
lib/flutter_bones.dart [new file with mode: 0644]
lib/main.dart [new file with mode: 0644]
lib/src/helper/settings.dart [new file with mode: 0644]
lib/src/helper/validators.dart [new file with mode: 0644]
lib/src/model/field_model.dart [new file with mode: 0644]
lib/src/model/model_base.dart [new file with mode: 0644]
lib/src/model/model_types.dart [new file with mode: 0644]
lib/src/model/module/user_model.dart [new file with mode: 0644]
lib/src/model/module_model.dart [new file with mode: 0644]
lib/src/model/page_model.dart [new file with mode: 0644]
lib/src/model/section_model.dart [new file with mode: 0644]
lib/src/model/widget_model.dart [new file with mode: 0644]
lib/src/page/login_page.dart [new file with mode: 0644]
lib/src/page/page_data.dart [new file with mode: 0644]
lib/src/page/role_page.dart [new file with mode: 0644]
lib/src/page/user_page.dart [new file with mode: 0644]
lib/src/private/bappbar.dart [new file with mode: 0644]
lib/src/private/bdrawer.dart [new file with mode: 0644]
lib/src/private/bsettings.dart [new file with mode: 0644]
lib/src/widget/simpleform.dart [new file with mode: 0644]
pubspec.yaml [new file with mode: 0644]
test/helpers/settings_test.dart [new file with mode: 0644]
test/helpers/validators_test.dart [new file with mode: 0644]
test/widget_test.dart [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..95b551c
--- /dev/null
@@ -0,0 +1,47 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+pubspec.lock
+
+# wk: @ToDo: nicht sicher
+android/
+ios/
+web/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+/build/
+
+# Web related
+lib/generated_plugin_registrant.dart
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
diff --git a/.metadata b/.metadata
new file mode 100644 (file)
index 0000000..837b938
--- /dev/null
+++ b/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: cba91c97d03ac8aae66317097f9e8aeade86257f
+  channel: master
+
+project_type: app
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..0e259d4
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,121 @@
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+    LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+    INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+    REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+    PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+    THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+    HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+  i. the right to reproduce, adapt, distribute, perform, display,
+     communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+     likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+     subject to the limitations in paragraph 4(a), below;
+  v. rights protecting the extraction, dissemination, use and reuse of data
+     in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+     European Parliament and of the Council of 11 March 1996 on the legal
+     protection of databases, and under any national implementation
+     thereof, including any amended or successor version of such
+     directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+     world based on applicable law or treaty, and any national
+     implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+    surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+    warranties of any kind concerning the Work, express, implied,
+    statutory or otherwise, including without limitation warranties of
+    title, merchantability, fitness for a particular purpose, non
+    infringement, or the absence of latent or other defects, accuracy, or
+    the present or absence of errors, whether or not discoverable, all to
+    the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+    that may apply to the Work or any use thereof, including without
+    limitation any person's Copyright and Related Rights in the Work.
+    Further, Affirmer disclaims responsibility for obtaining any necessary
+    consents, permissions or other rights required for any use of the
+    Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+    party to this document and has no duty or obligation with respect to
+    this CC0 or use of the Work.
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..56cfb39
--- /dev/null
+++ b/README.md
@@ -0,0 +1,16 @@
+# bones
+
+Sammlung von Widgets und Bibliotheksfunktionen als Basis für andere Projekte
+
+## Getting Started
+
+This project is a starting point for a Flutter application.
+
+A few resources to get you started if this is your first Flutter project:
+
+- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
+- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
+
+For help getting started with Flutter, view our
+[online documentation](https://flutter.dev/docs), which offers tutorials,
+samples, guidance on mobile development, and a full API reference.
diff --git a/lib/app.dart b/lib/app.dart
new file mode 100644 (file)
index 0000000..d947f36
--- /dev/null
@@ -0,0 +1,43 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bones/flutter_bones.dart';
+import 'package:flutter_bones/src/private/bsettings.dart';
+
+class BoneApp extends StatefulWidget {
+  @override
+  BoneAppState createState() => BoneAppState();
+}
+
+class BoneAppState extends State<BoneApp> {
+
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp(
+      title: 'Virtuelle Fragestunde',
+      theme: ThemeData(
+        primarySwatch: Colors.blue,
+        visualDensity: VisualDensity.adaptivePlatformDensity,
+      ),
+      initialRoute: '/login',
+      onGenerateRoute: _getRoute,
+    );
+  }
+
+}
+
+Route<dynamic> _getRoute(RouteSettings settings) {
+  MaterialPageRoute route;
+  if (settings.name == '/login') {
+    route = MaterialPageRoute<void>(
+      settings: settings,
+      builder: (BuildContext context) => LoginPage(BSettings.instance.pageData),
+      fullscreenDialog: false,
+    );
+  } else {
+    route = MaterialPageRoute<void>(
+  settings: settings,
+  builder: (BuildContext context) => LoginPage(BSettings.instance.pageData),
+  fullscreenDialog: false,
+    );
+  }
+  return route;
+}
diff --git a/lib/flutter_bones.dart b/lib/flutter_bones.dart
new file mode 100644 (file)
index 0000000..82ea4cd
--- /dev/null
@@ -0,0 +1,14 @@
+export 'src/widget/simpleform.dart';
+export 'src/helper/settings.dart';
+export 'src/helper/validators.dart';
+export 'src/page/page_data.dart';
+export 'src/page/login_page.dart';
+export 'src/page/role_page.dart';
+export 'src/page/user_page.dart';
+export 'src/model/model_types.dart';
+export 'src/model/model_base.dart';
+export 'src/model/widget_model.dart';
+export 'src/model/field_model.dart';
+export 'src/model/module_model.dart';
+export 'src/model/section_model.dart';
+export 'src/model/page_model.dart';
\ No newline at end of file
diff --git a/lib/main.dart b/lib/main.dart
new file mode 100644 (file)
index 0000000..7f57697
--- /dev/null
@@ -0,0 +1,7 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bones/app.dart';
+
+void main() {
+  runApp(BoneApp());
+}
+
diff --git a/lib/src/helper/settings.dart b/lib/src/helper/settings.dart
new file mode 100644 (file)
index 0000000..59b75ca
--- /dev/null
@@ -0,0 +1,26 @@
+import 'package:flutter/material.dart';
+
+class Settings {
+  static Locale locale = Locale('US', 'en');
+
+  /// Sets the locale code.
+  /// [locale] the info about localisation
+  /// Finding [locale] of an app:  @see https://github.com/flutter/website/blob/master/examples/internationalization/minimal/lib/main.dart
+  static setLocale(locale) => Settings.locale = locale;
+  static setLocaleByNames({String country='US', String language='en'}) => Settings.locale = Locale(country, language);
+  /// Translates a [text] with a given translation [map].
+  /// Structure of the [map]: { <English text> : { <language code> : translation } }
+  /// return: [text] if no translation has been found, otherwise: the translation from the [map]
+  static String translate(String text, Map<String, Map<String, String>> map, {Map<String, String> placeholders}){
+    var rc = text;
+    if (map.containsKey(text) && map[text].containsKey(locale.languageCode)){
+      rc =  map[text][locale.languageCode];
+    }
+    if (placeholders != null){
+      placeholders.keys.forEach((key) {
+        rc = rc.replaceAll('%{$key}', placeholders[key]);
+      });
+    }
+    return rc;
+  }
+}
\ No newline at end of file
diff --git a/lib/src/helper/validators.dart b/lib/src/helper/validators.dart
new file mode 100644 (file)
index 0000000..fee37c3
--- /dev/null
@@ -0,0 +1,69 @@
+import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter_bones/flutter_bones.dart';
+import 'package:meta/meta.dart';
+
+/// Tests whether [input] is an valid email address.
+/// returns null if not empty, otherwise an error message
+String checkEmail(String input) => Validation.isEmail(input)
+    ? null
+    : _vt('Not an email address: %{0} Example: joe@example.com', {'0': input});
+
+/// returns null if not empty, otherwise an error message
+String checkInt(String input) =>
+    Validation.isInt(input) ? null : _vt('Not a number: %{0}', {'0': input});
+
+/// Validates an [input] with many [validators].
+String checkMany(String input, List<Function> validators) {
+  String rc;
+  for (var item in validators) {
+    final rc2 = item(input);
+    if (rc2 != null) {
+      rc = rc2;
+      break;
+    }
+  }
+  return rc;
+}
+
+/// returns null if not empty, otherwise an error message
+String checkNat(String input) => Validation.isNat(input)
+    ? null
+    : _vt('Not a not negative number: %{0}', {'0': input});
+
+// Tests whether [input] is a natural number (>= 0).
+/// Tests whether [input] is not empty.
+/// returns null if not empty, otherwise an error message
+String checkNotEmpty(String input) {
+  return input.isEmpty ? _vt('Please fill in') : null;
+}
+
+// Tests whether [input] is an integer (>= 0).
+/// Tests whether [input] is an valid phone number.
+/// returns null if not empty, otherwise an error message
+String checkPhoneNumber(String input) => Validation.isPhoneNumber(input)
+    ? null
+    : _vt('Not a phone number: %{0} Examples: "089-123452 "+49-89-12345"',
+        {'0': input});
+
+String _vt(String key, [Map<String, String> placeholders]) =>
+    Settings.translate(key, ValidatorTranslations.translations,
+        placeholders: placeholders);
+
+@protected
+class ValidatorTranslations {
+  static final translations = {
+    'Please fill in': {
+      'de': 'Bitte ausfüllen',
+    },
+    'Not a number: %{0}': {
+      'de': 'Keine Zahl: %{0}',
+    },
+    'Not an email address: %{0} Example: joe@example.com': {
+      'de': 'Keine gültige EMailadresse: %{0} Beispiel: joe@example.com',
+    },
+    'Not a phone number: %{0} Examples: "089-123452 "+49-89-12345"': {
+      'de':
+          'Keine gültige Telefonnummer: %{0} Beispiele: "089-123452 "+49-89-12345"',
+    },
+  };
+}
diff --git a/lib/src/model/field_model.dart b/lib/src/model/field_model.dart
new file mode 100644 (file)
index 0000000..adb0092
--- /dev/null
@@ -0,0 +1,85 @@
+import 'package:flutter/material.dart';
+import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter_bones/flutter_bones.dart';
+import 'package:meta/meta.dart';
+
+class FieldModel extends ModelBase {
+  static final regExprOptions = RegExp(r'^(undef|readonly|disabled|password|required}');
+  final SectionModel parent;
+  String name;
+  String label;
+  String toolTip;
+  FieldModelType fieldModelType;
+  DataType dataType;
+  int maxSize;
+  int rows;
+  List<String> options;
+  FormFieldValidator<String> validator;
+  FormFieldSetter onSaved;
+
+  final Map<String, dynamic> map;
+
+  FieldModel(this.parent, this.map, BaseLogger logger) : super(logger);
+
+  /// Returns the name including the names of the parent
+  @override
+  String fullName() => '${parent.fullName()}.$name';
+
+  /// Parses the map and stores the data in the instance.
+  void parse() {
+    checkSuperfluous(map, 'name label toolTip fieldModelType dataType maxSize rows options'.split(' '));
+    name = parseString('name', map, required: true);
+    label = parseString('label', map, required: false);
+    toolTip = parseString('toolTip', map, required: false);
+    fieldModelType = parseEnum<FieldModelType>(
+        'fieldModelType', map, FieldModelType.values);
+    dataType = parseEnum<DataType>(
+        'dataType', map, DataType.values);
+    maxSize = parseInt('maxSize', map, required: false);
+    rows = parseInt('rows', map, required: false);
+    options = parseOptions('options', map);
+  }
+
+  /// Tests the validity of the entries in [optionList]
+  /// Errors will be logged.
+  bool checkOptions() {
+    bool rc = false;
+    options.forEach((element) {
+      if (regExprOptions.firstMatch(element) == null) {
+        logger.error('unbekannte Feldoption $element in ${fullName()}');
+        rc = true;
+      }
+    });
+    return rc;
+  }
+  /// Returns the widget representing the field.
+  Widget widget(Key formKey){
+    Widget rc;
+    switch(fieldModelType){
+      case FieldModelType.text:
+        rc = TextFormField(key: formKey,
+          validator: validator,
+          decoration: InputDecoration(labelText: label),
+          onSaved: onSaved,
+          maxLength: maxSize,
+          maxLines: rows,
+          readOnly: options.contains('readonly'),
+          obscureText: options.contains('password'),
+        );
+        break;
+      case FieldModelType.checkbox:
+        rc = Checkbox(key: formKey,
+          decoration: InputDecoration(labelText: label),
+          onSaved: onSaved,
+          maxLength: maxSize,
+          maxLines: rows,
+          readOnly: options.contains('readonly'),
+          obscureText: options.contains('password'),
+        );
+        break;
+    }
+    return rc;
+  }
+}
+
+enum FieldModelType { checkbox, combobox, image, text, }
diff --git a/lib/src/model/model_base.dart b/lib/src/model/model_base.dart
new file mode 100644 (file)
index 0000000..ff06252
--- /dev/null
@@ -0,0 +1,90 @@
+import 'package:flutter/material.dart';
+import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter_bones/flutter_bones.dart';
+import 'package:meta/meta.dart';
+
+/// Base class of all models.
+abstract class ModelBase {
+  BaseLogger logger;
+  ModelBase(this.logger);
+
+  /// Tests a [map] for superfluous [keys].
+  /// Keys of the [map] that are not listed in [keys] will be logged as errors.
+  /// Keys that do not have the type String are logged as errors.
+  void checkSuperfluous(Map<String, dynamic> map, List<String> keys) {
+    map.keys.forEach((element) {
+      if (element.runtimeType != String) {
+        logger.error('wrong key type ${element.runtimeType} in ${fullName()}');
+      } else if (!keys.contains(map[element])) {
+        logger.error('wrong key ${element.runtimeType} in ${fullName()}');
+      }
+    });
+  }
+
+  /// Returns the name including the names of the parent
+  String fullName();
+
+  /// Fetches an entry from a map addressed by a [key].
+  /// An error is logged if [required] is true and the map does not contain the key.
+  T parseEnum<T>(String key, Map<String, dynamic> map, List values,
+      {required: bool}) {
+    T rc;
+    if (!map.containsKey(key)) {
+      if (required) {
+        logger.error('missing $key in ${fullName()}');
+      }
+    } else {
+      rc = StringUtils.stringToEnum(map[key], values);
+    }
+    return rc;
+  }
+
+  /// Fetches an entry from a map addressed by a [key].
+  /// An error is logged if [required] is true and the map does not contain the key.
+  int parseInt(String key, Map<String, dynamic> map, {required: bool}) {
+    int rc;
+    if (!map.containsKey(key)) {
+      if (required) {
+        logger.error('missing $key in ${fullName()}');
+      } else {
+        if (map[key].runtimeType == int) {
+          rc = map[key];
+        } else if (map[key].runtimeType == String) {
+          if (Validation.isInt(map[key])) {
+            rc = StringUtils.asInt(map[key]);
+          } else {
+            logger
+                .error('not an integer: ${map[key]} map[$key] in {fullName()}');
+          }
+        } else {
+          logger.error('not an integer: ${map[key]} map[$key] in {fullName()}');
+        }
+      }
+    }
+    return rc;
+  }
+
+  /// Fetches an entry from a map addressed by a [key].
+  /// An error is logged if [required] is true and the map does not contain the key.
+  List<String> parseOptions(String key, Map<String, dynamic> map) {
+    List<String> rc = [];
+    if (map.containsKey(key)) {
+      rc = map[key].split(';');
+    }
+    return rc;
+  }
+
+  /// Fetches an entry from a map addressed by a [key].
+  /// An error is logged if [required] is true and the map does not contain the key.
+  String parseString(String key, Map<String, dynamic> map, {required: bool}) {
+    String rc;
+    if (!map.containsKey(key)) {
+      if (required) {
+        logger.error('missing $key in ${fullName()}');
+      } else {
+        rc = map[key] as String;
+      }
+    }
+    return rc;
+  }
+}
diff --git a/lib/src/model/model_types.dart b/lib/src/model/model_types.dart
new file mode 100644 (file)
index 0000000..d441b53
--- /dev/null
@@ -0,0 +1,4 @@
+import 'package:flutter_bones/flutter_bones.dart';
+enum DataType {
+  bool, currency, date, dateTime, float, int, reference, string,
+}
diff --git a/lib/src/model/module/user_model.dart b/lib/src/model/module/user_model.dart
new file mode 100644 (file)
index 0000000..6416fcd
--- /dev/null
@@ -0,0 +1,43 @@
+import 'package:flutter_bones/flutter_bones.dart';
+import 'package:dart_bones/dart_bones.dart';
+
+class UserModel extends ModuleModel{
+
+  static final model = <String, dynamic>{
+    "module": "user",
+    "pages": [
+      {
+        "name": "create",
+        "pageModeType": "create",
+        "sections": [
+          {
+            "sectionModeType": "simpleForm",
+            "fields": [
+              {
+                "name": "user",
+                "fieldModelType": "text",
+                "label": "Benutzer",
+                "options": "required;unique",
+              },
+              {
+                "name": "displayname",
+                "label": "Anzeigename",
+                "fieldModelType": "text",
+                "options": "required",
+              },
+              {
+                "name": "role",
+                "label": "Rolle",
+                "fieldModelType": "combobox",
+                "dataType": "reference",
+                "options": "required;undef",
+              },
+            ]
+          }
+        ]
+      },
+    ],
+  };
+  UserModel(BaseLogger logger): super(model, logger);
+
+}
\ No newline at end of file
diff --git a/lib/src/model/module_model.dart b/lib/src/model/module_model.dart
new file mode 100644 (file)
index 0000000..dfacd9f
--- /dev/null
@@ -0,0 +1,24 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bones/flutter_bones.dart';
+import 'package:meta/meta.dart';
+import 'package:dart_bones/dart_bones.dart';
+
+class ModuleModel extends ModelBase {
+  final Map<String, dynamic> map;
+  String name;
+  List<String> options;
+  List<PageModel> pages;
+
+  ModuleModel(this.map, BaseLogger logger): super(logger);
+  /// Returns the name including the names of the parent
+  @override
+  String fullName() => name;
+
+  /// Parses the map and stores the data in the instance.
+  void parse(){
+    checkSuperfluous(map, 'module pages options'.split(' '));
+    name = parseString('module', map, required: true);
+    pages = PageModel.parseList(this, map['pages'], logger);
+    options = parseOptions('options', map);
+  }
+}
diff --git a/lib/src/model/page_model.dart b/lib/src/model/page_model.dart
new file mode 100644 (file)
index 0000000..1281b73
--- /dev/null
@@ -0,0 +1,41 @@
+import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter_bones/flutter_bones.dart';
+import 'package:meta/meta.dart';
+
+/// Represents one screen of the application.
+class PageModel extends ModelBase {
+  static final regExprOptions = RegExp(r'^(unknown)');
+  final ModuleModel module;
+  final Map<String, dynamic>map;
+  String name;
+  PageModelType pageModelType;
+  final List<SectionModel>sections = [];
+  List<String> options;
+
+  PageModel(this.module, this.map, BaseLogger logger): super(logger);
+
+  /// Returns the name including the names of the parent
+  @override
+  String fullName() => '${module.fullName()}.$name';
+
+  /// Parses the map and stores the data in the instance.
+  void parse(){
+    checkSuperfluous(map, 'name pageModelType sections options'.split(' '));
+    name = parseString('name', map, required: true);
+    pageModelType = parseEnum<PageModelType>('fieldTypeInfo', map, PageModelType.values);
+    options = parseOptions('options', map);
+  }
+  /// Returns a list of Pages constructed by the Json like [map].
+  static List<PageModel> parseList(
+      ModuleModel parent, List<Map<String, dynamic>> map, BaseLogger logger) {
+    final rc = map.map((item) => PageModel(parent, item, logger));
+    return rc;
+  }
+}
+
+enum PageModelType {
+  change,
+  create,
+  delete,
+  overview,
+}
diff --git a/lib/src/model/section_model.dart b/lib/src/model/section_model.dart
new file mode 100644 (file)
index 0000000..70ec50b
--- /dev/null
@@ -0,0 +1,41 @@
+import 'package:flutter/material.dart';
+import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter_bones/flutter_bones.dart';
+import 'package:meta/meta.dart';
+
+/// A part of a page represented by one widget.
+class SectionModel extends ModelBase {
+  static final regExprOptions = RegExp(r'^(unknown)');
+  SectionModelType sectionModelType;
+  final PageModel page;
+  String name;
+  List<FieldModel> fields;
+  List<String> options;
+  final Map<String, dynamic> map;
+  SectionModel(this.page, this.map, BaseLogger logger) : super(logger);
+
+  /// Returns the name including the names of the parent
+  @override
+  String fullName() => '${page.fullName()}.$name';
+
+  /// Parses the map and stores the data in the instance.
+  void parse() {
+    checkSuperfluous(map, 'name sectionModelType fields options'.split(' '));
+    name = parseString('name', map, required: true);
+    sectionModelType = parseEnum<SectionModelType>(
+        'sectionModelType', map, PageModelType.values);
+    options = parseOptions('options', map);
+  }
+
+  /// Returns a list of Pages constructed by the Json like [map].
+  static List<SectionModel> parseList(
+      PageModel parent, List<Map<String, dynamic>> map, BaseLogger logger) {
+    final rc = map.map((item) => SectionModel(parent, item, logger));
+    return rc;
+  }
+}
+
+enum SectionModelType {
+  simpleForm,
+  query,
+}
diff --git a/lib/src/model/widget_model.dart b/lib/src/model/widget_model.dart
new file mode 100644 (file)
index 0000000..fe688cd
--- /dev/null
@@ -0,0 +1,75 @@
+import 'package:flutter/material.dart';
+import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter_bones/flutter_bones.dart';
+import 'package:meta/meta.dart';
+
+/// A base class for items inside a page: SectionModel FieldModel TextModel...
+abstract class WidgetModel extends ModelBase {
+  final SectionModel section;
+  final PageModel page;
+  final Map<String, dynamic> map;
+
+  WidgetModel(this.section, this.page, BaseLogger logger) : super(logger);
+
+  /// Returns the name including the names of the parent
+  @override
+  String fullName() => '${parent.fullName()}.$name';
+
+  /// Parses the map and stores the data in the instance.
+  void parse() {
+    checkSuperfluous(map, 'name label toolTip fieldModelType dataType maxSize rows options'.split(' '));
+    name = parseString('name', map, required: true);
+    label = parseString('label', map, required: false);
+    toolTip = parseString('toolTip', map, required: false);
+    fieldModelType = parseEnum<WidgetModelType>(
+        'fieldModelType', map, WidgetModelType.values);
+    dataType = parseEnum<DataType>(
+        'dataType', map, DataType.values);
+    maxSize = parseInt('maxSize', map, required: false);
+    rows = parseInt('rows', map, required: false);
+    options = parseOptions('options', map);
+  }
+
+  /// Tests the validity of the entries in [optionList]
+  /// Errors will be logged.
+  bool checkOptions() {
+    bool rc = false;
+    options.forEach((element) {
+      if (regExprOptions.firstMatch(element) == null) {
+        logger.error('unbekannte Feldoption $element in ${fullName()}');
+        rc = true;
+      }
+    });
+    return rc;
+  }
+  /// Returns the widget representing the field.
+  Widget widget(Key formKey){
+    Widget rc;
+    switch(fieldModelType){
+      case WidgetModelType.text:
+        rc = TextFormField(key: formKey,
+          validator: validator,
+          decoration: InputDecoration(labelText: label),
+          onSaved: onSaved,
+          maxLength: maxSize,
+          maxLines: rows,
+          readOnly: options.contains('readonly'),
+          obscureText: options.contains('password'),
+        );
+        break;
+      case WidgetModelType.checkbox:
+        rc = Checkbox(key: formKey,
+          decoration: InputDecoration(labelText: label),
+          onSaved: onSaved,
+          maxLength: maxSize,
+          maxLines: rows,
+          readOnly: options.contains('readonly'),
+          obscureText: options.contains('password'),
+        );
+        break;
+    }
+    return rc;
+  }
+}
+
+enum WidgetModelType { checkbox, combobox, image, text, }
diff --git a/lib/src/page/login_page.dart b/lib/src/page/login_page.dart
new file mode 100644 (file)
index 0000000..d03a477
--- /dev/null
@@ -0,0 +1,67 @@
+import 'package:flutter/material.dart';
+import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter_bones/flutter_bones.dart';
+
+class LoginPage extends StatefulWidget {
+  final PageData pageData;
+  LoginPage(this.pageData, { Key key }) : super(key: key);
+  @override
+  LoginPageState createState() {
+    // LoginPageState.setPageData(pageData);
+    final rc = LoginPageState(pageData);
+
+    return rc;
+  }
+}
+
+class LoginPageState extends State<LoginPage>{
+  LoginPageState(this.pageData);
+  final PageData pageData;
+
+  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
+  static User currentUser = User();
+
+  @override
+  Widget build(BuildContext context) {
+    final user = LoginUser();
+    return Scaffold(
+      appBar: pageData.appBarBuilder('login'),
+        drawer: pageData.drawerBuilder(context),
+        body: SimpleForm.simpleForm(
+          key: _formKey,
+          configuration: pageData.configuration,
+          fields: <Widget>[
+            Text('Bitte loggen Sie sich ein.'),
+            TextFormField(
+              validator: checkNotEmpty,
+              decoration: InputDecoration(labelText: 'Name'),
+              onSaved: (input) => user.name = input,
+            ),
+            TextFormField(
+              validator: (input) => Validation.isEmail(input) || Validation.isPhoneNumber(input) ? null : 'keine Emailadresse und keine Telefonnummer: $input',
+              decoration: InputDecoration(labelText: 'Passwort'),
+              onSaved: (input) => user.password = input,
+              obscureText: true,
+            ),
+          ],
+          buttons: <Widget>[
+            RaisedButton(
+              onPressed: () => login(context),
+              child: Text('Anmelden'),
+            ),
+          ],
+        ));
+  }
+
+  void login(context) async {
+    if (_formKey.currentState.validate()) {
+      _formKey.currentState.save();
+      //@ToDo: store in database
+    }
+  }
+}
+
+class LoginUser {
+  String name;
+  String password;
+}
\ No newline at end of file
diff --git a/lib/src/page/page_data.dart b/lib/src/page/page_data.dart
new file mode 100644 (file)
index 0000000..70ad597
--- /dev/null
@@ -0,0 +1,15 @@
+import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter/material.dart';
+
+/// Data class for storing parameter to build a page.
+class PageData {
+  BaseConfiguration configuration;
+  /// Signature: AppBar func(String title)
+  AppBar Function(String title) appBarBuilder;
+  Drawer Function(dynamic context) drawerBuilder;
+  /// Constructor.
+  /// [configuration] is a map with the widget data (e.g. padding)
+  /// [appBarBuilder] is a factory of a function returning a outside designed AppBar
+  /// [drawerBuilder] is a factory of a function returning a Drawer which handles the "Hamburger menu"
+  PageData(this.configuration, this.appBarBuilder, this.drawerBuilder);
+}
\ No newline at end of file
diff --git a/lib/src/page/role_page.dart b/lib/src/page/role_page.dart
new file mode 100644 (file)
index 0000000..d37fea1
--- /dev/null
@@ -0,0 +1,64 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bones/flutter_bones.dart';
+
+class RolePage extends StatefulWidget {
+  final PageData pageData;
+  RolePage(this.pageData, { Key key }) : super(key: key);
+  @override
+  RolePageState createState() {
+    // RolePageState.setPageData(pageData);
+    final rc = RolePageState(pageData);
+
+    return rc;
+  }
+}
+
+class RolePageState extends State<RolePage>{
+  RolePageState(this.pageData);
+  final PageData pageData;
+
+  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
+  static Role currentRole = Role();
+
+  @override
+  Widget build(BuildContext context) {
+    final role = Role();
+    return Scaffold(
+      appBar: pageData.appBarBuilder('Rollen'),
+        drawer: pageData.drawerBuilder(context),
+        body: SimpleForm.simpleForm(
+          key: _formKey,
+          configuration: pageData.configuration,
+          fields: <Widget>[
+            TextFormField(
+              validator: checkNotEmpty,
+              decoration: InputDecoration(labelText: 'Name'),
+              onSaved: (input) => role.name = input,
+            ),
+            TextFormField(
+              validator: checkNat,
+              decoration: InputDecoration(labelText: 'Priorität'),
+              onSaved: (input) => role.priority = input,
+            ),
+          ],
+          buttons: <Widget>[
+            RaisedButton(
+              onPressed: () => login(context),
+              child: Text('Anmelden'),
+            ),
+          ],
+        ));
+  }
+
+  void login(context) async {
+    if (_formKey.currentState.validate()) {
+      _formKey.currentState.save();
+      //@ToDo: store in database
+    }
+  }
+}
+
+class Role {
+  String priority;
+  String name;
+}
\ No newline at end of file
diff --git a/lib/src/page/user_page.dart b/lib/src/page/user_page.dart
new file mode 100644 (file)
index 0000000..85166d4
--- /dev/null
@@ -0,0 +1,69 @@
+import 'package:flutter/material.dart';
+import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter_bones/flutter_bones.dart';
+
+class UserPage extends StatefulWidget {
+  final PageData pageData;
+  UserPage(this.pageData, { Key key }) : super(key: key);
+  @override
+  UserPageState createState() {
+    // UserPageState.setPageData(pageData);
+    final rc = UserPageState(pageData);
+
+    return rc;
+  }
+}
+
+class UserPageState extends State<UserPage>{
+  UserPageState(this.pageData);
+  final PageData pageData;
+
+  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
+  static User currentUser = User();
+
+  @override
+  Widget build(BuildContext context) {
+    final user = User();
+    return Scaffold(
+      appBar: pageData.appBarBuilder('Benutzer'),
+        drawer: pageData.drawerBuilder(context),
+        body: SimpleForm.simpleForm(
+          key: _formKey,
+          configuration: pageData.configuration,
+          fields: <Widget>[
+            TextFormField(
+              validator: checkNotEmpty,
+              decoration: InputDecoration(labelText: 'Name'),
+              onSaved: (input) => user.name = input,
+            ),
+            TextFormField(
+              validator: (input) => Validation.isEmail(input) || Validation.isPhoneNumber(input) ? null : 'keine Emailadresse und keine Telefonnummer: $input',
+              decoration: InputDecoration(labelText: 'Passwort'),
+              onSaved: (input) => user.password = input,
+              obscureText: true,
+            ),
+          ],
+          buttons: <Widget>[
+            RaisedButton(
+              onPressed: () => store(context),
+              child: Text('Speichern'),
+            ),
+          ],
+        ));
+  }
+
+  void store(context) async {
+    if (_formKey.currentState.validate()) {
+      _formKey.currentState.save();
+      //@ToDo: store in database
+    }
+  }
+}
+
+class User {
+  String name;
+  String email;
+  String displayName;
+  String password;
+  int role;
+}
\ No newline at end of file
diff --git a/lib/src/private/bappbar.dart b/lib/src/private/bappbar.dart
new file mode 100644 (file)
index 0000000..0ec5ff2
--- /dev/null
@@ -0,0 +1,6 @@
+import 'package:flutter/material.dart';
+
+class BAppBar extends AppBar {
+  BAppBar({String title}) : super(title: Text(title));
+  static BAppBar builder(String title) => BAppBar(title: title);
+}
diff --git a/lib/src/private/bdrawer.dart b/lib/src/private/bdrawer.dart
new file mode 100644 (file)
index 0000000..544dd48
--- /dev/null
@@ -0,0 +1,76 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bones/flutter_bones.dart';
+import 'package:flutter_bones/src/private/bsettings.dart';
+
+class MenuItem {
+  final String title;
+  final dynamic page;
+  final IconData icon;
+  MenuItem(this.title, this.page, this.icon);
+
+  static List<MenuItem> menuItems() {
+    final settings = BSettings.instance;
+    return <MenuItem>[
+      MenuItem('Login', () => LoginPage(settings.pageData),
+          Icons.account_box_outlined),
+    ];
+  }
+}
+
+class VFrageDrawer extends Drawer {
+  VFrageDrawer(context) : super(child: buildGrid(context));
+
+  /// Returns a method creating a drawer.
+  static VFrageDrawer builder(dynamic context) => VFrageDrawer(context);
+
+  static Widget buildGrid(context) {
+    final list = MenuItem.menuItems();
+    final rc = Card(
+        child: GridView.count(
+      crossAxisCount: 2,
+      crossAxisSpacing: 16.0,
+      children: list
+          .map((item) => GridTile(
+                child: new InkResponse(
+                  enableFeedback: true,
+                  child: Card(
+                    child: Container(
+                      alignment: Alignment.center,
+                      child: Column(
+                        children: [
+                          SizedBox(width: 10.0, height: 40.0),
+                          Icon(item.icon),
+                          Text(item.title)
+                        ],
+                      ),
+                    ),
+                  ),
+                  onTap: () => Navigator.push(context,
+                      MaterialPageRoute(builder: (context) => item.page())),
+                ),
+              ))
+          .toList(),
+    ));
+    return rc;
+  }
+  static Widget buildListView(context) {
+    final list = MenuItem.menuItems();
+    final rc = Card(
+        child: ListView(
+            shrinkWrap: true,
+            physics: ClampingScrollPhysics(),
+            children: list
+                .map((item) => ListTile(
+                      title: Text(item.title),
+                      onTap: () {
+                        // What happens after you tap the navigation item
+                        Navigator.push(
+                            context,
+                            MaterialPageRoute(
+                                builder: (context) => item.page()));
+                      },
+                    ))
+                .toList()));
+    return rc;
+  }
+}
diff --git a/lib/src/private/bsettings.dart b/lib/src/private/bsettings.dart
new file mode 100644 (file)
index 0000000..c0b8b5d
--- /dev/null
@@ -0,0 +1,30 @@
+import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter_bones/flutter_bones.dart';
+import 'package:flutter_bones/src/private/bdrawer.dart';
+import 'package:flutter_bones/src/private/bappbar.dart';
+
+class BSettings {
+  static BSettings _instance;
+
+  /// Returns the singleton of VSetting.
+  static BSettings get instance {
+    if (_instance == null) {
+      final map = {
+        'form.card.padding': '16.0',
+        'form.gap.field_button.height': '16.0',
+      };
+      final logger = MemoryLogger(LEVEL_FINE);
+      final pageData = PageData(BaseConfiguration(map, logger), BAppBar.builder,
+          VFrageDrawer.builder);
+      _instance = BSettings(BaseConfiguration(map, logger), pageData, logger);
+    }
+    return _instance;
+  }
+
+  final BaseConfiguration configuration;
+
+  final BaseLogger logger;
+
+  final PageData pageData;
+  BSettings(this.configuration, this.pageData, this.logger);
+}
diff --git a/lib/src/widget/simpleform.dart b/lib/src/widget/simpleform.dart
new file mode 100644 (file)
index 0000000..3bc5269
--- /dev/null
@@ -0,0 +1,23 @@
+import 'package:flutter/material.dart';
+import 'package:dart_bones/dart_bones.dart';
+
+class SimpleForm {
+  static Form simpleForm({@required Key key, @required List<Widget> fields,
+    @required List<Widget> buttons, @required BaseConfiguration configuration} ) {
+    final padding = configuration.asFloat('form.card.padding', defaultValue: 16.0);
+    return Form(
+        key: key,
+        child: Card(
+        margin: EdgeInsets.symmetric(vertical: padding, horizontal: padding),
+    child: Padding(
+    padding:
+    EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0),
+    child: ListView(
+    children:
+      <Widget>[...fields,
+        SizedBox(height: configuration.asFloat('form.gap.field_button.height', defaultValue: 16.0)),
+        ...buttons]
+    )),
+    ));
+  }
+}
\ No newline at end of file
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644 (file)
index 0000000..e1d0a53
--- /dev/null
@@ -0,0 +1,83 @@
+name: flutter_bones
+description: Helpers for a quick building of a Flutter app.
+
+# The following line prevents the package from being accidentally published to
+# pub.dev using `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 used as CFBundleVersion.
+# Read more about iOS versioning at
+# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
+version: 1.0.0+1
+
+environment:
+  sdk: ">=2.7.0 <3.0.0"
+
+dependencies:
+  flutter:
+    sdk: flutter
+  flutter_localizations:
+    sdk: flutter
+  dart_bones: "^0.2.2"
+
+
+  # The following adds the Cupertino Icons font to your application.
+  # Use with the CupertinoIcons class for iOS style icons.
+  cupertino_icons: ^1.0.0
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+  pedantic: ^1.8.0
+  test: ^1.6.0
+  mockito: ^4.1.1
+  test_coverage: ^0.4.2
+
+# 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.
+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
+
+  # To add assets to your application, add an assets section, like this:
+  # 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
diff --git a/test/helpers/settings_test.dart b/test/helpers/settings_test.dart
new file mode 100644 (file)
index 0000000..cebeaa2
--- /dev/null
@@ -0,0 +1,23 @@
+import 'package:flutter_bones/flutter_bones.dart';
+import 'package:test/test.dart';
+
+void main() {
+  final map = { 'help' : { 'de' : 'Hilfe'},
+  'wrong name %{0}' : { 'de' : 'falscher name %{0}' }
+  };
+  setUpAll(() => Settings.setLocaleByNames(language: 'en'));
+  group('Settings', () {
+    test('translate-en', () {
+      Settings.setLocaleByNames(language: 'en');
+      expect(Settings.translate('abc', map), equals('abc'));
+      expect(Settings.translate('help', map), equals('help'));
+      expect(Settings.translate('wrong name %{0}', map,  placeholders: {'0' : 'Joe'}), equals('wrong name Joe'));
+    });
+    test('translate-de', () {
+      Settings.setLocaleByNames(language: 'de');
+      expect(Settings.translate('abc', map), equals('abc'));
+      expect(Settings.translate('help', map), equals('Hilfe'));
+      expect(Settings.translate('wrong name %{0}', map, placeholders: {'0' : 'Joe'}), equals('falscher name Joe'));
+    });
+});
+}
diff --git a/test/helpers/validators_test.dart b/test/helpers/validators_test.dart
new file mode 100644 (file)
index 0000000..f682a9d
--- /dev/null
@@ -0,0 +1,53 @@
+import 'package:flutter_bones/flutter_bones.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('Validators', () {
+    test('checkEmail', () {
+      expect(checkEmail('joe@example.com'), isNull);
+      expect(
+          checkEmail('joe@example@com'),
+          equals(
+              'Not an email address: joe@example@com Example: joe@example.com'));
+    });
+    test('checkPhoneNumber', () {
+      expect(checkPhoneNumber('089-12345'), isNull);
+      expect(
+          checkPhoneNumber('handy'),
+          equals(
+              'Not a phone number: handy Examples: "089-123452 "+49-89-12345"'));
+    });
+    test('checkNotEmpty', () {
+      expect(checkNotEmpty('a'), isNull);
+      expect(
+          checkNotEmpty(''),
+          equals(
+              'Please fill in'));
+    });
+    test('checkMany', () {
+      expect(checkMany('089-1234', [checkNotEmpty, checkPhoneNumber]), isNull);
+      expect(
+          checkMany('', [checkNotEmpty, checkPhoneNumber]),
+          equals(
+              'Please fill in'));
+      expect(
+          checkMany('handy', [checkNotEmpty, checkPhoneNumber]),
+          equals(
+              'Not a phone number: handy Examples: "089-123452 "+49-89-12345"'));
+    });
+    test('checkNat', () {
+      expect(checkNat('123'), isNull);
+      expect(
+          checkNat('-1'),
+          equals(
+              'Not a not negative number: -1'));
+    });
+    test('checkInt', () {
+      expect(checkNat('123'), isNull);
+      expect(
+          checkNat('-a'),
+          equals(
+              'Not a not negative number: -a'));
+    });
+});
+}
diff --git a/test/widget_test.dart b/test/widget_test.dart
new file mode 100644 (file)
index 0000000..49ab1a7
--- /dev/null
@@ -0,0 +1,33 @@
+// This is a basic Flutter widget test.
+//
+// To perform an interaction with a widget in your test, use the WidgetTester
+// utility that Flutter provides. 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:flutter_bones/flutter_bones.dart';
+*/
+void main() {
+  /*
+  testWidgets('Counter increments smoke test', (WidgetTester tester) async {
+    // Build our app and trigger a frame.
+    await tester.pumpWidget(MyApp());
+
+    // 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);
+  });
+   */
+}