package:flutter_slidable
방향성 있는 슬라이드 동작과 해제가 가능한 슬라이드 목록 항목에 대한 Flutter 구현입니다.
리스트 타일이 나열되어 있고 그중에서 자신이 원하는 아이템을 선택하고 그 화면에서 그 아이템에 대한 어떠한 동작을 결정하게 하려면 어떠한 방식이 가장 좋을까? 가장 좋은 방식 중 하나로는 그 아이템을 드래그, 슬라이드 동작을 진행하여 몇 가지 작업을 보여주는 것일 것이다. 이러한 동작을 쉽게 구현할 수 있게 도와주는 위젯이 바로 이번에 알아볼 package:flutter_slidable이다.
이 flutter_slidable 패키지는 슬라이드 가능한 리스트 항목(Slidable list items)을 만들기 위한 매우 유용한 패키지로 이 패키지를 사용하면 사용자가 리스트 아이템을 좌우로 스와이프 해서 삭제, 수정, 공유 등의 액션을 트리거할 수 있는 인터페이스를 구현할 수 있게 된다.
일단 사용하기 위해선 먼저 프로젝트의 pubspec.yaml 파일 안에 animations 패키지를 추가해야 할 것이다.

펍데브(pub.dev)에서 가져와서 프로젝트에 추가해 준다.
flutter pub add flutter_slidable

pubspec.yaml 파일 안에 문제없이 들어갔다면 이제 바로 사용해 주면 된다. 바로 사용해 보자.
가장 먼저 Slidable위젯을 사용해 주면 되는데 이 위젯은 필수 파라미터로 child를 받는다. 근데 이 위젯이 사용되는 위치가 어디인가? 바로 ListTile이다. 그러므로 LisitTile을 자식으로 넣어주면 된다.
Slidable(
child: ListTile(
title: Text('item'),
),
)
자 이렇게 하면 사용을 할 아주 기본적인 준비가 완료되었다.
이대로 실행해도 오류 없이 돌아가긴 할 텐데 그냥 이 상태는 ListTile 그 자체라 뭐 다른 것은 없을 것이다.
그렇다면 이제 스와이프 하여 기능 목록은 어떻게 열어야 하는 걸까? 바로 startActionPane파라미터와 endActionPane 파라미터를 사용해 주면 된다.
이 두 파라미터는 각각 왼쪽(start)과 오른쪽(end)으로 슬라이드 할 수 있도록 도와주는데 어차피 둘에 들어가는 코드는 다를 것이 없으니 startActionPane를 사용해 주겠다.
Slidable(
startActionPane: ActionPane(
// TODO
),
child: ListTile(
title: Text('item'),
),
)
startActionPane과 endActionPane파라미터에는 공통적으로 AtionPane위젯을 사용해 주면 된다.
이제 이 위젯 안에서 동작과 슬라이드 후 나올 위젯을 지정해 주면 되겠다.
사용하기 위해서 알아야 AtionPane위젯의 필수 파라미터로는 motion과 children이 있다.
특히 이 motion파라미터에서 리스트 타일을 슬라이드 했을 때 나타나는 애니메이션 방식을 지정해 줄 수 있게 되는데 그 애니메이션 방식들은 아래와 같다. (전부 위젯 형태이다.)
Behind Motion

Drawer Motion

Scroll Motion

Stretch Motion

이 모션을 지정해 줄 수 있는 위젯들을 사용해서 어떻게 나타날지 정해주면 된다.
나는 공식 문서 예제에서 사용하는 ScrollMotion을 사용해 주었다.
startActionPane: ActionPane(
motion: ScrollMotion(),
),
그다음은 이제 children파라미터인데 이 파라미터 안에서 이제 슬라이더를 했을 때 나타는 위젯을 선언해 줄 수 있다.
startActionPane: ActionPane(
motion: ScrollMotion(),
children: [
SlidableAction(
onPressed: (context) {},
),
],
),
SlidableAction위젯을 사용하여 배치해 줄 수 있는데 기본적으로 필수로 onPressed를 파라미터로 주기 때문에 context를 인자로 한 콜백함수를 넣어줘야 한다.
이제 코드에서 아무런 오류가 나지 않을 텐데 그렇다고 실행하지 않길 바란다. (실행하면 아래와 같이 나온다.)

