flutter - 如何刷新屏幕以更新元素列表?

标签 flutter dart

我有一个大型 Flutter 项目,它使用我的 Woocommerce 网站作为后端。一切工作正常,但食谱屏幕上缺少搜索功能。我对 Flutter 完全陌生,但因为我有 Java 经验,加上一些运气和奇迹,我能够使用 ListView 创建此功能 [搜索],并在输入搜索词时调用专用端点。这是有效的(万岁),但问题是,如果我调用搜索,元素列表不会刷新。它停留在“全部” View 上,只有当我拉下屏幕并刷新时,我才会看到搜索结果...我尝试使用小部件的建议“键”,但因为我不是真正熟悉 Flutter 的我很可能使用的是错误的或不在正确的元素上......使这项工作有效的最佳方法是什么?我可以在调用搜索后以某种方式调用刷新函数(我试图找到它,但失败了),或者在这种情况下是否可以强制小部件重新绘制?

非常感谢。

编辑3:

这是 searchRecipeModel 类:

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;
}

这是 Blog 类,它是一个 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),
          ],
        ),
      ),
    );
  }
}

编辑2:

这是recipe_helper全局类:

library globals;

String recipeSerachTerm = "";

编辑:

这是 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 正在使用库 Provider .
Consumer 中观看的值是一个对象列表。
为了“强制”重新绘制 View ,他必须执行以下任一操作

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

_list = await fetchDatasFromService();
notifyListeners();

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

原始答案:

在进行一些数据操作后,有多种方法可以刷新 View 。


没有任何状态管理库:

如果您使用“vanilla”进行开发:您必须执行数据操作,然后在完成后“强制”刷新 UI。

用于刷新 UI 的方法是 setState((){});
注意:要实现此功能,您必须处于 StatefulWidget

这是一个完整的工作示例:

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 errors
      //And here I can update my var _counter.
      
      //The setState method forces 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 for example
    await Future.delayed(const Duration(seconds: 3)); 
    return _counter + 1;
  }
}

重要说明:考虑在 initState 中触发异步请求或在您的 afterFirstLayout方法。 如果你在 build 中触发它方法你最终会得到不需要的循环。

只要您想要更新触发请求的 Widget,上述解决方案就可以工作。
如果您想更新ListRecipeScreen在您的 CustomSearchDelegate 中进行一些数据操作后的小部件,您必须调用 setState方法IN ListRecipeScreen .

触发此setState在父Widget中,您可以使用回调方法。
在以下示例中,MyHomePage将是你的ListRecipeScreenOtherWidget将是你的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 errors
      //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 for example
    await Future.delayed(const Duration(seconds: 3));
    return 12;
  }
} 

注意:您的委托(delegate)似乎正在更新存储为全局变量的值。
在这种情况下,您甚至不需要创建带有参数的回调方法(就像我在 OtherWidget 中所做的那样:您可以简单地使用不带任何参数的 Function ,或 VoidCallback


使用状态管理库

正如您在上面的回答中看到的,在进行一些数据操作后刷新 View 并不难。
但是,如果您必须刷新一个不是操作数据的 Widget 的直接父级的 Widget,该怎么办?
您可以使用级联回调(请不要这样做)或 InheritedWidget ,但是随着项目的增长,这两种解决方案将变得更难维护。

为此,有a lot of State Management libraries已开发。

以下示例展示了它如何与库提供程序配合使用:

  1. 我为我的页面创建一个 Controller 来操作我的数据。
    该 Controller 扩展了 ChangeNotifier这样我就可以在操作完成时发出通知。
class HomePageController extends ChangeNotifier {

  // I exported 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 for 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();
  }

}
  • 在小部件树中注入(inject) Controller 并使用它所保存的值。
  • 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()
                  ],
                ),
              );
            },
          ),
        );
      }
    }
    
  • 在子小部件中,我检索了对我的 HomePageController 的引用并触发异步请求。
    数据操作完成后,notifyListeners()方法将触发每个 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 lib,但每个状态管理库的逻辑都是相似的:)

    关于flutter - 如何刷新屏幕以更新元素列表?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/73175326/

    相关文章:

    flutter - Navigation.of(context) 不去另一条路线

    flutter - 在 flutter 中显示透明对话框

    Flutter:Debug Console:字母:I,W 是什么意思?

    list - 如何计算列表中项目的出现次数

    dart - 在 _MaterialAppState 中找不到路由 "home-page"的生成器

    flutter - 如何不围绕文本创建边框,而是围绕框边框创建边框?

    dart - MainApp 根据值发送到页面

    firebase - 从Firestore获取字段图

    flutter - 如何将 reCaptcha 实现到 Flutter 应用程序中

    android - 如何在 Flutter 中将 png 图像转换为 jpg 格式?