Merge pull request #1 from Iconica-Development/develop

Develop
This commit is contained in:
FlutterJoey 2022-09-22 10:15:24 +02:00 committed by GitHub
commit efbcdaac21
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 753 additions and 403 deletions

View file

@ -1,29 +1,38 @@
[![style: effective dart](https://img.shields.io/badge/style-effective_dart-40c4ff.svg)](https://github.com/tenhobi/effective_dart) # Flutter Profile
Short description of what your package is, why you created it. What issues it fixes and how it works. Also mention the available platforms Flutter Profile is a package you can use to display any user data and let them alter it if desired.
![alt text](example/image/example_profile.png)
## Features
Display every type of user data.
Display an image/avatar.
Enable the user to alter his data or withhold the user from doing so.
## Setup ## Setup
What setup steps are neccesarry and why> To use this package, add `flutter_profile` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels).
<details> ## How To Use
<summary>PLATFORM</summary>
specific platform steps See the [Example Code](example/lib/main.dart) for an example on how to use this package.
</details> Underneath are all paramters, of the 'ProfilePage' widget, listed with an explanation.
## How to use | Parameter | Explaination |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
How can we use the package descibe the most common ways with examples in | user | The class that contains all the user data. |
| service | The service which determines what happens the user wants to update their profile, update their avatar or press the InkWell at the bottom of the page. |
```dart | style | With the use of ProfileStyle a couple of style options can be set for the form. |
codeblocks | customAvatar | An option to override the standard avatar. |
``` | showAvatar | The ability to disable/enable the avatar. |
| itemBuilder | The way to override the standard textfield for each standard piece of user data. |
| itemBuilderOptions | The options used by the standard itemBuilder to alter the function and style of the textfields |
## Issues ## Issues
Please file any issues, bugs or feature request as an issue on our [GitHub](REPO URL) 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). Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_profile/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 ## Want to contribute
@ -31,4 +40,4 @@ If you would like to contribute to the plugin (e.g. by improving the documentati
## Author ## Author
This flutter_profile package for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at <support@iconica.nl> This `flutter-image-picker` 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: 209 KiB

View file

@ -1,29 +1,44 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:example/utils/example_profile_service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:profile/profile.dart'; import 'package:flutter_profile/flutter_profile.dart';
import 'utils/example_profile_data.dart';
void main() { void main() {
runApp(const MyApp()); runApp(const MyApp());
} }
class MyApp extends StatefulWidget { class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key); const MyApp({super.key});
@override @override
State<MyApp> createState() => _MyAppState(); Widget build(BuildContext context) {
return const MaterialApp(
home: ProfileExample(),
);
}
} }
class _MyAppState extends State<MyApp> { class ProfileExample extends StatefulWidget {
const ProfileExample({Key? key}) : super(key: key);
@override
State<ProfileExample> createState() => _ProfileExampleState();
}
class _ProfileExampleState extends State<ProfileExample> {
late User _user; late User _user;
MyProfileData profileData = MyProfileData(); ProfileData profileData =
ExampleProfileData().fromMap({'email': 'example@email.com'});
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_user = User( _user = User(
'firstName', 'Firstname',
'lastName', 'Lastname',
Uint8List.fromList( Uint8List.fromList(
[], [],
), ),
@ -33,125 +48,56 @@ class _MyAppState extends State<MyApp> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return Scaffold(
theme: ThemeData( body: ProfilePage(
primarySwatch: Colors.blue, bottomActionText: 'Log out',
itemBuilderOptions: ItemBuilderOptions(
inputDecorationField: {
'firstName': const InputDecoration(
label: Text('First name'),
),
'lastName': const InputDecoration(
label: Text('Last name'),
),
'email': const InputDecoration(
label: Text('E-mail'),
),
},
validators: {
'firstName': (String? value) {
if (value == null || value.isEmpty) {
return 'Field empty';
}
return null;
},
'lastName': (String? value) {
if (value == null || value.isEmpty) {
return 'Field empty';
}
return null;
},
'email': (String? value) {
if (value == null || value.isEmpty) {
return 'Field empty';
}
return null;
},
},
), ),
home: ProfilePage(
service: MyProfileService(),
user: _user, user: _user,
service: ExampleProfileService(),
style: ProfileStyle(
avatarStyle: const AvatarStyle(
displayNameStyle: TextStyle(fontSize: 20),
),
pagePadding: EdgeInsets.only(
top: 50,
bottom: 50,
left: MediaQuery.of(context).size.width * 0.35,
right: MediaQuery.of(context).size.width * 0.35,
),
),
), ),
); );
} }
} }
class MyProfileService extends ProfileService {
@override
deleteProfile() {
return super.deleteProfile();
}
@override
editProfile<T extends ProfileData>(
User<ProfileData> user, String key, String value) {
return super.editProfile(user, key, value);
}
@override
uploadImage() {
return super.uploadImage();
}
}
class MyProfileData extends ProfileData {
MyProfileData({
this.justMyNumber = '1',
this.justMyString = 2,
});
final String justMyNumber;
int justMyString;
@override
Map<String, dynamic> mapWidget(Function update) {
return {
'justMyString': Container(
height: 100,
width: 300,
child: Row(
children: [
ElevatedButton(
style: ElevatedButton.styleFrom(
primary: justMyString == 1 ? Colors.green : Colors.blue,
),
onPressed: () {
justMyString = 1;
update();
print(justMyString);
},
child: const Text('1'),
),
const Spacer(),
ElevatedButton(
style: ElevatedButton.styleFrom(
primary: justMyString == 2 ? Colors.green : Colors.blue,
),
onPressed: () {
justMyString = 2;
update();
print(justMyString);
},
child: const Text('2'),
),
const Spacer(),
ElevatedButton(
style: ElevatedButton.styleFrom(
primary: justMyString == 3 ? Colors.green : Colors.blue,
),
onPressed: () {
justMyString = 3;
update();
},
child: const Text('3'),
),
const Spacer(),
ElevatedButton(
style: ElevatedButton.styleFrom(
primary: justMyString == 4 ? Colors.green : Colors.blue,
),
onPressed: () {
justMyString = 4;
update();
},
child: const Text('4'),
),
],
),
),
'justMyNumber': null,
};
}
@override
ProfileData fromMap(Map<String, dynamic> data) {
return MyProfileData(
justMyNumber: data['justMyNumber'],
justMyString: int.parse(
data['justMyString'].toString(),
),
);
}
@override
Map<String, dynamic> toMap() {
return {
'justMyNumber': justMyNumber,
'justMyString': justMyString,
};
}
@override
ProfileData create() {
return MyProfileData();
}
}

