Sort dynamically ListView in Flutter - flutter

I would like to sort a ListView based on a StreamController in a BottomNavigationBar.
The problem is that the Listview doesn't refresh when I click the button on the app bar.
I would like to pass as parameter the function (Comparable of Company) which the user chooses.
Here's my code:
Home.dart
final CompanyService _companyService = CompanyService();
final AuthService _auth = AuthService();
class HomePage extends StatefulWidget {
HomePage({Key key}) : super(key: key);
Home createState() => Home();
}
class Home extends State<HomePage> {
Comparator<Company> _sortFunction;
int _currentIndex;
var _tabs = [];
#override
void initState() {
super.initState();
_currentIndex = 0;
_sortFunction = (a, b) => a.name.compareTo(b.name);
}
PreferredSize getDoubleHomeAppBar() {
return PreferredSize(
preferredSize: Size.fromHeight(55.0),
child: Row(
children: <Widget>[
Expanded(
child: Container(
padding: EdgeInsets.only(left: 12.0),
margin:
const EdgeInsets.only(left: 12.0, bottom: 8.0, right: 12.0),
color: PRIMARY_COLOR,
),
),
FlatButton.icon(
icon: Icon(Icons.sort_by_alpha),
label: Text(
'Sort',
),
onPressed: () {
setState(() {
_sortFunction = (a, b) => b.city.compareTo(a.city);
_tabs[0] = CompanyTab(sortFunction: _sortFunction);
});
},
),
],
));
}
#override
Widget build(BuildContext context) {
_tabs = [
CompanyTab(sortFunction: _sortFunction),
MapTab(),
Center(child: Text('Profile')),
Center(child: Text('Settings')),
];
return Scaffold(
backgroundColor: BACKGROUND_COLOR,
appBar: AppBar(
elevation: 0.0,
centerTitle: true,
title: Text(
'JobAdvisor',
style: TextStyle(
fontSize: MediaQuery.of(context).size.height / 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
bottom: _currentIndex == 0 ? getDoubleHomeAppBar() : null,
actions: <Widget>[...],
),
body: _tabs[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
backgroundColor: BACKGROUND_COLOR,
type: BottomNavigationBarType.fixed,
items: [
...
],
onTap: (index) {
setState(() {
_currentIndex = index;
print('Index: $index');
print('Current index: $_currentIndex');
});
},
),
);
}
}
CompanyTab.dart
#immutable
class CompanyTab extends StatefulWidget {
final CompanyService _companyService = CompanyService();
final Comparator<Company> sortFunction;
CompanyTab({Key key, this.sortFunction}) : super(key: key);
#override
_CompanyTabState createState() =>
_CompanyTabState(_companyService, sortFunction);
}
class _CompanyTabState extends State<CompanyTab> {
StreamController<List<Company>> _streamController;
final CompanyService _companyService;
Comparator<Company> sortFunction;
_CompanyTabState(this._companyService, this.sortFunction);
#override
void initState() {
super.initState();
}
StreamBuilder companyList() {
return StreamBuilder<List<Company>>(
initialData: [],
stream: _streamController.stream,
builder: (BuildContext context, AsyncSnapshot<List<Company>> snapshot) {
if (snapshot.hasError) {
return Text("Something went wrong");
}
if (snapshot.connectionState == ConnectionState.waiting ||
snapshot.connectionState == ConnectionState.none ||
snapshot.data == null) {
return LoadingWidget();
} else {
return ListView.builder(
padding: const EdgeInsets.all(10),
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
Company company = snapshot.data.elementAt(index);
...
}
}
});
}
#override
void dispose() {
_streamController.close();
super.dispose();
}
void _getData() {
_streamController = new StreamController<List<Company>>();
_companyService.getCompanies().then((value) => {_elaborateList(value)});
}
void _elaborateList(List<Company> list) {
List<Company> tmp = list;
tmp.sort(sortFunction);
print(tmp.toString());
_streamController.sink.add(tmp);
}
#override
Widget build(BuildContext context) {
_getData();
return Center(
child: Container(
child: companyList(),
),
);
}
}

I think the problem is in your sort method. It should be like this:
_sortFunction.sort((a, b) => a.name.compareTo(b.name));
You can read from the official document.
EDIT:
And you need to use sortFunction of the widget in here:
tmp.sort(widget.sortFunction);
You are not using the same sortFunction in CompanyTab. You should use the sortFunction which comes as a parameter. Here is a blog about it.

Related

Flutter bottomNavigationBar not changing pages (when it's the "same page")

I have a bottom navigation bar and realized that the different pages/widgets that the navigator was going to were pretty much the exact same page (except for 2 parameters that changed). So instead of creating 2 pages/widgets which were pretty much identical (with only 2 differing parameters), I wanted to consolidate it into only one widget and pass the parameters from the page with the bottom navigator. The problem is that now that I did that it won't change the page it displays, or at least it won't change consistently (it usually will only show the page that corresponds to the first tab in the navigator (i.e., index = 0)). Here is my page with the bottom navigator:
class FreestylePage extends StatefulWidget {
const FreestylePage({Key? key}) : super(key: key);
#override
State<StatefulWidget> createState() {
return _FreestylePageState();
}
}
class _FreestylePageState extends State<FreestylePage> {
int _currentIndex = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: showCategory(_currentIndex),
)),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.looks_one_outlined),
label: 'Single rope',
backgroundColor: Color.fromRGBO(204, 16, 138, 1)),
BottomNavigationBarItem(
icon: Icon(Icons.looks_two_outlined),
label: 'Double dutch',
backgroundColor: Color.fromRGBO(204, 16, 138, 1)),
],
onTap: (index) {
if (mounted) {
setState(() {
_currentIndex = index;
});
}
},
),
);
}
showCategory(index) {
if (index == 0) {
return [
WorkoutListPage(categoryIndex: 2, subCategories: Utils.srfDropdown)
];
} else {
return [
WorkoutListPage(categoryIndex: 3, subCategories: Utils.ddfDropdown)
];
}
}
}
And the WorkoutListPage looks as follows:
class WorkoutListPage extends StatefulWidget {
final int categoryIndex;
final List<String> subCategories;
const WorkoutListPage(
{Key? key, required this.categoryIndex, required this.subCategories})
: super(key: key);
#override
State<StatefulWidget> createState() {
return _WorkoutListPageState();
}
}
class _WorkoutListPageState extends State<WorkoutListPage> {
bool isLoading = true;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) =>
FutureBuilder<List<Map<String, dynamic>>>(
future: MyCard.getData(widget.categoryIndex, widget.subCategories)!
.whenComplete(() => setState(() {
isLoading = false;
})),
builder: ((context, snapshot) {
if (snapshot.hasData && snapshot.data!.isNotEmpty) {
return FutureBuilder<List<MyCard>>(
future: MyCard.readData(snapshot.data),
builder: (context, cards) {
if (cards.hasData) {
final card = cards.data!;
return Expanded(
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: card.length,
itemBuilder: (context, index) {
return MyCard.buildCard(card[index], context);
},
),
);
} else {
return const Text("No data");
}
});
} else {
return isLoading
? Column(
children: const [CircularProgressIndicator()],
)
: const Text("You do not have any workouts yet");
}
}),
);
}
This doesn't work, but ironically if I change my showCategory function in the widget with the bottom navigation bar to the following:
showCategory(index) {
if (index == 0) {
return [
WorkoutListPage(categoryIndex: 2, subCategories: Utils.srfDropdown)
];
} else {
return [const FreestyleDDPage()];
}
}
where the FreestyleDDPage is the following:
class FreestyleDDPage extends StatefulWidget {
const FreestyleDDPage({Key? key}) : super(key: key);
#override
State<StatefulWidget> createState() {
return _FreestyleDDPageState();
}
}
class _FreestyleDDPageState extends State<FreestyleDDPage> {
var isLoading = true;
#override
Widget build(BuildContext context) =>
FutureBuilder<List<Map<String, dynamic>>>(
future: MyCard.getData(3, Utils.ddfDropdown)!
.whenComplete(() => setState(() {
isLoading = false;
})),
builder: ((context, snapshot) {
if (snapshot.hasData && snapshot.data!.isNotEmpty) {
return FutureBuilder<List<MyCard>>(
future: MyCard.readData(snapshot.data),
builder: (context, cards) {
if (cards.hasData) {
final card = cards.data!;
return Expanded(
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: card.length,
itemBuilder: (context, index) {
return MyCard.buildCard(card[index], context);
},
),
);
} else {
return const Text("No data");
}
});
} else {
return isLoading
? Column(
children: const [CircularProgressIndicator()],
)
: const Text("You do not have any workouts yet");
}
}),
);
}
then it works.
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: _title,
home: MyStatefulWidget(),
);
}
}
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({super.key});
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _selectedIndex = 0;
static const TextStyle optionStyle =
TextStyle(fontSize: 30, fontWeight: FontWeight.bold);
static const List<Widget> _widgetOptions = <Widget>[
CustomWidgetWithParametr(index: 0 , categoryName: "HOME"),
CustomWidgetWithParametr(index: 1 , categoryName: "BUSINES"),
CustomWidgetWithParametr(index: 2 , categoryName: "SCHOOL"),
CustomWidgetWithParametr(index: 3 , categoryName: "Settings"),
];
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('BottomNavigationBar Sample'),
),
body: Center(
child: _widgetOptions.elementAt(_selectedIndex),
),
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
backgroundColor: Colors.red,
),
BottomNavigationBarItem(
icon: Icon(Icons.business),
label: 'Business',
backgroundColor: Colors.green,
),
BottomNavigationBarItem(
icon: Icon(Icons.school),
label: 'School',
backgroundColor: Colors.purple,
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: 'Settings',
backgroundColor: Colors.pink,
),
],
currentIndex: _selectedIndex,
selectedItemColor: Colors.amber[800],
onTap: _onItemTapped,
),
);
}
}
class CustomWidgetWithParametr extends StatefulWidget {
const CustomWidgetWithParametr({Key? key, required this.index, required this.categoryName}) : super(key: key);
final int index;
final String categoryName;
#override
State<CustomWidgetWithParametr> createState() => _CustomWidgetWithParametrState();
}
class _CustomWidgetWithParametrState extends State<CustomWidgetWithParametr> {
#override
Widget build(BuildContext context) {
return
Column(mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(widget.index.toString()),
Text(widget.categoryName),
],
);
}
}

