diff --git a/app/lib/bloc/settings/theme.dart b/app/lib/bloc/settings/theme.dart
new file mode 100644
index 00000000..e835e1d3
--- /dev/null
+++ b/app/lib/bloc/settings/theme.dart
@@ -0,0 +1,139 @@
+import 'dart:async';
+
+import 'package:bloc/bloc.dart';
+import 'package:copy_with/copy_with.dart';
+import 'package:event_bus/event_bus.dart';
+import 'package:flutter/material.dart';
+import 'package:kiwi/kiwi.dart';
+import 'package:logging/logging.dart';
+import 'package:nc_photos/di_container.dart';
+import 'package:nc_photos/event/event.dart';
+import 'package:nc_photos/theme.dart';
+import 'package:np_codegen/np_codegen.dart';
+import 'package:to_string/to_string.dart';
+
+part 'theme.g.dart';
+
+@autoCopyWith
+@toString
+class ThemeSettingsState {
+  const ThemeSettingsState({
+    required this.isFollowSystemTheme,
+    required this.isUseBlackInDarkTheme,
+    required this.seedColor,
+  });
+
+  @override
+  String toString() => _$toString();
+
+  final bool isFollowSystemTheme;
+  final bool isUseBlackInDarkTheme;
+  final Color seedColor;
+}
+
+abstract class ThemeSettingsEvent {
+  const ThemeSettingsEvent();
+}
+
+@toString
+class ThemeSettingsSetFollowSystemTheme extends ThemeSettingsEvent {
+  const ThemeSettingsSetFollowSystemTheme(this.value);
+
+  @override
+  String toString() => _$toString();
+
+  final bool value;
+}
+
+@toString
+class ThemeSettingsSetUseBlackInDarkTheme extends ThemeSettingsEvent {
+  const ThemeSettingsSetUseBlackInDarkTheme(this.value, this.theme);
+
+  @override
+  String toString() => _$toString();
+
+  final bool value;
+  final ThemeData theme;
+}
+
+@toString
+class ThemeSettingsSetSeedColor extends ThemeSettingsEvent {
+  const ThemeSettingsSetSeedColor(this.value);
+
+  @override
+  String toString() => _$toString();
+
+  final Color value;
+}
+
+class ThemeSettingsError {
+  const ThemeSettingsError(this.ev, [this.error, this.stackTrace]);
+
+  final ThemeSettingsEvent ev;
+  final Object? error;
+  final StackTrace? stackTrace;
+}
+
+@npLog
+class ThemeSettingsBloc extends Bloc<ThemeSettingsEvent, ThemeSettingsState> {
+  ThemeSettingsBloc(DiContainer c)
+      : assert(require(c)),
+        _c = c,
+        super(ThemeSettingsState(
+          isFollowSystemTheme: c.pref.isFollowSystemThemeOr(false),
+          isUseBlackInDarkTheme: c.pref.isUseBlackInDarkThemeOr(false),
+          seedColor: getSeedColor(),
+        )) {
+    on<ThemeSettingsSetFollowSystemTheme>(_onSetFollowSystemTheme);
+    on<ThemeSettingsSetUseBlackInDarkTheme>(_onSetUseBlackInDarkTheme);
+    on<ThemeSettingsSetSeedColor>(_onSetSeedColor);
+  }
+
+  static bool require(DiContainer c) => DiContainer.has(c, DiType.pref);
+
+  Stream<ThemeSettingsError> errorStream() => _errorStream.stream;
+
+  Future<void> _onSetFollowSystemTheme(ThemeSettingsSetFollowSystemTheme ev,
+      Emitter<ThemeSettingsState> emit) async {
+    final oldValue = state.isFollowSystemTheme;
+    emit(state.copyWith(isFollowSystemTheme: ev.value));
+    if (await _c.pref.setFollowSystemTheme(ev.value)) {
+      KiwiContainer().resolve<EventBus>().fire(ThemeChangedEvent());
+    } else {
+      _log.severe("[_onSetFollowSystemTheme] Failed writing pref");
+      _errorStream.add(ThemeSettingsError(ev));
+      emit(state.copyWith(isFollowSystemTheme: oldValue));
+    }
+  }
+
+  Future<void> _onSetUseBlackInDarkTheme(ThemeSettingsSetUseBlackInDarkTheme ev,
+      Emitter<ThemeSettingsState> emit) async {
+    final oldValue = state.isUseBlackInDarkTheme;
+    emit(state.copyWith(isUseBlackInDarkTheme: ev.value));
+    if (await _c.pref.setUseBlackInDarkTheme(ev.value)) {
+      if (ev.theme.brightness == Brightness.dark) {
+        KiwiContainer().resolve<EventBus>().fire(ThemeChangedEvent());
+      }
+    } else {
+      _log.severe("[_onSetUseBlackInDarkTheme] Failed writing pref");
+      _errorStream.add(ThemeSettingsError(ev));
+      emit(state.copyWith(isUseBlackInDarkTheme: oldValue));
+    }
+  }
+
+  Future<void> _onSetSeedColor(
+      ThemeSettingsSetSeedColor ev, Emitter<ThemeSettingsState> emit) async {
+    final oldValue = state.seedColor;
+    emit(state.copyWith(seedColor: ev.value));
+    if (await _c.pref.setSeedColor(ev.value.withAlpha(0xFF).value)) {
+      KiwiContainer().resolve<EventBus>().fire(ThemeChangedEvent());
+    } else {
+      _log.severe("[_onSetSeedColor] Failed writing pref");
+      _errorStream.add(ThemeSettingsError(ev));
+      emit(state.copyWith(seedColor: oldValue));
+    }
+  }
+
+  final DiContainer _c;
+  final _errorStream = StreamController<ThemeSettingsError>.broadcast();
+}
diff --git a/app/lib/bloc/settings/theme.g.dart b/app/lib/bloc/settings/theme.g.dart
new file mode 100644
index 00000000..2d62caef
--- /dev/null
+++ b/app/lib/bloc/settings/theme.g.dart
@@ -0,0 +1,74 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'theme.dart';
+
+// **************************************************************************
+// CopyWithGenerator
+// **************************************************************************
+
+extension $ThemeSettingsStateCopyWith on ThemeSettingsState {
+  ThemeSettingsState copyWith(
+          {bool? isFollowSystemTheme,
+          bool? isUseBlackInDarkTheme,
+          Color? seedColor}) =>
+      _$copyWith(
+          isFollowSystemTheme: isFollowSystemTheme,
+          isUseBlackInDarkTheme: isUseBlackInDarkTheme,
+          seedColor: seedColor);
+
+  ThemeSettingsState _$copyWith(
+      {bool? isFollowSystemTheme,
+      bool? isUseBlackInDarkTheme,
+      Color? seedColor}) {
+    return ThemeSettingsState(
+        isFollowSystemTheme: isFollowSystemTheme ?? this.isFollowSystemTheme,
+        isUseBlackInDarkTheme:
+            isUseBlackInDarkTheme ?? this.isUseBlackInDarkTheme,
+        seedColor: seedColor ?? this.seedColor);
+  }
+}
+
+// **************************************************************************
+// NpLogGenerator
+// **************************************************************************
+
+extension _$ThemeSettingsBlocNpLog on ThemeSettingsBloc {
+  // ignore: unused_element
+  Logger get _log => log;
+
+  static final log = Logger("bloc.settings.theme.ThemeSettingsBloc");
+}
+
+// **************************************************************************
+// ToStringGenerator
+// **************************************************************************
+
+extension _$ThemeSettingsStateToString on ThemeSettingsState {
+  String _$toString() {
+    // ignore: unnecessary_string_interpolations
+    return "ThemeSettingsState {isFollowSystemTheme: $isFollowSystemTheme, isUseBlackInDarkTheme: $isUseBlackInDarkTheme, seedColor: $seedColor}";
+  }
+}
+
+extension _$ThemeSettingsSetFollowSystemThemeToString
+    on ThemeSettingsSetFollowSystemTheme {
+  String _$toString() {
+    // ignore: unnecessary_string_interpolations
+    return "ThemeSettingsSetFollowSystemTheme {value: $value}";
+  }
+}
+
+extension _$ThemeSettingsSetUseBlackInDarkThemeToString
+    on ThemeSettingsSetUseBlackInDarkTheme {
+  String _$toString() {
+    // ignore: unnecessary_string_interpolations
+    return "ThemeSettingsSetUseBlackInDarkTheme {value: $value, theme: $theme}";
+  }
+}
+
+extension _$ThemeSettingsSetSeedColorToString on ThemeSettingsSetSeedColor {
+  String _$toString() {
+    // ignore: unnecessary_string_interpolations
+    return "ThemeSettingsSetSeedColor {value: $value}";
+  }
+}
diff --git a/app/lib/widget/settings/theme_settings.dart b/app/lib/widget/settings/theme_settings.dart
index ae3ba688..7d761f83 100644
--- a/app/lib/widget/settings/theme_settings.dart
+++ b/app/lib/widget/settings/theme_settings.dart
@@ -1,39 +1,63 @@
-import 'package:event_bus/event_bus.dart';
+import 'dart:async';
+
 import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:flutter_colorpicker/flutter_colorpicker.dart';
 import 'package:kiwi/kiwi.dart';
 import 'package:logging/logging.dart';
 import 'package:nc_photos/app_localizations.dart';
