How to show PhotoViewGallery class index value in appBar - flutter

I'm using photo_view_gallery class and I would like to show dynamically the value of current image index of PhotoViewGallery.builder into the appBar.
I'm new in Flutter, I googled a lot but I can't found any solution for this
body: PhotoViewGallery.builder(
itemCount: listaPagine.length,
builder: (context, index) {
saveIndex(index);
String myImg =
'http://www.attilofficina.altervista.org/phpbackend/JOB/000004/fullsize/' +
listaPagine[index].nomefile.toString() +
'.jpg';
return PhotoViewGalleryPageOptions(
imageProvider: AdvancedNetworkImage(myImg,
retryLimit: 1, timeoutDuration: Duration(seconds: 30)),
);
},
),
I also try a function that save index to another variable, but the value is still unavailable in appBar
Here the code and position of this function (in appBar is shown null)
class GalleryPageState extends State<GalleryPage> {
int curr;
...
#override
Widget build(BuildContext context) {
...
saveIndex(int index) {
int curr = index;
print('*** curr = ' + curr.toString()); /// PRINTS CORRECT VALUE
return curr;
}
...
return Scaffold(
appBar: AppBar(
title: Text(curr.toString(), /// BUT IT SHOWS NULL
),
),
body: PhotoViewGallery.builder(
itemCount: listaPagine.length,
builder: (context, index) {
salvaIndex(index);
String myImg =
'http://www.attilofficina.altervista.org/phpbackend/JOB/000004/fullsize/' +
listaPagine[index].nomefile.toString() +
'.jpg';
return PhotoViewGalleryPageOptions(
imageProvider: AdvancedNetworkImage(myImg,
retryLimit: 1, timeoutDuration: Duration(seconds: 30)),
);
},
),
);
}
}
Can someone help me?
special thanks mario

I think you can use onPageChanged
import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _currentIndex = 0;
final List<String> listaPagine = [
'https://picsum.photos/id/451/200/300',
'https://picsum.photos/id/200/200/300'
];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('# $_currentIndex'),
),
body: PhotoViewGallery.builder(
itemCount: listaPagine.length,
builder: (BuildContext context, int index) {
String myImg = listaPagine[index];
return PhotoViewGalleryPageOptions(
imageProvider: NetworkImage(myImg),
);
},
onPageChanged: (int index) {
setState(() {
_currentIndex = index;
});
},
),
);
}
}

There's three things wrong in your following code:
saveIndex(int index) {
int curr = index;
print('*** curr = ' + curr.toString()); /// PRINTS CORRECT VALUE
return curr;
}
Here you are creating new int curr variable which should be curr to use the existing global scope variable.
You should create the saveIndex method outside of build function.
Also you should update the variable in setState like
setState((){
curr=index;
});
This will update the variable and recreate the widget tree with AppBar with updated values.
Also you don't need the return statement in saveIndex
Editing for #attila's help
The basic about the state is, it just keep the current state or values in memory. You can always change any value & it will be updated in the state but it will not reflected on the UI. Because your application or view still don't know that some value is updated & it needs to re-render the UI.
That's where the setState come in picture. What it does is while updating the value it also call the build function so new UI with updated values could be re-rendered on screen.
To test it, you can just update the values & call setState((){}); after that. It will still work as same.
Hope this helps you.

Related

Switching from ReorderableListView to ListView breaks Interaction of rows

