使用FilePicker中的数据更新ListView



我正在尝试从FilePicker创建一个文件列表,并将它们添加到ListView中。我有UpdateInformationScreen,他有一个子DocumentPicker。DocumentPicker有一个子DocumentCard。

ListView会延迟更新,例如,如果我验证一个文件,它将不会在列表中显示。如果我验证第二个文件,则只有第一个文件可见。我确信问题来自异步,但我不知道到底在哪里

更新信息屏幕:

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:app_test/utils/utils.dart';
import 'package:app_test/view/information_screen.dart';
import 'package:app_test/widget/document_picker.dart';
import 'package:app_test/widget/photo_picker.dart';
import 'package:intl_phone_field/intl_phone_field.dart';
import 'package:provider/provider.dart';
import '../constant/color.dart';
import '../constant/text.dart';
import '../model/instructor.dart';
import '../provider/instructor_view_model.dart';
class UpdateInformationScreen extends StatefulWidget {
static const String ROUTE_NAME = "/updateInformation";
const UpdateInformationScreen({Key? key}) : super(key: key);
@override
State<UpdateInformationScreen> createState() => _UpdateInformationScreenState();
}
class _UpdateInformationScreenState extends State<UpdateInformationScreen> {
final firstNameController = TextEditingController();
final phoneNumberController = TextEditingController();
String? phoneNumber;
File? photo;
String? photoURL;
//----------------------------------------------------------------------------------------------
//----------------------------- Free memory allocated to the existing variables ----------------
//----------------------------------------------------------------------------------------------
@override
void dispose() {
firstNameController.dispose();
phoneNumberController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final instructor = ModalRoute.of(context)!.settings.arguments as Instructor;
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.pushNamed(context, InformationScreen.ROUTE_NAME);
},
),
title: const Text(appTitle),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 20,),
PhotoPicker(
onImageSelected: (image) {
print("Selected image: $image");
setState(() {
photo = image;
});
},
),
const SizedBox(height: 20,),
//----------------------------- First name -------------------------
TextField(
controller: firstNameController,
cursorColor: cursorColor,
textInputAction: TextInputAction.next,
decoration: InputDecoration(
prefixIcon: const Icon(
Icons.person,
color: grey,
size: 30,
),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.grey),
borderRadius: BorderRadius.circular(5.5),
),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.orange),
borderRadius: BorderRadius.circular(5.5),
),
labelText: instructor.firstName != null
? instructor.firstName.toString()
: updateFirstnameText,
),
),
const SizedBox(
height: 20,
),
//----------------------------- Phone number -----------------------
IntlPhoneField(
controller: phoneNumberController,
initialValue: instructor.phoneNumber != null
? Utils.phoneNumberWithoutCountryCode(instructor.phoneNumber.toString())
: phoneSignInLabelText,
decoration: InputDecoration(
labelText: instructor.phoneNumber != null
? Utils.phoneNumberWithoutCountryCode(instructor.phoneNumber.toString())
: phoneSignInLabelText,
border: const OutlineInputBorder(
borderSide: BorderSide(),
),
),
initialCountryCode: 'FR',
onChanged: (phone) {
phoneNumber = phone.completeNumber;
print(phone.completeNumber);
},
),
const SizedBox(
height: 20,
),
const DocumentPicker(),
const SizedBox(height: 20,),
//----------------------------- Update button ----------------------
ElevatedButton(
onPressed: () async {
await _updateInstructorPhoto(instructor.uid!, photo);
_clickOnUpdateInformationButton(
instructor.uid!,
//----------------------------- Photo ----------------------
photoURL.toString().isEmpty || photo == null
? instructor.photo
: photoURL,
//----------------------------- First name -----------------
firstNameController.text.isNotEmpty
? firstNameController.text.trim()
: instructor.firstName,
//----------------------------- Phone number ---------------
phoneNumberController.text.isNotEmpty
? phoneNumber!
: instructor.phoneNumber,
);
},
style: ElevatedButton.styleFrom(
fixedSize: const Size(double.infinity, 50),
primary: cursorColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
child: Row(
children: const [
Padding(
padding: EdgeInsets.only(left: 5, right: 100),
child: Icon(
Icons.update,
color: Colors.white,
size: 25,
),
),
Text(
updateInfoButton,
style: TextStyle(
fontSize: 18,
color: Colors.white,
),
),
],
),
),
],
),
),
);
}
//----------------------------------------------------------------------------
//----------------------------- Click on update information button -----------
//----------------------------------------------------------------------------
void _clickOnUpdateInformationButton(String uid, String? storagePhotoUrl, String? firstName, String? phoneNumber) {
final instructorViewModel = Provider.of<InstructorViewModel>(context, listen: false);
instructorViewModel.updateInstructor(uid, storagePhotoUrl, firstName, phoneNumber);
print("Instructor:  n"
"uid: $uid n"
"photo: $storagePhotoUrl n"
"firstname: $firstName n"
"phone: $phoneNumber n");
print("Photo: ${photoURL.toString()}");
Utils.goToInformationScreen(context);
Utils.showSuccessMessage(updateInformationSuccessText);
}
//----------------------------------------------------------------------------
//----------------------------- Update instructor photo ----------------------
//----------------------------------------------------------------------------
_updateInstructorPhoto(String uid, File? file) async {
if(photo != null){
await InstructorViewModel.uploadInstructorPhoto(uid, file!);
await getUrlPhoto(uid);
}
}
//----------------------------------------------------------------------------
//----------------------------- Get instructor photo from Storage ------------
//----------------------------------------------------------------------------
getUrlPhoto(String uid) async {
final instructorViewModel = Provider.of<InstructorViewModel>(context, listen: false);
photoURL = await instructorViewModel.getInstructorPhotoUrl(uid);
}
}

