Flutter – Slidable
Slidable in an application can be used to perform a wide range of tasks with just a simple swipe to either right or left on the tile. It not only makes the UI very user-friendly but also saves a lot of time in doing trivial tasks which if done in other ways can be hectic and redundant to design. In this article, we will look into the process of designing a slidable for your application.
Here we will build a simple application with tiles that when swiped left to right archives the tile and when swiped right to left deletes the tile. To do so follow the below steps:
- Add the flutter_slidable dependency to the pubspec.yaml file.
- Import the dependency to the main.dart file
- Create a StatelessWidget to give the app a structure
- Use a StateFulWidget to add a Homepage to the application
- Use SlidableCntroller to set up the slide actions
- Use FloatingActionButton to assign actions to the buttons generated while sliding the tile
- Use the WidgetBuilder to build the tiles on the homepage
Let’s discuss the steps in detail.
Adding Dependency:
You can import the flutter_slidable dependency in the pubspec.yaml file as shown below:
Importing Dependency:
To import the dependency in the main.dart file use the following:
import 'package:flutter_slidable/flutter_slidable.dart';
Creating App Structure:
Use a StatelessWidget to give the application a simple structure as shown below:
Dart
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Slidable ' , theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(title: 'GeeksForGeeks' ), ); } } |
Creating Homepage:
Use a StatefulWidget to set up the homepage for the application that would in the future hold the tiles that can be swiped in either direction to perform tasks assigned to them as shown below:
Dart
class MyHomePage extends StatefulWidget { MyHomePage({Key key, this .title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { SlidableController slidableController; final List<_HomeItem> items = List.generate( 20, (i) => _HomeItem( i, 'Slide Bar $i' , _getSubtitle(i), _getAvatarColor(i), ), ); |
Designing the slides:
Use the SlidableController to setup the slides for the application. It can be done by the use of either Slidable Constructor or Slidable.builder Constructor. The app we are building has 4 essential components :
- Slide Actions
- A slide Action panel widget
- An extent ratio between a slide action extent and the item extent.
- A child
To design the slidable use the following:
Dart
Slidable( actionPane: SlidableDrawerActionPane(), actionExtentRatio: 0.25, child: Container( color: Colors.white, child: ListTile( leading: CircleAvatar( backgroundColor: Colors.indigoAccent, child: Text( '$3' ), foregroundColor: Colors.white, ), title: Text( 'Tile $3' ), subtitle: Text( 'SlidableDrawerDelegate' ), ), ), actions: <Widget>[ IconSlideAction( caption: 'Archive' , color: Colors.blue, icon: Icons.archive, onTap: () => _showSnackBar( 'Archive' ), ), IconSlideAction( caption: 'Share' , color: Colors.indigo, icon: Icons.share, onTap: () => _showSnackBar( 'Share' ), ), ], secondaryActions: <Widget>[ IconSlideAction( caption: 'More' , color: Colors.black45, icon: Icons.more_horiz, onTap: () => _showSnackBar( 'More' ), ), IconSlideAction( caption: 'Delete' , color: Colors.red, icon: Icons. delete , onTap: () => _showSnackBar( 'Delete' ), ), ], ); |
Assigning Actions:
As the slides are swiped a FloatingActionButton will appear depending upon the direction of the slide. In both the case two actions will be assigned to each swipe as following:
- For left to right slide:
- Archive tile
- Share tile
- For the right to left slide:
- Delete tile
- More
For the sake of simplicity, we will only assign actions to the Archive tile button and delete the tile button which will archive and delete the tile respectively. To do so use the following:
Dart
@ protected void initState() { slidableController = SlidableController( onSlideAnimationChanged: handleSlideAnimationChanged, onSlideIsOpenChanged: handleSlideIsOpenChanged, ); super.initState(); } Animation< double > _rotationAnimation; Color _fabColor = Colors.blue; void handleSlideAnimationChanged(Animation< double > slideAnimation) { setState(() { _rotationAnimation = slideAnimation; }); } void handleSlideIsOpenChanged( bool isOpen) { setState(() { _fabColor = isOpen ? Colors.green : Colors.blue; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), backgroundColor: Colors.green, ), body: Center( child: OrientationBuilder( builder: (context, orientation) => _buildList( context, orientation == Orientation.portrait ? Axis.vertical : Axis.horizontal), ), ), floatingActionButton: FloatingActionButton( backgroundColor: _fabColor, onPressed: null, child: _rotationAnimation == null ? Icon(Icons.add) : RotationTransition( turns: _rotationAnimation, child: Icon(Icons.add), ), ), ); } Widget _buildList(BuildContext context, Axis direction) { return ListView.builder( scrollDirection: direction, itemBuilder: (context, index) { final Axis slidableDirection = direction == Axis.horizontal ? Axis.vertical : Axis.horizontal; var item = items[index]; if (item.index < 8) { return _getSlidableWithLists(context, index, slidableDirection); } else { return _getSlidableWithDelegates(context, index, slidableDirection); } }, itemCount: items.length, ); } Widget _getSlidableWithLists( BuildContext context, int index, Axis direction) { final _HomeItem item = items[index]; return Slidable( key: Key(item.title), controller: slidableController, direction: direction, dismissal: SlidableDismissal( child: SlidableDrawerDismissal(), onDismissed: (actionType) { _showSnackBar( context, actionType == SlideActionType.primary ? 'Dismiss Archive' : 'Dismiss Delete' ); setState(() { items.removeAt(index); }); }, ), actionPane: _getActionPane(item.index), actionExtentRatio: 0.25, child: direction == Axis.horizontal ? VerticalListItem(items[index]) : HorizontalListItem(items[index]), actions: <Widget>[ IconSlideAction( caption: 'Archive' , color: Colors.blue, icon: Icons.archive, onTap: () => _showSnackBar(context, 'Archive' ), ), IconSlideAction( caption: 'Share' , color: Colors.indigo, icon: Icons.share, onTap: () => _showSnackBar(context, 'Share' ), ), ], secondaryActions: <Widget>[ Container( height: 800, color: Colors.green, child: Text( 'a' ), ), IconSlideAction( caption: 'More' , color: Colors.grey.shade200, icon: Icons.more_horiz, onTap: () => _showSnackBar(context, 'More' ), closeOnTap: false , ), IconSlideAction( caption: 'Delete' , color: Colors.red, icon: Icons. delete , onTap: () => _showSnackBar(context, 'Delete' ), ), ], ); } |
Complete Source Code:
Dart
import 'package:flutter/material.dart' ; import 'package:flutter_slidable/flutter_slidable.dart' ; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Slidable ' , theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(title: 'GeeksForGeeks' ), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key, this .title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { SlidableController slidableController; final List<_HomeItem> items = List.generate( 20, (i) => _HomeItem( i, 'Slide Bar $i' , _getSubtitle(i), _getAvatarColor(i), ), ); @ protected void initState() { slidableController = SlidableController( onSlideAnimationChanged: handleSlideAnimationChanged, onSlideIsOpenChanged: handleSlideIsOpenChanged, ); super.initState(); } Animation< double > _rotationAnimation; Color _fabColor = Colors.blue; void handleSlideAnimationChanged(Animation< double > slideAnimation) { setState(() { _rotationAnimation = slideAnimation; }); } void handleSlideIsOpenChanged( bool isOpen) { setState(() { _fabColor = isOpen ? Colors.green : Colors.blue; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), backgroundColor: Colors.green, ), body: Center( child: OrientationBuilder( builder: (context, orientation) => _buildList( context, orientation == Orientation.portrait ? Axis.vertical : Axis.horizontal), ), ), floatingActionButton: FloatingActionButton( backgroundColor: _fabColor, onPressed: null, child: _rotationAnimation == null ? Icon(Icons.add) : RotationTransition( turns: _rotationAnimation, child: Icon(Icons.add), ), ), ); } Widget _buildList(BuildContext context, Axis direction) { return ListView.builder( scrollDirection: direction, itemBuilder: (context, index) { final Axis slidableDirection = direction == Axis.horizontal ? Axis.vertical : Axis.horizontal; var item = items[index]; if (item.index < 8) { return _getSlidableWithLists(context, index, slidableDirection); } else { return _getSlidableWithDelegates(context, index, slidableDirection); } }, itemCount: items.length, ); } Widget _getSlidableWithLists( BuildContext context, int index, Axis direction) { final _HomeItem item = items[index]; //final int t = index; return Slidable( key: Key(item.title), controller: slidableController, direction: direction, dismissal: SlidableDismissal( child: SlidableDrawerDismissal(), onDismissed: (actionType) { _showSnackBar( context, actionType == SlideActionType.primary ? 'Dismiss Archive' : 'Dismiss Delete' ); setState(() { items.removeAt(index); }); }, ), actionPane: _getActionPane(item.index), actionExtentRatio: 0.25, child: direction == Axis.horizontal ? VerticalListItem(items[index]) : HorizontalListItem(items[index]), actions: <Widget>[ IconSlideAction( caption: 'Archive' , color: Colors.blue, icon: Icons.archive, onTap: () => _showSnackBar(context, 'Archive' ), ), IconSlideAction( caption: 'Share' , color: Colors.indigo, icon: Icons.share, onTap: () => _showSnackBar(context, 'Share' ), ), ], secondaryActions: <Widget>[ Container( height: 800, color: Colors.green, child: Text( 'a' ), ), IconSlideAction( caption: 'More' , color: Colors.grey.shade200, icon: Icons.more_horiz, onTap: () => _showSnackBar(context, 'More' ), closeOnTap: false , ), IconSlideAction( caption: 'Delete' , color: Colors.red, icon: Icons. delete , onTap: () => _showSnackBar(context, 'Delete' ), ), ], ); } Widget _getSlidableWithDelegates( BuildContext context, int index, Axis direction) { final _HomeItem item = items[index]; return Slidable.builder( key: Key(item.title), controller: slidableController, direction: direction, dismissal: SlidableDismissal( child: SlidableDrawerDismissal(), closeOnCanceled: true , onWillDismiss: (item.index != 10) ? null : (actionType) { return showDialog< bool >( context: context, builder: (context) { return AlertDialog( title: Text( 'Delete' ), content: Text( 'Item will be deleted' ), actions: <Widget>[ FlatButton( child: Text( 'Cancel' ), onPressed: () => Navigator.of(context).pop( false ), ), FlatButton( child: Text( 'Ok' ), onPressed: () => Navigator.of(context).pop( true ), ), ], ); }, ); }, onDismissed: (actionType) { _showSnackBar( context, actionType == SlideActionType.primary ? 'Dismiss Archive' : 'Dismiss Delete' ); setState(() { items.removeAt(index); }); }, ), actionPane: _getActionPane(item.index), actionExtentRatio: 0.25, child: direction == Axis.horizontal ? VerticalListItem(items[index]) : HorizontalListItem(items[index]), actionDelegate: SlideActionBuilderDelegate( actionCount: 2, builder: (context, index, animation, renderingMode) { if (index == 0) { return IconSlideAction( caption: 'Archive' , color: renderingMode == SlidableRenderingMode.slide ? Colors.blue.withOpacity(animation.value) : (renderingMode == SlidableRenderingMode.dismiss ? Colors.blue : Colors.green), icon: Icons.archive, onTap: () async { var state = Slidable.of(context); var dismiss = await showDialog< bool >( context: context, builder: (context) { return AlertDialog( title: Text( 'Delete' ), content: Text( 'Item will be deleted' ), actions: <Widget>[ FlatButton( child: Text( 'Cancel' ), onPressed: () => Navigator.of(context).pop( false ), ), FlatButton( child: Text( 'Ok' ), onPressed: () => Navigator.of(context).pop( true ), ), ], ); }, ); if (dismiss) { state.dismiss(); } }, ); } else { return IconSlideAction( caption: 'Share' , color: renderingMode == SlidableRenderingMode.slide ? Colors.indigo.withOpacity(animation.value) : Colors.indigo, icon: Icons.share, onTap: () => _showSnackBar(context, 'Share' ), ); } }), secondaryActionDelegate: SlideActionBuilderDelegate( actionCount: 2, builder: (context, index, animation, renderingMode) { if (index == 0) { return IconSlideAction( caption: 'More' , color: renderingMode == SlidableRenderingMode.slide ? Colors.grey.shade200.withOpacity(animation.value) : Colors.grey.shade200, icon: Icons.more_horiz, onTap: () => _showSnackBar(context, 'More' ), closeOnTap: false , ); } else { return IconSlideAction( caption: 'Delete' , color: renderingMode == SlidableRenderingMode.slide ? Colors.red.withOpacity(animation.value) : Colors.red, icon: Icons. delete , onTap: () => _showSnackBar(context, 'Delete' ), ); } }), ); } static Widget _getActionPane( int index) { switch (index % 4) { case 0: return SlidableBehindActionPane(); case 1: return SlidableStrechActionPane(); case 2: return SlidableScrollActionPane(); case 3: return SlidableDrawerActionPane(); default : return null; } } static Color _getAvatarColor( int index) { switch (index % 4) { case 0: return Colors.red; case 1: return Colors.green; case 2: return Colors.blue; case 3: return Colors.indigoAccent; default : return null; } } static String _getSubtitle( int index) { switch (index % 4) { case 0: return ' ' ; case 1: return ' ' ; case 2: return ' ' ; case 3: return ' ' ; default : return null; } } void _showSnackBar(BuildContext context, String text) { Scaffold.of(context).showSnackBar(SnackBar(content: Text(text))); } } class HorizontalListItem extends StatelessWidget { HorizontalListItem( this .item); final _HomeItem item; @override Widget build(BuildContext context) { return Container( color: Colors.white, width: 160.0, child: Column( mainAxisSize: MainAxisSize.max, children: <Widget>[ Expanded( child: CircleAvatar( backgroundColor: item.color, child: Text( '${item.index}' ), foregroundColor: Colors.white, ), ), Expanded( child: Center( child: Text( item.subtitle, ), ), ), ], ), ); } } class VerticalListItem extends StatelessWidget { VerticalListItem( this .item); final _HomeItem item; @override Widget build(BuildContext context) { return GestureDetector( onTap: () => Slidable.of(context)?.renderingMode == SlidableRenderingMode.none ? Slidable.of(context)?.open() : Slidable.of(context)?.close(), child: Container( color: Colors.white, child: ListTile( leading: CircleAvatar( backgroundColor: item.color, child: Text( '${item.index}' ), foregroundColor: Colors.white, ), title: Text(item.title), subtitle: Text(item.subtitle), ), ), ); } } class _HomeItem { const _HomeItem( this .index, this .title, this .subtitle, this .color, ); final int index; final String title; final String subtitle; final Color color; } |
Output:
Please Login to comment...