我有一个大的Flutter 翼项目,这是使用我的WooCommerce网站作为后端.一切都很好,但它缺少这个食谱屏幕上的搜索功能.我完全是个初学者,但因为我有过一些幸运和奇迹般的Java经验,所以我能够使用列表视图创建这个函数[搜索],并在输入搜索词时调用专用端点.这是有效的(万岁),但问题是,如果我调用搜索,元素列表不会刷新.它停留在"全部"视图上,只有当我拉下屏幕并进行刷新时,才会看到搜索结果……我试着为窗口小部件建议的"键",但因为我不是真的熟悉Flutter 翼,很可能我使用的是错误的或不在正确的元素上…让这件事起作用的最好方法是什么?在调用搜索之后,我可以以某种方式调用刷新函数(我试图找到它,但失败了),或者在这种情况下是否可以强制小部件重新绘制?

非常感谢.

编辑3.以下内容:

这是samRecipeModel类:

import '../../../models/entities/blog.dart';

import '../../../models/paging_data_provider.dart';
import '../repositories/search_recipe_repository.dart';

export '../../../models/entities/blog.dart';

class SearchRecipeModel extends PagingDataProvider<Blog> {
  SearchRecipeModel() : super(dataRepo: SearchRecipeRepository());

  List<Blog> get recipes => data;

  Future<void> searchRecipes() => getData();
}

这是SearchRecipeRepository类:

import '../../../common/base/paging_repository.dart';

import '../../../models/entities/blog.dart';
import '../../../models/entities/paging_response.dart';

class SearchRecipeRepository extends PagingRepository<Blog> {
  @override
  Future<PagingResponse<Blog>> Function(dynamic) get requestApi =>
      service.api.searchRecipes;
}

这是博客类,它是一个WordPress实体:

    import 'dart:convert';

import 'package:html_unescape/html_unescape.dart';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';

import '../../common/packages.dart';
import '../../services/index.dart';
import '../serializers/blog.dart';

class Blog {
  final dynamic id;
  final String title;
  final String subTitle;
  final String date;
  final String content;
  final String author;
  final String imageFeature;

  const Blog({
    this.id,
    this.title,
    this.subTitle,
    this.date,
    this.content,
    this.author,
    this.imageFeature,
  });

  const Blog.empty(this.id)
      : title = '',
        subTitle = '',
        date = '',
        author = '',
        content = '',
        imageFeature = '';

  factory Blog.fromJson(Map<String, dynamic> json) {
    switch (Config().type) {
      case ConfigType.woo:
        return Blog._fromWooJson(json);
      case ConfigType.shopify:
        return Blog._fromShopifyJson(json);
      case ConfigType.strapi:
        return Blog._fromStrapiJson(json);
      case ConfigType.mylisting:
      case ConfigType.listeo:
      case ConfigType.listpro:
        return Blog._fromListingJson(json);
      default:
        return const Blog.empty(0);
    }
  }

  Blog._fromShopifyJson(Map<String, dynamic> json)
      : id = json['id'],
        author = json['authorV2']['name'],
        title = json['title'],
        subTitle = null,
        content = json['contentHtml'],
        imageFeature = json['image']['transformedSrc'],
        date = json['publishedAt'];

  factory Blog._fromStrapiJson(Map<String, dynamic> json) {
    var model = SerializerBlog.fromJson(json);
    final id = model.id;
    final author = model.user.displayName;
    final title = model.title;
    final subTitle = model.subTitle;
    final content = model.content;
    final imageFeature = Config().url + model.images.first.url;
    final date = model.date;
    return Blog(
      author: author,
      title: title,
      subTitle: subTitle,
      content: content,
      id: id,
      date: date,
      imageFeature: imageFeature,
    );
  }

  Blog._fromListingJson(Map<String, dynamic> json)
      : id = json['id'],
        author = json['author_name'],
        title = HtmlUnescape().convert(json['title']['rendered']),
        subTitle = HtmlUnescape().convert(json['excerpt']['rendered']),
        content = json['content']['rendered'],
        imageFeature = json['image_feature'],
        date = DateFormat.yMMMMd('en_US').format(DateTime.parse(json['date']));

