You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter实时数据库联动下拉框切换时报错问题求助

Flutter Firebase实时数据库联动下拉框切换异常问题

问题描述

使用Flutter结合Firebase实时数据库实现三级联动下拉框(Exam → Semester → Course),首次加载时能正常显示Exam选项(spring、fall)及对应层级的Semester、Course,但切换Exam下拉框选项时出现错误。

用户提供的原始代码

import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';

class homepg extends StatelessWidget {
const homepg({Key? key}) : super(key: key);

  @override
Widget build(BuildContext context) {
     return Scaffold(
  appBar: AppBar(
    title: Text("home"),
  ),
  body: MyHomePage(),
);
 }
}

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

 @override
 State<MyHomePage> createState() => _MyHomePageState();
 }

 class _MyHomePageState extends State<MyHomePage> {
 var ref = FirebaseDatabase.instance.ref();
 var _selectedexam;
 var _selectedsemester;

 var _selectedcourse;

 @override
 Widget build(BuildContext context) {
   return Container(
     margin: EdgeInsets.all(15),
     child: Center(
      child: Column(
      children: <Widget>[
        Row(
          children: [
            const Align(
              alignment: Alignment.centerLeft,
              child: Text(
                "Exam ",
                style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
              ),
            ),
            const SizedBox(
              width: 40,
            ),
            Container(
              width: 400,
              child: FutureBuilder(
                  future: ref.child("/Exam").get(),
                  builder:
                      (context, AsyncSnapshot<DataSnapshot> asyncSnapshot) {
                    if (asyncSnapshot.hasError)
                      return Text("Error: ${asyncSnapshot.error}");
                    if (!asyncSnapshot.hasData)
                      return const CircularProgressIndicator();

                    return DropdownButton<String>(
                      isExpanded: true,
                      items: asyncSnapshot.data!.children
                          .map((snap) => DropdownMenuItem(
                              value: snap.key,
                              child: Text(snap.key.toString())))
                          .toList(),
                      value: _selectedexam,
                      onChanged: (String? newValue) {
                        setState(() {
                          _selectedexam = newValue!;
                          print(_selectedexam);
                        });
                      },
                    );
                  }),
            ),
          ],
        ),
        const SizedBox(
          height: 100,
        ),
        Row(
          children: [
            const Align(
                alignment: Alignment.centerLeft,
                child: Text(
                  "Semester",
                  style:
                      TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
                )),
            const SizedBox(
              width: 20,
            ),
            Container(
              width: 400,
              child: FutureBuilder(
                  future: ref.child("/Exam/$_selectedexam").get(),
                  builder:
                      (context, AsyncSnapshot<DataSnapshot> asyncSnapshot) {
                    if (asyncSnapshot.hasError)
                      return Text("Error: ${asyncSnapshot.error}");
                    if (!asyncSnapshot.hasData)
                      return const CircularProgressIndicator();

                    return DropdownButton<String>(
                      isExpanded: true,
                      items: asyncSnapshot.data!.children
                          .map((snap) => DropdownMenuItem(
                              value: snap.key,
                              child: Text(snap.key.toString())))
                          .toList(),
                      value: _selectedsemester,
                      onChanged: (String? newValue) {
                        setState(() {
                          _selectedsemester = newValue!;
                          print(_selectedsemester);
                        });
                      },
                    );
                  }),
            ),
          ],
        ),
        const SizedBox(
          height: 100,
        ),
        Row(
          children: [
            const Align(
                alignment: Alignment.centerLeft,
                child: Text(
                  "Course",
                  style:
                      TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
                )),
            const SizedBox(
              width: 40,
            ),
            Container(
              width: 400,
              child: FutureBuilder(
                  future: ref
                      .child("/Exam/$_selectedexam/$_selectedsemester")
                      .get(),
                  builder:
                      (context, AsyncSnapshot<DataSnapshot> asyncSnapshot) {
                    if (asyncSnapshot.hasError)
                      return Text("Error: ${asyncSnapshot.error}");
                    if (!asyncSnapshot.hasData)
                      return const CircularProgressIndicator();

                    return DropdownButton<String>(
                      isExpanded: true,
                      items: asyncSnapshot.data!.children
                          .map((snap) => DropdownMenuItem(
                              value: snap.key,
                              child: Text(snap.key.toString())))
                          .toList(),
                      value: _selectedcourse,
                      onChanged: (String? newValue) {
                        setState(() {
                          _selectedcourse = newValue!;
                          print(_selectedcourse);
                        });
                      },
                    );
                  }),
            ),
          ],
        ),
      ],
    ),
  ),
);
}
}

错误原因分析

  1. 下级选中值未重置:切换Exam选项时,仅更新了_selectedexam,但_selectedsemester和_selectedcourse仍保留旧值,这些旧值在新的Exam对应的子列表中不存在,会导致DropdownButton抛出"Value not in items"异常。
  2. 空值路径请求错误:初始状态下_selectedexam为null,此时Semester的数据库请求路径为/Exam/null,会触发无效请求报错。
  3. FutureBuilder未主动重建:Semester和Course的FutureBuilder依赖上级选中值,但上级值变化时,Future对象未更新,导致请求不会重新触发,无法加载新的子列表数据。

修复方案

1. 切换上级选项时重置下级选中值

在Exam的onChanged回调中,清空下级选中状态,避免旧值不匹配新数据:

