← Back to Blog

Flutter SliverList Localization: Scrollable Lists in Custom Scroll Views for Multilingual Apps

fluttersliverlistsliverlistlocalizationrtl

Flutter SliverList Localization: Scrollable Lists in Custom Scroll Views for Multilingual Apps

SliverList is a Flutter widget that creates a linear list of items inside a CustomScrollView. In multilingual applications, SliverList is essential for building scrollable lists with translated content that integrate with other slivers, creating lazy-loaded translated item lists that only build visible items, supporting RTL scroll alignment and text direction, and combining translated list sections with sliver headers and grids in a single scroll view.

Understanding SliverList in Localization Context

SliverList renders a list of children as a sliver, typically inside a CustomScrollView alongside other slivers like SliverAppBar or SliverGrid. For multilingual apps, this enables:

  • Translated list items in a sliver-based scroll layout
  • Lazy construction of localized items with SliverList.builder
  • RTL-aware list alignment that respects text direction
  • Combined sliver layouts with translated headers, lists, and grids

Why SliverList Matters for Multilingual Apps

SliverList provides:

  • Sliver integration: Translated lists that scroll alongside app bars, headers, and grids
  • Lazy building: Only visible items are built, efficient for long translated lists
  • Flexible delegates: SliverChildBuilderDelegate and SliverChildListDelegate for different use cases
  • Separator support: SliverList.separated for lists with dividers between translated items

Basic SliverList Implementation

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

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

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Scaffold(
      body: CustomScrollView(
        slivers: [
          SliverAppBar(
            expandedHeight: 150,
            pinned: true,
            flexibleSpace: FlexibleSpaceBar(
              title: Text(l10n.articlesTitle),
            ),
          ),
          SliverList.builder(
            itemCount: 20,
            itemBuilder: (context, index) {
              return ListTile(
                leading: CircleAvatar(
                  child: Text('${index + 1}'),
                ),
                title: Text('${l10n.articleLabel} ${index + 1}'),
                subtitle: Text(l10n.articleDescription),
                trailing: Icon(
                  Directionality.of(context) == TextDirection.rtl
                      ? Icons.chevron_left
                      : Icons.chevron_right,
                ),
                onTap: () {},
              );
            },
          ),
        ],
      ),
    );
  }
}

Advanced SliverList Patterns for Localization

Sectioned SliverList with Translated Headers

Multiple SliverLists separated by sticky translated section headers.

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

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    final sections = [
      _Section(l10n.todayLabel, [
        l10n.meetingWithTeamItem,
        l10n.codeReviewItem,
        l10n.designFeedbackItem,
      ]),
      _Section(l10n.tomorrowLabel, [
        l10n.clientCallItem,
        l10n.sprintPlanningItem,
      ]),
      _Section(l10n.thisWeekLabel, [
        l10n.releasePreparationItem,
        l10n.documentationItem,
        l10n.testingItem,
        l10n.deploymentItem,
      ]),
    ];

    return Scaffold(
      body: CustomScrollView(
        slivers: [
          SliverAppBar(
            pinned: true,
            title: Text(l10n.tasksTitle),
          ),
          for (final section in sections) ...[
            SliverToBoxAdapter(
              child: Container(
                padding: const EdgeInsetsDirectional.fromSTEB(16, 16, 16, 8),
                color: Theme.of(context).colorScheme.surfaceContainerHighest,
                child: Text(
                  section.title,
                  style: Theme.of(context).textTheme.titleSmall?.copyWith(
                        color: Theme.of(context).colorScheme.onSurfaceVariant,
                      ),
                ),
              ),
            ),
            SliverList.builder(
              itemCount: section.items.length,
              itemBuilder: (context, index) {
                return ListTile(
                  leading: Checkbox(
                    value: false,
                    onChanged: (value) {},
                  ),
                  title: Text(section.items[index]),
                );
              },
            ),
          ],
        ],
      ),
    );
  }
}

class _Section {
  final String title;
  final List<String> items;

  _Section(this.title, this.items);
}

SliverList with Separated Items

SliverList.separated with translated content and dividers between items.

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

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    final notifications = List.generate(15, (index) {
      return _Notification(
        title: '${l10n.notificationLabel} ${index + 1}',
        message: l10n.notificationMessage,
        time: '${index + 1}${l10n.hoursAgoSuffix}',
        isRead: index > 3,
      );
    });

    return Scaffold(
      body: CustomScrollView(
        slivers: [
          SliverAppBar(
            pinned: true,
            title: Text(l10n.notificationsTitle),
            actions: [
              TextButton(
                onPressed: () {},
                child: Text(l10n.markAllReadLabel),
              ),
            ],
          ),
          SliverList.separated(
            itemCount: notifications.length,
            itemBuilder: (context, index) {
              final notification = notifications[index];
              return ListTile(
                leading: CircleAvatar(
                  backgroundColor: notification.isRead
                      ? Theme.of(context).colorScheme.surfaceContainerHighest
                      : Theme.of(context).colorScheme.primaryContainer,
                  child: Icon(
                    notification.isRead
                        ? Icons.notifications_none
                        : Icons.notifications_active,
                    color: notification.isRead
                        ? Theme.of(context).colorScheme.onSurfaceVariant
                        : Theme.of(context).colorScheme.onPrimaryContainer,
                  ),
                ),
                title: Text(
                  notification.title,
                  style: TextStyle(
                    fontWeight:
                        notification.isRead ? FontWeight.normal : FontWeight.bold,
                  ),
                ),
                subtitle: Text(notification.message),
                trailing: Text(
                  notification.time,
                  style: Theme.of(context).textTheme.bodySmall,
                ),
              );
            },
            separatorBuilder: (context, index) {
              return const Divider(height: 1, indent: 72);
            },
          ),
        ],
      ),
    );
  }
}