  factory Blog._fromWooJson(Map<String, dynamic> json) {
    String imageFeature;
    var imgJson = json['better_featured_image'];
    if (imgJson != null) {
      if (imgJson['media_details']['sizes']['medium_large'] != null) {
        imageFeature =
            imgJson['media_details']['sizes']['medium_large']['source_url'];
      }
    }

    if (imageFeature == null) {
      var imgMedia = json['_embedded']['wp:featuredmedia'];
      if (imgMedia != null &&
          imgMedia[0]['media_details'] != null &&
          imgMedia[0]['media_details']['sizes']['large'] != null) {
        imageFeature =
            imgMedia[0]['media_details']['sizes']['large']['source_url'];
      }
      /**
       * Netbloom
       * Featured image fix
       */
      if(imageFeature == null &&
          imgMedia[0]['media_details'] != null &&
          imgMedia[0]['media_details']['sizes']['medium_large'] != null){
        imageFeature =
        imgMedia[0]['media_details']['sizes']['medium_large']['source_url'];

      }
      if(imageFeature == null &&
          imgMedia[0]['media_details'] != null &&
          imgMedia[0]['media_details']['file'] != null){
        imageFeature =
        "https://okosgrill.hu/wp-content/uploads/" + imgMedia[0]['media_details']['file'];

      }
      if(imageFeature == null && json['featured_image_urls'] != null && json['featured_image_urls']['medium_large'] != null){
        imageFeature = json['featured_image_urls']['medium_large'];
      }
      if(imageFeature == null && json['featured_image_urls'] != null && json['featured_image_urls']['medium'] != null){
        imageFeature = json['featured_image_urls']['medium'];
      }
      //Fallback
      if(imageFeature == null){
        imageFeature =
        "https://okosgrill.hu/wp-content/uploads/okosgrill-tippek.jpg";
      }
    }
    final author = json['_embedded']['author'] != null
        ? json['_embedded']['author'][0]['name']
        : '';
    final date =
        DateFormat.yMMMMd('hu_HU').format(DateTime.parse(json['date']));

    final id = json['id'];
    final title = HtmlUnescape().convert(json['title']['rendered']);
    final subTitle = json['excerpt']!= null ? HtmlUnescape().convert(json['excerpt']['rendered']) : '';
    final content = json['content']['rendered'];

    return Blog(
      author: author,
      title: title,
      subTitle: subTitle,
      content: content,
      id: id,
      date: date,
      imageFeature: imageFeature,
    );
  }

  static Future getBlogs({String url, categories, page = 1}) async {
    try {
      var param = '_embed&page=$page';
      if (categories != null) {
        param += '&categories=$categories';
      }
      final response =
          await http.get('$url/wp-json/wp/v2/posts?$param'.toUri());

      if (response.statusCode != 200) {
        return [];
      }
      return jsonDecode(response.body);
    } on Exception catch (_) {
      return [];
    }
  }

  static Future<dynamic> getBlog({url, id}) async {
    final response =
        await http.get('$url/wp-json/wp/v2/posts/$id?_embed'.toUri());
    return jsonDecode(response.body);
  }

  @override
  String toString() => 'Blog { id: $id  title: $title}';
}

这是BlogListItem类:

import 'package:flutter/material.dart';
import 'package:html/parser.dart';

import '../../../../common/constants.dart' show RouteList;
import '../../../../common/tools.dart' show Tools, kSize;
import '../../../../models/entities/blog.dart';
import '../../../../routes/flux_navigate.dart';

class BlogListItem extends StatelessWidget {
  final Blog blog;

  const BlogListItem({@required this.blog});

  @override
  Widget build(BuildContext context) {
    var screenWidth = MediaQuery.of(context).size.width;
    if (blog.id == null) return const SizedBox();

    return InkWell(
      onTap: () => FluxNavigate.pushNamed(
        RouteList.detailBlog,
        arguments: blog,
      ),
      child: Container(
        padding: const EdgeInsets.only(right: 15, left: 15),
        child: Column(
          children: <Widget>[
            const SizedBox(height: 20.0),
            ClipRRect(
              borderRadius: BorderRadius.circular(3.0),
              child: Tools.image(
                url: blog.imageFeature,
                width: screenWidth,
                height: screenWidth * 0.5,
                fit: BoxFit.fitWidth,
                size: kSize.medium,
              ),
            ),
            SizedBox(
              height: 30,
              width: screenWidth,
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.end,
                children: <Widget>[
                  Text(
                    blog.date ?? '',
                    style: TextStyle(
                      fontSize: 14,
                      color: Theme.of(context).accentColor.withOpacity(0.5),
                    ),
                    maxLines: 2,
                  ),
                  const SizedBox(width: 20.0),
                  if (blog.author != null)
                    Text(
                      blog.author.toUpperCase(),
                      style: const TextStyle(
                        fontSize: 11,
                        height: 2,
                        fontWeight: FontWeight.bold,
                      ),
                      maxLines: 2,
                    ),
                ],
              ),
            ),
            const SizedBox(height: 20.0),
            Text(
              blog.title ?? '',
              style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
              textAlign: TextAlign.center,
              maxLines: 2,
            ),
            const SizedBox(height: 10.0),
            Text(
              blog.subTitle != null
                  ? parse(blog.subTitle).documentElement.text
                  : '',
              textAlign: TextAlign.center,
              style: TextStyle(
                fontSize: 14,
                height: 1.3,
                color: Theme.of(context).accentColor.withOpacity(0.8),
              ),
              maxLines: 2,
            ),
            const SizedBox(height: 20.0),
          ],
        ),
      ),
    );
  }
}

