Cupertino Tab Bar - flutter

I am trying to implement the Cupertino bottom tab bar. I have 2 issues. When I call the widgetBuilder in the Cupertino tab view I get 'The function can't be unconditionally invoked because it can be 'null'', so add a null check '!' but then it requires another one.
Then, when I try to return the CupertinoHomeScaffold with currentTab, onSelectTab, widgetBuilder as field it says parameter 'key' is required. Not sure what I should do.
Let me know if you see anything! Thanks in advance..!
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
enum TabItem { jobs, entries, account }
class TabItemData {
const TabItemData({required this.title, required this.icon});
final String title;
final IconData icon;
static const Map<TabItem, TabItemData> allTabs = {
TabItem.jobs: TabItemData(title: 'Jobs', icon: Icons.work),
TabItem.entries: TabItemData(title: 'entries', icon: Icons.view_headline),
TabItem.account: TabItemData(title: 'entries', icon: Icons.person),
};
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
TabItem _currentTab = TabItem.jobs;
Map<TabItem, WidgetBuilder> get widgetBuilder {
return {
TabItem.jobs: (_) => Container(),
TabItem.entries: (_) => Container(),
TabItem.account: (_) => Container(),
};
}
void _select(TabItem tabItem) {
setState(() => _currentTab = tabItem);
}
#override
Widget build(BuildContext context) {
return CupertinoHomeScaffold(
currentTab: _currentTab,
onSelectTab: _select,
widgetBuilder: widgetBuilder,
);
}
}
class CupertinoHomeScaffold extends StatelessWidget {
const CupertinoHomeScaffold({
required Key key,
required this.currentTab,
required this.onSelectTab,
required this.widgetBuilder,
}) : super(key: key);
final TabItem currentTab;
final ValueChanged<TabItem> onSelectTab;
final Map<TabItem, WidgetBuilder> widgetBuilder;
#override
Widget build(BuildContext context) {
return CupertinoTabScaffold(
tabBar: CupertinoTabBar(
items: [
_buildItem(TabItem.jobs),
_buildItem(TabItem.entries),
_buildItem(TabItem.account),
],
onTap: (index) => onSelectTab(TabItem.values[index]),
),
tabBuilder: (context, index) {
final item = TabItem.values[index];
return CupertinoTabView(
builder: (context) => widgetBuilder![item](context),
);
},
);
}
BottomNavigationBarItem _buildItem(TabItem tabItem) {
final itemData = TabItemData.allTabs[tabItem];
return BottomNavigationBarItem(
icon: Icon(itemData!.icon),
label: itemData.title,
);
}
}

Make key a nullable variable:
Key? key,
For widgetBuilder, you need to put ! after ]:
widgetBuilder[item]!(context),
Working Example
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() => runApp(
MaterialApp(
home: HomePage(),
),
);
enum TabItem { jobs, entries, account }
class TabItemData {
const TabItemData({required this.title, required this.icon});
final String title;
final IconData icon;
static const Map<TabItem, TabItemData> allTabs = {
TabItem.jobs: TabItemData(title: 'Jobs', icon: Icons.work),
TabItem.entries: TabItemData(title: 'entries', icon: Icons.view_headline),
TabItem.account: TabItemData(title: 'entries', icon: Icons.person),
};
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
TabItem _currentTab = TabItem.jobs;
Map<TabItem, WidgetBuilder> get widgetBuilder {
return {
TabItem.jobs: (_) => Container(),
TabItem.entries: (_) => Container(),
TabItem.account: (_) => Container(),
};
}
void _select(TabItem tabItem) {
setState(() => _currentTab = tabItem);
}
#override
Widget build(BuildContext context) {
return CupertinoHomeScaffold(
currentTab: _currentTab,
onSelectTab: _select,
widgetBuilder: widgetBuilder,
);
}
}
class CupertinoHomeScaffold extends StatelessWidget {
const CupertinoHomeScaffold({
Key? key,
required this.currentTab,
required this.onSelectTab,
required this.widgetBuilder,
}) : super(key: key);
final TabItem currentTab;
final ValueChanged<TabItem> onSelectTab;
final Map<TabItem, WidgetBuilder> widgetBuilder;
#override
Widget build(BuildContext context) {
return CupertinoTabScaffold(
tabBar: CupertinoTabBar(
items: [
_buildItem(TabItem.jobs),
_buildItem(TabItem.entries),
_buildItem(TabItem.account),
],
onTap: (index) => onSelectTab(TabItem.values[index]),
),
tabBuilder: (context, index) {
final item = TabItem.values[index];
return CupertinoTabView(
builder: (context) => widgetBuilder[item]!(context),
);
},
);
}
BottomNavigationBarItem _buildItem(TabItem tabItem) {
final itemData = TabItemData.allTabs[tabItem];
return BottomNavigationBarItem(
icon: Icon(itemData!.icon),
label: itemData.title,
);
}
}

