Using NavigationRail with multiple Futures - flutter

I am trying to make a screen with NavigationRail where each destination displays table with different data fetched from backend.
I tried implementing it with NavigationRail and three futures, but each time I transit from one destination to another, I see a red screen with error on a second and then transit completes. My logs show following error:
The following TypeErrorImpl was thrown building FutureBuilder<List>(state: _FutureBuilderState<List>#675cb):
Expected a value of type 'D1', but got one of type 'D2'
The relevant error-causing widget was
FutureBuilder<List>
Could you please see if I am missing something? I tried the same approach with three futures with carousel_slider widget and did not run into a same problem. I guess I am misusing NavigationRail somehow.
Here is base code:
class DataScreen extends StatefulWidget {
const DataScreen({super.key});
#override
State<DataScreen> createState() => _DataScreenState();
}
class _DataScreenState extends State<DataScreen> {
late Future<List<D1>> data1;
late Future<List<D2>> data2;
late Future<List<D3>> data3;
int _selectedIndex = 0;
#override
void initState() {
super.initState();
data1 = D1Service().getD1();
data2 = D2Service().getD2();
data3 = D3Service().getD3();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: <Widget>[
NavigationRail(
selectedIndex: _selectedIndex,
groupAlignment: 0.0,
onDestinationSelected: (int index) {
setState(() {
_selectedIndex = index;
});
},
labelType: NavigationRailLabelType.all,
leading: const SizedBox(),
trailing: const SizedBox(),
destinations: const <NavigationRailDestination>[
NavigationRailDestination(
icon: Icon(Icons.perm_identity),
selectedIcon: Icon(Icons.person),
label: Text('D1'),
),
NavigationRailDestination(
icon: Icon(Icons.groups_outlined),
selectedIcon: Icon(Icons.groups),
label: Text('D2'),
),
NavigationRailDestination(
icon: Icon(Icons.inventory_2_outlined),
selectedIcon: Icon(Icons.inventory_2),
label: Text('D3'),
),
],
),
const VerticalDivider(thickness: 1, width: 1),
const Spacer(),
// This is the main content.
Center(
child: Container(
child: _toTableAll(),
),
),
const Spacer(),
],
),
);
}
Widget _toTableAll() {
if (_selectedIndex == 0) {
return _toTable(data1);
} else if (_selectedIndex == 1) {
return _toTable(data2);
}
return _toTable(data3);
}
Widget _toTable<T>(Future<List<T>> item) {
return FutureBuilder<List<T>>(
future: item,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Container(
constraints: const BoxConstraints(maxWidth: 800),
child: MTable(snapshot.data!),
);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
},
);
}
}

Changing snapshot.hasData with snapshot.connectionState == ConnectionState.done solved the problem

Related

Flutter: How to change the view of my NavigationRail without clicking on the icons?

I use a classic NavigationRail, inside a Row() divided in two, to change the view. But I would also like to change the view by clicking on items displayed in the view.
For example, at the beginning ShowTables() is visible, and when I click on buttons built by ShowTables(), I want to replace ShowTables() by an other widget. This new widget could have the possibility to return to ShowTables(), keeping the value of my Map Tables (or at least my app state data if not possible). How can I do ?
class NavBarTest extends StatefulWidget {
final Metadata? data;
const NavBarTest({Key? key, required this.data}) : super(key: key);
#override
State<NavBarTest> createState() => _NavBarTestState();
}
class _NavBarTestState extends State<NavBarTest> {
Map tables = {};
int _selectedIndex = 0;
#override
void initState() {
/*
here I init tables...
*/
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child:
Row(
children: <Widget>[
NavigationRail(
groupAlignment: -1.0,
selectedIndex: _selectedIndex,
leading:
Column(children: [
SizedBox(
height: MediaQuery.of(context).size.height/3,
),
SizedBox(
width: 50,
height: 50,
child: Image.asset("assets/logo.png"),
),
],),
onDestinationSelected: (int index) {
setState(() {
_selectedIndex = index;
});
},
labelType: NavigationRailLabelType.none,
destinations: const <NavigationRailDestination>[
NavigationRailDestination(
icon: Icon(Icons.spoke_rounded,color: icon),
selectedIcon: Icon(Icons.spoke_rounded,color: Colors.yellow),
label: Text('First'),
),
NavigationRailDestination(
icon: Icon(Icons.show_chart_sharp,color: icon),
selectedIcon: Icon(Icons.show_chart_sharp,color: Colors.yellow),
label: Text('Second'),
),
NavigationRailDestination(
icon: Icon(Icons.info_sharp,color: icon),
selectedIcon: Icon(Icons.info_outline,color: Colors.yellow),
label: Text('Third'),
),
],
),
// This is the main content.
Expanded(
child: buildPage(_selectedIndex)
)
],
)
)
);
}
Widget buildPage(index){
switch (index) {
case 0:
return ShowTables(tables: tables);
case 1:
return const Details();
case 2:
return Links(tables: tables);
default:
return ShowTables(tables: tables);
}
}
}