class _Notification {
  final String title;
  final String message;
  final String time;
  final bool isRead;

  _Notification({
    required this.title,
    required this.message,
    required this.time,
    required this.isRead,
  });
}

Mixed Sliver Layout with Translated Content

A CustomScrollView combining SliverList with SliverAppBar and SliverToBoxAdapter for a rich translated layout.

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

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Scaffold(
      body: CustomScrollView(
        slivers: [
          SliverAppBar(
            expandedHeight: 200,
            pinned: true,
            flexibleSpace: FlexibleSpaceBar(
              title: Text(l10n.discoverTitle),
              background: Container(
                color: Theme.of(context).colorScheme.primaryContainer,
                child: Center(
                  child: Icon(
                    Icons.explore,
                    size: 80,
                    color: Theme.of(context).colorScheme.onPrimaryContainer,
                  ),
                ),
              ),
            ),
          ),
          SliverToBoxAdapter(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Text(
                l10n.featuredSectionLabel,
                style: Theme.of(context).textTheme.titleLarge,
              ),
            ),
          ),
          SliverList.builder(
            itemCount: 3,
            itemBuilder: (context, index) {
              return Card(
                margin: const EdgeInsetsDirectional.fromSTEB(16, 0, 16, 8),
                child: ListTile(
                  leading: const Icon(Icons.star),
                  title: Text('${l10n.featuredItemLabel} ${index + 1}'),
                  subtitle: Text(l10n.featuredItemDescription),
                ),
              );
            },
          ),
          SliverToBoxAdapter(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Text(
                l10n.recentSectionLabel,
                style: Theme.of(context).textTheme.titleLarge,
              ),
            ),
          ),
          SliverList.separated(
            itemCount: 10,
            itemBuilder: (context, index) {
              return ListTile(
                leading: CircleAvatar(child: Text('${index + 1}')),
                title: Text('${l10n.recentItemLabel} ${index + 1}'),
                subtitle: Text(l10n.recentItemDescription),
              );
            },
            separatorBuilder: (context, index) => const Divider(height: 1),
          ),
        ],
      ),
    );
  }
}

RTL Support and Bidirectional Layouts

SliverList respects the ambient text direction. List items align correctly in RTL layouts, and leading/trailing widgets swap positions automatically.

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

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;
    final isRtl = Directionality.of(context) == TextDirection.rtl;

    return Scaffold(
      body: CustomScrollView(
        slivers: [
          SliverAppBar(
            pinned: true,
            title: Text(l10n.contactsTitle),
          ),
          SliverList.builder(
            itemCount: 10,
            itemBuilder: (context, index) {
              return ListTile(
                leading: const CircleAvatar(child: Icon(Icons.person)),
                title: Text('${l10n.contactLabel} ${index + 1}'),
                subtitle: Text(l10n.contactSubtitle),
                trailing: Icon(
                  isRtl ? Icons.chevron_left : Icons.chevron_right,
                ),
              );
            },
          ),
        ],
      ),
    );
  }
}

Testing SliverList Localization

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

void main() {
  Widget buildTestWidget({Locale locale = const Locale('en')}) {
    return MaterialApp(
      locale: locale,
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
      home: const LocalizedSliverListExample(),
    );
  }

  testWidgets('SliverList renders localized items', (tester) async {
    await tester.pumpWidget(buildTestWidget());
    await tester.pumpAndSettle();
    expect(find.byType(CustomScrollView), findsOneWidget);
  });

  testWidgets('SliverList works in RTL', (tester) async {
    await tester.pumpWidget(buildTestWidget(locale: const Locale('ar')));
    await tester.pumpAndSettle();
    expect(tester.takeException(), isNull);
  });
}

Best Practices

  1. Use SliverList.builder for long translated lists to lazily build only visible items, keeping performance optimal regardless of list length.

  2. Use SliverList.separated to add dividers between translated list items without extra padding or manual separator logic.

  3. Combine SliverList with SliverToBoxAdapter for section headers that use translated text, creating structured scrollable layouts.

  4. Use EdgeInsetsDirectional for all padding in list items and headers so spacing adapts correctly in RTL layouts.

  5. Swap directional icons (chevron_left/chevron_right) based on Directionality.of(context) for list item trailing icons.

  6. Test with verbose translations to verify list items don't clip and text wraps correctly within each item's layout.

Conclusion

SliverList provides a sliver-based list widget for CustomScrollView layouts in Flutter. For multilingual apps, it handles translated list items with lazy building, supports separated lists with dividers, and integrates with other slivers for rich scrollable layouts. By combining SliverList with sectioned headers, mixed sliver layouts, and RTL-aware item alignment, you can build complex scrollable interfaces that display translated content efficiently across all supported languages.

Further Reading