Related

How to remove space between expanded ExpansionPanels in ExpansionPanelList?

This is an example code for ExpansionPanelList
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: Scaffold(
appBar: AppBar(title: const Text(_title)),
body: const MyStatefulWidget(),
),
);
}
}
// stores ExpansionPanel state information
class Item {
Item({
required this.expandedValue,
required this.headerValue,
this.isExpanded = false,
});
String expandedValue;
String headerValue;
bool isExpanded;
}
List<Item> generateItems(int numberOfItems) {
return List<Item>.generate(numberOfItems, (int index) {
return Item(
headerValue: 'Panel $index',
expandedValue: 'This is item number $index',
);
});
}
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key? key}) : super(key: key);
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
final List<Item> _data = generateItems(8);
#override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Container(
child: _buildPanel(),
),
);
}
Widget _buildPanel() {
return ExpansionPanelList(
expansionCallback: (int index, bool isExpanded) {
setState(() {
_data[index].isExpanded = !isExpanded;
});
},
children: _data.map<ExpansionPanel>((Item item) {
return ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return ListTile(
title: Text(item.headerValue),
);
},
body: ListTile(
title: Text(item.expandedValue),
subtitle:
const Text('To delete this panel, tap the trash can icon'),
trailing: const Icon(Icons.delete),
onTap: () {
setState(() {
_data.removeWhere((Item currentItem) => item == currentItem);
});
}),
isExpanded: item.isExpanded,
);
}).toList(),
);
}
}
And it gives the following result:
As you see there is grey space between Panel 0 and Panel 1, and between Panel 1 and Panel 2. Could anyone say how to remove this space, if it is possible?
This space is added by MaterialGap inside source code.
if (_isChildExpanded(index) && index != 0 && !_isChildExpanded(index - 1))
items.add(MaterialGap(
key: _SaltedKey<BuildContext, int>(context, index * 2 - 1)));
You can remove/comment this part or better create a local project file and comment this part.
To use your customized ExpansionPanelList, import your file like
import 'customized_expansionlist.dart' as customExp;
...
customExp.ExpansionPanelList(... customExp.ExpansionPanel(...))

Instantiating a List with Provider