왜냐하면 이 SlidableAction 위젯은 보이는 것과 다르게 필수 파라미터를 두 개 더 받기 때문인데 바로 icon과 label파라미터이다. 이 두 파라미터에는 이름으로 유추할 수 있듯이 그냥 아이콘과 라벨(텍스트)을 넣어주면 오류 없이 실행할 수 있게 된다.
Slidable(
startActionPane: ActionPane(
motion: ScrollMotion(),
children: [
SlidableAction(
onPressed: (context) {},
icon: Icons.add,
label: 'Add',
),
],
),
child: ListTile(
title: Text('item'),
),
),

위의 GIF에 보이는 것처럼 현재 startActionPane를 사용해 줬기 때문에 왼쪽으로 슬라이더 시 위젯이 나타나게 된다.
눌렀을 때 기본적인 동작은(onPressed에 아무것도 기입하지 않을 시) 다시 원상태로 복구되는 것인데 이제 여기에 무엇을 넣을지는 선택의 영역이 되겠다.
그런데 이제 여기에서 슬라이더를 끝까지 당겨서 삭제하는, 즉 Dismissible위젯 같은 동작을 하려면 어떻게 해야 할까?
바로 ActionPane위젯의 파라미터인 dismissible을 사용해 주면 된다.
dismissible: DismissiblePane(onDismissed: () {},),
dismissible파라미터 안에는 DismissiblePane위젯을 넣어주면 되는데 이 위젯을 넣어주면 이제 슬라이더가 끝까지 당겨지게 된다.
근데 여기서 문제가 발생한다. 바로 당겨보면 아래와 같은 오류가 뜬다는 것이다.

근데 사실 이 오류를 해결하는 것은 아주 간단하다. 바로 Slidable위젯에 키 값을 먹여주면 된다.
Slidable(
key: ValueKey(1), // 추가!
startActionPane: ActionPane(
dismissible: DismissiblePane(onDismissed: () {},),
motion: ScrollMotion(),
children: [
SlidableAction(
onPressed: (context) {},
icon: Icons.add,
label: 'Add',
),
],
),
child: ListTile(
tileColor: Theme.of(context).canvasColor,
title: Text('item'),
),
)

위의 GIF처럼 슬라이더를 끝까지 당기면 리스트 타일이 삭제가 되는 것을 볼 수 있다.
근데 이제 위젯이 잘 보이지 않는다는 문제가 있는데 이것의 해결 방법은 아주 간단하다. 바로 색을 먹여주면 된다.
SlidableAction(
backgroundColor: Colors.blue.shade100,
onPressed: (context) {},
icon: Icons.add,
label: 'Add',
),