onChanged: (String? newValue) {
  setState(() {
    _selectedexam = newValue!;
    _selectedsemester = null; // 重置学期选中值
    _selectedcourse = null;   // 重置课程选中值
    print(_selectedexam);
  });
},

2. 增加空值校验,避免无效请求

在Semester和Course的FutureBuilder前添加校验,提示用户先完成上级选择:

// Semester下拉框部分
child: _selectedexam == null
    ? const Text("请先选择Exam")
    : FutureBuilder(...),

// Course下拉框部分
child: (_selectedexam == null || _selectedsemester == null)
    ? const Text("请先选择Exam和Semester")
    : FutureBuilder(...),

3. 完善类型声明与默认提示

为选中变量添加明确的类型声明,并给DropdownButton添加hint属性提示用户选择:

String? _selectedexam;
String? _selectedsemester;
String? _selectedcourse;

// 示例:Exam下拉框添加hint
DropdownButton<String>(
  hint: const Text("选择Exam"),
  // ...其他属性
)

完整修复后的代码

import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';

class homepg extends StatelessWidget {
  const homepg({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("home"),
      ),
      body: const MyHomePage(),
    );
  }
}

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

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final ref = FirebaseDatabase.instance.ref();
  String? _selectedexam;
  String? _selectedsemester;
  String? _selectedcourse;

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.all(15),
      child: Center(
        child: Column(
          children: <Widget>[
            Row(
              children: [
                const Align(
                  alignment: Alignment.centerLeft,
                  child: Text(
                    "Exam ",
                    style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
                  ),
                ),
                const SizedBox(width: 40),
                SizedBox(
                  width: 400,
                  child: FutureBuilder(
                    future: ref.child("/Exam").get(),
                    builder: (context, AsyncSnapshot<DataSnapshot> asyncSnapshot) {
                      if (asyncSnapshot.hasError)
                        return Text("Error: ${asyncSnapshot.error}");
                      if (!asyncSnapshot.hasData)
                        return const CircularProgressIndicator();

                      return DropdownButton<String>(
                        isExpanded: true,
                        hint: const Text("选择Exam"),
                        items: asyncSnapshot.data!.children
                            .map((snap) => DropdownMenuItem(
                                  value: snap.key,
                                  child: Text(snap.key.toString()),
                                ))
                            .toList(),
                        value: _selectedexam,
                        onChanged: (String? newValue) {
                          setState(() {
                            _selectedexam = newValue!;
                            _selectedsemester = null;
                            _selectedcourse = null;
                            print(_selectedexam);
                          });
                        },
                      );
                    },
                  ),
                ),
              ],
            ),
            const SizedBox(height: 100),
            Row(
              children: [
                const Align(
                  alignment: Alignment.centerLeft,
                  child: Text(
                    "Semester",
                    style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
                  ),
                ),
                const SizedBox(width: 20),
                SizedBox(
                  width: 400,
                  child: _selectedexam == null
                      ? const Text("请先选择Exam")
                      : FutureBuilder(
                          future: ref.child("/Exam/$_selectedexam").get(),
                          builder: (context, AsyncSnapshot<DataSnapshot> asyncSnapshot) {
                            if (asyncSnapshot.hasError)
                              return Text("Error: ${asyncSnapshot.error}");
                            if (!asyncSnapshot.hasData)
                              return const CircularProgressIndicator();

                            return DropdownButton<String>(
                              isExpanded: true,
                              hint: const Text("选择Semester"),
                              items: asyncSnapshot.data!.children
                                  .map((snap) => DropdownMenuItem(
                                        value: snap.key,
                                        child: Text(snap.key.toString()),
                                      ))
                                  .toList(),
                              value: _selectedsemester,
                              onChanged: (String? newValue) {
                                setState(() {
                                  _selectedsemester = newValue!;
                                  _selectedcourse = null;
                                  print(_selectedsemester);
                                });
                              },
                            );
                          },
                        ),
                ),
              ],
            ),
            const SizedBox(height: 100),
            Row(
              children: [
                const Align(
                  alignment: Alignment.centerLeft,
                  child: Text(
                    "Course",
                    style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
                  ),
                ),
                const SizedBox(width: 40),
                SizedBox(
                  width: 400,
                  child: (_selectedexam == null || _selectedsemester == null)
                      ? const Text("请先选择Exam和Semester")
                      : FutureBuilder(
                          future: ref.child("/Exam/$_selectedexam/$_selectedsemester").get(),
                          builder: (context, AsyncSnapshot<DataSnapshot> asyncSnapshot) {
                            if (asyncSnapshot.hasError)
                              return Text("Error: ${asyncSnapshot.error}");
                            if (!asyncSnapshot.hasData)
                              return const CircularProgressIndicator();

                            return DropdownButton<String>(
                              isExpanded: true,
                              hint: const Text("选择Course"),
                              items: asyncSnapshot.data!.children
                                  .map((snap) => DropdownMenuItem(
                                        value: snap.key,
                                        child: Text(snap.key.toString()),
                                      ))
                                  .toList(),
                              value: _selectedcourse,
                              onChanged: (String? newValue) {
                                setState(() {
                                  _selectedcourse = newValue!;
                                  print(_selectedcourse);
                                });
                              },
                            );
                          },
                        ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

内容的提问来源于stack exchange,提问作者Falak Khalid

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 12:57:25