Edit2.:

这是Recipe_Helper全局类:

library globals;

String recipeSerachTerm = "";

发帖主题:Re:Kolibrios

这是BaseScreen的类:

import 'package:flutter/material.dart';

abstract class BaseScreen<T extends StatefulWidget> extends State<T> {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance
        .addPostFrameCallback((_) => afterFirstLayout(context));
  }

  void afterFirstLayout(BuildContext context) {}

  /// Get size screen
  Size get screenSize => MediaQuery.of(context).size;
}

这是此屏幕的类别:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

import '../../../common/constants.dart';
import '../../../generated/l10n.dart';
import '../../../models/entities/blog.dart';
import '../../../widgets/common/skeleton.dart';
import '../../../widgets/paging_list.dart';
import '../../base.dart';
import '../models/list_recipe_model.dart';
import '../models/search_recipe_model.dart';
import '../helpers/recipe_helper.dart' as globals;
import 'widgets/blog_list_item.dart';

class ListRecipeScreen extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _ListRecipeScreenState();
}

class _ListRecipeScreenState extends BaseScreen<ListRecipeScreen> {
  @override
  Widget build(BuildContext context) {
    key: UniqueKey();
    return Scaffold(
      appBar: !kIsWeb
          ? AppBar(
              elevation: 0.1,
              title: Text(
                S.of(context).recipe,
                style: const TextStyle(color: Colors.white),
              ),
              leading: Center(
                child: GestureDetector(
                  onTap: () => Navigator.pop(context),
                  child: const Icon(
                    Icons.arrow_back_ios,
                    color: Colors.white,
                  ),
                ),
              ),
              actions: <Widget>[
                IconButton(
                  icon: Icon(Icons.search),
                  color: Colors.white,
                  onPressed: () {
                    showSearch(
                      context: context,
                      delegate: CustomSearchDelegate(),
                    );
                  },
                ),
              ],
            )
          : null,
      body: PagingList<ListRecipeModel, Blog>(
        itemBuilder: (context, blog) => BlogListItem(blog: blog),
        loadingWidget: _buildSkeleton(),
        lengthLoadingWidget: 3
      ),
    );
  }

  Widget _buildSkeleton() {
    key: UniqueKey();
    return Padding(
      padding: const EdgeInsets.only(
        left: 16.0,
        right: 16.0,
        bottom: 24.0,
        top: 12.0,
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          const Skeleton(height: 200),
          const SizedBox(height: 12),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              const Skeleton(width: 120),
              const Skeleton(width: 80),
            ],
          ),
          const SizedBox(height: 16),
          const Skeleton(),
        ],
      ),
    );
  }
}

class CustomSearchDelegate extends SearchDelegate {
  @override
  List<Widget> buildActions(BuildContext context) {
    return [
      IconButton(
        icon: Icon(Icons.clear),
        onPressed: () {
          query = '';
        },
      ),
    ];
  }

  @override
  Widget buildLeading(BuildContext context) {
    return IconButton(
      icon: Icon(Icons.arrow_back),
      onPressed: () {
        close(context, null);
      },
    );
  }

