FAQs (Frequently Asked Questions) are a crucial part of any application, providing users with instant answers to common queries. In this guide, we will implement an FAQ screen in Flutter using the ExpansionTile
widget, making it both visually appealing and functionally efficient. This implementation will support dynamic data retrieval, ensuring a seamless user experience.

Overview of the Implementation
Our approach involves using:
ExpansionTile
to create collapsible sections for questions and answers.ListView.builder
to dynamically generate the list of FAQs.Obx
(from GetX state management) to handle state updates dynamically.- Gradient background and default Flutter styles for an enhanced UI.
- API call integration to fetch FAQs dynamically from a backend.
Breaking Down the Code
1. Initializing the FAQ Screen
The CoursesFaqScreeen
widget is a StatefulWidget
since we need to fetch data and update the UI accordingly. The CourseFaqController
is used to manage the API calls and store FAQ data.
final CourseFaqController courseFaqController = Get.find();
In the initState
method, we call the API using the getCourseFaq
function with the course ID passed via widget.args
:
@override
void initState() {
courseFaqController.getCourseFaq(widget.args['courseId']);
super.initState();
}
This ensures that FAQs are fetched as soon as the screen loads.
2. Structuring the UI
The Scaffold
widget contains an AppBar
with the title “FAQs”. The background is set using a gradient for a smooth visual effect.
body: Container(
height: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.blueAccent,
Colors.lightBlue,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),

3. Handling Loading and No Data Cases
We use Obx
to listen to changes in courseFaqController.isLoading
. If loading is in progress, we show a loader. If no FAQs are available, a “No Data Found” widget is displayed.
child: Obx(() => courseFaqController.isLoading.value
? Center(child: CircularProgressIndicator())
: courseFaqController.faqs.isEmpty
? Center(child: Text("No FAQs Available"))
: ListView.builder(
4. Displaying FAQs Using ExpansionTile
Each FAQ is displayed as an expandable section. The ExpansionTile
takes the question as its title and the answer inside a ListTile
that is decorated with a light gray background.
child: ExpansionTile(
title: Text(
courseFaqController.faqs[index].question ?? '',
style: TextStyle(
color: Colors.black,
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
children: <Widget>[
ListTile(
title: Container(
decoration: BoxDecoration(
color: Colors.grey.withOpacity(0.1),
borderRadius: const BorderRadius.all(Radius.circular(5.0)),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
courseFaqController.faqs[index].answer ?? '',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w400),
),
),
),
),
],
),
5. Controller Implementation
The CourseFaqController
manages the API call and stores the FAQ data. It uses GetX’s reactive state management to update the UI when data changes.
Note: Create your own controller
import 'package:get/get.dart';
import 'package:solh/constants/api.dart';
class CourseFaqController extends GetxController {
var isLoading = false.obs;
var err = ''.obs;
var faqs = <Faqs>[].obs;
Future<void> getCourseFaq(String id) async {
try {
isLoading.value = true;
final response = await GetConnect().get("${APIConstants.api}/api/lms/user/course-faqs/$id");
if (response.status.hasError) {
err.value = response.statusText ?? "Unknown error";
} else {
faqs.value = List<Faqs>.from(response.body['faqs'].map((x) => Faqs.fromJson(x)));
}
} catch (e) {
err.value = e.toString();
} finally {
isLoading.value = false;
}
}
}
Output
Complete code
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solh/constants/api.dart';
class CourseFaqScreen extends StatefulWidget {
final Map<String, dynamic> args;
const CourseFaqScreen({Key? key, required this.args}) : super(key: key);
@override
_CourseFaqScreenState createState() => _CourseFaqScreenState();
}
class _CourseFaqScreenState extends State<CourseFaqScreen> {
final CourseFaqController courseFaqController = Get.find();
@override
void initState() {
super.initState();
courseFaqController.getCourseFaq(widget.args['courseId']);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("FAQs"),
backgroundColor: Colors.blueAccent,
),
body: Container(
height: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.blueAccent,
Colors.lightBlue,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Obx(() {
if (courseFaqController.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
if (courseFaqController.faqs.isEmpty) {
return const Center(child: Text("No FAQs Available"));
}
return ListView.builder(
itemCount: courseFaqController.faqs.length,
itemBuilder: (context, index) {
return ExpansionTile(
title: Text(
courseFaqController.faqs[index].question ?? '',
style: const TextStyle(
color: Colors.black,
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
children: <Widget>[
ListTile(
title: Container(
decoration: BoxDecoration(
color: Colors.grey.withOpacity(0.1),
borderRadius: const BorderRadius.all(Radius.circular(5.0)),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
courseFaqController.faqs[index].answer ?? '',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w400),
),
),
),
),
],
);
},
);
}),
),
);
}
}
class CourseFaqController extends GetxController {
var isLoading = false.obs;
var err = ''.obs;
var faqs = <Faqs>[].obs;
Future<void> getCourseFaq(String id) async {
try {
isLoading.value = true;
final response = await GetConnect().get("${APIConstants.api}/api/lms/user/course-faqs/$id");
if (response.status.hasError) {
err.value = response.statusText ?? "Unknown error";
} else {
faqs.value = List<Faqs>.from(response.body['faqs'].map((x) => Faqs.fromJson(x)));
}
} catch (e) {
err.value = e.toString();
} finally {
isLoading.value = false;
}
}
}
class Faqs {
final String? question;
final String? answer;
Faqs({this.question, this.answer});
factory Faqs.fromJson(Map<String, dynamic> json) {
return Faqs(
question: json['question'],
answer: json['answer'],
);
}
}
Why Use ExpansionTile for FAQs?
- Provides a clean and organized way to display questions and answers.
- Saves screen space by keeping answers hidden until tapped.
- Smooth expand/collapse animations improve user experience.
Conclusion
This implementation provides a scalable way to handle FAQs in Flutter applications. By leveraging ExpansionTile
and ListView.builder
, we ensure the list is dynamically generated and updates automatically when new data is fetched. The use of Obx
makes state management seamless, making this approach ideal for apps that require real-time FAQ updates.
Related Articles
- Related Articles
- How to make Ludo app in Flutter with Source Code Step by step
- How to make PDF Reader app in Flutter with Source Code Step by step
- How to make QR Scanner app in Flutter with Source Code Step by step
- How to Make a ToDo App with Flutter with source Code StepWise in 2024
- What is package in Flutter (Dart) with example in 2024
- What is class in Flutter(Dart) with example step by step
- Advantage of Flutter with examples in 2024
- Top 15 Amazing Applications Built with Flutter Framework
- Creating an Instruction UI Screen in Flutter Application