I understand how to apply keepAlive in PageView. But when I come to flutter_pagewise ( https://pub.dev/packages/flutter_pagewise ), I was confused about how to implement to keep the state for PagewiseListView since when changing the page in PageView caused this PagewiseListView to build again. Anyone can tell me a solution?
My PageView is:
PageView(
children: <Widget>[
CardListView(),
Container(
color: Colors.cyan,
),
],
),
My CardListView is:
class CardListView extends StatefulWidget {
#override
State<StatefulWidget> createState() => _CardListViewState();
}
class _CardListViewState extends State<CardListView>
with AutomaticKeepAliveClientMixin<CardListView> {
static const int PAGE_SIZE = 10;
var postsMap = Map<int, List<PostModel>>();
#override
Widget build(BuildContext context) {
super.build(context);
void updatePostsMap(int pageIndex, List<PostModel> posts) {
setState(() {
postsMap[pageIndex] = posts;
});
}
return PagewiseListView(
pageSize: PAGE_SIZE,
itemBuilder: this._itemBuilder,
pageFuture: (pageIndex) {
if (postsMap.containsKey(pageIndex)) {
print('found');
return Future.value(postsMap[pageIndex]);
} else {
print('load');
return BackendService.getPosts(pageIndex * PAGE_SIZE, PAGE_SIZE)
.then((List<PostModel> posts){
updatePostsMap(pageIndex, posts);
return posts;
});
}
}
);
}
Related
I try to use lazy load to show the order of the customer by using the ScrollController.
Of course, the new user has a low number of orders and those items are not enough to take up the entire screen. So the ScrollController doesn't work. What I can do?
This code will show a basic lazy load. You can change the _initialItemsLength to a low value like 1 to see this issue.
You can try this at api.flutter.dev
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: Scaffold(
appBar: AppBar(title: const Text(_title)),
body: const Center(
child: MyStatefulWidget(),
),
),
);
}
}
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key? key}) : super(key: key);
#override
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
late List myList;
ScrollController _scrollController = ScrollController();
int _initialItemsLength = 10, _currentMax = 10;
#override
void initState() {
super.initState();
myList = List.generate(_initialItemsLength, (i) => "Item : ${i + 1}");
_scrollController.addListener(() {
print("scrolling: ${_scrollController.position.pixels}");
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
_getMoreData();
}
});
}
_getMoreData() {
print("load more: ${myList.length}");
for (int i = _currentMax; i < _currentMax + 10; i++) {
myList.add("Item : ${i + 1}");
}
_currentMax = _currentMax + 10;
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
controller: _scrollController,
itemBuilder: (context, i) {
if (i == myList.length) {
return CupertinoActivityIndicator();
}
return ListTile(
title: Text(myList[i]),
);
},
itemCount: myList.length + 1,
),
);
}
}
First, start _initialItemsLength with 10. The scroller will be available and you will see it in the console. After that, change _initialItemsLength to 1. The console will be blank.
scroll listener will be triggered only if user try to scroll
as an option you need to check this condition _scrollController.position.pixels == _scrollController.position.maxScrollExtent after build method executed and each time when user scroll to bottom
just change a bit initState and _getMoreData methods
#override
void initState() {
super.initState();
myList = List.generate(_initialItemsLength, (i) => 'Item : ${i + 1}');
_scrollController.addListener(() => _checkIsMaxScroll());
WidgetsBinding.instance.addPostFrameCallback((_) => _checkIsMaxScroll());
}
void _checkIsMaxScroll() {
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
_getMoreData();
}
}
_getMoreData() {
print('load more: ${myList.length}');
for (int i = _currentMax; i < _currentMax + 10; i++) {
myList.add('Item : ${i + 1}');
}
_currentMax = _currentMax + 10;
setState(() => WidgetsBinding.instance.addPostFrameCallback((_) => _checkIsMaxScroll()));
}
You can set your ListView with physics: AlwaysScrollableScrollPhysics(), and thus it will be scrollable even when the items are not too many. This will lead the listener to be triggered.
Key code part:
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
physics: AlwaysScrollableScrollPhysics(),
controller: _scrollController,
itemBuilder: (context, i) {
if (i == myList.length) {
return CupertinoActivityIndicator();
}
return ListTile(
title: Text(myList[i]),
);
},
itemCount: myList.length + 1,
),
);
}
The point is 'Find some parameter that can tell whether scroll is enabled or not. If not just load more until the scroll is enabled. Then use a basic step for a lazy load like the code in my question.'
After I find this parameter on google, I don't find this. But I try to check any parameter as possible. _scrollController.any until I found this.
For someone who faces this issue like me.
You can detect the scroll is enabled by using _scrollController.position.maxScrollExtent == 0 with using some delay before that.
This is my code. You can see it works step by step in the console.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class PageStackoverflow72734370 extends StatefulWidget {
const PageStackoverflow72734370({Key? key}) : super(key: key);
#override
State<PageStackoverflow72734370> createState() => _PageStackoverflow72734370State();
}
class _PageStackoverflow72734370State extends State<PageStackoverflow72734370> {
late final List myList;
final ScrollController _scrollController = ScrollController();
final int _initialItemsLength = 1;
bool isScrollEnable = false, isLoading = false;
#override
void initState() {
super.initState();
print("\ninitState work!");
print("_initialItemsLength: $_initialItemsLength");
myList = List.generate(_initialItemsLength, (i) => 'Item : ${i + 1}');
_scrollController.addListener(() {
print("\nListener work!");
print("position: ${_scrollController.position.pixels}");
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) _getData();
});
_helper();
}
Future _helper() async {
print("\nhelper work!");
while (!isScrollEnable) {
print("\nwhile loop work!");
await Future.delayed(Duration.zero); //Prevent errors from looping quickly.
try {
print("maxScroll: ${_scrollController.position.maxScrollExtent}");
isScrollEnable = 0 != _scrollController.position.maxScrollExtent;
print("isScrollEnable: $isScrollEnable");
if (!isScrollEnable) _getData();
} catch (e) {
print(e);
}
}
print("\nwhile loop break!");
}
void _getData() {
print("\n_getData work!");
if (isLoading) return;
isLoading = true;
int i = myList.length;
int j = myList.length + 1;
for (i; i < j; i++) {
myList.add("Item : ${i + 1}");
}
print("myList.length: ${myList.length}");
isLoading = false;
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
controller: _scrollController,
itemBuilder: (context, i) {
if (i == myList.length) {
return const CupertinoActivityIndicator();
}
return ListTile(title: Text(myList[i]));
},
itemCount: myList.length + 1,
),
);
}
}
You can test in my test. You can change the initial and incremental values at ?initial=10&incremental=1.
I know, this case is rare. Most applications show more data widget height than the height of the screen or the data fetching 2 turns that enough for making these data widget height than the height of the screen. But I put these data widgets in the wrap for users that use the desktop app. So, I need it.
I am building a method that the user can select a prefered profile picture to show arround the app, using provider package. I used shared_preferences to save the profile picture preferences on locally as a int value. And it worked, means I can save the profile picture to local system. But the problem is, the provider package completely became useless in this case, because I have to convert the widget to statefull and call the setState method when ever I insert a profilePicture widget inside the widget tree. And even the profilePicture widget in the HomeScreen not updating this way. I want to know how can I use the provider package for this issue instead of using statefulWidgets.
watch the Gif or video
This is the Provider class I created:
class ProfilePicProvider with ChangeNotifier {
ProfilePicPref profilePicPreferences = ProfilePicPref();
int _svgNumber = 1;
int get svgNumber => _svgNumber;
set svgNumber(int value) {
_svgNumber = value;
profilePicPreferences.setProfilePic(value);
notifyListeners();
}
void changePic(int val) {
_svgNumber = val;
profilePicPreferences.setProfilePic(val);
notifyListeners();
}
}
This is the sharedPreferences class
class ProfilePicPref {
static const PRO_PIC_STS = 'PROFILESTATUS';
setProfilePic(int svgNo) async {
SharedPreferences profilePref = await SharedPreferences.getInstance();
profilePref.setInt(PRO_PIC_STS, svgNo);
}
Future<int> getProfilePicture() async {
SharedPreferences profilePref = await SharedPreferences.getInstance();
return profilePref.getInt(PRO_PIC_STS) ?? 1;
}
}
This is the image selection screen and save that data to sharedPreferences class
class SelectProfilePicture extends StatefulWidget {
const SelectProfilePicture({Key? key}) : super(key: key);
#override
State<SelectProfilePicture> createState() => _SelectProfilePictureState();
}
class _SelectProfilePictureState extends State<SelectProfilePicture> {
int svgNumber = 1;
ProfilePicProvider proProvider = ProfilePicProvider();
#override
void initState() {
getCurrentProfilePicture();
super.initState();
}
void getCurrentProfilePicture() async {
proProvider.svgNumber =
await proProvider.profilePicPreferences.getProfilePicture();
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
CurrentAccountPicture(
path: 'assets/svg/${proProvider.svgNumber}.svg'),
Expanded(
child: GridView.builder(
itemCount: 15,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
),
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
setState(() {
svgNumber = index + 1;
});
proProvider.changePic(index + 1);
proProvider.svgNumber = index + 1;
},
child: SvgPicture.asset('assets/svg/${index + 1}.svg'),
);
},
),
),
],
),
);
}
}
This is the HomeScreen which is not updating the profile image whether it is statefull or stateless
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final proPicProvider = Provider.of<ProfilePicProvider>(context);
return Scaffold(
body:
Column(
children: [
Row(
children: [
CurrentAccountPicture(
path: 'assets/svg/${proPicProvider.svgNumber}.svg'),
],
),
],
),
);
}
}
example:
I have to convert the widget to statefull and call setState method to get the current profile picture from sharedPreferences. You may find this screen from the GIF I provided.
class Progress extends StatefulWidget {
const Progress({Key? key}) : super(key: key);
#override
State<Progress> createState() => _ProgressState();
}
class _ProgressState extends State<Progress> {
ProfilePicProvider proProvider = ProfilePicProvider();
#override
void initState() {
getCurrentProfilePicture();
super.initState();
}
void getCurrentProfilePicture() async {
proProvider.svgNumber =
await proProvider.profilePicPreferences.getProfilePicture();
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: SizedBox(
height: 130.0,
width: 130.0,
child: SvgPicture.asset(
'assets/svg/${proProvider.svgNumber}.svg'),
),
),
);
}
}
The problem is in _SelectProfilePictureState when you create new instance of your ChangeNotifier:
ProfilePicProvider proProvider = ProfilePicProvider();. It means you are not using the provider available across the context but creating new one every time. So when the value of your provider changed it has effect only inside _SelectProfilePictureState. Instead of creating new instance you must call it always using the context:
class SelectProfilePicture extends StatefulWidget {
const SelectProfilePicture({Key? key}) : super(key: key);
#override
State<SelectProfilePicture> createState() => _SelectProfilePictureState();
}
class _SelectProfilePictureState extends State<SelectProfilePicture> {
int svgNumber = 1;
// [removed] ProfilePicProvider proProvider = ProfilePicProvider();
//removed
/*void getCurrentProfilePicture() async {
proProvider.svgNumber =
await proProvider.profilePicPreferences.getProfilePicture();
setState(() {});
}*/
#override
Widget build(BuildContext context) {
//use provider from the context
final proProvider = Provider.of<ProfilePicProvider>(context,listen:true);
return Scaffold(
body: Column(
children: [
CurrentAccountPicture(
path: 'assets/svg/${proProvider.svgNumber}.svg'),
Expanded(
child: GridView.builder(
itemCount: 15,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
),
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
setState(() {
svgNumber = index + 1;
});
proProvider.changePic(index + 1);
proProvider.svgNumber = index + 1;
},
child: SvgPicture.asset('assets/svg/${index + 1}.svg'),
);
},
),
),
],
),
);
}
}
If you enter the application you may want send initially selected image to your provider:
Add parameter to the constructor of ProfilePicProvider:
ProfilePicProvider(SharedPreferences prefs): _svgNumber = prefs.getInt(ProfilePicPref.PRO_PIC_STS) ?? 1;
In main.dart:
Future<void> main()async{
WidgetsFlutterBinding.ensureInitialized();
var prefs = await SharedPreferences.getInstance();
runApp(
MultiProvider(
providers:[
ChangeNotifierProvider( create:(_) => ProfilePicProvider(prefs))
],
child: yourtopWidget
)
);
}
I am new to flutter, so please excuse my experience.
I have 2 classes, both stateful widgets.
One class contains the tiles for a listview.
Each tile class has a checkbox with a state bool for alternating true or false.
The other class (main) contains the body for creating the listview.
What I'd like to do is retrieve the value for the checkbox in the main class, and then update a counter for how many checkbboxes from the listview tiles have been checked, once a checkbox value is updated. I am wondering what the best practices are for doing this.
Tile class
class ListTile extends StatefulWidget {
#override
_ListTileState createState() => _ListTileState();
}
class _ListTileState extends State<ListTile> {
#override
Widget build(BuildContext context) {
bool selected = false;
return Container(
child: Row(
children: [Checkbox(value: selected, onChanged: (v) {
// Do something here
})],
),
);
}
}
Main Class
class OtherClass extends StatefulWidget {
#override
_OtherClassState createState() => _OtherClassState();
}
class _OtherClassState extends State<OtherClass> {
#override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
Text("Checkbox selected count <count here>"),
ListView.builder(itemBuilder: (context, index) {
// Do something to get the selected checkbox count from the listview
return ListTile();
}),
],
),
);
}
}
Hope this is you are waiting for
class OtherClass extends StatefulWidget {
#override
_OtherClassState createState() => _OtherClassState();
}
class _OtherClassState extends State<OtherClass> {
bool selected = false;
#override
void initState() {
super.initState();
}
var items = [
Animal("1", "Buffalo", false),
Animal("2", "Cow", false),
];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("title")),
body: Container(
child: ListView.builder(
itemCount: items.length,
shrinkWrap: true,
itemBuilder: (ctx, i) {
return Row(
children: [
Text(items[i].name),
ListTile(
id: items[i].id,
index: i,
)
],
);
}),
));
}
}
ListTileClass
class ListTile extends StatefulWidget {
final String? id;
final int? index;
final bool? isSelected;
const ListTile ({Key? key, this.id, this.index, this.isSelected})
: super(key: key);
#override
_ListTileState createState() => _ListTileState();
}
class _ListTileState extends State<ListTile> {
bool? selected = false;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Container(
width: 20,
child: Checkbox(
value: selected,
onChanged: (bool? value) {
setState(() {
selected = value;
});
}));
}
}
I'd recommend using a design pattern such as BLoC or using the Provider package. I personally use the Provider Package. There are plenty of tutorials on youtube which can help get you started.
I am using google_mobile_ads: ^0.12.1+1 for showing ads in my app. It's working fine in all places except in the Scaffold bottomNavigationBar which has a PageView in the body. Relevant code is -
class QuizPage extends StatefulWidget {
final List<Question> questions;
QuizPage({Key key, this.questions}) : super(key: key);
#override
_QuizPageState createState() => _QuizPageState();
}
class _QuizPageState extends State<QuizPage> {
PageController controller;
Question question;
int currentPage = 0;
#override
void initState() {
super.initState();
controller = PageController();
question = widget.questions.first;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Title'),
),
body: buildQuestionsBody(),
bottomNavigationBar: loadAds(),
);
}
Widget buildQuestionsBody() {
return PageView.builder(
onPageChanged: (index) => nextQuestion(index: index),
controller: controller,
itemCount: widget.questions.length,
itemBuilder: (context, index) {
return Container(
child: Center(
child: Text(
widget.questions[index].questionText,
textAlign: TextAlign.center,
)),
);
},
);
}
Widget loadAds() {
return Container(
height: 50,
child: AdWidget(
key: UniqueKey(),
ad: AdMobService.createBannerAd()..load(),
),
);
}
void nextQuestion({int index, bool jump = false}) {
final nextPage = controller.page + 1;
final indexPage = index ?? nextPage.toInt();
setState(() {
question = widget.questions[indexPage];
currentPage = indexPage;
});
if (jump) {
controller.jumpToPage(indexPage);
}
}
}
If I run the above code as such or shift controller = PageController(); from initState to the top where I declare the variable, the ads load initially, but on changing pages they don't load, also the app freezes without giving any error.
If I remove controller = PageController(); from initState, the ads load normally, and also on swiping pages change but now I get an error - The getter 'page' was called on null.
I am not able to find out the source of the error.
Edit 1 - After some more tries I found out that this conflict occurs whenever setState() is called for e.g. whenever I call nextQuestion in this code or other methods where setState() is called. Without google ads, setState works fine in all methods. So it seems now the question should be - How to make google ads work with setState()?
I have tried this method and it works
class QuizPage extends StatefulWidget {
final List<Question> questions;
QuizPage({Key key, #required this.questions}) : super(key: key);
#override
_QuizPageState createState() => _QuizPageState();
}
class _QuizPageState extends State<QuizPage> {
PageController _controller=PageController();
List<Widget> _questions;
#override
void initState() {
super.initState();
if(widget.questions==null)
_questions = <Widget>[Text("Alas! No questions RN")];
else{
_questions = widget.questions.map((e)=>Container(
child: Center(
child: Text(
e.questionText,
textAlign: TextAlign.center,
)),
))
.toList();
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: Appbar(
title: Text('Title'),
),
body: buildQuestionsBody(),
bottomNavigationBar: loadAds(),
);
}
Widget buildQuestionsBody() {
return PageView(
controller: _controller,
children: _questions);
}
Widget loadAds() {
return Container(
height: 50,
child: AdWidget(
key: UniqueKey(),
ad: AdMobService.createBannerAd()..load(),
),
);
}
I am building 9 SwitchListTile using for loop, as now the button contains same code so am having trouble
in its onChanged as my each button will have specific event to perform, how should i achieve it? Is it possible to send the button text/id or anything unique based on which i can perform the specific tasks?
Here _onChanged(value, counter); 'counter' is nothing but you can assume a variable in for loop assigning values 1-9 for each button. So Onchange i should know which button was pressed!.
I tried assigning // key: ValueKey(counter), to SwitchListTile constructor but was unable to retrieve that value in onChanged.
class MySwitchListTilesContainer extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[800],
body: ListView(
children: List.generate(20, (i)=>MySwitchListTile(
)),
),
);
}
}
class MySwitchListTile extends StatefulWidget {
#override
_MySwitchListTileState createState() => new _MySwitchListTileState();
}
class _MySwitchListTileState extends State<MySwitchListTile> {
bool _v = false;
#override
Widget build(BuildContext context) {
return SwitchListTile(
value:_v,
onChanged: (value) {
_onChanged(value, counter);
},
);
}
}
void _onChanged(bool _v, int index) {
setState(() {
_v = _v;
if (index == 1) {
print(index);
} else {
print(index +1);
}
});
}
You can copy paste run full code below
You can pass callback to use in onChanged
code snippet
ListView(
children: List.generate(
20,
(i) => MySwitchListTile(
v: false,
callback: () {
print("index is $i");
setState(() {
});
},
)),
)
...
class MySwitchListTile extends StatefulWidget {
final bool v;
final VoidCallback callback;
...
return SwitchListTile(
value: widget.v,
onChanged: (value) {
widget.callback();
},
);
working demo
output of working demo
I/flutter ( 6597): index is 0
I/flutter ( 6597): index is 2
I/flutter ( 6597): index is 6
full code
import 'package:flutter/material.dart';
class MySwitchListTilesContainer extends StatefulWidget {
#override
_MySwitchListTilesContainerState createState() => _MySwitchListTilesContainerState();
}
class _MySwitchListTilesContainerState extends State<MySwitchListTilesContainer> {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[800],
body: ListView(
children: List.generate(
20,
(i) => MySwitchListTile(
v: false,
callback: () {
print("index is $i");
setState(() {
});
},
)),
),
);
}
}
class MySwitchListTile extends StatefulWidget {
final bool v;
final VoidCallback callback;
const MySwitchListTile({Key key, this.v, this.callback}) : super(key: key);
#override
_MySwitchListTileState createState() => new _MySwitchListTileState();
}
class _MySwitchListTileState extends State<MySwitchListTile> {
#override
Widget build(BuildContext context) {
return SwitchListTile(
value: widget.v,
onChanged: (value) {
widget.callback();
},
);
}
}
/*void _onChanged(bool _v, int index) {
setState(() {
_v = _v;
if (index == 1) {
print(index);
} else {
print(index + 1);
}
});
}*/
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MySwitchListTilesContainer(),
);
}
}