I have a view which switches between a ListView and a ReorderableListView.
Widget _buildList(
BuildContext context,
List<ExerciseTemplate> exerciseTemplates,
EditWorkoutModel dao,
) {
if (_isInEditingMode) {
return ReorderableListView(
key: ObjectKey('reordeableListView'),
onReorder: ((oldIndex, newIndex) {
dao.reorderIndexes(
oldIndex,
(oldIndex < newIndex) ? newIndex - 1 : newIndex,
);
}),
padding: const EdgeInsets.only(bottom: 120),
children: [
for (var exerciseTemplate in exerciseTemplates)
Provider(
key: ObjectKey('${exerciseTemplate.id}_compactExerciseTemplateRow_provider'),
create: (context) => EditExerciseModel(exerciseTemplate),
child: CompactExerciseTemplateRow(
key: ObjectKey('${exerciseTemplate.id}_compactExerciseTemplateRow'),
),
),
],
);
} else {
return ListView.builder(
key: ObjectKey('listView'),
itemCount: exerciseTemplates.length,
padding: const EdgeInsets.only(bottom: 120),
itemBuilder: (BuildContext context, int index) {
final exerciseTemplate = exerciseTemplates[index];
return Provider(
// Key is needed here to properly handle deleted rows in the ui.
// Without the key, deleted rows are being shown.
key: ObjectKey(
'${exerciseTemplate.id}_exerciseTemplateRow_provider'),
create: (context) => EditExerciseModel(exerciseTemplate),
child: ExerciseTemplateRow(
key: ObjectKey('${exerciseTemplate.id}_exerciseTemplateRow'),
onDelete: () async {
await dao.deleteExercise(exerciseTemplate);
return true;
},
),
);
},
);
}
}
Both lists show the same data, but tapping a button, switches to a ReorderableListView which shows the data with different widgets. Tapping the button again switches back to the ListView.
However, switching forth and back results that I am not able to interact with elements within the row of the ListView. This issue appeared after I added a globalKey for each element in the ListView. I need this key, to properly handle deleting rows, so I can not just remove the key again.
How can I make it work, that I can interact with widgets within the row after I switched to the ReorderableListView and back to the ListView?
Copied from Provider document:
https://pub.dev/packages/provider
DON'T create your object from variables that can change over time.
In such a situation, your object would never update when the value changes.
int count;
Provider(
create: (_) => MyModel(count),
child: ...
)
If you want to pass variables that can change over time to your object, consider using ProxyProvider:
int count;
ProxyProvider0(
update: (_, __) => MyModel(count),
child: ...
)
It's ok to use Global key and switch between ListView and ReorderableListView, see example below:
https://dartpad.dev/?id=fd39a89b67448d86e682dd2c5ec77453
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool reOrder = false;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(reOrder ? "ReoderableListView" : "ListView"),
),
floatingActionButton: FloatingActionButton(onPressed: () {
setState(() {
reOrder = !reOrder;
});
}),
body: MyListView(reOrder));
}
}
final data = List.generate(10, (index) => {"title": 'item $index', "value": false});
class MyListView extends StatefulWidget {
final bool reOrder;
const MyListView(this.reOrder, {super.key});
#override
State<MyListView> createState() => _MyListViewState();
}
class _MyListViewState extends State<MyListView> {
#override
Widget build(BuildContext context) {
if (widget.reOrder) {
return ReorderableListView(
key: const ObjectKey('reordeableListView'),
onReorder: (int oldIndex, int newIndex) {
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final item = data.removeAt(oldIndex);
data.insert(newIndex, item);
});
},
children: [
for (var item in data)
ListTile(
key: ObjectKey('${item["title"]}_compactExerciseTemplateRow_provider'),
title: Text(item["title"] as String),
trailing: Text((item["value"] as bool).toString()),
),
],
);
} else {
return ListView.builder(
key: const ObjectKey('listView'),
itemCount: data.length,
padding: const EdgeInsets.only(bottom: 120),
itemBuilder: (BuildContext context, int index) {
return CheckboxListTile(
key: ObjectKey('${data[index]["title"]}_exerciseTemplateRow_provider'),
title: Text(data[index]["title"] as String),
value: (data[index]["value"] as bool),
onChanged: (bool? value) {
setState(() => data[index]["value"] = value!);
},
);
},
);
}
}
}
So the issue was that I was using ObjectKey instead of ValueKey.
The difference between those two is that ObjectKey checks if the identity (the instance) is the same. ValueKey checks the underlying value with the == operator.
My guess here is that by using ObjectKey in my case, flutter is not able to properly replace the old widget with the new one, since the new widget always have a different key. By using ValueKey flutter can distinguish between old and new widgets. Widgets will be in my case replaced after I switch between the lists, because the row widget won't be visible and therefor disposed.
Because the widgets were not properly replaced, somehow the old widgets are still being rendered, but all gesture listeners were already disposed. Therefor no interaction was possible anymore.
These are just my assumption, let me know if I am completely wrong here.

Is there a way I can get flutter to rebuild a gridview every time a flatbutton is pressed?

