如何在 HookWidget 中使用 useStreamController?

how to use useStreamController in HookWidget?

我是 flutter hooks 和 riverpod(状态管理)的新手,

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"]),
                    )
                  ],
                );
              },
            );
          },
        ),
      ),
    );
  }
}

我只是想将上面的 statefulWidget 转换为 HookWidget 以及如何使用 riverpod 作为上面示例的 statemanagenent。我知道钩子和 riverpod 的一些基础知识,但我仍然对钩子、statemanagement(riverpod) 感到困惑。 请有人帮助理解它们并提供一些示例或至少将上述代码转换为挂钩小部件,并使用 hookbuilder

提前致谢

首先,代码:

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. 我们添加了一个外部文本提供程序,以便我们可以从其他提供程序读取文本字段。
  2. 我们创建了一个 FutureProviderFamily,这样我们就可以使用参数(来自您的文本字段的文本)执行 API 调用。在 Riverpod 中,家庭允许将参数传递给提供者。
  3. 我们创建了一个 Computed,它会在每次文本提供程序的值发生变化时调用 Future。这个 returns 一个 AsyncValue 是您使用的 StreamBuilder 的绝佳替代品(将解释更多)。
  4. 稍微重构了去抖动搜索以使用 useEffect 挂钩。这将为您的计时器处理资源并根据需要更新 textProvider。 (这是我从 Remi's Marvel example 那里学到的)
  5. 我们不再需要 onChanged 或手动按下按钮来进行搜索,因为只要控制器发生变化,文本提供程序的状态就会更新。
  6. 将您的页面主体移到它自己的 class 中,以将需要加载的内容与静态内容分开。
  7. 现在,我们可以使用 AsyncValue 代替 StreamBuilder 来处理构建的加载、错误和成功状态。

我知道要涵盖的内容很多,所以我建议您真正深入研究文档以了解有关此示例中所有内容的更多信息。