Multi Page Form Architecture - flutter

I'm trying to build a system to create forms with multiples pages. My approach was to separate it in three different parts.
FormPages: The different form pages (Each one will have its own logic to validate fields).
ProjectFormContainer: The container page that holds the pages inside a Navigator.
MultiPageFormController: A controller to manage the navigation between form pages.
I've managed to make some progress adding to the ProjectFormContainer a ChangeNotifierProvider of MultiPageFormController but I'm not sure how to connect the individual formPages logic with the rest of the elements and what's the best way to make a decent architecture for this model.
I hope you guys could give me some advice. Thanks in advance!

Here is a Solution based on Flow Builder from Felix Angelov, the author of Bloc State Management.
I used the following packages:
Flow Builder to manage the flow of our form
Flutter Hooks to get rid of the Stateful Widgets and have a leaner code base
Freezed to manage the immutability of our User Info
[optional] Flutter Color Picker
As you will see if you check Flow Builder documentation, it manages a deck of Form Widgets, one for each step of the Flow. Once it completes the Flow, it pops the Widgets out and gets back to the Main Page with the User Info.
1. Creation of the Flow
FlowBuilder<UserInfo>(
state: const UserInfo(),
onGeneratePages: (profile, pages) {
return [
const MaterialPage(child: NameForm()),
if (profile.name != null) const MaterialPage(child: AgeForm()),
if (profile.age != null) const MaterialPage(child: ColorForm()),
];
},
),
2. Flow Management
At the end of each step, we either continue the flow:
context
.flow<UserInfo>()
.update((info) => info.copyWith(name: name.value));
Or, we complete it:
context
.flow<UserInfo>()
.complete((info) => info.copyWith(favoriteColor: color.value));
3. Main Page
And, on our Main Page, we navigate to the OnboardingFlow and await for it to complete:
userInfo.value = await Navigator.of(context).push(OnboardingFlow.route());
Full source code for easy copy-paste
import 'package:flow_builder/flow_builder.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
part 'main.freezed.dart';
void main() {
runApp(
const MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flow Demo',
home: HomePage(),
),
);
}
// MAIN PAGE
class HomePage extends HookWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final userInfo = useState<UserInfo?>(null);
return Scaffold(
backgroundColor:
userInfo.value == null ? Colors.white : userInfo.value!.favoriteColor,
appBar: AppBar(title: const Text('Flow')),
body: Container(
padding: const EdgeInsets.all(8.0),
alignment: Alignment.center,
child: userInfo.value == null
? ElevatedButton(
onPressed: () async {
userInfo.value =
await Navigator.of(context).push(OnboardingFlow.route());
},
child: const Text('GET STARTED'),
)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Welcome, ${userInfo.value!.name}!',
style: const TextStyle(fontSize: 48.0),
),
const SizedBox(height: 48.0),
Text(
'So, you are ${userInfo.value!.age} years old and this is your favorite color? Great!',
style: const TextStyle(fontSize: 32.0),
),
],
),
),
);
}
}
// FLOW
class OnboardingFlow extends StatelessWidget {
const OnboardingFlow({Key? key}) : super(key: key);
static Route<UserInfo> route() {
return MaterialPageRoute(builder: (_) => const OnboardingFlow());
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FlowBuilder<UserInfo>(
state: const UserInfo(),
onGeneratePages: (profile, pages) {
return [
const MaterialPage(child: NameForm()),
if (profile.name != null) const MaterialPage(child: AgeForm()),
if (profile.age != null) const MaterialPage(child: ColorForm()),
];
},
),
);
}
}
// FORMS
class NameForm extends HookWidget {
const NameForm({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final name = useState<String?>(null);
return Scaffold(
appBar: AppBar(title: const Text('Name')),
body: Container(
padding: const EdgeInsets.all(8.0),
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextField(
autofocus: true,
onChanged: (value) => name.value = value,
decoration: const InputDecoration(
labelText: 'Name',
hintText: 'Enter your name',
),
),
const SizedBox(height: 24.0),
ElevatedButton(
child: const Text('Continue'),
onPressed: () {
if (name.value != null && name.value!.isNotEmpty) {
context
.flow<UserInfo>()
.update((info) => info.copyWith(name: name.value));
}
},
)
],
),
),
);
}
}
class AgeForm extends HookWidget {
const AgeForm({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final age = useState<int?>(null);
return Scaffold(
appBar: AppBar(title: const Text('Age')),
body: Container(
padding: const EdgeInsets.all(8.0),
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
DropdownButtonFormField<int>(
items: List.generate(
200,
(index) => DropdownMenuItem(
value: index,
child: Text(index.toString()),
),
),
onChanged: (value) => age.value = value,
decoration: const InputDecoration(
labelText: 'Age',
hintText: 'How old are you?',
),
),
const SizedBox(height: 24.0),
ElevatedButton(
child: const Text('Continue'),
onPressed: () {
if (age.value != null) {
context
.flow<UserInfo>()
.update((info) => info.copyWith(age: age.value));
}
},
)
],
),
),
);
}
}
class ColorForm extends HookWidget {
const ColorForm({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final color = useState<Color>(Colors.amber);
return Scaffold(
appBar: AppBar(title: const Text('Favorite Color')),
body: Container(
padding: const EdgeInsets.all(8.0),
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ColorPicker(
pickerColor: color.value,
onColorChanged: (value) => color.value = value,
pickerAreaHeightPercent: 0.8,
),
const SizedBox(height: 24.0),
ElevatedButton(
child: const Text('Continue'),
onPressed: () {
context.flow<UserInfo>().complete(
(info) => info.copyWith(favoriteColor: color.value));
},
)
],
),
),
);
}
}
// DOMAIN
#freezed
abstract class UserInfo with _$UserInfo {
const factory UserInfo({
String? name,
int? age,
Color? favoriteColor,
}) = _UserInfo;
}