StreamBuilder not updating widget

i have a Widget that has an AppBar with a progress bar, and a PageView with 4 pages, when moving between pages i am increasing / decreasing the progress bar.
I'm trying to do all the logic in my ViewModel.
This is my ViewModel (omitted non relevant stuff):
class RegisterViewModel extends BaseViewModel with RegisterViewModelInputs, RegisterViewModelOutputs {
final StreamController _sexStreamController = StreamController<int>.broadcast();
final StreamController _progressBarController = StreamController<double>.broadcast();
final StreamController _currentIndexController = StreamController<int>.broadcast();
final StreamController _isBackEnabled = StreamController<bool>.broadcast();
double _progress = 0.25;
int _index = 0;
#override
setCurrentIndex(int index) {
currentIndex.add(index);
}
#override
increaseProgress() {
if (_progress <= 1.0) {
_progress += 0.25;
progress.add(_progress);
}
}
#override
decreaseProgress() {
if (_progress > 0) {
_progress -= 0.25;
progress.add(_progress);
}
}
#override
setIsBackEnabled(int index) {
_isBackEnabled.add(index > 0 ? true : false);
}
#override
nextPage() {
if (_index < 4) {
_index++;
increaseProgress();
setCurrentIndex(_index);
}
}
#override
previousPage() {
if (_index > 0) {
_index--;
decreaseProgress();
setCurrentIndex(_index);
}
}
#override
Sink get currentIndex => _currentIndexController.sink;
#override
Sink get progress => _progressBarController.sink;
#override
Sink get isBackEnabled => _isBackEnabled.sink;
#override
Stream<int> get outputCurrentIndex => _currentIndexController.stream.map((currentIndex) => currentIndex);
#override
Stream<double> get outputProgress => _progressBarController.stream.map((progress) => progress);
#override
Stream<bool> get outputIsBackEnabled => outputIsBackEnabled.map((isEnabled) => isEnabled);
}
And here is my View:
class RegisterView extends StatefulWidget {
const RegisterView({Key? key}) : super(key: key);
#override
_RegisterViewState createState() => _RegisterViewState();
}
class _RegisterViewState extends State<RegisterView> {
final RegisterViewModel _viewModel = getIt<RegisterViewModel>();
final PageController _pageController = PageController(initialPage: 0);
final FixedExtentScrollController _weightScrollController = FixedExtentScrollController(initialItem: 80);
final FixedExtentScrollController _ageScrollController = FixedExtentScrollController(initialItem: 13);
final FixedExtentScrollController _heightScrollController = FixedExtentScrollController(initialItem: 13);
#override
void initState() {
_bind();
super.initState();
}
#override
void dispose() {
_viewModel.dispose();
super.dispose();
}
_bind() {
_viewModel.start();
}
#override
Widget build(BuildContext context) {
_viewModel.outputCurrentIndex.listen((index) {
_pageController.animateToPage(index, duration: const Duration(milliseconds: 1000), curve: Curves.ease);
});
List<Widget> pagesList = [
SexPage(
onConfirm: (sex) {
_viewModel.setSex(sex);
_viewModel.nextPage();
},
),
AgePage(
scrollController: _ageScrollController,
),
WeightPage(scrollController: _weightScrollController),
HeightPage(scrollController: _heightScrollController),
];
return Scaffold(
backgroundColor: ColorManager.backgroundColor,
appBar: AppBar(
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: ColorManager.backgroundColor,
statusBarBrightness: Brightness.dark,
statusBarIconBrightness: Brightness.dark,
),
centerTitle: true,
title: AppBarWidget(_pageController),
elevation: AppSize.s0,
),
body: PageView(
reverse: true,
controller: _pageController,
physics: NeverScrollableScrollPhysics(),
children: [...pagesList],
),
);
}
}
class AppBarWidget extends StatelessWidget {
final PageController pageController;
final RegisterViewModel _viewModel = getIt<RegisterViewModel>();
AppBarWidget(
this.pageController, {
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
_viewModel.outputCurrentIndex.listen((index) {
pageController.animateToPage(index, duration: const Duration(milliseconds: 1000), curve: Curves.ease);
});
return Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
flex: 1,
child: InkWell(
child: Text(
AppStrings.skip,
style: Theme.of(context).textTheme.labelMedium,
),
onTap: () => _viewModel.nextPage(),
),
),
Expanded(
flex: 4,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: AppPadding.p60),
child: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(pi),
child: StreamBuilder<double>(
stream: _viewModel.outputProgress,
builder: (context, snapshot) {
return Progresso(
progress: snapshot.data ?? 0,
progressStrokeCap: StrokeCap.round,
backgroundStrokeCap: StrokeCap.round,
progressColor: ColorManager.primary,
backgroundColor: ColorManager.progressBarBackgroundGrey,
progressStrokeWidth: 10.0,
backgroundStrokeWidth: 10.0,
);
}),
),
),
),
StreamBuilder<int>(
stream: _viewModel.outputCurrentIndex,
builder: (context, snapshot) {
return Expanded(
flex: 1,
child: (snapshot.data ?? 0) > 0
? InkWell(
child: Row(
children: [
Text(
AppStrings.back,
style: Theme.of(context).textTheme.labelMedium,
),
Icon(
Icons.arrow_forward_ios,
color: ColorManager.subtitleGrey,
),
],
),
onTap: () => _viewModel.previousPage(),
)
: Container(),
);
}),
],
);
}
}
When i'm calling _viewModel.previousPage() & _viewModel.previousPage()` from the AppBarWidget, the progress bar view is updated, and there is a scroll animation to the next page.
But for some reason if the onConfirm callback:
onConfirm: (sex) {
_viewModel.setSex(sex);
_viewModel.nextPage();
}
is called from within SexPage, the scroll animation is working, but the progress bar view and the isBackEnabled is not updating.
I have checked and a new value is being added to the _progressBarController sink, but for some reason the StreamBuilder does not receive it? same for the isBackEnabled stream..
What am i doing wrong?
And another question i have is where should I listen to the outputCurrentIndex stream, and call _pageController.animateToPage()?
Apparently i had an issue with my Dependency Injection.
I'm using get_it and i used registerFactory, instead of registerLazySingleton.
Which probably made me have 2 separate ViewModels in each widget.

