Search code examples
flutterriverpodflutter-state

Riverpod state management - update UI, when request gets called


I have a MainPage, where I have a ListView of my rooms. The ListView uses the response from my getUserRooms service. Everything works fine; I can't understand why The ListView doesn't update when my createRoom request gets called, which is in the same service as the getUserRooms functionality. I'm managing the state using Riverpod

Here are my HTTP requests:

class RoomService {
  RoomRoutes roomRoutes = RoomRoutes();

  final _preferencesService = SharedPreferences();

  Future<List> createRoom(RoomModelRequest postBody) async {
    if (postBody.roomPassword!.trim().isEmpty) {
      postBody.roomPassword = null;
    }

    if (postBody.roomName!.trim().isEmpty) {
      postBody.roomName = null;
    }

    try {
      final userData = await _preferencesService.getPreferences();

      final response = await http.post(Uri.parse(roomRoutes.roomsURL),
          headers: {
            'Content-Type': 'application/json; charset=UTF-8',
            'Authorization': 'Token ${userData.token}'
          },
          body: jsonEncode(postBody.toJson()));

      switch (response.statusCode) {
        case 201:
          final data = jsonDecode(response.body);
          return [response.statusCode, data["Success"]];

        
        default:
          throw Exception(response.reasonPhrase);
      }
    } on SocketException {
      throw Exception("No internet connection");
    } on TimeoutException catch (e) {
      throw Exception("Connection timeout: ${e.message} ");
    } on Exception {
      rethrow;
    }
  }

  Future<List<RoomModelResponse>> getUserRooms() async {
    try {
      final userData = await _preferencesService.getPreferences();

      final response =
          await http.get(Uri.parse(roomRoutes.extendedRoomsURL), headers: {
        'Content-Type': 'application/json; charset=UTF-8',
        'Authorization': 'Token ${userData.token}'
      });

      switch (response.statusCode) {
        case 200:
          Iterable json = jsonDecode(response.body);
          return json.map((room) => RoomModelResponse.fromJson(room)).toList();

       
        default:
          throw Exception(response.reasonPhrase);
      }
    } on SocketException {
      throw Exception("No internet connection");
    } on TimeoutException catch (e) {
      throw Exception("Connection timeout: ${e.message} ");
    } on Exception {
      rethrow;
    }
  }
}

Here in the same file, I have my room provider:

final roomProvider = Provider<RoomService>((ref) => RoomService());

And I'm watching the state of that provider, where on update the getUserRooms is returned.

final roomDataProvider = FutureProvider<List<RoomModelResponse>>((ref) async {
  return ref.watch(roomProvider).getUserRooms();
});

In my MainPage that is how I get the response data

class MainPage extends ConsumerWidget {
  const MainPage({super.key});