I am developing a maze app on flutter. I have a grid of 100 flatButtons that all start as grey. When a flatButton is pressed, I want it to turn green if the move is legitimate and red if the move is illegitimate (I used the splashColor property for this). Moves are constantly changing from illegitimate to legitimate (based on where the user currently is) and the splashColor of all the buttons should be dynamically updating. Right now, only the splashColor of a button only changes if I click it. I want the entire gridView of buttons to update their splashColor property whenever any button is pressed. Code is attached. Any help is much appreciated!
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'dart:math';
import 'package:flutter/scheduler.dart' show timeDilation;
//lastmove initially set to 1 for simplicity.
int lastMove=1;
class GameButton extends StatefulWidget {
final int id;
int onPath=0;
bool testOnPath(){
if (this.onPath==1){
return true;
}
else {
return false;
}
}
bool testIfLegal(lastMove){
return (((this.id-lastMove).abs()==1) ^ ((this.id-lastMove).abs()==10));
}
bool moveCheck(){
if(this.testIfLegal(lastMove) & this.testOnPath()){
return true;
}
else{
return false;
}
}
GameButton(this.id, lastMove);
#override
_GameButtonState createState() => _GameButtonState();
}
class _GameButtonState extends State<GameButton> {
#override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(border: Border.all(color: Colors.black)),
child: FlatButton(
color:Colors.grey,
splashColor: widget.moveCheck()?Colors.green:Colors.red,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onPressed: () {
bool a=widget.moveCheck();
if(a){
setState(() {
lastMove=widget.id;
print("GOOD MOVE");
}
);
}
else{
print("Illegitmate Move");
}
},
),
);
}
}
class Maze extends StatefulWidget {
List<GameButton> empty_grid = [for(var i=0; i<100; i++) new GameButton(i,lastMove)];
#override
_MazeState createState() => _MazeState();
}
class _MazeState extends State<Maze> {
#override
Widget build(BuildContext context) {
}
}
void main() {
//slow down animation to give user more time
timeDilation = 3.0;
Maze maze1 = new Maze();
//filling maze manually-will be done with function in future
(maze1.empty_grid[0]).onPath = 1;
(maze1.empty_grid[10]).onPath = 1;
(maze1.empty_grid[20]).onPath = 1;
(maze1.empty_grid[30]).onPath = 1;
(maze1.empty_grid[40]).onPath = 1;
(maze1.empty_grid[50]).onPath = 1;
(maze1.empty_grid[60]).onPath = 1;
(maze1.empty_grid[70]).onPath = 1;
(maze1.empty_grid[80]).onPath = 1;
(maze1.empty_grid[90]).onPath = 1;
(maze1.empty_grid[91]).onPath = 1;
(maze1.empty_grid[92]).onPath = 1;
(maze1.empty_grid[93]).onPath = 1;
(maze1.empty_grid[94]).onPath = 1;
(maze1.empty_grid[95]).onPath = 1;
(maze1.empty_grid[96]).onPath = 1;
(maze1.empty_grid[97]).onPath = 1;
(maze1.empty_grid[98]).onPath = 1;
(maze1.empty_grid[99]).onPath = 1;
//done filling maze1
return runApp(
MaterialApp(
home: Scaffold(
backgroundColor: Colors.blue,
appBar: AppBar(
title: Text('Gorton Maze Test'),
backgroundColor: Colors.blueAccent,
),
body: Center(
child: GridView.builder(
itemCount: 100,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 10, crossAxisSpacing: 0, mainAxisSpacing: 0),
shrinkWrap: true,
itemBuilder: (BuildContext context, int index){
return maze1.empty_grid[index];
},
)
),
),
)
);
}
Yes. You can update entire GridView if you wrap by StreamController.
On the other hand, I don't think do everything in main class is a great idea. You should split class and function for easier. It'll make you code cleaner.
body: HomePage()),
And in HomePage, I using stateful widget for more reasons.
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
StreamController _controller;
#override
void initState() {
super.initState();
_controller = new StreamController();
}
#override
void dispose() {
_controller.close();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Center(
child: StreamBuilder(
stream: _controller.stream,
builder: (_, __) {
return GridView.builder(
itemCount: 100,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 10,
crossAxisSpacing: 0,
mainAxisSpacing: 0),
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
return maze1.empty_grid[index];
},
);
}));
}
}
And every when you want to update your gridview. You can add StreamController with new value
_controller.add(null);
Hope this idea will help you.
With your existing design, you can achieve it by having a callback defined in GameButton that points to a method in the Maze class. Each time the button is pressed after changing the color of the button you call the callback into Maze class where you change the grid color as well. Sudo code:
class GameButton extends StatefulWidget {
final VoidCallback clickCallback
GameButton(this.id, lastMove, this.clickCallback);
.....
}
class Maze extends StatefulWidget {
List<GameButton> empty_grid = [for(var i=0; i<100; i++) new GameButton(i,lastMove, onButtonPressed)];
#override
_MazeState createState() => _MazeState();
void onButtonPressed(){
// change grid color based on index
}
}
class _MazeState extends State<Maze> {
#override
Widget build(BuildContext context) {
}
}
You can do it by calling the setstate method.
while clicking on the button after writing your logic use the setstate method. Basically setstate will rebuild the screen. so you can do something like this.

