Flutter căn bản-Gửi Request POST-GET-PUT-DELETE tới Restful API đính kèm hình ảnh
Đăng lúc: 09:05 AM - 14/07/2024 bởi Charles Chung - 669Trong bài viết này tôi sẽ hướng dẫn các bạn gọi Restful API trong Flutter, các thao tác gồm POST, PUT, GET, DELETE. Khi POST, PUT có kèm theo hình ảnh.
![](\images\posts\050532-26062024-crud-restful-api-in-flutter.jpg)
1. Giới thiệu
Việc trao đổi dữ liệu qua internet trên nền tảng di dộng là rất cần thiết với hầu hết các ứng dụng, trong Dart và Flutter cung cấp cho người lập trình gói http giúp việc gửi nhận dữ liệu qua internet dễ dàng hơn. Trong bài này tôi sẽ hướng dẫn các bạn thực hiện các hành động POST, GET, PUT, DELETE với Restful API.
2. Chuẩn bị dữ liệu và ứng dụng restfull api
- Cơ sở dữ liệu lưu trên SQL Server gồm 2 bảng có cấu trúc như sau
- Ứng dụng Restful API được viết trên Spring Boot + JPA, bạn có thể tải project tại đây, khi khởi chạy ứng dụng cung cấp một số Uri API mô tả dưới bảng sau
STT | Uri | Method | Cấu trúc Rresponse | Mô tả |
1 | http://localhost:8080/departments/active | GET |
[ { "departmentId": 1, "departmentName": "Hành chính nhân sự", "active": true }, ...] |
API lấy tất các departments có active=true |
2 | http://localhost:8080/departments/{depId} | GET |
{ "departmentId": 1, "departmentName": "Hành chính nhân sự", "active": true } |
API lấy departments theo depId |
3 | http://localhost:8080/employees/active | GET |
[ { "employeeId": "E001", "fullName": "Nguyễn Thanh Tùng", "birthday": "2000-01-20", "picture": "images/tungnt.jpg", "gender": true, "address": "Hà Nội", "phone": "0955346633", "email": "tungnt@gmail.com", "departmentId": 2, "active": true }, ...] |
API lấy tất cả employees có active=true |
4 | http://localhost:8080/employees/dep/{depId} | GET | Trả về danh sách employees có cấu trúc như trên | API lấy employees có active=true theo mã phòng (depId) |
5 | http://localhost:8080/employees/search/{name} | GET | Trả về danh sách employees có cấu trúc như trên | API tìm employees có chứa tên (name) |
6 | http://localhost:8080/employees/{empId) | GET | Trả về 1 employee có cấu trúc như trên | API lấy employee theo mã số (empId) |
7 | http://localhost:8080/employees | POST | Trả về thống báo dạng {"msg":"Thêm thành công/không thành công"} |
API post employee, cấu trúc post dạng Form Data kèm với trường file chứa hình ảnh mô tả như mẫu dưới đây { "employeeId": "E001", "fullname": "Nguyễn Thanh Tùng", "birthday": "20/01/2000", "gender": true, "address": "Hà Nội", "phone": "0955346633", "email": "tungnt@gmail.com", "departmentId": 2, "active": true "file": select file image } |
8 | http://localhost:8080/employees/{empId} | PUT | Trả về thống báo dạng {"msg":"Sửa thành công/không thành công"} |
API put employee, cấu trúc put dạng Form Data kèm với trường file chứa hình ảnh, trường picture chứa đường dẫn ảnh cũ mô tả như mẫu dưới đây { "employeeId": "E001", "fullname": "Nguyễn Thanh Tùng", "birthday": "20/01/2000", "picture": "images/tungnt.jpg", "gender": true, "address": "Hà Nội", "phone": "0955346633", "email": "tungnt@gmail.com", "departmentId": 2, "active": true "file": select file image } |
9 | http://localhost:8080/employees/{empId} | DELETE | Trả về thông báo dạng {"msg":"Xóa thành công/không thành công"} | API xóa nhân viên theo mã số (empId) |
- Mở cửa sổ Command trên windows và gõ lệnh ipconfig để xem IPV4 của máy để sau sử dụng thay thế localhost trong chuỗi Uri của restful api.
3. Xây dựng ứng dụng Flutter gọi Restful API
- Mở Android Studio -> Tạo mới Flutter Project với tên "lab15"
- Tạo thư mục assets/images -> Copy các ảnh cần thiết vào thư mục này (tải tại đây)
- Khai báo các dependency "http" và "image_picker" và cấu hình thư mục assets/images vào file pubspec.yaml như hình dưới
- Tạo models, screens và services vào trong thư mục lib và các tệp tin .dart như hình dưới
- Tệp common.dart định nghĩa lớp Common (khai báo thuộc tính static dùng chung)
class Common{ static const String _domain="http://172.16.0.65:8080"; static String get domain=>_domain; }
- Tệp department.dart định nghĩa lớp Department biểu diễn dữ liệu phòng ban
class Department { int departmentId; String departmentName; bool active; Department(this.departmentId, this.departmentName, this.active); factory Department.fromJon(Map<String, Object?> data) { return Department( int.parse(data['departmentId'].toString()), data['departmentName'].toString(), bool.parse(data['active'].toString())); } Map<String, Object?> toMap() { return { "departmentid": departmentId, "departmentname": departmentName, "active": active }; } }
- Tệp employee.dart định nghĩa lớp Employee biểu diễn dữ liệu nhân viên
import 'dart:math'; class Employee { String employeeId; String fullName; DateTime birthday; String picture; bool gender; String address; String phone; String email; int departmentId; bool active; Employee( this.employeeId, this.fullName, this.birthday, this.picture, this.gender, this.address, this.phone, this.email, this.departmentId, this.active); factory Employee.fromJson(Map<String, Object?> data) { return Employee( data['employeeId'].toString(), data['fullName'].toString(), DateTime.parse(data['birthday'].toString()), data['picture'].toString(), bool.parse(data['gender'].toString()), data['address'].toString(), data['phone'].toString(), data['email'].toString(), int.parse(data['departmentId'].toString()), bool.parse(data['active'].toString())); } Map<String, String> toMap() { return { "employeeid": employeeId, "fullname":fullName, "birthday": '${birthday.day}/${birthday.month}/${birthday.year}', "picture": picture, "gender": gender.toString(), "address": address, "phone": phone, "email": email, "departmentid": departmentId.toString(), "active": active.toString() }; } }
- Tệp department_service.dart định nghĩa lớp DepartmentService thực hiện các công việc gửi request tới restful api lên quan đến phòng ban
import 'package:lab15/models/common.dart'; import 'package:http/http.dart' as http; class DepartmentService { Map<String, String> headers = { "Content-Type": "application/json; charset=UTF-8" }; Future<http.Response> getDepartments() { return http .get(Uri.parse('${Common.domain}/departments/active'), headers: headers); } }
- Tệp employee_service.dart định nghĩa lớp EmployeeService thực hiện các công việc gửi request tới restful api lên quan đến nhân viên
import 'package:lab15/models/common.dart'; import 'package:http/http.dart' as http; import 'package:lab15/models/employee.dart'; class EmployeeService { Map<String, String> headers = { "Content-Type": "application/json; charset=UTF-8" }; Future<http.Response> get(int depId) { return http .get(Uri.parse('${Common.domain}/employees/dep/$depId'), headers: headers); } Future<http.Response> delete(String empId){ return http.delete(Uri.parse('${Common.domain}/employees/$empId')); } Future<http.StreamedResponse> save(Employee emp, String method, bool hasFile, String filePath) async{ var uri=method=='POST'?'employees':'employees/${emp.employeeId}'; var request = http.MultipartRequest( method, Uri.parse('${Common.domain}/$uri')); if (!hasFile) { request.files.add(http.MultipartFile.fromBytes( "file", [], filename: "")); } else { request.files.add(await http.MultipartFile.fromPath( "file", filePath)); } request.fields.addAll(emp.toMap()); return request.send(); } }
- Tệp home_screen.dart định nghĩa màn hình chính của ứng dụng, hiển thị danh sách phòng ban
import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:lab15/models/department.dart'; import 'package:lab15/screens/employee_screen.dart'; import 'package:lab15/services/department_service.dart'; class MyHomePage extends StatefulWidget { const MyHomePage({super.key}); @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { List<Department> departments = []; @override void initState() { super.initState(); _loadDepartments(); } void _loadDepartments() async { DepartmentService().getDepartments().then((value) { var data = jsonDecode(const Utf8Decoder().convert(value.bodyBytes)) as List; setState(() { departments = data.map((e) => Department.fromJon(e)).toList(); }); }); } @override Widget build(BuildContext context) { double screenwidth = MediaQuery.of(context).size.width; return Scaffold( appBar: AppBar( title: const Center( child: Text('QUẢN LÝ NHÂN SỰ', textAlign: TextAlign.center)), ), body: SingleChildScrollView( padding: const EdgeInsets.all(5.0), child: Column( children: [ Image.asset( "assets/images/hanam88.jpg", fit: BoxFit.fill, width: screenwidth, ), const SizedBox( height: 10.0, ), Text( 'Danh sách phòng ban', style: Theme.of(context).textTheme.headlineSmall, ), const SizedBox( height: 10.0, ), ListView.builder( shrinkWrap: true, primary: false, physics: const NeverScrollableScrollPhysics(), itemCount: departments.length, itemBuilder: (context, int index) { var dep = departments[index]; return ElevatedButton( style: const ButtonStyle( backgroundColor: MaterialStatePropertyAll<Color>(Colors.green)), onPressed: () { Navigator.of(context).push(MaterialPageRoute( builder: (context) => ScreenEmployee(dep: dep))); }, child: Text( dep.departmentName, style: const TextStyle(fontSize: 20), )); }), ], ), ), ); } } // IPV4 của máy local đang chạy restful api
- Tệp employee_screen.dart định nghĩa màn hình hiển thị nhân viên theo phòng ban
import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:lab15/models/common.dart'; import 'package:lab15/models/department.dart'; import 'package:lab15/models/employee.dart'; import 'package:lab15/screens/form_employee_screen.dart'; import 'package:lab15/services/employee_service.dart'; class ScreenEmployee extends StatefulWidget { Department dep; ScreenEmployee({super.key, required this.dep}); @override State<StatefulWidget> createState() => _ScreenEmployeeState(); } class _ScreenEmployeeState extends State<ScreenEmployee> { List<Employee> employees = []; @override void initState() { super.initState(); _loadEmployees(); } void _loadEmployees() async { EmployeeService().get(widget.dep.departmentId).then((value) { var data = jsonDecode(const Utf8Decoder().convert(value.bodyBytes)) as List; setState(() { employees = data.map((e) => Employee.fromJson(e)).toList(); }); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Center( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Phòng ${widget.dep.departmentName}'), IconButton( onPressed: () { _loadEmployees(); }, icon: const Icon(Icons.refresh)) ]))), body: ListView.builder( itemCount: employees.length, itemBuilder: (context, index) { var emp = employees[index]; return Padding( padding: const EdgeInsets.fromLTRB(0, 10.0, 0, 0), child: GestureDetector( onTap: () { Navigator.of(context) .push(MaterialPageRoute( builder: (context) => EmployeeForm(dep: widget.dep, emp: emp))) .then((value) { setState(() { widget.dep = value; }); _loadEmployees(); }); }, child: ListTile( leading:(emp.picture.isEmpty)?Icon(Icons.account_box_outlined): Image.network('${Common.domain}/${emp.picture}'), title: Text(emp.fullName), subtitle: Text(emp.phone), trailing: ElevatedButton( onPressed: () { _showAlertDialog(context, emp.employeeId); }, style: const ButtonStyle( backgroundColor: MaterialStatePropertyAll(Colors.red)), child: const Icon(Icons.remove_circle)), )), ); }), floatingActionButton: FloatingActionButton( onPressed: () { Navigator.of(context) .push(MaterialPageRoute( builder: (context) => EmployeeForm(dep: widget.dep))) .then((value) { setState(() { widget.dep = value; }); _loadEmployees(); }); }, child: const Icon(Icons.add_box)), ); } _showAlertDialog(BuildContext context, String empid) { // set up the buttons Widget cancelButton = TextButton( child: Text("Không"), onPressed: () { Navigator.pop(context); }, ); Widget okButton = TextButton( child: Text("Có"), onPressed: () { EmployeeService().delete(empid).then((value) { _loadEmployees(); }); Navigator.pop(context); }, ); // set up the AlertDialog AlertDialog alert = AlertDialog( title: const Text("Hỏi xóa"), content: Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Image.asset('assets/images/question-64.png'), const Text("Bạn có muốn xóa không?"), ]), actions: [ cancelButton, okButton, ], ); // show the dialog showDialog( context: context, builder: (BuildContext context) { return alert; }, ); } }
- Tệp form_employee_screen.dart định nghĩa màn hình hiển thêm và cập nhật nhân viên
import 'dart:convert'; import 'dart:io'; import 'package:http/http.dart' as http; import 'package:flutter/material.dart'; import 'package:lab15/models/common.dart'; import 'package:lab15/models/department.dart'; import 'package:image_picker/image_picker.dart'; import 'package:lab15/models/employee.dart'; import 'package:lab15/services/department_service.dart'; import 'package:lab15/services/employee_service.dart'; class EmployeeForm extends StatefulWidget { Department dep; Employee? emp; EmployeeForm({super.key, required this.dep, this.emp}); @override State<StatefulWidget> createState() => _EmployeeFormState(); } class _EmployeeFormState extends State<EmployeeForm> { final _formkey = GlobalKey<FormState>(); final _empIdController = TextEditingController(); final _fullNameController = TextEditingController(); final _birthdayController = TextEditingController(); final _emailController = TextEditingController(); final _phoneController = TextEditingController(); final _addressController = TextEditingController(); String _pictureOld = ''; String? _selectedDep; String _title="Thêm mới"; String _labelMode = 'Nữ'; bool _gender = false; File? _image; List<Department> departments = []; Future _getImage() async { final picker = ImagePicker(); final pickedFile = await picker.pickImage(source: ImageSource.gallery); if (pickedFile != null) { setState(() { _image = File(pickedFile.path); }); } } @override void initState() { super.initState(); if (widget.emp != null) { _empIdController.text = widget.emp!.employeeId; _fullNameController.text = widget.emp!.fullName; _birthdayController.text = widget.emp!.birthday.toString(); _addressController.text = widget.emp!.address; _phoneController.text = widget.emp!.phone; _emailController.text = widget.emp!.email; _empIdController.text = widget.emp!.employeeId; _pictureOld = widget.emp!.picture; _gender = widget.emp!.gender; _labelMode = _gender ? "Nam" : "Nữ"; _selectedDep = widget.dep.departmentId.toString(); _title="Sửa"; } _loadDepartments(); } void _loadDepartments() async { DepartmentService().getDepartments().then((value) { var data = jsonDecode(const Utf8Decoder().convert(value.bodyBytes)) as List; setState(() { departments = data.map((e) => Department.fromJon(e)).toList(); }); }); } @override Widget build(BuildContext context) { return WillPopScope( onWillPop: () { Navigator.pop(context, widget.dep); return Future.value(false); }, child: Scaffold( appBar: AppBar( title: Center( child: Text('$_title nhân viên'), )), body: SingleChildScrollView( padding: const EdgeInsets.all(10.0), child: Form( key: _formkey, child: Column( children: [ TextFormField( controller: _empIdController, decoration: const InputDecoration( enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.blueGrey, width: 2.0)), border: OutlineInputBorder(borderSide: BorderSide()), fillColor: Colors.white, filled: true, hintText: 'Mã nhân viên', labelText: 'Mã nhân viên', ), validator: (value) { if (value == null || value.isEmpty) { return 'Hãy nhập mã nhân viên'; } return null; }, ), const SizedBox( height: 10.0, ), TextFormField( controller: _fullNameController, decoration: const InputDecoration( enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.blueGrey, width: 2.0)), border: OutlineInputBorder(borderSide: BorderSide()), fillColor: Colors.white, filled: true, hintText: 'Họ và tên', labelText: 'Họ và tên', ), validator: (value) { if (value == null || value.isEmpty) { return 'Hãy nhập họ và tên'; } return null; }, ), const SizedBox( height: 10.0, ), TextFormField( controller: _birthdayController, decoration: const InputDecoration( enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.blueGrey, width: 2.0)), border: OutlineInputBorder(borderSide: BorderSide()), fillColor: Colors.white, filled: true, hintText: 'yyyy-MM-dd', labelText: 'Ngày sinh', ), validator: (value) { if (value == null || value.isEmpty) { return 'Hãy nhập Ngày sinh yyyy-MM-dd'; } return null; }, ), const SizedBox( height: 10.0, ), TextFormField( controller: _addressController, decoration: const InputDecoration( enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.blueGrey, width: 2.0)), border: OutlineInputBorder(borderSide: BorderSide()), fillColor: Colors.white, filled: true, hintText: 'Địa chỉ', labelText: 'Nhập địa chỉ', ), ), const SizedBox( height: 10.0, ), TextFormField( controller: _phoneController, decoration: const InputDecoration( enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.blueGrey, width: 2.0)), border: OutlineInputBorder(borderSide: BorderSide()), fillColor: Colors.white, filled: true, hintText: 'Điện thoại', labelText: 'Nhập điện thoại', ), ), const SizedBox( height: 10.0, ), TextFormField( controller: _emailController, decoration: const InputDecoration( enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.blueGrey, width: 2.0)), border: OutlineInputBorder(borderSide: BorderSide()), fillColor: Colors.white, filled: true, hintText: 'Email', labelText: 'Email', ), validator: (value) { if (value == null || value.isEmpty) { return 'Hãy nhập email'; } return null; }, ), const SizedBox( height: 10.0, ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Chọn phòng ban '), DropdownButton<String>( value: _selectedDep, icon: const Icon(Icons.arrow_downward), iconSize: 24, elevation: 16, style: const TextStyle(color: Colors.deepPurple), underline: Container( height: 2, color: Colors.grey, ), onChanged: (String? newValue) { setState(() { _selectedDep = newValue; widget.dep = departments.firstWhere((element) => element.departmentId.toString() == newValue); }); }, items: departments .map<DropdownMenuItem<String>>((Department value) { return DropdownMenuItem<String>( value: value.departmentId.toString(), child: Text(value.departmentName), ); }).toList(), ), ], ), const SizedBox( height: 10.0, ), Row( mainAxisAlignment: MainAxisAlignment.start, children: [ const Text('Giới tính '), Switch( value: _gender, onChanged: (value) { setState(() { _gender = value; _labelMode = _gender ? 'Nam' : 'Nữ'; }); }), Text(_labelMode), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ (_image != null) ? Image.file(_image!, width: 100) : (widget.emp != null && widget.emp!.picture != "") ? Image.network( '${Common.domain}/${widget.emp!.picture}', width: 100, ) : const Text('Không có ảnh được chọn.'), ElevatedButton( onPressed: _getImage, child: const Icon(Icons.add_a_photo)) ]), const SizedBox( height: 10.0, ), ElevatedButton( onPressed: () async { if (_formkey.currentState!.validate()) { if (_selectedDep == null) { _showMessage( context: context, icon: "error-64.png", content: "Hãy chọn phòng ban"); return; } var emp = Employee( _empIdController.text, _fullNameController.text, DateTime.parse(_birthdayController.text), _pictureOld, _gender, _addressController.text, _phoneController.text, _emailController.text, int.parse(_selectedDep!), true); var method = widget.emp == null ? 'POST' : 'PUT'; var hasFile=_image==null?false:true; var filePath=_image==null?"":_image!.path; var response=EmployeeService().save(emp, method, hasFile, filePath); response.then((value) async { final res =await http.Response.fromStream(value); var data = jsonDecode(res.body); if (!context.mounted) return; if (res.statusCode == 200) { _showMessage( context: context, content: '${data["msg"]}', icon: "success-64.png"); } else { _showMessage( context: context, content: '${data["msg"]}', icon: "error-64.png"); } }); } else { // Xử lý khi có lỗi xảy ra } }, child: const Text('Cập nhật')) ], ), ), ), )); } void _showMessage( {required BuildContext context, String title = "Thông báo", String content = "", required String icon}) { showDialog( context: context, builder: (context) { return AlertDialog( title: Text(title), content: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Image.asset("assets/images/$icon"), Text(content), ]), actions: [ TextButton( onPressed: Navigator.of(context).pop, child: const Text('Đồng ý'), ), ], ); }); } }
- Tệp main.dart định nghĩa lớp MyApp khởi chạy ứng dụng
import 'package:flutter/material.dart'; import 'package:lab15/screens/home_screen.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( title: 'HRM-HANAM-88', home: MyHomePage(), debugShowCheckedModeBanner: false, ); } }
- Khởi động Android Emulator và Run ứng dụng ( lưu ý ứng dụng restful api phải chạy trên máy local trước nhé)
Video quay kết quả
thay lời cảm ơn!
Các bài cũ hơn
- Flutter căn bản-Làm việc với cơ sở dữ liệu SQLite-CRUD (08:55 AM - 11/07/2024)
- Flutter cơ bản-Gửi nhận dữ liệu với post và get kèm JWT tới Web API (02:20 PM - 26/06/2024)
- Flutter cơ bản-Sử dụng font chữ (11:49 AM - 26/06/2024)
- Flutter cơ bản-Thiết kế màn hình hiển thị sản phẩm giống trang Shopee (07:58 PM - 24/06/2024)
- Flutter cơ bản-Thiết kế màn hình hồ sơ cá nhân (10:17 PM - 23/06/2024)