✅ البحث محلياً (الأفضل حالياً)
نجيب كل الملاحظات مرة
↓
نفلتر داخل Cubit
↓
نحدّث UIDart🧠 لماذا اخترنا البحث المحلي؟
✔ أسرع
✔ لا يحتاج API إضافي
✔ مناسب لتطبيقك الحاليDart🧱 التعديل سيكون فقط في:
✔ Cubit
✔ State
✔ UIDart❌ لا نعدل DataSource
❌ لا نعدل Repository
❌ لا نعدل UseCase
🧱 1) State
📁 notes_state.dart
✏️ نضيف:
class NotesLoaded extends NotesState {
final List<Note> notes;
NotesLoaded(this.notes);
}Dart🧠 لماذا لا نضيف State جديد؟
لأن البحث فقط تغيير في البيانات
وليس حالة جديدة (Loading / Error)Dart🧱 2) Cubit (أهم جزء 🔥)
📁 notes_cubit.dart
📄 الكود + شرح داخلي
class NotesCubit extends Cubit<NotesState> {
final GetNotesUseCase getNotesUseCase;
/// 🔥 قائمة أصلية (كل البيانات)
List<Note> allNotes = [];
NotesCubit(this.getNotesUseCase) : super(NotesInitial());
/// جلب البيانات
Future<void> fetchNotes() async {
emit(NotesLoading());
try {
final notes = await getNotesUseCase();
/// نخزن النسخة الأصلية
allNotes = notes;
emit(NotesLoaded(notes));
} catch (e) {
emit(NotesError(e.toString()));
}
}
/// 🔥 البحث
void searchNotes(String query) {
/// إذا المستخدم مسح البحث
if (query.isEmpty) {
emit(NotesLoaded(allNotes));
return;
}
/// فلترة
final filtered = allNotes.where((note) {
return note.title.toLowerCase().contains(query.toLowerCase()) ||
note.content.toLowerCase().contains(query.toLowerCase());
}).toList();
emit(NotesLoaded(filtered));
}
}Dart🧠 شرح مفصل جداً
🔹 allNotes
نسخة احتياطية من البياناتDart👉 ليش؟
لأنه بعد الفلترة بنخسر البيانات الأصليةDart🔹 searchNotes
دالة تأخذ النص وتفلترDart🔹 where
allNotes.where(...)Dart👉 يعني:
جيب العناصر التي تحقق الشرطDart🔹 contains
note.title.contains(query)Dart👉 يعني:
هل النص يحتوي الكلمة؟Dart🔹 toLowerCase
لجعل البحث غير حساس لحالة الأحرفDart🧱 3) UI (إضافة SearchBar)
📁 notes_page.dart
📄 الكود
Column(
children: [
/// 🔍 البحث
Padding(
padding: const EdgeInsets.all(10),
child: TextField(
decoration: InputDecoration(
hintText: "ابحث...",
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
),
/// 👇 عند الكتابة
onChanged: (value) {
context.read<NotesCubit>().searchNotes(value);
},
),
),
/// القائمة
Expanded(
child: BlocBuilder<NotesCubit, NotesState>(
builder: (context, state) {
if (state is NotesLoading) {
return Center(child: CircularProgressIndicator());
}
if (state is NotesLoaded) {
if (state.notes.isEmpty) {
return Center(child: Text("لا توجد نتائج"));
}
return GridView.builder(
itemCount: state.notes.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemBuilder: (context, index) {
final note = state.notes[index];
return NoteCard(note: note);
},
);
}
return SizedBox();
},
),
),
],
)Dart🧠 ماذا يحدث الآن؟
تكتب في TextField
↓
onChanged
↓
Cubit.searchNotes()
↓
فلترة
↓
emit(NotesLoaded)
↓
UI يتحدثDart💥 مثال عملي
عندك:
["hello", "flutter", "notes"]Dartكتبت:
"fl"Dartالنتيجة:
["flutter"]Dart🧠 تحسين احترافي (اختياري)
debounce (تأخير البحث)
حتى لا ينفذ كل حرف:
يمنع الضغط على الأداءDart💥 خلاصة قوية
search = فلترة محلية
Cubit = العقل
UI = الإدخال والعرضDart🧠 الجملة الذهبية
Search is filtering, not fetchingDart🧭 الفكرة العامة
UI (TextField)
↓
Cubit.searchNotes()
↓
UseCase
↓
Repository
↓
DataSource
↓
Supabase (query with filter)
↓
يرجع النتائج
↓
UI يعرضهاDart🧠 الفرق عن البحث السابق
| النوع | كيف يعمل |
|---|---|
| Local Search | فلترة داخل التطبيق |
| Supabase Search | query من السيرفر |
🧱 1) DataSource (أهم خطوة 🔥)
📁 notes_remote_datasource.dart
📄 الكود
Future<List<NoteModel>> searchNotes(String query) async {
/// ilike = بحث غير حساس للأحرف (case insensitive)
final response = await client
.from('notes')
.select()
.or('title.ilike.%$query%,content.ilike.%$query%');
/// تحويل List<dynamic> → List<NoteModel>
return (response as List)
.map((e) => NoteModel.fromJson(e))
.toList();
}Dart🧠 شرح مفصل جداً
🔹 .or()
.or('title.ilike.%query%,content.ilike.%query%')Dart👉 معناها:
ابحث في title أو contentDart🔹 ilike
بحث بدون حساسية للأحرفDart🔹 %query%
% = wildcard
يعني يحتوي على الكلمةDart🧱 2) Repository
📁 notes_repository.dart
Future<List<Note>> searchNotes(String query);Dart🧠 ليش؟
Domain يعرف العملية فقطDart🧱 3) RepositoryImpl
📁 notes_repository_impl.dart
@override
Future<List<Note>> searchNotes(String query) async {
return await remote.searchNotes(query);
}Dart🧠 شرح
يمرر الطلب فقطDart🧱 4) UseCase
📁 search_notes_usecase.dart
import '../entities/note.dart';
import '../repository/notes_repository.dart';
class SearchNotesUseCase {
final NotesRepository repo;
SearchNotesUseCase(this.repo);
Future<List<Note>> call(String query) async {
return await repo.searchNotes(query);
}
}Dart🧠 شرح
يمثل عملية البحثDart🧱 5) Cubit
📁 notes_cubit.dart
final SearchNotesUseCase searchNotesUseCase;
NotesCubit(
this.getNotesUseCase,
this.addNoteUseCase,
this.deleteNoteUseCase,
this.updateNoteUseCase,
this.searchNotesUseCase,
) : super(NotesInitial());
/// 🔥 البحث من السيرفر
Future<void> searchNotes(String query) async {
/// إذا فاضي رجع كل البيانات
if (query.isEmpty) {
fetchNotes();
return;
}
emit(NotesLoading());
try {
final results = await searchNotesUseCase(query);
emit(NotesLoaded(results));
} catch (e) {
emit(NotesError(e.toString()));
}
}Dart🧠 شرح
🔹 لماذا emit(Loading)؟
لأننا ننتظر APIDart🔹 لماذا fetchNotes عند empty؟
لإعادة القائمة الأصليةDart🧱 6) UI
📁 notes_page.dart
TextField(
decoration: InputDecoration(
hintText: "ابحث...",
prefixIcon: Icon(Icons.search),
),
onChanged: (value) {
context.read<NotesCubit>().searchNotes(value);
},
),Dart🧠 ماذا يحدث الآن؟
تكتب
↓
Cubit.searchNotes
↓
Supabase query
↓
يرجع النتائج
↓
UI يتحدثDart💥 مثال
المستخدم كتب:
"flutter"DartSupabase ينفذ:
WHERE title ILIKE '%flutter%'
OR content ILIKE '%flutter%'Dart⚠️ مهم جداً
🔴 كثرة الطلبات
كل حرف = request 😱
🔥 الحل الاحترافي (Debounce)
داخل Cubit:
Timer? _debounce;
void searchNotes(String query) {
_debounce?.cancel();
_debounce = Timer(const Duration(milliseconds: 400), () async {
if (query.isEmpty) {
fetchNotes();
return;
}
emit(NotesLoading());
final results = await searchNotesUseCase(query);
emit(NotesLoaded(results));
});
}Dart🧠 لماذا؟
نمنع إرسال request لكل حرفDart💥 الخلاصة
Search API =
Cubit → UseCase → Repo → DataSource → SupabaseDart🧠 الجملة الذهبية
Server search = query, not filterDart