Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter persistent app bar across PageView

Tags:

flutter

Ideally I would like to set up my Flutter app as follows

  • PageView to swipe left/right between 3 pages and a bottom navigation bar to serve as a label and also help with navigation
  • Persistent appbar on top with drawer and contextual icons
  • Page content in between

As can be seen in the image, I have this mostly set up the way I would like in the following manner

main.dart - app entry point, set up appbar, set up pageview with children for new PeoplePage, new TimelinePage, new StatsPage

people_page.dart
timeline_page.dart
stats_page.dart

These three pages just deliver the content to the PageView children as required.

Is this the correct way to achieve this? On the surface it works fine. The issue I am coming across is that on the people page I want to implement a selectable list that changes the appbar title/color as in this example, but the appbar is set up on the main page. Can I access the appbar globally?

I could build a new appbar for each page, but I dont want a new appbar swiping in when switching page. I'd prefer the appbar to look persistent and only have the content swipe in.

Any guidance on the best way to accomplish this would be appreciated.

sample

like image 572
BGH Avatar asked Jun 29 '18 05:06

BGH


2 Answers

I put together a quick example of how you might communicate from your screen down to the pages and then also back again. This should solve your problem.

https://gist.github.com/slightfoot/464fc225b9041c2d66ec8ab36fbdb935

import 'package:flutter/material.dart';

void main() => runApp(TestApp());

class TestApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primaryColor: Colors.green[900],
        scaffoldBackgroundColor: Colors.grey[200],
      ),
      home: MainScreen(),
    );
  }
}

class AppBarParams {
  final Widget title;
  final List<Widget> actions;
  final Color backgroundColor;

  AppBarParams({
    this.title,
    this.actions,
    this.backgroundColor,
  });
} 

class MainScreen extends StatefulWidget {
  final int initialPage;

  const MainScreen({
    Key key,
    this.initialPage = 0,
  }) : super(key: key);

  @override
  MainScreenState createState() => MainScreenState();

  static MainScreenState of(BuildContext context) {
    return context.ancestorStateOfType(TypeMatcher<MainScreenState>());
  }
}

class MainScreenState extends State<MainScreen> {
  final List<GlobalKey<MainPageStateMixin>> _pageKeys = [
    GlobalKey(),
    GlobalKey(),
    GlobalKey(),
  ];

  PageController _pageController;
  AppBarParams _params;
  int _page;

  set params(AppBarParams value) {
    setState(() => _params = value);
  }

  @override
  void initState() {
    super.initState();
    _page = widget.initialPage ?? 0;
    _pageController = PageController(initialPage: _page);
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _pageKeys[0].currentState.onPageVisible();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: _params?.title,
        actions: _params?.actions,
        backgroundColor: _params?.backgroundColor,
      ),
      body: PageView(
        controller: _pageController,
        onPageChanged: _onPageChanged,
        children: <Widget>[
          PeoplePage(key: _pageKeys[0]),
          TimelinePage(key: _pageKeys[1]),
          StatsPage(key: _pageKeys[2]),
        ],
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _page,
        onTap: _onBottomNavItemPressed,
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            title: Text('people'),
            icon: Icon(Icons.people),
          ),
          BottomNavigationBarItem(
            title: Text('timeline'),
            icon: Icon(Icons.history),
          ),
          BottomNavigationBarItem(
            title: Text('stats'),
            icon: Icon(Icons.pie_chart),
          ),
        ],
      ),
    );
  }

  @override
  void reassemble() {
    super.reassemble();
    _onPageChanged(_page);
  }

  void _onPageChanged(int page) {
    setState(() => _page = page);
    _pageKeys[_page].currentState.onPageVisible();
  }

  void _onBottomNavItemPressed(int index) {
    setState(() => _page = index);
    _pageController.animateToPage(
      index,
      duration: Duration(milliseconds: 400),
      curve: Curves.fastOutSlowIn,
    );
  }
}

abstract class MainPageStateMixin<T extends StatefulWidget> extends State<T> {
  void onPageVisible();
}

class PeoplePage extends StatefulWidget {
  const PeoplePage({Key key}) : super(key: key);

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

class PeoplePageState extends State<PeoplePage> with MainPageStateMixin {
  final List<Color> _colors = [
    Colors.orange,
    Colors.purple,
    Colors.green,
  ];

  int _personCount = 3;

