WillPopScope not working on different WebView present in different tabs - flutter

I have made a flutter project in which I am facing issue with two pages.
Plugin used for two pages- https://pub.dev/packages/flutter_inappwebview
One Page that is "Single App page" render a Webpage & WillPopScope is working here absolutely fine.
Another Page that is "Compare App Page" render a different Webpages into different tabs & here WillPopScope is only working for 1st tab and not working for the rest of tab.
I want to implement WillPopScope for each tab so that each tab have its own history and when a person present on a particular tab & hitting back button(I want to do this inbuilt back button & not via a created back button) takes him to back in history.
Note- A common widget is used in both Single & Compare App as children.
Below are the Main code
class NewCompareApp extends StatefulWidget {
#override
_NewCompareAppState createState() => _NewCompareAppState();
}
class _NewCompareAppState extends State<NewCompareApp> {
List apps;
#override
void initState() {
super.initState();
apps = getCompareApps();
}
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: apps.length,
child: Scaffold(
appBar: AppBar(
titleSpacing: 0,
title: Card(
elevation: 10,
child: Container(
height: 35,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(8.0)),
),
padding: EdgeInsets.only(left: 5),
child: TextField(
autofocus: false,
cursorColor: Colors.grey,
decoration: InputDecoration(
hintText: 'Search', border: InputBorder.none),
),
),
),
bottom: TabBar(
indicatorWeight: 1,
labelColor: Colors.white,
unselectedLabelColor: Colors.black,
indicatorColor: Colors.white,
isScrollable: true,
tabs: apps
.map((ca) => Tab(
text: ca.name,
))
.toList(),
),
),
resizeToAvoidBottomInset: false,
body: TabBarView(
physics: NeverScrollableScrollPhysics(),
children: apps
.map((ca) => WebApp(
url: ca.url,
forWidget: 'cmp',
))
.toList(),
)),
);
}
}
class WebApp extends StatefulWidget {
final String url;
final String forWidget;
WebApp({Key key, #required this.url, #required this.forWidget})
: super(key: key);
#override
_WebAppState createState() => _WebAppState();
}
class _WebAppState extends State<WebApp>
with AutomaticKeepAliveClientMixin<WebApp> {
#override
bool get wantKeepAlive => true;
var currentUrl = '';
InAppWebViewController controller;
Future<bool> _handleBack(context) async {
var status = await controller.canGoBack();
if (status) {
controller.goBack();
} else {
getExitDialog(context, extra: {
"in_app": true,
});
}
return false;
}
#override
Widget build(BuildContext context) {
Widget mainWidget = Column(
children: <Widget>[
Expanded(
child: WillPopScope(
onWillPop: () => _handleBack(context),
child: InAppWebView(
initialUrl: widget.url,
onWebViewCreated: (InAppWebViewController webViewController) {
controller = webViewController;
},
onLoadStart: (InAppWebViewController controller, String url) {
this.currentUrl = url;
},
initialOptions: InAppWebViewGroupOptions(
crossPlatform: InAppWebViewOptions(
horizontalScrollBarEnabled: false,
verticalScrollBarEnabled: false),
),
),
),
),
Container(
color: Colors.white,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
FlatButton(
padding: EdgeInsets.zero,
child: Icon(Icons.arrow_back),
onPressed: () => _handleBack(context),
),
FlatButton(
padding: EdgeInsets.zero,
child: Icon(Icons.refresh),
onPressed: () {
if (controller != null) {
controller.reload();
}
},
),
FlatButton(
padding: EdgeInsets.zero,
child: Icon(Icons.arrow_forward),
onPressed: () {
if (controller != null) {
controller.goForward();
}
},
),
FlatButton(
padding: EdgeInsets.zero,
child: Icon(Icons.share),
onPressed: null,
),
],
),
),
],
);
return widget.forWidget == 'single_app'
? Scaffold(body: SafeArea(top: true, child: mainWidget))
: mainWidget;
}
}
Code in detail- https://gist.github.com/ycv005/13dec1df2b57535271eb346e132c6775
Thanks in advance.

