image

12 Jan 2026

9K

35K

Building an FAQ Widget with Search Filter in Flutter

FAQs (Frequently Asked Questions) sections are crucial for providing instant answers to common user queries, reducing support load, and improving user experience. In modern applications, merely listing FAQs isn't enough; users expect efficient ways to find information. This article will guide you through building a dynamic FAQ widget in Flutter, complete with an interactive search filter, enhancing discoverability and usability.

Core Components

1. Defining Your FAQ Data Model

First, let's create a simple data model for our FAQ items. Each FAQ will have a question and an answer, along with a flag to manage its expansion state.


class FaqItem {
  final String question;
  final String answer;
  bool isExpanded; // To manage expansion state

  FaqItem({required this.question, required this.answer, this.isExpanded = false});
}

// Sample data
List sampleFaqs = [
  FaqItem(question: 'What is Flutter?', answer: 'Flutter is an open-source UI software development kit created by Google.'),
  FaqItem(question: 'How do I install Flutter?', answer: 'You can install Flutter by downloading the SDK from the official website and setting up your environment variables.'),
  FaqItem(question: 'What is a Widget?', answer: 'In Flutter, almost everything is a widget. Widgets are the building blocks of the user interface.'),
  FaqItem(question: 'Can Flutter build for Web?', answer: 'Yes, Flutter supports building applications for web, mobile, and desktop from a single codebase.'),
  FaqItem(question: 'What programming language does Flutter use?', answer: 'Flutter uses Dart programming language.'),
];

2. The Basic FAQ Item Widget

Next, we'll create a reusable widget to display a single FAQ item. An ExpansionTile is a perfect fit for expand/collapse functionality.


import 'package:flutter/material.dart';

class FaqItemWidget extends StatelessWidget {
  final FaqItem faqItem;
  final ValueChanged onExpansionChanged;

  const FaqItemWidget({
    Key? key,
    required this.faqItem,
    required this.onExpansionChanged,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
      child: ExpansionTile(
        key: PageStorageKey(faqItem.question), // Helps maintain state across widget tree changes
        title: Text(
          faqItem.question,
          style: const TextStyle(fontWeight: FontWeight.bold),
        ),
        initiallyExpanded: faqItem.isExpanded,
        onExpansionChanged: onExpansionChanged,
        children: [
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Text(faqItem.answer),
          ),
        ],
      ),
    );
  }
}

3. Building the FAQ List Structure

Now, let's create the main StatefulWidget that will hold our FAQ list and manage its state, including the search filter and the expansion state of individual items.


import 'package:flutter/material.dart';
// Assume FaqItem and sampleFaqs are defined as above or in a separate file.
// import 'faq_data.dart'; // if separated
// import 'faq_item_widget.dart'; // if separated

class FaqScreen extends StatefulWidget {
  const FaqScreen({Key? key}) : super(key: key);

  @override
  State createState() => _FaqScreenState();
}

class _FaqScreenState extends State {
  List _faqs = [];
  List _filteredFaqs = [];
  TextEditingController _searchController = TextEditingController();

  @override
  void initState() {
    super.initState();
    _faqs = List.from(sampleFaqs); // Initialize with all FAQs (create a mutable copy)
    _filteredFaqs = _faqs; // Initially, all FAQs are displayed
  }

  @override
  void dispose() {
    _searchController.dispose();
    super.dispose();
  }