Unable to show data on Data Table at startsup the page while using Search filter but showing data after search something?

I trying to implement rows Search on Data Table on my flutter web app using TextEditingController. The data comes from The API as Json formte using Employee model.
I used FutureBuilder to get the data from the API. And i inserted Snapshot data into 'empList' as List. Also Created empsFiltered List to show search filtered data.
The issue is: Unable to show actual data in the datatable on startup. But the data is shown while searching and after clearing the searchtextfield.
I want to show the actual data on startup. And also the data should be shown as searched.
How to do this.
class EditorHome extends StatefulWidget {
const EditorHome({Key? key}) : super(key: key);
#override
_EditorHomeState createState() => _EditorHomeState();
}
class _EditorHomeState extends State<EditorHome> {
TextEditingController searchController = TextEditingController();
String _searchResult = '';
List empList = [];
List empsFiltered = [];
#override
void initState() {
super.initState();
empsFiltered = empList;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Fetch Data Table Example with search'),
),
body: SingleChildScrollView(
child: Column(
children: [
Card(
child: ListTile(
leading: const Icon(Icons.search),
title: TextField(
controller: searchController,
decoration: const InputDecoration(
hintText: 'Search', border: InputBorder.none),
onChanged: (value) {
setState(() {
_searchResult = value;
empsFiltered = empList
.where((e) =>
e.name.contains(_searchResult.toLowerCase()) ||
e.email.contains(_searchResult.toLowerCase()))
.toList();
});
}),
trailing: IconButton(
icon: const Icon(Icons.cancel),
onPressed: () {
setState(() {
searchController.clear();
_searchResult = '';
empsFiltered = empList;
});
},
),
),
),
FutureBuilder<List<Employees>>(
//initialData: const <Employees>[],
future: fetchResults(),
builder: (context, snapshot) {
if (snapshot.hasError ||
snapshot.data == null ||
snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
empList = snapshot.data!;
return DataTable(
headingTextStyle: const TextStyle(
fontWeight: FontWeight.bold, color: Colors.black),
headingRowHeight: 50,
showBottomBorder: true,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey, width: 1)),
columns: const [
DataColumn(label: SizedBox(width: 30, child: Text('ID'))),
DataColumn(
label: SizedBox(width: 100, child: Text('Name'))),
DataColumn(
label: SizedBox(width: 100, child: Text('Email'))),
],
rows: List.generate(
empsFiltered.length,
(index) {
var emp = empsFiltered[index];
return DataRow(cells: [
DataCell(
Text(emp.id.toString()),
),
DataCell(
Text(emp.name),
),
DataCell(
Text(emp.email),
),
]);
},
).toList(),
);
},
),
],
),
),
);
}
}
Below is my API:
Future<List<Employees>> fetchResults() async {
//List<Employees> _results = [];
Uri url = Uri.parse(" http:link ");
var response = await http.get(url);
var resultsJson = json.decode(response.body).cast<Map<String, dynamic>>();
List<Employees> emplist = await resultsJson
.map<Employees>((json) => Employees.fromJson(json))
.toList();
return emplist;
}
Posting here to add the code snippet:
You should create a state variable to contain the result of fetchResults, call fetchResults/set that variable in your initState. Use that new variable as future of the futureBuilder instead of the function call directly
class _EditorHomeState extends State<EditorHome> {
Future<List> futureList;
#override
void initState() {
super.initState();
futureList = fetchResults();
}
#override
Widget build(BuildContext context) {
return Scaffold(
...
FutureBuilder<List<Employees>>(
future: futureList,
builder: (context, snapshot) {
...
);
}
}
I got a solution. In this Case I used Two StatefulWidgets. One is for calling Future with fetchResults() and converted into a List, And second is for Table with Search filter, And called that first stateFulWidget List into a variable on second StatefulWidgets and set that List variable in initState as into empFiltered List. Working Fine.
Example Code:
class EditorHome extends StatefulWidget {
const EditorHome({Key? key}) : super(key: key);
#override
_EditorHomeState createState() => _EditorHomeState();
}
class _EditorHomeState extends State<EditorHome> {
List empList = [];
#override
Widget build(BuildContext context) {
return FutureBuilder<List<Employees>>(
future: fetchResults(),
builder: (context, snapshot) {
if (snapshot.hasError ||
snapshot.data == null ||
snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
empList = snapshot.data!;
return TableSec(fempList: empList);
});
}
}
class TableSec extends StatefulWidget {
final List fempList;
const TableSec({Key? key, required this.fempList}) : super(key: key);
#override
_TableSecState createState() => _TableSecState();
}
class _TableSecState extends State<TableSec> {
late List empList = widget.fempList;
List empsFiltered = [];
TextEditingController searchController = TextEditingController();
String _searchResult = '';
#override
void initState() {
super.initState();
empsFiltered = empList;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Table"),
),
body: SingleChildScrollView(
child: Column(
children: [
Card(
child: ListTile(
leading: const Icon(Icons.search),
title: TextField(
controller: searchController,
decoration: const InputDecoration(
hintText: 'Search', border: InputBorder.none),
onChanged: (value) {
setState(() {
_searchResult = value;
empsFiltered = empList
.where((e) =>
e.name.contains(_searchResult.toLowerCase()) ||
e.email.contains(_searchResult.toLowerCase()))
.toList();
//print(_searchResult);
});
}),
trailing: IconButton(
icon: const Icon(Icons.cancel),
onPressed: () {
setState(() {
searchController.clear();
_searchResult = '';
empsFiltered = empList;
});
},
),
),
),
DataTable(
headingTextStyle: const TextStyle(
fontWeight: FontWeight.bold, color: Colors.black),
headingRowHeight: 50,
showBottomBorder: true,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey, width: 1)),
columns: const [
DataColumn(label: SizedBox(width: 30, child: Text('ID'))),
DataColumn(label: SizedBox(width: 100, child: Text('Name'))),
DataColumn(label: SizedBox(width: 100, child: Text('Email'))),
],
rows: List.generate(
empsFiltered.length,
(index) {
var emp = empsFiltered[index];
return DataRow(cells: [
DataCell(
Text(emp.id.toString()),
),
DataCell(
Text(emp.name),
),
DataCell(
Text(emp.email),
),
]);
},
).toList(),
)
],
),
),
);
}
}