After struggling I found below solution where a global controller that keep changing on tab change and plus have a local controller as well to handle other stuff.
In bottom, selecting global controller and TabBarView(wrapping with WillPopScope)
bottom: TabBar(
onTap: (int index) async {
currentIndex = index;
print('here is index- $index');
if (tabWebControllerMap.containsKey(currentIndex)) {
globalController = tabWebControllerMap[currentIndex];
final hereUrl = await globalController.getUrl();
print('here url- $hereUrl');
}
},
controller: _tabController,
indicatorWeight: 1,
labelColor: Colors.white,
unselectedLabelColor: Colors.black,
indicatorColor: Colors.white,
isScrollable: true,
tabs: apps
.map((ca) => Tab(
child: Text(
ca.name,
style: TextStyle(fontSize: 12),
),
))
.toList(),
),
),

Related

Rebuild screen in flutter TabBar

I use TabBar and its screens List of StatelessWidget
I want when go to one of those screens rebuild it again ( re call api ) because its data depend on lots of actions on the other screens , for now when I open it for the first time the api call and the screen is built but if I go back to it it shows the last data ... note that i use getx
example of one screen controller
class UnderReviewPostsController extends GetxController
with StateMixin<List<UnderReviewPost>> {
final DashboardApiProvider dashboardApiProvider;
UnderReviewPostsController({required this.dashboardApiProvider});
#override
void onInit() {
_getUnderReviewPosts();
super.onInit();
}
void _getUnderReviewPosts() async {
await dashboardApiProvider
.getUnderReviewPosts()
.then((value) {
change(value, status: RxStatus.success());
}, onError: (err) {
change(null, status: RxStatus.error(err.toString()));
});
}
}
TabBar Ui :
class UserPosts extends StatelessWidget {
UserPosts({super.key});
final GlobalKey<ScaffoldState> _key = GlobalKey();
final TabControllers _tabx = Get.find();
static final List<StatelessWidget> _views = [
ActivePostsItems(),
RenewablePostsItems(),
const UnderReviewPostsItems(),
const DisapprovedPostsItems(),
const MarketerPostsItems(),
];
#override
Widget build(BuildContext context) {
User user = Get.arguments;
return DefaultTabController(
length: 5,
child: Directionality(
textDirection: TextDirection.rtl,
child: Scaffold(
key: _key,
drawer: NavBar(
user: user,
),
appBar: AppBar(
leading: IconButton(
onPressed: () {
_key.currentState?.openDrawer();
},
icon: const Icon(
Icons.menu,
color: kBlackColor,
),
),
automaticallyImplyLeading: false,
bottom: TabBar(
controller: _tabx.tabController,
unselectedLabelColor: Colors.grey,
indicatorWeight: 10,
indicatorSize: TabBarIndicatorSize.tab,
indicatorPadding: const EdgeInsets.all(5),
indicator: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: kPrimaryColor,
),
isScrollable: true,
physics: const BouncingScrollPhysics(),
enableFeedback: true,
tabs: _tabx.tabs,
),
backgroundColor: kLightGrayColor,
),
body: TabBarView(
physics: const BouncingScrollPhysics(),
controller: _tabx.tabController,
children: _views,
),
),
),
);
}
}
thanks

How I can onTab method call when I change tab by swiping or scrolling?