  @override
  void onPageVisible() {
    MainScreen.of(context).params = AppBarParams(
      title: Text('People'),
      actions: <Widget>[
        IconButton(
          icon: Icon(Icons.person_add),
          onPressed: () => setState(() => _personCount++),
        ),
      ],
      backgroundColor: Colors.green,
    );
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: _personCount,
      itemBuilder: (BuildContext context, int index) {
        return Card(
          child: InkWell(
            onTap: () => _onTapCard(index),
            child: Padding(
              padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
              child: Row(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  Material(
                    type: MaterialType.circle,
                    color: _colors[index % _colors.length],
                    child: Container(
                      width: 48.0,
                      height: 48.0,
                      alignment: Alignment.center,
                      child: Text('$index', style: TextStyle(color: Colors.white)),
                    ),
                  ),
                  SizedBox(width: 16.0),
                  Text(
                    'Item #$index',
                    style: TextStyle(
                      color: Colors.grey[600],
                      fontSize: 18.0,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ],
              ),
            ),
          ),
        );
      },
    );
  }

  void _onTapCard(int index) {
    Scaffold.of(context).showSnackBar(SnackBar(content: Text('Item #$index')));
  }
}

class TimelinePage extends StatefulWidget {
  const TimelinePage({Key key}) : super(key: key);

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

class TimelinePageState extends State<TimelinePage> with MainPageStateMixin {
  @override
  void onPageVisible() {
    MainScreen.of(context).params = AppBarParams(
      title: Text('Timeline'),
      actions: <Widget>[
        IconButton(
          icon: Icon(Icons.alarm_add),
          onPressed: () {},
        ),
      ],
      backgroundColor: Colors.purple,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text('Coming soon'),
    );
  }
}

class StatsPage extends StatefulWidget {
  const StatsPage({Key key}) : super(key: key);

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

class StatsPageState extends State<StatsPage> with MainPageStateMixin {
  @override
  void onPageVisible() {
    MainScreen.of(context).params = AppBarParams(
      title: Text('Stats'),
      actions: <Widget>[
        IconButton(
          icon: Icon(Icons.add_box),
          onPressed: () {},
        ),
      ],
      backgroundColor: Colors.orange,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text('Coming soon'),
    );
  }
}
like image 57
Simon Avatar answered Nov 06 '22 18:11

Simon


One way to tackle this would be to have the AppBar title and background color as state variables, and in your PageView set the onPageChanged to a function. This function takes in the page int and based on the page int it sets the state of the title and color to the values that you desire. For the multiselect list you set the title to the variable which keeps the values you have selected, may be keep it as a state variable in the main page and pass it down to the child component. You can use any of the state management strategies and that should probably work fine.

Example of onPageChanged function:

void onPageChanged(int page) {
    String _temptitle = "";
    Color _tempColor;
    switch (page) {
      case 0:
        _temptitle = "People";
        _tempColor = Colors.pink;
        break;
      case 1:
        _temptitle = "Timeline";
        _tempColor = Colors.green;
        break;
      case 2:
        _temptitle = "Stats";
        _tempColor = Colors.deepPurple;
        break;
    }
    setState(() {
      this._page = page;
      this._title = _temptitle;
      this._appBarColor = _tempColor;
    });
  }

So for the multiselect case, instead of setting the title to some constant you set the title to the variable which holds the values of the selected options.

Sample here

Full code is here:

import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  PageController _pageController;
  int _page = 0;
  String _title = "MyApp";
  Color _appBarColor = Colors.pink;

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(_title),
        backgroundColor: _appBarColor,
      ),
      body: PageView(
        children: <Widget>[
          Container(
            child: Center(child: Text("People")),
          ),
          Container(
            child: Center(child: Text("Timeline")),
          ),
          Container(
            child: Center(child: Text("Stats")),
          ),
        ],
        controller: _pageController,
        onPageChanged: onPageChanged,
      ),
      bottomNavigationBar: BottomNavigationBar(
        items: [
          BottomNavigationBarItem(
            icon: Icon(Icons.people),
            title: Text("People"),
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.access_time),
            title: Text("Timeline"),
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.pie_chart),
            title: Text("Stats"),
          ),
        ],
        onTap: navigateToPage,
        currentIndex: _page,
      ),
    );
  }

  void navigateToPage(int page) {
    _pageController.animateToPage(page,
        duration: Duration(milliseconds: 300), curve: Curves.ease);
  }

  void onPageChanged(int page) {
    String _temptitle = "";
    Color _tempColor;
    switch (page) {
      case 0:
        _temptitle = "People";
        _tempColor = Colors.pink;
        break;
      case 1:
        _temptitle = "Timeline";
        _tempColor = Colors.green;
        break;
      case 2:
        _temptitle = "Stats";
        _tempColor = Colors.deepPurple;
        break;
    }
    setState(() {
      this._page = page;
      this._title = _temptitle;
      this._appBarColor = _tempColor;
    });
  }

  @override
  void initState() {
    super.initState();
    _pageController = new PageController();
    _title = "People";
  }

  @override
  void dispose() {
    super.dispose();
    _pageController.dispose();
  }
}

You can improve this code for your needs. Hope this was helpful in someway. Let me know if there is something I can improve about this answer.

like image 24
ssiddh Avatar answered Nov 06 '22 17:11

ssiddh