Pass StreamBuilder between BottomNavigationBar tabs => Bad state: Stream has already been listened to

I'm trying to pass Stream from a tab to another tab but when I comeback in the home.dart
May I should close/destroy the Stream when tab changed?
When the app runs the data are fetched correctly and everything is good.The problem appears only when I change tab.
The data are stored to a firestore database.
I get this error:
Bad state: Stream has already been listened to
Here my Home.dart
class HomePage extends StatefulWidget {
HomePage({Key key}) : super(key: key);
Home createState() => Home();
}
class Home extends State<HomePage> {
int _currentIndex;
var _tabs = [];
List<Company> currentCompaniesList = List();
StreamController<List<Company>> _streamController;
Stream<List<Company>> companiesStream;
_getData() async {
_companyService
.getCompanies()
.then((value) => _streamController.sink.add(value));
}
#override
void initState() {
super.initState();
_currentIndex = 0;
_streamController = StreamController<List<Company>>();
_getData();
companiesStream = _streamController.stream;
}
}
#override
Widget build(BuildContext context) {
_tabs = [
CompanyTab(stream: companiesStream),
MapTab(),
Center(child: Text('Profile')),
Center(child: Text('Settings')),
];
return Scaffold(
...
actions: ...,
body: _tabs[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
backgroundColor: BACKGROUND_COLOR,
type: BottomNavigationBarType.fixed,
items: [
BottomNavigationBarItem(
...
),
BottomNavigationBarItem(
...
),
BottomNavigationBarItem(
...
),
BottomNavigationBarItem(
...
)
],
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
),
);
}
}
Here my CompanyTab.dart
class CompanyTab extends StatefulWidget {
Stream stream;
CompanyTab({Key key, this.stream}) : super(key: key);
#override
_CompanyTabState createState() => _CompanyTabState(stream);
}
class _CompanyTabState extends State<CompanyTab> {
Stream stream;
_CompanyTabState(this.stream);
#override
void initState() {
super.initState();
}
StreamBuilder companyList() {
return StreamBuilder<List<Company>>(
initialData: [],
stream: stream,
builder: (BuildContext context, AsyncSnapshot<List<Company>> snapshot) {
if (snapshot.hasError) {
return Text("Something went wrong");
}
if (snapshot.connectionState == ConnectionState.waiting ||
snapshot.connectionState == ConnectionState.none ||
snapshot.data == null) {
return LoadingWidget();
} else {
return ListView.builder(
padding: const EdgeInsets.all(10),
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
Company company = snapshot.data.elementAt(index);
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 1.0, horizontal: 4.0),
child: Card(
child: ListTile(
onTap: () {},
title: Text(company.name),
...
),
),
),
);
});
}
});
}
#override
Widget build(BuildContext context) {
return Center(
child: Container(
child: companyList(),
),
);
}
}
It's about widgets lifecycle. I can suggest you two options.
1. Move _streamController and _getData() method to _CompanyTabState.
By default BottomNavigationBar destroys tab when you go away from
one and init it again when you return back to it. If it's desired
behaviour you need to move _streamController and _getData()
method into _CompanyTabState. Don't forget to call
_streamController.close() inside dispose() method of
_CompanyTabState, it's important. _companyService can be
injected into _CompanyTabState. It's a matter of it's life time.
Should work like this:
...
class _CompanyTabState extends State<CompanyTab> {
final _streamController = StreamController<List<Company>>();
final CompanyService _companyService;
_CompanyTabState(this._companyService);
#override
void initState() {
super.initState();
_getData();
}
StreamBuilder companyList() {
return StreamBuilder<List<Company>>(
initialData: [],
stream: _streamController.stream,
builder: (BuildContext context, AsyncSnapshot<List<Company>> snapshot) {
if (snapshot.hasError) {
return Text("Something went wrong");
}
if (snapshot.connectionState == ConnectionState.waiting ||
snapshot.connectionState == ConnectionState.none ||
snapshot.data == null) {
return LoadingWidget();
} else {
return ListView.builder(
padding: const EdgeInsets.all(10),
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
Company company = snapshot.data.elementAt(index);
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 1.0, horizontal: 4.0),
child: Card(
child: ListTile(
onTap: () {},
title: Text(company.name),
...
),
),
),
);
});
}
});
}
#override
Widget build(BuildContext context) {
return Center(
child: Container(
child: companyList(),
),
);
}
#override
void dispose() {
super.dispose();
_streamController.close();
}
void _getData() {
_companyService
.getCompanies()
.then((value) => _streamController.sink.add(value));
}
}
2. Use IndexedStack
You can save tab's state and widget data (like scroll offset, entered text etc.) when you go away from the tab. It's iOS UITabBarController-like behaviour. Use IndexedStack to achieve this:
...
return Scaffold(
...
actions: ...,
body: IndexedStack(
children: _tabs,
index: _currentIndex,
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
backgroundColor: BACKGROUND_COLOR,
type: BottomNavigationBarType.fixed,
items: [
BottomNavigationBarItem(
...
),
BottomNavigationBarItem(
...
),
BottomNavigationBarItem(
...
),
BottomNavigationBarItem(
...
)
],
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
),
);
What option to use is up to you, you can use both if you want. But I would strongly recommend to move _streamController to _CompanyTabState as their lifecycles should be the same.

