البحث عم الملاحظة داخل SupaBase

الآن سنبني بحث احترافي كامل متوافق مع آخر نسخة من مشروعك:

✅ Clean Architecture
✅ Cubit
✅ Supabase Search
✅ Debounce
✅ SearchBar احترافي
✅ Loading
✅ إعادة النتائج
✅ البحث بالعنوان والمحتوى
✅ شرح كل ملف بالتفصيل

🧭 الفلو الكامل
User يكتب

TextField.onChanged

Cubit.searchNotes()

Debounce

UseCase

Repository

DataSource

Supabase Query

NotesLoaded(results)

UI يتحدث
JSX
🧱 1) DataSource
📁 notes_remote_datasource.dart
📄 الكود الكامل
import 'package:supabase_flutter/supabase_flutter.dart';

import '../models/note_model.dart';

class NotesRemoteDatasource {
  final SupabaseClient client;

  NotesRemoteDatasource(this.client);

  /// 🔥 جلب كل الملاحظات
  Future<List<NoteModel>> getNotes() async {

    final response = await client
        .from('notes')
        .select();

    return (response as List)
        .map((e) => NoteModel.fromJson(e))
        .toList();
  }

  /// 🔥 البحث داخل Supabase
  Future<List<NoteModel>> searchNotes(String query) async {

    final response = await client
        .from('notes')

        /// select = جلب البيانات
        .select()

        /// البحث في title OR content
        .or(
          'title.ilike.%$query%,content.ilike.%$query%',
        );

    /// تحويل JSON → NoteModel
    return (response as List)
        .map((e) => NoteModel.fromJson(e))
        .toList();
  }
}


Future<List<NoteModel>> searchNotes(String query) async {
  if (query.trim().isEmpty) return [];

  final response = await client
      .from('notes')
      .select()
      .or(
        'title.ilike.%$query%,content.ilike.%$query%',
      );

  return (response as List)
      .map((e) => NoteModel.fromJson(e))
      .toList();
}
JSX
🧠 شرح الملف بالتفصيل
أولاً: ما وظيفة هذا الـ Method ؟

هذا الميثود يقوم بـ:

  1. استقبال كلمة بحث من المستخدم
  2. إرسال Query إلى Supabase
  3. البحث داخل:
    • title
    • content
  4. جلب النتائج
  5. تحويل JSON القادم من Supabase إلى Objects من نوع NoteModel
  6. إرجاع List تحتوي الملاحظات المطابقة
شرح الترويسة (Function Signature)
Future<List<NoteModel>> searchNotes(String query)
JSX

نفككها:

1️⃣ Future

يعني العملية غير فورية وتحتاج وقت.

لماذا؟

لأننا نتصل بالسيرفر (Supabase).

أي عملية:

  • API
  • Database
  • Internet

تكون Async.

2️⃣ List<NoteModel>

يعني النتيجة النهائية ستكون:

[
  NoteModel(),
  NoteModel(),
  NoteModel(),
]
JSX

أي قائمة ملاحظات.

3️⃣ searchNotes

اسم الدالة.

4️⃣ String query

القيمة التي يكتبها المستخدم داخل البحث.

مثلاً:

searchNotes("flutter")
JSX

هنا:

  • query = flutter
الآن داخل الدالة
أول سطر
final response = await client
JSX
ماذا يعني؟

client

هو كائن Supabase Client.

غالباً يكون:

final client = Supabase.instance.client;
JSX

هذا الكائن مسؤول عن:

  • الاتصال بقاعدة البيانات
  • تنفيذ Queries
  • Authentication
  • Storage
await

يعني:

انتظر حتى يرجع السيرفر النتيجة.

لأن الاتصال بالسيرفر يحتاج وقت.

السطر التالي
.from('notes')
JSX

يعني:

اذهب إلى جدول اسمه notes

يشبه SQL:

FROM notes
JSX
السطر التالي
.select()
JSX

يعني:

اجلب البيانات

يشبه SQL:

SELECT *<br>FROM notes
JSX
لماذا لم نكتب أسماء الأعمدة؟

لأن:

.select()
JSX

بدون Parameters تعني:

SELECT *
JSX

أي جميع الأعمدة.

يمكن تحديد أعمدة

مثلاً:

.select('id,title')
JSX