  @override
  Widget buildResults(BuildContext context) {
    if (query.length < 4) {
      return Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Center(
            child: Text(
              "Search term must be longer than three letters.",
            ),
          ),
        ],
      );
    }else{
      globals.recipeSerachTerm = query;
    }

    return Scaffold(
      appBar: !kIsWeb
          ? AppBar(
        elevation: 0.1,
        title: Text(
          S.of(context).recipe,
          style: const TextStyle(color: Colors.white),
        ),
        leading: Center(
          child: GestureDetector(
            onTap: () => Navigator.pop(context),
            child: const Icon(
              Icons.arrow_back_ios,
              color: Colors.white,
            ),
          ),
        ),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.search),
            color: Colors.white,
            onPressed: () {
              showSearch(
                context: context,
                delegate: CustomSearchDelegate(),
              );
            },
          ),
        ],
      )
          : null,
      body: PagingList<SearchRecipeModel, Blog>(
        itemBuilder: (context, blog) => BlogListItem(blog: blog),
        loadingWidget: _buildSkeleton(),
        lengthLoadingWidget: 3,
      ),
    );
  }

  Widget _buildSkeleton() {
    key: UniqueKey();
    return Padding(
      padding: const EdgeInsets.only(
        left: 16.0,
        right: 16.0,
        bottom: 24.0,
        top: 12.0,
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          const Skeleton(height: 200),
          const SizedBox(height: 12),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              const Skeleton(width: 120),
              const Skeleton(width: 80),
            ],
          ),
          const SizedBox(height: 16),
          const Skeleton(),
        ],
      ),
    );
  }

  @override
  Widget buildSuggestions(BuildContext context) {
    // This method is called everytime the search term changes.
    // If you want to add search suggestions as the user enters their search term, this is the place to do that.
    return Column();
  }
}


 

推荐答案

@VORiAND is using the Library Provider.
The value watched in the Consumer is a List of Objects.
To 'f或ce' the re-draw of the view, he had to either

  • 将他的对象列表设置为空,通知监听器,更新他的列表,通知监听器.
_list = null;
notifyListeners();

_list = await fetchDatasFromService();
notifyListeners();

  • 重新创建新的列表对象并通知监听器
final datasFromService = await fetchDatasFromService();
_list = List.from(datasFromService);
notifyListeners();

最初的答案是:

在进行一些数据操作之后,有多种方法可以刷新视图.


Without any State Management library :

If you're developing in 'vanilla' : you'll have to execute your data operations and then 'f或ce' a refresh of the UI once it's done.

The method to use in 或der to refresh the UI is setState((){});
Note : F或 this to w或k, you HAVE to be in a StatefulWidget

Here is a fully w或king example :

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  
  @override
  void initState() {
    super.initState();
    //Triggering my async loading of datas
    calculateCounter().then((updatedCounter){
      //The `then` is Triggered once the Future completes without err或s
      //And here I can update my var _counter.
      
      //The setState method f或ces a rebuild of the Widget tree 
      //Which will update the view with the new value of `_counter`
      setState((){
        _counter = updatedCounter;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'Current counter value:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
    );
  }

  Future<int> calculateCounter() async {
    //Demo purpose : it'll emulate a query toward a Server f或 example
    await Future.delayed(const Duration(seconds: 3)); 
    return _counter + 1;
  }
}

Imp或tant note : Consider triggering your async requests in the initState 或 in your afterFirstLayout methods. If you trigger it in the build method you'll end up with unwanted loops.

The above solution will w或k as long as you want to update the Widget which triggered the request.
If you want to update the ListRecipeScreen widget after some data manipulation in your CustomSearchDelegate, you'll have to call the setState method IN the ListRecipeScreen.

To trigger this setState in the parent Widget, you could use a Callback method.
In the following example, MyHomePage would be your ListRecipeScreen and OtherWidget would be your CustomSearchDelegate

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'Current counter value:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
            OtherWidget(callback: (counterValue) {
              //This callback can be called any time by the OtherWidget widget
              //Once it's trigger, the method I'm writing in will be triggered.
              //Since I want to update my Widget MyHomePage, I call setState here.
              setState(() {
                _counter = counterValue;
              });
            })
          ],
        ),
      ),
    );
  }
}

class OtherWidget extends StatefulWidget {
  const OtherWidget({required this.callback, Key? key}) : super(key: key);

  final Function(int counter) callback;

  @override
  State<OtherWidget> createState() => _OtherWidgetState();
}

