Implementing a Dynamic FAQ Screen UI in Flutter Using ExpansionTile

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.

<img decoding=

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,
    ),
  ),
<img decoding=

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

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top