← Назад к вопросам
Реализовать профиль пользователя с аватаром
2.0 Middle🔥 201 комментариев
#Flutter виджеты#Нативная интеграция#Хранение данных
Условие
Создайте экран профиля пользователя с возможностью выбора аватара.
Требования
- Отображение аватара пользователя (круглое изображение)
- Возможность выбрать фото из галереи
- Возможность сделать фото с камеры
- Кроппинг изображения (квадрат)
- Сохранение изображения локально
- Отображение имени и email пользователя
- Возможность редактирования данных
Дополнительные баллы
- Сжатие изображения перед сохранением
- Placeholder аватара с инициалами
- Анимация при смене аватара
- Загрузка аватара на сервер
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение: Профиль пользователя с аватаром
Полнофункциональный экран профиля с выбором, кроппингом и загрузкой аватара.
Модель пользователя
class User {
final String id;
final String name;
final String email;
final String? avatarPath;
final DateTime createdAt;
final DateTime? updatedAt;
User({
required this.id,
required this.name,
required this.email,
this.avatarPath,
required this.createdAt,
this.updatedAt,
});
User copyWith({
String? id,
String? name,
String? email,
String? avatarPath,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return User(
id: id ?? this.id,
name: name ?? this.name,
email: email ?? this.email,
avatarPath: avatarPath ?? this.avatarPath,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
}
Контроллер профиля
class ProfileController extends GetxController {
final UserRepository _repository = UserRepository();
final ImageService _imageService = ImageService();
final user = Rxn<User>();
final isLoading = false.obs;
final nameController = TextEditingController();
final emailController = TextEditingController();
@override
void onInit() {
super.onInit();
loadUserProfile();
}
Future<void> loadUserProfile() async {
isLoading.value = true;
try {
final userData = await _repository.getUserProfile();
user.value = userData;
_updateControllers();
} catch (e) {
Get.snackbar('Ошибка', 'Не удалось загрузить профиль');
} finally {
isLoading.value = false;
}
}
void _updateControllers() {
if (user.value != null) {
nameController.text = user.value!.name;
emailController.text = user.value!.email;
}
}
Future<void> selectAvatarFromGallery() async {
try {
final pickedFile = await ImagePicker().pickImage(
source: ImageSource.gallery,
);
if (pickedFile != null) {
await _cropAndSaveAvatar(pickedFile.path);
}
} catch (e) {
Get.snackbar('Ошибка', 'Не удалось выбрать фото');
}
}
Future<void> selectAvatarFromCamera() async {
try {
final pickedFile = await ImagePicker().pickImage(
source: ImageSource.camera,
);
if (pickedFile != null) {
await _cropAndSaveAvatar(pickedFile.path);
}
} catch (e) {
Get.snackbar('Ошибка', 'Не удалось сделать фото');
}
}
Future<void> _cropAndSaveAvatar(String imagePath) async {
try {
final croppedFile = await ImageCropper().cropImage(
sourcePath: imagePath,
aspectRatio: CropAspectRatio(ratioX: 1, ratioY: 1),
uiSettings: [
AndroidUiSettings(
toolbarTitle: 'Обрезать аватар',
toolbarColor: Color(0xFF667EEA),
toolbarWidgetColor: Colors.white,
initAspectRatio: CropAspectRatioPreset.square,
lockAspectRatio: true,
),
IOSUiSettings(
title: 'Обрезать аватар',
),
],
);
if (croppedFile != null) {
await _processAndSaveAvatar(croppedFile.path);
}
} catch (e) {
Get.snackbar('Ошибка', 'Не удалось обрезать изображение');
}
}
Future<void> _processAndSaveAvatar(String imagePath) async {
isLoading.value = true;
try {
// Сжимаем изображение
final compressedPath = await _imageService.compressImage(imagePath);
// Сохраняем локально
final savedPath = await _imageService.saveImageLocally(compressedPath);
// Обновляем пользователя
final updatedUser = user.value!.copyWith(
avatarPath: savedPath,
updatedAt: DateTime.now(),
);
// Загружаем на сервер
await _repository.updateUserProfile(updatedUser);
user.value = updatedUser;
Get.snackbar('Успех', 'Аватар обновлён');
} catch (e) {
Get.snackbar('Ошибка', 'Не удалось сохранить аватар');
} finally {
isLoading.value = false;
}
}
Future<void> updateProfile() async {
isLoading.value = true;
try {
final updatedUser = user.value!.copyWith(
name: nameController.text,
email: emailController.text,
updatedAt: DateTime.now(),
);
await _repository.updateUserProfile(updatedUser);
user.value = updatedUser;
Get.snackbar('Успех', 'Профиль обновлён');
} catch (e) {
Get.snackbar('Ошибка', 'Не удалось обновить профиль');
} finally {
isLoading.value = false;
}
}
@override
void onClose() {
nameController.dispose();
emailController.dispose();
super.onClose();
}
}
Сервис обработки изображений
class ImageService {
Future<String> compressImage(String imagePath) async {
final imageFile = File(imagePath);
final tempDir = await getTemporaryDirectory();
final targetPath = '${tempDir.absolute.path}/compressed_${DateTime.now().millisecondsSinceEpoch}.jpg';
final result = await FlutterImageCompress.compressAndGetFile(
imagePath,
targetPath,
quality: 85,
format: CompressFormat.jpeg,
);
return result?.path ?? imagePath;
}
Future<String> saveImageLocally(String imagePath) async {
final appDir = await getApplicationDocumentsDirectory();
final fileName = 'avatar_${DateTime.now().millisecondsSinceEpoch}.jpg';
final savedPath = '${appDir.path}/$fileName';
await File(imagePath).copy(savedPath);
return savedPath;
}
}
Главная страница профиля
class ProfilePage extends StatefulWidget {
@override
_ProfilePageState createState() => _ProfilePageState();
}
class _ProfilePageState extends State<ProfilePage> with TickerProviderStateMixin {
final ProfileController _controller = Get.put(ProfileController());
late AnimationController _scaleController;
@override
void initState() {
super.initState();
_scaleController = AnimationController(
duration: Duration(milliseconds: 500),
vsync: this,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Профиль'),
elevation: 0,
backgroundColor: Color(0xFF667EEA),
),
body: Obx(() {
if (_controller.isLoading.value && _controller.user.value == null) {
return Center(child: CircularProgressIndicator());
}
return SingleChildScrollView(
child: Column(
children: [
SizedBox(height: 24),
_buildAvatarSection(),
SizedBox(height: 32),
_buildProfileForm(),
SizedBox(height: 32),
_buildActionButtons(),
SizedBox(height: 24),
],
),
);
}),
);
}
Widget _buildAvatarSection() {
return Center(
child: Stack(
children: [
Obx(() {
final avatarPath = _controller.user.value?.avatarPath;
final name = _controller.user.value?.name ?? 'User';
return ScaleTransition(
scale: Tween(begin: 0.8, end: 1.0).animate(
CurvedAnimation(parent: _scaleController, curve: Curves.elasticOut),
),
child: Container(
width: 150,
height: 150,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Color(0xFF667EEA),
boxShadow: [
BoxShadow(
color: Color(0xFF667EEA).withOpacity(0.3),
blurRadius: 8,
offset: Offset(0, 4),
),
],
),
child: avatarPath != null && avatarPath.isNotEmpty
? ClipRRect(
borderRadius: BorderRadius.circular(75),
child: Image.file(
File(avatarPath),
fit: BoxFit.cover,
),
)
: _buildAvatarPlaceholder(name),
),
);
}),
Positioned(
bottom: 0,
right: 0,
child: GestureDetector(
onTap: _showAvatarOptions,
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
),
],
),
child: Icon(Icons.camera_alt, color: Color(0xFF667EEA)),
),
),
),
],
),
);
}
Widget _buildAvatarPlaceholder(String name) {
final initials = name.split(' ').map((e) => e[0]).join('').toUpperCase();
return Center(
child: Text(
initials.length > 2 ? initials.substring(0, 2) : initials,
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
);
}
void _showAvatarOptions() {
Get.bottomSheet(
Container(
color: Colors.white,
child: Wrap(
children: [
ListTile(
leading: Icon(Icons.photo_library),
title: Text('Выбрать из галереи'),
onTap: () {
Get.back();
_controller.selectAvatarFromGallery();
},
),
ListTile(
leading: Icon(Icons.camera_alt),
title: Text('Сделать фото'),
onTap: () {
Get.back();
_controller.selectAvatarFromCamera();
},
),
ListTile(
leading: Icon(Icons.close),
title: Text('Закрыть'),
onTap: () => Get.back(),
),
],
),
),
);
}
Widget _buildProfileForm() {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
TextField(
controller: _controller.nameController,
decoration: InputDecoration(
labelText: 'Имя',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
prefixIcon: Icon(Icons.person),
),
),
SizedBox(height: 16),
TextField(
controller: _controller.emailController,
decoration: InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
prefixIcon: Icon(Icons.email),
),
),
],
),
);
}
Widget _buildActionButtons() {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
Obx(() {
return SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _controller.isLoading.value
? null
: () {
_scaleController.forward(from: 0.0);
_controller.updateProfile();
},
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF667EEA),
padding: EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: _controller.isLoading.value
? SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
valueColor:
AlwaysStoppedAnimation(Colors.white),
),
)
: Text(
'Сохранить',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
);
}),
SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => Get.back(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[400],
padding: EdgeInsets.symmetric(vertical: 14),
),
child: Text(
'Назад',
style: TextStyle(color: Colors.white),
),
),
),
],
),
);
}
@override
void dispose() {
_scaleController.dispose();
super.dispose();
}
}
Dependencies в pubspec.yaml
dependencies:
flutter:
sdk: flutter
get: ^4.6.5
image_picker: ^1.0.0
image_cropper: ^4.0.0
flutter_image_compress: ^2.1.0
path_provider: ^2.1.0
Ключевые особенности
- Выбор фото — из галереи или камеры
- Кроппинг — квадратное изображение
- Сжатие — оптимизация размера файла
- Локальное сохранение — в документах приложения
- Placeholder — инициалы при отсутствии аватара
- Анимация — масштабирование при обновлении
- Редактирование данных — имя и email
- Загрузка на сервер — синхронизация с backend