Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Preserve Widget State in PageView while enabling Navigation

Tags:

flutter

I have a rather complex situation in a Flutter App. I have a Home screen that is a swipable PageView,that displays 3 child Widgets : Calendar, Messages, Profile.

My issue at the moment is with the Calendar Widget. It is populated dynamically from the initState() method.

I managed to fix a first issue that came from swiping from one page to another that caused rebuilding the Calendar Widget every time.

My issue now is when I tap an item in the Calendar list, I open the detail view. Then, when I close it… all is still OK. However, when I swipe again the initState() method is called once more and the List view is rebuilt. I would like to prevent that and preserve it's state. any suggestions ?

Here is the Home code.

class HomeStack extends StatefulWidget {

  final pages = <HomePages> [
    CalendarScreen(),
    MessagesScreen(),
    ProfileScreen(),
  ];

  @override
  _HomeStackState createState() => _HomeStackState();
}

class _HomeStackState extends State<HomeStack> with AutomaticKeepAliveClientMixin<HomeStack> {

  User user;

  @override
  bool get wantKeepAlive{
    return true;
  }

  @override
  void initState() {
    print("Init home");
    _getUser();
    super.initState();
  }

  void _getUser() async {
    User _user = await HomeBloc.getUserProfile();
    setState(() {
      user = _user;
    });
  }

  final PageController _pageController = PageController();
  int _selectedIndex = 0;

  void _onPageChanged(int index) {
    _selectedIndex = index;
  }

  void _navigationTapped(int index) {
    _pageController.animateToPage(
        index,
        duration: const Duration(milliseconds: 300),
        curve: Curves.ease
    );
  }

  GestureDetector _navBarItem({int pageIndex, IconData iconName, String title}) {
    return GestureDetector(
      child: HomeAppBarTitleItem(
          index: pageIndex,
          icon: iconName,
          title: title,
          controller: _pageController
      ),
      onTap: () => _navigationTapped(pageIndex),
    );
  }

  Widget _buildWidget() {
    if (user == null) {
      return Center(
        child: ProgressHud(imageSize: 70.0, progressSize: 70.0, strokeWidth: 5.0),
      );
    } else {
      return Scaffold(
        appBar: AppBar(
          title: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              _navBarItem(
                pageIndex: 0,
                iconName: Icons.calendar_today,
                title: AppLocalizations.of(context).calendarViewTitle,
              ),
              _navBarItem(
                pageIndex: 1,
                iconName: Icons.message,
                title: AppLocalizations.of(context).messagesViewTitle,
              ),
              _navBarItem(
                pageIndex: 2,
                iconName: Icons.face,
                title: AppLocalizations.of(context).profileViewTitle,
              ),
            ],
          ),
          backgroundColor: Colors.transparent,
          elevation: 0.0,
        ),
        backgroundColor: Colors.transparent,
        body: PageView(
          onPageChanged: (index) => _onPageChanged(index),
          controller: _pageController,
          children: widget.pages,
        ),
        floatingActionButton: widget.pages.elementAt(_selectedIndex).fabButton,
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      child: Stack(
        children: <Widget>[
          BackgroundGradient(),
          _buildWidget(),
        ],
      ),
      onWillPop: () async  {
        return true;
      },
    );
  }
}

And the Calendar code.

class CalendarScreen extends StatelessWidget implements HomePages {

  /// TODO: Prevent reloading
  /// when :
  /// 1) push detail view
  /// 2) swipe pageView
  /// 3) come back to calendar it reloads

  static const String routeName = "/calendar";

  static Color borderColor(EventPresence status) {
    switch (status) {
      case EventPresence.present:
        return CompanyColors.grass;
      case EventPresence.absent:
        return CompanyColors.asher;
      case EventPresence.pending:
        return CompanyColors.asher;
      default:
        return CompanyColors.asher;
    }
  }

  final FloatingActionButton fabButton = FloatingActionButton(
    onPressed: () {}, /// TODO: Add action to action button
    backgroundColor: CompanyColors.sky,
    foregroundColor: CompanyColors.snow,
    child: Icon(Icons.add),
  );

  @override
  Widget build(BuildContext context) {
    return CalendarProvider(
      child: CalendarList(),
    );
  }
}

class CalendarList extends StatefulWidget {
  @override
  _CalendarListState createState() => _CalendarListState();
}

class _CalendarListState extends State<CalendarList> with AutomaticKeepAliveClientMixin<CalendarList> {

  Events events;

  void _getEvents() async {
    Events _events = await CalendarBloc.getAllEvents();
    setState(() {
      events = _events;
    });
  }

  @override
  void initState() {
    _getEvents();
    super.initState();
  }

  @override
  bool get wantKeepAlive{
    return true;
  }

  Widget _displayBody() {
    if (events == null) {
      return ProgressHud(imageSize: 30.0, progressSize: 40.0, strokeWidth: 3.0);
    } else if(events.future.length == 0 && events.past.length == 0) return _emptyStateView();
    return EventsListView(events: events);
  }

