skip to Main Content

I have a big Flutter project which is using my Woocommerce website as backend. Everything is working fine, but it was missing the search function on this recipes screen. I’m completly new to Flutter, but because I have Java experience with some luck and miracle I was able to create this function [the search] using the list view and call the dedicated endpoint if I enter the search term. This is working (hooray), but the problem is, that the list of elements are not refreshing if I call the search. It stays on the "all" view, only if I pull down the screen and make a refresh, only than I will see the search results… I tried with the suggested "key" for the widgets, but because I’m not really familiar with Flutter most likely I use is wrong or not on the right element… What is the best way to make this work? Can I call the refresh function somehow (I tried to find it, but failed) after calling the search or is it possible to force the widget re-draw in this case?

Thank you very much.

Edit3.:

This is the searchRecipeModel class:

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

This is the SearchRecipeRepository class:

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

This is the Blog class, it’s a WordPress entity:

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

This is the BlogListItem class:

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.:

This is the recipe_helper global class:

library globals;

String recipeSerachTerm = "";

Edit.:

This is the class of the 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;
}

This is class of this screen:

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


 

4

Answers


  1. To make the widget "redraw", you need to call the setState() method like this:

    setState(() {
        // Here you can fix widget vars values;
    });
    
    Login or Signup to reply.
  2. For that you just need to call setState((){}), this will notify the framework that the internal state of the object has changed and will redraw the widget.

    Documentation SetState

    Login or Signup to reply.
  3. Solution

    @VORiAND is using the Library Provider.
    The value watched in the Consumer is a List of Objects.
    To ‘force’ the re-draw of the view, he had to either

    • Set his list of Objects to null, notify the listeners, update his list, notify the listeners.
    _list = null;
    notifyListeners();
    
    _list = await fetchDatasFromService();
    notifyListeners();
    

    or

    • Re-create a new List Object and notify the Listeners
    final datasFromService = await fetchDatasFromService();
    _list = List.from(datasFromService);
    notifyListeners();
    

    Original Answer:

    There are multiple ways to refresh a view after some data manipulation.


    Without any State Management library :

    If you’re developing in ‘vanilla’ : you’ll have to execute your data operations and then ‘force’ a refresh of the UI once it’s done.

    The method to use in order to refresh the UI is setState((){});
    Note : For this to work, you HAVE to be in a StatefulWidget

    Here is a fully working 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 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;
      }
    }
    

    Important note : Consider triggering your async requests in the initState or in your afterFirstLayout methods.
    If you trigger it in the build method you’ll end up with unwanted loops.

    The above solution will work 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 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;
      }
    } 
    

    Note: It looks like your delegate is updating a value stored 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, or 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) or an InheritedWidget, but those two solutions will get harder to maintain as your project grows.

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

    The following example showcases how it’d work with the Library Provider :

    1. I create a Controller for 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 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();
      }
    
    }
    
    1. Injection of the Controller in the Widgets Tree and Consuming of the value it holds.
    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();
      }
    }
    
    

    The code above is specific to the Provider lib, but the logic is similar in every State Management library 🙂

    Login or Signup to reply.
  4. So your listing inside any dialog box because haven’t run anything but i guess i know the answer please let me know are your doing in any dialog or in main screen.

    So if you are showing into the any dialog then i have added a code for example like you need to statefulbuilder which is comes with it’s own setState for inner rebuilt the inner UI

      int i = 0;
    
      @override
      Widget build(BuildContext context) {
        return SafeArea(
          child: Scaffold(
            body: Center(
              child: GestureDetector(
                onTap: () {
                  showDialog(
                      context: context,
                      builder: (c) {
                        return StatefulBuilder(builder: (context, setStateInner) {
                          return Dialog(
                              backgroundColor: Colors.transparent,
                              elevation: 0,
                              child: InkWell(
                                  onTap: () {
                                    setStateInner(() {
                                      ++i;
                                      print("$i");
                                    });
                                  },
                                  child: Center(
                                    child: Column(
                                      mainAxisAlignment: MainAxisAlignment.center,
                                      children: <Widget>[
                                        Container(
                                            //width: 100,
                                            color: Theme.of(context)
                                                .dialogBackgroundColor,
                                            padding: const EdgeInsets.all(15),
                                            child: Column(
                                                children: <Widget>[Text("$i")]))
                                      ],
                                    ),
                                  )));
                        });
                      });
                },
                child: Text("Tap me"),
              ),
            ),
          ),
        );
      }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search