smart_scroll 1.0.2 copy "smart_scroll: ^1.0.2" to clipboard
smart_scroll: ^1.0.2 copied to clipboard

The library simplifies working with lists, integrates and provides customization for list scrolling, pull refresh, loadMore .

example/lib/main.dart

import 'dart:math' as math;

import 'package:flutter/material.dart';
import 'package:smart_scroll/smart_scroll.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Smart Scroll Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const DemoSelector(),
    );
  }
}

// ─── Demo selector ─────────────────────────────────────────────────
class DemoSelector extends StatelessWidget {
  const DemoSelector({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Smart Scroll Demos')),
      body: ListView(
        children: [
          ListTile(
            title: const Text('Classic Header (WaterDrop)'),
            subtitle: const Text('Original indicator'),
            trailing: const Icon(Icons.chevron_right),
            onTap: () => Navigator.push(
              context,
              MaterialPageRoute(builder: (_) => const ClassicDemo()),
            ),
          ),
          const Divider(),
          ListTile(
            title: const Text('🆕 PlatformHeader Demo'),
            subtitle: const Text('iOS = Cupertino spinner, Android = Material'),
            trailing: const Icon(Icons.chevron_right),
            onTap: () => Navigator.push(
              context,
              MaterialPageRoute(builder: (_) => const PlatformHeaderDemo()),
            ),
          ),
          const Divider(),
          ListTile(
            title: const Text('🆕 BuilderHeader Demo'),
            subtitle:
                const Text('Custom indicator with dragProgress animation'),
            trailing: const Icon(Icons.chevron_right),
            onTap: () => Navigator.push(
              context,
              MaterialPageRoute(builder: (_) => const BuilderHeaderDemo()),
            ),
          ),
        ],
      ),
    );
  }
}

// ─── Classic Demo (original) ───────────────────────────────────────
class ClassicDemo extends StatefulWidget {
  const ClassicDemo({super.key});

  @override
  State<ClassicDemo> createState() => _ClassicDemoState();
}

class _ClassicDemoState extends State<ClassicDemo> {
  List<String> items = ["1", "2", "3", "4", "5", "6", "7", "8"];
  final RefreshController _refreshController =
      RefreshController(initialRefresh: false);

  void _onRefresh() async {
    await Future.delayed(const Duration(milliseconds: 1000));
    _refreshController.refreshCompleted();
  }

  void _onLoading() async {
    await Future.delayed(const Duration(milliseconds: 1000));
    items.add((items.length + 1).toString());
    if (mounted) setState(() {});
    _refreshController.loadComplete();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Classic Header')),
      body: SafeArea(
        child: SmartScroll(
          enablePullDown: true,
          enablePullUp: true,
          header: const WaterDropHeader(),
          controller: _refreshController,
          onRefresh: _onRefresh,
          onLoading: _onLoading,
          child: ListView.builder(
            itemBuilder: (c, i) => Card(child: Center(child: Text(items[i]))),
            itemExtent: 100.0,
            itemCount: items.length,
          ),
        ),
      ),
    );
  }
}

// ─── PlatformHeader Demo ───────────────────────────────────────────
// Tests the native-adaptive indicator (iOS = Cupertino, Android = Material)
class PlatformHeaderDemo extends StatefulWidget {
  const PlatformHeaderDemo({super.key});

  @override
  State<PlatformHeaderDemo> createState() => _PlatformHeaderDemoState();
}

class _PlatformHeaderDemoState extends State<PlatformHeaderDemo> {
  List<String> items = List.generate(15, (i) => 'Item ${i + 1}');
  final RefreshController _refreshController =
      RefreshController(initialRefresh: false);

  void _onRefresh() async {
    await Future.delayed(const Duration(milliseconds: 1500));
    _refreshController.refreshCompleted();
  }

  void _onLoading() async {
    await Future.delayed(const Duration(milliseconds: 1000));
    final start = items.length;
    items.addAll(List.generate(5, (i) => 'Item ${start + i + 1}'));
    if (mounted) setState(() {});
    _refreshController.loadComplete();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('PlatformHeader Demo'),
        actions: [
          Center(
            child: Padding(
              padding: const EdgeInsets.only(right: 16),
              child: Text(
                Theme.of(context).platform == TargetPlatform.iOS
                    ? '🍎 iOS Mode'
                    : '🤖 Android Mode',
                style: const TextStyle(fontWeight: FontWeight.bold),
              ),
            ),
          ),
        ],
      ),
      body: SafeArea(
        child: SmartScroll(
          enablePullDown: true,
          enablePullUp: true,
          controller: _refreshController,
          onRefresh: _onRefresh,
          onLoading: _onLoading,

          // ★ Native-adaptive header — auto picks Cupertino or Material
          header: PlatformHeader(),

          // ★ Native-adaptive footer
          footer: const PlatformFooter(),

          child: ListView.builder(
            itemBuilder: (c, i) => Card(
              margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
              child: ListTile(
                leading: CircleAvatar(child: Text('${i + 1}')),
                title: Text(items[i]),
                subtitle: const Text('Pull down to see native refresh'),
              ),
            ),
            itemCount: items.length,
          ),
        ),
      ),
    );
  }
}

// ─── BuilderHeader Demo ────────────────────────────────────────────
// Demonstrates the new BuilderHeader API with dragProgress-driven animation
class BuilderHeaderDemo extends StatefulWidget {
  const BuilderHeaderDemo({super.key});