How to properly initialize a Future in Flutter Provider

so I am trying to build up a list in my provider from a Future Call.
So far, I have the following ChangeNotifier class below:
class MainProvider extends ChangeNotifier {
List<dynamic> _list = <dynamic>[];
List<dynamic> get list => _list;
int count = 0;
MainProvider() {
initList();
}
initList() async {
var db = new DatabaseHelper();
addToList(Consumer<MainProvider>(
builder: (_, provider, __) => Text(provider.count.toString())));
await db.readFromDatabase(1).then((result) {
result.forEach((item) {
ModeItem _modelItem= ModeItem.map(item);
addToList(_modelItem);
});
});
}
addToList(Object object) {
_list.add(object);
notifyListeners();
}
addCount() {
count += 1;
notifyListeners();
}
}
However, this is what happens whenever I use the list value:
I can confirm that my initList function is executing properly
The initial content from the list value that is available is the
Text() widget that I firstly inserted through the addToList function, meaning it appears that there is only one item in the list at this point
When I perform Hot Reload, the rest of the contents of the list seems to appear now
Notes:
I use the value of list in a AnimatedList widget, so I am
supposed to show the contents of list
What appears initially is that the content of my list value is only one item
My list value doesn't seem to automatically update during the
execution of my Future call
However, when I try to call the addCount function, it normally
updates the value of count without needing to perform Hot Reload -
this one seems to function properly
It appears that the Future call is not properly updating the
contents of my list value
My actual concern is that on initial loading, my list value doesn't
properly initialize all it's values as intended
Hoping you guys can help me on this one. Thank you.
UPDATE: Below shows how I use the ChangeNotifierClass above
class ParentProvider extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<MainProvider>(
create: (context) => MainProvider(),
),
],
child: ParentWidget(),
);
}
}
class ParentWidget extends StatelessWidget {
final GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
#override
Widget build(BuildContext context) {
var mainProvider = Provider.of<MainProvider>(context);
buildItem(BuildContext context, int index, Animation animation) {
print('buildItem');
var _object = mainProvider.list[index];
var _widget;
if (_object is Widget) {
_widget = _object;
} else if (_object is ModelItem) {
_widget = Text(_object.unitNumber.toString());
}
return SizeTransition(
key: ValueKey<int>(index),
axis: Axis.vertical,
sizeFactor: animation,
child: InkWell(
onTap: () {
listKey.currentState.removeItem(index,
(context, animation) => buildItem(context, index, animation),
duration: const Duration(milliseconds: 300));
mainProvider.list.removeAt(index);
mainProvider.addCount();
},
child: Card(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: _widget,
),
),
),
);
}
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.white,
child: Padding(
padding: const EdgeInsets.all(32.0),
child: mainProvider.list == null
? Container()
: AnimatedList(
key: listKey,
initialItemCount: mainProvider.list.length,
itemBuilder:
(BuildContext context, int index, Animation animation) =>
buildItem(context, index, animation),
),
),
),
);
}
}
You are retrieving your provider from a StatelessWidget. As such, the ChangeNotifier can't trigger your widget to rebuild because there is no state to rebuild. You have to either convert ParentWidget to be a StatefulWidget or you need to get your provider using Consumer instead of Provider.of:
class ParentWidget extends StatelessWidget {
final GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
#override
Widget build(BuildContext context) {
return Consumer<MainProvider>(
builder: (BuildContext context, MainProvider mainProvider, _) {
...
}
);
}
As an aside, the way you are using provider is to add the MainProvider to its provider and then retrieve it from within its immediate child. If this is the only place you are retrieving the MainProvider, this makes the provider pattern redundant as you can easily just declare it within ParentWidget, or even just get your list of images using a FutureBuilder. Using provider is a good step toward proper state management, but also be careful of over-engineering your app.

Allow distinction between providers

I am building an app with flutter and the provider pattern. I have particular one ViewModel, that gets provided with Provider.of<AddressBookModel>(context).
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<AddressBookViewModel>(
builder:(_) => AddressBookViewModel(),
child: Scaffold(
body: _getBody(context);
}
Widget _getBody(BuildContext context) {
AddressBookViewModel vm = Provider.of<AddressBookViewModel>(context);
// AddressBookViewModel holds a list of contact objects
// (id, name, street, starred etc.)
List<Contact> contacts = vm.contacts;
return ListView.builder(
itemCount: contacts.length,
itemBuilder: (context, index) => ListTile(
title: Text(contacts[index].name),
trailing: contacts[index].starred
? Icon(Icons.star))
: null,
/**
* Changing one object rebuilds and redraws the whole list
*/
onLongPress: () => vm.toggleStarred(contacts[index]);
));
}
}
And the respective ViewModel
class AddressBookViewModel with ChangeNotifier {
final List<Contact> contacts;
AddressBookViewModel({this.contacts = []});
void toggleStarred(Contact contact) {
int index = contacts.indexOf(contact);
// the contact object is immutable
contacts[index] = contact.copy(starred: !contact.starred);
notifyListeners();
}
}
The problem I am facing is, once I am changing one contact object in the list with toggleStarred(),
the provider is rebuilding and redrawing the whole list. This is not necessary in my opinion, as only
the one entry needs to be rebuild. Is there any way to have a provider that is only responsible
for one list item? Or any other way to solve this problem?
When working with lists, you'll want to have a "provider" for each item of your list and extract the list item into a constant – especially if the data associated to your item is immutable.
Instead of:
final contactController = Provider.of<ContactController>(context);
return ListView.builder(
itemCount: contactController.contacts.length,
builder: (_, index) {
reurn Text(contactController.contacts[index].name);
}
)
Prefer:
final contactController = Provider.of<ContactController>(context);
return ListView.builder(
itemCount: contactController.contacts.length,
builder: (_, index) {
reurn Provider.value(
value: contactController.contacts[index],
child: const ContactItem(),
);
}
)
Where ContactItem is a StatelessWidget that typically look like so:
class ContactItem extends StatelessWidget {
const ContactItem({Key key}): super(key: key);
#override
Widget build(BuildContext context) {
return Text(Provider.of<Contact>(context).name);
}
}
Note : full code available on the end
Step 1 : extend Contact class with ChangeNotifier class
class Contact with ChangeNotifier { }
Step 2 : remove final form starred field
bool starred;
Step 3 : move toggleStarred method form AddressBookViewModel class to Contact class
void toggleStarred() {
starred = !starred;
notifyListeners();
}
Steps[1,2,3] Code Changes Review :
class Contact with ChangeNotifier {
final String name;
bool starred;
Contact(this.name, this.starred);
void toggleStarred() {
starred = !starred;
notifyListeners();
}
}
Step 4 : move ListTile to sprate StatelessWidget called ContactView
class ContactView extends StatelessWidget {
Widget build(BuildContext context) {
return ListTile();
}
}
Step 5 : Change ListView itemBuilder method
(context, index) {
return ChangeNotifierProvider.value(
value: contacts[index],
child: ContactView(),
);
Step 6 : on the new StatelessWidget ContactView get Contact using Provider
final contact = Provider.of<Contact>(context);
Step 7 :change onLongPress to use the new toggleStarred
onLongPress: () => contact.toggleStarred(),
Steps[4,6,7] Code Changes Review :
class ContactView extends StatelessWidget {
#override
Widget build(BuildContext context) {
final contact = Provider.of<Contact>(context);
print("building ListTile item with contact " + contact.name);
return ListTile(
title: Text(contact.name),
trailing: contact.starred ? Icon(Icons.star) : null,
onLongPress: () => contact.toggleStarred(),
);
}
}
Steps[5] Code Changes Review :
return ListView.builder(
itemCount: contacts.length,
itemBuilder: (context, index) {
print("building ListView item with index $index");
return ChangeNotifierProvider.value(
value: contacts[index],
child: ContactView(),
);
},
);
Full Code
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
ChangeNotifierProvider<AddressBookViewModel>(
builder: (context) => AddressBookViewModel(),
child: HomeScreen(),
),
);
}
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<AddressBookViewModel>(
builder: (context) => AddressBookViewModel(),
child: MaterialApp(
home: Scaffold(
body: _getBody(context),
),
),
);
}
Widget _getBody(BuildContext context) {
AddressBookViewModel vm = Provider.of<AddressBookViewModel>(context);
final contacts = vm.contacts;
return ListView.builder(
itemCount: contacts.length,
itemBuilder: (context, index) {
print("building ListView item with index $index");
return ChangeNotifierProvider.value(
value: contacts[index],
child: ContactView(),
);
},
);
}
}
// product_item.dart
class ContactView extends StatelessWidget {
#override
Widget build(BuildContext context) {
final contact = Provider.of<Contact>(context);
print("building ListTile item with contact " + contact.name);
return ListTile(
title: Text(contact.name),
trailing: contact.starred ? Icon(Icons.star) : null,
onLongPress: () => contact.toggleStarred(),
);
}
}
class AddressBookViewModel with ChangeNotifier {
final contacts = [
Contact("Contact A", false),
Contact("Contact B", false),
Contact("Contact C", false),
Contact("Contact D", false),
];
void addcontacts(Contact contact) {
contacts.add(contact);
notifyListeners();
}
}
class Contact with ChangeNotifier {
final String name;
bool starred;
Contact(this.name, this.starred);
void toggleStarred() {
starred = !starred;
notifyListeners();
}
}
Ref :
Simple app state management - Flutter
[Question] Nested Providers and Lists · Issue #151 · rrousselGit/provider

