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