I use a default Tabbar. I have two tab .When I change tabview by clicking, onTab method call finely. But when I change tabview by swiping or scrolling, how I can call onTab method?. How I can listen my onTab changing value when I change my tabview by swiping or scrolling? I need change tabIndex value in controller when I change tabView by swiping or scroling.
UI Part here
#override
Widget build(BuildContext context) {
return DefaultTabController(
initialIndex: 0,
length: 2,
child: Scaffold(
appBar: AppBar(
backgroundColor: AllColors.deepPurple,
leading: InkWell(
onTap: () => Get.back(),
child: Icon(
Icons.arrow_back,
color: AllColors.whiteColor,
),
),
elevation: 0.0,
title: Text(
"Categories",
style: AllStyles.titleTextStyle,
),
actions: [
InkWell(
child: Padding(
padding: const EdgeInsets.only(right: 12.0),
child: Icon(Icons.add),
),
onTap: () {
},
)
],
bottom: TabBar(
controller: categoriesController.tabController,
onTap: (value) {
categoriesController.changeTabValue(value);
print("Value " + value.toString());
},
isScrollable: false,
indicatorColor: AllColors.whiteColor,
indicatorSize: TabBarIndicatorSize.label,
tabs: [Tab(text: "Income"), Tab(text: "Expense")],
),
),
body: TabBarView(
children: [
IncoomeTabCategories(),
ExpenseTabCategories()
],
),
),
);
}
Controller part here:
class CategoriesController extends GetxController with GetSingleTickerProviderStateMixin {
TabController? tabController;
int tabIndex=0;
#override
void onInit() {
super.onInit();
tabController = TabController(length: 2, vsync: this,initialIndex: 0)
}
#override
void dispose() {
super.dispose();
tabController!.dispose();
}
void changeTabValue(int index){
tabIndex=index;
update();
}
}

Flutter : Navigate with persistent side bar

