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),
],
),
),
),
);
}
3
Answers
By adding a separate Future List and assign to future of FutureBuilder, setState after http call works fine.
For scrollable cards without scroll search items, use ListView wrapped inside Expanded wrapped inside Column works perfectly.
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:
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:
And your button press should do something like (with setState being called to trigger rebuild):
You can make a Api call in a Future and return Column like a code snippet: