Flutter Firestore Animated SlideShow Build an animated and filterable slideshow with FlutterFire - inspired by the Reflectly app. 899 words. By Jeff Delaney Created Mar 20, 2019 Last Updated Mar 20, 2019 Code Slack #flutter #firestore #firebase #animation One of the best examples of a well-designed UI in Flutter is Reflectly - an AI-powered journal app for iOS and Android. The following lesson will show you how to build an animated slideshow carousel inspired by the work of Reflectly. In addition, we will wire it up to Firestore to make it filterable and able to scale to an infinite number of pages. PageView Widget Intro The most import widget in this lesson is the PageView, which makes it possible to easily build sliding pages. It is a great tool when you have a linear flow of content because it provides the gesture-detection and animation behavior out of the box. Optionally, you can control the state of the PageView slider with a PageController to navigate to a specific index in the children list. Basic Example Run the code below in your Flutter project to build a basic PageView slideshow. file_type_dartlang main.dart class MyApp extends StatelessWidget { final PageController ctrl = PageController(); @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: PageView( // scrollDirection: Axis.vertical, controller: ctrl, children: [ Container(color: Colors.green), Container(color: Colors.blue), Container(color: Colors.orange), Container(color: Colors.red) ] ) ), ); } } Firestore PageView Builder Now we’re ready to take things to the next level. We will create a custom animation for the active page and use Firestore create and filter pages dynamically from the backend database. Initial Setup Our feature will start with a StatefulWidget. There are two main events that should trigger a rebuild of the widgets. (1) The user swipes to a different page, and (2) the Firestore query changes. Both of these event sources will be setup during the initState lifecycle hook. file_type_dartlang main.dart class FirestoreSlideshow extends StatefulWidget { createState() => FirestoreSlideshowState(); } class FirestoreSlideshowState extends State<FirestoreSlideshow> { final PageController ctrl = PageController(viewportFraction: 0.8); final Firestore db = Firestore.instance; Stream slides; String activeTag = 'favorites'; // Keep track of current page to avoid unnecessary renders int currentPage = 0; @override void initState() { _queryDb(); // Set state when page changes ctrl.addListener(() { int next = ctrl.page.round(); if(currentPage != next) { setState(() { currentPage = next; }); } }); @override Widget build(BuildContext context) { // TODO } // Query Firestore _queryDb({ String tag ='favorites' }) { // TODO } // Builder Functions _buildStoryPage(Map data, bool active) { // TODO } _buildTagPage() { // TODO } _buildButton(tag) { // TODO } } Firestore Query Data Model Firestore contains the title, background image, and tags array for each page Query the Database by Tag We can query the database for all stories that contain a specific tag using Firestore’s array contains query type. In addition, it is useful to map the document snapshots to their raw data payload at this point to keep the main widget build method free of business logic. Lastly, we update the activeTag on the widget to style the corresponding button with a different color when that filter is applied. file_type_dartlang main.dart Stream _queryDb({ String tag ='favorites' }) { // Make a Query Query query = db.collection('stories').where('tags', arrayContains: tag); // Map the documents to the data payload slides = query.snapshots().map((list) => list.documents.map((doc) => doc.data)); // Update the active tag setState(() { activeTag = tag; }); } UI Design PageView Builder In the widget’s build method we first start by setting up a StreamBuilder so the widget reacts to data changes in Firestore. Next, we use the PageView.builder constructor, which allows us to build our UI on the fly and scale up to infinitely large collections. If the current index is zero, build the tag page, otherwise build the main story/card page. file_type_dartlang main.dart @override Widget build(BuildContext context) { return StreamBuilder( stream: slides, initialData: [], builder: (context, AsyncSnapshot snap) { List slideList = snap.data.toList(); return PageView.builder( controller: ctrl, itemCount: slideList.length + 1, itemBuilder: (context, int currentIdx) { if (currentIdx == 0) { return _buildTagPage(); } else if (slideList.length >= currentIdx) { // Active page bool active = currentIdx == currentPage; return _buildStoryPage(slideList[currentIdx - 1], active); } } ); } ); } Animating with an AnimatedContainer In this section, we use the AnimatedContainer Widget to give the active page more height and a stronger box shadow when it’s active in the PageView. An AnimatedContainer is just like a regular Container, expect it requires a duration and curve. When its attributes change, Flutter will automatically perform a linear interpolation between the values with a transition animation. file_type_dartlang main.dart _buildStoryPage(Map data, bool active) { // Animated Properties final double blur = active ? 30 : 0; final double offset = active ? 20 : 0; final double top = active ? 100 : 200; return AnimatedContainer( duration: Duration(milliseconds: 500), curve: Curves.easeOutQuint, margin: EdgeInsets.only(top: top, bottom: 50, right: 30), decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), image: DecorationImage( fit: BoxFit.cover, image: NetworkImage(data['img']), ), boxShadow: [BoxShadow(color: Colors.black87, blurRadius: blur, offset: Offset(offset, offset))] ), ); } Filterable Firestore List The final step is to create a page with the tag filter buttons. This section is simply lays out a Column with several buttons that call _queryDb when pressed to refine the database query on the backend. file_type_dartlang main.dart _buildTagPage() { return Container(child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Your Stories', style: TextStyle(fontSize: 40, fontWeight: FontWeight.bold),), Text('FILTER', style: TextStyle( color: Colors.black26 )), _buildButton('favorites'), _buildButton('happy'), _buildButton('sad') ], ) ); } _buildButton(tag) { Color color = tag == activeTag ? Colors.purple : Colors.white; return FlatButton(color: color, child: Text('#$tag'), onPressed: () => _queryDb(tag: tag)); }