Screens / account
Edit profile
Lets people change the photo, name, username checked while typing and bio of their profile, and asks before discarding unsaved changes.
Import
Edit profile is part of Fliin Pro. With a license key, sign in once with fliin login, then add it in a Flutter app where fliin init has run:
fliin login
fliin add edit-profileAlso installs: alert, app-bar, avatar, button, confirm-delete, core, empty-state, link-button, page, sheet, skeleton, text-field, icons
Examples
Simple
A profile with a photo and a name only: showUsername and showBio remove the fields your app does not have, and the draft keeps their values unchanged.
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_controller.dart';
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_data.dart';
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_screen.dart';
import 'package:flutter/widgets.dart';
class EditProfileSimpleExample extends StatelessWidget {
const EditProfileSimpleExample({super.key});
@override
Widget build(BuildContext context) {
final profile = ProfileData(
name: sampleProfile.name,
username: sampleProfile.username,
bio: sampleProfile.bio,
photo: const AssetImage('images/people/1.jpg'),
);
return EditProfileScreen.standalone(
service: SampleEditProfileService(profile: profile, latency: const Duration(milliseconds: 600)),
initial: EditProfileEditing(saved: profile, draft: profile),
showUsername: false,
showBio: false,
onPickPhoto: (source) async => const AssetImage('images/people/3.jpg'),
onBack: () {},
);
}
}Ready
The profile as saved: photo, name, username and bio. standalone holds its own controller over the service you pass.
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_controller.dart';
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_data.dart';
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_screen.dart';
import 'package:flutter/widgets.dart';
class EditProfileReadyExample extends StatelessWidget {
const EditProfileReadyExample({super.key});
@override
Widget build(BuildContext context) {
return EditProfileScreen.standalone(
service: const SampleEditProfileService(latency: Duration(milliseconds: 600)),
initial: const EditProfileEditing(saved: sampleProfile, draft: sampleProfile),
onBack: () {},
);
}
}Modified
Changes not saved yet: the save action lights up, and an available username is confirmed under its field.
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_controller.dart';
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_data.dart';
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_screen.dart';
import 'package:flutter/widgets.dart';
class EditProfileModifiedExample extends StatelessWidget {
const EditProfileModifiedExample({super.key});
@override
Widget build(BuildContext context) {
return EditProfileScreen.standalone(
service: const SampleEditProfileService(latency: Duration(milliseconds: 600)),
initial: EditProfileEditing(
saved: sampleProfile,
draft: sampleProfile.copyWith(name: 'Camille Dunn', username: 'camille.dunn'),
username: UsernameCheck.available,
),
onBack: () {},
);
}
}Saving
While the changes are saved, the save action shows its spinner.
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_controller.dart';
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_data.dart';
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_screen.dart';
import 'package:flutter/widgets.dart';
class EditProfileSavingExample extends StatelessWidget {
const EditProfileSavingExample({super.key});
@override
Widget build(BuildContext context) {
return EditProfileScreen.standalone(
service: const SampleEditProfileService(latency: Duration(milliseconds: 600)),
initial: EditProfileEditing(
saved: sampleProfile,
draft: sampleProfile.copyWith(name: 'Camille Dunn', username: 'camille.dunn'),
username: UsernameCheck.available,
save: EditProfileSave.saving,
),
onBack: () {},
);
}
}Save error
Saving failed: an alert says so and the changes stay in the form.
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_controller.dart';
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_data.dart';
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_screen.dart';
import 'package:flutter/widgets.dart';
class EditProfileSaveErrorExample extends StatelessWidget {
const EditProfileSaveErrorExample({super.key});
@override
Widget build(BuildContext context) {
return EditProfileScreen.standalone(
service: const SampleEditProfileService(latency: Duration(milliseconds: 600)),
initial: EditProfileEditing(
saved: sampleProfile,
draft: sampleProfile.copyWith(name: 'Camille Dunn', username: 'camille.dunn'),
username: UsernameCheck.available,
save: EditProfileSave.failed,
),
onBack: () {},
);
}
}Loading
While the profile loads, placeholders hold the place of each field.
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_controller.dart';
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_data.dart';
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_screen.dart';
import 'package:flutter/widgets.dart';
class EditProfileLoadingExample extends StatelessWidget {
const EditProfileLoadingExample({super.key});
@override
Widget build(BuildContext context) {
return EditProfileScreen.standalone(
service: const SampleEditProfileService(latency: Duration(milliseconds: 600)),
initial: const EditProfileLoading(),
onBack: () {},
);
}
}Error
When loading fails, the screen says so and offers to try again.
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_controller.dart';
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_data.dart';
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_screen.dart';
import 'package:flutter/widgets.dart';
class EditProfileErrorExample extends StatelessWidget {
const EditProfileErrorExample({super.key});
@override
Widget build(BuildContext context) {
return EditProfileScreen.standalone(
service: const SampleEditProfileService(latency: Duration(milliseconds: 600)),
initial: const EditProfileFailed(),
onBack: () {},
);
}
}Offline
Without a connection, the screen asks to reconnect rather than showing an error.
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_controller.dart';
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_data.dart';
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_screen.dart';
import 'package:flutter/widgets.dart';
class EditProfileOfflineExample extends StatelessWidget {
const EditProfileOfflineExample({super.key});
@override
Widget build(BuildContext context) {
return EditProfileScreen.standalone(
service: const SampleEditProfileService(latency: Duration(milliseconds: 600)),
initial: const EditProfileOffline(),
onBack: () {},
);
}
}Routing
fliin add edit-profile prints the route for the router it finds in your pubspec, a file next to the page that wires its navigation. Each of them:
go_router
// lib/edit_profile_route.dart, next to edit_profile_page.dart.
import 'package:go_router/go_router.dart';
import 'edit_profile_page.dart';
/// Add editProfileRoute to the routes of your GoRouter.
final editProfileRoute = GoRoute(
path: '/settings/profile',
builder: (context, state) => EditProfilePage(
// Alone in the stack (deep link, go): your app's home.
onBack: () => context.canPop() ? context.pop() : context.go('/'),
// Alone in the stack (deep link, go): your app's home.
onSaved: (_) => context.canPop() ? context.pop() : context.go('/'),
),
);navigator
// lib/edit_profile_route.dart, next to edit_profile_page.dart.
import 'package:flutter/material.dart';
import 'edit_profile_page.dart';
/// Opens “Edit profile”: `Navigator.of(context).push(editProfileRoute())`.
Route<void> editProfileRoute() => MaterialPageRoute<void>(
builder: (context) => EditProfilePage(
onBack: () => Navigator.of(context).maybePop(),
onSaved: (_) => Navigator.of(context).maybePop(),
),
);State
The controller is a ChangeNotifier from widgets.dart, with no package. fliin add edit-profile prints the page that holds it with the state management it finds, its imports written for your package; below they read your_app. Your backend implements the Service interface of the screen's _data.dart file:
none
// lib/edit_profile_page.dart
// Your backend: implement EditProfileService (edit_profile_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_data.dart';
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_screen.dart';
import 'package:flutter/widgets.dart';
class EditProfilePage extends StatelessWidget {
const EditProfilePage({
super.key,
required this.onBack,
required this.onSaved,
});
final VoidCallback onBack;
final ValueChanged<ProfileData> onSaved;
@override
Widget build(BuildContext context) => EditProfileScreen.standalone(
service: const SampleEditProfileService(),
onBack: onBack,
onSaved: onSaved,
);
}provider
// lib/edit_profile_page.dart
// Your backend: implement EditProfileService (edit_profile_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_controller.dart';
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_data.dart';
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_screen.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
class EditProfilePage extends StatelessWidget {
const EditProfilePage({
super.key,
required this.onBack,
required this.onSaved,
});
final VoidCallback onBack;
final ValueChanged<ProfileData> onSaved;
@override
Widget build(BuildContext context) => ChangeNotifierProvider(
create: (_) => EditProfileController(service: const SampleEditProfileService())..load(),
child: Builder(
builder: (context) => EditProfileScreen(
controller: context.read<EditProfileController>(),
onBack: onBack,
onSaved: onSaved,
),
),
);
}riverpod
// lib/edit_profile_page.dart
// Your backend: implement EditProfileService (edit_profile_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_controller.dart';
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_data.dart';
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_screen.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final editProfileProvider = Provider.autoDispose<EditProfileController>((ref) {
final controller = EditProfileController(service: const SampleEditProfileService())..load();
ref.onDispose(controller.dispose);
return controller;
});
class EditProfilePage extends ConsumerWidget {
const EditProfilePage({
super.key,
required this.onBack,
required this.onSaved,
});
final VoidCallback onBack;
final ValueChanged<ProfileData> onSaved;
@override
Widget build(BuildContext context, WidgetRef ref) => EditProfileScreen(
controller: ref.watch(editProfileProvider),
onBack: onBack,
onSaved: onSaved,
);
}bloc
// lib/edit_profile_page.dart
// Your backend: implement EditProfileService (edit_profile_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_controller.dart';
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_data.dart';
import 'package:your_app/ui/fliin/screens/edit_profile/edit_profile_screen.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
/// The screen's controller as a bloc, for your BlocListener and BlocBuilder:
/// one emission per controller notification. `version` tells them apart
/// when `state` does not change (mode, filters, text…, read them on
/// `controller`). The screen listens to the controller itself.
class EditProfileCubit extends Cubit<({EditProfileState state, int version})> {
EditProfileCubit(this.controller) : super((state: controller.state, version: 0)) {
controller.addListener(_changed);
}
final EditProfileController controller;
void _changed() => emit((state: controller.state, version: state.version + 1));
@override
Future<void> close() {
controller
..removeListener(_changed)
..dispose();
return super.close();
}
}
class EditProfilePage extends StatelessWidget {
const EditProfilePage({
super.key,
required this.onBack,
required this.onSaved,
});
final VoidCallback onBack;
final ValueChanged<ProfileData> onSaved;
@override
Widget build(BuildContext context) => BlocProvider(
create: (_) => EditProfileCubit(EditProfileController(service: const SampleEditProfileService())..load()),
child: Builder(
builder: (context) => EditProfileScreen(
controller: context.read<EditProfileCubit>().controller,
onBack: onBack,
onSaved: onSaved,
),
),
);
}API
ProfilePhotoPicker: typedef = Future<ImageProvider?> Function(ProfilePhotoSource source)
EditProfileScreen: class extends StatefulWidget
EditProfileScreen.new: const new({super.key, required EditProfileController controller, VoidCallback? onBack, ValueChanged<ProfileData>? onSaved, ProfilePhotoPicker? onPickPhoto, bool showUsername, bool showBio})
EditProfileScreen.standalone: const new({super.key, required EditProfileService service, EditProfileState? initial, VoidCallback? onBack, ValueChanged<ProfileData>? onSaved, ProfilePhotoPicker? onPickPhoto, bool showUsername, bool showBio})
EditProfileScreen.controller: get EditProfileController?
EditProfileScreen.service: get EditProfileService?
EditProfileScreen.initial: get EditProfileState?
EditProfileScreen.onBack: get VoidCallback?
EditProfileScreen.onSaved: get ValueChanged<ProfileData>?
EditProfileScreen.onPickPhoto: get ProfilePhotoPicker?
EditProfileScreen.showUsername: get bool
EditProfileScreen.showBio: get bool
EditProfileScreen.createState: State<EditProfileScreen>()
EditProfileState: sealed class permits {EditProfileEditing, EditProfileFailed, EditProfileLoading, EditProfileOffline}
EditProfileState.new: const new()
EditProfileLoading: final class extends EditProfileState
EditProfileLoading.new: const new()
EditProfileFailed: final class extends EditProfileState
EditProfileFailed.new: const new()
EditProfileOffline: final class extends EditProfileState
EditProfileOffline.new: const new()
UsernameCheck: enum {unchanged, invalid, checking, available, taken, unknown}
UsernameCheck.unchanged: static final UsernameCheck
UsernameCheck.invalid: static final UsernameCheck
UsernameCheck.checking: static final UsernameCheck
UsernameCheck.available: static final UsernameCheck
UsernameCheck.taken: static final UsernameCheck
UsernameCheck.unknown: static final UsernameCheck
UsernameCheck.allowsSave: get bool
EditProfileSave: enum {idle, saving, failed, offline}
EditProfileSave.idle: static final EditProfileSave
EditProfileSave.saving: static final EditProfileSave
EditProfileSave.failed: static final EditProfileSave
EditProfileSave.offline: static final EditProfileSave
EditProfileEditing: final class extends EditProfileState
EditProfileEditing.new: const new({required ProfileData saved, required ProfileData draft, UsernameCheck username, EditProfileSave save})
EditProfileEditing.saved: get ProfileData
EditProfileEditing.draft: get ProfileData
EditProfileEditing.username: get UsernameCheck
EditProfileEditing.save: get EditProfileSave
EditProfileEditing.dirty: get bool
EditProfileEditing.nameMissing: get bool
EditProfileEditing.bioTooLong: get bool
EditProfileEditing.canSave: get bool
EditProfileController: class extends ChangeNotifier
EditProfileController.new: new({required EditProfileService service, EditProfileState initial, Duration debounce})
EditProfileController.service: get EditProfileService
EditProfileController.debounce: get Duration
EditProfileController.state: get EditProfileState
EditProfileController.load: Future<void>()
EditProfileController.setName: void(String name)
EditProfileController.setBio: void(String bio)
EditProfileController.setPhoto: void(ImageProvider? photo)
EditProfileController.setUsername: void(String username)
EditProfileController.save: Future<bool>()
EditProfileController.dispose: void()
ProfileData: final class
ProfileData.new: const new({required String name, required String username, required String bio, ImageProvider? photo})
ProfileData.name: get String
ProfileData.username: get String
ProfileData.bio: get String
ProfileData.photo: get ImageProvider?
ProfileData.initials: get String
ProfileData.copyWith: ProfileData({String? name, String? username, String? bio, ImageProvider? photo, bool removePhoto})
ProfileData.sameAs: bool(ProfileData other)
maxBioLength: const dynamic
isValidUsername: bool(String username)
ProfilePhotoSource: enum {camera, library}
ProfilePhotoSource.camera: static final ProfilePhotoSource
ProfilePhotoSource.library: static final ProfilePhotoSource
EditProfileNoNetwork: final class implements Exception
EditProfileNoNetwork.new: const new()
EditProfileService: abstract interface class
EditProfileService.new: new()
EditProfileService.load: abstract Future<ProfileData>()
EditProfileService.isUsernameAvailable: abstract Future<bool>(String username)
EditProfileService.save: abstract Future<ProfileData>(ProfileData profile)
sampleProfile: const dynamic
SampleEditProfileService: final class implements EditProfileService
SampleEditProfileService.new: const new({ProfileData profile, Set<String> taken, Duration latency})
SampleEditProfileService.profile: get ProfileData
SampleEditProfileService.taken: get Set<String>
SampleEditProfileService.latency: get Duration
SampleEditProfileService.load: Future<ProfileData>()
SampleEditProfileService.isUsernameAvailable: Future<bool>(String username)
SampleEditProfileService.save: Future<ProfileData>(ProfileData profile)
editProfileL10n: const Map<String, Map<String, String>>
EditProfileStrings: final class
EditProfileStrings.new: const new(String language)
EditProfileStrings.of: factory new(BuildContext context)
EditProfileStrings.language: get String
EditProfileStrings.title: get String
EditProfileStrings.back: get String
EditProfileStrings.save: get String
EditProfileStrings.loading: get String
EditProfileStrings.photo: get String
EditProfileStrings.changePhoto: get String
EditProfileStrings.takePhoto: get String
EditProfileStrings.choosePhoto: get String
EditProfileStrings.removePhoto: get String
EditProfileStrings.close: get String
EditProfileStrings.name: get String
EditProfileStrings.nameRequired: get String
EditProfileStrings.username: get String
EditProfileStrings.usernameRule: get String
EditProfileStrings.usernameChecking: get String
EditProfileStrings.usernameAvailable: String(String username)
EditProfileStrings.usernameTaken: get String
EditProfileStrings.usernameUnknown: get String
EditProfileStrings.bio: get String
EditProfileStrings.bioPlaceholder: get String
EditProfileStrings.bioCount: String(int count)
EditProfileStrings.bioTooLong: get String
EditProfileStrings.saveErrorTitle: get String
EditProfileStrings.saveErrorMessage: get String
EditProfileStrings.saveOfflineMessage: get String
EditProfileStrings.discardTitle: get String
EditProfileStrings.discardMessage: get String
EditProfileStrings.discard: get String
EditProfileStrings.keepEditing: get String
EditProfileStrings.errorTitle: get String
EditProfileStrings.errorMessage: get String
EditProfileStrings.offlineTitle: get String
EditProfileStrings.offlineMessage: get String
EditProfileStrings.retry: get String
EditProfileStrings.usernameNote: get StringStrings
Every label in English, French and Arabic. The screen follows the closest Localizations; the same table is in the manifest, ready for your ARB files.
| Key | en | fr | ar |
|---|---|---|---|
editProfileTitle | Edit profile | Modifier le profil | تعديل الملف الشخصي |
editProfileBack | Back | Retour | رجوع |
editProfileSave | Save | Enregistrer | حفظ |
editProfileLoading | Loading your profile | Chargement du profil | جارٍ تحميل ملفك الشخصي |
editProfilePhoto | Profile photo | Photo de profil | صورة الملف الشخصي |
editProfileChangePhoto | Change photo | Changer la photo | تغيير الصورة |
editProfileTakePhoto | Take a photo | Prendre une photo | التقاط صورة |
editProfileChoosePhoto | Choose from library | Choisir dans la galerie | اختيار من المعرض |
editProfileRemovePhoto | Remove photo | Retirer la photo | إزالة الصورة |
editProfileClose | Close | Fermer | إغلاق |
editProfileName | Display name | Nom affiché | الاسم المعروض |
editProfileNameRequired | Enter your name | Saisissez votre nom | أدخل اسمك |
editProfileUsername | Username | Nom d’utilisateur | اسم المستخدم |
editProfileUsernameRule | 3 to 20 lowercase letters, digits, dots or underscores | 3 à 20 minuscules, chiffres, points ou tirets bas | من 3 إلى 20 حرفًا صغيرًا أو رقمًا أو نقطة أو شرطة سفلية |
editProfileUsernameNote | 3 to 20 lowercase letters, digits, dots or underscores for the username. | 3 à 20 minuscules, chiffres, points ou tirets bas pour le nom d’utilisateur. | من 3 إلى 20 حرفًا صغيرًا أو رقمًا أو نقطة أو شرطة سفلية لاسم المستخدم. |
editProfileUsernameChecking | Checking availability… | Vérification de la disponibilité… | جارٍ التحقق من التوفر… |
editProfileUsernameAvailable | @{username} is available | @{username} est disponible | @{username} متاح |
editProfileUsernameTaken | This username is taken | Ce nom d’utilisateur est déjà pris | اسم المستخدم هذا مأخوذ |
editProfileUsernameUnknown | Availability could not be checked | Disponibilité non vérifiée | تعذّر التحقق من التوفر |
editProfileBio | Bio | Bio | نبذة |
editProfileBioPlaceholder | A few words about you | Quelques mots sur vous | بضع كلمات عنك |
editProfileBioCount | {count}/{max} | {count}/{max} | {count}/{max} |
editProfileBioTooLong | {max} characters at most | {max} caractères au plus | {max} حرفًا كحد أقصى |
editProfileSaveErrorTitle | Your changes were not saved | Vos modifications n’ont pas été enregistrées | لم يتم حفظ تغييراتك |
editProfileSaveErrorMessage | Something went wrong on our side. Try saving again. | Un problème est survenu de notre côté. Enregistrez à nouveau. | حدثت مشكلة من جهتنا. أعد الحفظ. |
editProfileSaveOfflineMessage | You are offline. Check your connection, then save again. | Vous êtes hors ligne. Vérifiez votre connexion, puis enregistrez à nouveau. | أنت غير متصل. تحقق من اتصالك ثم احفظ مجددًا. |
editProfileDiscardTitle | Discard your changes? | Abandonner les modifications ? | تجاهل التغييرات؟ |
editProfileDiscardMessage | Your edits to the profile will be lost. | Les modifications du profil seront perdues. | ستفقد التعديلات على ملفك الشخصي. |
editProfileDiscard | Discard | Abandonner | تجاهل |
editProfileKeepEditing | Keep editing | Continuer | متابعة التعديل |
editProfileErrorTitle | Your profile could not load | Impossible de charger le profil | تعذّر تحميل ملفك الشخصي |
editProfileErrorMessage | Something went wrong on our side. | Un problème est survenu de notre côté. | حدثت مشكلة من جهتنا. |
editProfileOfflineTitle | You are offline | Vous êtes hors ligne | أنت غير متصل |
editProfileOfflineMessage | Check your connection, then try again. | Vérifiez votre connexion, puis réessayez. | تحقق من اتصالك ثم أعد المحاولة. |
editProfileRetry | Try again | Réessayer | أعد المحاولة |
Reference renders
The 16 images published with version 0.2.5, rendered by the shared harness on macOS with Flutter 3.47.7. Each shows every state side by side, at the size of an iPhone (390 × 844) or of an Android phone (412 × 915). fliin verify edit-profile compares your copy to them.
















Use it to
- change the profile photo from the camera or the library
- change the username and check that it is free
- edit the profile bio with a character counter
- submit the changes made to the profile
- confirm before leaving a profile with unsaved changes
- Not for
- a single text field in a page: use a text field (text-field)
- account settings, security and deletion: use account settings (account-settings)
- States