Dynamically build pages in a PageView

1,988 views
Skip to first unread message

Chris Robison

unread,
Jan 3, 2019, 10:10:43 PM1/3/19
to Flutter Dev
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, but I'm struggling to get it to work when pages are added. Here is a snippet of the stateful object:

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;
 }
}

This should provide enough context. What you are not seeing here is that each page represents one question with a set of answers. When an answer is selected, it fires off a notification that will evaluate conditions which will hide or show other questions. When that happens I try to update the PageView. 
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.

Any suggestions?
Reply all
Reply to author
Forward
0 new messages