I am trying to learn how to use ChangeNotifierProvider and have gotten stuck. I've setup the class as so:
void main() => runApp(
ChangeNotifierProvider(create: (context) => ItemList(),
child: MyApp(),
)
);
class ItemData {
final String title;
final int score;
ItemData({required this.title, required this.score});
}
class ItemList extends ChangeNotifier{
final _items = [];
void add(item){
_items.add(item);
notifyListeners();
}
void update(){
notifyListeners();
}
}
final itemList = ItemList();
Now I want to create the list:
I'm trying to add items by calling:
itemList.add(ItemData({elements}))
but this isn't working. How do I create my list so I can put it into a Listview Builder?
Try this one:
main.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() => runApp(
const MyApp(),
);
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ChangeNotifierProvider(
create: (context) => ItemList(), child: const NewHomePage()),
);
}
}
class NewHomePage extends StatefulWidget {
const NewHomePage({Key? key}) : super(key: key);
#override
_NewHomePageState createState() => _NewHomePageState();
}
class _NewHomePageState extends State<NewHomePage> {
#override
Widget build(BuildContext context) {
return Consumer<ItemList>(builder: (context, providerItem, child) {
return Scaffold(
appBar: AppBar(
backgroundColor: const Color(0XFF2e3438),
),
body: Column(
mainAxisSize: MainAxisSize.min,
children: [
providerItem.basketItem.isEmpty
? const Text("No item in the list")
: ListView.builder(
itemCount: providerItem.basketItem.length,
shrinkWrap: true,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
"Title: " + providerItem.basketItem[index].title),
);
}),
ElevatedButton(
onPressed: () {
providerItem.addItem(ItemData(
title: DateTime.now().toString(),
score: DateTime.now().month));
print("data added successfully" +
providerItem.basketItem.length.toString());
},
child: const Text("Add Data")),
],
));
});
}
}
item_data.dart
class ItemData {
final String title;
final int score;
ItemData({required this.title, required this.score});
}
item_list.dart
class ItemList extends ChangeNotifier {
List<ItemData> _items = [];
void addItem(ItemData itemData) {
_items.add(itemData);
notifyListeners();
}
List<ItemData> get basketItem {
return _items;
}
}

Behaivor of instances new-ed from the same StatefulWidget

I have troubled with the behaivor of instances from the same class.
I made two instances of StatefulWidget(BodyLayout) class.
and switch them by BottomNavigationBar
But only one of initState() of BodyLayout is called.
I am confused by this behavior , State is shared each instances???
I want to each initState() is called separately.
Please help some hint.
These are full source code below.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
routes: {
"/": (_) => new MyHomePage(),
"/browser": (_) => new Text("not use"),
}
);
}
}
class Article{
String title;
String url;
Article();
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<Widget> _myLayouts = [];
int _currentIndex = 0;
#override
void initState() {
super.initState();
_myLayouts = [
new BodyLayout("latest"),
new BodyLayout("pop"),
];
}
void _onItemTapped(int index) {
print("itemTapped :" +index.toString());
setState(() {
_currentIndex = index;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// title: Text(widget.title),
),
body: _myLayouts[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('latest'),
),
BottomNavigationBarItem(
icon: Icon(Icons.business),
title: Text('pop'),
),
],
currentIndex: _currentIndex,
selectedItemColor: Colors.amber[800],
onTap: _onItemTapped,
),
);
}
}
class BodyLayout extends StatefulWidget {
final String mode;
BodyLayout(this.mode);
#override
_BodyLayoutState createState() => _BodyLayoutState();
}
class _BodyLayoutState extends State<BodyLayout>{
List<Article> articles = [];
bool loading = true;
int page = 1;
#override
void initState(){
super.initState();
print ("init:" + widget.mode);// this called only one time.....
_callApi(); // this called only one time.....
}
void _callApi() {
var a = Article();
a.title = widget.mode;
a.url = widget.mode;
articles.add(a);
setState((){
loading = false;
});
}
#override
Widget build(BuildContext context) {
if(loading) {
return CircularProgressIndicator();
}
return ListView.builder(
itemCount: articles.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(articles[index].title),
);
},
);
}
}
I am confused by this behavior , State is shared each instances???
Yes, it is. You need to provide unique keys to your widgets:
class BodyLayout extends StatefulWidget {
BodyLayout(this.mode, {Key key}) : super(key: key);
...
_myLayouts = [
new BodyLayout("latest", key: Key('1')),
new BodyLayout("pop", key: Key('2')),
];
and bam - it works:
I/flutter (12871): init:latest
I/flutter (12871): itemTapped :1
I/flutter (12871): init:pop
From the docs:
A StatefulWidget keeps the same State object when moving from one location in the tree to another if its creator used a GlobalKey for its key.
So basically, if your widgets have the same key (or don't have one), they're interpreted as the same widget.

Where content of StatefulWidget should be stored?

I switch two layout by BottomNavigationBar
Every time switching the bar, it loads initState() and lost all variables in _BodyLayoutState()
So, I wonder,
1.Keeping the contents(List<Article> articles = [];) in State is not good?? I should keep contents in upper class like _MyHomePageState?
2.Is there any way to keep contents in State and not dispose when switching ???
These are source code.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
routes: {
"/": (_) => new MyHomePage(),
"/browser": (_) => new Text("not use"),
}
);
}
}
class Article{
String title;
String url;
Article();
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<Widget> _myLayouts = [];
int _currentIndex = 0;
#override
void initState() {
super.initState();
_myLayouts = [
new BodyLayout("latest", key: Key('1')),
new BodyLayout("pop",key: Key('2')),
];
}
void _onItemTapped(int index) {
print("itemTapped :" +index.toString());
setState(() {
_currentIndex = index;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// title: Text(widget.title),
),
body: _myLayouts[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('latest'),
),
BottomNavigationBarItem(
icon: Icon(Icons.business),
title: Text('pop'),
),
],
currentIndex: _currentIndex,
selectedItemColor: Colors.amber[800],
onTap: _onItemTapped,
),
);
}
}
class BodyLayout extends StatefulWidget {
final String mode;
BodyLayout(this.mode, {Key key}) : super(key: key);
#override
_BodyLayoutState createState() => _BodyLayoutState();
}
class _BodyLayoutState extends State<BodyLayout>{
List<Article> articles = [];
bool loading = true;
bool firstLoaded = false;
int page = 1;
#override
void initState(){
super.initState();
print ("init:" + widget.mode);
if (firstLoaded == false){
print("I don't want to load twice");
_callApi();
}
}
void _callApi() {
var a = Article();
a.title = widget.mode;
a.url = widget.mode;
articles.add(a);
firstLoaded == true;
setState((){
loading = false;
});
}
#override
Widget build(BuildContext context) {
if(loading) {
return CircularProgressIndicator();
}
return ListView.builder(
itemCount: articles.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(articles[index].title),
);
},
);
}
}
You can try IndexedStack to display your BodyLayout's.
Just change
body: _myLayouts[_currentIndex],
to
body: IndexedStack(children: _myLayouts, index: _currentIndex,),
You can use the Provider package to store that information https://pub.dev/packages/provider