Dynamically build a Flutter PageView

I need some suggestions on how to use the PageView. I have an online project that allows users to build custom forms, complete with conditions that hide or show questions depending on the answers in other questions. The mobile project allows users to fill out these forms. I've been playing with the PageView, which works for this, but I'm struggling to figure out how to indicate the end to the PageView. Right now, it will allow scrolling to continue forever.
return PageView.builder(
controller: _controller,
onPageChanged: (int index) {
FocusScope.of(context).requestFocus(FocusNode());
},
itemBuilder: (BuildContext context, int index) {
if (index >= _form.controls.length) {
print("Returning null");
return null;
}
return FormControlFactory.createFormControl(
_form.controls[index], null);
},
);
Since I'm not sure until the end of the form how many elements how do end the scrolling?
Update: In my example, I try returning null, but it still scrolls past the end.
Update: Here is where I'm currently at:
class _FormViewerControllerState extends State<FormViewerController> {
int _currentIndex = 0;
List<FormGroupController> _groups = List();
List<StreamSubscription> _subscriptions = List();
Map<int, FormControlController> _controllerMap = Map();
bool _hasVisibilityChanges = false;
#override
void initState() {
super.initState();
for (var i = 0; i < widget.form.controls.length; i++) {
var control = widget.form.controls[i];
if (control.component == ControlType.header) {
_groups.add(FormGroupController(
form: widget.form,
formResponses: widget.responses,
headerIndex: i));
}
}
_controllerMap[_currentIndex] = _getControl(_currentIndex);
_subscriptions.add(FormsEventBus()
.on<FormControlVisibilityChanging>()
.listen(_onControlVisibilityChanging));
_subscriptions.add(FormsEventBus()
.on<FormControlVisibilityChanged>()
.listen(_onControlVisibilityChanged));
}
#override
Widget build(BuildContext context) {
print("Building pageview, current index: $_currentIndex");
return PageView.builder(
controller: PageController(
initialPage: _currentIndex,
keepPage: true,
),
onPageChanged: (int index) {
print("Page changed: $index");
_currentIndex = index;
FocusScope.of(context).requestFocus(FocusNode());
},
itemBuilder: (BuildContext context, int index) {
print("Building $index");
_controllerMap[index] = _getControl(index);
return _controllerMap[index].widget;
},
itemCount: _groups
.map((g) => g.visibleControls)
.reduce((curr, next) => curr + next),
);
}
#override
void dispose() {
_subscriptions.forEach((sub) => sub.cancel());
_groups.forEach((g) => g.dispose());
super.dispose();
}
FormControlController _getControl(int index) {
for (var group in _groups) {
// We want to reduce the index so it can be local to group
if (index >= group.visibleControls) {
index -= group.visibleControls;
continue;
}
for (var instance in group.instances) {
// We want to reduce the index so it can be local to the instance
if (index >= instance.visibleControls) {
index -= instance.visibleControls;
continue;
}
return instance.controls.where((c) => c.visible).elementAt(index);
}
}
throw StateError("Weird, the current control doesn't exist");
}
int _getControlIndex(FormControlController control) {
var index = 0;
for (var group in _groups) {
if (control.groupInstance.group.groupId != group.groupId) {
index += group.visibleControls;
continue;
}
for (var instance in group.instances) {
if (control.groupInstance.groupInstanceId != instance.groupInstanceId) {
index += instance.visibleControls;
continue;
}
for (var c in instance.controls.where((c) => c.visible)) {
if (c.control.id != control.control.id) {
index++;
continue;
}
return index;
}
}
}
throw StateError("Weird, can't find the control's index");
}
_onControlVisibilityChanging(FormControlVisibilityChanging notification) {
_hasVisibilityChanges = true;
}
_onControlVisibilityChanged(FormControlVisibilityChanged notification) {
if (!_hasVisibilityChanges) {
return;
}
setState(() {
print("Setting state");
var currentControl = _controllerMap[_currentIndex];
_controllerMap.clear();
_currentIndex = _getControlIndex(currentControl);
_controllerMap[_currentIndex] = currentControl;
});
_hasVisibilityChanges = false;
}
}
The problem now is that if the changes result in a new page before the current one, in order to stay on the same page, the page index has to change so that it stays on the current one and that part isn't working. The build method is getting called multiple times and ends up showing the original index for some reason.
Here some sample print statements that show what I mean:
flutter: Control Text Box 2 (5) visible: true
flutter: Group instance Section 2 (1) visible controls: 1 -> 2
flutter: Group 1 clearing visible controls count
flutter: Setting state
flutter: Building pageview, current index: 2
flutter: Building 1
flutter: Building 2
flutter: Building pageview, current index: 2
flutter: Building 1
So I'm on index 1 at the beginning. I choose something on that view that results in a new page being inserted before index 1, so a NEW index 1. I call set state to set the current index to 2 since that is the new index of the current view. As you can see, the build method in the widget gets called twice, the first once renders index 1 and 2 in the page view, but the next one only renders index 1 even though the initial index is set to 2.
Since I'm unable to run the minimal repro you've posted. I tried to replicate the behavior locally from a sample app. From my tests, returning null on itemBuilder works as expected. I'm using Flutter stable channel version 1.22.0
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(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
int pageViewIndex;
class _MyHomePageState extends State<MyHomePage> {
ActionMenu actionMenu;
final PageController pageController = PageController();
int currentPageIndex = 0;
int pageCount = 1;
#override
void initState() {
super.initState();
actionMenu = ActionMenu(this.addPageView, this.removePageView);
}
addPageView() {
setState(() {
pageCount++;
});
}
removePageView(BuildContext context) {
if (pageCount > 1)
setState(() {
pageCount--;
});
else
Scaffold.of(context).showSnackBar(SnackBar(
content: Text("Last page"),
));
}
navigateToPage(int index) {
pageController.animateToPage(
index,
duration: Duration(milliseconds: 300),
curve: Curves.ease,
);
}
getCurrentPage(int page) {
pageViewIndex = page;
}
createPage(int page) {
return Container(
child: Center(
child: Text('Page $page'),
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
actions: <Widget>[
actionMenu,
],
),
body: Container(
child: PageView.builder(
controller: pageController,
onPageChanged: getCurrentPage,
// itemCount: pageCount,
itemBuilder: (context, position) {
if (position == 5) return null;
return createPage(position + 1);
},
),
),
);
}
}
enum MenuOptions { addPageAtEnd, deletePageCurrent }
List<Widget> listPageView = List();
class ActionMenu extends StatelessWidget {
final Function addPageView, removePageView;
ActionMenu(this.addPageView, this.removePageView);
#override
Widget build(BuildContext context) {
return PopupMenuButton<MenuOptions>(
onSelected: (MenuOptions value) {
switch (value) {
case MenuOptions.addPageAtEnd:
this.addPageView();
break;
case MenuOptions.deletePageCurrent:
this.removePageView(context);
break;
}
},
itemBuilder: (BuildContext context) => <PopupMenuItem<MenuOptions>>[
PopupMenuItem<MenuOptions>(
value: MenuOptions.addPageAtEnd,
child: const Text('Add Page at End'),
),
const PopupMenuItem<MenuOptions>(
value: MenuOptions.deletePageCurrent,
child: Text('Delete Current Page'),
),
],
);
}
}
Here how the sample app looks.