I have a BottomNavigationBar and realized that when I was changing the index I was displaying pretty much the exact same page/widget except for a 2 parameters. So I decided to consolidate the widgets into one that take in the parameters, but the issue is that now that I did that it doesn't work. I assume it has something to do with the page already being initialized and having a state? Here is what my widget with the BottomNavigationBar looks like:
class SpeedPage extends StatefulWidget {
const SpeedPage({Key? key}) : super(key: key);
#override
State<StatefulWidget> createState() {
return _SpeedPageState();
}
}
class _SpeedPageState extends State<SpeedPage> {
int _currentIndex = 0;
static const List<Widget> _widgetOptions = <Widget>[
WorkoutListPage(categoryIndex: 0, subCategories: Utils.srsDropdown),
WorkoutListPage(categoryIndex: 1, subCategories: Utils.ddsDropdown),
];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [_widgetOptions.elementAt(_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: _onItemTapped,
),
);
}
void _onItemTapped(int index) {
setState(() {
_currentIndex = index;
});
}
}
and here is what my WorkoutListPage widget looks like:
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");
}
}),
);
}
and this doesn't work – it lags between tabs or the page simply doesn't change. Ironically, if in my SpeedPage I change my _widgetOptions to be the following:
static const List<Widget> _widgetOptions = <Widget>[
SpeedSRPage(),
SpeedDDPage()
];
where my SpeedSRPage() and SpeedDDPage() are:
class SpeedSRPage extends StatefulWidget {
const SpeedSRPage({Key? key}) : super(key: key);
#override
State<StatefulWidget> createState() {
return _SpeedSRPageState();
}
}
class _SpeedSRPageState extends State<SpeedSRPage> {
var isLoading = true;
#override
Widget build(BuildContext context) =>
FutureBuilder<List<Map<String, dynamic>>>(
future: MyCard.getData(0, Utils.srsDropdown)!
.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");
}
}),
);
}
and
class SpeedDDPage extends StatefulWidget {
const SpeedDDPage({Key? key}) : super(key: key);
#override
State<StatefulWidget> createState() {
return _SpeedDDPageState();
}
}
class _SpeedDDPageState extends State<SpeedDDPage> {
var isLoading = true;
#override
Widget build(BuildContext context) =>
FutureBuilder<List<Map<String, dynamic>>>(
future: MyCard.getData(1, Utils.ddsDropdown)!
.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");
}
}),
);
}
respectively, it works. I also know that it isn't a problem with the WorkoutListPage because if I change my _widgetOptions to something like:
static const List<Widget> _widgetOptions = <Widget>[
WorkoutListPage(categoryIndex: 0, subCategories: Utils.srsDropdown),
SpeedDDPage(),
];
it also works. So the issue seems to be that I am using the same widget (WorkoutListPage) twice in one BottomNavigationBar. I suspect it has something to do with the state initiation; if I add this line to the workoutListPageState():
void initState() {
print("Initiated state");
super.initState();
}
"Initiated state" only prints once if I switch tabs and the items in my _widgetOptions are both a WorkoutListPage. But if I switch my _widgetOptions to have different widgets "Initiated state" prints out every time I change tabs.
Related
I have a FutureBuilder and inside is a PageView.builder. The future is calling an api and a list of flags is being fetched from the server. The flags are based on '0' & '1' like this ['1','1','0','0']. What I want to do is to skip the pages from start that have flag '1'. In this case, page first should be the first '0' at index 2. However, all pages should be built in case user want to see the previous pages as well.
Here is my code:
import 'package:flutter/material.dart';
class ScreenForBuilder extends StatefulWidget {
const ScreenForBuilder({Key? key}) : super(key: key);
#override
State<ScreenForBuilder> createState() => _ScreenForBuilderState();
}
class _ScreenForBuilderState extends State<ScreenForBuilder> {
List isCompleted = [];
apiCallFunction() async {
isCompleted = ['1','1','0','0'];
return isCompleted;
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: FutureBuilder(
future: apiCallFunction(),
builder: (context, snapshot){
return PageView.builder(
itemCount: 4,
itemBuilder: (context, index){
return Center(
child: Text('${index+1}', style: const TextStyle(fontSize: 50),),
);
}
);
}
),
),
);
}
}
you can set the initialPage in the PageController
class ScreenForBuilder extends StatefulWidget {
const ScreenForBuilder({Key? key}) : super(key: key);
#override
State<ScreenForBuilder> createState() => _ScreenForBuilderState();
}
class _ScreenForBuilderState extends State<ScreenForBuilder> {
List isCompleted = [];
Future<List<String>> apiCallFunction() async {
List<String> isCompleted = ['1','1','0','0'];
return isCompleted;
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: FutureBuilder<List<String>>(
future: apiCallFunction(),
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasError || !snapshot.hasData) {
return const Center(
child: Text('something went wrong'),
);
}
final initialPage = snapshot.data!.indexWhere(
(element) => element == '0',
);
return PageView.builder(
controller: PageController(
initialPage: initialPage == -1 ? 0 : initialPage,
),
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
return Center(
child: Text(
'${index + 1}',
style: const TextStyle(fontSize: 50),
),
);
},
);
},
),
),
);
}
}
The code #Hydra provided worked great for me but here is an alternative method. I set the initial page with indexOf a flag. Here's my code that also worked.
import 'package:flutter/material.dart';
class ScreenForBuilder extends StatefulWidget {
const ScreenForBuilder({Key? key}) : super(key: key);
#override
State<ScreenForBuilder> createState() => _ScreenForBuilderState();
}
class _ScreenForBuilderState extends State<ScreenForBuilder> {
List isCompleted = [];
apiCallFunction() async {
isCompleted = ['1','1','0','0'];
return isCompleted;
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: FutureBuilder(
future: apiCallFunction(),
builder: (context, snapshot){
if(snapshot.hasData) {
return PageView.builder(
controller: PageController(initialPage: isCompleted.indexOf('0')),
itemCount: isCompleted.length,
itemBuilder: (context, index){
return Center(
child: Text('${index}', style: const TextStyle(fontSize: 50),),
);
}
);
} else
return CircularProgressIndicator();
}
),
),
);
}
}
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),
],
);
}
}
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.
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.
I fetched some interest(data) through an API and show them using future builders as containers. I want to change the background color of the container when I clicked on it. Here is what I did and it's changing the background color of all the containers when I clicked on one.
I added an if condition to the color of the container to check whether it is clicked or not
color: isClicked? Colors.white : Color(0xFFFFEBE7),
and set the isClicked state to true when clicked.
bool isClicked = false;
FutureBuilder(
future: GetInterests.getInterests(),
builder: (context, snapshot) {
final datalist = snapshot.data;
if (snapshot.connectionState ==
ConnectionState.done) {
return Expanded(
child: SizedBox(
height: 35,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return Wrap(
direction: Axis.vertical,
children: <Widget>[
GestureDetector(
onTap: (){
final inte_id = "${datalist[index]['_id']}";
log(inte_id);
setState(() {
isClicked = true;
});
},
child: new Container(
margin: EdgeInsets.only(right: 7),
height: 30,
width: MediaQuery.of(context)
.size
.width /
5.2,
decoration: BoxDecoration(
color: isClicked? Colors.white : Color(0xFFFFEBE7),
border: Border.all(
color: Color(0xFFE0E0E0)),
borderRadius:
BorderRadius.only(
topLeft:
Radius.circular(
50.0),
topRight:
Radius.circular(
50.0),
bottomRight:
Radius.circular(
50.0),
bottomLeft:
Radius.circular(
0.0))),
child: Center(
child: Text(
"${datalist[index]['iname']}",
style: TextStyle(
fontFamily: 'Montserrat',
color: Color(0xFFFF5E3A),
fontSize: 13),
),
),
),
),
],
);
},
itemCount: datalist.length,
),
),
);
}
return Padding(
padding: const EdgeInsets.only(left: 140.0),
child: Center(
child: CircularProgressIndicator(),
),
);
},
)
I was able to print the interest id in the console which belongs to the container I clicked on. but don't know how to change its color only
Instead of this you can use a variable to store selectedIndex and check if the currentIndex is selected or not and compare if currentIndex is selected or not and style the selected widget.
import 'package:flutter/material.dart';
final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatefulWidget {
_MyWidgetState createState()=>_MyWidgetState();
}
class _MyWidgetState extends State<MyWidget>{
List _selectedIndexs=[];
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: 4,
itemBuilder: (ctx,i){
final _isSelected=_selectedIndexs.contains(i);
return GestureDetector(
onTap:(){
setState((){
if(_isSelected){
_selectedIndexs.remove(i);
}else{
_selectedIndexs.add(i);
}
});
},
child:Container(
color:_isSelected?Colors.red:null,
child:ListTile(title:Text("Khadga")),
),
);
}
);
}
}
modify your listview builder as i have done in above case.
While the accepted answer will work, a much more sophisticated architecture using ChangeNotifier and package provider will produce more loosely coupled, better code, in some folks opinion.
Combining ideas from the following
https://flutter.dev/docs/cookbook/networking/fetch-data
https://flutter.dev/docs/development/data-and-backend/state-mgmt/simple
I was focused on architecture and data flow. Not on widget layout to match the original question's screenshot.
import 'dart:collection';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:provider/provider.dart';
// Model ---------------------------------------------------
class Interest with ChangeNotifier {
final String title;
bool _selected = false;
Interest({
#required this.title,
}) : assert(title != null);
factory Interest.fromMap(final Map<String, dynamic> map) {
return Interest(
title: map['title'],
);
}
bool get selected {
return this._selected;
}
void select() {
this._selected = true;
this.notifyListeners();
}
void toggleSelect() {
this._selected = !this._selected;
this.notifyListeners();
}
}
class Interests extends ChangeNotifier {
final List<Interest> _interests = <Interest>[];
Interests();
factory Interests.fromList(final List<Map<String, dynamic>> list) {
final Interests interests = Interests();
for (final Map<String, dynamic> map in list) {
interests.add(Interest.fromMap(map));
}
return interests;
}
int get length {
return this._interests.length;
}
Interest operator [](final int index) {
return this._interests[index];
}
UnmodifiableListView<Interest> get interests {
return UnmodifiableListView<Interest>(this._interests);
}
void add(final Interest interest) {
this._interests.add(interest);
this.notifyListeners();
}
void selectAll() {
for (final Interest interest in this._interests) {
interest.select();
}
}
}
// Services ------------------------------------------------
Future<Interests> fetchInterests() async {
// Some data source that has a list of objects with titles.
final response = await http.get('https://jsonplaceholder.typicode.com/posts');
if (response.statusCode == 200) {
return Interests.fromList(json.decode(response.body).cast<Map<String, dynamic>>());
} else {
throw Exception('Failed to load post');
}
}
// User Interface ------------------------------------------
void main() {
runApp(InterestsApp());
}
class InterestsApp extends StatelessWidget {
#override
Widget build(final BuildContext context) {
return MaterialApp(
title: 'Interests App',
theme: ThemeData(primarySwatch: Colors.blue),
home: InterestsPage(),
);
}
}
class InterestsPage extends StatelessWidget {
#override
Widget build(final BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Interests')),
body: InterestsBody(),
);
}
}
class InterestsBody extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _InterestsBodyState();
}
}
class _InterestsBodyState extends State<InterestsBody> {
Future<Interests> _futureInterests;
#override
void initState() {
super.initState();
this._futureInterests = fetchInterests();
}
#override
Widget build(final BuildContext context) {
return FutureBuilder<Interests>(
future: this._futureInterests,
builder: (final BuildContext context, final AsyncSnapshot<Interests> snapshot) {
if (snapshot.hasData) {
return ChangeNotifierProvider.value(
value: snapshot.data,
child: InterestsList(),
);
} else if (snapshot.hasError) {
return Center(child: Text("${snapshot.error}"));
}
return Center(child: CircularProgressIndicator());
},
);
}
}
class InterestsList extends StatelessWidget {
#override
Widget build(final BuildContext context) {
return Consumer<Interests>(
builder: (final BuildContext context, final Interests interests, final Widget child) {
return Column(
children: <Widget>[
Center(
child: RaisedButton(
child: Text("Select All"),
onPressed: () {
interests.selectAll();
},
),
),
Expanded(
child: ListView.builder(
itemCount: interests.length,
itemBuilder: (final BuildContext context, final int index) {
return ChangeNotifierProvider<Interest>.value(
value: interests[index],
child: InterestTile(),
);
},
),
),
],
);
},
);
}
}
class InterestTile extends StatelessWidget {
#override
Widget build(final BuildContext context) {
return Consumer<Interest>(
builder: (final BuildContext context, final Interest interest, final Widget child) {
return ListTile(
title: Text(interest.title),
trailing: interest.selected ? Icon(Icons.check) : null,
onTap: () {
interest.toggleSelect();
},
);
},
);
}
}
you also can use flutter chip or ActionChip or ChoiceChip
class MyThreeOptions extends StatefulWidget {
#override
_MyThreeOptionsState createState() => _MyThreeOptionsState();
}
class _MyThreeOptionsState extends State<MyThreeOptions> {
int? _value = 1;
#override
Widget build(BuildContext context) {
return Wrap(
children: List<Widget>.generate(
3,
(int index) {
return ChoiceChip(
label: Text('Item $index'),
selected: _value == index,
onSelected: (bool selected) {
setState(() {
_value = selected ? index : null;
});
},
);
},
).toList(),
);
}
}