← Back to Blog

Flutter SwitchListTile Localization: Toggle Settings for Multilingual Apps

flutterswitchlisttileswitchmateriallocalizationrtl

Flutter SwitchListTile Localization: Toggle Settings for Multilingual Apps

SwitchListTile is a Flutter Material Design widget that combines a ListTile with a Switch, creating a tappable row for toggling boolean settings. In multilingual applications, SwitchListTile must handle translated titles and subtitles of varying lengths, adapt switch positioning for RTL layouts, and provide localized accessibility announcements that describe both the setting name and its current on/off state.

Understanding SwitchListTile in Localization Context

SwitchListTile renders a list tile with a trailing (or leading in RTL) switch toggle, commonly used in settings screens, preferences, and feature toggles. For multilingual apps, this enables:

  • Translated setting titles and descriptive subtitles that may vary dramatically in length
  • Automatic RTL layout reversal where the switch moves to the leading side
  • Localized accessibility announcements for screen readers describing the toggle state
  • Consistent visual alignment across languages with different text densities

Why SwitchListTile Matters for Multilingual Apps

SwitchListTile provides:

  • Built-in RTL: Switch position automatically reverses in right-to-left locales
  • Flexible text area: Title and subtitle text wrap naturally to accommodate longer translations
  • Accessibility: Screen readers announce the localized title and current state together
  • Theming: Integrates with Material 3 color system for consistent cross-locale appearance

Basic SwitchListTile Implementation

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

class LocalizedSwitchListTileExample extends StatefulWidget {
  const LocalizedSwitchListTileExample({super.key});

  @override
  State<LocalizedSwitchListTileExample> createState() =>
      _LocalizedSwitchListTileExampleState();
}

class _LocalizedSwitchListTileExampleState
    extends State<LocalizedSwitchListTileExample> {
  bool _notificationsEnabled = true;
  bool _darkModeEnabled = false;
  bool _analyticsEnabled = true;

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.settingsTitle)),
      body: ListView(
        children: [
          SwitchListTile(
            title: Text(l10n.notificationsLabel),
            subtitle: Text(l10n.notificationsDescription),
            value: _notificationsEnabled,
            onChanged: (value) =>
                setState(() => _notificationsEnabled = value),
          ),
          SwitchListTile(
            title: Text(l10n.darkModeLabel),
            subtitle: Text(l10n.darkModeDescription),
            value: _darkModeEnabled,
            onChanged: (value) =>
                setState(() => _darkModeEnabled = value),
          ),
          SwitchListTile(
            title: Text(l10n.analyticsLabel),
            subtitle: Text(l10n.analyticsDescription),
            value: _analyticsEnabled,
            onChanged: (value) =>
                setState(() => _analyticsEnabled = value),
          ),
        ],
      ),
    );
  }
}

Advanced SwitchListTile Patterns for Localization

Settings Groups with Localized Section Headers

Settings screens typically organize toggles into themed groups with translated section headers.

class GroupedSettingsToggles extends StatefulWidget {
  const GroupedSettingsToggles({super.key});

  @override
  State<GroupedSettingsToggles> createState() => _GroupedSettingsTogglesState();
}

class _GroupedSettingsTogglesState extends State<GroupedSettingsToggles> {
  final Map<String, bool> _settings = {
    'push': true,
    'email': false,
    'sms': false,
    'biometric': true,
    'twoFactor': false,
    'autoUpdate': true,
  };

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

    return ListView(
      children: [
        Padding(
          padding: const EdgeInsetsDirectional.fromSTEB(16, 16, 16, 8),
          child: Text(
            l10n.notificationSettingsHeader,
            style: Theme.of(context).textTheme.titleSmall?.copyWith(
              color: Theme.of(context).colorScheme.primary,
            ),
          ),
        ),
        SwitchListTile(
          title: Text(l10n.pushNotificationsLabel),
          subtitle: Text(l10n.pushNotificationsDescription),
          value: _settings['push']!,
          onChanged: (v) => setState(() => _settings['push'] = v),
        ),
        SwitchListTile(
          title: Text(l10n.emailNotificationsLabel),
          subtitle: Text(l10n.emailNotificationsDescription),
          value: _settings['email']!,
          onChanged: (v) => setState(() => _settings['email'] = v),
        ),
        SwitchListTile(
          title: Text(l10n.smsNotificationsLabel),
          value: _settings['sms']!,
          onChanged: (v) => setState(() => _settings['sms'] = v),
        ),
        const Divider(),
        Padding(
          padding: const EdgeInsetsDirectional.fromSTEB(16, 16, 16, 8),
          child: Text(
            l10n.securitySettingsHeader,
            style: Theme.of(context).textTheme.titleSmall?.copyWith(
              color: Theme.of(context).colorScheme.primary,
            ),
          ),
        ),
        SwitchListTile(
          title: Text(l10n.biometricLoginLabel),
          subtitle: Text(l10n.biometricLoginDescription),
          value: _settings['biometric']!,
          onChanged: (v) => setState(() => _settings['biometric'] = v),
        ),
        SwitchListTile(
          title: Text(l10n.twoFactorLabel),
          subtitle: Text(l10n.twoFactorDescription),
          value: _settings['twoFactor']!,
          onChanged: (v) => setState(() => _settings['twoFactor'] = v),
        ),
      ],
    );
  }
}

SwitchListTile with Localized Secondary Actions

Some toggle rows need additional actions like info buttons or links to detailed settings, all with localized labels.