Related

Getx not updating list of TextFormField correctly

I am using Getx and ListView.builder in a Flutter Web app to render a list of items with TextFormField. Each rendered item has a delete button. When I click to delete an item, the list containing the data seems to update correctly but the corresponding UI incorrectly removes the absolute last item displayed instead of the actual item that you clicked to delete. This problem seems to happen with TextFormField specifically.
I have included below a sample app that illustrates the problem. To test, just install Getx, then run the app (I run it as a Web app). Once the app is running, in the left column (named 'Using TextFormFields'), try to delete items and you'll see the problem -- it is always the last item displayed that deletes, even if you click to delete the first item. For comparison, I have included on right side a set up using ListTiles instead of TextFormFields and that works without a problem.
Does anyone know why this problem occurs with TextFormField specifically? Do you know how to solve this? Thanks in advance for any help!
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class Item {
final int id;
final String name;
Item({
required this.id,
required this.name,
});
}
class OverviewPageController extends GetxController {
final itemsList = [
Item(id: 0, name: 'Item 1'),
Item(id: 1, name: 'Item 2'),
Item(id: 2, name: 'Item 3'),
Item(id: 3, name: 'Item 4'),
].obs;
void deleteItem(Item item) {
int index = itemsList.indexOf(item);
var itemRemoved = itemsList.removeAt(index);
print('item deleted: ${itemRemoved.name}');
itemsList.refresh();
}
}
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const GetMaterialApp(
debugShowCheckedModeBanner: false,
home: OverviewPage(),
);
}
}
class OverviewPage extends StatelessWidget {
const OverviewPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Padding(
padding: const EdgeInsets.all(40),
child: Row(
children: const [
Expanded(child: TextFormFieldsSection()),
SizedBox(width: 40),
Expanded(child: ListTilesSection()),
],
),
),
),
);
}
}
class ListTilesSection extends StatelessWidget {
const ListTilesSection({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final controller1 = Get.put(OverviewPageController(), tag: '1');
return Column(
children: [
const Text('Using ListTiles', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
const SizedBox(height: 20),
Obx(
() => ListView.builder(
shrinkWrap: true,
itemCount: controller1.itemsList.length,
itemBuilder: (context, index) {
return Column(
children: [
ListTile(
title: Text(controller1.itemsList[index].name),
trailing: OutlinedButton(
onPressed: () => controller1.deleteItem(controller1.itemsList[index]),
child: const Icon(Icons.delete_forever),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: const BorderSide(color: Colors.black12),
),
),
const SizedBox(height: 5),
],
);
},
),
),
],
);
}
}
class TextFormFieldsSection extends StatelessWidget {
const TextFormFieldsSection({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final controller2 = Get.put(OverviewPageController(), tag: '2');
return Column(
children: [
const Text('Using TextFormFields', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
const SizedBox(height: 20),
Obx(
() => ListView.builder(
shrinkWrap: true,
itemCount: controller2.itemsList.length,
itemBuilder: (context, index) {
return Column(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.black12)
),
child: Row(
children: [
Expanded(
child: TextFormField(
readOnly: true,
initialValue: controller2.itemsList[index].name,
decoration: const InputDecoration(border: InputBorder.none),
),
),
OutlinedButton(
onPressed: () => controller2.deleteItem(controller2.itemsList[index]),
child: const Icon(Icons.delete_forever),
),
],
),
),
const SizedBox(height: 5),
],
);
},
),
),
],
);
}
}
you're code is totally right and should work fine, it's Flutter that doesn't know which widget to exactly delete, I will explain:
when Flutter engine notices that an existent element in the tree is deleted from the tree ( which your code does ) it look to replace it with some other widget with it's same runtimeType, so when you wanna delete a TextFormField, Flutter mistaken the exact widget to remove even if your code is totally fine.
This behavior is really helpful for performance in most cases, to avoid extra necessary builds in the tree when it should.
The Answer of your problem is by telling Flutter that each TextFormField is unique from all others, by assigning a unique Key to the TextFormField, Here is your new code:
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class Item {
final int id;
final String name;
Item({
required this.id,
required this.name,
});
}
class OverviewPageController extends GetxController {
final itemsList = [
Item(id: 0, name: 'Item 1'),
Item(id: 1, name: 'Item 2'),
Item(id: 2, name: 'Item 3'),
Item(id: 3, name: 'Item 4'),
].obs;
void deleteItem(Item item) {
int index = itemsList.indexOf(item);
var itemRemoved = itemsList.removeAt(index);
print('item deleted: ${itemRemoved.name}');
itemsList.refresh();
}
}
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const GetMaterialApp(
debugShowCheckedModeBanner: false,
home: OverviewPage(),
);
}
}
class OverviewPage extends StatelessWidget {
const OverviewPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Padding(
padding: const EdgeInsets.all(40),
child: Row(
children: const [
Expanded(child: TextFormFieldsSection()),
SizedBox(width: 40),
Expanded(child: ListTilesSection()),
],
),
),
),
);
}
}
class ListTilesSection extends StatelessWidget {
const ListTilesSection({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final controller1 = Get.put(OverviewPageController(), tag: '1');
return Column(
children: [
const Text('Using ListTiles',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
const SizedBox(height: 20),
Obx(
() => ListView.builder(
shrinkWrap: true,
itemCount: controller1.itemsList.length,
itemBuilder: (context, index) {
return Column(
key: UniqueKey(), // add UniqueKey()
children: [
ListTile(
key: UniqueKey(),
title: Text(controller1.itemsList[index].name),
trailing: OutlinedButton(
onPressed: () =>
controller1.deleteItem(controller1.itemsList[index]),
child: const Icon(Icons.delete_forever),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: const BorderSide(color: Colors.black12),
),
),
const SizedBox(height: 5),
],
);
},
),
),
],
);
}
}
class TextFormFieldsSection extends StatelessWidget {
const TextFormFieldsSection({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final controller2 = Get.put(OverviewPageController(), tag: '2');
return Column(
children: [
const Text('Using TextFormFields',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
const SizedBox(height: 20),
Obx(
() => ListView.builder(
shrinkWrap: true,
itemCount: controller2.itemsList.length,
itemBuilder: (context, index) {
return Column(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.black12)),
child: Row(
children: [
Expanded(
child: TextFormField(
key: UniqueKey(), // add UniqueKey()
readOnly: true,
initialValue: controller2.itemsList[index].name,
decoration:
const InputDecoration(border: InputBorder.none),
),
),
OutlinedButton(
onPressed: () => controller2
.deleteItem(controller2.itemsList[index]),
child: const Icon(Icons.delete_forever),
),
],
),
),
const SizedBox(height: 5),
],
);
},
),
),
],
);
}
}
Note the UniqueKey() I assigned to widgets, now run your app again and it should work as you expect.
Please, refer also to those topics:
When to Use Keys - Flutter Widget
Key