View file

@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:flutter_profile/flutter_profile.dart';
class ExampleProfileData extends ProfileData {
ExampleProfileData({
this.email,
});
String? email;
@override
Map<String, dynamic> mapWidget(
VoidCallback update,
BuildContext context,
) {
return {
'email': null,
};
}
@override
ProfileData fromMap(Map<String, dynamic> data) {
return ExampleProfileData(
email: data['email'],
);
}
@override
Map<String, dynamic> toMap() {
return {
'email': email,
};
}
@override
ProfileData create() {
return ExampleProfileData();
}
}

View file

@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
import 'package:flutter_profile/flutter_profile.dart';
class ExampleProfileService extends ProfileService {
ExampleProfileService();
@override
void pageBottomAction() {
debugPrint('Bottom page action');
}
@override
void editProfile(
User user,
String key,
String value,
) {
debugPrint('Editing key: $key with $value');
}
@override
Future<void> uploadImage(BuildContext context) async {
debugPrint('Updating avatar');
}
}

View file

@ -7,7 +7,7 @@ packages:
name: async name: async
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.8.2" version: "2.9.0"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@ -21,21 +21,14 @@ packages:
name: characters name: characters
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0" version: "1.2.1"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.1"
clock: clock:
dependency: transitive dependency: transitive
description: description:
name: clock name: clock
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0" version: "1.1.1"
collection: collection:
dependency: transitive dependency: transitive
description: description:
@ -56,7 +49,7 @@ packages:
name: fake_async name: fake_async
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.0" version: "1.3.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -69,6 +62,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.1" version: "2.0.1"
flutter_profile:
dependency: "direct main"
description:
path: ".."
relative: true
source: path
version: "0.0.1"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -87,35 +87,28 @@ packages:
name: matcher name: matcher
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.12.11" version: "0.12.12"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.4" version: "0.1.5"
meta: meta:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.7.0" version: "1.8.0"
path: path:
dependency: transitive dependency: transitive
description: description:
name: path name: path
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.1" version: "1.8.2"
profile:
dependency: "direct main"
description:
path: ".."
relative: true
source: path
version: "0.0.1"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -127,7 +120,7 @@ packages:
name: source_span name: source_span
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.2" version: "1.9.0"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -148,21 +141,21 @@ packages:
name: string_scanner name: string_scanner
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0" version: "1.1.1"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
name: term_glyph name: term_glyph
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0" version: "1.2.1"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.9" version: "0.4.12"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:

View file