Flutter Quick Actions change selected Bottom Navigation Bar item

I'm trying to implement home screen quick actions / app shortcuts in my Flutter app. What I'm trying to achieve is when the user launches my app via a quick action, the app changes the selected tab inside the bottom navigation bar. Any help is appreciated.
main.dart:
runApp(
MaterialApp(
theme: Themes.appLightTheme,
darkTheme: Themes.appDarkTheme,
home: QuickActionsController(
child: HomeFrame(currentIndex: 0),
),
My QuickActionsController class:
import 'package:binfinder/screens/HomeFrame.dart';
import 'package:flutter/material.dart';
import 'package:quick_actions/quick_actions.dart';
class QuickActionsController extends StatefulWidget {
final HomeFrame child;
QuickActionsController({Key key, this.child}) : super(key: key);
#override
_QuickActionsControllerState createState() => _QuickActionsControllerState();
}
class _QuickActionsControllerState extends State<QuickActionsController> {
final QuickActions quickActions = QuickActions();
int _currentIndex = 0;
#override
void initState() {
super.initState();
_handleQuickActions();
_setupQuickActions();
}
void _setupQuickActions() {
quickActions.setShortcutItems(<ShortcutItem>[
ShortcutItem(
type: 'action_map',
localizedTitle: 'Map',
),
]);
}
void _handleQuickActions() {
quickActions.initialize((shortcutType) {
if (shortcutType == 'action_map') {
setState(() {
_currentIndex = 1;
});
} else {
setState(() {
_currentIndex = 0;
});
}
});
}
#override
Widget build(BuildContext context) {
widget.child.currentIndex = _currentIndex;
return widget.child;
}
}
In the demo below, direct click app will enter First Page and In Quick Action choose Main view will enter Second Page
_handleQuickActions need to use
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => BottomNavigationBarController(
initialIndex: 1,
)));
and use initial index to control page index
class BottomNavigationBarController extends StatefulWidget {
final int initialIndex;
BottomNavigationBarController({
this.initialIndex,
Key key,
}) : super(key: key);
#override
_BottomNavigationBarControllerState createState() =>
_BottomNavigationBarControllerState();
}
full code
import 'package:flutter/material.dart';
import 'package:quick_actions/quick_actions.dart';
import 'dart:io';
class QuickActionsManager extends StatefulWidget {
final Widget child;
QuickActionsManager({Key key, this.child}) : super(key: key);
_QuickActionsManagerState createState() => _QuickActionsManagerState();
}
class _QuickActionsManagerState extends State<QuickActionsManager> {
final QuickActions quickActions = QuickActions();
#override
void initState() {
super.initState();
_setupQuickActions();
_handleQuickActions();
}
#override
Widget build(BuildContext context) {
return widget.child;
}
void _setupQuickActions() {
quickActions.setShortcutItems(<ShortcutItem>[
ShortcutItem(
type: 'action_main',
localizedTitle: 'Main view',
icon: Platform.isAndroid ? 'quick_box' : 'QuickBox'),
ShortcutItem(
type: 'action_help',
localizedTitle: 'Help',
icon: Platform.isAndroid ? 'quick_heart' : 'QuickHeart')
]);
}
void _handleQuickActions() {
quickActions.initialize((shortcutType) {
if (shortcutType == 'action_main') {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => BottomNavigationBarController(
initialIndex: 1,
)));
} else if (shortcutType == 'action_help') {
print('Show the help dialog!');
}
});
}
}
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'QuickActions Demo',
home: QuickActionsManager(child: BottomNavigationBarController(initialIndex: 0,)));
}
}
class Home extends StatelessWidget {
const Home({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(body: Center(child: Text('Home')));
}
}
class Login extends StatelessWidget {
const Login({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(body: Center(child: Text('Login')));
}
}
class BottomNavigationBarController extends StatefulWidget {
final int initialIndex;
BottomNavigationBarController({
this.initialIndex,
Key key,
}) : super(key: key);
#override
_BottomNavigationBarControllerState createState() =>
_BottomNavigationBarControllerState();
}
class _BottomNavigationBarControllerState
extends State<BottomNavigationBarController> {
final List<Widget> pages = [
FirstPage(
key: PageStorageKey('Page1'),
),
SecondPage(
key: PageStorageKey('Page2'),
),
];
final PageStorageBucket bucket = PageStorageBucket();
int _selectedIndex = 0;
Widget _bottomNavigationBar(int selectedIndex) => BottomNavigationBar(
onTap: (int index) => setState(() => _selectedIndex = index),
currentIndex: selectedIndex,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.add), title: Text('First Page')),
BottomNavigationBarItem(
icon: Icon(Icons.list), title: Text('Second Page')),
],
);
#override
void initState() {
_selectedIndex = widget.initialIndex;
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: _bottomNavigationBar(_selectedIndex),
body: PageStorage(
child: pages[_selectedIndex],
bucket: bucket,
),
);
}
}
class FirstPage extends StatelessWidget {
const FirstPage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("First Screen"),
),
body: ListView.builder(itemBuilder: (context, index) {
return ListTile(
title: Text('Lorem Ipsum'),
subtitle: Text('$index'),
);
}),
);
}
}
class SecondPage extends StatelessWidget {
const SecondPage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Screen"),
),
body: ListView.builder(itemBuilder: (context, index) {
return ListTile(
title: Text('Lorem Ipsum'),
subtitle: Text('$index'),
);
}),
);
}
}
demo, emulator is a little slow when enter Second Page