يعني:

SELECT id, title
JSX
أهم جزء
.or(
 'title.ilike.%$query%,content.ilike.%$query%',
)
JSX

هذا هو البحث الحقيقي.

ماذا تفعل or() ؟

تعني:

ابحث إذا تحقق أحد الشرطين.

مثل SQL:

WHERE title ILIKE '%flutter%'
OR content ILIKE '%flutter%'
JSX
نفكك النص
'title.ilike.%$query%'
JSX

لو:

query = flutter
JSX

تصبح:

title.ilike.%flutter%
JSX
ما معنى ilike ؟

في PostgreSQL:

  • like → حساس للأحرف الكبيرة
  • ilike → غير حساس

مثال:

النصيطابق flutter ؟
Flutterنعم
FLUTTERنعم
flutterنعم
ما معنى % ؟

في SQL:

%
JSX

تعني:

أي شيء

مثال
%flutter%
JSX

تعني:

  • flutter course
  • تعلم flutter
  • flutter123
  • abcflutterxyz

أي شيء يحتوي الكلمة.

الشرط الثاني
content.ilike.%$query%
JSX

يعني:

ابحث أيضاً داخل المحتوى.

النتيجة النهائية للـ Query

لو المستخدم كتب:

flutter
JSX

سيصبح SQL تقريباً:

SELECT *
FROM notes
WHERE title ILIKE '%flutter%'
OR content ILIKE '%flutter%'
JSX
ماذا يعيد Supabase ؟

يرجع JSON List.

مثال:

[
{
"id": 1,
"title": "Flutter",
"content": "Bloc tutorial"
},
{
"id": 2,
"title": "Dart",
"content": "Flutter basics"
}
]
الآن هذا الجزء
return (response as List)
JSX
لماذا as List ؟

لأن response نوعه dynamic.

فنحن نقول لـ Dart:

اعتبره List

الآن map()
.map((e) => NoteModel.fromJson(e))
JSX

هذا أهم جزء بالتحويل.

ماذا تفعل map ؟

تمر على كل عنصر داخل القائمة.

مثلاً:

[
 json1,
  json2,
  json3
]
JSX

وتحول كل عنصر.

e ماذا يمثل؟

يمثل عنصر واحد من الـ JSON.

مثلاً:

{
 "id": 1,
  "title": "Flutter",
  "content": "Bloc"
}
JSX
fromJson

تحول JSON → Object

مثال

NoteModel.fromJson(e)
JSX

تصنع:

NoteModel(
  id: 1,
  title: "Flutter",
  content: "Bloc",
)
JSX
لماذا نحتاج Model ؟

لأن التعامل مع Objects أسهل من JSON.

بدلاً من:

note['title']
JSX

نستخدم:

note.title
JSX

أخيراً

.toList();
JSX

لأن map يرجع:

Iterable
JSX

وليس List.

فنحوّله إلى List.


الشكل النهائي للرحلة

User يكتب كلمة

searchNotes(query)

Supabase Query

جلب JSON

تحويل JSON → NoteModel

إرجاع List<NoteModel>

كيف تستخدم داخل Cubit ؟

غالباً:

final notes = await repository.searchNotes(query);
emit(SearchSuccess(notes));
JSX
ملاحظة مهمة جداً ⚠️

هذا الكود معرض لمشكلة إذا كتب المستخدم:

%
JSX

أو رموز SQL خاصة.

الأفضل تنظيف query قبل الإرسال.

نسخة احترافية أكثر
Future<List<NoteModel>> searchNotes(String query) async {
  if (query.trim().isEmpty) return [];

  final response = await client
      .from('notes')
      .select()
      .or(
        'title.ilike.%$query%,content.ilike.%$query%',
      );

  return (response as List)
      .map((e) => NoteModel.fromJson(e))
      .toList();
}
JSX
ماذا يحدث في الذاكرة أثناء التنفيذ؟
response

List<Map<String,dynamic>>

↓ map()

NoteModel
NoteModel
NoteModel

↓ toList()

List<NoteModel>

الفرق بين map و forEach هنا

map

للتحويل.

json → object

forEach

للتنفيذ فقط.

مثل:

print()

ولا يعيد List جديدة.


لماذا استخدمنا OR وليس AND ؟

