2

I'm triying to create a list of File from FilePicker and add them to a ListView. I have UpdateInformationScreen who has a child DocumentPicker. And DocumentPicker has a child DocumentCard.

The ListView is updated with a delay, example if I validate 1 file it will not be visible in the list if I validate a second one, only the first one will be visible. I am sure that the problem comes from an async but I don't know where exactly

UpdateInformationScreen:

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,);*/
  }
}

DocumentCard:

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,
              )
          ),
        ],
      ),
    );
  }
}

Edit: I've tried this with the same result

 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!;
  }

And added a 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,);
            }
          },
        ),

Thanks in advance

Seth
  • 127
  • 1
  • 2
  • 12
  • maybe you need to update ui after get the data, try `streambuilder` – Sayyid J Jul 21 '22 at 12:51
  • in future builder your ui only render when `if (future.hasData)`, no matter 1 or more, but in stream you can actualy sink that data and update ui, until you close that stream or dispose – Sayyid J Jul 21 '22 at 12:55

1 Answers1

0

@Sayyid J thanks for the StreamBuilder suggestion, I've searched and found this:

How to add controller to streamBuilder?

So I've done this and it works

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:

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;
}

Add list in 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!);
      }
    }
  }

Add the 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,);
              }
            },
        ),
Seth
  • 127
  • 1
  • 2
  • 12