PushNamed issue: Type 'FillData' (a Statefulwidget) is not a subtype of type 'List<Object>'

I'm new in Flutter. I'm trying to push a List from NewData to FillData screen with pushNamed. But it said:
The following _TypeError was thrown while handling a gesture:
type 'FillData' is not a subtype of type 'List'
If i remove the comment in '/FillData', i receive null data instead. What should i do?
This is my code:
SettingNavigator
class SettingNavigator extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => Home(),
'/NewData': (context) => NewData(),
// '/FillData': (context) => FillData(), (in comment)
}
onGenerateRoute: (setting) {
if (setting.name == '/FillData') {
final ChartGroupData chartName = setting.arguments;
final List<ChartGroupData> groupNames = setting.arguments;
return MaterialPageRoute(builder: (context) {
return FillData(
chartName: chartName,
gName: groupNames,
);
});
}
return null;
},
);
}
}
NewData
import 'package:flutter/material.dart';
class NewData extends StatefulWidget {
List<ChartGroupData> groupNames;
NewData({Key key, #required this.groupNames}) : super(key: key);
#override
NewDataStage createState() => NewDataStage();
}
class NewDataStage extends State<NewData> {
TextEditingController _nameCtrl = new TextEditingController();
var textFields = <Widget>[];
var groupTECs = <TextEditingController>[];
#override
void initState() {
super.initState();
textFields.add(createCustomTextField());
}
Widget createCustomTextField() {
var groupCtrl = TextEditingController();
groupTECs.add(groupCtrl);
return Container(
padding: EdgeInsets.fromLTRB(0, 5, 0, 0),
child: Row(
children: <Widget>[
Expanded(flex: 3, child: Text("Group ${textFields.length}")),
Container(
constraints: BoxConstraints.tightFor(width: 120, height: 60),
child: TextField(
controller: groupCtrl,
),
),
],
),
);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Center(child: Text("New Chart")),
),
body: Container(
alignment: AlignmentDirectional.center,
constraints: BoxConstraints.expand(),
child: Column(
children: <Widget>[
Text(
"Your chart name",
style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
),
TextField(
style: TextStyle(fontSize: 20),
controller: _nameCtrl,
),
Expanded(
flex: 3,
child: Container(
child: ListView.builder(
shrinkWrap: true,
itemCount: textFields.length,
itemBuilder: (BuildContext context, int index) {
return textFields[index];
},
),
),
),
SizedBox(
height: 60,
width: 120,
child: RaisedButton(
onPressed: _onTapNext,
child: Text("NEXT"),
color: Colors.green,
),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _onTapCreate,
child: Icon(Icons.add, color: Colors.white),
shape: CircleBorder(),
),
),
);
}
void _onTapNext() {
/// Push Groups name to FillData
widget.groupNames = List<ChartGroupData>();
for (int i = 0; i < textFields.length; i++) {
var name = groupTECs[i].text;
widget.groupNames.add(ChartGroupData(name));
}
print(widget.groupNames.toString());
Navigator.pushNamed(context, '/FillData',
arguments: FillData(
gName: widget.groupNames,
chartName: ChartGroupData(_nameCtrl.text),
));
}
void _onTapCreate() {
setState(() {
textFields.add(createCustomTextField());
});
}
}
FillData
class FillData extends StatefulWidget {
final ChartGroupData chartName;
final List<ChartGroupData> gName;
FillData({Key key, #required this.chartName, #required this.gName})
: super(key: key);
#override
FillDataStage createState() => FillDataStage();
}
class FillDataStage extends State<FillData> {
void _showDialog() {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text("Received Data"),
content: Text(widget.chartName.toString()),
);
},
);
}
void _onTapPrintReceivedData() {
print(widget.gName);
print(widget.chartName);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Center(
child: Text("Fill your Data"),
),
),
body: Center(
child: RaisedButton(
onPressed: () {
_onTapPrintReceivedData();
_showDialog();
},
child: Text("Print Data"),
),
),
));
}
}
Class ChartGroupData
lass ChartGroupData {
final String groupNames;
ChartGroupData(this.groupNames);
#override
String toString() {
return 'Group: $groupNames';
}
}
You have 2 problems with your code:
1- you cant user routes with onGenerateRoute, because now the app doesn't know where to go, to the widget that you didn't pass anything to (inside routes) or to the widget inside the onGenerateRoute.
2- arguments is a general object that you can put whatever you want inside of it, and doing this:
final ChartGroupData chartName = setting.arguments; final
List groupNames = setting.arguments;
passes the same value to two different objects, I solved this by doing the following (it's not the best but will give you a rough idea of what you should do)
created a new object that contains the data to be passed:
class ObjectToPass {
final ChartGroupData chartName;
final List<ChartGroupData> groupNames;
ObjectToPass({this.chartName, this.groupNames});
}
changed FillData implementation:
class FillData extends StatefulWidget {
final ObjectToPass objectToPass;
FillData({Key key, #required this.objectToPass}) : super(key: key);
#override
FillDataStage createState() => FillDataStage();
}
...
void _showDialog() {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text("Received Data"),
content: Text(widget.objectToPass.chartName.toString()),
);
},
);
}
void _onTapPrintReceivedData() {
print(widget.objectToPass.groupNames);
print(widget.objectToPass.chartName);
}
to navigate to FillData you would:
Navigator.pushNamed(
context,
'/FillData',
arguments: ObjectToPass(
chartName: ChartGroupData(_nameCtrl.text),
groupNames: groupNames,
),
);
finally this is how your MaterialApp should look like:
return MaterialApp(
initialRoute: '/NewData',
onGenerateRoute: (setting) {
if (setting.name == '/FillData') {
return MaterialPageRoute(builder: (context) {
return FillData(
objectToPass: setting.arguments,
);
});
} else if (setting.name == '/NewData') {
return MaterialPageRoute(builder: (_) => NewData());
}
return null;
},
);
you can pass a list instead of the object I created and get your objects from it by it's index.