SlidableAction위젯에서 backgroundColor파라미터를 사용해 주면 간단하게 위젯의 배경에 색을 먹여줄 수 있게 된다.
SlidableAction위젯에는 다른 속성들도 있긴 한데 솔직히 쓸만한 건 이거밖에 없지 않나 싶다.
이제 기능적인 부분은 이게 끝인데 이제 여기서 알아야 할 것은 ActionPane위젯의 children파라미터는 속성이 List<Widget>이라는 것이다. 그런 만큼 SlidableAction위젯을 여러 개 배치할 수 있는데 이러한 점을 이용해 아래와 같이 만들 수 있다. (아래는 package:flutter_slidable의 공식 예제 코드이다.)
import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
void main() => runApp(const MyApp());
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
late final controller = SlidableController(this);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Slidable Example',
home: Scaffold(
body: ListView(
children: [
Slidable(
key: const ValueKey(0),
startActionPane: ActionPane(
motion: const ScrollMotion(),
dismissible: DismissiblePane(onDismissed: () {}),
children: const [
SlidableAction(
onPressed: doNothing,
backgroundColor: Color(0xFFFE4A49),
foregroundColor: Colors.white,
icon: Icons.delete,
label: 'Delete',
),
SlidableAction(
onPressed: doNothing,
backgroundColor: Color(0xFF21B7CA),
foregroundColor: Colors.white,
icon: Icons.share,
label: 'Share',
),
],
),
endActionPane: ActionPane(
motion: const ScrollMotion(),
children: [
SlidableAction(
flex: 2,
onPressed: (_) => controller.openEndActionPane(),
backgroundColor: const Color(0xFF7BC043),
foregroundColor: Colors.white,
icon: Icons.archive,
label: 'Archive',
),
SlidableAction(
onPressed: (_) => controller.close(),
backgroundColor: const Color(0xFF0392CF),
foregroundColor: Colors.white,
icon: Icons.save,
label: 'Save',
),
],
),
child: const ListTile(title: Text('Slide me')),
),
Slidable(
controller: controller,
key: const ValueKey(1),
startActionPane: const ActionPane(
motion: ScrollMotion(),
children: [
SlidableAction(
onPressed: doNothing,
backgroundColor: Color(0xFFFE4A49),
foregroundColor: Colors.white,
icon: Icons.delete,
label: 'Delete',
),
SlidableAction(
onPressed: doNothing,
backgroundColor: Color(0xFF21B7CA),
foregroundColor: Colors.white,
icon: Icons.share,
label: 'Share',
),
],
),
endActionPane: ActionPane(
motion: const ScrollMotion(),
dismissible: DismissiblePane(onDismissed: () {}),
children: const [
SlidableAction(
flex: 2,
onPressed: doNothing,
backgroundColor: Color(0xFF7BC043),
foregroundColor: Colors.white,
icon: Icons.archive,
label: 'Archive',
),
SlidableAction(
onPressed: doNothing,
backgroundColor: Color(0xFF0392CF),
foregroundColor: Colors.white,
icon: Icons.save,
label: 'Save',
),
],
),
child: const ListTile(title: Text('Slide me')),
),
],
),
),
);
}
}
void doNothing(BuildContext context) {}

혹은 ListView.Builder를 사용하여 타일 여러 개를 한 번에 제어하게 할 수도 있다.
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(
home: SlidableExample(),
);
}
}
class SlidableExample extends StatelessWidget {
final List<String> items = List.generate(10, (i) => 'Item {i + 1}');
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Slidable List')),
body: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return Slidable(
key: ValueKey(items[index]),
startActionPane: ActionPane(
motion: const DrawerMotion(),
children: [
SlidableAction(
onPressed: (context) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('공유: {items[index]}')),
);
},
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
icon: Icons.share,
label: '공유',
),
],
),
endActionPane: ActionPane(
motion: const DrawerMotion(),
children: [
SlidableAction(
onPressed: (context) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('삭제: {items[index]}')),
);
},
backgroundColor: Colors.red,
foregroundColor: Colors.white,
icon: Icons.delete,
label: '삭제',
),
],
),
child: ListTile(
title: Text(items[index]),
),
);
},
),
);
}
}

이렇게 package:flutter_slidable에 대해서 알아보았다. 이 패키지는 Slidable위젯 하나에 몰빵한 특이한 케이스의 패키지인데 그래서인지 이 Slidable위젯만 알고 있으면 '이 패키지를 완벽하게 사용이 가능하다'라는 스탠스가 가능하다..
이 패키지로 하여금 Dismissible위젯을 사용하지 않고 이 위젯으로 사용하여 더욱 유연하게 상황 대처가 가능해질 것이기에 꼭 알아뒀으면 한다. 도움이 되었길 바라며 마치겠다.