لأننا نريد:

  • العنوان يحتوي الكلمة
    أو
  • المحتوى يحتوي الكلمة
🔹 .from(‘notes’)
تحديد الجدول
JSX
🔹 select()
جلب البيانات
JSX
🔹 or()
بحث في أكثر من حقل
JSX
🔹 ilike
بحث غير حساس للأحرف
JSX
🔹 %query%
يعني يحتوي على النص
JSX
🧱 2) Repository Interface
📁 notes_repository.dart
📄 الكود
import '../entities/note.dart';

abstract class NotesRepository {

  Future<List<Note>> getNotes();

  /// 🔥 البحث
  Future<List<Note>> searchNotes(String query);
}
JSX
🧠 شرح
Domain يعرف العملية فقط
ولا يعرف Supabase
JSX
🧱 3) RepositoryImpl
📁 notes_repository_impl.dart
📄 الكود
import '../../domain/entities/note.dart';
import '../../domain/repository/notes_repository.dart';

import '../datasource/notes_remote_datasource.dart';

class NotesRepositoryImpl
    implements NotesRepository {

  final NotesRemoteDatasource remote;

  NotesRepositoryImpl(this.remote);

  @override
  Future<List<Note>> getNotes() async {
    return await remote.getNotes();
  }

  /// 🔥 البحث
  @override
  Future<List<Note>> searchNotes(
    String query,
  ) async {

    return await remote.searchNotes(query);
  }
}
JSX
🧠 شرح
يمرر الطلب من DomainData
JSX
🧱 4) UseCase
📁 search_notes_usecase.dart
📄 الكود
import '../entities/note.dart';
import '../repository/notes_repository.dart';

class SearchNotesUsecase {

  final NotesRepository repo;

  SearchNotesUsecase(this.repo);

  /// call()
  Future<List<Note>> call(
    String query,
  ) async {

    return await repo.searchNotes(query);
  }
}
JSX
🧠 شرح
يمثل عملية البحث
JSX
🧱 5) Cubit
📁 notes_cubit.dart
📄 الكود الكامل
import 'dart:async';

import 'package:flutter_bloc/flutter_bloc.dart';

import '../../domain/usecases/get_notes_usecases.dart';
import '../../domain/usecases/search_notes_usecase.dart';

import 'notes_state.dart';

class NotesCubit extends Cubit<NotesState> {

  final GetNotesUsecases getNotes;

  final SearchNotesUsecase searchUsecase;

  NotesCubit(
    this.getNotes,
    this.searchUsecase,
  ) : super(NotesInitial());

  /// debounce timer
  Timer? debounce;

  /// 🔥 جلب البيانات
  Future<void> fetchNotes() async {

    emit(NotesLoading());

    try {

      final notes = await getNotes();

      emit(NotesLoaded(notes));

    } catch (e) {

      emit(
        NotesError(e.toString()),
      );
    }
  }

  /// 🔥 البحث
  Future<void> searchNotes(
    String query,
  ) async {

    /// إلغاء الطلب السابق
    debounce?.cancel();

    debounce = Timer(
      const Duration(milliseconds: 500),

      () async {

        /// إذا الحقل فارغ
        if (query.isEmpty) {

          fetchNotes();

          return;
        }

        emit(NotesLoading());

        try {

          final results =
              await searchUsecase(query);

          emit(
            NotesLoaded(results),
          );

        } catch (e) {

          emit(
            NotesError(e.toString()),
          );
        }
      },
    );
  }

  @override
  Future<void> close() {

    debounce?.cancel();

    return super.close();
  }
}
JSX
🧠 شرح Cubit بالتفصيل
🔹 Timer
يؤخر البحث
JSX
🔹 debounce?.cancel()
إلغاء البحث السابق
JSX
🔹 Duration(500ms)
انتظار المستخدم ينتهي من الكتابة
JSX
🔹 query.isEmpty
إذا حذف المستخدم النص
نرجع كل الملاحظات
JSX
🔹 close()
تنظيف الذاكرة
JSX
🧱 6) Main.dart
📄 أضف:
final searchNotes =
   SearchNotesUsecase(repo);
