flutter_foldable_kit 1.1.0
flutter_foldable_kit: ^1.1.0 copied to clipboard
A modern, plug-and-play Flutter library for Foldable, Flip, and Dual-Screen devices. Easily build SplitView, OnHingeChange detection, FoldableTabView, and NavigationSplitView.
import 'package:flutter/material.dart';
import 'package:flutter_foldable_kit/flutter_foldable_kit.dart';
void main() {
runApp(const FoldableDemoApp());
}
class FoldableDemoApp extends StatelessWidget {
const FoldableDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Foldable Kit Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorSchemeSeed: Colors.indigo,
brightness: Brightness.light,
),
darkTheme: ThemeData(
useMaterial3: true,
colorSchemeSeed: Colors.indigo,
brightness: Brightness.dark,
),
themeMode: ThemeMode.system,
home: const ShowcaseHome(),
);
}
}
class ShowcaseHome extends StatefulWidget {
const ShowcaseHome({super.key});
@override
State<ShowcaseHome> createState() => _ShowcaseHomeState();
}
class _ShowcaseHomeState extends State<ShowcaseHome> {
int _activeDemoIndex = 0;
final List<String> _eventLogs = [];
void _addLog(String message) {
setState(() {
final time = DateTime.now();
final stamp =
'${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}:${time.second.toString().padLeft(2, '0')}';
_eventLogs.insert(0, '[$stamp] $message');
if (_eventLogs.length > 20) {
_eventLogs.removeLast();
}
});
}
@override
Widget build(BuildContext context) {
return OnHingeChange(
onHingeChange: (info) =>
_addLog('Hinge changed: ${info.orientation.name}, ${info.posture.name}'),
onPostureChange: (posture) => _addLog('Posture changed: ${posture.name}'),
onOrientationChange: (orientation) =>
_addLog('Orientation changed: ${orientation.name}'),
onTabletopEnter: () => _addLog('Entered TABLETOP / Flex Mode! π»'),
onTabletopExit: () => _addLog('Exited Tabletop Mode'),
onBookModeEnter: () => _addLog('Entered BOOK MODE! π'),
onBookModeExit: () => _addLog('Exited Book Mode'),
child: Scaffold(
appBar: AppBar(
title: Row(
children: [
const Icon(Icons.devices_fold, size: 24),
const SizedBox(width: 8),
const Text(
'Foldable Kit',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: context.isFoldable
? Colors.green.withAlpha(50)
: Colors.orange.withAlpha(50),
borderRadius: BorderRadius.circular(12),
),
child: Text(
context.isFoldable
? (context.isTabletop ? 'Flex Mode' : 'Book Mode')
: 'Single Screen',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: context.isFoldable ? Colors.green : Colors.orange,
),
),
),
],
),
actions: [
IconButton(
tooltip: 'Reset Logs',
icon: const Icon(Icons.delete_outline),
onPressed: () => setState(() => _eventLogs.clear()),
),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(48),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildTabButton(0, 'SplitView', Icons.splitscreen),
_buildTabButton(1, 'Hinge Monitor', Icons.sensor_window),
_buildTabButton(2, 'TabView', Icons.tab),
_buildTabButton(3, 'Master-Detail', Icons.view_sidebar),
],
),
),
),
body: _buildCurrentDemo(),
),
);
}
Widget _buildTabButton(int index, String title, IconData icon) {
final isSelected = _activeDemoIndex == index;
final theme = Theme.of(context);
return InkWell(
onTap: () => setState(() => _activeDemoIndex = index),
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
children: [
Icon(
icon,
size: 18,
color: isSelected ? theme.colorScheme.primary : theme.hintColor,
),
const SizedBox(width: 6),
Text(
title,
style: TextStyle(
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? theme.colorScheme.primary : theme.hintColor,
),
),
],
),
),
);
}
Widget _buildCurrentDemo() {
switch (_activeDemoIndex) {
case 0:
return const SplitViewDemoPage();
case 1:
return HingeMonitorDemoPage(logs: _eventLogs);
case 2:
return const TabViewDemoPage();
case 3:
return const MasterDetailDemoPage();
default:
return const SplitViewDemoPage();
}
}
}
// ---------------------------------------------------------------------------
// 1. SplitView Demo
// ---------------------------------------------------------------------------
class SplitViewDemoPage extends StatefulWidget {
const SplitViewDemoPage({super.key});
@override
State<SplitViewDemoPage> createState() => _SplitViewDemoPageState();
}
class _SplitViewDemoPageState extends State<SplitViewDemoPage> {
double _ratio = 0.5;
@override
Widget build(BuildContext context) {
return FoldableSplitView(
ratio: _ratio,
startPane: Container(
color: Theme.of(context).colorScheme.surfaceContainerLow,
padding: const EdgeInsets.all(20),
child: ListView(
children: [
Text(
'Left / Primary Pane',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
const Text(
'FoldableSplitView adjusts intelligently. When a physical or simulated hinge is present, it aligns exactly with the crease boundaries without overlapping!',
),
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Non-Hinge Screen Ratio:'),
Slider(
value: _ratio,
min: 0.2,
max: 0.8,
divisions: 6,
label: '${(_ratio * 100).toInt()}%',
onChanged: (val) => setState(() => _ratio = val),
),
],
),
),
),
],
),
),
endPane: Container(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
padding: const EdgeInsets.all(20),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
context.isTabletop ? Icons.laptop : Icons.book,
size: 64,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 16),
Text(
'Right / Secondary Pane',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
context.isTabletop
? 'In Tabletop mode, this becomes the bottom control surface!'
: 'In Book mode, this sits comfortably on the right half of the fold.',
textAlign: TextAlign.center,
),
],
),
),
),
);
}
}
// ---------------------------------------------------------------------------
// 2. Hinge Monitor Demo
// ---------------------------------------------------------------------------
class HingeMonitorDemoPage extends StatelessWidget {
final List<String> logs;
const HingeMonitorDemoPage({super.key, required this.logs});
@override
Widget build(BuildContext context) {
final info = context.foldableInfo;
return ListView(
padding: const EdgeInsets.all(16),
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.info_outline, color: Colors.indigo),
const SizedBox(width: 8),
Text(
'Live Foldable State',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const Divider(),
_buildInfoRow('Has Hinge', '${info.hasHinge}'),
_buildInfoRow('Orientation', info.orientation.name),
_buildInfoRow('Posture', info.posture.name),
_buildInfoRow('Is Tabletop (Flex)', '${info.isTabletop}'),
_buildInfoRow('Is Book Mode', '${info.isBookMode}'),
_buildInfoRow('Hinge Bounds', '${info.hingeBounds}'),
_buildInfoRow('Hinge Thickness', '${info.hingeThickness} px'),
_buildInfoRow('Pane 1 Bounds', '${info.pane1Bounds}'),
_buildInfoRow('Pane 2 Bounds', '${info.pane2Bounds}'),
],
),
),
),
const SizedBox(height: 16),
Text(
'Hinge Event History (OnHingeChange)',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Card(
child: Container(
height: 220,
padding: const EdgeInsets.all(12),
child: logs.isEmpty
? const Center(
child: Text('No hinge events yet. Try switching presets!'))
: ListView.builder(
itemCount: logs.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Text(
logs[index],
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 13,
),
),
);
},
),
),
),
],
);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: const TextStyle(fontWeight: FontWeight.w600)),
Text(value, style: const TextStyle(fontFamily: 'monospace')),
],
),
);
}
}
// ---------------------------------------------------------------------------
// 3. TabView Demo
// ---------------------------------------------------------------------------
class TabViewDemoPage extends StatelessWidget {
const TabViewDemoPage({super.key});
@override
Widget build(BuildContext context) {
return FoldableTabView(
tabs: [
FoldableTabItem(
label: 'Dashboard',
icon: const Icon(Icons.dashboard_outlined),
activeIcon: const Icon(Icons.dashboard),
content: _buildTabPlaceholder(
context,
'Dashboard View',
Icons.speed,
Colors.blue,
),
),
FoldableTabItem(
label: 'Analytics',
icon: const Icon(Icons.analytics_outlined),
activeIcon: const Icon(Icons.analytics),
content: _buildTabPlaceholder(
context,
'Analytics & Insights',
Icons.bar_chart,
Colors.purple,
),
),
FoldableTabItem(
label: 'Messages',
icon: const Icon(Icons.chat_bubble_outline),
activeIcon: const Icon(Icons.chat_bubble),
content: _buildTabPlaceholder(
context,
'Direct Messages',
Icons.forum,
Colors.teal,
),
),
FoldableTabItem(
label: 'Settings',
icon: const Icon(Icons.settings_outlined),
activeIcon: const Icon(Icons.settings),
content: _buildTabPlaceholder(
context,
'App Settings',
Icons.tune,
Colors.amber,
),
),
],
);
}
Widget _buildTabPlaceholder(
BuildContext context,
String title,
IconData icon,
Color color,
) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircleAvatar(
radius: 36,
backgroundColor: color.withAlpha(40),
child: Icon(icon, size: 36, color: color),
),
const SizedBox(height: 16),
Text(
title,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 32),
child: Text(
'Notice how navigation adapts automatically: Rail on Book Mode, Bottom Bar on standard phone, and Flex controls on Tabletop!',
textAlign: TextAlign.center,
),
),
],
),
);
}
}
// ---------------------------------------------------------------------------
// 4. Master-Detail Navigation Demo
// ---------------------------------------------------------------------------
class NoteItem {
final String id;
final String title;
final String excerpt;
final String date;
final Color categoryColor;
const NoteItem({
required this.id,
required this.title,
required this.excerpt,
required this.date,
required this.categoryColor,
});
}
final sampleNotes = [
const NoteItem(
id: '1',
title: 'Foldable Device Architecture',
excerpt: 'Exploring display features, hinge bounds, and posture handling.',
date: '10:45 AM',
categoryColor: Colors.deepPurple,
),
const NoteItem(
id: '2',
title: 'Flutter 3.44 Engine Updates',
excerpt: 'Deep dive into Android Jetpack WindowManager bindings.',
date: 'Yesterday',
categoryColor: Colors.teal,
),
const NoteItem(
id: '3',
title: 'UI Design for Tabletop Flex Mode',
excerpt: 'How to arrange camera view on top and shutter controls on bottom.',
date: 'Sep 09',
categoryColor: Colors.orange,
),
const NoteItem(
id: '4',
title: 'Dual-Screen Book Mode',
excerpt: 'Enhancing reading experiences across foldable phone displays.',
date: 'Sep 08',
categoryColor: Colors.blue,
),
];
class MasterDetailDemoPage extends StatelessWidget {
const MasterDetailDemoPage({super.key});
@override
Widget build(BuildContext context) {
return FoldableNavigationSplitView<NoteItem>(
initialItem: sampleNotes.first,
masterBuilder: (context, selectedItem, onSelect) {
return Scaffold(
appBar: AppBar(
title: const Text('Notes Inbox'),
),
body: ListView.separated(
itemCount: sampleNotes.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
final note = sampleNotes[index];
final isSelected = selectedItem?.id == note.id;
return ListTile(
selected: isSelected,
leading: CircleAvatar(
backgroundColor: note.categoryColor.withAlpha(40),
child: Icon(Icons.note_alt, color: note.categoryColor),
),
title: Text(
note.title,
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(
note.excerpt,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: Text(
note.date,
style: Theme.of(context).textTheme.bodySmall,
),
onTap: () => onSelect(note),
);
},
),
);
},
detailBuilder: (context, item, isSplit) {
return Scaffold(
appBar: AppBar(
title: Text(item.title),
leading: isSplit ? null : const BackButton(),
),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Chip(
avatar: CircleAvatar(
backgroundColor: item.categoryColor,
),
label: Text(item.date),
),
const SizedBox(width: 8),
Chip(
label: Text(isSplit ? 'Split Mode' : 'Pushed Stack'),
),
],
),
const SizedBox(height: 16),
Text(
item.title,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Text(
item.excerpt,
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 24),
Card(
color: Theme.of(context).colorScheme.primaryContainer,
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(
'This note adapts to your screen mode. On a folded screen, opening a note pushes a standard route with Back navigation. On unfolded screens, it presents seamlessly on the right pane.',
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
),
),
],
),
),
);
},
);
}
}