Scaffold in flutter

I am new to flutter. I have a question about scaffold in my project.
I have a home screen that I use to display the BottomNavigation widget. I guess that I also use if as a container to display all of the other pages/screens in so that the BottomNavigation will stay visible. Here is the code below:
class Home_Screen extends StatefulWidget {
static const String id = 'home_screen';
#override
_Home_ScreenState createState() => _Home_ScreenState();
}
// ignore: camel_case_types
class _Home_ScreenState extends State<Home_Screen> {
PageController _pageController = PageController();
List<Widget> _screens = [
AgentDashboardScreen(),
TransactionDetailScreen(),
AgentProfileScreen(),
];
int _selectedIndex = 0;
void _onPageChanged(int index) {
setState(() {
_selectedIndex = index;
});
}
void _itemTapped(int selectedIndex) {
if (selectedIndex == 3) {
Navigator.of(context).pushAndRemoveUntil(
// the new route
MaterialPageRoute(
builder: (BuildContext context) => WelcomeScreen(),
),
(Route route) => false,
);
} else {
_pageController.jumpToPage(selectedIndex);
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: PageView(
controller: _pageController,
children: _screens,
onPageChanged: _onPageChanged,
physics: NeverScrollableScrollPhysics(),
),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
onTap: _itemTapped,
items: [
BottomNavigationBarItem(
icon: Icon(
Icons.home,
color: _selectedIndex == 0 ? Colors.blueAccent : Colors.grey,
),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(
Icons.account_balance,
color: _selectedIndex == 1 ? Colors.blueAccent : Colors.grey,
),
label: 'Add Tran',
),
BottomNavigationBarItem(
icon: Icon(
Icons.person,
color: _selectedIndex == 2 ? Colors.blueAccent : Colors.grey,
),
label: 'Profile',
),
BottomNavigationBarItem(
icon: Icon(
Icons.album_outlined,
color: _selectedIndex == 3 ? Colors.blueAccent : Colors.grey,
),
label: 'Logout',
),
],
),
);
}
}
In one of the screens that I can navigate to from the BottomNavigator I am having issues with a large white space above the keyboard. I have read that having a scaffold inside another scaffold can cause this.
So, when I navigate to the next page do I have a scaffold inside another scaffold? Here is a snippet from the second page.
class TransactionDetailScreen extends StatefulWidget {
static const String id = 'transaction_detail_screen';
final QueryDocumentSnapshot trxns;
//final Trxns trxns;
//final QuerySnapshot queryTrxns = trxns;
TransactionDetailScreen([this.trxns]);
#override
_TransactionDetailScreenState createState() =>
_TransactionDetailScreenState();
}
class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
String _trxnStatus = 'Listed';
#override
Widget build(BuildContext context) {
// Get the stream of transactions created in main.dart
final trxnProvider = Provider.of<TrxnProvider>(context);
return Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar(
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('assets/images/Appbar_logo.png',
fit: BoxFit.cover, height: 56),
],
),
),
backgroundColor: Colors.white,
body: SingleChildScrollView(
reverse: true,
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: <Widget>[
Text(
'Transaction Details',
style: TextStyle(
fontSize: 30,
),
),
SizedBox(
height: 8.0,
),
TextField(
autofocus: true,
keyboardType: TextInputType.text,
controller: clientFNameController,
textAlign: TextAlign.center,
onChanged: (value) {
trxnProvider.changeclientFName(value);
},
decoration: kTextFieldDecoration.copyWith(
hintText: 'Client First Name',
labelText: 'Client First Name'),
),
RoundedButton(
title: 'Save',
colour: Colors.blueAccent,
onPressed: () async {
setState(() {
showSpinner = true;
});
try {
trxnProvider.saveTrxn();
Navigator.push(
context,
new MaterialPageRoute(
builder: (context) => AgentDashboardScreen(),
),
);
setState(() {
showSpinner = false;
});
} catch (e) {
// todo: add better error handling
print(e);
}
},
),
SizedBox(
height: 8.0,
),
(widget != null)
? RoundedButton(
title: 'Delete',
colour: Colors.red,
onPressed: () async {
setState(() {
showSpinner = true;
});
try {
trxnProvider.deleteTrxn(widget.trxns['trxnId)']);
Navigator.push(
context,
new MaterialPageRoute(
builder: (context) => AgentDashboardScreen(),
),
);
setState(() {
showSpinner = false;
});
} catch (e) {
// todo: add better error handling
print(e);
}
},
)
: Container(),
],
),
),
),
);
}
}
The keyboard works/looks as expected (no white space above) if the textboxes are empty. Am I doing this correctly or should I do it differently?
Thanks