-import 'package:nc_photos/event/event.dart';
+import 'package:nc_photos/bloc/settings/theme.dart';
+import 'package:nc_photos/di_container.dart';
 import 'package:nc_photos/k.dart' as k;
 import 'package:nc_photos/mobile/android/android_info.dart';
 import 'package:nc_photos/platform/k.dart' as platform_k;
-import 'package:nc_photos/pref.dart';
 import 'package:nc_photos/snack_bar_manager.dart';
 import 'package:nc_photos/theme.dart';
 import 'package:np_codegen/np_codegen.dart';
 
 part 'theme_settings.g.dart';
 
-class ThemeSettings extends StatefulWidget {
+class ThemeSettings extends StatelessWidget {
   const ThemeSettings({super.key});
 
   @override
-  createState() => _ThemeSettingsState();
+  Widget build(BuildContext context) {
+    return BlocProvider(
+      create: (_) => ThemeSettingsBloc(KiwiContainer().resolve<DiContainer>()),
+      child: const _WrappedThemeSettings(),
+    );
+  }
+}
+
+class _WrappedThemeSettings extends StatefulWidget {
+  const _WrappedThemeSettings();
+
+  @override
+  State<StatefulWidget> createState() => _WrappedThemeSettingsState();
 }
 
 @npLog
-class _ThemeSettingsState extends State<ThemeSettings> {
+class _WrappedThemeSettingsState extends State<_WrappedThemeSettings> {
   @override
-  initState() {
+  void initState() {
     super.initState();
-    _isFollowSystemTheme = Pref().isFollowSystemThemeOr(false);
-    _isUseBlackInDarkTheme = Pref().isUseBlackInDarkThemeOr(false);
-    _seedColor = getSeedColor();
+    _errorSubscription =
+        context.read<ThemeSettingsBloc>().errorStream().listen((_) {
+      SnackBarManager().showSnackBar(SnackBar(
+        content: Text(L10n.global().writePreferenceFailureNotification),
+        duration: k.snackBarDurationNormal,
+      ));
+    });
   }
 
   @override
-  build(BuildContext context) {
+  void dispose() {
+    _errorSubscription.cancel();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
     return Scaffold(
       body: Builder(
         builder: (context) => _buildContent(context),
@@ -51,32 +75,60 @@ class _ThemeSettingsState extends State<ThemeSettings> {
         SliverList(
           delegate: SliverChildListDelegate(
             [
-              ListTile(
-                title: Text(L10n.global().settingsSeedColorTitle),
-                subtitle: Text(L10n.global().settingsSeedColorDescription),
-                trailing: Icon(
-                  Icons.circle,
-                  size: 32,
-                  color: _seedColor,
-                ),
-                onTap: () => _onSeedColorPressed(context),
+              BlocBuilder<ThemeSettingsBloc, ThemeSettingsState>(
+                buildWhen: (previous, current) =>
+                    previous.seedColor != current.seedColor,
+                builder: (context, state) {
+                  return ListTile(
+                    title: Text(L10n.global().settingsSeedColorTitle),
+                    subtitle: Text(L10n.global().settingsSeedColorDescription),
+                    trailing: Icon(
+                      Icons.circle,
+                      size: 32,
+                      color: state.seedColor,
+                    ),
+                    onTap: () => _onSeedColorPressed(context),
+                  );
+                },
               ),
               if (platform_k.isAndroid &&
                   AndroidInfo().sdkInt >= AndroidVersion.Q)
-                SwitchListTile(
-                  title: Text(L10n.global().settingsFollowSystemThemeTitle),
-                  value: _isFollowSystemTheme,
-                  onChanged: (value) => _onFollowSystemThemeChanged(value),
+                BlocBuilder<ThemeSettingsBloc, ThemeSettingsState>(
+                  buildWhen: (previous, current) =>
+                      previous.isFollowSystemTheme !=
+                      current.isFollowSystemTheme,
+                  builder: (context, state) {
+                    return SwitchListTile(
+                      title: Text(L10n.global().settingsFollowSystemThemeTitle),
+                      value: state.isFollowSystemTheme,
+                      onChanged: (value) {
+                        context
+                            .read<ThemeSettingsBloc>()
+                            .add(ThemeSettingsSetFollowSystemTheme(value));
+                      },
+                    );
+                  },
                 ),
-              SwitchListTile(
-                title: Text(L10n.global().settingsUseBlackInDarkThemeTitle),
-                subtitle: Text(_isUseBlackInDarkTheme
-                    ? L10n.global().settingsUseBlackInDarkThemeTrueDescription
-                    : L10n.global()
-                        .settingsUseBlackInDarkThemeFalseDescription),
-                value: _isUseBlackInDarkTheme,
-                onChanged: (value) =>
-                    _onUseBlackInDarkThemeChanged(context, value),
+              BlocBuilder<ThemeSettingsBloc, ThemeSettingsState>(
+                buildWhen: (previous, current) =>
+                    previous.isUseBlackInDarkTheme !=
+                    current.isUseBlackInDarkTheme,
+                builder: (context, state) {
+                  return SwitchListTile(
+                    title: Text(L10n.global().settingsUseBlackInDarkThemeTitle),
+                    subtitle: Text(state.isUseBlackInDarkTheme
+                        ? L10n.global()
+                            .settingsUseBlackInDarkThemeTrueDescription
+                        : L10n.global()
+                            .settingsUseBlackInDarkThemeFalseDescription),
+                    value: state.isUseBlackInDarkTheme,
+                    onChanged: (value) {
+                      context.read<ThemeSettingsBloc>().add(
+                          ThemeSettingsSetUseBlackInDarkTheme(
+                              value, Theme.of(context)));
+                    },
+                  );
+                },
               ),
             ],
           ),
@@ -85,47 +137,6 @@ class _ThemeSettingsState extends State<ThemeSettings> {
     );
   }
 
-  Future<void> _onFollowSystemThemeChanged(bool value) async {
-    final oldValue = _isFollowSystemTheme;
-    setState(() {
-      _isFollowSystemTheme = value;
-    });
-    if (await Pref().setFollowSystemTheme(value)) {
-      KiwiContainer().resolve<EventBus>().fire(ThemeChangedEvent());
-    } else {
-      _log.severe("[_onFollowSystemThemeChanged] Failed writing pref");
-      SnackBarManager().showSnackBar(SnackBar(
-        content: Text(L10n.global().writePreferenceFailureNotification),
-        duration: k.snackBarDurationNormal,
-      ));
-      setState(() {
-        _isFollowSystemTheme = oldValue;
-      });
-    }
-  }
-
-  Future<void> _onUseBlackInDarkThemeChanged(
-      BuildContext context, bool value) async {
-    final oldValue = _isUseBlackInDarkTheme;
-    setState(() {
-      _isUseBlackInDarkTheme = value;
-    });
-    if (await Pref().setUseBlackInDarkTheme(value)) {
-      if (Theme.of(context).brightness == Brightness.dark) {
-        KiwiContainer().resolve<EventBus>().fire(ThemeChangedEvent());
-      }
-    } else {
-      _log.severe("[_onUseBlackInDarkThemeChanged] Failed writing pref");
-      SnackBarManager().showSnackBar(SnackBar(
-        content: Text(L10n.global().writePreferenceFailureNotification),
-        duration: k.snackBarDurationNormal,
-      ));
-      setState(() {
-        _isUseBlackInDarkTheme = oldValue;
-      });
-    }
-  }
-
   Future<void> _onSeedColorPressed(BuildContext context) async {
     final result = await showDialog<Color>(
       context: context,
@@ -134,28 +145,12 @@ class _ThemeSettingsState extends State<ThemeSettings> {
     if (result == null) {
       return;
     }
-
-    final oldValue = _seedColor;
-    setState(() {
-      _seedColor = result;
-    });
-    if (await Pref().setSeedColor(result.withAlpha(0xFF).value)) {
-      KiwiContainer().resolve<EventBus>().fire(ThemeChangedEvent());
-    } else {
-      _log.severe("[_onSeedColorPressed] Failed writing pref");
-      SnackBarManager().showSnackBar(SnackBar(
-        content: Text(L10n.global().writePreferenceFailureNotification),
-        duration: k.snackBarDurationNormal,
-      ));
-      setState(() {
-        _seedColor = oldValue;
-      });
+    if (mounted) {
+      context.read<ThemeSettingsBloc>().add(ThemeSettingsSetSeedColor(result));
     }
   }
 
-  late bool _isFollowSystemTheme;
-  late bool _isUseBlackInDarkTheme;
-  late Color _seedColor;
+  late final StreamSubscription _errorSubscription;
 }
 
 class _SeedColorPicker extends StatefulWidget {
diff --git a/app/lib/widget/settings/theme_settings.g.dart b/app/lib/widget/settings/theme_settings.g.dart
index 6f679148..417759a2 100644
--- a/app/lib/widget/settings/theme_settings.g.dart
+++ b/app/lib/widget/settings/theme_settings.g.dart
@@ -6,10 +6,10 @@ part of 'theme_settings.dart';
 // NpLogGenerator
 // **************************************************************************
 
-extension _$_ThemeSettingsStateNpLog on _ThemeSettingsState {
+extension _$_WrappedThemeSettingsStateNpLog on _WrappedThemeSettingsState {
   // ignore: unused_element
   Logger get _log => log;
 
   static final log =
-      Logger("widget.settings.theme_settings._ThemeSettingsState");
+      Logger("widget.settings.theme_settings._WrappedThemeSettingsState");
 }
diff --git a/app/pubspec.lock b/app/pubspec.lock
index 0cdae77c..355cbaac 100644
--- a/app/pubspec.lock
+++ b/app/pubspec.lock
@@ -297,6 +297,24 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "3.0.2"
+  copy_with:
+    dependency: "direct main"
+    description:
+      path: copy_with
+      ref: "copy_with-1.0.0"
+      resolved-ref: c3ef6b3b5337f99ee7c0e1fb655bacf635d8b072
+      url: "https://gitlab.com/nkming2/dart-copy-with"
+    source: git
+    version: "1.0.0"
+  copy_with_build:
+    dependency: "direct dev"
+    description:
+      path: copy_with_build
+      ref: "copy_with_build-1.0.0"
+      resolved-ref: "8303676d56dc16bc2fb1e38ad95c5d97b493e613"
+      url: "https://gitlab.com/nkming2/dart-copy-with"
+    source: git
+    version: "1.0.0"
   coverage:
     dependency: transitive
     description:
diff --git a/app/pubspec.yaml b/app/pubspec.yaml
index 1d14bbcb..33086e21 100644
--- a/app/pubspec.yaml
+++ b/app/pubspec.yaml
@@ -40,6 +40,11 @@ dependencies:
   circular_reveal_animation: ^2.0.1
   collection: ^1.15.0
   connectivity_plus: ^2.0.2
+  copy_with:
+    git:
+      url: https://gitlab.com/nkming2/dart-copy-with
+      path: copy_with
+      ref: copy_with-1.0.0
   devicelocale: ^0.5.0
   device_info_plus: ^4.0.0
   draggable_scrollbar:
@@ -124,6 +129,11 @@ dev_dependencies:
   test: any
   bloc_test: any
   build_runner: ^2.1.11
+  copy_with_build:
+    git:
+      url: https://gitlab.com/nkming2/dart-copy-with
+      path: copy_with_build
+      ref: copy_with_build-1.0.0
   drift_dev: ^1.7.0
   flutter_lints: ^2.0.1
   flutter_test: