how to use useStreamController in HookWidget?

Issue

Im new to flutter hooks, and riverpod(state-manangement),

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String _url = "https://owlbot.info/api/v4/dictionary/";
  String _token = "ae7cbdfff57e548a4360348ee519123a741d8e3d";

  TextEditingController _controller = TextEditingController();

  StreamController _streamController;
  Stream _stream;

  Timer _debounce;

  Future _search() async {
    if (_controller.text == null || _controller.text.length == 0) {
      _streamController.add(null);
      return;
    }

    _streamController.add("waiting");
    Response response = await get(_url + _controller.text.trim(),
        headers: {"Authorization": "Token " + _token});
    _streamController.add(json.decode(response.body));
  }

  @override
  void initState() {
    super.initState();

    _streamController = StreamController();
    _stream = _streamController.stream;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Flictionary"),
        bottom: PreferredSize(
          preferredSize: Size.fromHeight(48.0),
          child: Row(
            children: <Widget>[
              Expanded(
                child: Container(
                  margin: const EdgeInsets.only(left: 12.0, bottom: 8.0),
                  decoration: BoxDecoration(
                    color: Colors.white,
                    borderRadius: BorderRadius.circular(24.0),
                  ),
                  child: TextFormField(
                    onChanged: (String text) {
                      if (_debounce?.isActive ?? false) _debounce.cancel();
                      _debounce = Timer(const Duration(milliseconds: 1000), () {
                        _search();
                      });
                    },
                    controller: _controller,
                    decoration: InputDecoration(
                      hintText: "Search for a word",
                      contentPadding: const EdgeInsets.only(left: 24.0),
                      border: InputBorder.none,
                    ),
                  ),
                ),
              ),
              IconButton(
                icon: Icon(
                  Icons.search,
                  color: Colors.white,
                ),
                onPressed: () {
                  _search();
                },
              )
            ],
          ),
        ),
      ),
      body: Container(
        margin: const EdgeInsets.all(8.0),
        child: StreamBuilder(
          stream: _stream,
          builder: (BuildContext ctx, AsyncSnapshot snapshot) {
            if (snapshot.data == null) {
              return Center(
                child: Text("Enter a search word"),
              );
            }

            if (snapshot.data == "waiting") {
              return Center(
                child: CircularProgressIndicator(),
              );
            }

            return ListView.builder(
              itemCount: snapshot.data["definitions"].length,
              itemBuilder: (BuildContext context, int index) {
                return ListBody(
                  children: <Widget>[
                    Container(
                      color: Colors.grey[300],
                      child: ListTile(
                        leading: snapshot.data["definitions"][index]
                                    ["image_url"] ==
                                null
                            ? null
                            : CircleAvatar(
                                backgroundImage: NetworkImage(snapshot
                                    .data["definitions"][index]["image_url"]),
                              ),
                        title: Text(_controller.text.trim() +
                            "(" +
                            snapshot.data["definitions"][index]["type"] +
                            ")"),
                      ),
                    ),
                    Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Text(
                          snapshot.data["definitions"][index]["definition"]),
                    )
                  ],
                );
              },
            );
          },
        ),
      ),
    );
  }
}

i just wanted to convert above statefulWidget to HookWidget and how to use riverpod as statemanagenent for the above example. I know some basics of hooks and riverpod, but still i’m confused between the hooks, statemanagement(riverpod).
Please Can someone help understand them and provide some examples or at least convert the above code into the hook widget, and using hookbuilder

Thanks in advance

Solution

First off, the code:

final textProvider = StateProvider<String>((_) => '');

final responseFutureProvider =
    FutureProvider.autoDispose.family<Response, String>((ref, text) async {
  if (text == null || text.length == 0) {
    throw Error();
  }

  final String _url = "https://owlbot.info/api/v4/dictionary/";
  final String _token = "ae7cbdfff57e548a4360348ee519123a741d8e3d";

  return await get(_url + text.trim(), headers: {"Authorization": "Token " + _token});
});

final responseProvider = Computed<AsyncValue<Response>>((read) {
  final text = read(textProvider).state;
  return read(responseFutureProvider(text));
});

String _useDebouncedSearch(TextEditingController controller) {
  final search = useState(controller.text);

  useEffect(() {
    Timer? timer;
    void listener() {
      timer?.cancel();
      timer = Timer(
        const Duration(milliseconds: 1000),
        () => search.value = controller.text,
      );
    }

    controller.addListener(listener);
    return () {
      timer?.cancel();
      controller.removeListener(listener);
    };
  }, [controller]);

  return search.value;
}

class MyHomePage extends HookWidget {
  const MyHomePage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final controller = useTextEditingController();
    final text = useProvider(textProvider);
    text.state = _useDebouncedSearch(controller);

    return Scaffold(
      appBar: AppBar(
        title: Text("Flictionary"),
        bottom: PreferredSize(
          preferredSize: Size.fromHeight(48.0),
          child: Row(
            children: <Widget>[
              Expanded(
                child: Container(
                  margin: const EdgeInsets.only(left: 12.0, bottom: 8.0),
                  decoration: BoxDecoration(
                    color: Colors.white,
                    borderRadius: BorderRadius.circular(24.0),
                  ),
                  child: TextFormField(
                    controller: controller,
                    decoration: InputDecoration(
                      hintText: "Search for a word",
                      contentPadding: const EdgeInsets.only(left: 24.0),
                      border: InputBorder.none,
                    ),
                  ),
                ),
              ),
              Icon(
                Icons.search,
                color: Colors.white,
              ),
            ],
          ),
        ),
      ),
      body: MyHomePageBody(),
    );
  }
}

class MyHomePageBody extends HookWidget {
  const MyHomePageBody({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final text = useProvider(textProvider).state;
    final response = useProvider(responseProvider);

    response.when(
      error: (err, stack) => Center(child: Text('Error: $err')),
      loading: () => Center(child: CircularProgressIndicator()),
      data: (response) => ListView.builder(
        itemCount: Response["definitions"].length,
        itemBuilder: (BuildContext context, int index) {
          return ListBody(
            children: <Widget>[
              Container(
                color: Colors.grey[300],
                child: ListTile(
                  leading: response["definitions"][index]["image_url"] == null
                      ? null
                      : CircleAvatar(
                          backgroundImage:
                              NetworkImage(response["definitions"][index]["image_url"]),
                        ),
                  title: Text(text.trim() + "(" + response["definitions"][index]["type"] + ")"),
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(8.0),
                child: Text(response["definitions"][index]["definition"]),
              )
            ],
          );
        },
      ),
    );
  }
}
  1. We add an external text provider so that we can read the text field from other providers.
  2. We create a FutureProviderFamily so that we can perform the API call with a parameter, the text from your text field. In Riverpod, families enable the passing parameters to providers.
  3. We create a Computed that will call the Future every time the value of the text provider changes. This returns an AsyncValue which is a wonderful replacement to the StreamBuilder you were using (will explain more).
  4. Refactored your debounced search a bit to use the useEffect hook. This will handle disposing the resources for your timer and update the textProvider as necessary. (I learned this from Remi’s Marvel example)
  5. We no longer need an onChanged or manual button press to search, as the textprovider’s state is being updated whenever the controller changes.
  6. Moved the body of your page into its own class to separate what needs to be loaded from what is static.
  7. Now, instead of a StreamBuilder, we can use AsyncValue to handle the loading, error, and success states of your build.

I know this was a lot to cover, so I’d recommend really digging into the docs to learn more about everything that’s in this example.

Answered By – Alex Hartford

Answer Checked By – Gilberto Lyons (FlutterFixes Admin)

Leave a Reply

Your email address will not be published. Required fields are marked *