I'm working on an audiobook listening app. There I have my main page, which consists of 3 page widgets linked through a Page View that I can swipe to navigate through these pages. On each page I have laid exactly one banner ad from google_mobile_ads package. The problem is that if I add those 3 banners, my app starts to lag a lot, I did check the release version of my app, it is still laggy. In order to understand better the situation I will explain how my ad loading works.
I have a file, where I store the classes regarding those banner ads:
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:flutter/material.dart';
late AnchoredAdaptiveBannerAdSize adaptiveBannerSize;
class AdWidgets {
static Container? libraryPag;
static Container? homePag;
static Container? settingsPag;
}
void _setLoaded() {
AdState.loaded = true;
}
class AdState {
static bool loaded = false;
late Future<InitializationStatus> initialization;
AdState(this.initialization);
String get bannerAdUnitId => 'ca-app-pub-3940256099942544/6300978111';
BannerAdListener get adListener => _adListener;
BannerAdListener _adListener = BannerAdListener(
onAdLoaded: (ad) {
_setLoaded();
}
);
}
There is the AdState class which I use to load my banner ads by providing them a BannerAdListener and an AdUnitId, the bool loaded is used in the widgets that display those banners in order for them to know if the ads where loaded and can be displayed properly.
Next I have a Stateful Widget Content, that I use to navigate through my app routes:
class _ContentState extends State<Content> with SingleTickerProviderStateMixin {
#override
void didChangeDependencies() {
// TODO: implement didChangeDependencies
super.didChangeDependencies();
final adState = Provider.of<AdState>(context);
adState.initialization.then((status) {
AdWidgets.libraryPag = Container(
width: MediaQuery.of(context).size.width,
height: adaptiveBannerSize.height.toDouble(),
child: AdWidget(
ad: BannerAd(
adUnitId: adState.bannerAdUnitId,
size: adaptiveBannerSize,
request: AdRequest(),
listener: adState.adListener
)
..load(),
)
);
AdWidgets.homePag = Container(
width: 320,
height: 100,
child: AdWidget(ad: BannerAd(
adUnitId: adState.bannerAdUnitId,
size: AdSize.largeBanner,
request: AdRequest(),
listener: adState.adListener
)
..load()),
);
AdWidgets.settingsPag = Container(
width: 320,
height: 250,
child: AdWidget(ad: BannerAd(
adUnitId: adState.bannerAdUnitId,
size: AdSize.mediumRectangle,
request: AdRequest(),
listener: adState.adListener
)
..load()
)
);
});
}
#override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: Settings.theme,
builder: (context, value, _) {
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Settings.theme.value == 'Dark' ? Brightness.light : Brightness.dark,
));
return WillPopScope(
onWillPop: () async {
if (bottomBarIndex.value != 1) {
moveHome();
}
return false;
},
child: Scaffold(
resizeToAvoidBottomInset: false,
body: Navigator(
key: Content.contentNavigatorKey,
initialRoute: '/',
onGenerateRoute: (RouteSettings settings) {
Widget builder;
switch (settings.name) {
case '/':
builder = MainPage();
break;
case '/bookPage':
Book book = settings.arguments as Book;
builder = BookPage(book: book);
bottomBarIndex.value = -1;
break;
case '/addBook':
builder = AddBookPage();
bottomBarIndex.value = -1;
break;
case '/changeCover':
Book book = settings.arguments as Book;
builder = ChangeCoverPage(book: book);
bottomBarIndex.value = -1;
break;
default:
throw Exception('Invalid route: ${settings.name}');
}
return PageRouteBuilder(
transitionDuration: Duration(milliseconds: 300),
transitionsBuilder:
(context, animation, secAnimation, child) {
animation = CurvedAnimation(
parent: animation, curve: Curves.easeIn);
return FadeTransition(
opacity: animation,
child: child,
);
},
pageBuilder: (context, animation, secAnimation) {
return builder;
});
},
onPopPage: (route, result) {
return route.didPop(result);
},
),
));
},
);
}
}
In the didChangeDependencies method I initialize the adState (the way it is done in the Monetizing apps with Flutter official tutorial) and then assign three banner widgets to the each of the static Container from the AdWidgets custom class. Then I just display those Containers as I please, for example:
class LibraryPage extends StatefulWidget{
const LibraryPage({Key? key}) : super(key: key);
#override
_LibraryPageState createState() => _LibraryPageState();
}
class _LibraryPageState extends State<LibraryPage> with TickerProviderStateMixin {
late TabController _tabController;
late AppBar appBar;
#override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
appBar = AppBar(
brightness: Settings.theme.value == 'Dark' ? Brightness.dark : Brightness.light,
shadowColor: Settings.theme.value == 'Dark' ? Color.fromRGBO(0, 0, 0, 0.1) : Color.fromRGBO(0, 0, 0, 0.5),
backgroundColor: Settings.colors[2],
title: Text('Library'),
bottom: TabBar(
controller: _tabController,
labelPadding: EdgeInsets.zero,
labelStyle: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w500
),
labelColor: Settings.colors[3],
indicator: UnderlineTabIndicator(
borderSide: BorderSide(color: Settings.colors[3], width: 2)
),
tabs: [
Tab(
text: 'Reading',
),
Tab(
text: 'New',
),
Tab(
text: 'Read',
)
],
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Settings.colors[1],
resizeToAvoidBottomInset: false,
appBar: appBar,
body: Column(
children: [
if (AdWidgets.libraryPag != null && AdState.loaded)
AdWidgets.libraryPag!
else
Container(
width: MediaQuery.of(context).size.width,
height: adaptiveBannerSize.height.toDouble(),
color: Settings.colors[0],
child: Center(
child: Text(
'Ad not loaded',
style: TextStyle(
color: Settings.colors[4],
fontFamily: 'Open Sans'),
)),
),
ScrollConfiguration(
behavior: MyBehavior(),
child: SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height - appBar.preferredSize.height - appBar.bottom!.preferredSize.height - adaptiveBannerSize.height,
child: TabBarView(
controller: _tabController,
children: [
Category(category: 'reading'),
Category(category: 'new'),
Category(category: 'read')
],
),
),
),
],
),
);
}
}
As you can see there, I check if the AdWidgets.libraryPag is not null and if the ad was loaded and then if true - I display my ad.
The major issue is that - when I navigate from one page to another or scroll down it is very laggy even in the release build, also my bottomNavBar starts flickering a lot. Also when I do any kind of action in my app I get a stack of messages in of the console log saying D/AudioManager(30711): getStreamVolume isRestricted mode = 0. My guess is that the ads are reloading too often causing some performance issues.
Here is a small portion of my console log:
W/ContentCatcher(30711): Failed to notify a WebView
W/Choreographer(30711): Already have a pending vsync event. There should only be one at a time.
W/Choreographer(30711): OPTS_INPUT: First frame was drawed before optimized, so skip!
W/ContentCatcher(30711): Failed to notify a WebView
W/ContentCatcher(30711): Failed to notify a WebView
W/Choreographer(30711): Already have a pending vsync event. There should only be one at a time.
W/Choreographer(30711): Already have a pending vsync event. There should only be one at a time.
W/ContentCatcher(30711): Failed to notify a WebView
W/ContentCatcher(30711): Failed to notify a WebView
W/Choreographer(30711): OPTS_INPUT: First frame was drawed before optimized, so skip!
W/Choreographer(30711): OPTS_INPUT: First frame was drawed before optimized, so skip!
W/ContentCatcher(30711): Failed to notify a WebView
I/Ads (30711): Use RequestConfiguration.Builder().setTestDeviceIds(Arrays.asList("B0B49AF69923DB48675A21F6F88D2525")) to get test ads on this device.
D/AudioManager(30711): getStreamVolume isRestricted mode = 0
D/AudioManager(30711): getStreamVolume isRestricted mode = 0
How can I fix this performance issue?
Refer to this answer on issue,
Prior to Android 10 AndroidView Should have better performance. if this is the case, maybe we can check the device OS and use AndroidView Instead of PlatformViewLink.
atrope make fork from repo google_mobile_ads and do this trick for better performance for device os less than Android 10
Use this repo instead of official google_ads in your pubspec.yaml
google_mobile_ads:
git:
url: https://github.com/SuaMusica/googleads-mobile-flutter.git
path: packages/google_mobile_ads
ref: feature/suamusica
Special thanks for atrope
The problem is not in your implementation or code. It is because the package is using androidView which is very expensive when handled by devices with android below 10. You can force flutter to switch to platformviewlink like this and it will fix the issue for the moment.
if (Platform.isAndroid) {
androidInfo = await DeviceInfoPlugin().androidInfo;
final isAndroidOld = (androidInfo.version.sdkInt ?? 0) < 29; //Android 10
useHybridComposition = remoteConfig.getBool(
isAndroidOld
? RemoteConfigKey.useHybridCompositionOlderOS
: RemoteConfigKey.useHybridCompositionNewerOS,
);
if (isAndroidOld && useHybridComposition) {
await PlatformViewsService.synchronizeToNativeViewHierarchy(false);
}
}
Related
Im currently working on an app where it gets the pictures from galleries, then listing them out. But i cant seem to get it right. Currently facing an issue where i get the error
"_TypeError (type 'Future' is not a subtype of type 'Widget')".
any ideas
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
import 'package:multi_image_picker2/multi_image_picker2.dart';
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
class TestPage extends StatefulWidget {
const TestPage({Key? key}) : super(key: key);
#override
_TestPageState createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> {
List<Asset> claims = <Asset>[];
final ImagePicker imgpicker = ImagePicker();
List<Asset>? imagefiles;
loadLimitedImages() async {
try {
var pickedfiles = await MultiImagePicker.pickImages(maxImages: 3);
if (pickedfiles != null) {
setState(() {
imagefiles = pickedfiles;
});
} else {
print("No image is selected.");
}
} catch (e) {
print("error while picking file.");
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text(
'Testing Functions',
style: TextStyle(color: Colors.black),
),
centerTitle: true,
backgroundColor: Colors.white,
iconTheme: IconThemeData(color: Colors.black),
),
body: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(10),
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
imagefiles != null ? loadLimitedImages() : Container(),
imagefiles?.length == 3
? Container()
: IconButton(
onPressed: () => loadLimitedImages(),
icon: Icon(Icons.camera_enhance),
iconSize: 100,
),
],
),
),
),
),
);
}
}
I can only have 3 max images and it must be stacked in a row, when the images are picked, it goes back to the screen. If 3 images are showing, the camera icon dissapears, if not it will be beside the picked images if less than 3.
You get this error because of the following line in your build() method.
imagefiles != null ? loadLimitedImages() : Container(),
Here you call loadLimitedImages() which will return a Future, which is not a Widget. That method does not return anything, so this won't work in the way you try to use it.
If I understand it correctly, you want to pick 3 images, that operation should be a result of an action, e.g. a user taps a button to pick images. That is where you can call your loadLimitedImages() method. You shouldn't call such a method inside the build() method, since it could run frequently. The build() method's purpose is to build the UI based on the current state.
If you need to pick images without user interaction you can initiate it in the initState() of your State.
so I've already have a problem with this code before I ask for some help and I got some.
The help fix my error but I have a new one .
So basically I'm waiting for a variable and this variable is not null because when I print it I can see the value of this variable.
The screen return me the fallback values and I don't know why.
I have two screens one for create my variable and the other for all the graphic stuff.
This is the detail screen:
class DetailScreen extends StatefulWidget {
const DetailScreen({Key? key, required this.mangaImg, required this.mangaTitle, required this.mangalink}) : super(key: key);
final String mangaImg,mangaTitle,mangalink;
#override
_DetailScreenState createState() => _DetailScreenState();
}
class _DetailScreenState extends State<DetailScreen> {
String? mangaGenre,mangaStatus,mangaAuthor,mangaDesc;
List<Map<String,dynamic>>? mangaDetail;
List<Map<String,dynamic>>? mangaDescList;
List<Map<String,dynamic>>? mangaChapters;
Future<void> getMangaInfo() async {
String TempBaseurl = widget.mangalink.split(".com")[0] + ".com";
String TempRoute = widget.mangalink.split(".com")[1];
final webscraper = WebScraper(TempBaseurl);
if (await webscraper.loadWebPage(TempRoute)){
mangaDetail = webscraper.getElement("div.panel-story-info > div.story-info-right > table > tbody > tr > td.table-value", []);
mangaDescList = webscraper.getElement("div.panel-story-info > div.panel-story-info-description", []);
}
mangaGenre = mangaDetail![3]['title'].toString().trim();
mangaStatus = mangaDetail![2]['title'].toString().trim();
mangaAuthor = mangaDetail![1]['title'].toString().trim();
mangaDesc = mangaDescList![0]['title'].toString().trim();
print(mangaDesc);
print(mangaGenre);
print(mangaStatus);
}
#override
Future<void> getMangaInfos()async {
await getMangaInfo();
}
#override
Widget build(BuildContext context) {
Size screensize = MediaQuery.of(context).size;
return Scaffold(
appBar: AppBar(
backgroundColor: Constants.mygreen,
title: Text(widget.mangaTitle),
centerTitle: true,
),
body: Container(
height: screensize.height,
width: screensize.width,
child: SingleChildScrollView(
child: Column(
children: [
MangaInfo(
mangaImg: widget.mangaImg,
mangaStatus: mangaStatus??"Error" ,
mangaAuthor : mangaAuthor??"Error" ,
And this is the graphic screen:
class MangaInfo extends StatelessWidget {
const MangaInfo({Key? key, required this.mangaImg, required this.mangaStatus, required this.mangaAuthor}) : super(key: key);
final String mangaImg, mangaStatus,mangaAuthor;
#override
Widget build(BuildContext context) {
return Container(
height: 300,
width: double.infinity,
child: Column(
children: [
Expanded(child: Center(child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Container(height: 170, width: 130,child: Image.network(mangaImg, fit: BoxFit.cover,)
),
Text("By $mangaAuthor - $mangaStatus"
, style: const TextStyle(
fontFamily: "SFProDisplay",
))
],
))),
const SizedBox(
height: 10,
),
Container(
height: 80,
width: double.infinity,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: const[
MangaInfoBtn(icon:Icons.play_arrow, title: "Read"),
MangaInfoBtn(icon:Icons.library_add_check, title: "Favorites"),
MangaInfoBtn(icon:Icons.list, title: "Chapters"),]
An image of the screen for more details :
So you get the manga information on the method called getMangaInfo, you await said method on the method getMangaInfos But you never call getMangaInfos! you also override it, but I don't think it is a method declared in State class. This leads me to believe that what you actually meant to do was this:
Future<void> initState() async {
await getMangaInfo();
}
The above is NOT POSSIBLE and will result in a compilation error because initstate should never return a future. The next best thing is this:
void initState() {
getMangaInfo();
}
But this means you are not awaiting getMangaInfo
Both initState and build methods are synchronous, which means you can't use the await keyword on them, on the other hand getMangaInfo is asynchronous. Asynchronous methods will be run whenever flutter has some free time, while synchronous methods will run in order always.
So this is the order in which the methods finish running:
initstate -> build -> getMangaInfo
So by the time you build, getMangaInfo is not done.
I propose two solutions:
1. Use setState to tell flutter when to rebuild:
Future<void> getMangaInfo() async {
String TempBaseurl = widget.mangalink.split(".com")[0] + ".com";
String TempRoute = widget.mangalink.split(".com")[1];
final webscraper = WebScraper(TempBaseurl);
if (await webscraper.loadWebPage(TempRoute)){
mangaDetail = webscraper.getElement("div.panel-story-info > div.story-info-right > table > tbody > tr > td.table-value", []);
mangaDescList = webscraper.getElement("div.panel-story-info > div.panel-story-info-description", []);
}
setState(() {
mangaGenre = mangaDetail![3]['title'].toString().trim();
mangaStatus = mangaDetail![2]['title'].toString().trim();
mangaAuthor = mangaDetail![1]['title'].toString().trim();
mangaDesc = mangaDescList![0]['title'].toString().trim();
});
}
With the change above, the new order looks like this:
initstate -> build -> getMangaInfo -> build.
2. Using a future builder:
This method is probably what I would do, but it might be confusing if you don't know what a FutureBuilder is:
Widget build(BuildContext context) {
Size screensize = MediaQuery.of(context).size;
return FutureBuilder(
future: getMangaInfo(),
builder: (context, snapshot) {
return Scaffold(
appBar: AppBar(
backgroundColor: Constants.mygreen,
title: Text(widget.mangaTitle),
centerTitle: true,
),
body: Container(
height: screensize.height,
width: screensize.width,
child: SingleChildScrollView(
child: Column(
children: [
MangaInfo(
mangaImg: widget.mangaImg,
mangaStatus: mangaStatus??"Error" ,
mangaAuthor : mangaAuthor??"Error" ,
I want to add a button at the end of my GridView. I viewed another similar problem but the code given in the answer does not scroll. Here is the link to that answer.
My design has a similar. Here is a rough sketch.
Also just for clarification, I want the button to appear once the user has scrolled to the end of the grid view.
I am still new to flutter so your help would be much appreciated :)
The thing which you need is ScrollController class.
WHY SCROLLCONTROLLER?
It keeps track of what are we doing with scrolling, that is, scrolling, reached bottom, or top
HOW CAN WE USE IT?
You need to use it inside GridView, to get your things up and running
// Simply initialise your controller in your project
ScrollController _controller = new ScrollController();
// add listener to the controller to find out the scrolling event
_controller.addListener((){});
// pass this into your GridView.
// We we will add it to GridView. ScrollController is for every scrolling widget
// in Flutter
GridView.builder(
controller: _controller,
)
DISCLAIMER: Please do not look at the UI aspect, since we care about the scrolling event tracking and show/hide our button
I have referred to your given link only for creating the UI => Your Provided Link
Also, I have added scrolling event to identify whether we're scrolling or not, but it is commented
The project currently show the button when we reach the bottom, and hide it when we are the top
CODE
class _MyHomePageState extends State<MyHomePage> {
List<String> homeList = [];
//to check whether we have reached bottom
bool isBottom = false;
ScrollController _controller = new ScrollController();
#override
void initState() {
super.initState();
homeList = List.generate(10, (ind) => 'Item $ind').toList();
// adding controller to check whether the page is
// at the bottom
_controller.addListener((){
// reached bottom
if (_controller.offset >= _controller.position.maxScrollExtent &&
!_controller.position.outOfRange) {
setState(() => isBottom = true);
}
// IS SCROLLING
// if (_controller.offset >= _controller.position.minScrollExtent &&
// _controller.offset < _controller.position.maxScrollExtent && !_controller.position.outOfRange) {
// setState(() {
// isBottom = false;
// });
// }
// REACHED TOP
if (_controller.offset <= _controller.position.minScrollExtent &&
!_controller.position.outOfRange) {
setState(() => isBottom = false);
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Container(
height: MediaQuery.of(context).size.height,
child: Stack(
children: [
GridView.builder(
shrinkWrap: true,
controller: _controller,
itemCount: homeList.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, crossAxisSpacing: 20),
itemBuilder: (ctx, i) {
return GestureDetector(
onTap: () => print(i),
child: Container(
margin: EdgeInsets.only(bottom: 20.0),
decoration: BoxDecoration(
color: Colors.indigo[300],
borderRadius: BorderRadius.circular(15.0)
)
)
);
}
),
// if we are bottom we show the button
// else empty container, which is nothing but
// hiding technique in Flutter
isBottom ? Positioned(
bottom: 20,
left: 18,
right: 18,
child: Container(
alignment: Alignment.center,
height: 50,
decoration: BoxDecoration(
color: Colors.orangeAccent,
borderRadius: BorderRadius.circular(15),
),
child: Text('Your widget at the end')
)
) : Container()
]
)
)
);
}
}
RESULT
Whenever I scroll in my listview, I get this error spammed in console:
ScrollController not attached to any scroll views.
'package:flutter/src/widgets/scroll_controller.dart':
Failed assertion: line 110 pos 12: '_positions.isNotEmpty'
I've been trying to fix this all day and I'd like to have someone else take a look at it.
There are more problems with this code, but right now I'm mainly interested in fixing this error.
I've tried to use Listview.builder, checking for hController.hasClients and many more small things. They didn't seem to change anything
class MyHome extends StatefulWidget {
#override
MyHomeState createState() => new MyHomeState();
}
class MyHomeState extends State<MyHome> with SingleTickerProviderStateMixin {
ScrollController hController;
ScrollController tController;
ScrollController fController;
ScrollController bController;
#override
void initState() {
super.initState();
hController = new ScrollController()..addListener(_scrollListener);
tController = new ScrollController()..addListener(_scrollListener);
fController = new ScrollController()..addListener(_scrollListener);
bController = new ScrollController()..addListener(_scrollListener);
}
#override
void dispose() {
super.dispose();
hController.removeListener(_scrollListener);
tController.removeListener(_scrollListener);
fController.removeListener(_scrollListener);
bController.removeListener(_scrollListener);
}
#override
Widget build(BuildContext context) {
return new DefaultTabController(
length: 4,
child: new Scaffold(
//Removed AppBar for readability
body: new TabBarView(
children: [
new Container(// hot
child: ListView(
controller: hController,
children: <Widget>[
Utils.show("hot")
],
),
),
new Container( //Trending
child: ListView(
controller: tController,
children: <Widget>[
Utils.show("trending")
],
),
),
new Container( //Fresh
child: ListView(
controller: fController,
children: <Widget>[
Utils.show("fresh")
],
),
),
new Container( //Best
child: ListView(
controller: bController,
children: <Widget>[
Utils.show("best")
],
),
),
],
),
));
}
void _scrollListener() {
if (hController.position.extentAfter == 0.0) {
setState(() {
Utils.show("hot");
});
}else if (tController.position.extentAfter == 0.0) {
setState(() {
Utils.show("trending");
});
} else if (fController.position.extentAfter == 0.0) {
setState(() {
Utils.show("fresh");
});
} else if (bController.position.extentAfter == 0.0) {
setState(() {
Utils.show("best");
});
}
}
}
Edit: For some clarity, the first time I posted this code, I used tController twice. This was ofcourse a mistake, but did not solve the error. The error happens when scrolling in every one of the four listviews.
To avoid such type of errors use the getter
ScrollController.hasClient
If this is false, then members that interact with the
[ScrollPosition],such as [position], [offset], [animateTo], and [jumpTo],
must not be called.
for example:
if (_controller.hasClients) {
_controller.animateTo(
...
}
The problem is inside _scrollListener.
When you are checking controllers in if-else there is only one view at the scene. Means only one listview is rendered & only one scrollcontroller is completely setup. But in your code they are checking all scrollcontroller's positions in single function. Thats why you are getting this error. First check if controller have the positions, which it will only have if the controller is attached & view is rendered correctly. After that check for extentAfter value.
Exa -
if (hController.positions.length > 0 && tController.position.extentAfter == 0.0) {
}
else if (tController.positions.length > 0 && tController.position.extentAfter == 0.0) {
}
& so on
check controller does not have client ant then delay jump:
if (!_scrollController.hasClients) {
_scrollController.animateTo(_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut);
}
If you paste your code correctly - it seems there can be mistake:
new Container(// hot
child: ListView(
controller: tController,
children: <Widget>[
Utils.show("hot")
],
),
),
new Container( //Trending
child: ListView(
controller: tController,
children: <Widget>[
Utils.show("trending")
],
),
),
You have used tController two times and haven't used hController
Update your flutter sdk it will solve this problem
That is work for me
run this on your cmd - flutter update
I'm building an app for training in Flutter and I'm actually stuck in the filter functionality.
I have a ListView where I fetch data from TheMovieDB API and a ModalBottomSheet with three FilterChips for selecting the filter criteria (popular, top rated and latest movies).
And here's where I'm stuck. I want to call the "_loadNextPage()" method when the user presses the "Done" button in the ModalBottomSheet through "performUpdate()" but I can't do it because they're not in the same class.
I'll post the code down below for better understanding.
class _HomePageState extends State<HomePage> {
RequestProvider _requestProvider = new RequestProvider();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("FluttieDB"),
actions: <Widget>[
IconButton(
icon: Icon(Icons.filter_list),
onPressed: () => buildFilterBottomSheet(),
)
],
),
body: MovieList(_requestProvider, _currentFilter),
);
}
void buildFilterBottomSheet() {
showModalBottomSheet(
context: context,
builder: (builder) {
return Container(
height: 150.0,
decoration: BoxDecoration(color: Colors.white),
child: Column(
children: <Widget>[
buildFilterTitle(context),
Expanded(
child: _FilterChipRow(),
),
],
),
);
});
}
Widget buildFilterTitle(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0),
alignment: Alignment.centerLeft,
height: 46.0,
decoration: BoxDecoration(color: Colors.blue),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Text(
"Filter by",
style: TextStyle(color: Colors.white, fontSize: 20.0),
),
OutlineButton(
onPressed: () => performUpdate(context),
padding: const EdgeInsets.all(0.0),
shape: const StadiumBorder(),
child: Text(
"Done",
style: TextStyle(color: Colors.white),
),
),
],
),
);
}
void performUpdate(BuildContext context) {
MovieList _movieList = new MovieList(_requestProvider, _currentFilter);
_movieList.createState()._loadNextPage();
Navigator.pop(context);
}
}
class MovieList extends StatefulWidget {
MovieList(this.provider, this.currentFilter, {Key key}) : super(key: key);
final RequestProvider provider;
final String currentFilter;
#override
_MovieListState createState() => new _MovieListState();
}
class _MovieListState extends State<MovieList> {
List<Movie> _movies = List();
int _pageNumber = 1;
LoadingState _loadingState = LoadingState.LOADING;
bool _isLoading = false;
_loadNextPage() async {
_isLoading = true;
try {
var nextMovies = await widget.provider
.provideMedia(widget.currentFilter, page: _pageNumber);
setState(() {
_loadingState = LoadingState.DONE;
_movies.addAll(nextMovies);
_isLoading = false;
_pageNumber++;
});
} catch (e) {
_isLoading = false;
if (_loadingState == LoadingState.LOADING) {
setState(() => _loadingState = LoadingState.ERROR);
}
}
}
#override
void initState() {
super.initState();
_loadNextPage();
}
#override
Widget build(BuildContext context) {
switch (_loadingState) {
case LoadingState.DONE:
return ListView.builder(
itemCount: _movies.length,
itemBuilder: (BuildContext context, int index) {
if (!_isLoading && index > (_movies.length * 0.7)) {
_loadNextPage();
}
return MovieListItem(_movies[index]);
});
case LoadingState.ERROR:
return Center(
child: Text("Error retrieving movies, check your connection"));
case LoadingState.LOADING:
return Center(child: CircularProgressIndicator());
default:
return Container();
}
}
}
As you can see, I did some experiments in the performUpdate() but it doesn't refresh the ListView with the selected option in the filters and I don't think it's the best way to achieve what I want.
Thanks and sorry if the question is a bit dumb. I'm a little bit newbie in Flutter.
Redux is a great state management library that originated with React and JS, but has been ported to Dart, and has a flutter specific library as well. Redux is a very powerful framework which uses a pub/sub system to allow your view to subscribe to changes to the model, while using a system of "actions" and "reducers" to update the model.
A great tutorial for getting up and running with Redux in Flutter can be found here
Alternatively you could look into the scoped model, which is another state management library for flutter. The scoped model is less capable, but for simple use cases may be more than adequate.
Further reading:
Understand and choose a state management solution
You Might Not Need Redux