Flutter Cookbook (Effects - download button)
Updated:
Effects
Create a download button
- Download 버튼을 생성해서 다운로드 중에는 Progress indicator 를 나타내고 완료되면 오픈 버튼을 사용할 수 있게 만드는 기능입니다
Check point!
-
new stateful widget 생성
-
버튼의 가능한 visual states 생성
-
button 의 모양 설정
-
button 의 text 설정
-
fetching download 중에 spinner 동작 설정 (progress indicator)
-
다운로드 중에 progress and stop button 설정
-
button tap callbacks 추가하기
// in main.dart
import 'package:cook_book_practice/download_button.dart';
import 'package:cook_book_practice/download_controller.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'demo_app_icon.dart';
void main() {
runApp(const MaterialApp(
home: ExampleCupertinoDownloadButton(),
debugShowCheckedModeBanner: false,
));
}
@immutable
class ExampleCupertinoDownloadButton extends StatefulWidget {
const ExampleCupertinoDownloadButton({Key? key}) : super(key: key);
@override
_ExampleCupertinoDownloadButtonState createState() =>
_ExampleCupertinoDownloadButtonState();
}
class _ExampleCupertinoDownloadButtonState
extends State<ExampleCupertinoDownloadButton> {
// DownloadController widget type 변수 선언
late final List<DownloadController> _downloadControllers;
@override
// initState 설정
void initState() {
super.initState();
// _downloadControllers 의 List 20 개 생성 후, SimulatedDownloadController() download_controller.dart 파일에서 import 후에 openDowload open
_downloadControllers = List<DownloadController>.generate(
20,
(index) => SimulatedDownloadController(onOpenDownload: () {
_openDownload(index);
}),
);
}
// Open button 을 누를때 Snackbar 가 표시되게 설정
void _openDownload(int index) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Open App ${index + 1}'),
),
);
}
// main 화면 appBar, body 부분 설정
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Apps')),
body: _buildList(),
);
}
// 리스트 생성 _downloadControllers 길이만큼 생성 하고, meterial design 스타일 Divider() seperatorBuilder 각각 적용, itemBuilder 에 _buildListItem 위젯 연결
Widget _buildList() {
return ListView.separated(
itemCount: _downloadControllers.length,
separatorBuilder: (context, index) => const Divider(),
itemBuilder: _buildListItem,
);
}
// _buildListItem 위젯 생성
Widget _buildListItem(BuildContext context, int index) {
// theme, downloadController 변수 생성
final theme = Theme.of(context);
final downloadController = _downloadControllers[index];
// return 값으로 ListTile 생성
return ListTile(
// leading : demo_app_icon.dart state 연결
leading: const DemoAppIcon(),
title: Text(
'App ${index + 1}',
// text가 overflow 될때 ... 생성
overflow: TextOverflow.ellipsis,
style: theme.textTheme.headline6,
),
subtitle: Text(
'Lorem ipsum dolor #${index + 1}',
overflow: TextOverflow.ellipsis,
style: theme.textTheme.caption,
),
// trailing: title 뒷부분의 버튼 설정
trailing: SizedBox(
width: 96.0,
// AnimatedBuilder 생성 : animation : animated builder 연결 , builder: animation 이 변경될때 마다의 값을 widget 값으로 return
child: AnimatedBuilder(
animation: downloadController,
builder: (context, child) {
// Download button state 연결
return DownloadButton(
status: downloadController.downloadStatus,
downloadProgress: downloadController.progress,
onDownload: downloadController.startDownload,
onCancel: downloadController.stopDownload,
onOpen: downloadController.openDownload,
);
},
),
),
);
}
}
// in download_controller.dart
import 'package:flutter/material.dart';
// enum 으로 DownloadStatus 의 required type 설정
enum DownloadStatus {
notDownloaded,
fetchingDownload,
downloading,
downloaded,
}
// class 상속 정리
// 1.implement: 다중상속 가능 class, abstract class 필수구현
// 2.extends: 다중 상속 불가능, class 상속 선택구현, abstract 필수 구현
// 3.with: 상속 불가능, 다른 class 에서 기능을 가져 오거나 오버라이드 가능
// abstract class(추상클래스) 는 그대로 인스턴스화할 수 없으며 다른 클래스에서 임플리먼트하여 기능을 완성하는 상속 재료로 사용됩니다.
// 추상 class DownloadController 생성 ChangeNotifier implemets
abstract class DownloadController implements ChangeNotifier {
// DownloadStatus, progress getter
DownloadStatus get downloadStatus;
double get progress;
// start, stop open Download
void startDownload();
void stopDownload();
void openDownload();
}
// SimulatedDownloadController class 생성
class SimulatedDownloadController extends DownloadController
with ChangeNotifier {
SimulatedDownloadController({
DownloadStatus downloadStatus = DownloadStatus.notDownloaded,
double progress = 0.0,
required VoidCallback onOpenDownload,
}) : _downloadStatus = downloadStatus,
_progress = progress,
_onOpenDownload = onOpenDownload;
// _downloadStatus override
DownloadStatus _downloadStatus;
@override
DownloadStatus get downloadStatus => _downloadStatus;
// _downloadStatus override
double _progress;
@override
double get progress => _progress;
// _onOpenDownload voidCallback , _isDownloading 선언
final VoidCallback _onOpenDownload;
bool _isDownloading = false;
// 다운로드 시작 DownloadStatus 다운로드 상태가 아닐때, _doSimulatedDownload 시작
@override
void startDownload() {
if (downloadStatus == DownloadStatus.notDownloaded) {
_doSimulatedDownload();
}
}
// 다운로드 중단, _isDownloading 가 false, _downloadStatus 가 DownloadStatus 가 다운로드 상태가 아닐때, _progress 가 0.0 일경우 이경우에 상태가 변경되었기 때문에 notifyListener 를 호출해 Provider 에서 알린다
@override
void stopDownload() {
if (_isDownloading) {
_isDownloading = false;
_downloadStatus = DownloadStatus.notDownloaded;
_progress = 0.0;
notifyListeners();
}
}
// DownloadStatus 가 downloaded 완료 되었을때 _onOpenDownload() 실행
@override
void openDownload() {
if (downloadStatus == DownloadStatus.downloaded) {
_onOpenDownload();
}
}
// 중요!! futer type _doSimulatedDownload 함수
Future<void> _doSimulatedDownload() async {
// _isDownloading 을 true , _downloadStatus 을 fetch 상태로 놓고 상태가 변경되었기 때문에 notifyListener 를 호출해 Provider 에서 알린다
_isDownloading = true;
_downloadStatus = DownloadStatus.fetchingDownload;
notifyListeners();
// fetch time 동안 1초 씩 progress delayed 시키기
await Future<void>.delayed(const Duration(seconds: 1));
// 만약 유저가 cancel button 누르면 download 중단 하고 return 시킴 (종료)
if (!_isDownloading) {
return;
}
// 다운로드 단계로 들어가는것으로 _downloadStatus 설정 후 notifyListener 를 호출해 Provider 에서 알린다
_downloadStatus = DownloadStatus.downloading;
notifyListeners();
// 다운로드 progeress 단계 임의로 설정
const downloadProgressStops = [0.0, 0.15, 0.45, 0.80, 1.0];
// 반복문 시작 하나씩 돌면서 1초씩 delayed 설정
for (final stop in downloadProgressStops) {
// 1초마다 downloadProgressStops 설정
await Future<void>.delayed(const Duration(seconds: 1));
// 만약 유저가 cancel button 누르면 반복문 종료 시킴
if (!_isDownloading) {
return;
}
// for loop 돌때마다 _progress 를 list 의 items 로 설정 즉, 완료 되면 _progress = 1.0 됨
_progress = stop;
// for loop 돌때마다 state 변경되기때문에 notifyListeners() 호출
notifyListeners();
}
// await Future<void>.delayed(const Duration(seconds: 1));
// If the user chose to cancel the download, stop the simulation.
// if (!_isDownloading) {
// return;
// }
// 다운로드 완료 되었기 때문에 DownloadStatus downloaded 로 설정하고 _isDownloading = false 하고 notifyListeners() 호출
_downloadStatus = DownloadStatus.downloaded;
_isDownloading = false;
notifyListeners();
}
}
// in download_button.dart
import 'package:cook_book_practice/download_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
class DownloadButton extends StatelessWidget {
// download 버튼 constructor
const DownloadButton({
Key? key,
required this.status,
this.downloadProgress = 0.0,
required this.onDownload,
required this.onCancel,
required this.onOpen,
this.transitionDuration = const Duration(milliseconds: 500),
}) : super(key: key);
// download 버튼 값 초기화
final DownloadStatus status;
final double downloadProgress;
final VoidCallback onDownload;
final VoidCallback onCancel;
final VoidCallback onOpen;
final Duration transitionDuration;
// dowinloading, fetching, downloaded getter
bool get _isDownloading => status == DownloadStatus.downloading;
bool get _isFetching => status == DownloadStatus.fetchingDownload;
bool get _isDownloaded => status == DownloadStatus.downloaded;
// _onPressed 함수 생성
void _onPressed() {
// 각 상태 일때
switch (status) {
case DownloadStatus.notDownloaded:
onDownload();
break;
case DownloadStatus.fetchingDownload:
// fetchingDownload 일때는 아무것도 하지 않기.
break;
case DownloadStatus.downloading:
onCancel();
break;
case DownloadStatus.downloaded:
onOpen();
break;
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
// 버튼 클릭 시 _onPressed() 실행
onTap: _onPressed,
// stack 으로 버튼이 겹치게 설정
child: Stack(
children: [
// _buildButtonShape 실행
_buildButtonShape(
child: _buildText(context),
),
// _buildDownloadingProgress 실행
_buildDownloadingProgress(),
],
),
);
}
Widget _buildButtonShape({
required Widget child,
}) {
return AnimatedContainer(
duration: transitionDuration,
curve: Curves.ease,
width: double.infinity,
decoration: _isDownloading || _isFetching
? ShapeDecoration(
shape: const CircleBorder(),
color: Colors.white.withOpacity(0.0),
)
: const ShapeDecoration(
shape: StadiumBorder(),
color: CupertinoColors.lightBackgroundGray,
),
child: child,
);
}
Widget _buildText(BuildContext context) {
final text = _isDownloaded ? 'OPEN' : 'GET';
final opacity = _isDownloading || _isFetching ? 0.0 : 1.0;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: AnimatedOpacity(
duration: transitionDuration,
opacity: opacity,
curve: Curves.ease,
child: Text(
text,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.button?.copyWith(
fontWeight: FontWeight.bold,
color: CupertinoColors.activeBlue,
),
),
),
);
}
Widget _buildDownloadingProgress() {
return Positioned.fill(
child: AnimatedOpacity(
duration: transitionDuration,
opacity: _isDownloading || _isFetching ? 1.0 : 0.0,
curve: Curves.ease,
child: Stack(
alignment: Alignment.center,
children: [
_buildProgressIndicator(),
if (_isDownloading)
const Icon(
Icons.stop,
size: 14.0,
color: CupertinoColors.activeBlue,
),
],
),
),
);
}
Widget _buildProgressIndicator() {
return AspectRatio(
aspectRatio: 1.0,
child: TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: downloadProgress),
duration: const Duration(milliseconds: 200),
builder: (context, progress, child) {
return CircularProgressIndicator(
backgroundColor: _isDownloading
? CupertinoColors.lightBackgroundGray
: Colors.white.withOpacity(0.0),
valueColor: AlwaysStoppedAnimation(_isFetching
? CupertinoColors.lightBackgroundGray
: CupertinoColors.activeBlue),
strokeWidth: 2.0,
value: _isFetching ? null : progress,
);
},
),
);
}
}
// in demo_app_icon.dart
import 'package:flutter/material.dart';
class DemoAppIcon extends StatelessWidget {
const DemoAppIcon({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const AspectRatio(
aspectRatio: 1.0,
child: FittedBox(
child: SizedBox(
width: 80.0,
height: 80.0,
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.red, Colors.blue],
),
borderRadius: BorderRadius.all(Radius.circular(20.0)),
),
child: Center(
child: Icon(
Icons.ac_unit,
color: Colors.white,
size: 40.0,
),
),
),
),
),
);
}
}
🔶 🔷 📌 🔑
Reference
Flutter cookbook - https://flutter.dev/docs/cookbook
Leave a comment