How to populate a form from a listview on tap in flutter

I have a form widget, a list widget, and a "wrapper" widget or in other words, a parent/container widget. So to give an idea of the widget tree, it is as such.
Parent/Container Widget
Form Widget
Button Widget
List Widget
Notice that the form, buttons and list widget are all siblings, inside of the parent/container widget. What I want to happen, is tap on a list item in the list widget, and populate the form widget with the data that gets passed from the list widget.
Here is my parent widget.
import 'package:andplus_flutter_7_gui/model/user.dart';
import 'package:andplus_flutter_7_gui/services/user_service.dart';
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'crud_form.dart';
import 'crud_list.dart';
class Crud extends StatefulWidget {
Crud({Key key, this.title}) : super(key: key);
final String title;
_CrudContainerState createState() => _CrudContainerState();
}
class _CrudContainerState extends State<Crud> {
List<User> users;
User user = User();
UserService userService;
#override
void initState() {
super.initState();
if (userService == null) {
userService = UserService(user);
}
}
#override
void dispose() {
// TODO: implement dispose
super.dispose();
userService.dispose();
}
#override
Widget build(BuildContext context) {
return Material(
child: Scaffold(
resizeToAvoidBottomPadding: false,
appBar: AppBar(
title: Text(widget.title),
),
body: Builder(
builder: (context) => Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
flex: 2,
child: StreamBuilder(
builder: (context, AsyncSnapshot<User> snapshot) {
return CrudForm(
user: snapshot.data,
onUserAdded: (user) {
userService.addUser(user);
},
);
},
stream: userService.userObservable,
),
),
Expanded(
child: Text("Future button widget"),
),
Expanded(
flex: 3,
child: StreamBuilder(
builder: (ctx, AsyncSnapshot<List<User>> snap) {
return CrudList(
onUserSelected: userService.userSelected,
users: snap.data,
);
},
stream: userService.usersObservable,
),
),
],
),
),
),
);
}
void onEditUser(User user) {
setState(() {
user = user;
});
}
}
The above widget wraps the three widgets I mentioned.
Here are the children widget:
Form:
import 'package:andplus_flutter_7_gui/model/user.dart';
import 'package:flutter/material.dart';
class CrudForm extends StatefulWidget {
CrudForm({Key key, this.onUserAdded, this.user}) : super(key: key);
final User user;
final void Function(User user) onUserAdded;
_CrudFormState createState() => _CrudFormState(user: user);
}
class _CrudFormState extends State<CrudForm> {
_CrudFormState({this.user});
User user = User();
var _key = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
return Container(
child: Builder(
builder: (context) => Container(
color: Colors.blueAccent[100],
child: Form(
key: _key,
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Row(
children: <Widget>[
Text(
"First Name",
style: TextStyle(fontSize: 20),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
initialValue: widget.user?.firstName == null ||
widget.user.firstName.isEmpty
? user.firstName
: widget.user.firstName,
validator: (value) {
if (value.isEmpty) {
return "First name is required";
}
return null;
},
onSaved: (value) {
setState(() {
user.firstName = value;
});
},
),
),
)
],
),
Row(
children: <Widget>[
Text(
"Last Name",
style: TextStyle(fontSize: 20),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
validator: (value) {
if (value.isEmpty) {
return "Last name is required";
}
return null;
},
onSaved: (value) {
setState(() {
user.lastName = value;
});
},
),
),
),
],
),
RaisedButton(
child: Text(
"Save",
),
splashColor: Colors.blueGrey,
onPressed: () {
if (!_key.currentState.validate()) {
return;
}
_key.currentState.save();
widget.onUserAdded(
new User(
firstName: user.firstName,
lastName: user.lastName,
),
);
},
)
],
),
),
),
),
),
);
}
}
Here is my list widget.
import 'package:andplus_flutter_7_gui/model/user.dart';
import 'package:flutter/material.dart';
class CrudList extends StatefulWidget {
CrudList({Key key, this.users, this.onUserSelected}) : super(key: key);
final List<User> users;
final SelectUser onUserSelected;
_CrudListState createState() => _CrudListState();
}
class _CrudListState extends State<CrudList> {
#override
Widget build(BuildContext context) {
return Container(
color: Colors.green,
child: ListView.builder(
itemCount: widget.users?.length ?? 0,
itemBuilder: (BuildContext context, int index) {
var user = widget.users[index];
return ListTile(
key: Key(index.toString()),
title: Center(
child: Text(
"${user.firstName} ${user.lastName}",
style: TextStyle(color: Colors.white),
),
),
onTap: () {
print("${widget.users[index]} $index");
widget.onUserSelected(widget.users[index]);
},
);
},
),
);
}
}
typedef void SelectUser(User user);
And just for further context, here is my user service, responsible for adding the objects to the stream, and using the stream builder within rxdart to notify of state changes.
import 'package:andplus_flutter_7_gui/model/user.dart';
import 'package:rxdart/rxdart.dart';
class UserService {
User _editedUser = User();
List<User> _users = <User>[];
BehaviorSubject<User> _userSubject;
BehaviorSubject<List<User>> _usersSubject;
UserService(this._editedUser) {
_userSubject = BehaviorSubject<User>.seeded(_editedUser);
_usersSubject = BehaviorSubject<List<User>>.seeded(_users);
}
Observable<List<User>> get usersObservable => _usersSubject.stream;
Observable<User> get userObservable => _userSubject.stream;
addUser(User user) {
_users.add(user);
_usersSubject.add(_users);
}
dispose() {
_userSubject.close();
_usersSubject.close();
}
void userSelected(User user) {
_editedUser = user;
_userSubject.add(_editedUser);
}
}
What am I missing? It looks like my widget rebuilds, and tries to set the initial value in the form when I tap the user in the list widget. But the actual field doesn't get updated and I'm not sure why.
I'd appreciate any documentation or articles on how to better approach data and state management between sibling widgets within the flutter framework.
Here's a similar use case that I tried to implement locally. What I'm doing here is I generate TextFormFields dynamically and assign TextEditingController.
Column textField(int n) {
List<Widget> listForm = [];
while (n > 0) {
var textEditingController = TextEditingController();
listForm.add(
TextFormField(
controller: textEditingController,
onTap: () {
_selectedField = textEditingController;
},
),
);
n--;
}
return Column(children: listForm);
}
Clicking a ListView item updates the text of the currently selected TextFormField.
InkWell(
onTap: () {
debugPrint('Selected $index!');
if (_selectedField != null) {
_selectedField!.value = TextEditingValue(text: 'Item $index');
}
},
child: ListTile(
title: Text('Item $index'),
),
);
Complete sample
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
TextEditingController? _selectedField;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Container(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(flex: 1, child: textField(3)),
Expanded(flex: 1, child: listItems()),
],
),
),
),
);
}
Column textField(int n) {
List<Widget> listForm = [];
while (n > 0) {
var textEditingController = TextEditingController();
listForm.add(
TextFormField(
controller: textEditingController,
onTap: () {
debugPrint('Current Controller: $textEditingController');
_selectedField = textEditingController;
},
),
);
n--;
}
return Column(children: listForm);
}
ListView listItems() {
return ListView.builder(
itemCount: 5,
itemBuilder: (BuildContext context, int index) {
return InkWell(
onTap: () {
if (_selectedField != null) {
_selectedField!.value = TextEditingValue(text: 'Item $index');
}
},
child: ListTile(
title: Text('Item $index'),
),
);
},
);
}
}
Demo