See for the invoice page I have BlocBuilder wrapped in a scaffold of stateful page, inside that body under several widgets is a call to future void in separate file call to create a dialog widget. And inside the dialog method is a call to create an invoice form which is in a separate file and is stateful class displayed to be displayed on the dialog screen. In this form the user will be able to add and delete UI elements from a list view what I need to do is rebuild the widget either dialog screen/form or the list view/ to reflect the changes made by the user
import 'package:flutter/material.dart';
import 'dart:developer' as dev;
import 'package:track/src/features/invoices/application/bloc.dart';
import 'package:track/src/features/invoices/application/events.dart';
import 'package:track/src/features/invoices/application/pdf_invoice_api.dart';
class InvoiceForm extends StatefulWidget {
final InvoiceBlocController blocController;
const InvoiceForm(this.blocController, {Key? key}) : super(key: key);
@override
State<InvoiceForm> createState() => _InvoiceFormState();
}
class _InvoiceFormState extends State<InvoiceForm> {
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
controller: TextEditingController()
..text = widget.blocController.invoice.client,
validator: (value) {
value!.isEmpty ? 'Enter a value for client' : null;
},
style: Theme.of(context).textTheme.labelMedium,
decoration: InputDecoration(
focusedBorder: const UnderlineInputBorder(
borderSide: BorderSide(
color: Colors.white,
),
),
enabledBorder: const UnderlineInputBorder(
borderSide: BorderSide(
color: Colors.white,
),
),
labelText: 'Client:',
labelStyle: Theme.of(context).textTheme.labelMedium),
),
TextFormField(
controller: TextEditingController()
..text =
'${widget.blocController.invoice.projectNumber}-${widget.blocController.invoice.invoiceNumber}',
validator: (value) {
value!.isEmpty ? 'Enter a valid project number' : null;
},
style: Theme.of(context).textTheme.labelMedium,
decoration: InputDecoration(
focusedBorder: const UnderlineInputBorder(
borderSide: BorderSide(
color: Colors.white,
),
),
enabledBorder: const UnderlineInputBorder(
borderSide: BorderSide(
color: Colors.white,
),
),
labelText: 'Client:',
labelStyle: Theme.of(context).textTheme.labelMedium),
),
ListView.builder(
shrinkWrap: true,
itemCount: widget.blocController.invoice.items.length,
itemBuilder: (context, index) {
final item = widget.blocController.invoice.items[index];
return ListTile(
contentPadding: EdgeInsets.zero,
trailing: IconButton(
onPressed: () {
widget.blocController.add(DeleteItemFromInvoice(index));
},
icon: Icon(Icons.delete)),
title: Column(
children: [
Row(
children: [
itemTextFormField(
initialValue: item.name ?? '',
labelText: 'name',
index: index),
SizedBox(width: 20),
itemTextFormField(
initialValue: item.description ?? '',
labelText: 'description',
index: index),
],
),
Row(
children: [
itemTextFormField(
initialValue: item.quantity.toString(),
labelText: 'quantity',
index: index),
SizedBox(width: 20),
itemTextFormField(
initialValue: item.costBeforeVAT.toString(),
labelText: 'Cost Before VAT',
index: index),
],
),
SizedBox(height: 30),
Divider(
thickness: 2,
color: Colors.black,
)
],
),
);
},
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
onPressed: () {
dev.log('button clicked to add new item');
widget.blocController.add(AddNewItemToInvoice());
},
icon: Icon(Icons.add)),
IconButton(
onPressed: () async {
_formKey.currentState!.save();
Navigator.pop(context);
await PdfInvoiceApi.generate(widget.blocController.invoice);
},
icon: Icon(Icons.send))
],
)
],
),
);
}
Expanded itemTextFormField({
required String initialValue,
required String labelText,
required int index,
}) {
return Expanded(
child: TextFormField(
controller: TextEditingController()..text = initialValue,
onSaved: (newValue) {
widget.blocController.add(UpdateInvoiceDetails(index));
},
style: Theme.of(context).textTheme.labelMedium,
decoration: InputDecoration(
focusedBorder: const UnderlineInputBorder(
borderSide: BorderSide(
color: Colors.white,
),
),
enabledBorder: const UnderlineInputBorder(
borderSide: BorderSide(
color: Colors.white,
),
),
labelText: labelText,
labelStyle: Theme.of(context).textTheme.labelMedium,
),
),
);
}
}
InvoiceDialog Source code: https://pastebin.com/PCjmCWsk InvoiceDialog Source code: https://pastebin.com/VS5CG22D
Edit 2: Made the follwoing changes to bloc per Mostafa answer as best I could, getting pressed against a deadline here so really need some help: These changes were to main page calling the show dialog passing bloc.
showDialog(
context: context,
builder: (context) => BlocProvider.value(
value: blocController,
child: InvoiceDetailsDialog(
screenWidth: screenWidth,
screenHeight: screenHeight),
),
);
This file was the original place where showdialog was called and was custom Future return showDialog. Results: showDialog takes enitre screen. Rendering Invoice form reulsts in error being displayed in place of the form:
No Material widget found.
Edit 3: fixed previous error but back where i started bloc is still being called succesfully but no changes to the ui:
Widget build(BuildContext context) {
final blocController = BlocProvider.of<InvoiceBlocController>(context);
return Center(
child: Material(color: Colors.red,
borderRadius: BorderRadius.circular(50),
child: SizedBox(
width: screenWidth / 2, height: screenHeight / 2,
child: Padding(padding: const EdgeInsets.all(20),
child: Column(children: [
Expanded(child: ListView(children: [
Text('Invoices',
style: Theme.of(context)
.textTheme.bodyLarge?.copyWith(color: Colors.white)),
InvoiceForm()
]))])))));
}
As form nothing changed except instead of passing the blocController through a method I am now calling it like:
class _InvoiceFormState extends State<InvoiceForm> {
final _formKey = GlobalKey<FormState>();
late final InvoiceBlocController blocController;
@override
void initState() {
blocController = BlocProvider.of<InvoiceBlocController>(context);
super.initState();
}
Still nothing changes.
Edit 4: Set state does work and leaving in bloc code was executing and if I clicked add two items would be added or delete would remove two items. But with setstate commented out it went back to not rebuilding. Using setstate for now but not preferred.
Edit 5: Don't if this is still being paid attention to hopefully is. Can I keep add add events like: add(NewItem), add(deleteItem),add(GeneratePDF). Without changing state. currently I have done that once so far. Is this bad practice
You can pass the main bloc to the dialog widget and call the bloc function that you want and it will reflect on the main screen
How can you do this? by injecting the MainBloc
value to DialogWidget
with BlocProvider.value
MainWidget
class MainWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (BuildContext context) => MainBloc(),
child: BlocConsumer<MainBloc, MainStates>(
listener: (BuildContext context, MainStates state) {},
builder: (BuildContext context, MainStates state) {
final bloc = MainBloc.get(context);
return GestureDetector(
onTap: () {
showDialog(
context,
builder: (context) => BlocProvider.value(
value: bloc,
child: WidgetTwoDialog(),
),
);
},
child: Item(),
);
},
),
);
}
}
DialogWidget
class DialogWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final bloc = MainBloc.get(context);
return GestureDetector(
onTap: () {
bloc.addToList();
},
child: Text('Remove form the main screen'),
);
}
}
Also, this answer might help you to get my point well here