DocumentPicker:

import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:g_conduite_instructor_test/provider/document_view_model.dart';
import 'package:path/path.dart' as Path;
import '../constant/color.dart';
import '../constant/text.dart';
import '../model/document.dart';
import 'document_card.dart';
class DocumentPicker extends StatefulWidget {
const DocumentPicker({Key? key}) : super(key: key);
@override
State<DocumentPicker> createState() => _DocumentPickerState();
}
class _DocumentPickerState extends State<DocumentPicker> {
final photoTitleController = TextEditingController();
File? _file;
User? user = FirebaseAuth.instance.currentUser;
List<Document>? documents = [];
late Document document;
String extension = "";
//----------------------------------------------------------------------------------------------
//----------------------------- Free memory allocated to the existing variables ----------------
//----------------------------------------------------------------------------------------------
@override
void dispose() {
photoTitleController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
String fileName = _file != null ? Path.basename(_file!.path) : noFileSelectedText;
print("Extension 1: $extension");
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
//----------------------------- Pick file from device --------------
TextButton.icon(
onPressed: () async {
_clickOnAddFile();
},
icon: const Icon(
Icons.download,
size: 30,
color: googleButtonTextColor,
),
label: const Text(
addFile,
style: TextStyle(
fontSize: 18,
color: googleButtonTextColor,
),
),
),
//----------------------------- Validate picked file ---------------
TextButton.icon(
onPressed: () async {
await _clickOnValidate();
_clearPhotoTitleTextField();
},
icon: const Icon(
Icons.done,
size: 30,
color: googleButtonTextColor,
),
label: const Text(
validate,
style: TextStyle(
fontSize: 18,
color: googleButtonTextColor,
),
),
),
],
),
//----------------------------- Picked file default name ---------------
Text(
fileName,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: black,
),
),
const SizedBox(height: 10,),
//----------------------------- Picked file title ----------------------
TextField(
controller: photoTitleController,
cursorColor: cursorColor,
textInputAction: TextInputAction.next,
decoration: InputDecoration(
prefixIcon: const Icon(
Icons.edit,
color: grey,
size: 30,
),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.grey),
borderRadius: BorderRadius.circular(5.5),
),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.orange),
borderRadius: BorderRadius.circular(5.5),
),
labelText: fileTitleText,
),
),
const SizedBox(height: 10,),
//----------------------------- Show list of picked file ---------------
//documents!.isEmpty ? const SizedBox(height: 0,) : _showFileList(),
_showFileList(),
],
);
}
//----------------------------------------------------------------------------
//----------------------------- Pick file from device ------------------------
//----------------------------------------------------------------------------
Future _clickOnAddFile() async{
final result = await FilePicker.platform.pickFiles(
allowMultiple: false,
type: FileType.custom,
allowedExtensions: ["jpg", "jpeg", "png", "pdf"],
);
if(result == null) return null;
final path = result.files.single.path!;
extension = result.files.first.extension!;
setState(() => _file = File(path));
}
//----------------------------------------------------------------------------
//----------------------------- Validate picked file -------------------------
//----------------------------------------------------------------------------
Future _clickOnValidate() async {
if(_file == null) return null;
final fileName = photoTitleController.text.trim();
final destination = "instructorDocuments/${user!.uid}/$fileName";
document = Document(_file, fileName);
documents?.add(document);
if (documents != null) {
for (var d in documents!){
DocumentViewModel.uploadInstructorDocument(destination, d.documentPhoto!);
}
}
}
//----------------------------------------------------------------------------
//----------------------------- Clear photo title text field -----------------
//----------------------------------------------------------------------------
_clearPhotoTitleTextField() {
photoTitleController.clear();
}
//----------------------------------------------------------------------------
//----------------------------- Add List of file from file picker -----------------
//----------------------------------------------------------------------------
/*Future<Widget>*/Widget _showFileList() /*async*/{
return ListView.builder(
itemCount: documents!.length,
itemBuilder: (context, index) => DocumentCard(document: documents![index], extension: extension,),
shrinkWrap: true,
physics: const BouncingScrollPhysics(),
);
/*setState(() {
ListView.builder(
itemCount: documents!.length,
itemBuilder: (context, index) => DocumentCard(document: documents![index], extension: extension,),
shrinkWrap: true,
);
});
return const SizedBox(height: 0,);*/
}
}