I'm creating a flutter web project that looks like a dashboard. It has a side navigation and a body area that displays different screens.
To achieve this, I have divided my screen into two parts using Expanded and given them a flex value.
And to display different screens I have used IndexedStack.
Here is my main.dart file :
import 'package:flutter/material.dart';
import 'package:xxx/screens/courses/courses_screen.dart';
import 'package:xxx/side_navigation/menu_item.dart';
import 'package:xxx/componnents/header.dart';
import 'package:websafe_svg/websafe_svg.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
visualDensity: VisualDensity.adaptivePlatformDensity,
scaffoldBackgroundColor: Color(0xfffafafa),
fontFamily: 'Poppins',
),
home: MainScreenLayout(),
);
}
}
class MainScreenLayout extends StatefulWidget {
#override
_MainScreenLayoutState createState() => _MainScreenLayoutState();
}
class _MainScreenLayoutState extends State<MainScreenLayout> {
int selectedIndex = 0;
int hoverIndex = -1;
Color bgColor = Colors.transparent;
final List<MenuItem> menus = <MenuItem>[
MenuItem(label: 'Home', icon: Icons.home, screen: Container()),
MenuItem(label: 'Courses', icon: Icons.add, screen: Container()),
MenuItem(label: 'Students', icon: Icons.face_outlined, screen: Container()),
MenuItem(label: 'Home', icon: Icons.home, screen: Container()),
MenuItem(label: 'Courses', icon: Icons.add, screen: Container()),
MenuItem(label: 'Students', icon: Icons.face_outlined, screen: Container()),
];
final List<Widget> _screens = [
Container(
child: Image.asset(
'assets/try.png',
width: 100,
height: 100,
),
),
CoursesScreen(),
Container(
child: WebsafeSvg.asset('assets/folder_icon.svg'),
),
];
void _changeBg(int index) {
setState(() {
hoverIndex = index;
});
}
void _resetBg() {
setState(() {
hoverIndex = -1;
});
}
#override
Widget build(BuildContext context) {
double _width = MediaQuery.of(context).size.width;
return Scaffold(
body: Row(
children: [
Expanded(
flex: 2,
child: Container(
decoration: BoxDecoration(
color: Color(0xffffffff),
borderRadius: BorderRadius.only(
topRight: Radius.circular(_width * 0.03),
bottomRight: Radius.circular(_width * 0.03),
),
),
child: Column(
children: <Widget>[
SizedBox(
height: 50,
),
Flexible(
child: ListView.builder(
itemCount: menus.length,
itemBuilder: (BuildContext context, int index) {
return MouseRegion(
onHover: (event) {
_changeBg(index);
},
onExit: (event) {
_resetBg();
},
child: MenuItemLayout(
bgColor: hoverIndex == index
? Color(0xfffafafa)
: Colors.transparent,
menuItem: menus[index],
isSelected: selectedIndex == index ? true : false,
onTap: () {
setState(() {
selectedIndex = index;
});
},
),
);
},
),
),
],
),
),
),
Expanded(
flex: 7,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Header(
label: menus[selectedIndex].label,
),
Expanded(
child: Container(
child: IndexedStack(
index: selectedIndex,
children: _screens,
),
),
),
],
),
)
],
),
);
}
}
Here is the code for one of the screens (CourseScreen)
import 'package:flutter/material.dart';
import 'package:vjti_dashboard/componnents/responsive.dart';
import 'package:vjti_dashboard/screens/courses/courses_card.dart';
import 'package:vjti_dashboard/screens/courses/courses_model.dart';
class CoursesScreen extends StatelessWidget {
final List<CoursesModel> allCourses = [
CoursesModel(courseId: '123', courseName: 'Ecommerce'),
CoursesModel(courseId: '123', courseName: 'Big Data Analytics'),
CoursesModel(courseId: '123', courseName: 'User Experience Design'),
CoursesModel(courseId: '123', courseName: 'Technical Seminar'),
CoursesModel(courseId: '123', courseName: 'Elective 1'),
];
#override
Widget build(BuildContext context) {
return ListView(
scrollDirection: Axis.vertical,
shrinkWrap: true,
children: [
Sample(
headingLabel: 'First Year',
allCourses: allCourses,
courseColor: Color(0xffEEF1E6),
),
Sample(
headingLabel: 'Second Year',
allCourses: allCourses,
courseColor: Color(0xffF9F1D6),
),
Sample(
headingLabel: 'Third Year',
allCourses: allCourses,
courseColor: Color(0xffE2F0CB),
),
],
);
}
}
class Sample extends StatelessWidget {
final String headingLabel;
final Color courseColor;
final List<CoursesModel> allCourses;
const Sample({Key key, this.allCourses, this.headingLabel, this.courseColor})
: super(key: key);
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
GestureDetector(
onTap: () {},
child: Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Color(0xffd6d6d6),
width: 1.0,
),
),
),
child: Text(headingLabel),
),
),
GridView.count(
primary: false,
physics: ScrollPhysics(), // to disable GridView's scrolling
shrinkWrap: true,
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 0),
crossAxisSpacing: 20,
childAspectRatio: 3 / 1,
mainAxisSpacing: 20,
crossAxisCount: Responsive.isDesktop(context)
? 4
: Responsive.isTablet(context)
? 3
: 2,
children: List.generate(
allCourses.length,
(index) {
return CourseCard(
coursesModel: allCourses[index],
containerColor: courseColor,
);
},
),
),
],
),
);
}
}
The side navigation works as it should and different pages are loaded while the navigation sticks to the left.
When I click on any widget on the CourseScreen, I want to open another screen that would replace CourseScreen but the navigation should still be there.
How can I achieve this?
Note : I'm new to flutter and most of the code that I have written is not perfect and probably is not a good way. I would appreciate if you can point out bad codes in the above files.
Thank You!!!
onGenerateRoute: (settings) => MaterialPageRoute(
builder: (context) => Parent(),

Passing value to previous widget

I have simple form , inside it have CircularAvatar when this is pressed show ModalBottomSheet to choose between take picture from gallery or camera. To make my widget more compact , i separated it to some file.
FormDosenScreen (It's main screen)
DosenImagePicker (It's only CircularAvatar)
ModalBottomSheetPickImage (It's to show ModalBottomSheet)
The problem is , i don't know how to passing value from ModalBottomSheetPickImage to FormDosenScreen. Because value from ModalBottomSheetPickImage i will use to insert operation.
I only success passing from third Widget to second Widget , but when i passing again from second Widget to first widget the value is null, and i think the problem is passing from Second widget to first widget.
How can i passing from third Widget to first Widget ?
First Widget
class FormDosenScreen extends StatefulWidget {
static const routeNamed = '/formdosen-screen';
#override
_FormDosenScreenState createState() => _FormDosenScreenState();
}
class _FormDosenScreenState extends State<FormDosenScreen> {
String selectedFile;
#override
Widget build(BuildContext context) {
final detectKeyboardOpen = MediaQuery.of(context).viewInsets.bottom;
print('trigger');
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text('Tambah Dosen'),
actions: <Widget>[
PopupMenuButton(
itemBuilder: (_) => [
PopupMenuItem(
child: Text('Tambah Pelajaran'),
value: 'add_pelajaran',
),
],
onSelected: (String value) {
switch (value) {
case 'add_pelajaran':
Navigator.of(context).pushNamed(FormPelajaranScreen.routeNamed);
break;
default:
}
},
)
],
),
body: Stack(
fit: StackFit.expand,
children: <Widget>[
SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
SizedBox(height: 20),
DosenImagePicker(onPickedImage: (file) => selectedFile = file),
SizedBox(height: 20),
Card(
margin: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextFormFieldCustom(
onSaved: (value) {},
labelText: 'Nama Dosen',
),
SizedBox(height: 20),
TextFormFieldCustom(
onSaved: (value) {},
prefixIcon: Icon(Icons.email),
labelText: 'Email Dosen',
keyboardType: TextInputType.emailAddress,
),
SizedBox(height: 20),
TextFormFieldCustom(
onSaved: (value) {},
keyboardType: TextInputType.number,
inputFormatter: [
// InputNumberFormat(),
WhitelistingTextInputFormatter.digitsOnly
],
prefixIcon: Icon(Icons.local_phone),
labelText: 'Telepon Dosen',
),
],
),
),
),
SizedBox(height: kToolbarHeight),
],
),
),
Positioned(
child: Visibility(
visible: detectKeyboardOpen > 0 ? false : true,
child: RaisedButton(
onPressed: () {
print(selectedFile);
},
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
color: colorPallete.primaryColor,
child: Text(
'SIMPAN',
style: TextStyle(fontWeight: FontWeight.bold, fontFamily: AppConfig.headerFont),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
textTheme: ButtonTextTheme.primary,
),
),
bottom: kToolbarHeight / 2,
left: sizes.width(context) / 15,
right: sizes.width(context) / 15,
)
],
),
);
}
}
Second Widget
class DosenImagePicker extends StatefulWidget {
final Function(String file) onPickedImage;
DosenImagePicker({#required this.onPickedImage});
#override
DosenImagePickerState createState() => DosenImagePickerState();
}
class DosenImagePickerState extends State<DosenImagePicker> {
String selectedImage;
#override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.center,
child: InkWell(
onTap: () async {
await showModalBottomSheet(
context: context,
builder: (context) => ModalBottomSheetPickImage(
onPickedImage: (file) {
setState(() {
selectedImage = file;
widget.onPickedImage(selectedImage);
print('Hellooo dosen image picker $selectedImage');
});
},
),
);
},
child: CircleAvatar(
foregroundColor: colorPallete.black,
backgroundImage: selectedImage == null ? null : MemoryImage(base64.decode(selectedImage)),
radius: sizes.width(context) / 6,
backgroundColor: colorPallete.accentColor,
child: selectedImage == null ? Text('Pilih Gambar') : SizedBox(),
),
),
);
}
}
Third Widget
class ModalBottomSheetPickImage extends StatelessWidget {
final Function(String file) onPickedImage;
ModalBottomSheetPickImage({#required this.onPickedImage});
#override
Widget build(BuildContext context) {
return SizedBox(
child: Padding(
padding: const EdgeInsets.all(15.0),
child: Wrap(
alignment: WrapAlignment.spaceEvenly,
children: <Widget>[
InkWell(
onTap: () async {
final String resultBase64 =
await commonFunction.pickImage(quality: 80, returnFile: ReturnFile.BASE64);
onPickedImage(resultBase64);
},
child: CircleAvatar(
foregroundColor: colorPallete.white,
backgroundColor: colorPallete.green,
child: Icon(Icons.camera_alt),
),
),
InkWell(
onTap: () async {
final String resultBase64 =
await commonFunction.pickImage(returnFile: ReturnFile.BASE64, isCamera: false);
onPickedImage(resultBase64);
},
child: CircleAvatar(
foregroundColor: colorPallete.white,
backgroundColor: colorPallete.blue,
child: Icon(Icons.photo_library),
),
),
],
),
),
);
}
}
The cleanest and easiest way to do this is through Provider. It is one of the state management solutions you can use to pass values around the app as well as rebuild only the widgets that changed. (Ex: When the value of the Text widget changes). Here is how you can use Provider in your scenario:
This is how your model should look like:
class ImageModel extends ChangeNotifier {
String _base64Image;
get base64Image => _base64Image;
set base64Image(String base64Image) {
_base64Image = base64Image;
notifyListeners();
}
}
Don't forget to add getters and setters so that you can use notifyListeners() if you have any ui that depends on it.
Here is how you can access the values of ImageModel in your UI:
final model=Provider.of<ImageModel>(context,listen:false);
String image=model.base64Image; //get data
model.base64Image=resultBase64; //set your image data after you used ImagePicker
Here is how you can display your data in a Text Widget (Ideally, you should use Selector instead of Consumer so that the widget only rebuilds if the value its listening to changes):
#override
Widget build(BuildContext context) {
//other widgets
Selector<ImageModel, String>(
selector: (_, model) => model.base64Image,
builder: (_, image, __) {
return Text(image);
},
);
}
)
}
You could achieve this easily. If you are using Blocs.