@ -34,7 +34,7 @@ dependencies:
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2 cupertino_icons: ^1.0.2
profile: flutter_profile:
path: ../ path: ../
dev_dependencies: dev_dependencies:

9
lib/flutter_profile.dart Normal file
View file

@ -0,0 +1,9 @@
library flutter_profile;
export 'src/widgets/profile/profile_page.dart';
export 'src/widgets/profile/profile_style.dart';
export 'src/widgets/avatar/avatar_style.dart';
export 'src/services/profile_service.dart';
export 'src/widgets/item_builder/item_builder.dart';
export 'src/models/user.dart';
export 'src/widgets/item_builder/item_builder_options.dart';

View file

@ -1,6 +0,0 @@
library profile;
export 'src/widgets/profile/profile_page.dart';
export 'src/services/profile_service.dart';
export 'src/widgets/item_builder/item_builder.dart';
export 'src/models/user.dart';

View file

@ -1,15 +1,15 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:profile/profile.dart';
import 'package:profile/src/widgets/item_builder/item_builder.dart';
import 'package:profile/src/widgets/item_builder/item_builder_options.dart';
class User<T extends ProfileData> { /// User is used to contain all user data. It consists of three standard fields: firstName, lastName and image.
///
/// For additional data profileData can be used.
class User {
String? firstName; String? firstName;
String? lastName; String? lastName;
Uint8List? image; Uint8List? image;
T? profileData; ProfileData? profileData;
User( User(
this.firstName, this.firstName,
@ -37,6 +37,11 @@ class User<T extends ProfileData> {
} }
} }
/// ProfileData is used to store custom/addintional data for a user.
///
/// The MapWidget method is used to bind a [Widget] to one of the keys. This will override the standard textfield.
///
/// The Builditems method is used to make the list of field to house the user data.
abstract class ProfileData { abstract class ProfileData {
const ProfileData(); const ProfileData();
@ -44,46 +49,7 @@ abstract class ProfileData {
Map<String, dynamic> toMap(); Map<String, dynamic> toMap();
Map<String, dynamic> mapWidget(Function update); Map<String, dynamic> mapWidget(VoidCallback update, BuildContext context);
ProfileData create(); ProfileData create();
List<Widget> buildItems(
Map<String, dynamic> items,
Map<String, dynamic> typeMap,
double spacing,
Function(String, String) updateProfile, {
ItemBuilder? itemBuilder,
ItemBuilderOptions? itemBuilderOptions,
}) {
var widgets = <Widget>[];
ItemBuilder builder = ItemBuilder(
options: itemBuilderOptions ?? const ItemBuilderOptions(),
);
for (var item in items.entries) {
itemBuilder == null
? widgets.add(
builder.build(
item.value,
typeMap[item.key],
(value) {
updateProfile(item.key, value);
},
),
)
: widgets.add(
itemBuilder.build(
item.value,
typeMap[item.key],
(value) {
updateProfile(item.key, value);
},
),
);
widgets.add(SizedBox(
height: spacing,
));
}
return widgets;
}
} }

View file

@ -1,26 +1,21 @@
import 'package:profile/profile.dart'; import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_profile/src/models/user.dart';
/// ProfileService can be extended and set for the profilePage. The following method can be overriden.
///
/// BottompageAction is called when the [InkWell] at the bottom of the page is tapped.
///
/// EditProfile is called when a user changes and submits a standard textfields.
///
/// UploadImage is called when te user presses the avatar.
abstract class ProfileService { abstract class ProfileService {
const ProfileService(); const ProfileService();
deleteProfile() { FutureOr<void> pageBottomAction();
print("Request to delete profile");
// TODO(anyone) project specific
}
editProfile<T extends ProfileData>(User user, String key, String value) { FutureOr<void> editProfile(User user, String key, String value);
if (user.profileData != null) {
var map = user.profileData!.toMap();
if (map.containsKey(key)) {
map[key] = value;
var profile = user.profileData!.create();
user.profileData = profile.fromMap(map);
}
}
}
uploadImage() { FutureOr<void> uploadImage(BuildContext context);
print('Request to change picture');
// TODO(anyone) open image picker and update profile
}
} }

View file

@ -1,19 +1,21 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:profile/src/widgets/avatar/avatar_style.dart'; import 'package:flutter_profile/src/widgets/avatar/avatar_style.dart';
class Avatar extends StatelessWidget { class Avatar extends StatelessWidget {
const Avatar({ const Avatar({
Key? key, Key? key,
this.image, this.image,
this.name, this.firstName,
this.lastName,
this.avatar, this.avatar,
this.style = const AvatarStyle(), this.style = const AvatarStyle(),
}) : super(key: key); }) : super(key: key);
final Uint8List? image; final Uint8List? image;
final String? name; final String? firstName;
final String? lastName;
final Widget? avatar; final Widget? avatar;
final AvatarStyle style; final AvatarStyle style;
@ -22,9 +24,12 @@ class Avatar extends StatelessWidget {
return Column( return Column(
children: [ children: [
_avatar(), _avatar(),
if (name != null) const SizedBox(
height: 16,
),
if (firstName != null || lastName != null)
Text( Text(
name!, '${firstName ?? ''} ${lastName ?? ''}',
style: style.displayNameStyle, style: style.displayNameStyle,
) )
], ],
@ -39,43 +44,41 @@ class Avatar extends StatelessWidget {
return Container( return Container(
width: style.width, width: style.width,
height: style.height, height: style.height,
decoration: const BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
image: DecorationImage(
image: MemoryImage(image!),
fit: BoxFit.fill,
),
), ),
child: Image.memory(image!),
); );
} else if (name != null && name!.isNotEmpty) { } else if (firstName != null || lastName != null) {
return Container( return Container(
width: style.width, width: style.width,
height: style.height, height: style.height,
decoration: BoxDecoration( decoration: BoxDecoration(
color: _generateColorWithIntials(name!), color: _generateColorWithIntials(firstName, lastName),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: Center( child: Center(
child: Text( child: Text(
_getInitials(name!), style: const TextStyle(fontSize: 40),
_getInitials(firstName, lastName),
), ),
), ),
); );
} else { } else {
return SizedBox( return Container();
width: style.width,
height: style.height,
// TODO(anyone) child fallback image
);
} }
} }
String _getInitials(String name) { String _getInitials(String? firstName, String? lastName) {
var nameList = name.split(' '); return (firstName?[0] ?? '') + (lastName?[0] ?? '');
return nameList.first[0] + nameList.last[0];
} }
Color _generateColorWithIntials(String name) { Color _generateColorWithIntials(String? firstName, String? lastName) {
var nameList = name.split(' '); var uniqueInitialId = (firstName?.toLowerCase().codeUnitAt(0) ?? 0) +
var uniqueInitialId = nameList.first.toLowerCase().codeUnitAt(0) + (lastName?.toLowerCase().codeUnitAt(0) ?? 0);
nameList.last.toLowerCase().codeUnitAt(0);
return Colors.primaries[uniqueInitialId % Colors.primaries.length]; return Colors.primaries[uniqueInitialId % Colors.primaries.length];
} }

View file

@ -1,5 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// AvatarStyle is used to set the style of the avatar displayed at the top of the [ProfilePage].
///
/// Width is used to set the width of the avatar. Defaults to 100.
///
/// Height is used to set the height of the avatar. Defaults to 100.
///
/// InitialStyle sets the [TextStyle] of the initial which are shown when no image is provided.
///
/// DisplayNameStyle sets the [TextStyle] for the displayname underneath the avatar.
class AvatarStyle { class AvatarStyle {
const AvatarStyle({ const AvatarStyle({
this.width = 100, this.width = 100,

View file

@ -1,6 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:profile/src/widgets/item_builder/item_builder_options.dart'; import 'package:flutter_profile/src/widgets/item_builder/item_builder_options.dart';
/// ItemBuilder is used to set the standard textfield for each undefined users data item.
///
/// Options sets options for the textfield.
class ItemBuilder { class ItemBuilder {
ItemBuilder({ ItemBuilder({
required this.options, required this.options,
@ -8,19 +11,34 @@ class ItemBuilder {
final ItemBuilderOptions options; final ItemBuilderOptions options;
Widget build(dynamic value, Widget? widget, Function(String) updateItem) { Widget build(String key, GlobalKey<FormState> formKey, dynamic value,
Widget? widget, Function(String) updateItem) {
if (widget == null) { if (widget == null) {
var controller = TextEditingController( var controller = TextEditingController(
text: '$value', text: '${value ?? ''}',
); );
return TextField( late InputDecoration inputDecoration;
inputDecoration =
options.inputDecorationField?[key] ?? options.inputDecoration;
return Form(
key: formKey,
child: TextFormField(
key: Key(key),
controller: controller, controller: controller,
decoration: options.inputDecoration, decoration: inputDecoration,
readOnly: options.readOnly, readOnly: options.readOnly,
onSubmitted: (s) { onFieldSubmitted: (value) {
updateItem(s); if (formKey.currentState!.validate()) {
updateItem(value);
}
}, },
validator: (value) {
return options.validators?[key]?.call(value);
},
),
); );
} }
return widget; return widget;

View file

@ -1,11 +1,22 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// ItemBuilderOptions is a class to store all settings for a field in the profile page.
///
/// InputDecoration sets the decoration for all standard textfields. This is overridden if a field specific decoration is set by inputDecorationField.
///
/// inputDecorationField sets the inputdecoration by key of the user data field. So a field can have its own specific decoration.
///
/// Validator can be used to set a validator for the standard textfield.
class ItemBuilderOptions { class ItemBuilderOptions {
const ItemBuilderOptions({ ItemBuilderOptions({
this.inputDecoration = const InputDecoration(), this.inputDecoration = const InputDecoration(),
this.inputDecorationField,
this.readOnly = false, this.readOnly = false,
this.validators,
}); });
final InputDecoration inputDecoration; final InputDecoration inputDecoration;
final Map<String, InputDecoration>? inputDecorationField;
final bool readOnly; final bool readOnly;
final Map<String, String? Function(String?)>? validators;
} }

View file

@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
import 'package:flutter_profile/src/widgets/item_builder/item_builder.dart';
import 'package:flutter_profile/src/widgets/item_builder/item_builder_options.dart';
class ItemList extends StatefulWidget {
const ItemList(
this.items,
this.typeMap,
this.spacing,
this.updateProfile, {
this.itemBuilder,
this.itemBuilderOptions,
super.key,
});
final Map<String, dynamic> items;
final Map<String, dynamic> typeMap;
final double spacing;
final Function(String, String) updateProfile;
final ItemBuilder? itemBuilder;
final ItemBuilderOptions? itemBuilderOptions;
@override
State<ItemList> createState() => _ItemListState();
}
class _ItemListState extends State<ItemList> {
Map<String, GlobalKey<FormState>> formKeys = {};
@override
void initState() {
super.initState();
for (var item in widget.items.entries) {
formKeys.addAll(
{item.key: GlobalKey<FormState>()},
);
}
}
@override
Widget build(BuildContext context) {
var widgets = <Widget>[];
ItemBuilder builder = ItemBuilder(
options: widget.itemBuilderOptions ?? ItemBuilderOptions(),
);
for (var item in widget.items.entries) {
widget.itemBuilder == null
? widgets.add(
builder.build(
item.key,
formKeys['item.key'] ?? GlobalKey<FormState>(),
item.value,
widget.typeMap[item.key],
(value) {
widget.updateProfile(item.key, value);
},
),
)
: widgets.add(
widget.itemBuilder!.build(
item.key,
formKeys['item.key'] ?? GlobalKey<FormState>(),
item.value,
widget.typeMap[item.key],
(value) {
widget.updateProfile(item.key, value);
},
),
);
widgets.add(SizedBox(
height: widget.spacing,
));
}
return Column(
children: widgets,
);
}
}

View file

@ -1,10 +1,28 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:profile/profile.dart'; import 'package:flutter_profile/src/models/user.dart';
import 'package:profile/src/widgets/avatar/avatar.dart'; import 'package:flutter_profile/src/services/profile_service.dart';
import 'package:profile/src/widgets/item_builder/item_builder_options.dart'; import 'package:flutter_profile/src/widgets/item_builder/item_builder.dart';
import 'package:flutter_profile/src/widgets/item_builder/item_builder_options.dart';
import 'package:profile/src/widgets/profile/profile_style.dart'; import 'package:flutter_profile/src/widgets/profile/profile_style.dart';
import 'package:flutter_profile/src/widgets/profile/proifle_wrapper.dart';
/// The ProfilePage widget is able to show the data of a user. By default the user is able to change this data. The widget has a couple of parameters listed below:
///
/// User will contain the data of the user which atleast contain a first name, last name and an avatar/image. Besides this information the [ProfileData] can be used to set custom user fields.
///
/// With the use of the service set by a [ProfileService] some actions can be determined what should occur when the user does the following actions: Deleting/editing the profile or uploading an image.
///
/// The style can be used the set some style options regarding the whole form. This is done by setting a [ProfileStyle]. The following styling can be set: The style of the avatar, the padding of the page and default padding between items.
///
/// CustomAvatar can be set to override the standard avatar using any [Widget].
///
/// ShowAvatar can be set using a [bool] to determine whether the avatar should be shown and be able to be set by the user. Default set to true.
///
/// BottomActionText sets the text for the inkwell at the bottom of the page. If this is set the null then the [InkWell] is disabled.
///
/// ItemBuilder is used to determine how the user data is represented.
///
/// ItemBuilderOptions can be used to just set the settings for fields instead of defining the field itself and how it is used. This field should not be used when the itemBuilder is set.
class ProfilePage extends StatefulWidget { class ProfilePage extends StatefulWidget {
const ProfilePage({ const ProfilePage({
Key? key, Key? key,
@ -15,17 +33,31 @@ class ProfilePage extends StatefulWidget {
this.showAvatar = true, this.showAvatar = true,
this.itemBuilder, this.itemBuilder,
this.itemBuilderOptions, this.itemBuilderOptions,
this.showDeleteProfile = true, this.bottomActionText,
}) : super(key: key); }) : super(key: key);
/// User containing all the user data.
final User user; final User user;
/// The service the determine what should happen when the user takes action.
final ProfileService service; final ProfileService service;
/// Style to set some general styling parameters fot the whole page.
final ProfileStyle style; final ProfileStyle style;
/// The way to override the standard avatar is needed.
final Widget? customAvatar; final Widget? customAvatar;
/// Whether to show the users avatar.
final bool showAvatar; final bool showAvatar;
final bool showDeleteProfile;
/// Sets the text for the [InkWell] at the bottom of the profile page. The [InkWell] is disabled when null.
final String? bottomActionText;
/// Itembuilder is used the build each field in the user.
final ItemBuilder? itemBuilder; final ItemBuilder? itemBuilder;
/// Used to set settings of eacht field in user.
final ItemBuilderOptions? itemBuilderOptions; final ItemBuilderOptions? itemBuilderOptions;
@override @override
@ -44,132 +76,10 @@ class _ProfilePageState extends State<ProfilePage> {
style: widget.style, style: widget.style,
customAvatar: widget.customAvatar, customAvatar: widget.customAvatar,
showAvatar: widget.showAvatar, showAvatar: widget.showAvatar,
showDeleteProfile: widget.showDeleteProfile, bottomActionText: widget.bottomActionText,
itemBuilder: widget.itemBuilder, itemBuilder: widget.itemBuilder,
itemBuilderOptions: widget.itemBuilderOptions, itemBuilderOptions: widget.itemBuilderOptions,
key: UniqueKey(), key: UniqueKey(),
); );
} }
} }
class ProfileWrapper extends StatefulWidget {
const ProfileWrapper({
Key? key,
required this.user,
required this.service,
required this.rebuild,
this.style = const ProfileStyle(),
this.customAvatar,
this.showAvatar = true,
this.itemBuilder,
this.itemBuilderOptions,
this.showDeleteProfile = true,
}) : super(key: key);
final User user;
final ProfileService service;
final ProfileStyle style;
final Widget? customAvatar;
final bool showAvatar;
final bool showDeleteProfile;
final ItemBuilder? itemBuilder;
final Function rebuild;
final ItemBuilderOptions? itemBuilderOptions;
@override
State<ProfileWrapper> createState() => _ProfileWrapperState();
}
class _ProfileWrapperState extends State<ProfileWrapper> {
late List<Widget> profileItems;
List<Widget> defaultItems = [];
@override
void initState() {
super.initState();
profileItems = widget.user.profileData!.buildItems(
widget.user.profileData!.toMap(),
widget.user.profileData!.mapWidget(() {
widget.rebuild();
}),
widget.style.betweenDefaultItemPadding,
(key, value) {
widget.service.editProfile(widget.user, key, value);
},
itemBuilder: widget.itemBuilder,
itemBuilderOptions: widget.itemBuilderOptions,
);
if (widget.itemBuilder == null) {
ItemBuilder builder = ItemBuilder(
options: widget.itemBuilderOptions ?? const ItemBuilderOptions(),
);
defaultItems.add(builder.build(widget.user.firstName, null, (v) {
widget.user.firstName = v;
}));
defaultItems.add(
SizedBox(
height: widget.style.betweenDefaultItemPadding,
),
);
defaultItems.add(builder.build(widget.user.lastName, null, (v) {
widget.user.lastName = v;
}));
} else {
defaultItems
.add(widget.itemBuilder!.build(widget.user.firstName, null, (v) {
widget.user.firstName = v;
}));
defaultItems.add(
SizedBox(
height: widget.style.betweenDefaultItemPadding,
),
);
defaultItems
.add(widget.itemBuilder!.build(widget.user.lastName, null, (v) {
widget.user.lastName = v;
}));
}
}
@override
Widget build(BuildContext context) {
return Material(
child: Padding(
padding: widget.style.pagePadding,
child: Column(
children: [
if (widget.showAvatar)
InkWell(
onTap: () {
widget.service.uploadImage();
},
child: Avatar(
name: '${widget.user.firstName} ${widget.user.lastName}',
style: widget.style.avatarStyle,
avatar: widget.customAvatar,
image: widget.user.image,
),
),
if (widget.showAvatar)
SizedBox(
height: widget.style.betweenDefaultItemPadding,
),
...defaultItems,
...profileItems,
if (widget.showDeleteProfile)
SizedBox(
height: widget.style.betweenDefaultItemPadding,
),
if (widget.showDeleteProfile)
InkWell(
onTap: () {
widget.service.deleteProfile();
},
child: const Text('Profiel verwijderen'),
),
],
),
),
);
}
}

View file

@ -1,6 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:profile/src/widgets/avatar/avatar_style.dart'; import 'package:flutter_profile/src/widgets/avatar/avatar_style.dart';
/// ProfielStyle is used to set a couple of style paramaters for the whole profile page.
///
/// AvatarStyle is used to set some styling for the avatar using [AvatarStyle].
///
/// PagePaddign is used to set the padding around the whole profile page with its parent.
///
/// BetweenDefaultitemPadding sets te padding between each user data item.
class ProfileStyle { class ProfileStyle {
const ProfileStyle({ const ProfileStyle({
this.avatarStyle = const AvatarStyle(), this.avatarStyle = const AvatarStyle(),
@ -8,7 +15,12 @@ class ProfileStyle {
this.pagePadding = EdgeInsets.zero, this.pagePadding = EdgeInsets.zero,
}); });
/// AvatarStyle can be used to set some avatar styling parameters.
final AvatarStyle avatarStyle; final AvatarStyle avatarStyle;
/// PagePadding can be set to determine the padding of the whole page againt the profile page parent.
final EdgeInsetsGeometry pagePadding; final EdgeInsetsGeometry pagePadding;
/// BetweenDefaultItemPadding sets the
final double betweenDefaultItemPadding; final double betweenDefaultItemPadding;
} }

View file

@ -0,0 +1,180 @@
import 'package:flutter/material.dart';
import 'package:flutter_profile/src/models/user.dart';
import 'package:flutter_profile/src/services/profile_service.dart';
import 'package:flutter_profile/src/widgets/avatar/avatar.dart';
import 'package:flutter_profile/src/widgets/item_builder/item_builder.dart';
import 'package:flutter_profile/src/widgets/item_builder/item_builder_options.dart';
import 'package:flutter_profile/src/widgets/item_builder/item_list.dart';
import 'package:flutter_profile/src/widgets/profile/profile_style.dart';
class ProfileWrapper extends StatefulWidget {
const ProfileWrapper({
Key? key,
required this.user,
required this.service,
required this.rebuild,
this.style = const ProfileStyle(),
this.customAvatar,
this.showAvatar = true,
this.itemBuilder,
this.itemBuilderOptions,
this.bottomActionText,
}) : super(key: key);
final User user;
final ProfileService service;
final ProfileStyle style;
final Widget? customAvatar;
final bool showAvatar;
final String? bottomActionText;
final ItemBuilder? itemBuilder;
final Function rebuild;
final ItemBuilderOptions? itemBuilderOptions;
@override
State<ProfileWrapper> createState() => _ProfileWrapperState();
}
class _ProfileWrapperState extends State<ProfileWrapper> {
List<Widget> defaultItems = [];
GlobalKey<FormState> firstNameKey = GlobalKey<FormState>();
GlobalKey<FormState> lastNameKey = GlobalKey<FormState>();
@override
void initState() {
super.initState();
if (widget.itemBuilder == null) {
ItemBuilder builder = ItemBuilder(
options: widget.itemBuilderOptions ?? ItemBuilderOptions(),
);
defaultItems.add(
builder.build(
'firstName',
firstNameKey,
widget.user.firstName,
null,
(v) {
widget.user.firstName = v;
widget.service.editProfile(widget.user, 'firstName', v);
},
),
);
defaultItems.add(
SizedBox(
height: widget.style.betweenDefaultItemPadding,
),
);
defaultItems.add(
builder.build(
'lastName',
lastNameKey,
widget.user.lastName,
null,
(v) {
widget.user.lastName = v;
widget.service.editProfile(widget.user, 'lastName', v);
},
),
);
} else {
defaultItems.add(
widget.itemBuilder!.build(
'firstName',
firstNameKey,
widget.user.firstName,
null,
(v) {
widget.user.firstName = v;
widget.service.editProfile(widget.user, 'firstname', v);
},
),
);
defaultItems.add(
SizedBox(
height: widget.style.betweenDefaultItemPadding,
),
);
defaultItems.add(
widget.itemBuilder!.build(
'lastName',
lastNameKey,
widget.user.lastName,
null,
(v) {
widget.user.lastName = v;
widget.service.editProfile(widget.user, 'lastName', v);
},
),
);
}
}
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: Padding(
padding: widget.style.pagePadding,
child: Column(
children: [
if (widget.showAvatar)
InkWell(
onTap: () async {
await widget.service.uploadImage(context);
},
child: Avatar(
firstName: widget.user.firstName,
lastName: widget.user.lastName,
style: widget.style.avatarStyle,
avatar: widget.customAvatar,
image: widget.user.image,
),
),
if (widget.showAvatar)
SizedBox(
height: widget.style.betweenDefaultItemPadding,
),
...defaultItems,
ItemList(
widget.user.profileData!.toMap(),
widget.user.profileData!.mapWidget(
() {
widget.rebuild();
},
context,
),
widget.style.betweenDefaultItemPadding,
(key, value) {
widget.service.editProfile(widget.user, key, value);
},
itemBuilder: widget.itemBuilder,
itemBuilderOptions: widget.itemBuilderOptions,
),
if (widget.bottomActionText != null)
SizedBox(
height: widget.style.betweenDefaultItemPadding,
),
const Spacer(),
if (widget.bottomActionText != null)
InkWell(
onTap: () {
widget.service.pageBottomAction();
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(widget.bottomActionText!),
),
),
],
),
),
);
}
}

View file

@ -1,4 +1,4 @@
name: profile name: flutter_profile
description: A new Flutter package project. description: A new Flutter package project.
version: 0.0.1 version: 0.0.1
homepage: homepage:

View file

@ -1 +1,94 @@
// todo import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_profile/flutter_profile.dart';
import 'package:flutter_test/flutter_test.dart';
import 'test_classes/test_profile_data.dart';
import 'test_classes/test_profile_service.dart';
void main() {
testWidgets('Profile page with preset values', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: ProfilePage(
user: User(
'Firstname',
'Lastname',
Uint8List.fromList([]),
TestProfileData(email: 'test@email.com'),
),
service: TestProfileService(),
),
),
),
);
final firstNameFinder = find.text('Firstname');
final lastNameFinder = find.text('Lastname');
final emailFinder = find.text('test@email.com');
expect(firstNameFinder, findsOneWidget);
expect(lastNameFinder, findsOneWidget);
expect(emailFinder, findsOneWidget);
});
testWidgets('Profile page without preset value', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: ProfilePage(
user: User(
null,
null,
null,
TestProfileData(email: null),
),
service: TestProfileService(),
),
),
),
);
});
testWidgets('Profile page with preset value and changing them',
(tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: ProfilePage(
user: User(
'Firstname',
'Lastname',
null,
TestProfileData(email: 'test@email.com'),
),
service: TestProfileService(),
),
),
),
);
await tester.enterText(find.text('Firstname'), 'FirstEditedName');
await tester.testTextInput.receiveAction(TextInputAction.send);
await tester.pump();
await tester.enterText(find.text('test@email.com'), 'edited@emial.com');
await tester.testTextInput.receiveAction(TextInputAction.send);
await tester.pump();
final firstNameFinder = find.text('Firstname');
final firstNameEditedFinder = find.text('FirstEditedName');
final lastNameFinder = find.text('Lastname');
final emailFinder = find.text('test@email.com');
final emailEditedFinder = find.text('edited@emial.com');
expect(firstNameFinder, findsNothing);
expect(firstNameEditedFinder, findsOneWidget);
expect(lastNameFinder, findsOneWidget);
expect(emailFinder, findsNothing);
expect(emailEditedFinder, findsOneWidget);
});
}

View file

@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:flutter_profile/flutter_profile.dart';
class TestProfileData extends ProfileData {
TestProfileData({
this.email,
});
String? email;
@override
Map<String, dynamic> mapWidget(
VoidCallback update,
BuildContext context,
) {
return {
'email': null,
};
}
@override
ProfileData fromMap(Map<String, dynamic> data) {
return TestProfileData(
email: data['email'],
);
}
@override
Map<String, dynamic> toMap() {
return {
'email': email,
};
}
@override
ProfileData create() {
return TestProfileData();
}
}

View file

@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
import 'package:flutter_profile/flutter_profile.dart';
class TestProfileService extends ProfileService {
TestProfileService();
@override
void pageBottomAction() {}
@override
void editProfile(
User user,
String key,
String value,
) {}
@override
Future<void> uploadImage(BuildContext context) async {}
}