  @override
  Widget build(BuildContext context, ref) {
    // provider
    final data = ref.watch(roomDataProvider);

    final roomIDController = TextEditingController();
    final uniqueIDRoom = TextEditingController();

    // UI screen size
    final size = MediaQuery.of(context).size;

    double deviceWidth = size.width;
    double deviceHeight = size.height;

    return Scaffold(
        backgroundColor: bluePrimary,
        body: data.when(
            data: (data) {
              List<RoomModelResponse> roomList =
                  data.map((room) => room).toList();
              return SafeArea(
                  child: Container(
                padding:
                    const EdgeInsets.symmetric(horizontal: 36, vertical: 16),
                child: Column(
                    //crossAxisAlignment: CrossAxisAlignment.center,
                    children: [
                      Row(
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        children: [
                          IconButton(
                            iconSize: deviceWidth * 0.09,
                            icon: const Icon(Icons.person_outline,
                                color: orangePrimary),
                            onPressed: () {},
                          ),
                          IconButton(
                            icon: const Icon(
                              Icons.add,
                              color: orangePrimary,
                            ),
                            iconSize: deviceWidth * 0.09,
                            onPressed: () {
                              Navigator.push(
                                context,
                                MaterialPageRoute(
                                    builder: (context) =>
                                        const CreateRoomScreen()),
                              );
                            },
                          )
                        ],
                      ),
                      SizedBox(height: deviceHeight * 0.04),
                      Align(
                        alignment: Alignment.centerLeft,
                        child: Text("W",
                            style: TextStyle(
                                fontFamily: 'Chalet',
                                fontSize: deviceWidth * 0.12,
                                color: orangePrimary,
                                fontWeight: FontWeight.w300,
                                height: deviceHeight * 0.001)),
                      ),
                      Align(
                        alignment: Alignment.centerLeft,
                        child: Text("W",
                            style: TextStyle(
                                fontFamily: 'Chalet',
                                fontSize: deviceWidth * 0.12,
                                color: whitePrimary,
                                fontWeight: FontWeight.w300,
                                height: deviceHeight * 0.001)),
                      ),
                      Align(
                        alignment: Alignment.centerLeft,
                        child: Text("M",
                            style: TextStyle(
                                fontFamily: 'Chalet',
                                fontSize: deviceWidth * 0.12,
                                color: orangePrimary,
                                fontWeight: FontWeight.w300,
                                height: deviceHeight * 0.001)),
                      ),
                      SizedBox(height: deviceHeight * 0.04),
                      Align(
                        alignment: Alignment.centerLeft,
                        child: Text("Join room",
                            style: TextStyle(
                                fontFamily: 'Chalet',
                                fontSize: deviceWidth * 0.07,
                                color: whitePrimary,
                                fontWeight: FontWeight.w100,
                                height: deviceHeight * 0.001)),
                      ),
                      SizedBox(height: deviceHeight * 0.008),

                      // email textField
                      SizedBox(
                        width: MediaQuery.of(context).size.width * 0.85,
                        child: TextField(
                          controller: roomIDController,
                          decoration: InputDecoration(
                              filled: true,
                              fillColor: whitePrimary,
                              border: OutlineInputBorder(
                                  borderRadius: BorderRadius.circular(12),
                                  borderSide: BorderSide.none),
                              hintText: 'Enter room ID to join it',
                              hintStyle: const TextStyle(
                                  color: Color.fromARGB(255, 174, 173, 173))),
                        ),
                      ),

                      SizedBox(height: deviceHeight * 0.016),

                      Align(
                        alignment: Alignment.bottomRight,
                        child: FloatingActionButton(
                          backgroundColor: orangePrimary,
                          child: const Icon(Icons.arrow_forward_ios_rounded,
                              color: whitePrimary),
                          onPressed: () {},
                        ),
                      ),

                      SizedBox(height: deviceHeight * 0.020),

                      Align(
                        alignment: Alignment.centerLeft,
                        child: Text("My rooms",
                            style: TextStyle(
                                fontFamily: 'Chalet',
                                fontSize: deviceWidth * 0.07,
                                color: whitePrimary,
                                fontWeight: FontWeight.w100,
                                height: deviceHeight * 0.001)),
                      ),

                      SizedBox(height: deviceHeight * 0.014),

                      // Display horizontal scroll rooms
                      Align(
                        alignment: Alignment.centerLeft,
                        child: SizedBox(
                          width: deviceWidth,
                          child: SizedBox(
                            height: deviceWidth * 0.56,
                            width: deviceWidth * 0.42,
                            child: ListView.builder(
                              scrollDirection: Axis.horizontal,
                              itemCount: roomList.length,
                              itemBuilder: (context, index) {
                                return Stack(children: [
                                  Container(
                                    height: deviceWidth * 0.8,
                                    width: deviceWidth * 0.42,
                                    margin: const EdgeInsets.symmetric(
                                        horizontal: 3),
                                    decoration: BoxDecoration(
                                        color: Colors.white,
                                        borderRadius:
                                            BorderRadius.circular(10)),
                                  ),
                                  InkWell(
                                    child: Container(
                                      height: deviceWidth * 0.4,
                                      width: deviceWidth * 0.42,
                                      margin: const EdgeInsets.symmetric(
                                          horizontal: 3),
                                      decoration: BoxDecoration(
                                          color: orangePrimary,
                                          borderRadius:
                                              BorderRadius.circular(10),
                                          boxShadow: const [
                                            BoxShadow(
                                                color: Colors.black,
                                                offset: Offset(0, 5),
                                                blurRadius: 10)
                                          ]),
                                      child: Image.asset(
                                          "assets/Logo.png"),
                                    ),
                                    onTap: () {},
                                  ),
                                  Positioned(
                                      bottom: 35,
                                      left: 12,
                                      child: Text(roomList[index].roomName)),
                                  Positioned(
                                      bottom: 15,
                                      left: 12,
                                      child: GestureDetector(
                                        onTap: () {
                                          uniqueIDRoom.text =
                                              roomList[index].uniqueID;
                                          Clipboard.setData(ClipboardData(
                                              text: uniqueIDRoom.text));

                                          const snackBar = SnackBar(
                                            content: Text("Copied room ID"),
                                          );

                                          // Find the ScaffoldMessenger in the widget tree
                                          // and use it to show a SnackBar.
                                          ScaffoldMessenger.of(context)
                                              .showSnackBar(snackBar);
                                        },
                                        child: Text(
                                            "ID: ${roomList[index].uniqueID}",
                                            style: const TextStyle(
                                                fontWeight: FontWeight.bold)),
                                      ))
                                ]);
                              },
                            ),
                          ),
                        ),
                      )
                    ]),
              ));
            },
            error: (err, s) => Text(err.toString()),
            loading: () => const Center(
                  child: CircularProgressIndicator(),
                )));
  }
}
 

Then, to create the room, I navigate the user to another screen perform a HTTP request, which, if successful returns the user to the MainPage again, and I expect the rooms to be updated, including the new room, also.


Solution

  • Your RoomService and roomDataProvider are not implemented to notify changes to listeners of those providers. You shall modify you code such that depending on you case they extend a Notifier class, in your case it should be AsyncNotifier.

    Key concepts: A Provider is just an utility that (as its name indicates) provides, across your entire app, a certain type of object. That object is cached and hence the same object is returned. A Notifier is a special type of Provider which has an internal state property which stores the object to be returned. This state can be updated by implementing methods which assign new instances to the state property. Listeners to that NotifierProvider will be notified of any updates on that state (this is done by invoking the watch method in your code wherever you need that state object).

    It is key that you identify and clearly define what the state returned by your provider is, and implement your code accordingly (maybe in your case it is a list of rooms??)

    Then when that state is modified, for example when getting new data when performing a get HTTP request, you shall assign that new data object to the state variable of the Riverpod notifier, which will notify any Consumer widgets which are "watching".

    Your use case is almost exactly what the official Riverpod documentation explains in its example for To-do lists. You should take a look at it.