  // This method will be implemented later
  void _filterFaqs(String query) {
    // Filtering logic goes here
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('FAQ'),
      ),
      body: Column(
        children: [
          // Search bar will be inserted here
          Expanded(
            child: ListView.builder(
              itemCount: _filteredFaqs.length,
              itemBuilder: (context, index) {
                final faq = _filteredFaqs[index];
                return FaqItemWidget(
                  faqItem: faq,
                  onExpansionChanged: (bool? isExpanded) {
                    setState(() {
                      faq.isExpanded = isExpanded ?? false;
                    });
                  },
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

4. Implementing the Search Bar

A TextField will serve as our search bar. We'll place it at the top of our FaqScreen's body, just above the FAQ list.


// Inside _FaqScreenState's build method, replace the placeholder comment '// Search bar will be inserted here' with:
Padding(
  padding: const EdgeInsets.all(16.0),
  child: TextField(
    controller: _searchController,
    decoration: InputDecoration(
      hintText: 'Search FAQs...',
      prefixIcon: const Icon(Icons.search),
      border: OutlineInputBorder(
        borderRadius: BorderRadius.circular(10.0),
        borderSide: BorderSide.none,
      ),
      filled: true,
      fillColor: Colors.grey[200],
    ),
    onChanged: _filterFaqs, // Call our filter method on text change
  ),
),

5. Integrating Search Filtering Logic

Now, let's implement the _filterFaqs method in _FaqScreenState. This method will filter the FAQ list based on the search query, matching text in both the question and answer fields.


// Inside _FaqScreenState class
void _filterFaqs(String query) {
  setState(() {
    if (query.isEmpty) {
      _filteredFaqs = List.from(_faqs); // Show all if query is empty, maintaining expansion state
    } else {
      _filteredFaqs = _faqs.where((faq) {
        final searchLower = query.toLowerCase();
        final questionLower = faq.question.toLowerCase();
        final answerLower = faq.answer.toLowerCase();

        return questionLower.contains(searchLower) || answerLower.contains(searchLower);
      }).toList();
    }
  });
}

6. The Complete FAQ Widget

Combining all the pieces, here's the full code for our dynamic FAQ widget. You can integrate this FaqScreen into your Flutter application.


import 'package:flutter/material.dart';

// 1. FAQ Data Model
class FaqItem {
  final String question;
  final String answer;
  bool isExpanded;

  FaqItem({required this.question, required this.answer, this.isExpanded = false});
}

// Sample data (can be replaced with data fetched from an API)
List sampleFaqs = [
  FaqItem(question: 'What is Flutter?', answer: 'Flutter is an open-source UI software development kit created by Google.'),
  FaqItem(question: 'How do I install Flutter?', answer: 'You can install Flutter by downloading the SDK from the official website and setting up your environment variables.'),
  FaqItem(question: 'What is a Widget?', answer: 'In Flutter, almost everything is a widget. Widgets are the building blocks of the user interface.'),
  FaqItem(question: 'Can Flutter build for Web?', answer: 'Yes, Flutter supports building applications for web, mobile, and desktop from a single codebase.'),
  FaqItem(question: 'What programming language does Flutter use?', answer: 'Flutter uses Dart programming language.'),
  FaqItem(question: 'Is Flutter free to use?', answer: 'Yes, Flutter is completely free and open-source.'),
  FaqItem(question: 'Where can I find Flutter documentation?', answer: 'Official Flutter documentation is available at docs.flutter.dev.'),
  FaqItem(question: 'How to manage state in Flutter?', answer: 'Flutter offers various state management solutions like Provider, BLoC, Riverpod, GetX, or simple setState.'),
];

// 2. FAQ Item Widget
class FaqItemWidget extends StatelessWidget {
  final FaqItem faqItem;
  final ValueChanged onExpansionChanged;

  const FaqItemWidget({
    Key? key,
    required this.faqItem,
    required this.onExpansionChanged,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
      child: ExpansionTile(
        key: PageStorageKey(faqItem.question),
        title: Text(
          faqItem.question,
          style: const TextStyle(fontWeight: FontWeight.bold),
        ),
        initiallyExpanded: faqItem.isExpanded,
        onExpansionChanged: onExpansionChanged,
        children: [
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Text(faqItem.answer),
          ),
        ],
      ),
    );
  }
}

// 3, 4, 5. Main FAQ Screen with Search Filter
class FaqScreen extends StatefulWidget {
  const FaqScreen({Key? key}) : super(key: key);

  @override
  State createState() => _FaqScreenState();
}

class _FaqScreenState extends State {
  List _faqs = [];
  List _filteredFaqs = [];
  final TextEditingController _searchController = TextEditingController();

  @override
  void initState() {
    super.initState();
    // Create a deep copy to ensure _faqs remains the original source of truth
    _faqs = sampleFaqs.map((item) => FaqItem(
      question: item.question,
      answer: item.answer,
      isExpanded: item.isExpanded,
    )).toList();
    _filteredFaqs = List.from(_faqs); // Initially, all FAQs are displayed
  }

  @override
  void dispose() {
    _searchController.dispose();
    super.dispose();
  }

  void _filterFaqs(String query) {
    setState(() {
      if (query.isEmpty) {
        // When search is cleared, reset to all original FAQs
        _filteredFaqs = List.from(_faqs);
      } else {
        // Filter based on question or answer
        _filteredFaqs = _faqs.where((faq) {
          final searchLower = query.toLowerCase();
          final questionLower = faq.question.toLowerCase();
          final answerLower = faq.answer.toLowerCase();

          return questionLower.contains(searchLower) || answerLower.contains(searchLower);
        }).toList();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('FAQ'),
      ),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: TextField(
              controller: _searchController,
              decoration: InputDecoration(
                hintText: 'Search FAQs...',
                prefixIcon: const Icon(Icons.search),
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(10.0),
                  borderSide: BorderSide.none,
                ),
                filled: true,
                fillColor: Colors.grey[200],
              ),
              onChanged: _filterFaqs,
            ),
          ),
          Expanded(
            child: _filteredFaqs.isEmpty && _searchController.text.isNotEmpty
                ? const Center(child: Text('No FAQs found matching your search.'))
                : ListView.builder(
                    itemCount: _filteredFaqs.length,
                    itemBuilder: (context, index) {
                      final faq = _filteredFaqs[index];
                      return FaqItemWidget(
                        faqItem: faq,
                        onExpansionChanged: (bool? isExpanded) {
                          setState(() {
                            faq.isExpanded = isExpanded ?? false;
                            // Optionally, update the original _faqs item's state too
                            // This ensures state is preserved if the item is filtered out and then back in
                            final originalFaq = _faqs.firstWhere((element) => element.question == faq.question);
                            originalFaq.isExpanded = faq.isExpanded;
                          });
                        },
                      );
                    },
                  ),
          ),
        ],
      ),
    );
  }
}

// Example of how to integrate this into your main.dart:
// void main() {
//   runApp(const MyApp());
// }
//
// class MyApp extends StatelessWidget {
//   const MyApp({Key? key}) : super(key: key);
//
//   @override
//   Widget build(BuildContext context) {
//     return MaterialApp(
//       title: 'FAQ App',
//       theme: ThemeData(
//         primarySwatch: Colors.blue,
//       ),
//       home: const FaqScreen(),
//     );
//   }
// }

Conclusion

By combining Flutter's declarative UI with simple state management, we've successfully built a robust FAQ widget featuring a dynamic search filter. This not only enhances the user experience by making information more accessible but also demonstrates fundamental Flutter concepts like StatefulWidget, TextEditingController, and list filtering. You can further extend this widget by fetching FAQs from an API, adding animations, or implementing more complex filtering logic and UI designs.

Related Articles

May 14, 2026

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter Countdown timers are essential in many applic

May 11, 2026

Unleashing Dynamic UIs: Flutter's Animation Prowess

Unleashing Dynamic UIs: Flutter's Animation Prowess for Slide & Scale Effects Flutter's declarative UI framework, combined with its powerful animation capabilit

May 11, 2026

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy A well-designed Product Detail Page (PDP) is