class SwitchTileWithActions extends StatefulWidget {
  const SwitchTileWithActions({super.key});

  @override
  State<SwitchTileWithActions> createState() => _SwitchTileWithActionsState();
}

class _SwitchTileWithActionsState extends State<SwitchTileWithActions> {
  bool _locationEnabled = false;

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

    return SwitchListTile(
      title: Text(l10n.locationServicesLabel),
      subtitle: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(l10n.locationServicesDescription),
          const SizedBox(height: 4),
          GestureDetector(
            onTap: () {},
            child: Text(
              l10n.learnMoreAboutLocationLabel,
              style: TextStyle(
                color: Theme.of(context).colorScheme.primary,
                decoration: TextDecoration.underline,
              ),
            ),
          ),
        ],
      ),
      secondary: Icon(
        _locationEnabled ? Icons.location_on : Icons.location_off,
        color: _locationEnabled
            ? Theme.of(context).colorScheme.primary
            : null,
      ),
      value: _locationEnabled,
      onChanged: (v) => setState(() => _locationEnabled = v),
    );
  }
}

Adaptive SwitchListTile with Dependent Settings

Some settings enable or disable dependent sub-settings. The disabled state must show localized explanatory text.

class DependentSwitchSettings extends StatefulWidget {
  const DependentSwitchSettings({super.key});

  @override
  State<DependentSwitchSettings> createState() =>
      _DependentSwitchSettingsState();
}

class _DependentSwitchSettingsState extends State<DependentSwitchSettings> {
  bool _syncEnabled = true;
  bool _syncOverWifi = true;
  bool _syncPhotos = false;

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

    return ListView(
      children: [
        SwitchListTile(
          title: Text(l10n.syncLabel),
          subtitle: Text(l10n.syncDescription),
          secondary: const Icon(Icons.sync),
          value: _syncEnabled,
          onChanged: (v) => setState(() => _syncEnabled = v),
        ),
        SwitchListTile(
          title: Text(l10n.wifiOnlySyncLabel),
          subtitle: Text(
            _syncEnabled
                ? l10n.wifiOnlySyncDescription
                : l10n.enableSyncFirstMessage,
          ),
          value: _syncOverWifi,
          onChanged: _syncEnabled
              ? (v) => setState(() => _syncOverWifi = v)
              : null,
        ),
        SwitchListTile(
          title: Text(l10n.syncPhotosLabel),
          subtitle: Text(
            _syncEnabled
                ? l10n.syncPhotosDescription
                : l10n.enableSyncFirstMessage,
          ),
          value: _syncPhotos,
          onChanged: _syncEnabled
              ? (v) => setState(() => _syncPhotos = v)
              : null,
        ),
      ],
    );
  }
}

RTL Support and Bidirectional Layouts

SwitchListTile automatically reverses its layout in RTL -- the switch moves to the leading (right-to-left start) side, and text aligns accordingly.

class BidirectionalSwitchListTile extends StatefulWidget {
  const BidirectionalSwitchListTile({super.key});

  @override
  State<BidirectionalSwitchListTile> createState() =>
      _BidirectionalSwitchListTileState();
}

class _BidirectionalSwitchListTileState
    extends State<BidirectionalSwitchListTile> {
  bool _rtlEnabled = false;
  bool _highContrast = false;

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

    return Column(
      children: [
        SwitchListTile(
          title: Text(l10n.rtlLayoutLabel),
          subtitle: Text(l10n.rtlLayoutDescription),
          secondary: const Icon(Icons.format_textdirection_r_to_l),
          value: _rtlEnabled,
          onChanged: (v) => setState(() => _rtlEnabled = v),
        ),
        SwitchListTile(
          title: Text(l10n.highContrastLabel),
          subtitle: Text(l10n.highContrastDescription),
          secondary: const Icon(Icons.contrast),
          value: _highContrast,
          onChanged: (v) => setState(() => _highContrast = v),
        ),
      ],
    );
  }
}

Testing SwitchListTile 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 LocalizedSwitchListTileExample(),
    );
  }

  testWidgets('SwitchListTile displays translated labels', (tester) async {
    await tester.pumpWidget(buildTestWidget());
    await tester.pumpAndSettle();
    expect(find.byType(SwitchListTile), findsWidgets);
  });

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

  testWidgets('Switch toggles correctly', (tester) async {
    await tester.pumpWidget(buildTestWidget());
    await tester.pumpAndSettle();

    await tester.tap(find.byType(SwitchListTile).first);
    await tester.pumpAndSettle();
  });
}

Best Practices

  1. Always provide a subtitle with descriptive translated text explaining what the setting does, as icon-less toggles can be ambiguous across cultures.

  2. Use section headers with Divider to group related SwitchListTile widgets, making settings screens navigable in all languages.

  3. Show localized disabled state messages when a toggle depends on another setting being enabled.

  4. Test with long translations to verify that titles and subtitles wrap correctly without clipping the switch control.

  5. Use secondary for icons that visually reinforce the setting's purpose, reducing reliance on text alone.

  6. Provide accessible state descriptions so screen readers announce both the translated label and current on/off state.

Conclusion

SwitchListTile is the standard widget for boolean settings in Flutter applications. For multilingual apps, it provides automatic RTL layout reversal, flexible text wrapping for translated labels, and built-in accessibility announcements. By organizing settings into localized groups, handling dependent toggles with translated state messages, and testing with verbose locales, you can build settings screens that work intuitively across all supported languages.

Further Reading