JSX
📄 مرره:
runApp(
  MyApp(
    getNotes,
    searchNotes,
  ),
);
JSX
📄 داخل MyApp
final SearchNotesUsecase searchNotes;
JSX
📄 داخل BlocProvider
create: (_) => NotesCubit(
  getNotes,
  searchNotes,
)..fetchNotes(),
JSX
🧱 7) UI
📁 notes_page.dart
📄 الكود الكامل للبحث

ضعه فوق Grid مباشرة:

Column(
  children: [

    /// 🔥 Search Field
    Padding(
      padding: const EdgeInsets.all(15),

      child: TextField(

        decoration: InputDecoration(

          hintText: "ابحث عن ملاحظة...",

          prefixIcon: const Icon(
            Icons.search,
          ),

          border: OutlineInputBorder(
            borderRadius:
                BorderRadius.circular(15),
          ),
        ),

        /// عند الكتابة
        onChanged: (value) {

          context
              .read<NotesCubit>()
              .searchNotes(value);
        },
      ),
    ),

    /// القائمة
    Expanded(
      child: BlocBuilder<
          NotesCubit,
          NotesState>(
        builder: (context, state) {

          if (state is NotesLoading) {

            return const Center(
              child:
                  CircularProgressIndicator(),
            );
          }

          if (state is NotesError) {

            return Center(
              child: Text(state.message),
            );
          }

          if (state is NotesLoaded) {

            if (state.notes.isEmpty) {

              return const Center(
                child: Text(
                  "لا توجد نتائج",
                ),
              );
            }

            return GridView.builder(

              padding:
                  const EdgeInsets.all(15),

              gridDelegate:
                  const SliverGridDelegateWithFixedCrossAxisCount(

                crossAxisCount: 2,

                crossAxisSpacing: 10,

                mainAxisSpacing: 10,

                childAspectRatio: 1,
              ),

              itemCount:
                  state.notes.length,

              itemBuilder:
                  (context, index) {

                final note =
                    state.notes[index];

                final color =
                    noteColors[
                      index %
                      noteColors.length
                    ];

                return NoteCard(
                  note: note,
                  color: color,
                );
              },
            );
          }

          return const SizedBox();
        },
      ),
    ),
  ],
)
JSX
🧠 ماذا يحدث الآن؟
المستخدم يكتب

onChanged

Cubit.searchNotes()

Debounce

Supabase Query

NotesLoaded(results)

GridView يتحدث
JSX
💥 النتيجة النهائية

✅ بحث احترافي
✅ سريع
✅ لا يرسل request لكل حرف
✅ يعمل على Supabase مباشرة
✅ Clean Architecture صحيح

🧠 الجملة الذهبية
Professional search is server-driven + debounced
JSX

سنقوم بـ:

إضافة Search TextField
ربطه مع Cubit
الحفاظ على BlocBuilder
الحفاظ على BlocListener
عدم تخريب الهيكل الحالي
JSX
🧠 الفكرة المهمة جداً

حالياً body عندك:

body: BlocBuilder(...)
JSX
❌ هذا لا يسمح بإضافة SearchBar فوق Grid بسهولة

لأن:

BlocBuilder يعيد Widget واحدة فقط
JSX
✅ لذلك سنحول body إلى:
Column
JSX
🎯 الشكل النهائي
Column
 ├── SearchField
 └── Expanded
      └── BlocBuilder
JSX
🧱 لماذا Expanded؟

لأن:

GridView داخل Column
يحتاج مساحة محددة
JSX
📄 الملف الكامل بعد التعديل + شرح داخلي
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

import 'package:notes_app/features/notes/presentation/pages/add_note_page.dart';

import 'package:notes_app/features/notes/presentation/widgets/note_card.dart';

import '../cubit/notes_cubit.dart';
import '../cubit/notes_state.dart';

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

  @override
  State<Notes_page> createState() => _Notes_pageState();
}

class _Notes_pageState extends State<Notes_page> {