文件卡:

import 'package:flutter/material.dart';
import 'package:app_test/model/document.dart';
import '../constant/color.dart';
class DocumentCard extends StatelessWidget {
final Document document;
final String extension;
const DocumentCard({Key? key, required this.document,
required this.extension}) : super(key: key);
@override
Widget build(BuildContext context) {
print("Extension 2: $extension");
return Card(
elevation: 5,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0),
),
child: Row(
children: [
extension == "pdf"
? Image.asset(
fit: BoxFit.cover,
width: 100,
height: 100,
"assets/images/pdf.png",
)
: Image.file(
fit: BoxFit.cover,
width: 100,
height: 100,
document.documentPhoto!,
),
const SizedBox(width: 20,),
Text(
document.photoTitle!,
style: const TextStyle(
fontSize: 16,
color: black,
),
),
const Spacer(),
IconButton(
onPressed: () {
print("Delete icon clicked");
},
icon: const Icon(
Icons.delete,
color: red,
)
),
],
),
);
}
}

编辑:我尝试过,结果相同

Future _clickOnValidate() async {
if(_file == null) return null;
final fileName = photoTitleController.text.trim();
final destination = "instructorDocuments/${user!.uid}/$fileName";
document = Document(_file, fileName);
documents?.add(document);
if (documents != null) {
for (var d in documents!){
DocumentViewModel.uploadInstructorDocument(destination, d.documentPhoto!);
}
}
}
//----------------------------------------------------------------------------
//----------------------------- Get future document list ---------------------
//----------------------------------------------------------------------------

Future<List<Document>>? getDocumentList() async {
print("Future selected list: $documents");
return documents!;
}

并添加了一个FutureBuilder:

//documents!.isEmpty ? const SizedBox(height: 0,) : _showFileList(),
//_showFileList(),
FutureBuilder<List<Document>>(
future: getDocumentList(),
builder: (context, future) {
if(future.hasData) {
List<Document>? selectedFilesList = future.data;
print("Selected file list $selectedFilesList");
return ListView.builder(
itemCount: selectedFilesList!.length,
itemBuilder: (context, index) => DocumentCard(
document: selectedFilesList[index], 
extension: extension,
),
shrinkWrap: true,
physics: const BouncingScrollPhysics(),
);
}
else {
return const SizedBox(height: 0,);
}
},
),

提前感谢

@Sayyid J感谢StreamBuilder的建议,我已经搜索并找到了这个:

如何将控制器添加到streamBuilder?

所以我做了这个,它工作

class _DocumentPickerState extends State<DocumentPicker> {
final photoTitleController = TextEditingController();
File? _file;
User? user = FirebaseAuth.instance.currentUser;
List<Document>? documents = [];
late Document document;
String extension = "";
String destination = "";
final FileListController fileController = FileListController(); <---------------------
//----------------------------------------------------------------------------------------------
//----------------------------- Free memory allocated to the existing variables ----------------
//----------------------------------------------------------------------------------------------
@override
void dispose() {
photoTitleController.dispose();
fileController.controller.close(); <---------------------
super.dispose();
}
@override
Widget build(BuildContext context) {}
}

FileController类:

class FileListController {
final StreamController<List<Document>?> controller = StreamController<List<Document>?>.broadcast();

Sink<List<Document>?> get inputFileList => controller.sink;
Stream<List<Document>?> get outputFileList => controller.stream;
}

在fileController:中添加列表

Future _clickOnValidate() async {
if(_file == null) return null;
final defaultName = Path.basename(_file!.path);
final fileName = photoTitleController.text.trim();
destination = "instructorDocuments/${user!.uid}/$fileName";
if(fileName.isEmpty) {
destination = "instructorDocuments/${user!.uid}/$defaultName";
}
document = Document(_file, fileName);
documents?.add(document);
fileController.inputFileList.add(documents); <---------------------
if (documents != null) {
for(var d in documents!){
DocumentViewModel.uploadInstructorDocument(destination, d.documentPhoto!);
}
}
}

添加StreamBuilder

StreamBuilder<List<Document>?>(
stream: fileController.outputFileList,
builder: (context, snapshot) {
if(snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) => DocumentCard(
document: snapshot.data![index],
extension: extension,
),
shrinkWrap: true,
physics: const BouncingScrollPhysics(),
);
}
else {
return const SizedBox(height: 0,);
}
},
),

最新更新