  @override
  Widget build(BuildContext context) {
    return _displayBody();
  }

  Widget _emptyStateView() {
    return Center(
      child: Text("No data"),
    );
  }
}

class EventsListView extends StatefulWidget {

  final Events events;

  EventsListView({this.events});

  @override
  _EventsListViewState createState() => _EventsListViewState();
}

class _EventsListViewState extends State<EventsListView> {

  GlobalKey _pastEventsScrollViewKey = GlobalKey();
  GlobalKey _scrollViewKey = GlobalKey();

  double _opacity = 0.0;

  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      RenderSliverList renderSliver = _pastEventsScrollViewKey.currentContext.findRenderObject();
      setState(() {
        CustomScrollView scrollView = _scrollViewKey.currentContext.widget;
        scrollView.controller.jumpTo(renderSliver.geometry.scrollExtent);
        _opacity = 1.0;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(top: 8.0),
      child: AnimatedOpacity(
        opacity: _opacity,
        duration: Duration(milliseconds: 300),
        child: CustomScrollView(
          key: _scrollViewKey,
          controller: ScrollController(
            //initialScrollOffset: initialScrollOffset,
            keepScrollOffset: true,
          ),
          slivers: <Widget>[
            SliverList(
              key: _pastEventsScrollViewKey,
              delegate: SliverChildBuilderDelegate( (context, index) {
                Event event = widget.events.past[index];
                switch (event.type) {
                  case EventType.competition:
                    return CompetitionListItem(event: event);
                  case EventType.training:
                    return TrainingListItem(event: event);
                  case EventType.event:
                    return EventListItem(event: event);
                }
              },
                childCount: widget.events.past.length,
              ),
            ),
            SliverList(
              delegate: SliverChildBuilderDelegate( (context, index) {
                return Padding(
                  padding: EdgeInsets.only(top: 32.0, left: 16.0, right: 16.0, bottom: 16.0),
                  child: Text(
                    DateFormat.MMMMEEEEd().format(DateTime.now()),
                    style: Theme.of(context).textTheme.body2.copyWith(
                      color: CompanyColors.snow,
                    ),
                  ),
                );
              },
                childCount: 1,
              ),
            ),
            SliverList(
              delegate: SliverChildBuilderDelegate( (context, index) {
                Event event = widget.events.future[index];
                switch (event.type) {
                  case EventType.competition:
                    return CompetitionListItem(event: event);
                  case EventType.training:
                    return TrainingListItem(event: event);
                  case EventType.event:
                    return EventListItem(event: event);
                }
              },
                childCount: widget.events.future.length,
              ),
            ),
          ],
        ),
      ),
    );
  }
}
like image 481
Benjamin Avatar asked Dec 07 '18 17:12

Benjamin


People also ask

What is widget state?

State is information that (1) can be read synchronously when the widget is built and (2) might change during the lifetime of the widget. It is the responsibility of the widget implementer to ensure that the State is promptly notified when such state changes, using State.

What is PageView widget in flutter?

A PageView is a widget that generates the pages of the screen that are scrollable. The pages can be fixed or repeating pages that is built by the builder function. PageView behaves like ListView in constructing the elements.

What is Page Controller in flutter?

A page controller lets you manipulate which page is visible in a PageView. In addition to being able to control the pixel offset of the content inside the PageView, a PageController also lets you control the offset in terms of pages, which are increments of the viewport size.

How to keep the state of the page on the screen?

Or you can make use of widgets like PageView or Stack to achieve the same. Hope that helps! Show activity on this post. Use “ IndexedStack Widget ” with “ Bottom Navigation Bar Widget ” to keep state of Screens/pages/Widget

Does Pageview widget support swipeable scrolling?

PageView Widget supports both Vertical and Horizontal swipeable scrolling. In PageView Widget each child widget should be in the same as Parent Viewport. If your parent widget size is full screen then it will make the swipeable widgets full screen.

How to use Pageview widget in flutter?

The PageView widget allows the user to transition between different screens in their flutter application. All you need to set it up are a PageViewController and a PageView. scrollDirection: It sets the axis of scrolling ( Vertical or horizontal ).

What is a Pageview and pagecontroller?

A little side note here: you may notice that page is a double, meaning it can represent a fraction of a page. PageView and PageController are designed to be flexible enough to handle a wide variety of use cases. Like say you want to implement a “peek” animation to let users know your app is swipeable.


Video Answer


1 Answers

From the documentation on AutomaticKeepAliveClientMixin:

/// A mixin with convenience methods for clients of [AutomaticKeepAlive]. Used with [State] subclasses.

/// Subclasses must implement [wantKeepAlive], and their [build] methods must call super.build (the return value will always return null, and should be ignored).

So in your code, before you return the Scaffold just call super.build:

  Widget build(BuildContext context) {
    super.build(context);
    return Scaffold(...);
  }
like image 81
anmol.majhail Avatar answered Nov 16 '22 20:11

anmol.majhail