Shopping cart using bloc does not update cart count

I'm trying to build a simple shopping cart using bloc. It works fine but the only issue is the cart count doesn't get updated when I add/remove the item. I need to switch to the cartscreen to see the change. I set the counter to cartBloc.cart.length.toString(). What am I missing and am I using bloc correctly?
Cart Bloc
class CartBloc{
List<ProductModel> cart = [];
double totalCartPrice = 0;
final _cartController = StreamController.broadcast();
Stream get getCartStream => _cartController.stream;
void addToCart(ProductModel product) {
cart.add(product);
totalCartPrice = totalCartPrice + double.parse(product.price);
_cartController.sink.add(cart);
}
void removeFromCart(ProductModel product) {
cart.remove(product);
totalCartPrice = totalCartPrice - double.parse(product.price);
_cartController.sink.add(cart);
}
void dispose() {
_cartController?.close();
}
}
final cartBloc = CartBloc();
Main Screen
class _MainScreenState extends State<MainScreen> {
int _currentIndex = 0;
PageController _pageController;
GlobalKey bottomNavigationKey = GlobalKey();
#override
void initState() {
super.initState();
_pageController = PageController();
}
void dispose(){
super.dispose();
_pageController.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor:Color(0xFF20232A),
appBar: PreferredSize(child: Container(),preferredSize: Size.fromHeight(0.0)),
body: SizedBox.expand(
child: PageView(
physics: NeverScrollableScrollPhysics(),
controller: _pageController,
onPageChanged: (index){
setState(() => _currentIndex = index);
},
children: [
Container(
child: ProductScreen()
),
Container(
child: CartScreen()
),
],
)
),
bottomNavigationBar: Container(
child: BottomNavyBar(
mainAxisAlignment: MainAxisAlignment.center,
containerHeight: 56.0,
backgroundColor: Style.Colors.backgroundColor,
selectedIndex: _currentIndex,
onItemSelected: (index){
setState(() => _currentIndex = index);
_pageController.jumpToPage(index);
},
items:<BottomNavyBarItem>[
BottomNavyBarItem(
textAlign: TextAlign.center,
activeColor: Color(0xFF010101),
title: Text(' PRODUCTS',style: TextStyle(
color:Style.Colors.mainColor,fontSize: 13.0
)),
icon: Padding(
padding: EdgeInsets.only(left:5.0),
child: Icon(
SimpleLineIcons.menu,
size:18.0,
color:_currentIndex == 0 ? Style.Colors.mainColor:Colors.white
),
)
),
BottomNavyBarItem(
textAlign: TextAlign.center,
activeColor: Color(0xFF010101),
title: Text(' CART',style: TextStyle(
color:Style.Colors.mainColor,fontSize: 13.0
)),
icon: Padding(
padding: EdgeInsets.only(left:5.0),
child: Badge(
badgeColor: Style.Colors.mainColor,
badgeContent: Text(cartBloc.cart.length.toString(),style: TextStyle(fontWeight: FontWeight.bold),), //not updating when select item
child: Icon(
SimpleLineIcons.basket,
size:18.0,
color:_currentIndex == 1 ? Style.Colors.mainColor:Colors.white
),
)
)
),
]
),
),
);
}
}
There are a lot of things you're not doing correctly using Bloc.
For example:
Your CartBloc needs to extend the Bloc class like so:
class CartBloc extends Bloc<SomeEvent, SomeState>
Then you need to override the mapEventToState method in that class like this.
#override
Stream<SomeState> mapEventToState(SomeEvent event) async* {
Then you need to provide your Bloc using:
BlocProvider(
create: (_) => CartBloc(),
child: YourWidget()
)
And then use BlocBuilder to build your widget using the data from the CartBloc
I suggest you take a look at the documentation and the example on how to use bloc
https://pub.dev/packages/flutter_bloc/example

How can I prevent my Drawer from rebuilding every time I open it in Flutter?

In Flutter Debug & Profile mode, I have a major performance issue. When I open my drawer, my app drops to about 20 FPS even in profile mode which is unacceptable. I've had issues like this that resolved by themselves in the past because Flutter (on startup) likes to use an old version of my app until I either hot reload or hot restart. But I've tried everything and still, the performance of my app is unacceptable. I just want to prevent my drawer from rebuilding itself every time the user opens it.
NOTE: The only time the app works well is in release mode.
CustomDrawer class:
int _selectedIndex = 0;
class CustomDrawer extends StatefulWidget {
get selectedIndex => _selectedIndex;
set selectedIndex(int newIndex) => _selectedIndex = newIndex;
final List<String> appRoutes = App.routes.keys.toList();
#override
_CustomDrawerState createState() => _CustomDrawerState();
}
class _CustomDrawerState extends State<CustomDrawer> {
final List<DrawerItem> _drawerItems = [
DrawerItem(
title: "Home",
icon: OMIcons.home,
),
DrawerItem(
title: "Assignments",
icon: OMIcons.assignment,
),
DrawerItem(
title: "Schedule",
icon: OMIcons.schedule,
),
DrawerItem(
title: "Subjects",
icon: OMIcons.subject,
),
DrawerItem(
title: "Settings",
icon: OMIcons.settings,
),
DrawerItem(
title: "Help and Feedback",
icon: OMIcons.help,
),
DrawerItem(
title: "Upgrade",
icon: OMIcons.star,
color: Colors.orange,
),
];
List<Widget> drawerOptions = [];
#override
void initState() {
super.initState();
_populateDrawerOptions();
}
void _populateDrawerOptions() {
for (var d in _drawerItems) {
var i = _drawerItems.indexOf(d);
drawerOptions.add(CustomListTile(
icon: d.icon,
topContainerColor:
_selectedIndex == i ? Color(0xffe8f0fe) : Colors.transparent,
iconColor: _selectedIndex == i ? Color(0xff1967d2) : d.color,
onTap: () => _onSelectItem(i),
text: d.title,
textColor: _selectedIndex == i ? Color(0xff1967d2) : d.color,
));
}
}
void _onSelectItem(int index) {
if (_selectedIndex == index) {
Navigator.of(context).pop();
return;
}
_selectedIndex = index;
Navigator.of(context).pushReplacementNamed(
widget.appRoutes[index],
);
}
final Widget drawerHeader = SafeArea(
top: true,
child: Container(
child: Text(
"School Life",
style: TextStyle(fontSize: 24.0, fontFamily: 'OpenSans'),
),
),
);
#override
Widget build(BuildContext context) {
final Color drawerColor = Theme.of(context).primaryColor;
return Drawer(
child: Container(
color: drawerColor,
child: Column(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
drawerHeader,
ListView(
padding: const EdgeInsets.only(top: 15, right: 10),
shrinkWrap: true,
children: drawerOptions,
),
],
),
),
);
}
}