Can someone help me regarding this error im facing while adding a flutter page in route?

class BMICalculator extends StatelessWidget {
const BMICalculator({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(
appBarTheme: const AppBarTheme(color: Color(0xFF0A0E21)),
scaffoldBackgroundColor: const Color(0xFF0A0E21),
),
initialRoute: '/',
routes: {
InputPage.routeName: (context) => const InputPage(),
ResultsPage.routeName: (context) => const ResultsPage(),
},
);
}
}
The named parameter 'xyz' is required, but theres no corresponding argument. Try adding the required argument.
Can someone guide me on how to tackle this. I dont know what to do and i cant find any solutions anywhere regarding how to nagivate without the error mentioned above
Im adding results page here as well
RESULTS PAGE
class ResultsPage extends StatefulWidget {
const ResultsPage(
{required this.bmiResult,
required this.interpretation,
required this.resultText,
super.key});
static const routeName = '/resultsPage';
final String? bmiResult;
final String? resultText;
final String? interpretation;
#override
State<ResultsPage> createState() => _ResultsPageState();
}
class _ResultsPageState extends State<ResultsPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Center(
child: Text('BMI Calculator'),
),
),
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Container(
padding: const EdgeInsets.all(15.0),
alignment: Alignment.bottomCenter,
child: const Text(
'Your Result',
style: kTitleTextStyle,
),
),
),
Expanded(
flex: 6,
child: ReusableCard(
colour: kActiveCardColor,
cardChild: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: const [
Text(
'Normal',
style: kResultTextStyle,
),
Text(
'18.3',
style: kBMITextStyle,
),
Text(
'Your res is low. Eat more',
style: kBodyTextStyle,
),
],
),
),
),
BottomButton(
onTap: () {
setState(() {
Navigator.pop(context);
});
},
buttonTitle: 'RE-CALCULATE',
),
],
),
),
);
}
}
You can remove required from parameters while all are nullable,
class ResultsPage extends StatefulWidget {
const ResultsPage(
{this.bmiResult, this.interpretation, this.resultText, super.key});
static const routeName = '/resultsPage';
Or you can do like
ResultsPage.routeName: (context) {
final args = ModalRoute.of(context)?.settings.arguments as Map?;//if you are passing map type data
return ResultsPage(
bmiResult: args?["bmiResult"],//use your key
interpretation: args?["interpretation"],
resultText: args?["resultText"],
);
},

How to pass parameters using pushNamedAndRemoveUntil?

I need to pass parameters to the initial page using pushNamedAndRemoveUntil .
I need to re-pass an arg that takes color and textController.text to valueText and color on the initial page. You need to do this with pushNamedAndRemoveUntil.
Please help implement this functionality.
Screen with arg:
class TextValue extends StatefulWidget {
static const routeName = '/text_value';
const TextValue({Key? key}) : super(key: key);
#override
State<TextValue> createState() => _TextValueState();
}
class _TextValueState extends State<TextValue> {
// controller for textField
TextEditingController textController = TextEditingController();
#override
void dispose() {
textController.dispose();
super.dispose();
}
//arg variable
ColorArguments? arg;
#override
Widget build(BuildContext context) {
//get arg from ColorPickerScreen
arg ??= ModalRoute.of(context)!.settings.arguments as ColorArguments;
return Scaffold(
backgroundColor: arg?.color,
appBar: AppBar(
title: const Text('Enter a value'),
centerTitle: true,
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextFormField(
controller: textController,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'(^\d*\.?\d*)'))
],
style: const TextStyle(color: Colors.black),
decoration: const InputDecoration(
hintText: 'Enter a value',
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2)),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2))),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
if (textController.text.isEmpty) {
} else {
Navigator.pushNamedAndRemoveUntil(
context, HomeScreen.routeName, (route) => false,
);
}
},
child: const Text('Done'),
)
],
),
),
);
}
}
Initial Screen:
import 'package:flutter/material.dart';
import 'package:flutter_app/screens/color_picker_screen.dart';
class HomeScreen extends StatefulWidget {
final String valueText;
final ColorArguments? color;
static const routeName = '/home';
const HomeScreen({Key? key, required this.valueText, required this.color})
: super(key: key);
#override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
// navigation to the next screen
void _colorScreen() {
Navigator.push(context,
MaterialPageRoute(builder: (context) => const ColorPicker()));
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
centerTitle: true,
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: _colorScreen, child: const Text('Choose a color')),
const SizedBox(height: 30.0),
TextFormField(
readOnly: true,
initialValue: widget.valueText,
),
const SizedBox(height: 100),
Container(
width: 50,
height: 50,
color: widget.color?.color,
),
],
),
),
);
}
}
You can use the named arguments parameter of the pushNamedAndRemoveUntil method. All you need to do is create a custom object for passing all the values you need to the push call. The arguments can be accessed in your initial screen by using
final args = ModalRoute.of(context)!.settings.arguments
You can refer to this Flutter cookbook on navigation using named routes with arguments.
Why dont you consider using Class contructors and passing data like this
Navigator.push(context,
MaterialPageRoute(builder: (context) => const ColorPicker(<Your_data>)));

