求助:Flutter前端如何对接已实现的Django REST后端API?
Flutter 连接 Django REST Framework 实现注册功能方案
步骤1:添加网络请求依赖
在项目的pubspec.yaml中添加http包,用于发送API请求:
dependencies: flutter: sdk: flutter image_picker: ^1.0.4 # 你已有的图片选择依赖 http: ^1.1.0
执行flutter pub get完成依赖安装。
步骤2:改造注册页面为有状态组件并添加输入控制
当前Regfirst是无状态组件,无法保存输入状态和处理异步请求,需要改为有状态组件,并为每个输入框绑定TextEditingController来获取用户输入:
import 'dart:io'; import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:http/http.dart' as http; import './register2.dart'; // 保留MyClip组件不变,此处省略重复代码 // 改造Avatar组件,添加回调传递选中的头像文件 class Avatar extends StatefulWidget { final Function(File?) onImageSelected; const Avatar({Key? key, required this.onImageSelected}) : super(key: key); @override State<Avatar> createState() => _AvatarState(); } class _AvatarState extends State<Avatar> { File? image; Future pickImage() async { try { final image = await ImagePicker().pickImage(source: ImageSource.gallery); if (image == null) return; final imageTemp = File(image.path); setState(() => this.image = imageTemp); // 通知父组件更新头像 widget.onImageSelected(imageTemp); } on PlatformException catch (e) { print('图片选择失败: $e'); } } @override Widget build(BuildContext context) { return GestureDetector( onTap: pickImage, // 添加点击事件触发图片选择 child: Container( child: image != null ? ClipOval( child: Image.file( image!, width: 100, height: 100, fit: BoxFit.cover, ), ) : Image.asset('assets/images/icon_sample.jpeg'), ), ); } } // 改造Regfirst为有状态组件 class Regfirst extends StatefulWidget { @override State<Regfirst> createState() => _RegfirstState(); } class _RegfirstState extends State<Regfirst> { // 输入控制器,用于获取每个输入框的值 final _firstNameController = TextEditingController(); final _lastNameController = TextEditingController(); final _emailController = TextEditingController(); final _phoneController = TextEditingController(); final _passwordController = TextEditingController(); final _confirmPasswordController = TextEditingController(); File? _avatarImage; // 更新头像的回调方法 void _updateAvatar(File? image) { setState(() { _avatarImage = image; }); } // 注册API请求函数 Future<void> _submitRegistration() async { // 先验证密码一致性 if (_passwordController.text != _confirmPasswordController.text) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('两次输入的密码不一致')), ); return; } // 替换为你的Django注册API地址 const apiUrl = 'http://你的Django服务器IP:8000/api/register/'; try { // 创建MultipartRequest,支持上传文件和表单数据 var request = http.MultipartRequest('POST', Uri.parse(apiUrl)); // 添加表单字段 request.fields['first_name'] = _firstNameController.text; request.fields['last_name'] = _lastNameController.text; request.fields['email'] = _emailController.text; request.fields['phone'] = _phoneController.text; request.fields['password'] = _passwordController.text; // 如果用户选择了头像,添加文件字段 if (_avatarImage != null) { request.files.add( await http.MultipartFile.fromPath( 'avatar', // 对应Django后端接收头像的字段名 _avatarImage!.path, ), ); } // 发送请求 var response = await request.send(); // 处理响应结果 if (response.statusCode == 201) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('注册成功!')), ); Navigator.push(context, MaterialPageRoute(builder: (context) => Regsec())); } else { var errorMsg = await response.stream.bytesToString(); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('注册失败: $errorMsg')), ); } } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('网络请求错误: $e')), ); } } @override void dispose() { // 销毁控制器,避免内存泄漏 _firstNameController.dispose(); _lastNameController.dispose(); _emailController.dispose(); _phoneController.dispose(); _passwordController.dispose(); _confirmPasswordController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { var bg = 'assets/images/bg.jpeg'; return Material( child: Container( decoration: BoxDecoration( image: DecorationImage( image: AssetImage(bg), fit: BoxFit.cover, ), ), child: Container( child: Stack( alignment: Alignment.center, children: [ Positioned( top: 0, height: 400, width: 330, // 传递头像更新回调 child: Avatar(onImageSelected: _updateAvatar), ), Positioned( top: 350, height: 300, width: 330, child: Column( children: [ Expanded( child: Row( children: [ Expanded( flex: 3, child: TextField( controller: _firstNameController, decoration: InputDecoration( hintText: "First name", hintStyle: const TextStyle( color: Colors.white, fontSize: 25, ), labelStyle: const TextStyle( fontSize: 24, color: Colors.white), border: const UnderlineInputBorder(), ), ), ), const SizedBox(width: 40), Expanded( flex: 3, child: TextField( controller: _lastNameController, decoration: InputDecoration( hintText: "Last name", hintStyle: const TextStyle( color: Colors.white, fontSize: 25, ), labelStyle: const TextStyle( fontSize: 24, color: Colors.white), border: const UnderlineInputBorder(), ), ), ), ], ), ), Expanded( child: Row( children: [ Expanded( flex: 3, child: TextField( controller: _emailController, keyboardType: TextInputType.emailAddress, decoration: InputDecoration( hintText: "Email", hintStyle: const TextStyle( color: Colors.white, fontSize: 25, ), labelStyle: const TextStyle( fontSize: 24, color: Colors.white), border: const UnderlineInputBorder(), ), ), ), const SizedBox(width: 40), Expanded( flex: 3, child: TextField( controller: _phoneController, keyboardType: TextInputType.phone, decoration: InputDecoration( hintText: "Phone number", hintStyle: const TextStyle( color: Colors.white, fontSize: 25, ), labelStyle: const TextStyle( fontSize: 24, color: Colors.white), border: const UnderlineInputBorder(), ), ), ), ], ), ), Expanded( child: Row( children: [ Expanded( flex: 3, child: TextField( controller: _passwordController, obscureText: true, decoration: InputDecoration( hintText: "Password", hintStyle: const TextStyle( color: Colors.white, fontSize: 25, ), labelStyle: const TextStyle( fontSize: 24, color: Colors.white), border: const UnderlineInputBorder(), ), ), ), ], ), ), Expanded( child: Row( children: [ Expanded( flex: 3, child: TextField( controller: _confirmPasswordController, obscureText: true, decoration: InputDecoration( hintText: "Confirm Password", hintStyle: const TextStyle( color: Colors.white, fontSize: 25, ), labelStyle: const TextStyle( fontSize: 24, color: Colors.white), border: const UnderlineInputBorder(), ), ), ), ], ), ), ], ), ), Positioned( top: 680, height: 45, width: 250, child: ElevatedButton( onPressed: _submitRegistration, // 绑定注册请求方法 child: const Text( 'Next', style: TextStyle(fontSize: 30), ), ), ), ], ), ), ), ); } }
步骤3:Django后端配置
- 跨域请求配置:Flutter调试时会触发跨域请求,需要安装并配置
django-cors-headers:
pip install django-cors-headers
在settings.py中添加以下配置:
INSTALLED_APPS = [ ... 'corsheaders', ] MIDDLEWARE = [ ... 'corsheaders.middleware.CorsMiddleware', 'django.middleware.common.CommonMiddleware', ] # 调试环境允许所有来源,生产环境请指定具体域名 CORS_ALLOW_ALL_ORIGINS = True
- 序列化器支持文件上传:确保Django的序列化器能正确处理头像字段:
from rest_framework import serializers from django.contrib.auth.models import User class UserRegistrationSerializer(serializers.ModelSerializer): avatar = serializers.ImageField(required=False) class Meta: model = User fields = ['first_name', 'last_name', 'email', 'phone', 'password', 'avatar'] extra_kwargs = {'password': {'write_only': True}} def create(self, validated_data): avatar = validated_data.pop('avatar', None) user = User.objects.create_user(**validated_data) if avatar: # 假设你的User模型有avatar字段,根据实际情况调整 user.avatar = avatar user.save() return user
关键说明
- 使用
http.MultipartRequest处理包含文件的表单提交,这是上传图片的标准方式。 - 每个输入框绑定
TextEditingController,方便直接获取用户输入值。 - 添加了密码一致性验证、网络错误捕获和用户反馈,提升交互体验。
- Avatar组件通过回调将选中的图片传递给父组件,实现状态共享。
内容的提问来源于stack exchange,提问作者Zhichen Xu
相关产品推荐
相关产品推荐

