flutter – How to update StreamBuilder after removeAt

I have a parent widget DocumentPicker with a child DocumentCard.

I’m trying to update the StreamBuilder inside DocumentPicker when I click on the delete IconButton inside DocumentCard.

Actually I have a FileListController and inside it StreamController<List?>, Sink<List?>, Stream<List?> used for update StreamBuilder when an object is added.

Now I’m trying to use Stream to update the StreamBuilder when an item is removed. For that I’ve tried to use StreamSubscription<List?> inside an initState() in DocumentCard:

I’ve done this:

FileListController:

import 'dart:async';

import '../model/document.dart';

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

DocumentPicker:

import 'dart:async';
import 'dart:io';

import 'package:file_picker/file_picker.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:app_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 '../utils/file_list_controller.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 = "";
  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) {
    String fileName = _file != null ? Path.basename(_file!.path) : noFileSelectedText;

    return Column(
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            //----------------------------- Pick file from device --------------
            TextButton.icon(
              onPressed: () {
                _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 ---------------
        StreamBuilder<List<Document>?>(
            stream: fileController.outputFileList,
            builder: (context, snapshot) {
              if(snapshot.hasData) {
                print("List sent to doc card: ${snapshot.data!}");
                return ListView.builder(
                  itemCount: snapshot.data!.length,
                  itemBuilder: (context, index) => DocumentCard(
                    document: snapshot.data![index],
                    extension: extension,
                    documents: snapshot.data!,
                    index: index,
                  ),
                  shrinkWrap: true,
                  physics: const BouncingScrollPhysics(),
                );
              }
              else {
                return const SizedBox(height: 0,);
              }
            },
        ),
      ],
    );
  }

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

  //----------------------------------------------------------------------------
  //----------------------------- Clear photo title text field -----------------
  //----------------------------------------------------------------------------

  _clearPhotoTitleTextField() {
    photoTitleController.clear();
  }
}

DocumentCard:

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:app_test/model/document.dart';

import '../constant/color.dart';
import '../constant/text.dart';
import '../utils/file_list_controller.dart';

class DocumentCard extends StatefulWidget {
  final Document document;
  final String extension;
  final List<Document>? documents;
  final int index;

  const DocumentCard({Key? key, required this.document,
    required this.extension, required this.documents, required this.index}) : super(key: key);

  @override
  State<DocumentCard> createState() => _DocumentCardState();
}

class _DocumentCardState extends State<DocumentCard> {
  final FileListController fileController = FileListController();
  late StreamSubscription<List<Document>?> subscription;

  @override
  void initState() {
    subscription = fileController.controller.stream.listen((event) {
      event!.removeAt(widget.index);
      print("List after remove inside InitState: ${event.length}");
    },
    );
    super.initState();
  }
  
  //----------------------------------------------------------------------------------------------
  //----------------------------- Free memory allocated to the existing variables ----------------
  //----------------------------------------------------------------------------------------------

  @override
  void dispose() {
    fileController.controller.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    print("Document: ${widget.documents} n"
          "Index item: ${widget.index}");

    return Card(
      elevation: 5,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(15.0),
      ),
      child: Row(
        children: [
          //----------------------------- Picked file image --------------------
          Image.file(
            fit: BoxFit.cover,
            width: 100,
            height: 100,
            widget.document.documentPhoto!,
            errorBuilder: (context, exception, stackTrace) {
              return widget.extension == "pdf" || widget.extension != "jpg" || widget.extension != "jpeg" || widget.extension != "png"
                  ? Image.asset(
                    fit: BoxFit.cover,
                    width: 100,
                    height: 100,
                    "assets/images/pdf.png",
                  )
                  : Container(
                      color: grey,
                      width: 100,
                      height: 100,
                      child: const Center(
                        child: Text(
                            errorLoadImage,
                            textAlign: TextAlign.center,
                        ),
                      ),
                  );
            },
          ),
          const SizedBox(width: 20,),
          //----------------------------- Picked file title --------------------
          Text(
            widget.document.photoTitle!,
            style: const TextStyle(
              fontSize: 16,
              color: black,
            ),
          ),
          const Spacer(),
          //----------------------------- Delete icon button -------------------
          IconButton(
              onPressed: () {
                print("List size before delete: ${widget.documents!.length}");
                _deleteDocument(widget.index);
                print("List size after delete: ${widget.documents!.length}");
              },
              icon: const Icon(
                Icons.delete,
                color: red,
              ),
          ),
        ],
      ),
    );
  }

  //----------------------------------------------------------------------------
  //----------------------------- Delete document ------------------------------
  //----------------------------------------------------------------------------

  Future _deleteDocument(int index) async {
    widget.documents!.removeAt(index);
    //fileController.inputFileList.add(widget.documents!);
  }
}

Thanks in advance

Leave a Comment