Flutter - How to make a row to stay at the top of screen when scrolled

Iam trying to implement a appbar like this
When scrolling down I need to hide the search bar alone and pin the row and the tabs on the device top. Which is like
And when we scroll down the all the three rows needs to be displayed.
Using SliverAppBar with bottom property tabs are placed and pinned when scrolling, but a row above it should be pinned at the top above the tabbar. Im not able to add a column with the row and tabbar because of preferedSizeWidget in bottom property. Flexible space bar also hides with the appbar so I cannot use it. Does anyone know how to make this layout in flutter.
Please try this.
body: Container(
child: Column(
children: <Widget>[
Container(
// Here will be your AppBar/Any Widget.
),
Expanded(
child: SingleChildScrollView(
child: Column(
children: <Widget>[
// All your scroll views
Container(),
Container(),
],
),
),
),
],
),
),
You could create your own SliverAppBar or you can divide them in 2 items, a SliverAppBar and a SliverPersistentHeader
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home>
with SingleTickerProviderStateMixin {
TabController controller;
TextEditingController textController = TextEditingController();
#override
void initState() {
super.initState();
controller = TabController(
length: 3,
vsync: this,
);
}
#override
void dispose(){
super.dispose();
controller.dispose();
textController.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
leading: const Icon(Icons.menu),
title: TextField(
controller: textController,
textInputAction: TextInputAction.search,
decoration: InputDecoration(
isDense: true,
hintText: 'Search Bar',
hintStyle: TextStyle(color: Colors.black.withOpacity(.5), fontSize: 16),
border: InputBorder.none
)
),
snap: true,
floating: true,
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () => print('searching for: ${textController.text}'),
)
]
),
//This is Where you create the row and your tabBar
SliverPersistentHeader(
delegate: MyHeader(
top: Row(
children: [
for(int i = 0; i < 4; i++)
Expanded(
child: OutlineButton(
child: Text('button $i'),
onPressed: () => print('button $i pressed'),
)
)
]
),
bottom: TabBar(
indicatorColor: Colors.white,
tabs: [
Tab(text: 'Tab 1'),
Tab(text: 'Tab 2'),
Tab(text: 'Tab 3'),
],
controller: controller,
),
),
pinned: true,
),
SliverFillRemaining(
child: TabBarView(
controller: controller,
children: <Widget>[
Center(child: Text("Tab one")),
Center(child: Text("Tab two")),
Center(child: Text("Tab three")),
],
),
),
],
),
);
}
}
//Your class should extend SliverPersistentHeaderDelegate to use
class MyHeader extends SliverPersistentHeaderDelegate {
final TabBar bottom;
final Widget top;
MyHeader({this.bottom, this.top});
#override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
color: Theme.of(context).accentColor,
height: math.max(minExtent, maxExtent - shrinkOffset),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if(top != null)
SizedBox(
height: kToolbarHeight,
child: top
),
if(bottom != null)
bottom
]
)
);
}
/*
kToolbarHeight = 56.0, you override the max and min extent with the height of a
normal toolBar plus the height of the tabBar.preferredSize
so you can fit your row and your tabBar, you give them the same value so it
shouldn't shrink when scrolling
*/
#override
double get maxExtent => kToolbarHeight + bottom.preferredSize.height;
#override
double get minExtent => kToolbarHeight + bottom.preferredSize.height;
#override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => false;
}
UPDATE
A NestedScollView let you have 2 ScrollViews so you can control the inner scroll with the outer (just like you want with a TabBar)
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
TextEditingController textController = TextEditingController();
List<String> _tabs = ['Tab 1', 'Tab 2', 'Tab 3'];
// Your tabs, or you can ignore this and build your list
// on TabBar and the TabView like my previous example.
// I don't create a TabController now because I wrap the whole widget with a DefaultTabController
#override
void initState() {
super.initState();
}
#override
void dispose() {
super.dispose();
textController.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: DefaultTabController(
length: _tabs.length, // This is the number of tabs.
child: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled){
return <Widget>[
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
elevation: 0.0,
leading: const Icon(Icons.menu),
title: TextField(
controller: textController,
textInputAction: TextInputAction.search,
decoration: InputDecoration(
isDense: true,
hintText: 'Search Bar',
hintStyle: TextStyle(
color: Colors.black.withOpacity(.5),
fontSize: 16),
border: InputBorder.none)
),
snap: true,
floating: true,
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () => print('searching for: ${textController.text}'),
)
]
),
),
SliverPersistentHeader(
delegate: MyHeader(
top: Row(children: [
for (int i = 0; i < 4; i++)
Expanded(
child: OutlineButton(
child: Text('button $i'),
onPressed: () => print('button $i pressed'),
))
]),
bottom: TabBar(
indicatorColor: Colors.white,
tabs: _tabs.map((String name) => Tab(text: name)).toList(),
),
),
pinned: true,
),
];
},
body: TabBarView(
children: _tabs.map((String name) {
return SafeArea(
child: Builder(
// This Builder is needed to provide a BuildContext that is
// "inside" the NestedScrollView, so that
// sliverOverlapAbsorberHandleFor() can find the
// NestedScrollView.
// You can ignore it if you're going to build your
// widgets in another Stateless/Stateful class.
builder: (BuildContext context) {
return CustomScrollView(
// The "controller" and "primary" members should be left
// unset, so that the NestedScrollView can control this
// inner scroll view.
// If the "controller" property is set, then this scroll
// view will not be associated with the NestedScrollView.
// The PageStorageKey should be unique to this ScrollView;
// it allows the list to remember its scroll position when
// the tab view is not on the screen.
key: PageStorageKey<String>(name),
slivers: <Widget>[
SliverOverlapInjector(
// This is the flip side of the SliverOverlapAbsorber
// above.
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: SliverFixedExtentList(
itemExtent: 48.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(
title: Text('Item $index'),
onTap: () => print('$name at index $index'),
);
},
childCount: 30,
),
),
),
],
);
},
),
);
}).toList(),
),
),
));
}
}
import 'dart:io';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primaryColor: Colors.white,
),
home: NewsScreen(),
debugShowCheckedModeBanner: false,
);
}
}
class NewsScreen extends StatefulWidget {
#override
State<StatefulWidget> createState() => _NewsScreenState();
}
class _NewsScreenState extends State<NewsScreen> {
final List<String> _tabs = <String>[
"Featured",
"Popular",
"Latest",
];
#override
Widget build(BuildContext context) {
return Material(
child: Scaffold(
body: DefaultTabController(
length: _tabs.length,
child: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverSafeArea(
top: false,
bottom: Platform.isIOS ? false : true,
sliver: SliverAppBar(
title: Text('Tab Demo'),
elevation: 0.0,
floating: true,
pinned: true,
snap: true,
forceElevated: innerBoxIsScrolled,
bottom: TabBar(
tabs: _tabs.map((String name) => Tab(text: name)).toList(),
),
),
),
),
];
},
body: TabBarView(
children: [
Icon(Icons.directions_car),
Icon(Icons.directions_transit),
Icon(Icons.directions_bike),
],
),
),
),
),
);
}
}