I want to achieve something like below (animation style doesn't matter, I'm looking for the way to do this)
However, all resources and question only explain how to create
item addition or removal animations.
My current code (I use BLoC pattern)
class _MembersPageState extends State<MembersPage> {
#override
Widget build(BuildContext context) {
return BlocProvider<MembersPageBloc>(
create: (context) =>
MembersPageBloc(userRepository: UserRepository.instance)..add(MembersPageShowed()),
child: BlocBuilder<MembersPageBloc, MembersPageState>(
builder: (context, state) {
if (state is MembersPageSuccess) {
return ListView.builder(
itemCount: state.users.length,
itemBuilder: (context, index) {
User user = state.users[index];
return ListTile(
isThreeLine: true,
leading: Icon(Icons.person, size: 36),
title: Text(user.name),
subtitle: Text(user.username),
onTap: () => null,
);
},
);
} else
return Text("I don't care");
},
),
);
}
}
Widgets like AnimatedOpacity and AnimatedPositioned can be used to achieve this. However, lifecycle of the children widgets in a ListView is a bit complex. They get destroyed and recreated according to the scroll position. If the child widget has an animation that starts on initialization, it will reanimate whenever the child gets visible to the UI.
Here is my hacky solution. I used a static boolean to indicate whether it's the first time or recreation state and simply ignore the recration. You can copy and try it in Dartpad.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: Colors.brown,
body: ListView(
children: List.generate(
25,
(i) => AnimatedListItem(i, key: ValueKey<int>(i)),
),
),
),
);
}
}
class AnimatedListItem extends StatefulWidget {
final int index;
const AnimatedListItem(this.index, {Key? key}) : super(key: key);
#override
State<AnimatedListItem> createState() => _AnimatedListItemState();
}
class _AnimatedListItemState extends State<AnimatedListItem> {
bool _animate = false;
static bool _isStart = true;
#override
void initState() {
super.initState();
if (_isStart) {
Future.delayed(Duration(milliseconds: widget.index * 100), () {
setState(() {
_animate = true;
_isStart = false;
});
});
} else {
_animate = true;
}
}
#override
Widget build(BuildContext context) {
return AnimatedOpacity(
duration: const Duration(milliseconds: 1000),
opacity: _animate ? 1 : 0,
curve: Curves.easeInOutQuart,
child: AnimatedPadding(
duration: const Duration(milliseconds: 1000),
padding: _animate
? const EdgeInsets.all(4.0)
: const EdgeInsets.only(top: 10),
child: Container(
constraints: const BoxConstraints.expand(height: 100),
child: Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
widget.index.toString(),
style: const TextStyle(fontSize: 24),
),
),
),
),
),
);
}
}
I created a gist on dartpad shows how to add initial animation for ListView
The key point is to create an AnimationController for each list item, cache it, and clean up when animation is complete.
Related
How am I supposed to pass a value in this big mess called Flutter?
30 years old php global $var wasn't good?
All these years were to come up with setState, passed in a controller which get redeclared as a key inside a stateful widget that receive the value from a Navigator?
By the way, I tried using Navigator.push but it seems to open a completely new window, the value is there but I'd need it to show in the tab body not in a new window, below is my code:
main.dart
import 'dart:core';
import 'dart:developer';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:qr_code_scanner/qr_code_scanner.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter App',
theme: ThemeData(
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: HomeView(),
);
}
}
class HomeView extends StatefulWidget {
#override
_HomeViewState createState() => _HomeViewState();
}
class _HomeViewState extends State<HomeView> {
final tabs = [QRViewExample(), SecondView(res: '')];
int _currentIndex = 0;
#override
void initState() {
setState(() {});
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
toolbarHeight: 40.0,
elevation: 0,
centerTitle: true,
title: Text('Flutter App'),
),
body: tabs[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
backgroundColor: Colors.red,
currentIndex: _currentIndex,
type: BottomNavigationBarType.fixed,
selectedItemColor: Colors.white,
unselectedItemColor: Colors.white.withOpacity(0.5),
items: [
BottomNavigationBarItem(
icon: Icon(Icons.qr_code),
label: 'Scan',
),
BottomNavigationBarItem(
icon: Icon(Icons.list),
label: 'List',
),
],
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
),
);
}
}
// SECOND TAB WIDGET (custom)
class SecondView extends StatelessWidget {
const SecondView({Key? key, required this.res}) : super(key: key);
final String? res;
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child: Text(res!),
),
);
}
}
// FIRST TAB WIDGET (qrcode)
class QRViewExample extends StatefulWidget {
const QRViewExample({Key? key}) : super(key: key);
#override
State<StatefulWidget> createState() => _QRViewExampleState();
}
class _QRViewExampleState extends State<QRViewExample> {
Barcode? result;
QRViewController? controller;
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
#override
void reassemble() {
super.reassemble();
if (Platform.isAndroid) {
controller!.pauseCamera();
}
controller!.resumeCamera();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
height: 500,
child: Padding(
padding: EdgeInsets.all(8.0),
child: Column(
children: <Widget>[
Expanded(flex: 4, child: _buildQrView(context)),
Expanded(
flex: 1,
child: FittedBox(
fit: BoxFit.contain,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
if (result != null)
Text(
'Barcode Type: ${describeEnum(result!.format)} Data: ${result!.code}')
else
const Text('Scan a code'),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
margin: const EdgeInsets.all(8),
child: ElevatedButton(
onPressed: () async {
await controller?.toggleFlash();
setState(() {});
},
child: FutureBuilder(
future: controller?.getFlashStatus(),
builder: (context, snapshot) {
return Text('Flash: ${snapshot.data}');
},
)),
),
Container(
margin: const EdgeInsets.all(8),
child: ElevatedButton(
onPressed: () async {
await controller?.flipCamera();
setState(() {});
},
child: FutureBuilder(
future: controller?.getCameraInfo(),
builder: (context, snapshot) {
if (snapshot.data != null) {
return Text(
'Camera facing ${describeEnum(snapshot.data!)}');
} else {
return const Text('loading');
}
},
)),
)
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
margin: const EdgeInsets.all(8),
child: ElevatedButton(
onPressed: () async {
await controller?.pauseCamera();
},
child: const Text('pause',
style: TextStyle(fontSize: 20)),
),
),
Container(
margin: const EdgeInsets.all(8),
child: ElevatedButton(
onPressed: () async {
await controller?.resumeCamera();
},
child: const Text('resume',
style: TextStyle(fontSize: 20)),
),
)
],
),
],
),
),
)
],
),
),
),
);
}
Widget _buildQrView(BuildContext context) {
var scanArea = (MediaQuery.of(context).size.width < 400 ||
MediaQuery.of(context).size.height < 400)
? 150.0
: 300.0;
return QRView(
key: qrKey,
onQRViewCreated: _onQRViewCreated,
overlay: QrScannerOverlayShape(
borderColor: Colors.cyanAccent,
borderRadius: 10,
borderLength: 30,
borderWidth: 10,
cutOutSize: scanArea),
onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p),
);
}
void _onQRViewCreated(QRViewController controller) {
setState(() {
this.controller = controller;
});
controller.scannedDataStream.listen((scanData) {
controller.pauseCamera();
setState(() {
result = scanData;
});
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondView(res: result!.code)))
.then((value) => controller.resumeCamera());
});
}
void _onPermissionSet(BuildContext context, QRViewController ctrl, bool p) {
log('${DateTime.now().toIso8601String()}_onPermissionSet $p');
if (!p) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('no Permission')),
);
}
}
#override
void dispose() {
controller?.dispose();
super.dispose();
}
}
How am I supposed to pass a value in this big mess called Flutter?
With state management tools like InheritedWidget, InheritedModel, Provider, BloC and many more.
30 years old php global $var wasn't good? All these years were to come up with setState, passed in a controller which get redeclared as a key inside a stateful widget that receive the value from a Navigator?
Well, you shouldn't do that and it's not meant to be done like that. We can use several methods to propagate data down the widget tree. Let me explain this with InheritedWidget. But sometimes you want to go for Provider which is a wrapper class for InheritedWidget.
First we create a class named QRListModel which extends InheritedModel:
class QRListModel extends InheritedWidget {
final List<Barcode> qrList = []; // <- This holds our data
QRListModel({required super.child});
#override
bool updateShouldNotify(QRListModel oldWidget) {
return !listEquals(oldWidget.qrList, qrList);
}
static QRListModel of(BuildContext context) {
final QRListModel? result = context.dependOnInheritedWidgetOfExactType<QRListModel>();
assert(result != null, 'No QRListModel found in context');
return result!;
}
}
updateShouldNotify is a method we have to override to tell Flutter, when we want the widgets to rebuild. We want this to happen when the list changes. The of method is just a handy way to access the QRListModel.
Now wrap a parent widget of both the scan tab view and the list tab view inside QRListModel. We go for HomeView:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter App',
theme: ThemeData(
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: QRListModel(child: HomeView()), // <- here!
);
}
}
We can take any parent widget but it should be a class where we don't call setState. Otherwise our QRListModel also gets rebuilt and our list is gone.
Now we can access QRListModel from anywhere inside the subtree. We need it here:
void _onQRViewCreated(QRViewController controller) {
setState(() {
this.controller = controller;
this.controller!.resumeCamera();
});
controller.scannedDataStream.listen((scanData) async {
controller.pauseCamera();
QRListModel.of(context).qrList.add(scanData); // <- Here we access the list
await showDialog(
context: context,
builder: (context) => SimpleDialog(
title: Text("Barcode was added!"),
children: [
Text(scanData.code!)
],
)
);
});
}
And here we read the list:
class SecondView extends StatelessWidget {
const SecondView({Key? key, required this.res}) : super(key: key);
final String? res;
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: QRListModel.of(context).qrList.length,
itemBuilder: (context, index) {
return Card(
child: ListTile(
title: Text(QRListModel.of(context).qrList[index].code ?? "NO"),
),
);
}
);
}
}
Now both pages have access to the qr list. Please do mind that a InheritedWidget can only have final fields. So if you need mutable fields, you need an additional wrapper class. We don't need it as we don't change the list but only its elements.
By the way: You shouldn't call setState inside initState. You did this here:
class _HomeViewState extends State<HomeView> {
final tabs = [QRViewExample(), SecondView(res: '')];
int _currentIndex = 0;
#override
void initState() {
setState(() {}); // <- Don't call setState inside initState!
super.initState();
}
i have a Widget that has an AppBar with a progress bar, and a PageView with 4 pages, when moving between pages i am increasing / decreasing the progress bar.
I'm trying to do all the logic in my ViewModel.
This is my ViewModel (omitted non relevant stuff):
class RegisterViewModel extends BaseViewModel with RegisterViewModelInputs, RegisterViewModelOutputs {
final StreamController _sexStreamController = StreamController<int>.broadcast();
final StreamController _progressBarController = StreamController<double>.broadcast();
final StreamController _currentIndexController = StreamController<int>.broadcast();
final StreamController _isBackEnabled = StreamController<bool>.broadcast();
double _progress = 0.25;
int _index = 0;
#override
setCurrentIndex(int index) {
currentIndex.add(index);
}
#override
increaseProgress() {
if (_progress <= 1.0) {
_progress += 0.25;
progress.add(_progress);
}
}
#override
decreaseProgress() {
if (_progress > 0) {
_progress -= 0.25;
progress.add(_progress);
}
}
#override
setIsBackEnabled(int index) {
_isBackEnabled.add(index > 0 ? true : false);
}
#override
nextPage() {
if (_index < 4) {
_index++;
increaseProgress();
setCurrentIndex(_index);
}
}
#override
previousPage() {
if (_index > 0) {
_index--;
decreaseProgress();
setCurrentIndex(_index);
}
}
#override
Sink get currentIndex => _currentIndexController.sink;
#override
Sink get progress => _progressBarController.sink;
#override
Sink get isBackEnabled => _isBackEnabled.sink;
#override
Stream<int> get outputCurrentIndex => _currentIndexController.stream.map((currentIndex) => currentIndex);
#override
Stream<double> get outputProgress => _progressBarController.stream.map((progress) => progress);
#override
Stream<bool> get outputIsBackEnabled => outputIsBackEnabled.map((isEnabled) => isEnabled);
}
And here is my View:
class RegisterView extends StatefulWidget {
const RegisterView({Key? key}) : super(key: key);
#override
_RegisterViewState createState() => _RegisterViewState();
}
class _RegisterViewState extends State<RegisterView> {
final RegisterViewModel _viewModel = getIt<RegisterViewModel>();
final PageController _pageController = PageController(initialPage: 0);
final FixedExtentScrollController _weightScrollController = FixedExtentScrollController(initialItem: 80);
final FixedExtentScrollController _ageScrollController = FixedExtentScrollController(initialItem: 13);
final FixedExtentScrollController _heightScrollController = FixedExtentScrollController(initialItem: 13);
#override
void initState() {
_bind();
super.initState();
}
#override
void dispose() {
_viewModel.dispose();
super.dispose();
}
_bind() {
_viewModel.start();
}
#override
Widget build(BuildContext context) {
_viewModel.outputCurrentIndex.listen((index) {
_pageController.animateToPage(index, duration: const Duration(milliseconds: 1000), curve: Curves.ease);
});
List<Widget> pagesList = [
SexPage(
onConfirm: (sex) {
_viewModel.setSex(sex);
_viewModel.nextPage();
},
),
AgePage(
scrollController: _ageScrollController,
),
WeightPage(scrollController: _weightScrollController),
HeightPage(scrollController: _heightScrollController),
];
return Scaffold(
backgroundColor: ColorManager.backgroundColor,
appBar: AppBar(
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: ColorManager.backgroundColor,
statusBarBrightness: Brightness.dark,
statusBarIconBrightness: Brightness.dark,
),
centerTitle: true,
title: AppBarWidget(_pageController),
elevation: AppSize.s0,
),
body: PageView(
reverse: true,
controller: _pageController,
physics: NeverScrollableScrollPhysics(),
children: [...pagesList],
),
);
}
}
class AppBarWidget extends StatelessWidget {
final PageController pageController;
final RegisterViewModel _viewModel = getIt<RegisterViewModel>();
AppBarWidget(
this.pageController, {
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
_viewModel.outputCurrentIndex.listen((index) {
pageController.animateToPage(index, duration: const Duration(milliseconds: 1000), curve: Curves.ease);
});
return Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
flex: 1,
child: InkWell(
child: Text(
AppStrings.skip,
style: Theme.of(context).textTheme.labelMedium,
),
onTap: () => _viewModel.nextPage(),
),
),
Expanded(
flex: 4,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: AppPadding.p60),
child: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(pi),
child: StreamBuilder<double>(
stream: _viewModel.outputProgress,
builder: (context, snapshot) {
return Progresso(
progress: snapshot.data ?? 0,
progressStrokeCap: StrokeCap.round,
backgroundStrokeCap: StrokeCap.round,
progressColor: ColorManager.primary,
backgroundColor: ColorManager.progressBarBackgroundGrey,
progressStrokeWidth: 10.0,
backgroundStrokeWidth: 10.0,
);
}),
),
),
),
StreamBuilder<int>(
stream: _viewModel.outputCurrentIndex,
builder: (context, snapshot) {
return Expanded(
flex: 1,
child: (snapshot.data ?? 0) > 0
? InkWell(
child: Row(
children: [
Text(
AppStrings.back,
style: Theme.of(context).textTheme.labelMedium,
),
Icon(
Icons.arrow_forward_ios,
color: ColorManager.subtitleGrey,
),
],
),
onTap: () => _viewModel.previousPage(),
)
: Container(),
);
}),
],
);
}
}
When i'm calling _viewModel.previousPage() & _viewModel.previousPage()` from the AppBarWidget, the progress bar view is updated, and there is a scroll animation to the next page.
But for some reason if the onConfirm callback:
onConfirm: (sex) {
_viewModel.setSex(sex);
_viewModel.nextPage();
}
is called from within SexPage, the scroll animation is working, but the progress bar view and the isBackEnabled is not updating.
I have checked and a new value is being added to the _progressBarController sink, but for some reason the StreamBuilder does not receive it? same for the isBackEnabled stream..
What am i doing wrong?
And another question i have is where should I listen to the outputCurrentIndex stream, and call _pageController.animateToPage()?
Apparently i had an issue with my Dependency Injection.
I'm using get_it and i used registerFactory, instead of registerLazySingleton.
Which probably made me have 2 separate ViewModels in each widget.
I have a short ListView of a maximum of 10 items. Each list item will contain a DropDownButton which will hold around 1K DropDownMenuItems for selections.
In native Android, I was able to implement one that performed very smoothly, but with Flutter it takes a while to build the ListView which causes the UI to freeze.
In my case, I will need to rebuild the ListView upon every change in one of its items, so It will be a major issue.
Is there a way to make the ListView build faster, or at least be able to display a ProgressBar till it builds?
N.B: Using --profile configuration to simulate a release version improves the performance a lot, but still there is a sensed freeze.
Here's my sample code which you can directly copy/paste if you want to test it yourself.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool showList = false;
final List<DropdownMenuItem<int>> selections = List.generate(
1000,
(index) => DropdownMenuItem<int>(
value: index,
child: Text("$index"),
),
);
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: Container(
width: double.infinity,
child: Column(
children: [
ElevatedButton(
child: Text("toggle list visibility"),
onPressed: () {
setState(() {
showList = !showList;
});
},
),
Expanded(
child: showList
? ListView.builder(
cacheExtent: 2000,
itemCount: 10,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Container(
height: 200,
color: Colors.green,
child: Column(
children: [
Text("List Item: $index"),
DropdownButton<int>(
onChanged: (i) {},
value: 1,
items: selections,
),
],
),
),
),
);
})
: Text("List Not Built"),
),
],
),
),
),
);
}
}
Load dropdown when clicking the button.
Add this widget on your main List View
InkWell(
onTap: () {
showDialog(
context: context,
builder: (_) {
return VendorListAlert(selectVendor: selectVendorTap);
});
},
child: // create a widget, looks like your drop down
),
Handle tap event
void selectVendorTap(pass your model){
// logic
}
Sample for custom Alert
No need to create a mutable widget, the immutable widget is better.
class VendorListAlert extends StatefulWidget {
final Function selectVendor;
const VendorListAlert({Key key, this.selectVendor}) : super(key: key);
#override
_VendorListAlertState createState() => _VendorListAlertState();
}
class _VendorListAlertState extends State<VendorListAlert> {
List<UserModel> _searchVendor = [];
#override
void initState() {
super.initState();
_searchVendor = List.from(ypModel);
}
#override
Widget build(BuildContext context) {
return AlertDialog(
content: Container(
width: width,
child: ListView.builder(
shrinkWrap: true,
itemCount: _searchVendor.length,
itemBuilder: (BuildContext context, int index) {
return Card(
child: InkWell(
onTap: () {
widget.selectVendor(_searchVendor[index]);
Navigator.pop(context);
},
child:
),
);
},
),
),
);
}
}
Question
Hi, how can I implement a custom multi selection widget in flutter? Here is an example :
I have a list of widget created with a ListView.builder and I simply want to change color based on userTap.
How can I implement this? I'm able to change color when user tap on one option but then, when another button is tapped I can't understand how to reverse the state of the old selected option.
I was searching for a solution with flutter standard state management or maybe with bloc library.
example code
class Test extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: 10,
itemBuilder: (context, index){
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: SelectableContainer(text: 'option', index: index,),
);
},
),
);
}
}
class SelectableContainer extends StatefulWidget {
final String text;
final int index;
SelectableContainer({
#required this.text,
#required this.index
});
#override
_SelectableContainerState createState() => _SelectableContainerState();
}
class _SelectableContainerState extends State<SelectableContainer> {
bool _isSelected = false;
Color unselectedColor = Colors.white;
Color selectedColor = Colors.blue;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: (){
setState(() {
_isSelected = true;
});
},
child: AnimatedContainer(
duration: Duration(milliseconds: 400),
height: 50,
color: _isSelected ? selectedColor : unselectedColor,
child: Center(
child: Text(widget.text),
),
),
);
}
}
Thanks
First store the selection in list when the user select an item
selectionList.add(title.id);
Then in the ListView.builder change the color of the title if it's in the selectionList
Title(color: selectionList.contain(listOfTitls[index].id)? Colors.green : Colors.White);
update
this trick will do it for you
return GestureDetector(
onTap: (){
setState(() {
_isSelected =_isSelected? false:true;//this line
});
},
In your setState method, you can just do _isSelected = !_isSelected; instead of always setting it to true. That way the tile will get unselected if _isSelected is set to true.
Although the above way wil solve your problem, there is also another widget flutter has for this same scenario called ListTile. Here is a link to the documentation. You might also want to check that out. It also has an onTap callback built into it directly.
Here is a quick app I built using ListTile. Hope it solves your problem. Cheers!
import 'package:flutter/material.dart';
class DemoClass extends StatefulWidget {
_DemoClassState createState() => _DemoClassState();
}
class _DemoClassState extends State<DemoClass> {
int _selectedIndex = -1;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView.builder(
itemCount: 10,
itemBuilder: (context, index) {
return AnimatedContainer(
duration: Duration(milliseconds: 300),
height: 50,
color: _selectedIndex == index ? Colors.blue : Colors.transparent,
child: ListTile(
title: Text('This is some title'),
onTap: () => setState(() {
_selectedIndex = index;
}),
),
);
}
),
);
}
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Center(
child: Container(
child: DemoClass(),
),
),
);
}
}
void main() {
runApp(MyApp());
}
I'm trying to update/rebuild a route beneath a popup window when the popup window is popped, using Navigator.pop(context). On the popup window the user can add something to a list, and the route beneath (the page), the added item shows in a ListView. So more specifically I need my ListView to be up to date with the actual list. But nothing I have tried yet, have worked.
As mentioned, I have already tried quite a few things, including:
didPopNext() (using RouteAware), changedExternalState (which I couldn't quite get the grasp of), trying to pass a function only containing a setState(() {}), and other things.
Hope somebody can help with how I can rebuild the ListView when the popup window is popped.
Thanks in advance.
This example should summarize my problem decently close (couldn't show you the actual code, because it's gotten way over 2.000 lines, but the core elements of the problem should be here):
import 'package:flutter/material.dart';
void main() => runApp(new MaterialApp(
home: MyPage()));
class PopupLayout extends ModalRoute<void> {
#override
Duration get transitionDuration => Duration(milliseconds: 300);
#override
bool get opaque => false;
#override
bool get barrierDismissible => false;
#override
bool get semanticsDismissible => true;
#override
Color get barrierColor =>
bgColor == null ? Colors.black.withOpacity(0.5) : bgColor;
#override
String get barrierLabel => null;
#override
bool get maintainState => false;
double top;
double bottom;
double left;
double right;
Color bgColor;
final Widget child;
PopupLayout(
{Key key,
this.bgColor,
#required this.child,
this.top,
this.bottom,
this.left,
this.right});
#override
Widget buildPage(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
if (top == null) this.top = 10;
if (bottom == null) this.bottom = 20;
if (left == null) this.left = 20;
if (right == null) this.right = 20;
return GestureDetector(
onTap: () {},
child: Material(
type: MaterialType.transparency,
child: _buildOverlayContent(context),
),
);
}
Widget _buildOverlayContent(BuildContext context) {
return Container(
margin: EdgeInsets.only(
bottom: this.bottom,
left: this.left,
right: this.right,
top: this.top),
child: child,
);
}
#override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return FadeTransition(
opacity: animation,
child: ScaleTransition(
scale: animation,
child: child,
),
);
}
}
class PopupContent extends StatefulWidget {
final Widget content;
PopupContent({
Key key,
this.content,
}) : super(key: key);
_PopupContentState createState() => _PopupContentState();
}
class _PopupContentState extends State<PopupContent> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Container(
child: widget.content,
);
}
}
popupContent(BuildContext context, Widget widget,
{BuildContext popupContext}) {
Navigator.push(
context,
PopupLayout(
top: 120,
left: 20,
right: 20,
bottom: 120,
child: PopupContent(
content: Scaffold(
backgroundColor: Colors.white,
resizeToAvoidBottomInset: false,
body: widget,
),
),
),
);
}
Widget popupWidget(BuildContext context) {
return new Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("This is a popupPage"),
FlatButton(
onPressed: () {
list.add("Irrelevant");
Navigator.pop(context); //The rebuild of the page underneath, needs to be called when this pops
},
child: Text("Press me to change text on home page"),
),
]
),
);
}
List list = [];
class MyPage extends StatefulWidget {
#override
createState() => MyPageState();
}
class MyPageState extends State<MyPage> {
#override
Widget build(BuildContext context) {
return new Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: Container(
width: 300,
height: 400,
child: ListView.builder(
itemCount: list.length,
itemBuilder: (BuildContext context, int index) {
return Center(
child: Text(index.toString()),
);
},
),
),
),
Center(
child: FlatButton(
onPressed: () {
popupContent(context, popupWidget(context));
},
child: Text("Press me for popup window"),
),
),
Center(
child: FlatButton(
onPressed: () {
setState(() {});
},
child: Text("Press me to rebuild page (setState)"),
),
),
]
),
);
}
}
You can add a function in MyPage and pass it to your popUpWidget. Code:
import 'package:flutter/material.dart';
void main() => runApp(new MaterialApp(home: MyPage()));
class PopupLayout extends ModalRoute<void> {
#override
Duration get transitionDuration => Duration(milliseconds: 300);
#override
bool get opaque => false;
#override
bool get barrierDismissible => false;
#override
bool get semanticsDismissible => true;
#override
Color get barrierColor =>
bgColor == null ? Colors.black.withOpacity(0.5) : bgColor;
#override
String get barrierLabel => null;
#override
bool get maintainState => false;
double top;
double bottom;
double left;
double right;
Color bgColor;
final Widget child;
PopupLayout(
{Key key,
this.bgColor,
#required this.child,
this.top,
this.bottom,
this.left,
this.right});
#override
Widget buildPage(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
if (top == null) this.top = 10;
if (bottom == null) this.bottom = 20;
if (left == null) this.left = 20;
if (right == null) this.right = 20;
return GestureDetector(
onTap: () {},
child: Material(
type: MaterialType.transparency,
child: _buildOverlayContent(context),
),
);
}
Widget _buildOverlayContent(BuildContext context) {
return Container(
margin: EdgeInsets.only(
bottom: this.bottom,
left: this.left,
right: this.right,
top: this.top),
child: child,
);
}
#override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return FadeTransition(
opacity: animation,
child: ScaleTransition(
scale: animation,
child: child,
),
);
}
}
class PopupContent extends StatefulWidget {
final Widget content;
PopupContent({
Key key,
this.content,
}) : super(key: key);
_PopupContentState createState() => _PopupContentState();
}
class _PopupContentState extends State<PopupContent> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Container(
child: widget.content,
);
}
}
popupContent(BuildContext context, Widget widget, {BuildContext popupContext}) {
Navigator.push(
context,
PopupLayout(
top: 120,
left: 20,
right: 20,
bottom: 120,
child: PopupContent(
content: Scaffold(
backgroundColor: Colors.white,
resizeToAvoidBottomInset: false,
body: widget,
),
),
),
);
}
Widget popupWidget(BuildContext context, Function callback) {
//Pass The Function Here //////////////////////
return new Container(
child:
Column(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
Text("This is a popupPage"),
FlatButton(
onPressed: () {
list.add("Irrelevant");
callback(); // Call It Here /////////////////////////
Navigator.pop(
context); //The rebuild of the page underneath, needs to be called when this pops
},
child: Text("Press me to change text on home page"),
),
]),
);
}
List list = [];
class MyPage extends StatefulWidget {
#override
createState() => MyPageState();
}
class MyPageState extends State<MyPage> {
//The Calback Function //////////////////////////
void callback() {
setState(() {});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: Container(
width: 300,
height: 400,
child: ListView.builder(
itemCount: list.length,
itemBuilder: (BuildContext context, int index) {
return Center(
child: Text(index.toString()),
);
},
),
),
),
Center(
child: FlatButton(
onPressed: () {
popupContent(context, popupWidget(context, this.callback)); //Passing the Function here ////////////
},
child: Text("Press me for popup window"),
),
),
Center(
child: FlatButton(
onPressed: () {
setState(() {});
},
child: Text("Press me to rebuild page (setState)"),
),
),
]),
);
}
}