flutter_foldable_kit 1.1.0 copy "flutter_foldable_kit: ^1.1.0" to clipboard
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.

example/lib/main.dart

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,
                      ),
                    ),
                  ),
                ),
              ],
            ),
          ),
        );
      },
    );
  }
}
4
likes
160
points
50
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A modern, plug-and-play Flutter library for Foldable, Flip, and Dual-Screen devices. Easily build SplitView, OnHingeChange detection, FoldableTabView, and NavigationSplitView.

Repository (GitHub)
View/report issues

Topics

#foldable #split-view #navigation #flutter

License

MIT (license)

Dependencies

flutter

More

Packages that depend on flutter_foldable_kit