إن السماح للمستخدمين بتبديل السمات في تطبيق Flutter أمر ضروري في الوقت الحاضر. في هذا المنشور، ستتعلم كيفية تغيير السمات في Flutter باستخدام Bloc مع Cubits. يضمن Bloc أن التطبيق يفتح دائمًا بالسمة التي يختارها المستخدم. بدلاً من استخدام فئات Bloc العادية، يمكننا تحقيق الوظيفة المطلوبة باستخدام cubits مما يجعل التنفيذ أبسط كثيرًا.
تثبيت الحزم لتغيير السمات في Flutter
لكي يصبح من الممكن تغيير السمات في Flutter باستخدام Bloc with cubits
نحتاج إلى تثبيت حزمة Flutter Bloc
https://pub.dev/packages/flutter_bloc
مع حزمة Shared Preferences و Equatable .
https://pub.dev/packages/equatable
https://pub.dev/packages/shared_preferences
يمكن القيام بذلك عن طريق تنفيذ الأمر التالي داخل مشروعك:
flutter pub add flutter_bloc shared_preferences equatable
بعد تنفيذ الأمر، تحقق من pubspec.yaml ملفك بحثًا عن التبعيات المضافة. يجب أن ترى حزم Flutter Bloc وShared Preferences وEquatable المضمنة في الشكل dependenciesالتالي:
إنشاء المجلدات والملفات
لتسهيل المتابعة، دعنا نبدأ بإنشاء المجلد والملفات التي سنستخدمها أثناء هذا البرنامج التعليمي. أولاً، سننشئ مجلدًا لضمان وجود جميع الملفات المتعلقة بالموضوع في مكان واحد. بعد إنشاء المجلد، 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 مع Cubits
في هذا القسم، سننشئ الوظيفة للتبديل بين السمة الفاتحة والداكنة. ولأننا نريد أن تكون السمة ثابتة، فسنستخدم حزمة التفضيلات المشتركة للاستمرار في السمة الحالية. نريد أيضًا التأكد من تعيين السمة الأولية على سمة جهاز المستخدم. على سبيل المثال، إذا تم تعيين سمة جهاز المستخدم على سمة داكنة، فنحن نريد أن يبدأ تطبيقنا في الوضع الداكن في البداية.
إنشاء فئة ThemeRepository
داخل ThemeRepositoryالفصل سيكون لدينا دالتين، ستكون الدالة الأولى مسؤولة عن الحصول على السمة الحالية وستكون الدالة الأخرى مسؤولة عن تعيين السمة.
import ‘package:flutter/material.dart’;
import ‘package:flutter/scheduler.dart’;
import ‘package:shared_preferences/shared_preferences.dart’;
class ThemeRepository {
Future getTheme() async =>
(await SharedPreferences.getInstance()).getBool(‘isDarkTheme’) ??
SchedulerBinding.instance.platformDispatcher.platformBrightness ==
Brightness.dark;
Future setTheme({required bool isDarkTheme}) async =>
(await SharedPreferences.getInstance()).setBool(‘isDarkTheme’, isDarkTheme);
}
في مقتطف التعليمات البرمجية، قمنا بإنشاء ThemeRepositoryفئة. داخل هذه الفئة، لدينا وظيفة getThemeلاسترداد السمة الحالية من SharedPreferencesاستخدام getBoolالوظيفة مع المفتاح “isDarkTheme”. إذا getBoolأعادت الوظيفة قيمة null، فإننا نضبط القيمة الأولية لسمة جهاز المستخدم باستخدام SchedulerBinding.instance.platformDispatcher.platformBrightnessgetter.
إنشاء فئة 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 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 {
ThemeCubit({
required ThemeRepository themeRepository,
}) : _themeRepository = themeRepository,
super(const ThemeState());
final ThemeRepository _themeRepository;
static late bool _isDarkTheme;
Future 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 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 createState() => _MyAppState();
}
class MyAppState extends State { @override Widget build(BuildContext context) { return MultiRepositoryProvider( providers: [ RepositoryProvider( create: (context) => widget._themeRepository, ), ], child: MultiBlocProvider( providers: [ BlocProvider( create: (BuildContext context) => ThemeCubit(themeRepository: widget._themeRepository) ..getCurrentTheme()) ], child: BlocBuilder( 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().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:
![](https://gdgsanaa.com/wp-content/uploads/2024/07/switching_themes_1.gif)
وحتى لو قمنا بإعادة تشغيل التطبيق الذي يعيد تعيين الحالة الحالية ستلاحظ أن التطبيق يحتفظ بموضوعه الحالي وهو في هذه الحالة ThemeMode.dark:
![](https://gdgsanaa.com/wp-content/uploads/2024/07/switching_themes_2-1030x910.gif)
التحسينات الممكنة
قبل أن ننهي هذا البرنامج التعليمي، هناك تحسينان أرغب في مناقشتهما.
استخدام 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 setIsDarkTheme({required bool isDarkTheme}) async =>
_preferences.setBool(_SharedPreferencesKeys.isDarkTheme, isDarkTheme);
}
تعديل فئة ThemeRepository
نظرًا لأننا قدمنا الفصل، SharedPreferencesControllerفيمكننا الآن تغيير الوظائف الموجودة داخل ThemeRepositoryالفصل لاستخدام الوظائف من SharedPreferencesControllerالفصل.
في مقتطف التعليمات البرمجية أعلاه، قمنا بتغيير كل من الدالتين getThemeand setThemeلاستخدام دالات الفئة SharedPreferencesController. الآن لم نعد بحاجة إلى الوصول مباشرة إلى SharedPreferencesداخل ThemeRepositoryالفئة getThemeولم تعد الوظيفة غير متزامنة مما يحسن الأداء.
تعديل الوظيفة الرئيسية
لجعل SharedPreferencesالمثيل متاحًا داخل تطبيقنا، SharedPreferencesControllerنحتاج إلى استدعاء initالوظيفة قبل بدء التطبيق. لذلك، نحتاج إلى إجراء التغييرات التالية داخل الوظيفة main.
Future 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 {
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 switchTheme() async {
_isDarkTheme = !_isDarkTheme;
await _themeRepository.setTheme(isDarkTheme: _isDarkTheme);
emit(state.copyWith(themeMode: _isDarkTheme ? ThemeMode.dark : ThemeMode.light));
}
}
في مقتطف التعليمات البرمجية أعلاه، استبدلنا عبارات if في الدالتين getCurrentThemeand switchThemeلاستخدام عامل ثلاثي. تظل وظيفة الدالتين كما هي، إلا أنها أصبحت الآن أقصر كثيرًا.
ضع في اعتبارك أن هذا الكود مبني على الكود المعاد صياغته ThemeRepository. إذا لم تقم بإجراء التغييرات المذكورة أعلاه باستخدام then فإن SharedPreferencesControllerالدالة getThemeلا تزال غير متزامنة وبالتالي getCurrentThemeيجب إرجاع Futureاستخدام asyncالكلمة الأساسية awaitوالدالة getTheme.
خاتمة
في هذا البرنامج التعليمي، قمنا بتنفيذ تبديل السمات باستخدام Flutter Bloc مع cubits. ناقشنا جميع الفئات التي تم استخدامها للتأكد من فهمك للوظيفة. في نهاية البرنامج التعليمي، ناقشنا التحسينات المحتملة لضمان اتباعك لأفضل الممارسات وجعل الكود الخاص بك أكثر قابلية للإدارة.