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

Flutter中如何将TextField输入值同步显示到其他页面单选按钮?

问题描述

在Flutter项目中,使用Riverpod连接Firestore实现了AddMealPage页面,其中包含一个用于输入餐品名称的TextField。需求是:每当修改该TextField的输入值时,需实时同步显示到另一个包含RadioListTile的RadioOption页面的单选按钮标题中。

相关代码片段

AddMealPage代码

class AddMealPage extends ConsumerWidget {
const AddMealPage({Key? key}) : super(key: key);
static const String route = "/addMeal";

@override
Widget build(BuildContext context, WidgetRef ref) {
  final model = ref.read(addMealProvider);
  return LoadingLayer(
    child: Scaffold(
      appBar: AppBar(
        elevation: 0,
        iconTheme: const IconThemeData(
          color: Colors.black,
        ),
        title: const Text(
          "Create Meal",
          style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold),
        ),
        backgroundColor: Colors.white,
      ),
      bottomNavigationBar: Padding(
        padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
        child: MaterialButton(
          padding: const EdgeInsets.all(16),
          color: Colors.black,
          onPressed: model.enabled
              ? () async {
                  try {
                    await model.writeMeal();
                    Navigator.pop(context);
                  } catch (e) {
                    AppSnackbar(context).error(e);
                  }
                }
              : null,
          child: const Text(
            "Add meal",
            style: TextStyle(color: Color.fromARGB(255, 247, 245, 245)),
          ),
        ),
      ),
      body: SingleChildScrollView(
        child: Container(
          padding:
              const EdgeInsets.symmetric(vertical: 60.0, horizontal: 10.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              GestureDetector(
                onTap: () async {
                  final picked = await ImagePicker()
                      .pickImage(source: ImageSource.gallery);
                  if (picked != null) {
                    model.file = File(picked.path);
                  }
                },
                child: Container(
                  margin: const EdgeInsets.only(),
                  width: MediaQuery.of(context).size.width,
                  height: 210.0,
                  decoration: BoxDecoration(
                      borderRadius: BorderRadius.circular(20),
                      image: (model.mealUrl != null || model.file != null)
                          ? DecorationImage(
                              image: model.file != null
                                  ? FileImage(model.file!)
                                  : NetworkImage(model.mealUrl!)
                                      as ImageProvider,
                              fit: BoxFit.cover,
                            )
                          : null),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.end,
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    children: [
                      if (model.mealUrl == null && model.file == null)
                        const Expanded(
                          child: Center(
                            child: Icon(
                              Icons.photo,
                            ),
                          ),
                        ),
                      const Material(
                        color: Colors.transparent,
                        child: Padding(
                          padding: EdgeInsets.all(8.0),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
              const SizedBox(
                height: 10,
              ),
              const Padding(
                padding: EdgeInsets.only(left: 15.0),
                child: Text(
                  "Meal Name",
                  style: TextStyle(fontSize: 17, fontWeight: FontWeight.w500),
                ),
              ),
              Container(
                height: 50,
                padding: const EdgeInsets.only(top: 10),
                width: MediaQuery.of(context).size.width,
                margin: const EdgeInsets.symmetric(horizontal: 20),
                child: TextFormField(
                  textInputAction: TextInputAction.done,
                  initialValue: model.mealName,
                  maxLines: 1,
                  decoration: InputDecoration(
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(10),
                      borderSide: const BorderSide(color: Colors.grey),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(5),
                      borderSide: const BorderSide(color: Colors.grey),
                    ),
                  ),
                  onChanged: (v) => model.mealName = v,
                ),
              ),

RadioOption页面代码

enum Variant { mealName }

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

  @override
  State<RadioOption> createState() => _RadioOptionState();
}

class _RadioOptionState extends State<RadioOption> {
  Variant? _character = Variant.mealName;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        RadioListTile<Variant>(
          title: const Text('$mealName'),
          value: Variant.MealName,
          groupValue: _character,
          onChanged: (Variant? value) {
        
        setState(() {
          _character = value;
        });
      },
    ),

实现方案

要完成餐品名称的实时同步,核心是利用Riverpod的状态监听机制,确保输入值的变化能被RadioOption页面感知并更新。具体步骤如下:

1. 完善AddMealProvider的状态通知机制

首先确保你的addMealProvider对应的模型类能在mealName变化时通知监听者,否则RadioOption无法感知到值的更新。根据你使用的Riverpod状态类型,做对应修改:

如果你用的是ChangeNotifier:

class AddMealModel extends ChangeNotifier {
  String _mealName = '';
  String get mealName => _mealName;

  set mealName(String value) {
    _mealName = value;
    notifyListeners(); // 必须调用此方法触发状态更新通知
  }

  // 其他已有方法(如writeMeal、enabled等)保持不变
}

// 对应provider定义
final addMealProvider = ChangeNotifierProvider((ref) => AddMealModel());

如果你用的是StateNotifier:

// 先定义状态类
class AddMealState {
  final String mealName;
  // 其他状态字段(如file、mealUrl、enabled等)

  AddMealState({
    this.mealName = '',
    // 初始化其他字段
  });

  AddMealState copyWith({
    String? mealName,
    // 其他字段的copy参数
  }) {
    return AddMealState(
      mealName: mealName ?? this.mealName,
      // 其他字段赋值
    );
  }
}

class AddMealModel extends StateNotifier<AddMealState> {
  AddMealModel() : super(AddMealState());

  set mealName(String value) {
    state = state.copyWith(mealName: value); // 通过copyWith更新状态
  }

  // 其他已有方法保持不变
}

// 对应provider定义
final addMealProvider = StateNotifierProvider<AddMealModel, AddMealState>((ref) => AddMealModel());

2. 修正AddMealPage的provider读取方式

在AddMealPage中,将ref.read改为ref.watch(确保provider的状态更新机制能正常工作):

@override
Widget build(BuildContext context, WidgetRef ref) {
  final model = ref.watch(addMealProvider); // 替换ref.read为ref.watch
  // 后续代码保持不变
}

3. 修改RadioOption页面以监听状态变化

将RadioOption改为ConsumerWidget(或ConsumerStatefulWidget,如果需要保留本地选中状态),读取addMealProvider中的mealName来动态更新单选按钮标题:

简化版(无本地选中状态):

enum Variant { mealName }

class RadioOption extends ConsumerWidget {
  const RadioOption({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 实时监听mealName的变化
    final mealName = ref.watch(addMealProvider).mealName;
    Variant? selectedVariant = Variant.mealName;

    return Column(
      children: <Widget>[
        RadioListTile<Variant>(
          // 动态显示餐品名称,为空时显示默认文本
          title: Text(mealName.isNotEmpty ? mealName : '未输入餐品名称'),
          value: Variant.mealName, // 修正枚举大小写错误
          groupValue: selectedVariant,
          onChanged: (Variant? value) {
            // 若需要将选中状态存入provider,可在此调用model方法
          },
        ),
      ],
    );
  }
}

保留本地选中状态版:

class RadioOption extends ConsumerStatefulWidget {
  const RadioOption({Key? key}) : super(key: key);

  @override
  ConsumerState<RadioOption> createState() => _RadioOptionState();
}

class _RadioOptionState extends ConsumerState<RadioOption> {
  Variant? _selectedVariant = Variant.mealName;

  @override
  Widget build(BuildContext context) {
    final mealName = ref.watch(addMealProvider).mealName;

    return Column(
      children: <Widget>[
        RadioListTile<Variant>(
          title: Text(mealName.isNotEmpty ? mealName : '未输入餐品名称'),
          value: Variant.mealName,
          groupValue: _selectedVariant,
          onChanged: (Variant? value) {
            setState(() {
              _selectedVariant = value;
            });
          },
        ),
      ],
    );
  }
}

4. 修复原代码中的错误

  • 枚举值大小写:原RadioOption代码中Variant.MealName要改为Variant.mealName,和枚举定义一致
  • 动态文本不能用const:原代码中const Text('$mealName')要去掉const,因为mealName是动态变量,无法编译通过

修改完成后,当你在AddMealPage的TextField中输入餐品名称时,RadioOption页面的RadioListTile标题会实时同步显示输入内容。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.21 15:25:44