class _OtherWidgetState extends State<OtherWidget> {
  @override
  void initState() {
    super.initState();
    //Triggering my async loading of datas
    calculateCounter().then((updatedCounter) {
      //The `then` is Triggered once the Future completes without err或s
      //And here I can trigger the Callback Method.

      //You can call here the Callback method passed as parameter,
      //Which will trigger the method written in the parent widget
      widget.callback(updatedCounter);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }

  Future<int> calculateCounter() async {
    //Demo purpose : it'll emulate a query toward a Server f或 example
    await Future.delayed(const Duration(seconds: 3));
    return 12;
  }
} 

Note: It looks like your delegate is updating a value st或ed as a Global variable.
In this case, you don't even need to create a Callback method with a parameter (like I did in the OtherWidget : you could simply use a Function without any params, 或 a VoidCallback


With a State Management Library

As you can see with my answer above, it's not that hard to refresh a view after some data manipulations.
But what if you have to refresh a Widget which isn't a direct parent of the Widget manipulating the datas ?
You could use a cascade of Callbacks (don't do that please) 或 an InheritedWidget, but those two solutions will get harder to maintain as your project grows.

F或 this reason, there are a lot of State Management libraries which were developed.

The following example showcases how it'd w或k with the Library Provider :

  1. I create a Controller f或 my page which will manipulate my datas.
    This controller extends ChangeNotifier so I can notify when the manipulation is done.
class HomePageController extends ChangeNotifier {

  // I exp或ted your global var in this Controller
  String _searchTerms = '';
  String get searchTerms => _searchTerms;

  Future<void> calculateCounter() async {
    //Demo purpose : it'll emulate a query toward a Server f或 example
    await Future.delayed(const Duration(seconds: 3));

    //Updating the class variable
    _searchTerms = 'New value entered by the user';

    //Method provided by the ChangeNotifier extension
    //It'll notify all the Consumers that a value has been changed
    notifyListeners();
  }

}
  1. 在窗口小部件树中注入控制器并使用它持有的值.
class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      //Injecting our HomePageController in the tree, and listening to it's changes
      body: ChangeNotifierProvider<HomePageController>(
        create: (_) => HomePageController(),
        builder: (context, _) {
          return Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text(
                  'Current counter value:',
                ),
                //The Consumer listens to every changes in the HomePageController
                //It means that every time the notifyListeners() is called
                //In the HomePageController, the cildren of the Consumer
                //Will check if they have to be re-drawn
                Consumer<HomePageController>(
                  builder: ((_, controller, __) {
                    return Text(
                      controller.searchTerms,
                      style: Theme.of(context).textTheme.headline4,
                    );
                  }),
                ),
                const OtherWidget()
              ],
            ),
          );
        },
      ),
    );
  }
}
  1. In the child widget, I retrieve a reference to my HomePageController and trigger the async request.
    Once the data manipulation is done, the notifyListeners() method will trigger every Consumer<HomePageController>

class OtherWidget extends StatefulWidget {
  const OtherWidget({Key? key}) : super(key: key);

  @override
  State<OtherWidget> createState() => _OtherWidgetState();
}

class _OtherWidgetState extends State<OtherWidget> {
  @override
  void initState() {
    super.initState();
    //Getting the instance of the HomePageController defined in the parent widget
    final parentController = Provider.of<HomePageController>(context, listen: false);
    //Triggering the data manipulation
    parentController.calculateCounter();
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

上面的代码特定于Provider库,但每个State Management库中的逻辑都是相似的:)

Flutter相关问答推荐

在Flutter中为不受约束的小部件制作动画

无法在Flatter中设计正确的布局

使用Flutter 提供程序包加载状态应用程序时,在自定义按钮中显示CircularProgressIndicator

错误:找不到字体功能类型.Ffltter Google_Fonts包错误

下拉图标未居中

为什么我的PUT请求不能正常工作?

如何在Flutter 中创造以下效果

从Firestore流式传输单个文档还是整个集合更好?

如何使用providerContainer监听/写入FutureProvider?

在带有 flutter 的 Native Android 中,EventChannel.EventSink 始终为 null

如何在flutter中将所有单词的第一个字母大写

无状态小部件被奇怪地重建

如何防止键盘将内容向上推?

Android: 从 URI 获取音频元数据

我怎样才能一次只选中一个复选框?

type '({bool growable}) => List' 不是类型转换中类型 'List' 的子类型

在 Android 12+ 中,REST API 调用在 flutter 中非常慢,在 WiFi 中需要 10 秒,而在移动数据中不到一秒

将一个 Flutter 应用程序集成到另一个 Flutter 应用程序中

Positioned 的宽度或 SizedBox 的宽度都不适用于堆栈小部件 - Flutter

如何在 Flutter 中创建类似箭头标签的设计