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.