๐ Bikin Aplikasi Jadwal Penerbangan Offline Keren dengan Flutter Web (Pakai zapp.run) ✈️
- Dapatkan link
- X
- Aplikasi Lainnya
Halo gengs! ๐
Flight Schedule App ala Developer Kekinian
PRO TIP Bukan To Do List biasa, tapi versi premium yang nyimpen data di localStorage. Aman kayak password mantan! ๐
๐ฅ Fitur-Fitur Super Keren
๐ซ
Tambah Jadwal
Input rute & jam, klik Add, langsung muncul di list. Gampang banget!
✏️
Edit Jadwal
Salah ketik? Edit aja langsung, nggak perlu ribet hapus dulu.
✈️
Animasi Pesawat
Ada efek smooth kayak pesawat mau take off. Keren banget!
๐ป Kode Lengkap Aplikasi
lib/main.dart
// lib/main.dart
// Flight Schedule — Full Version (Web / zapp.run)
// Features: CRUD, Edit, Delete, Duplicate, Favorite, Search, Filter, Sort,
// Detail view, Confirm delete, Export/Import JSON (copy, download, upload),
// animated plane on scroll, per-item slide+fade, glassmorphism cards,
// gradient background, FAB pulse, dark/light toggle.
import 'dart:convert';
import 'dart:html' as html;
import 'dart:math';
import 'package:flutter/material.dart';
void main() => runApp(const FlightApp());
const String storageKey = 'flight_app_data_v2';
class FlightApp extends StatefulWidget {
const FlightApp({super.key});
@override
State createState() => _FlightAppState();
}
class _FlightAppState extends State {
bool _isDark = true;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flight Schedule - Full',
debugShowCheckedModeBanner: false,
theme: ThemeData(
brightness: _isDark ? Brightness.dark : Brightness.light,
primarySwatch: Colors.indigo,
useMaterial3: false,
),
home: FlightHomePage(
isDark: _isDark,
onToggleTheme: () => setState(() => _isDark = !_isDark),
),
);
}
}
class FlightItem {
String id;
String code;
String from;
String to;
String time;
bool favorite;
int iconIndex;
FlightItem({
required this.id,
required this.code,
required this.from,
required this.to,
required this.time,
this.favorite = false,
required this.iconIndex,
});
Map toJson() => {
'id': id,
'code': code,
'from': from,
'to': to,
'time': time,
'favorite': favorite,
'iconIndex': iconIndex,
};
factory FlightItem.fromJson(Map j) {
return FlightItem(
id: j['id'] ?? UniqueKey().toString(),
code: j['code'] ?? '',
from: j['from'] ?? '',
to: j['to'] ?? '',
time: j['time'] ?? '',
favorite: j['favorite'] ?? false,
iconIndex: j['iconIndex'] ?? 0,
);
}
}
class FlightHomePage extends StatefulWidget {
final bool isDark;
final VoidCallback onToggleTheme;
const FlightHomePage({super.key, required this.isDark, required this.onToggleTheme});
@override
State createState() => _FlightHomePageState();
}
class _FlightHomePageState extends State
with SingleTickerProviderStateMixin {
final ScrollController _scrollController = ScrollController();
final TextEditingController _searchCtrl = TextEditingController();
List _flights = [];
List _visible = [];
String _sortBy = 'Newest';
String _filterBy = 'All';
final Random _rnd = Random();
final List _planeIcons = [
Icons.flight_takeoff,
Icons.airplanemode_active,
Icons.flight,
Icons.flight_land,
Icons.local_airport,
];
late final AnimationController _fabController;
late final Animation _fabAnim;
double _planePositionFraction = 0.0;
@override
void initState() {
super.initState();
_fabController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 900),
);
_fabAnim = Tween(begin: 1.0, end: 1.07).animate(
CurvedAnimation(parent: _fabController, curve: Curves.easeInOut),
);
_fabController.repeat(reverse: true);
_searchCtrl.addListener(_applyFilters);
_scrollController.addListener(_onScroll);
_loadData();
}
void _onScroll() {
if (!_scrollController.hasClients) return;
final max = _scrollController.position.maxScrollExtent;
final pos = _scrollController.position.pixels;
final frac = max <= 0 ? 0.0 : (pos / max).clamp(0.0, 1.0);
setState(() => _planePositionFraction = frac);
}
@override
void dispose() {
_fabController.dispose();
_scrollController.dispose();
_searchCtrl.dispose();
super.dispose();
}
// ---------- Storage ----------
void _loadData() {
final raw = html.window.localStorage[storageKey];
if (raw == null) {
// seed examples
_flights = [
FlightItem(
id: UniqueKey().toString(),
code: 'GA123',
from: 'Jakarta (CGK)',
to: 'Bali (DPS)',
time: '2025-08-20 10:30',
iconIndex: _rnd.nextInt(_planeIcons.length),
),
FlightItem(
id: UniqueKey().toString(),
code: 'ID456',
from: 'Jakarta (CGK)',
to: 'Surabaya (SUB)',
time: '2025-08-20 12:00',
iconIndex: _rnd.nextInt(_planeIcons.length),
),
];
_saveData();
} else {
try {
final List decoded = jsonDecode(raw);
_flights = decoded
.map((e) => FlightItem.fromJson(Map.from(e)))
.toList();
} catch (e) {
// corrupt -> clear and start new
html.window.localStorage.remove(storageKey);
_flights = [];
}
}
_applyFilters();
}
void _saveData() {
final jsonData = _flights.map((f) => f.toJson()).toList();
html.window.localStorage[storageKey] = jsonEncode(jsonData);
}
// ---------- Filters & Sorting ----------
void _applyFilters() {
final q = _searchCtrl.text.trim().toLowerCase();
final f = _filterBy;
List list = _flights.where((flight) {
final matchesQuery = q.isEmpty ||
flight.code.toLowerCase().contains(q) ||
flight.from.toLowerCase().contains(q) ||
flight.to.toLowerCase().contains(q);
final matchesFilter = (f == 'All') ||
(f == 'Outbound' && flight.from.toLowerCase().contains('jakarta')) ||
(f == 'Inbound' && !flight.from.toLowerCase().contains('jakarta'));
return matchesQuery && matchesFilter;
}).toList();
list.sort((a, b) {
if (_sortBy == 'A → Z') return a.to.compareTo(b.to);
if (_sortBy == 'Z → A') return b.to.compareTo(a.to);
if (_sortBy == 'Earliest') {
final da = DateTime.tryParse(a.time);
final db = DateTime.tryParse(b.time);
if (da != null && db != null) return da.compareTo(db);
return a.time.compareTo(b.time);
}
if (_sortBy == 'Latest') {
final da = DateTime.tryParse(a.time);
final db = DateTime.tryParse(b.time);
if (da != null && db != null) return db.compareTo(da);
return b.time.compareTo(a.time);
}
// newest by insertion order
return _flights.indexOf(b).compareTo(_flights.indexOf(a));
});
setState(() => _visible = list);
}
// ---------- CRUD ----------
void _openAddEdit({FlightItem? existing, int? index}) {
final codeCtrl = TextEditingController(text: existing?.code ?? '');
final fromCtrl = TextEditingController(text: existing?.from ?? '');
final toCtrl = TextEditingController(text: existing?.to ?? '');
final timeCtrl = TextEditingController(text: existing?.time ?? '');
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) {
return AnimatedPadding(
duration: const Duration(milliseconds: 200),
padding:
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).canvasColor.withOpacity(0.98),
borderRadius: BorderRadius.circular(12),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.2), blurRadius: 8)],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(children: [
Expanded(
child: Text(
existing == null ? 'Tambah Jadwal Baru' : 'Edit Jadwal',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
)),
IconButton(onPressed: () => Navigator.pop(context), icon: const Icon(Icons.close))
]),
const SizedBox(height: 8),
TextField(controller: codeCtrl, decoration: const InputDecoration(labelText: 'Kode (ex: GA123)')),
const SizedBox(height: 8),
TextField(controller: fromCtrl, decoration: const InputDecoration(labelText: 'Keberangkatan (ex: Jakarta (CGK))')),
const SizedBox(height: 8),
TextField(controller: toCtrl, decoration: const InputDecoration(labelText: 'Tujuan (ex: Bali (DPS))')),
const SizedBox(height: 8),
TextField(controller: timeCtrl, decoration: const InputDecoration(labelText: 'Waktu (YYYY-MM-DD HH:MM or text)')),
const SizedBox(height: 12),
Row(children: [
Expanded(
child: ElevatedButton.icon(
icon: const Icon(Icons.save),
label: Text(existing == null ? 'Tambah' : 'Simpan Perubahan'),
onPressed: () {
final code = codeCtrl.text.trim();
final from = fromCtrl.text.trim();
final to = toCtrl.text.trim();
final time = timeCtrl.text.trim();
if (code.isEmpty || from.isEmpty || to.isEmpty || time.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Isi semua field')));
return;
}
if (existing == null) {
_flights.add(FlightItem(
id: UniqueKey().toString(),
code: code,
from: from,
to: to,
time: time,
iconIndex: _rnd.nextInt(_planeIcons.length),
));
} else {
_flights[index!] = FlightItem(
id: existing.id,
code: code,
from: from,
to: to,
time: time,
favorite: existing.favorite,
iconIndex: existing.iconIndex,
);
}
_saveData();
_applyFilters();
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.blueAccent),
),
),
const SizedBox(width: 8),
if (existing != null)
IconButton(
icon: const Icon(Icons.delete_forever, color: Colors.redAccent),
tooltip: 'Hapus',
onPressed: () {
Navigator.pop(context);
_confirmDelete(index!);
},
),
]),
],
),
),
);
},
);
}
void _confirmDelete(int index) {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text('Konfirmasi Hapus'),
content: const Text('Yakin ingin menghapus jadwal ini?'),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Batal')),
ElevatedButton(
onPressed: () {
_flights.removeAt(index);
_saveData();
_applyFilters();
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.redAccent),
child: const Text('Hapus'),
),
],
),
);
}
void _duplicate(int index) {
final src = _flights[index];
final dup = FlightItem(
id: UniqueKey().toString(),
code: '${src.code}_copy',
from: src.from,
to: src.to,
time: src.time,
favorite: src.favorite,
iconIndex: src.iconIndex,
);
_flights.add(dup);
_saveData();
_applyFilters();
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Jadwal diduplikasi')));
}
void _toggleFavorite(int index) {
_flights[index].favorite = !_flights[index].favorite;
_saveData();
_applyFilters();
}
// ---------- Export / Import ----------
void _exportJsonToDialog() {
final jsonData = jsonEncode(_flights.map((f) => f.toJson()).toList());
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text('Export JSON'),
content: SizedBox(
width: double.maxFinite,
child: SingleChildScrollView(child: SelectableText(jsonData)),
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Tutup')),
ElevatedButton(
onPressed: () {
final blob = Uri.dataFromString(jsonData, mimeType: 'application/json', encoding: utf8).toString();
final anchor = html.AnchorElement(href: blob)
..setAttribute('download', 'flight_export.json')
..click();
},
child: const Text('Download JSON'),
)
],
),
);
}
Future _importJsonFromFile() async {
final input = html.FileUploadInputElement();
input.accept = '.json,application/json,text/json';
input.click();
input.onChange.listen((_) {
final files = input.files;
if (files == null || files.isEmpty) return;
final file = files[0];
final reader = html.FileReader();
reader.readAsText(file);
reader.onLoad.listen((e) {
try {
final decoded = jsonDecode(reader.result as String) as List;
_flights = decoded.map((e) => FlightItem.fromJson(Map.from(e))).toList();
_saveData();
_applyFilters();
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Import file berhasil')));
} catch (err) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('File JSON tidak valid')));
}
});
});
}
void _importJsonPasteDialog() {
final ctrl = TextEditingController();
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text('Import JSON (paste)'),
content: TextField(controller: ctrl, maxLines: 8, decoration: const InputDecoration(hintText: 'Tempel JSON di sini')),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Batal')),
ElevatedButton(
onPressed: () {
try {
final decoded = jsonDecode(ctrl.text) as List;
_flights = decoded.map((e) => FlightItem.fromJson(Map.from(e))).toList();
_saveData();
_applyFilters();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Import berhasil')));
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('JSON tidak valid')));
}
},
child: const Text('Import'),
),
],
),
);
}
// ---------- Detail ----------
void _openDetail(FlightItem item) {
Navigator.push(
context,
PageRouteBuilder(
transitionDuration: const Duration(milliseconds: 420),
pageBuilder: (_, __, ___) => FlightDetailPage(item: item, icon: _planeIcons[item.iconIndex % _planeIcons.length]),
transitionsBuilder: (_, a, __, child) {
final curved = Curves.easeOut.transform(a.value);
return FadeTransition(opacity: a, child: Transform.translate(offset: Offset(0, 40 * (1 - curved)), child: child));
},
),
);
}
// ---------- UI pieces ----------
Widget _topBar() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
child: Row(children: [
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(color: Colors.white.withOpacity(0.04), borderRadius: BorderRadius.circular(12)),
child: Row(children: [
const Icon(Icons.search, color: Colors.white70),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: _searchCtrl,
style: const TextStyle(color: Colors.white),
decoration: const InputDecoration.collapsed(hintText: 'Cari kode / tujuan / keberangkatan', hintStyle: TextStyle(color: Colors.white54)),
),
),
if (_searchCtrl.text.isNotEmpty)
IconButton(onPressed: () { _searchCtrl.clear(); _applyFilters(); }, icon: const Icon(Icons.clear, color: Colors.white70))
]),
),
),
const SizedBox(width: 10),
PopupMenuButton(
color: Colors.grey[900],
icon: const Icon(Icons.tune, color: Colors.white),
onSelected: (v) {
if (v == 'export_dialog') _exportJsonToDialog();
else if (v == 'import_file') _importJsonFromFile();
else if (v == 'import_paste') _importJsonPasteDialog();
else {
if (v == 'All' || v == 'Outbound' || v == 'Inbound') _filterBy = v;
else _sortBy = v;
_applyFilters();
}
},
itemBuilder: (_) => >[
const PopupMenuItem(value: 'Newest', child: Text('Sort: Newest')),
const PopupMenuItem(value: 'Earliest', child: Text('Sort: Earliest')),
const PopupMenuItem(value: 'Latest', child: Text('Sort: Latest')),
const PopupMenuItem(value: 'A → Z', child: Text('Sort: Destination A → Z')),
const PopupMenuItem(value: 'Z → A', child: Text('Sort: Destination Z → A')),
const PopupMenuDivider(),
const PopupMenuItem(value: 'All', child: Text('Filter: All')),
const PopupMenuItem(value: 'Outbound', child: Text('Filter: Outbound (From Jakarta)')),
const PopupMenuItem(value: 'Inbound', child: Text('Filter: Inbound (Not Jakarta)')),
const PopupMenuDivider(),
const PopupMenuItem(value: 'export_dialog', child: Text('Export JSON (dialog & download)')),
const PopupMenuItem(value: 'import_file', child: Text('Import JSON (file upload)')),
const PopupMenuItem(value: 'import_paste', child: Text('Import JSON (paste)')),
],
),
const SizedBox(width: 8),
IconButton(
tooltip: 'Toggle theme',
onPressed: () => widget.onToggleTheme(),
icon: Icon(widget.isDark ? Icons.light_mode : Icons.dark_mode, color: Colors.white70),
),
]),
);
}
Widget _flightCard(FlightItem item, int index) {
return TweenAnimationBuilder(
key: ValueKey(item.id),
tween: Tween(begin: 0.0, end: 1.0),
duration: Duration(milliseconds: 420 + (index * 40)),
builder: (context, v, child) {
return Opacity(opacity: v, child: Transform.translate(offset: Offset(0, 30 * (1 - v)), child: child));
},
child: GestureDetector(
onTap: () => _openDetail(item),
child: Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14),
color: Colors.white.withOpacity(0.06),
border: Border.all(color: Colors.white.withOpacity(0.12)),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.25), blurRadius: 8, offset: const Offset(0, 4))],
),
child: Row(
children: [
// Animated plane icon slide-in
TweenAnimationBuilder(
tween: Tween(begin: const Offset(-0.6, 0), end: Offset.zero),
duration: const Duration(milliseconds: 520),
builder: (context, off, child) => Transform.translate(offset: off * 40, child: child),
child: Container(
width: 56,
height: 56,
decoration: BoxDecoration(
gradient: LinearGradient(colors: [Colors.blue.shade300, Colors.purple.shade300]),
shape: BoxShape.circle,
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.18), blurRadius: 6)],
),
child: const Icon(Icons.flight_takeoff, color: Colors.white),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(item.code, style: const TextStyle(color: Colors.white70, fontSize: 12)),
const SizedBox(height: 6),
Text('${item.from} → ${item.to}', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700)),
const SizedBox(height: 6),
Text(item.time, style: const TextStyle(color: Colors.white60)),
]),
),
Column(children: [
IconButton(
tooltip: 'Favourite',
icon: Icon(item.favorite ? Icons.star : Icons.star_border, color: item.favorite ? Colors.amber : Colors.white70),
onPressed: () {
final idx = _flights.indexWhere((f) => f.id == item.id);
if (idx >= 0) _toggleFavorite(idx);
},
),
Row(children: [
IconButton(
tooltip: 'Duplicate',
onPressed: () {
final idx = _flights.indexWhere((f) => f.id == item.id);
if (idx >= 0) _duplicate(idx);
},
icon: const Icon(Icons.copy, color: Colors.white70),
),
IconButton(
tooltip: 'Edit',
onPressed: () {
final idx = _flights.indexWhere((f) => f.id == item.id);
if (idx >= 0) _openAddEdit(existing: item, index: idx);
},
icon: const Icon(Icons.edit, color: Colors.amberAccent),
),
IconButton(
tooltip: 'Delete',
onPressed: () {
final idx = _flights.indexWhere((f) => f.id == item.id);
if (idx >= 0) _confirmDelete(idx);
},
icon: const Icon(Icons.delete, color: Colors.redAccent),
),
])
])
],
),
),
),
);
}
// Flying plane widget — moves horizontally across top based on scroll
Widget _flyingPlane() {
final width = MediaQuery.of(context).size.width;
final left = (width - 160) * _planePositionFraction;
return Positioned(
left: left,
top: 12,
child: Opacity(
opacity: 0.95,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.06),
borderRadius: BorderRadius.circular(30),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.18), blurRadius: 6)],
),
child: Row(children: const [
Icon(Icons.airplanemode_active, color: Colors.white),
SizedBox(width: 6),
Text('✈', style: TextStyle(color: Colors.white)),
]),
),
),
);
}
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: Colors.transparent,
body: Stack(children: [
Container(
padding: const EdgeInsets.only(top: 36, left: 12, right: 12, bottom: 12),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: isDark
? [Colors.indigo.shade900, Colors.deepPurple.shade800, Colors.pink.shade700]
: [Colors.blue.shade200, Colors.purple.shade200, Colors.pink.shade200],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Column(children: [
_topBar(),
const SizedBox(height: 6),
Expanded(
child: _visible.isEmpty
? Center(
child: Column(mainAxisSize: MainAxisSize.min, children: [
Icon(Icons.airplanemode_active, size: 72, color: Colors.white24),
const SizedBox(height: 12),
const Text('Tidak ada jadwal', style: TextStyle(color: Colors.white70, fontSize: 18)),
const SizedBox(height: 8),
const Text('Tekan tombol Tambah untuk menambahkan jadwal baru', style: TextStyle(color: Colors.white54), textAlign: TextAlign.center),
]),
)
: NotificationListener(
onNotification: (n) {
if (n.metrics.maxScrollExtent > 0) {
final frac = (n.metrics.pixels / n.metrics.maxScrollExtent).clamp(0.0, 1.0);
setState(() => _planePositionFraction = frac);
}
return false;
},
child: ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.only(top: 8, bottom: 90),
itemCount: _visible.length,
itemBuilder: (context, idx) => _flightCard(_visible[idx], idx),
),
),
),
]),
),
_flyingPlane(),
]),
floatingActionButton: ScaleTransition(
scale: _fabAnim,
child: FloatingActionButton.extended(
backgroundColor: Colors.orangeAccent,
icon: const Icon(Icons.add),
label: const Text('Tambah'),
onPressed: () => _openAddEdit(),
),
),
);
}
}
// Detail page
class FlightDetailPage extends StatelessWidget {
final FlightItem item;
final IconData icon;
const FlightDetailPage({super.key, required this.item, required this.icon});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(item.code), backgroundColor: Colors.transparent, elevation: 0),
backgroundColor: Colors.transparent,
body: Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(gradient: LinearGradient(colors: [Colors.blue.shade900, Colors.purple.shade800], begin: Alignment.topLeft, end: Alignment.bottomRight)),
child: Center(
child: Container(
padding: const EdgeInsets.all(18),
constraints: const BoxConstraints(maxWidth: 720),
decoration: BoxDecoration(color: Colors.white.withOpacity(0.06), borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.white.withOpacity(0.12)), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.25), blurRadius: 12, offset: const Offset(0, 6))]),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Icon(icon, size: 72, color: Colors.white),
const SizedBox(height: 12),
Text(item.code, style: const TextStyle(color: Colors.white70)),
const SizedBox(height: 8),
Text('${item.from} → ${item.to}', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 20)),
const SizedBox(height: 8),
Text(item.time, style: const TextStyle(color: Colors.white60)),
const SizedBox(height: 14),
const Divider(color: Colors.white12),
const SizedBox(height: 8),
Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [
_infoPill(Icons.event_seat, 'Gate A1'),
_infoPill(Icons.timer, 'On time'),
_infoPill(Icons.airline_stops, 'Non-stop'),
]),
const SizedBox(height: 14),
ElevatedButton.icon(onPressed: () => Navigator.pop(context), icon: const Icon(Icons.arrow_back), label: const Text('Kembali'), style: ElevatedButton.styleFrom(backgroundColor: Colors.blueAccent)),
]),
),
),
),
);
}
static Widget _infoPill(IconData icon, String text) {
return Column(children: [Icon(icon, color: Colors.white54), const SizedBox(height: 6), Text(text, style: const TextStyle(color: Colors.white70))]);
}
}
✨ Cara Menggunakan Kode Ini
1
Buka zapp.run
2
Buat project Flutter Web baru
3
Ganti kode di lib/main.dart
4
Tekan Run untuk melihat hasil
๐ก Kesimpulan
Dengan Flutter Web + zapp.run, kita bisa bikin aplikasi offline yang:
- Ringan - Nggak berat buat browser
- Keren - Desainnya modern dan menarik
- Bisa diakses dari browser tanpa download - Praktis banget
- Punya desain yang bikin betah lihatnya - UI/UX diperhatikan
Serius, ini pengalaman ngoding yang bikin nagih.
Kalau mau belajar sambil seneng-seneng, cobain deh bikin kayak gini.
Bagaimana menurut kalian? Tertarik buat coba bikin juga? ๐
- Dapatkan link
- X
- Aplikasi Lainnya
Komentar
Posting Komentar