  @override
  Widget build(BuildContext context) {

    return BlocListener<NotesCubit, NotesState>(

      /// 🔥 الاستماع للحالات
      listener: (context, state) {

        /// إضافة
        if (state is NoteAdded) {

          ScaffoldMessenger.of(context).showSnackBar(

            SnackBar(
              content: Text("تمت إضافة الملاحظة"),
              backgroundColor: Colors.green,
            ),
          );
        }

        /// تعديل
        if (state is NoteUpdated) {

          ScaffoldMessenger.of(context).showSnackBar(

            SnackBar(
              content: Text("تم تعديل الملاحظة"),
              backgroundColor: Colors.green,
            ),
          );
        }

        /// حذف
        if (state is NoteDeleted) {

          ScaffoldMessenger.of(context).showSnackBar(

            SnackBar(
              content: Text("تم حذف الملاحظة"),
              backgroundColor: Colors.red,
            ),
          );
        }
      },

      child: Scaffold(

        /// 🔥 زر الإضافة
        floatingActionButton: FloatingActionButton(

          onPressed: () {

            Navigator.push(
              context,

              MaterialPageRoute(
                builder: (_) => AddNotePage(),
              ),
            );
          },

          child: Icon(Icons.add),
        ),

        appBar: AppBar(
          title: Text("صفحة الملاحظات"),
        ),

        /// 🔥 body أصبح Column
        body: Column(

          children: [

            /// =========================
            /// 🔥 Search Field
            /// =========================

            Padding(

              padding: const EdgeInsets.all(15),

              child: TextField(

                decoration: InputDecoration(

                  hintText: "ابحث عن ملاحظة...",

                  prefixIcon: Icon(Icons.search),

                  border: OutlineInputBorder(

                    borderRadius:
                        BorderRadius.circular(15),
                  ),
                ),

                /// 🔥 عند الكتابة
                onChanged: (value) {

                  context
                      .read<NotesCubit>()
                      .searchNotes(value);
                },
              ),
            ),

            /// =========================
            /// 🔥 Expanded مهم جداً
            /// =========================

            Expanded(

              child: BlocBuilder<
                  NotesCubit,
                  NotesState>(

                builder: (context, state) {

                  /// Loading
                  if (state is NotesLoading) {

                    return Center(
                      child:
                          CircularProgressIndicator(),
                    );
                  }

                  /// Error
                  if (state is NotesError) {

                    return Center(
                      child: Text(state.message),
                    );
                  }

                  /// Success
                  if (state is NotesLoaded) {

                    /// لا توجد بيانات
                    if (state.notes.isEmpty) {

                      return Center(
                        child:
                            Text("لا توجد نتائج"),
                      );
                    }

                    return Padding(

                      padding:
                          const EdgeInsets.all(15),

                      child: RefreshIndicator(

                        onRefresh: () async {

                          await context
                              .read<NotesCubit>()
                              .fetchNotes();
                        },

                        child: GridView.builder(

                          physics:
                              AlwaysScrollableScrollPhysics(),

                          gridDelegate:
                              SliverGridDelegateWithFixedCrossAxisCount(

                            crossAxisCount: 2,

                            crossAxisSpacing: 10,

                            mainAxisSpacing: 10,

                            childAspectRatio: 1,
                          ),

                          itemCount:
                              state.notes.length,

                          itemBuilder:
                              (context, index) {

                            final note =
                                state.notes[index];

                            /// 🔥 ألوان الكاردات
                            final color =
                                noteColors[
                                  index %
                                      noteColors.length
                                ];

                            return NoteCard(

                              note: note,

                              color: color,
                            );
                          },
                        ),
                      ),
                    );
                  }

                  return SizedBox();
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}
JSX
🧠 شرح أهم نقطة
🔥 لماذا وضعنا Search خارج BlocBuilder؟
لأن:
SearchBar لا يعتمد على state
JSX
بينما:
GridView يعتمد على state
JSX
🧠 ماذا يحدث الآن؟
المستخدم يكتب

onChanged

Cubit.searchNotes()

Supabase Query

emit(NotesLoaded)

BlocBuilder

GridView يتحدث
JSX
⚠️ مهم جداً

لا تنس إضافة:

searchNotes()
JSX

داخل Cubit

⚠️ وأيضاً:

لا تنس تمرير:

SearchNotesUsecase
JSX

في main.dart

💥 لماذا هذا التصميم احترافي؟
الجزءالمسؤولية
TextFieldإدخال المستخدم
Cubitإدارة البحث
UseCaseعملية البحث
Repositoryabstraction
DataSourceSupabase
BlocBuilderتحديث UI
🧠 الجملة الذهبية
Search input stays outside BlocBuilder, results stay inside
JSX