إن السماح للمستخدمين بالتبديل بين الوضع الداكن والوضع الفاتح في تطبيق Flutter أمر ضروري في الوقت الحاضر. في هذا المنشور، ستتعلم كيفية تغيير السمات في Flutter باستخدام Bloc مع Cubits. يضمن Bloc أن التطبيق يفتح دائمًا بالسمات التي يختارها المستخدم. بدلاً من استخدام فئات Bloc العادية، يمكننا تحقيق الوظيفة المطلوبة باستخدام cubits مما يجعل التنفيذ أبسط كثيرًا.
تثبيت الحزم للتبديل بين الوضع الداكن والفاتح في Flutter
لكي يصبح من الممكن التبديل بين الوضع الداكن والوضع الفاتح في Flutter باستخدام Bloc with cubits، نحتاج إلى تثبيت حزمة Flutter Bloc مع حزمة Shared Preferences و Equatable . يمكن القيام بذلك عن طريق تنفيذ الأمر التالي داخل مشروعك:
flutter pub add flutter_bloc shared_preferences equatable
بعد تنفيذ الأمر، تحقق من ملفك pubspec.yaml بحثًا عن التبعيات المضافة. يجب أن ترى حزم Flutter Bloc وShared Preferences وEquatable المضمنة في dependenciesالشكل التالي:
dependencies:
equatable: ^2.0.5
flutter_bloc: ^8.1.6
shared_preferences: ^2.2.3
إنشاء المجلدات والملفات
لتسهيل المتابعة، دعنا نبدأ بإنشاء المجلد والملفات التي سنستخدمها أثناء هذا البرنامج التعليمي. أولاً، سننشئ مجلدًا لضمان وجود جميع الملفات المتعلقة بالموضوع في مكان واحد. بعد إنشاء المجلد، themeيمكننا إنشاء المجلدات الفرعية cubitsو repositories. داخل cubitsالمجلد سيكون لدينا ملفان: theme_cubit.dartو theme_state.dart. في repositoriesالمجلد، سيكون لدينا theme_repository.dartالملف.
|-- theme/ |-- cubits/ |-- theme_cubit.dart |-- theme_state.dart |-- repositories/ |-- theme_repository.dart |-- main.dart
بعد إنشاء كافة المجلدات والملفات يمكننا الاستمرار من خلال تنفيذ وظيفة تبديل السمة.التبديل بين الوضع الداكن والوضع الفاتح في Flutter باستخدام Bloc With Cubits في هذا القسم، سننشئ الوظيفة للتبديل بين الوضع الداكن والوضع الفاتح. ولأننا نريد أن يكون السمة ثابتة، فسنستخدم حزمة التفضيلات المشتركة للاستمرار في السمة الحالية. نريد أيضًا التأكد من تعيين السمة الأولية على سمة جهاز المستخدم. على سبيل المثال، إذا تم تعيين سمة جهاز المستخدم على الوضع الداكن، فنحن نريد أن يبدأ تطبيقنا في الوضع الداكن في البداية.إنشاء فئة ThemeRepositoryداخل ThemeRepositoryالفصل سيكون لدينا دالتين، ستكون الدالة الأولى مسؤولة عن الحصول على السمة الحالية وستكون الدالة الأخرى مسؤولة عن تعيين السمة.import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:shared_preferences/shared_preferences.dart'; class ThemeRepository { Future<bool> getTheme() async => (await SharedPreferences.getInstance()).getBool('isDarkTheme') ?? SchedulerBinding.instance.platformDispatcher.platformBrightness == Brightness.dark; Future<void> setTheme({required bool isDarkTheme}) async => (await SharedPreferences.getInstance()).setBool('isDarkTheme', isDarkTheme); }في مقتطف التعليمات البرمجية، قمنا بإنشاء
ThemeRepository
فئة. داخل هذه الفئة، لديناgetTheme
وظيفة لاسترداد السمة الحالية منSharedPreferences
استخدامgetBool
الوظيفة مع المفتاح "isDarkTheme". إذاgetBool
أعادت الوظيفة قيمة null، فإننا نضبط القيمة الأولية لسمة جهاز المستخدم باستخدامSchedulerBinding.instance.platformDispatcher.platformBrightness
getter.إنشاء فئة ThemeStateللتأكد من أن تطبيقنا على علم بالموضوع الحالي، سنقوم بإنشاء الفصل ThemeStateالذي سيتم استخدامه في ThemeCubitالفصل القادم.part of 'theme_cubit.dart'; class ThemeState extends Equatable { const ThemeState({this.themeMode = ThemeMode.light}); final ThemeMode themeMode; ThemeState copyWith({ThemeMode? themeMode}) => ThemeState( themeMode: themeMode ?? this.themeMode, ); @override List<Object?> get props => [themeMode]; }في الكود أعلاه، قمنا بإنشاء ThemeStateفئة تمتد إلى Equatableالفئة. الامتداد مطلوب لضمان إمكانية مقارنة مثيلات الفئة ThemeStateمع بعضها البعض لتحديد التغييرات. ThemeStateتحتوي الفئة على سمة واحدة themeModeسيتم استخدامها لتخزين السمة الحالية. لاحظ أيضًا أن theme_state.dartالملف جزء من theme_cubit.dartالملف. وبالتالي، تتم جميع عمليات الاستيراد داخل theme_cubit.dartالملف.نظرًا لأن theme_state.dartالملف جزء من theme_cubit.dartالملف، فمن المحتمل أن يعرض برنامج IDE الخاص بك الكثير من الخطوط الحمراء. سيتم إصلاح هذا الأمر بمجرد الانتهاء من إنشاء الفصل ThemeCubit.إنشاء فئة ThemeCubit
ستحتوي الفئة ThemeCubitعلى دالتين سنقوم باستدعائهما داخل أدواتنا للحصول على السمة الحالية أو لتغيير السمة.import 'dart:async'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:switching_theme/theme/repositories/theme_repository.dart'; part 'theme_state.dart'; class ThemeCubit extends Cubit<ThemeState> { ThemeCubit({ required ThemeRepository themeRepository, }) : _themeRepository = themeRepository, super(const ThemeState()); final ThemeRepository _themeRepository; static late bool _isDarkTheme; Future<void> getCurrentTheme() async { _themeRepository.getTheme().then((isDarkTheme) { if (isDarkTheme) { _isDarkTheme = true; emit(state.copyWith(themeMode: ThemeMode.dark)); } else { _isDarkTheme = false; emit(state.copyWith(themeMode: ThemeMode.light)); } }); } Future<void> switchTheme() async { if (_isDarkTheme) { await _themeRepository.setTheme(isDarkTheme: false); _isDarkTheme = false; emit(state.copyWith(themeMode: ThemeMode.light)); } else { await _themeRepository.setTheme(isDarkTheme: true); _isDarkTheme = true; emit(state.copyWith(themeMode: ThemeMode.dark)); } } }في مقتطف التعليمات البرمجية أعلاه، نقوم بإنشاء ThemeCubitالفئة التي توسع Cubitالفئة بنوع ThemeState. ThemeCubitتحتوي الفئة على منشئ يأخذ مثيلًا من ThemeRepositoryالفئة. بخلاف ذلك، لدينا _isDarkThemeمتغير يستخدم لتتبع السمة الحالية ولدينا دالتان. الدالة الأولى هي getCurrentThemeالدالة التي تستدعي getThemeالدالة من ThemeRepositoryالفصل. بناءً على نتيجة الدالة، getThemeنقوم بتعيين _isDarkThemeالمتغير وتحديث الحالة بالحالة الحالية ThemeModeباستخدام emitالدالة. الوظيفة الثانية switchThemeتستخدم للتبديل بين ThemeMode.lightو ThemeMode.dark. بناءً على قيمة المتغير _isDarkThemeنقوم بتحديث المتغير نفسه وتحديث الحالة بالتحديث ThemeMode. إنشاء تطبيق المثالتطبيق المثال نفسه عبارة عن تطبيق بسيط سيعرض Switchأداة داخل AppBarأداة Scaffold. ومع ذلك، لجعل تبديل السمة يعمل باستخدام Bloc، نحتاج إلى تكوين الكثير داخل ملفنا main.dart.import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:switching_theme/theme/cubits/theme_cubit.dart'; import 'package:switching_theme/theme/repositories/theme_repository.dart'; void main() { runApp(MyApp( themeRepository: ThemeRepository(), )); } class MyApp extends StatefulWidget { const MyApp({ required ThemeRepository themeRepository, super.key, }) : _themeRepository = themeRepository; final ThemeRepository _themeRepository; @override State<MyApp> createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { @override Widget build(BuildContext context) { return MultiRepositoryProvider( providers: [ RepositoryProvider<ThemeRepository>( create: (context) => widget._themeRepository, ), ], child: MultiBlocProvider( providers: [ BlocProvider( create: (BuildContext context) => ThemeCubit(themeRepository: widget._themeRepository) ..getCurrentTheme()) ], child: BlocBuilder<ThemeCubit, ThemeState>( builder: (BuildContext context, ThemeState state) => MaterialApp( theme: ThemeData.light(useMaterial3: true), darkTheme: ThemeData.dark(useMaterial3: true), themeMode: state.themeMode, home: Scaffold( appBar: AppBar( actions: [ Switch( value: state.themeMode == ThemeMode.dark, onChanged: (_) async => context.read<ThemeCubit>().switchTheme(), ), ], ), ), ), ), ), ); } }
في الكود أعلاه، لدينا MyAppعنصر واجهة المستخدم الرئيسي. MyAppيتطلب عنصر واجهة المستخدم مثيلًا للفئة ThemeRepository. يمكن تمرير هذا المثيل داخل منشئه. لاحقًا، ThemeRepositoryيتم استخدام مثيل الفئة في فئة MultiRepositoryProviderو MultiBlocProvider. تعد فئتا المزود ضروريتين لضمان قدرتنا على الوصول عالميًا إلى مثيلات ThemeRepositoryوعبر ThemeCubitتطبيقنا. على سبيل المثال، نقوم بالوصول إلى ThemeCubitالمثيل في onChangedسمة القطعة Switchلاستدعاء switchThemeوظيفة الفئة ThemeCubitعلى السطر 51. للحصول على السمة الأولية أيضًا، نستدعي getCurrentThemeالوظيفة على الفور باستخدام تدوين الشلال على ThemeCubitالمثيل على السطر 38. للتأكد من أن MaterialAppعنصر واجهة المستخدم لدينا على دراية بالسمة الحالية، قمنا بتغليف عنصر واجهة المستخدم داخل BlocBuilderعنصر واجهة مستخدم. BlocBuilderعنصر واجهة المستخدم من النوع ThemeCubitوبالتالي ThemeStateلدينا إمكانية الوصول إلى ThemeStateداخل عنصر واجهة المستخدم لدينا MaterialApp. داخل MaterialAppعنصر واجهة المستخدم الخاص بنا، نقوم بتعريف كل من السمة الفاتحة باستخدام themeالسمة والسمة الداكنة باستخدام darkThemeالسمة. كما نقوم بتعيين themeModeالسمة إلى state.themeModeللتأكد من أنها تساوي دائمًا قيمة themeModeالمثيل ThemeStateالمنبعث من ThemeCubit. إنشاء تطبيق المثال إذا قمنا ببناء تطبيقنا، فسترى أنه يمكننا الآن التبديل بين ThemeMode.lightو ThemeMode.dark:
وحتى لو قمنا بإعادة تشغيل التطبيق الذي يعيد تعيين الحالة الحالية ستلاحظ أن التطبيق يحتفظ بموضوعه الحالي وهو في هذه الحالة ThemeMode.dark:
التحسينات الممكنة قبل أن ننهي هذا البرنامج التعليمي، هناك تحسينان أرغب في مناقشتهما.
استخدام SharedPreferencesController
بدلاً من الوصول SharedPreferencesمباشرةً إلى داخل صندوق الأمان الخاص بنا، ThemeRepositoryأوصي باستخدام تطبيق SharedPreferencesControllerلتسهيل إدارة جميع SharedPreferencesمفاتيحك.
import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:shared_preferences/shared_preferences.dart'; class _SharedPreferencesKeys { static const String isDarkTheme = 'isDarkTheme'; } class SharedPreferencesController { static late final SharedPreferences _preferences; static Future init() async => _preferences = await SharedPreferences.getInstance(); static bool get isDarkTheme => _preferences.getBool(_SharedPreferencesKeys.isDarkTheme) ?? SchedulerBinding.instance.platformDispatcher.platformBrightness == Brightness.dark; static Future<void> setIsDarkTheme({required bool isDarkTheme}) async => _preferences.setBool(_SharedPreferencesKeys.isDarkTheme, isDarkTheme); }
في مقتطف التعليمات البرمجية هذا، قمنا بإنشاء SharedPreferencesControllerفئة. داخل هذه الفئة، قمنا بإنشاء دالتين لاسترداد وتعيين إدخالنا SharedPreferencesباستخدام المفتاح "isDarkTheme". لمعرفة المزيد حول ذلك، SharedPreferencesControllerيمكنك قراءة المقالة التالية: كيفية إدارة التفضيلات المشتركة في Flutter
تعديل فئة ThemeRepository
نظرًا لأننا قدمنا الفصل SharedPreferencesController، فيمكننا الآن تغيير الوظائف الموجودة داخل الفصل ThemeRepositoryلاستخدام الوظائف من SharedPreferencesControllerالفصل.
import 'package:switching_theme/shared_preferences_controller.dart'; class ThemeRepository { bool getTheme() => SharedPreferencesController.isDarkTheme; Future<void> setTheme({required bool isDarkTheme}) async => SharedPreferencesController.setIsDarkTheme(isDarkTheme: isDarkTheme); }
في مقتطف التعليمات البرمجية أعلاه، قمنا بتغيير كل من الدالتين getThemeand setThemeلاستخدام دالات الفئة SharedPreferencesController. الآن لم نعد بحاجة إلى الوصول مباشرة إلى SharedPreferencesداخل ThemeRepositoryالفئة getThemeولم تعد الوظيفة غير متزامنة مما يحسن الأداء.
تعديل الوظيفة الرئيسية لجعل SharedPreferencesالمثيل متاحًا داخل تطبيقنا، SharedPreferencesControllerنحتاج إلى استدعاء initالوظيفة قبل بدء تشغيل التطبيق. لذلك، نحتاج إلى إجراء التغييرات التالية على mainالوظيفة.
Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); await SharedPreferencesController.init(); runApp(MyApp( themeRepository: ThemeRepository(), )); }
في الكود أعلاه، قمنا بتغيير mainالوظيفة لتصبح غير متزامنة. داخل mainالوظيفة، نبدأ باستدعاء WidgetsFlutterBinding.ensureInitializedالوظيفة للتأكد من تهيئة الارتباطات. فقط بعد تهيئة الارتباطات يمكننا الوصول إلى SharedPreferencesالمثيل، والذي يتم عن طريق استدعاء initوظيفة الفئة SharedPreferencesController.
إعادة تصميم ThemeCubit
حاليًا، تعد دالتا getCurrentTheme وswitchTheme التابعتان لفئة ThemeCubit مطوّلتين للغاية. يمكننا إعادة صياغتهما لجعلهما أقصر. ومع ذلك، فإن إعادة الصياغة قد تجعل فهمهما أكثر صعوبة.
import 'dart:async'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:switching_theme/theme/repositories/theme_repository.dart'; part 'theme_state.dart'; class ThemeCubit extends Cubit<ThemeState> { ThemeCubit({ required ThemeRepository themeRepository, }) : _themeRepository = themeRepository, super(const ThemeState()); final ThemeRepository _themeRepository; static late bool _isDarkTheme; void getCurrentTheme() { final isDarkTheme = _themeRepository.getTheme(); _isDarkTheme = isDarkTheme; emit(state.copyWith(themeMode: isDarkTheme ? ThemeMode.dark : ThemeMode.light)); } Future<void> switchTheme() async { _isDarkTheme = !_isDarkTheme; await _themeRepository.setTheme(isDarkTheme: _isDarkTheme); emit(state.copyWith(themeMode: _isDarkTheme ? ThemeMode.dark : ThemeMode.light)); } }
في مقتطف التعليمات البرمجية أعلاه، استبدلنا عبارات if في الدالتين getCurrentThemeand switchThemeلاستخدام عامل ثلاثي. تظل وظيفة الدالتين كما هي، إلا أنها أصبحت الآن أقصر كثيرًا.
خاتمة في هذا البرنامج التعليمي، قمنا بتنفيذ تبديل السمات باستخدام Flutter Bloc مع cubits للتبديل بين الوضع الداكن والوضع الفاتح. لقد ناقشنا جميع الفئات التي تم استخدامها للتأكد من فهمك للوظيفة. في نهاية البرنامج التعليمي، ناقشنا التحسينات الممكنة لضمان اتباعك لأفضل الممارسات وجعل الكود الخاص بك أكثر قابلية للإدارة.