create field for load images

I have a code that outputs fields for the user to fill in (code below. I have shortened it here for ease of reading.). I would like to add one more field to this form, which can upload various photos from the phone gallery (preferably with the ability to delete a photo if the user made a mistake when choosing). How can I implement this?
class FormForDeviceService extends StatefulWidget {
#override
State<StatefulWidget> createState() => _FormForDeviceService();
}
class _FormForDeviceService extends State {
final _formKey = GlobalKey<FormState>();
Widget build(BuildContext context) {
return Container(padding: const EdgeInsets.all(10.0),
child: Form(key: _formKey, child: Column(children: <Widget>[
new Text('What is problem', style: TextStyle(fontSize: 20.0),),
new TextFormField(decoration: const InputDecoration(
hintText: 'Describe the problem',),
ElevatedButton(
onPressed: (){if(_formKey.currentState!.validate()) {_formKey.currentState?.reset();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Form completed successfully', style: TextStyle(color: Colors.black),),
backgroundColor: Colors.yellow,));
}},
child: const Text('Submit', style: TextStyle(color: Colors.black),),
style: ButtonStyle(backgroundColor: MaterialStateProperty.all(Colors.yellow)),)
],)));
}
}
Page at the moment
my expectations (or something similar)
An another approach is here-
(I have made a separate widget to handle all these things and you just need to attach it in any scrollable widget)
my code is as follow:
Main Code with that custom widget:
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_memory/image_picker_widget.dart';
void main() {
runApp(GetMaterialApp(title: 'Flutter', home: Flutter()));
}
class Flutter extends StatefulWidget {
const Flutter({Key? key}) : super(key: key);
#override
State<Flutter> createState() => _FlutterState();
}
class _FlutterState extends State<Flutter> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter'),
centerTitle: true,
),
body: Center(
child: Column(
children: [
//This is the widget I am talking about
ImagePickerWidget()
],
),
),
);
}
}
And now the code for that custom widget:
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
class ImagePickerWidget extends StatefulWidget {
const ImagePickerWidget({Key? key}) : super(key: key);
#override
State<ImagePickerWidget> createState() => _ImagePickerWidgetState();
}
class _ImagePickerWidgetState extends State<ImagePickerWidget> {
late List<CustomImage> images;
late double size;
late ImagePicker imagePicker;
late int idGenerator;
#override
void initState() {
images = [];
size = 100;
idGenerator = 0;
imagePicker = ImagePicker();
}
#override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
pickImage();
},
child: Text('Pick Image')),
Wrap(
children: images.map((image) {
return Stack(children: [
SizedBox(
height: size,
width: size,
child: ClipRRect(
child: Image.memory(
image.imageData,
fit: BoxFit.fill,
))),
Positioned(
right: 4,
top: 4,
child: InkWell(
onTap: () {
//delete image
images.removeWhere(
(element) => element.imageData == image.imageData);
setState(() {});
},
child: Container(
color: Colors.white, child: Icon(Icons.clear))))
]);
}).toList())
],
);
}
Future<void> pickImage() async {
// XFile? image = await imagePicker.pickImage(source: ImageSource.camera);
XFile? image = await imagePicker.pickImage(source: ImageSource.gallery);
if (image != null) {
Uint8List imageData = await image.readAsBytes();
int id = idGenerator++;
images.add(CustomImage(imageData: imageData, id: id));
setState(() {});
}
}
}
class CustomImage {
Uint8List imageData;
int id;
CustomImage({required this.imageData, required this.id});
}
You can customize the widget in order to use the images list of that widget or you can simply pass the callbacks for that.
we store file here your can use path(string) instead file
List<File> myfile = [];
image_picker package used here to pick image
image_picker: ^0.8.4+10
call like this in your code
Container(
height: 200,
padding: EdgeInsets.all(4),
child: PickPhoto())
Pick photo widget
class PickPhoto extends StatefulWidget {
const PickPhoto({Key? key}) : super(key: key);
#override
State<PickPhoto> createState() => _PickPhotoState();
}
class _PickPhotoState extends State<PickPhoto> {
#override
Widget build(BuildContext context) {
return Material(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Container(
width: 45,
height: 45,
child: ElevatedButton(
onPressed: () async {
var file =
await picker?.pickImage(source: ImageSource.gallery);
setState(() {
myfile.add(File(file!.path));
});
},
child: Text("Add Photo"))),
),
Expanded(
child: ListView.builder(
// physics: NeverScrollableScrollPhysics(),
scrollDirection: Axis.horizontal,
itemCount: myfile.length,
itemBuilder: (context, index) => Container(
padding: EdgeInsets.all(4),
height: 175,
width: 125,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Align(
alignment: Alignment.topRight,
child: IconButton(
onPressed: () {
setState(() {
myfile.removeAt(index);
});
},
icon: Icon(Icons.close),
),
),
Expanded(
child: Container(
child: myfile[index] == null
? Text("")
: Image.file(
myfile[index],
fit: BoxFit.fill,
),
),
),
],
),
)),
)
],
),
);
}
}
SampleCode
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
ImagePicker? picker;
void main() {
WidgetsFlutterBinding.ensureInitialized();
picker = ImagePicker();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'MySQL Test',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Column(
children: [FormForDeviceService()],
),
);
}
}
List<File> myfile = [];
List<int> f = [1, 2, 3, 4, 5];
List<bool> fs = [false, false, false, true, true];
class FormForDeviceService extends StatefulWidget {
#override
State<StatefulWidget> createState() => _FormForDeviceService();
}
class _FormForDeviceService extends State {
final _formKey = GlobalKey<FormState>();
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(10.0),
child: Form(
key: _formKey,
child: Column(
children: <Widget>[
new Text(
'What is problem',
style: TextStyle(fontSize: 20.0),
),
new TextFormField(
decoration: const InputDecoration(
hintText: 'Describe the problem',
),
),
Container(
height: 200,
padding: EdgeInsets.all(4),
child: PickPhoto()),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
_formKey.currentState?.reset();
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text(
'Form completed successfully',
style: TextStyle(color: Colors.black),
),
backgroundColor: Colors.yellow,
));
}
},
child: const Text(
'Submit',
style: TextStyle(color: Colors.black),
),
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all(Colors.yellow)),
)
],
)));
}
}
class PickPhoto extends StatefulWidget {
const PickPhoto({Key? key}) : super(key: key);
#override
State<PickPhoto> createState() => _PickPhotoState();
}
class _PickPhotoState extends State<PickPhoto> {
#override
Widget build(BuildContext context) {
return Material(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Container(
width: 45,
height: 45,
child: ElevatedButton(
onPressed: () async {
var file =
await picker?.pickImage(source: ImageSource.gallery);
setState(() {
myfile.add(File(file!.path));
});
},
child: Text("Add Photo"))),
),
Expanded(
child: ListView.builder(
// physics: NeverScrollableScrollPhysics(),
scrollDirection: Axis.horizontal,
itemCount: myfile.length,
itemBuilder: (context, index) => Container(
padding: EdgeInsets.all(4),
height: 175,
width: 125,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Align(
alignment: Alignment.topRight,
child: IconButton(
onPressed: () {
setState(() {
myfile.removeAt(index);
});
},
icon: Icon(Icons.close),
),
),
Expanded(
child: Container(
child: myfile[index] == null
? Text("")
: Image.file(
myfile[index],
fit: BoxFit.fill,
),
),
),
],
),
)),
)
],
),
);
}
}

Listview scrolling and selecting Textfield afterwards is freezing my app

I am using the package
country_code_picker: ^1.4.0
https://pub.dev/packages/country_code_picker#-installing-tab-
with flutter 1.17.3
Which is pretty much one of the only country code picker packages. But I have one serious problem an I don't have a clue what it could be.
When I run this code
import 'package:flutter/material.dart';
import 'package:country_code_picker/country_code_picker.dart';
void main() {
runApp(App());
}
class App extends StatelessWidget {
App();
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: TestWidget(),
);
}
}
class TestWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(body: _buildCountryPicker(context));
}
Widget _buildCountryPicker(BuildContext context) {
return Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Center(
child: CountryCodePicker(
initialSelection: 'NL',
),
),
);
}
}
And I open the dialog to select a country. I scroll in the list and then select the TextField my keyboard opens and when I try to type something my entire app freezes. I can't even hot reload. I don't get a single error.
I am running this on my Huawei P30, but I also experience this on other android devices. I don't know if this is a flutter bug or a country code picker bug.
I think it is probably in this widget somewhere. If anyone could point me in the right direction it would help me alot!
class SelectionDialog extends StatefulWidget {
final List<CountryCode> elements;
final bool showCountryOnly;
final InputDecoration searchDecoration;
final TextStyle searchStyle;
final TextStyle textStyle;
final WidgetBuilder emptySearchBuilder;
final bool showFlag;
final double flagWidth;
final Size size;
final bool hideSearch;
/// elements passed as favorite
final List<CountryCode> favoriteElements;
SelectionDialog(
this.elements,
this.favoriteElements, {
Key key,
this.showCountryOnly,
this.emptySearchBuilder,
InputDecoration searchDecoration = const InputDecoration(),
this.searchStyle,
this.textStyle,
this.showFlag,
this.flagWidth = 32,
this.size,
this.hideSearch = false,
}) : assert(searchDecoration != null, 'searchDecoration must not be null!'),
this.searchDecoration =
searchDecoration.copyWith(prefixIcon: Icon(Icons.search)),
super(key: key);
#override
State<StatefulWidget> createState() => _SelectionDialogState();
}
class _SelectionDialogState extends State<SelectionDialog> {
/// this is useful for filtering purpose
List<CountryCode> filteredElements;
#override
Widget build(BuildContext context) => SimpleDialog(
titlePadding: const EdgeInsets.all(0),
title: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
IconButton(
padding: const EdgeInsets.all(0),
iconSize: 20,
icon: Icon(
Icons.close,
),
onPressed: () => Navigator.pop(context),
),
if (!widget.hideSearch)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: TextField(
style: widget.searchStyle,
decoration: widget.searchDecoration,
onChanged: _filterElements,
),
),
],
),
children: [
Container(
width: widget.size?.width ?? MediaQuery.of(context).size.width,
height:
widget.size?.height ?? MediaQuery.of(context).size.height * 0.7,
child: ListView(
children: [
widget.favoriteElements.isEmpty
? const DecoratedBox(decoration: BoxDecoration())
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...widget.favoriteElements.map(
(f) => SimpleDialogOption(
child: _buildOption(f),
onPressed: () {
_selectItem(f);
},
),
),
const Divider(),
],
),
if (filteredElements.isEmpty)
_buildEmptySearchWidget(context)
else
...filteredElements.map(
(e) => SimpleDialogOption(
key: Key(e.toLongString()),
child: _buildOption(e),
onPressed: () {
_selectItem(e);
},
),
),
],
),
),
],
);
Widget _buildOption(CountryCode e) {
return Container(
width: 400,
child: Flex(
direction: Axis.horizontal,
children: <Widget>[
if (widget.showFlag)
Flexible(
child: Padding(
padding: const EdgeInsets.only(right: 16.0),
child: Image.asset(
e.flagUri,
package: 'country_code_picker',
width: widget.flagWidth,
),
),
),
Expanded(
flex: 4,
child: Text(
widget.showCountryOnly
? e.toCountryStringOnly()
: e.toLongString(),
overflow: TextOverflow.fade,
style: widget.textStyle,
),
),
],
),
);
}
Widget _buildEmptySearchWidget(BuildContext context) {
if (widget.emptySearchBuilder != null) {
return widget.emptySearchBuilder(context);
}
return Center(
child: Text('No country found'),
);
}
#override
void initState() {
filteredElements = widget.elements;
super.initState();
}
void _filterElements(String s) {
s = s.toUpperCase();
setState(() {
filteredElements = widget.elements
.where((e) =>
e.code.contains(s) ||
e.dialCode.contains(s) ||
e.name.toUpperCase().contains(s))
.toList();
});
}
void _selectItem(CountryCode e) {
Navigator.pop(context, e);
}
}
Also filed an issue on the flutter github https://github.com/flutter/flutter/issues/59886
Edit:
I have a video of it right here
https://www.youtube.com/watch?v=669KitFG9ek&feature=youtu.be
I just had to remove the keys, so there probably was a duplicate key
...filteredElements.map(
(e) => SimpleDialogOption(
//key: Key(e.toLongString()),
child: _buildOption(e),
onPressed: () {
_selectItem(e);
},
),
),