Search code examples
flutterfuture

Flutter Dynamically build cards base on http response


I would like to create cards based on a http response when a button is clicked, and I would like the "Search Item" and "Search Button" stay on top while the card list still can scroll, please help.

I tried to use FutureBuilder widget but it loads data when the page loads the first time (without button being pressed).

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();

  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, _searchItem) async {
    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: Text('Client List')),
      body: SingleChildScrollView(
        child: 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: InputDecoration(
                    border: OutlineInputBorder(), labelText: 'Search Item'),
                controller: _searchItemController,
              ),
            ),
            SizedBox(height: 20),
            Container(
              height: 45,
              width: 250,
              decoration: BoxDecoration(
                  color: Colors.teal, borderRadius: BorderRadius.circular(16)),
              child: TextButton(
                onPressed: () {
                  fetchData(http.Client(), _searchItemController.text);
                },
                child: Text(
                  'Search',
                  style: TextStyle(color: Colors.white, fontSize: 20),
                ),
              ),
            ),
            FutureBuilder<List<clientRecord>>(
              future: fetchData(http.Client(), _searchItemController.text),
              builder: (context, snapshot) {
                if (snapshot.hasError) {
                  return const Center(
                    child: Text('An error has occurred!'),
                  );
                } else if (snapshot.hasData) {
                  return buildBody(dl: snapshot.data!);
                } else {
                  return const Center(
                    child: CircularProgressIndicator(),
                  );
                }
              },
            ),
          ],
        ),
      ),
    );
  }
}


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

  final List<clientRecord> dl;

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
        scrollDirection: Axis.vertical,
        controller: _scrollController,
        child: Column(
          children:
          dl.map((dataRecord) => dataCard(dataRecord, context)).toList(),
        ));
  }
}
Widget dataCard(dataRecord, 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: [
            Text(dataRecord.englishName),
          ],
        ),
      ),
    ),
  );
}




Solution

  • You have two separate questions here - about Future builder use; and keeping Search Item and Search Button on top. I'll try to answer the first question.

    Your Future Builder fires because you tell it to: you call it in FutureBuilder itself:

    FutureBuilder<List<clientRecord>>(
                  future: fetchData(http.Client(), _searchItemController.text),
    

    What you can do is introduce a separate Future variable, and based on it being null or not - you create your future builder. Something like:

    Future<List<clientRecord>>? myFuture;
    
    ...
    
    child: Column(
              children: [
    ...
      if (myFuture!=null) FutureBuilder<List<clientRecord>>(
                  future: myFuture
     ...
    else Text('Press button to get list')
    
    

    And your button press should do something like (with setState being called to trigger rebuild):

    TextButton(
                    onPressed: () {
                      myFuture=fetchData(http.Client(), _searchItemController.text);
                      setState((){});
                    },