Flutter update ui when fetching from API

Viewed 32

I'm using FutureBuilder for fetching API request. but it only builds once and it doesn't update ui unless hot reloded. i aslo tried StreamBuilder and convert the future method to stream. so how can i update my ui.

model

import 'dart:convert';

List<User> usersFromJson(String str) =>
    List<User>.from(json.decode(str).map((json) => User.fromJson(json)));
String usersToJson(List<User> data) =>
    json.encode(List<dynamic>.from(data.map((e) => e.toJson())));

class User {
  int? id;
  String? name;
  String? username;
  String? email;
  String? role;

  User({this.id, this.name, this.username, this.email, this.role});

  @override
  toString() => 'User: $name';

  factory User.fromJson(Map<String, dynamic> json) => User(
      email: json['email'],
      name: json['name'],
      id: json['id'],
      username: json['username'],
      role: json['role']);

  Map<String, dynamic> toJson() =>
      {"name": name, "username": username, "email": email, "role": role};
}

api call

Future fetchUsers() async {
  Uri url = Uri.parse("${BASE_URL}user");
  final response = await http.get(url, headers: <String, String>{
    'Content-Type': 'application/json; charset=UTF-8',
    'Authorization': 'Bearer $TOKEN',
  });

  var userview = <User>[];

  if (response.statusCode == 200) {
    var jsonres = json.decode(response.body);
    for (var res in jsonres) {
      userview.add(User.fromJson(res));
    }
  }
  return userview;
}

Future createUser(String name, String username, String email, String password,
    String roles) async {
  Uri url = Uri.parse("${BASE_URL}user");
  final response = await http
      .post(url,
          headers: <String, String>{
            'Content-Type': 'application/json; charset=UTF-8',
            'Authorization': 'Bearer $TOKEN',
          },
          body: jsonEncode(<String, dynamic>{
            'name': name,
            'email': email,
            'username': username,
            'password': password,
            'roles': roles
          }))
      .then((value) => fetchUsers());
}

and my page

StreamBuilder(
                        stream: fetchUsers().asStream(),
                        builder: (context, snapshot) {
                          if (snapshot.hasData) {
                            return Column(
                              children: [
                                PaginatedDataTable(
                                    sortColumnIndex: sortColumnIndex,
                                    sortAscending: isAscending,
                                    columns: [
                                      DataColumn(
                                          label: const Text("Id"),
                                          onSort: onSort),
                                      DataColumn(
                                          label: const Text("Name"),
                                          onSort: onSort),
                                      DataColumn(
                                          label: const Text("Username"),
                                          onSort: onSort),
                                      DataColumn(
                                          label: const Text("Email"),
                                          onSort: onSort),
                                      DataColumn(
                                          label: const Text("Roles"),
                                          onSort: onSort),
                                      DataColumn(
                                          label: const Text("Actions"),
                                          onSort: onSort),
                                    ],
                                    header: Row(
                                      mainAxisAlignment:
                                          MainAxisAlignment.spaceBetween,
                                      children: [
                                        Text(
                                          "Manage Users",
                                          style: TextStyle(
                                              fontSize: width * 0.04,
                                              fontWeight: FontWeight.normal),
                                        ),
                                        MaterialButton(
                                          onPressed: () {
                                            showMaterialModalBottomSheet(
                                                context: context,
                                                builder: (context) => SizedBox(
                                                      height: height * 0.9,
                                                      child:
                                                          BottomSheetWidget(),
                                                    ),
                                                shape:
                                                    const RoundedRectangleBorder(
                                                        borderRadius:
                                                            BorderRadius.only(
                                                                topLeft: Radius
                                                                    .circular(
                                                                        15),
                                                                topRight: Radius
                                                                    .circular(
                                                                        15))));
                                          },
                                          color: const Color.fromRGBO(
                                              30, 119, 66, 1),
                                          shape: RoundedRectangleBorder(
                                              borderRadius:
                                                  BorderRadius.circular(10)),
                                          child: Text(
                                            "Add User",
                                            style: TextStyle(
                                                color: Colors.white,
                                                fontSize: width * 0.03),
                                          ),
                                        )
                                      ],
                                    ),
                                    source: dataSource(
                                        snapshot.data! as List<User>))
                              ],
                            );
                          } else if (snapshot.hasError) {
                            return Text("${snapshot.error}");
                          }
                          return const Center(
                              child: CircularProgressIndicator());
                        })

additionally i'm using paginated datatable and this is the code for that.(it's related)

in the page

DataTableSource dataSource(List<User> userList) =>
      MyTable(datasList: userList, context: context);

and the datasource page i'm calling the createUser here.

class MyTable extends DataTableSource {
  MyTable({required this.datasList, required this.context});
  final List<User> datasList;
  BuildContext context;

  Widget Button(String title, Color color, String id) {
    return MaterialButton(
      onPressed: () {
        //deleteUser(id);
        //updateUser(id, title);
        createUser("name2", "user2", "email2@email.com", "password", "Admin");
      },
      child: Text(
        title,
        style: const TextStyle(color: Colors.white),
      ),
      color: color,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
    );
  }

  @override
  DataRow? getRow(int index) {
    return DataRow.byIndex(index: index, cells: [
      DataCell(Text(datasList[index].id.toString())),
      DataCell(ConstrainedBox(
        constraints: const BoxConstraints(maxWidth: 100),
        child: Text(
          datasList[index].name.toString(),
          overflow: TextOverflow.ellipsis,
        ),
      )),
      DataCell(ConstrainedBox(
          constraints: const BoxConstraints(maxWidth: 100),
          child: Text(
            datasList[index].username.toString(),
            overflow: TextOverflow.ellipsis,
          ))),
      DataCell(ConstrainedBox(
          constraints: const BoxConstraints(maxWidth: 100),
          child: Text(
            datasList[index].email.toString(),
            overflow: TextOverflow.ellipsis,
          ))),
      DataCell(ConstrainedBox(
          constraints: const BoxConstraints(maxWidth: 100),
          child: Text(
            datasList[index].role.toString(),
            overflow: TextOverflow.ellipsis,
          ))),
      DataCell(Row(
        children: [
          Button("Edit", Colors.lightBlue, datasList[index].id.toString()),
          const SizedBox(
            width: 5,
          ),
          Button("Delete", Colors.red, datasList[index].id.toString()),
        ],
      )),
    ]);
  }

  @override
  bool get isRowCountApproximate => false;

  @override
  int get rowCount => datasList.length;

  @override
  int get selectedRowCount => 0;
}
1 Answers

In your StreamBuilder you call fetchUsers() but you only call it once. The fact that the createUsers function also calls it doesn't affect the Streambuilder and therefore it doesn't update. You need to create a property field that is a Future of the type that fetchUsers() returns, and have a FutureBuilder listen to that property field. Whenever the createUser method changes anything it should complete that property field, and because the FutureBuilder is listening to that same future, it will rebuild.

I'd also suggest picking the appropriate strategy: if this is a stream of changes, use a property field that is a Stream, feed it with new data and listen to it with a StreamBuilder. Alternatively, if it is a value that changes frequently, use a ValueListenable and a ValueListenableBuilder, and if it is a value that only changes once (e.g. wait for one API call to complete before showing anything) then use a Future and a FutureBuilder - but don't mix them up.

Related