indonesia_holidays 0.0.2
indonesia_holidays: ^0.0.2 copied to clipboard
A Dart package to fetch Indonesian national holidays using the Libur API.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:indonesia_holidays/indonesia_holidays.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Indonesia Holidays Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.red),
useMaterial3: true,
),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _selectedIndex = 0;
static const List<Widget> _pages = [
TodayStatusPage(),
HolidayListPage(),
DateCheckPage(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Indonesia Holidays Demo'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: _pages[_selectedIndex],
bottomNavigationBar: BottomNavigationBar(
items: const [
BottomNavigationBarItem(icon: Icon(Icons.today), label: 'Today'),
BottomNavigationBarItem(
icon: Icon(Icons.calendar_view_month),
label: 'Holidays',
),
BottomNavigationBarItem(
icon: Icon(Icons.search),
label: 'Check Date',
),
],
currentIndex: _selectedIndex,
onTap: (index) {
setState(() {
_selectedIndex = index;
});
},
),
);
}
}
class TodayStatusPage extends StatefulWidget {
const TodayStatusPage({super.key});
@override
State<TodayStatusPage> createState() => _TodayStatusPageState();
}
class _TodayStatusPageState extends State<TodayStatusPage> {
bool useCustomStyling = false;
bool useAnimation = true;
Duration animationDuration = const Duration(milliseconds: 500);
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Status Hari Ini',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
SwitchListTile(
title: const Text('Gunakan Styling Custom'),
value: useCustomStyling,
onChanged: (value) => setState(() => useCustomStyling = value),
),
SwitchListTile(
title: const Text('Gunakan Animasi'),
value: useAnimation,
onChanged: (value) => setState(() => useAnimation = value),
),
if (useAnimation) ...[
const Text('Durasi Animasi:'),
Slider(
value: animationDuration.inMilliseconds.toDouble(),
min: 100,
max: 2000,
divisions: 19,
label: '${animationDuration.inMilliseconds}ms',
onChanged: (value) => setState(
() => animationDuration = Duration(milliseconds: value.toInt()),
),
),
],
const SizedBox(height: 20),
if (useCustomStyling)
TodayHolidayStatus(
useAnimation: useAnimation,
animationDuration: animationDuration,
holidayBuilder: (context, check) => AnimatedContainer(
duration: animationDuration,
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Colors.green, Colors.lightGreen],
),
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.green.withValues(alpha: 0.3),
blurRadius: 10,
spreadRadius: 2,
),
],
),
child: Column(
children: [
const Text(
'🎉 HARI LIBUR!',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 10),
...check.holidayList.map(
(holiday) => Text(
holiday,
style: const TextStyle(
fontSize: 18,
color: Colors.white,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 10),
Text(
'Tanggal: ${check.date}',
style: const TextStyle(
fontSize: 16,
color: Colors.white70,
),
),
],
),
),
workingDayBuilder: (context, check) => AnimatedContainer(
duration: animationDuration,
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Colors.blue, Colors.lightBlue],
),
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.blue.withValues(alpha: 0.3),
blurRadius: 10,
spreadRadius: 2,
),
],
),
child: const Column(
children: [
Text(
'💼 HARI KERJA',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
SizedBox(height: 10),
Text(
'Semangat bekerja! 🚀',
style: TextStyle(
fontSize: 18,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
],
),
),
loadingBuilder: (context) => Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(15),
),
child: const Column(
children: [
CircularProgressIndicator(),
SizedBox(height: 10),
Text('Memeriksa status hari...'),
],
),
),
)
else
TodayHolidayStatus(
useAnimation: useAnimation,
animationDuration: animationDuration,
),
],
),
);
}
}
class HolidayListPage extends StatefulWidget {
const HolidayListPage({super.key});
@override
State<HolidayListPage> createState() => _HolidayListPageState();
}
class _HolidayListPageState extends State<HolidayListPage> {
int? selectedYear;
int? selectedMonth;
String language = 'id';
bool useCustomItems = false;
bool useSeparators = false;
bool horizontalScroll = false;
double? itemExtent;
String scrollPhysicsType = 'clamping';
final ScrollPhysics clampingPhysics = ClampingScrollPhysics();
final ScrollPhysics bouncingPhysics = BouncingScrollPhysics();
final ScrollPhysics alwaysScrollablePhysics = AlwaysScrollableScrollPhysics();
ScrollPhysics get scrollPhysics {
switch (scrollPhysicsType) {
case 'bouncing':
return bouncingPhysics;
case 'always':
return alwaysScrollablePhysics;
case 'clamping':
default:
return clampingPhysics;
}
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
padding: const EdgeInsets.all(16.0),
color: Colors.grey.shade100,
child: Column(
children: [
Row(
children: [
Expanded(
child: DropdownButton<int?>(
value: selectedYear,
hint: const Text('Pilih Tahun'),
items: List.generate(5, (index) {
int year = DateTime.now().year + index;
return DropdownMenuItem(
value: year,
child: Text('$year'),
);
}),
onChanged: (value) {
setState(() {
selectedYear = value;
});
},
),
),
const SizedBox(width: 16),
Expanded(
child: DropdownButton<int?>(
value: selectedMonth,
hint: const Text('Pilih Bulan'),
items: List.generate(12, (index) {
int month = index + 1;
return DropdownMenuItem(
value: month,
child: Text('$month'),
);
}),
onChanged: (value) {
setState(() {
selectedMonth = value;
});
},
),
),
const SizedBox(width: 16),
DropdownButton<String>(
value: language,
items: const [
DropdownMenuItem(value: 'id', child: Text('ID')),
DropdownMenuItem(value: 'en', child: Text('EN')),
],
onChanged: (value) {
setState(() {
language = value!;
});
},
),
],
),
const SizedBox(height: 16),
Wrap(
spacing: 16,
runSpacing: 8,
children: [
FilterChip(
label: const Text('Custom Items'),
selected: useCustomItems,
onSelected: (value) =>
setState(() => useCustomItems = value),
),
FilterChip(
label: const Text('Separators'),
selected: useSeparators,
onSelected: (value) =>
setState(() => useSeparators = value),
),
FilterChip(
label: const Text('Horizontal'),
selected: horizontalScroll,
onSelected: (value) =>
setState(() => horizontalScroll = value),
),
FilterChip(
label: const Text('Fixed Height'),
selected: itemExtent != null,
onSelected: (value) =>
setState(() => itemExtent = value ? 80.0 : null),
),
],
),
const SizedBox(height: 8),
DropdownButton<String>(
value: scrollPhysicsType,
items: const [
DropdownMenuItem(value: 'clamping', child: Text('Clamping')),
DropdownMenuItem(value: 'bouncing', child: Text('Bouncing')),
DropdownMenuItem(
value: 'always',
child: Text('Always Scrollable'),
),
],
onChanged: (value) {
setState(() {
scrollPhysicsType = value!;
});
},
),
],
),
),
Expanded(
child: HolidayListView(
year: selectedYear,
month: selectedMonth,
language: language,
scrollDirection: horizontalScroll ? Axis.horizontal : Axis.vertical,
physics: scrollPhysics,
padding: const EdgeInsets.all(16.0),
separatorBuilder: useSeparators
? (context, index) => Container(
height: 1,
color: Colors.grey.shade300,
margin: const EdgeInsets.symmetric(vertical: 4),
)
: null,
itemBuilder: useCustomItems
? (context, holiday) => horizontalScroll
? Container(
width: 300,
margin: const EdgeInsets.all(8),
child: Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.red.shade100,
Colors.red.shade50,
],
),
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.red.shade200,
shape: BoxShape.circle,
),
child: const Icon(
Icons.celebration,
color: Colors.white,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
holiday.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 8),
Text(
holiday.date,
style: TextStyle(
color: Colors.grey.shade600,
),
),
const SizedBox(height: 4),
Text(
'${holiday.dateTime.day}/${holiday.dateTime.month}',
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.red,
),
),
],
),
),
),
)
: Card(
elevation: 4,
margin: const EdgeInsets.symmetric(vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.red.shade100,
Colors.red.shade50,
],
),
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.all(16),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.red.shade200,
shape: BoxShape.circle,
),
child: const Icon(
Icons.celebration,
color: Colors.white,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
holiday.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Text(
holiday.date,
style: TextStyle(
color: Colors.grey.shade600,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.red.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${holiday.dateTime.day}/${holiday.dateTime.month}',
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
),
],
),
),
)
: null,
loadingBuilder: (context) => Container(
padding: const EdgeInsets.all(32),
child: const Column(
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Memuat daftar libur...'),
],
),
),
errorBuilder: (context, error) => Container(
padding: const EdgeInsets.all(32),
child: Column(
children: [
const Icon(Icons.error_outline, size: 48, color: Colors.red),
const SizedBox(height: 16),
Text(
'Terjadi kesalahan: ${error ?? 'Kesalahan tidak diketahui'}',
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => setState(() {}),
child: const Text('Coba Lagi'),
),
],
),
),
emptyBuilder: (context) => Container(
padding: const EdgeInsets.all(32),
child: const Column(
children: [
Icon(Icons.calendar_today, size: 48, color: Colors.grey),
SizedBox(height: 16),
Text(
'Tidak ada hari libur ditemukan',
style: TextStyle(fontSize: 16),
),
],
),
),
),
),
],
);
}
}
class DateCheckPage extends StatefulWidget {
const DateCheckPage({super.key});
@override
State<DateCheckPage> createState() => _DateCheckPageState();
}
class _DateCheckPageState extends State<DateCheckPage> {
DateTime selectedDate = DateTime.now();
Future<HolidayCheck>? checkResult;
Future<bool>? isHolidayResult;
Future<String?>? holidayNameResult;
void _checkDate() {
setState(() {
checkResult = IndonesiaHolidays.checkDate(
selectedDate.year,
selectedDate.month,
selectedDate.day,
);
isHolidayResult = selectedDate.isHoliday();
holidayNameResult = selectedDate.getHolidayName();
});
}
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Pilih Tanggal untuk Dicek',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
ElevatedButton.icon(
onPressed: () async {
final picked = await showDatePicker(
context: context,
initialDate: selectedDate,
firstDate: DateTime(2020),
lastDate: DateTime(2030),
);
if (picked != null) {
setState(() {
selectedDate = picked;
// Reset results when date changes
checkResult = null;
isHolidayResult = null;
holidayNameResult = null;
});
}
},
icon: const Icon(Icons.calendar_today),
label: Text(
'Pilih Tanggal: ${selectedDate.day}/${selectedDate.month}/${selectedDate.year}',
),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _checkDate,
icon: const Icon(Icons.search),
label: const Text('Cek Apakah Libur'),
),
],
),
),
),
const SizedBox(height: 20),
if (checkResult != null) ...[
const Text(
'Hasil Pengecekan (Method Manual):',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
FutureBuilder<HolidayCheck>(
future: checkResult,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Card(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Row(
children: [
CircularProgressIndicator(),
SizedBox(width: 16),
Text('Memeriksa...'),
],
),
),
);
} else if (snapshot.hasError) {
return Card(
color: Colors.red.shade50,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text('Error: ${snapshot.error}'),
),
);
} else if (snapshot.hasData) {
final check = snapshot.data!;
return Card(
color: check.isHoliday
? Colors.green.shade50
: Colors.blue.shade50,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
check.isHoliday
? Icons.celebration
: Icons.work,
color: check.isHoliday
? Colors.green
: Colors.blue,
),
const SizedBox(width: 8),
Text(
check.isHoliday
? '🎉 Hari Libur!'
: '💼 Hari Kerja',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
if (check.isHoliday &&
check.holidayList.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
'Libur: ${check.holidayList.join(', ')}',
style: const TextStyle(fontSize: 16),
),
],
const SizedBox(height: 8),
Text(
'Tanggal: ${check.date}',
style: TextStyle(color: Colors.grey.shade600),
),
],
),
),
);
}
return const SizedBox();
},
),
const SizedBox(height: 20),
const Text(
'Hasil Pengecekan (Extension Methods):',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
Row(
children: [
Expanded(
child: Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
const Text(
'Apakah Libur?',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
if (isHolidayResult != null)
FutureBuilder<bool>(
future: isHolidayResult!,
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return const CircularProgressIndicator();
}
return Text(
snapshot.data == true ? 'Ya' : 'Tidak',
style: TextStyle(
fontSize: 18,
color: snapshot.data == true
? Colors.green
: Colors.blue,
fontWeight: FontWeight.bold,
),
);
},
),
],
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
const Text(
'Nama Libur',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
FutureBuilder<String?>(
future: holidayNameResult,
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return const CircularProgressIndicator();
}
return Text(
snapshot.data ?? 'Tidak ada',
style: const TextStyle(fontSize: 16),
textAlign: TextAlign.center,
);
},
),
],
),
),
),
),
],
),
],
],
),
);
}
}