  @override
  State<BuilderHeaderDemo> createState() => _BuilderHeaderDemoState();
}

class _BuilderHeaderDemoState extends State<BuilderHeaderDemo> {
  List<String> items = List.generate(15, (i) => 'Item ${i + 1}');
  final RefreshController _refreshController =
      RefreshController(initialRefresh: false);

  void _onRefresh() async {
    await Future.delayed(const Duration(milliseconds: 1500));
    _refreshController.refreshCompleted();
  }

  void _onLoading() async {
    await Future.delayed(const Duration(milliseconds: 1000));
    final start = items.length;
    items.addAll(List.generate(5, (i) => 'Item ${start + i + 1}'));
    if (mounted) setState(() {});
    _refreshController.loadComplete();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('BuilderHeader Demo')),
      body: SafeArea(
        child: SmartScroll(
          enablePullDown: true,
          enablePullUp: true,
          controller: _refreshController,
          onRefresh: _onRefresh,
          onLoading: _onLoading,

          // ★ NEW: BuilderHeader with full indicator data
          header: BuilderHeader(
            height: 80,
            triggerDistance: 80,
            builder: (context, indicator) {
              return _AnimatedRefreshIndicator(indicator: indicator);
            },
          ),

          // ★ NEW: BuilderFooter with full indicator data
          footer: BuilderFooter(
            builder: (context, indicator) {
              if (indicator.isNoMore) {
                return const SizedBox(
                  height: 60,
                  child: Center(
                    child: Text('No more data',
                        style: TextStyle(color: Colors.grey)),
                  ),
                );
              }
              if (indicator.isActive) {
                return const SizedBox(
                  height: 60,
                  child: Center(child: CircularProgressIndicator()),
                );
              }
              return const SizedBox(
                height: 60,
                child: Center(
                  child: Text('Pull to load more',
                      style: TextStyle(color: Colors.grey)),
                ),
              );
            },
          ),

          child: ListView.builder(
            itemBuilder: (c, i) => Card(
              margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
              child: ListTile(
                leading: CircleAvatar(child: Text('${i + 1}')),
                title: Text(items[i]),
                subtitle: Text('Subtitle for item ${i + 1}'),
              ),
            ),
            itemCount: items.length,
          ),
        ),
      ),
    );
  }
}

/// Custom animated indicator that uses dragProgress to drive visuals.
/// This is the kind of indicator you'd build with Lottie in production.
class _AnimatedRefreshIndicator extends StatelessWidget {
  final RefreshIndicatorData indicator;

  const _AnimatedRefreshIndicator({required this.indicator});

  @override
  Widget build(BuildContext context) {
    final progress = indicator.dragProgress;
    final mode = indicator.mode;

    return SizedBox(
      height: 80,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          // Animated icon that rotates based on drag progress
          _buildIcon(mode, progress),
          const SizedBox(height: 8),
          // Status text
          Text(
            _statusText(mode, progress),
            style: TextStyle(
              color: Colors.grey.shade600,
              fontSize: 12,
            ),
          ),
          // Progress bar showing exact drag position
          if (mode != RefreshStatus.refreshing &&
              mode != RefreshStatus.completed)
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 4),
              child: ClipRRect(
                borderRadius: BorderRadius.circular(4),
                child: LinearProgressIndicator(
                  value: progress.clamp(0.0, 1.0),
                  backgroundColor: Colors.grey.shade200,
                  valueColor: AlwaysStoppedAnimation(
                    indicator.triggered
                        ? Colors.green
                        : Colors.deepPurple.shade300,
                  ),
                ),
              ),
            ),
        ],
      ),
    );
  }

  Widget _buildIcon(RefreshStatus? mode, double progress) {
    if (mode == RefreshStatus.refreshing) {
      return const SizedBox(
        width: 24,
        height: 24,
        child: CircularProgressIndicator(strokeWidth: 2),
      );
    }
    if (mode == RefreshStatus.completed) {
      return const Icon(Icons.check_circle, color: Colors.green, size: 24);
    }
    if (mode == RefreshStatus.failed) {
      return const Icon(Icons.error, color: Colors.red, size: 24);
    }

    // Rotate arrow based on drag progress
    return Transform.rotate(
      angle: progress * math.pi, // 0° → 180° as user pulls
      child: Icon(
        Icons.arrow_downward,
        color: indicator.triggered ? Colors.green : Colors.deepPurple,
        size: 24,
      ),
    );
  }

  String _statusText(RefreshStatus? mode, double progress) {
    switch (mode) {
      case RefreshStatus.idle:
        return 'Pull to refresh (${(progress * 100).toInt()}%)';
      case RefreshStatus.canRefresh:
        return 'Release to refresh! ✓';
      case RefreshStatus.refreshing:
        return 'Refreshing...';
      case RefreshStatus.completed:
        return 'Done! ✓';
      case RefreshStatus.failed:
        return 'Failed ✗';
      default:
        return '';
    }
  }
}
7
likes
150
points
98
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

The library simplifies working with lists, integrates and provides customization for list scrolling, pull refresh, loadMore .

Repository (GitHub)
View/report issues
Contributing

License

BSL-1.0 (license)

Dependencies

flutter

More

Packages that depend on smart_scroll