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

Flutter技术问询:点击按钮基于HTTP响应动态构建卡片并固定搜索栏

问题解决:点击按钮触发请求生成卡片,搜索栏固定顶部

需求说明

需要实现:

  • 点击搜索按钮后才发起HTTP请求,根据响应动态创建卡片
  • 「Search Item」输入框和「Search Button」固定在页面顶部
  • 卡片列表区域可独立滚动

之前使用FutureBuilder时,页面首次加载就自动触发了数据请求,不符合需求。

解决方案

核心调整点:

  • 用状态变量存储请求Future,初始为null,仅在点击按钮时才触发请求
  • 重构布局结构,将搜索区域与列表区域分离,列表区域使用Expanded实现滚动,保证顶部元素固定

修改后的完整代码

import 'dart:convert' as convert;
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';

class clientRecord {
  final String englishName;
  final String chineseName;

  const clientRecord({
    required this.englishName,
    required this.chineseName,
  });

  factory clientRecord.fromJson(Map<String, dynamic> json) {
    return clientRecord(
      englishName: json['Ename'] as String,
      chineseName: json['Cname'] as String,
    );
  }
}

class homePage extends StatefulWidget {
  @override
  _homePageState createState() => _homePageState();
}

class _homePageState extends State<homePage> {
  final _searchItemController = TextEditingController();
  Future<List<clientRecord>>? _fetchFuture; // 存储请求Future的状态变量

  List<clientRecord> parseJson(String responseBody) {
    final parsed =
        convert.jsonDecode(responseBody).cast<Map<String, dynamic>>();
    return parsed
        .map<clientRecord>((json) => clientRecord.fromJson(json))
        .toList();
  }

  Future<List<clientRecord>> fetchData(http.Client client, String searchItem) async {
    // 建议将搜索参数拼接到请求URL中,示例:
    // final response = await client.get(Uri.parse('test.php?search=$searchItem'));
    final response = await client.get(Uri.parse('test.php'));
    return parseJson(response.body);
  }

  @override
  void dispose() {
    _searchItemController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Client List')),
      body: Column(
        children: [
          // 顶部固定的搜索区域
          Padding(
            padding: const EdgeInsets.fromLTRB(0, 30, 0, 20),
            child: Text(
              adminPassword(), // 假设该方法已实现
              style: TextStyle(
                  fontSize: 30,
                  fontWeight: FontWeight.bold,
                  color: Colors.orange[800]),
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(12.0),
            child: TextField(
              decoration: const InputDecoration(
                  border: OutlineInputBorder(), labelText: 'Search Item'),
              controller: _searchItemController,
            ),
          ),
          const SizedBox(height: 20),
          Container(
            height: 45,
            width: 250,
            decoration: BoxDecoration(
                color: Colors.teal, borderRadius: BorderRadius.circular(16)),
            child: TextButton(
              onPressed: () {
                // 点击按钮时触发请求,更新状态变量
                setState(() {
                  _fetchFuture = fetchData(http.Client(), _searchItemController.text);
                });
              },
              child: const Text(
                'Search',
                style: TextStyle(color: Colors.white, fontSize: 20),
              ),
            ),
          ),
          // 可滚动的卡片列表区域
          Expanded(
            child: _fetchFuture == null
                ? const Center(child: Text('点击搜索按钮获取数据'))
                : FutureBuilder<List<clientRecord>>(
                    future: _fetchFuture,
                    builder: (context, snapshot) {
                      if (snapshot.hasError) {
                        return const Center(
                          child: Text('请求出错!'),
                        );
                      } else if (snapshot.hasData) {
                        return buildBody(dl: snapshot.data!);
                      } else {
                        return const Center(
                          child: CircularProgressIndicator(),
                        );
                      }
                    },
                  ),
          ),
        ],
      ),
    );
  }
}

class buildBody extends StatelessWidget {
  const buildBody({super.key, required this.dl});

  final List<clientRecord> dl;

  @override
  Widget build(BuildContext context) {
    // 使用ListView实现高效滚动,替代原嵌套的SingleChildScrollView
    return ListView.builder(
      itemCount: dl.length,
      itemBuilder: (context, index) => dataCard(dl[index], context),
    );
  }
}

Widget dataCard(clientRecord dataRecord, BuildContext c) {
  return GestureDetector(
    onTap: () {
      // Navigator.push(
      //   c,
      //   MaterialPageRoute(builder: (c) => newdevelopmentdetail(dataRecord.id)),
      // );
    },
    child: Card(
      color: Colors.lightGreen[100],
      child: SizedBox(
        height: 100,
        child: Row(
          children: [
            Padding(
              padding: const EdgeInsets.all(16.0),
              child: Text(dataRecord.englishName),
            ),
            const SizedBox(width: 16),
            Text(dataRecord.chineseName),
          ],
        ),
      ),
    ),
  );
}

// 示例实现adminPassword方法
String adminPassword() {
  return 'Admin';
}

关键修改说明

  1. 请求触发逻辑:新增_fetchFuture状态变量,仅在点击搜索按钮时通过setState赋值,彻底避免页面初始化时自动请求
  2. 布局结构:将body改为Column,顶部搜索区域直接放置,列表区域用Expanded包裹,确保顶部元素固定,列表占满剩余空间并可滚动
  3. 列表组件优化:将buildBody中的SingleChildScrollView+Column替换为ListView.builder,更高效处理大量数据,同时解决原代码中_scrollController未定义的问题
  4. 初始状态提示:当_fetchFuture为null时,显示友好提示文本而非加载指示器

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 04:20:19