Flutter: ScrollController initialScrollOffset not working - flutter

I'm trying to initialize a SingleChildScrollView to start at a certain position with a custom ScrollController. I thought I could use initialScrollOffset and set an initial value in the init method. But somehow when the SingleChildScrollView renders, it only jumps to initialOffset at first build, then when I navigate to another instance of this Widget it doesn't jump to the initialOffset position.
I don't know why, and if I'm lucky maybe one of you have the answer.
Here's my code:
class Artiklar extends StatefulWidget {
final String path;
final double arguments;
Artiklar({
this.path,
this.arguments,
});
#override
_ArtiklarState createState() => _ArtiklarState(arguments: arguments);
}
class _ArtiklarState extends State<Artiklar> {
final double arguments;
_ArtiklarState({this.arguments});
ScrollController _scrollController;
double scrollPosition;
#override
void initState() {
super.initState();
double initialOffset = arguments != null ? arguments : 22.2;
_scrollController = ScrollController(initialScrollOffset: initialOffset);
}
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
final bool isAdmin = Provider.of<bool>(context) ?? false;
var pathElements = widget.path.split('/');
String tag;
if (pathElements.length == 2) {
tag = null;
} else if (pathElements.length == 3) {
tag = pathElements[2];
} else {
tag = null;
}
return StreamBuilder<List<ArtikelData>>(
stream: DatabaseService(tag: tag).artiklarByDate,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
return GlobalScaffold(
body: SingleChildScrollView(
child: Container(
child: Center(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 15),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
GradientHeading(text: "Artiklar", large: true),
isAdmin
? NormalButton(
text: "Skapa ny artikel",
onPressed: () {
Navigator.pushNamed(
context, createNewArtikelRoute);
},
)
: Container(),
SizedBox(height: 10),
SingleChildScrollView(
controller: _scrollController,
scrollDirection: Axis.horizontal,
child: TagList(path: tag),
),
SizedBox(height: 10),
LatestArtiklar(
snapshot: snapshot,
totalPosts: snapshot.data.length,
numberOfPosts: 10,
),
],
),
),
),
),
),
);
} else if (!snapshot.hasData) {
return GlobalScaffold(
body: SingleChildScrollView(
child: Container(
child: Center(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 15),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
GradientHeading(text: "Artiklar", large: true),
SizedBox(height: 10),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: TagList(path: tag),
),
SizedBox(height: 10),
LatestArtiklar(hasNoPosts: true)
],
),
),
),
),
),
);
} else {
return GlobalScaffold(
body: Center(child: CircularProgressIndicator()),
);
}
},
);
}
}

That's because that widget is already built on the tree and thus, initState won't be called again for that widget.
You can override the didUpdateWidget method that will trigger each time that widget is rebuilt and make it jump on there, for example.
#override
void didUpdateWidget(Widget old){
super.didUpdateWidget(old);
_scrollController.jumpTo(initialOffset);
}

keepScrollOffset: false
If this property is set to false, the scroll offset is never saved and initialScrollOffset is always used to initialize the scroll offset.

Related

How to wait for a request to complete using ObservableFuture?

When I transition to a screen where I get a list of information via an API, it initially gives an error:
_CastError (Null check operator used on a null value)
and only after loading the information, the screen is displayed correctly.
I am declaring the variables like this:
#observable
ObservableFuture<Model?>? myKeys;
#action
getKeys() {
myKeys = repository.getKeys().asObservable();
}
How can I enter the page only after loading the information?
In button action I tried this but to no avail!
await Future.wait([controller.controller.getKeys()]);
Modular.to.pushNamed('/home');
This is the page where the error occurs momentarily, but a short time later, that is, when the api call occurs, the data appears on the screen.
class MyKeyPage extends StatefulWidget {
const MyKeyPage({Key? key}) : super(key: key);
#override
State<MyKeyPage> createState() => _MyKeyPageState();
}
class _MyKeyPageState
extends ModularState<MyKeyPage, KeyController> {
KeyController controller = Modular.get<KeyController>();
Widget countKeys() {
return FutureBuilder(
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
final count =
controller.myKeys?.value?.data!.length.toString();
if (snapshot.connectionState == ConnectionState.none &&
!snapshot.hasData) {
return Text('..');
}
return ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: 1,
itemBuilder: (context, index) {
return Text(count.toString() + '/5');
});
},
future: controller.getCountKeys(),
);
}
#override
Widget build(BuildContext context) {
Size _size = MediaQuery.of(context).size;
return controller.getCountKeys() != "0"
? TesteScaffold(
removeHorizontalPadding: true,
onBackPressed: () => Modular.to.navigate('/exit'),
leadingIcon: ConstantsIcons.trn_arrow_left,
title: '',
child: Container(
width: double.infinity,
child: Padding(
padding: const EdgeInsets.only(left: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Keys',
style: kHeaderH3Bold.copyWith(
color: kBluePrimaryTrinus,
),
),
countKeys(),
],
),
),
),
body: Observer(builder: (_) {
return Padding(
padding: const EdgeInsets.only(bottom: 81),
child: Container(
child: ListView.builder(
padding: EdgeInsets.only(
left: 12.0,
top: 2.0,
right: 12.0,
),
itemCount:
controller.myKeys?.value?.data!.length,
itemBuilder: (context, index) {
var typeKey = controller
.myKeys?.value?.data?[index].type
.toString();
var id =
controller.myKeys?.value?.data?[index].id;
final value = controller
.myKeys?.value?.data?[index].value
.toString();
return GestureDetector(
onTap: () {
.
.
},
child: CardMeyKeys(
typeKey: typeKey,
value: value!.length > 25
? value.substring(0, 25) + '...'
: value,
myKeys: pixController
.minhasChaves?.value?.data?[index].type
.toString(),
),
);
},
),
),
);
}),
bottomSheet: ....
)
: TesteScaffold(
removeHorizontalPadding: true,
onBackPressed: () => Modular.to.navigate('/exit'),
leadingIcon: ConstantsIcons.trn_arrow_left,
title: '',
child: Container(
width: double.infinity,
child: Padding(
padding: const EdgeInsets.only(left: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'...',
style: kHeaderH3Bold.copyWith(
color: kBluePrimaryTrinus,
),
),
],
),
),
),
body: Padding(
padding: const EdgeInsets.only(bottom: 81),
child: Container(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Image.asset(
'assets/images/Box.png',
fit: BoxFit.cover,
width: 82.75,
height: 80.91,
),
SizedBox(
height: 10,
),
],
),
), //Center
),
),
bottomSheet: ...
);
}
List<ReactionDisposer> disposers = [];
#override
void initState() {
super.initState();
controller.getKeys();
}
#override
void dispose() {
disposers.forEach((toDispose) => toDispose());
super.dispose();
}
}
Initially the error occurs in this block
value: value!.length > 25
? value.substring(0, 25) + '...'
: value,
_CastError (Null check operator used on a null value)
I appreciate if anyone can help me handle ObservableFuture correctly!
You need to call the "future" adding
Future.wait
(the return type of getKeys) keys=await Future.wait([
controller.getKeys();
]);
The problem is your getKeys function isn't returning anything, so there's nothing for your code to await. You need to return a future in order to await it.
Future<Model?> getKeys() {
myKeys = repository.getKeys().asObservable();
return myKeys!; // Presumably this isn't null anymore by this point.
}
...
await controller.controller.getKeys();
Modular.to.pushNamed('/home');

How do I make the widget run sequentially in flutter?

I have the following code written in a flutter I want to make the pieces run in order (from 1 to the last piece) See the comment on line 34,
how can I Do ?
....
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: _clipsWidget(), // <<< I want to write a code here that allows widgets (widget 1-widget 2....) to be executed in an orderly and sequential manner
),
],
),
),
),
],
),
);
}
Widget _clipsWidget1() {
return Container(
height: 250,
margin: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: <Widget>[
Column(
children: <Widget>[
Container(
....
),
SizedBox(height: 20),
Container(
....
),
}
Widget _clipsWidget2() {.....}
Widget _clipsWidget3() {.....}
Widget _clipsWidget4() {.....}
To have multiple widgets arranged below each other, you can use a Row widget:
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
_clipsWidget1(),
_clipsWidget2(),
_clipsWidget3(),
_clipsWidget4()
]
)
)
Does having to be shown sequentially over time mean that we need animations? Then you should use AnimatedList -> https://api.flutter.dev/flutter/widgets/AnimatedList-class.html
if you need to show first widget for sometime than second widget and so on
then try
class _RunOneByOneState extends State<RunOneByOne> {
final widgets = <Widget>[
_clipsWidget1(),
_clipsWidget2(),
_clipsWidget3(),
_clipsWidget4()
];
int index = 0;
#override
void initState() {
Timer.periodic(const Duration(seconds: 20), (timer) {
if (index != widgets.length - 1) {
setState(() {
index++;
});
} else {
timer.cancel();
}
});
super.initState();
}
#override
Widget build(BuildContext context) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: widgets[index],
);
}
}

how to hide a widget in flutter?

I am trying to hide a widget which has List array and List array gets _images.length. For Example, if image.length is Null, like if there are no images so I want to hide the container which takes space.Not sure what I am missing. I tried the code below. Help me out pls, Thanks. or if there is any other way to do it pls let me know. I am just a beginner.
class PortfolioGallarySubPage extends StatefulWidget{
PortfolioGallarySubPage({Key key,#required this.urls,#required this.currentIndex})
:super(key:key);
#override
_PortfolioGallarySubPage createState() => _PortfolioGallarySubPage();
}
class _PortfolioGallarySubPage extends State<PortfolioGallarySubPage>
with SingleTickerProviderStateMixin{
final GlobalKey<FormState> formKey = new GlobalKey<FormState>();
final GlobalKey<ScaffoldState> key = new GlobalKey<ScaffoldState>();
List<File> _images = [];
final picker = ImagePicker();
Future getImage() async {
final pickedFile = await picker.getImage(source: ImageSource.gallery);
setState(() {
if (pickedFile != null) {
_images.add(File(pickedFile.path));
}
else {
print('No image selected.');
}
});
}
#override
void initState() {
super.initState();
}
#override void dispose()
{
super.dispose();
}
bool isVisible = true;
void changeVisibility(){
setState(() {
if(_images.length ==null ){
isVisible = !isVisible;
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: key,
extendBodyBehindAppBar: true,
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.transparent,
actions: [
ElevatedButton(
child: Text("DONE",style: TextStyle(fontSize: 15)),
onPressed: (){
_uploadImages();
},
style: ElevatedButton.styleFrom(padding: EdgeInsets.fromLTRB(25.0, 15.0, 25.0, 10.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30.0))),
),
],
),
body: Container(
width: _maxScreenWidth,
child: SafeArea(
child:Form(
key: formKey,
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Visibility(
visible: isVisible,
child: Container(
height: 150.0,
padding: EdgeInsets.symmetric(vertical: 15.0,horizontal: 15),
child: ListView(
scrollDirection: Axis.horizontal,
children: [
SizedBox(width: 15.0),
ListView.builder(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
itemCount: _images.length,
itemBuilder: (BuildContext context,int index){
//return Padding(padding: const EdgeInsets.only(top: 0.0,bottom: 0.0),
return InkWell(
onTap: () => print('tapped'),
child: Card(
elevation: 10,
child: SizedBox(height:150, width: 150,child: Image.file(_images[index], fit: BoxFit.cover,)) ,
),
);
},
),
],
),
),
),
],
),
),
],
),
),
),
),
),
);
}
}
_images array length will always return 0 if the list is empty, so you need to set the condition as
setState(() {
isVisible = _images.length > 0;
});
Instead of taking variable isVisible set the _images.length > 0 like
visible: _images.length > 0
And remove the isVisible variable.... it will update the visibility when _images list is updated
there is another solution of this without using visible widget :
class Mywidget extends StatefulWidget {
#override
_MywidgetState createState() => _MywidgetState();
}
class _MywidgetState extends State<Mywidget> {
double width;
double heigth;
void changeVisibility() {
setState(() {
if (_images.length == null) {
width=any width you want ;
heigth = any height you want ;
}else{
setState(() {
width=0;
heigth=0;
});
}
});
}
#override
Widget build(BuildContext context) {
// the contanier that you want to be visble
return Container(
height: heigth,
width: width,
// the list view that has the images
child: ListView(),
);
}
}
if there is an image the height and the width of the widget will be not zero
but if not the widget will be visible because the width and the height will be equal to zero
As I can see in the snippet, you are not calling the changeVisibility method anywhere. Hence, isVisible will always remain true
So make a call to changeVisibility wherever you are calling getImage method.
Also, the logic is inherently wrong,
initialize isVisible to false initially, this way you can make it true when there is an image.

filtering Streambuilder/ listviewBuilder flutter

i am new to flutter and been trying to create a function that refresh the ListView.builder based on users choice.i am saving cities names as Strings inside my firestore documents in user collection.
i have multiple buttons that presents different cities and based on choice i need the ListView builder to rebuild. i have been struggling for a while trying to find the solution to this.
anyone here can help?
this is how i retrieve data from firestore
StreamBuilder(
stream: Firestore.instance.collection('users').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return Text('loading...');
return Container(
width: 890.0,
height: 320.0,
margin: EdgeInsets.symmetric(
vertical: 10.0, horizontal: 00.0),
child: new ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: snapshot.data.documents.length,
itemBuilder: (BuildContext context, int index) {
User user = User.fromDoc(snapshot.data
.documents[index]);
return Padding(
padding: const EdgeInsets.only(top: 0),
child: Container(
height: 300,
width: 300,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(0),
),
child: _buildCard(user)),
);
}),
);
},
),
I just wrote this code to show the implementation for static no of cities, clicking the buttons changes the index which then changes the texts(you will change them to stream builders with custom city streams), you can also scale it to dynamic list by manipulating the city list.
class MyHomePage extends StatefulWidget {
MyHomePage({Key key,}) : super(key: key);
​
​
#override
_MyHomePageState createState() => _MyHomePageState();
}
​
class _MyHomePageState extends State<MyHomePage> {
int stackIndex = 0;
​
final List<String> cities = ['Berlin', 'Denver', 'Nairobi', 'Tokyo', 'Rio'];
​
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Sample'),
),
body: Center(
child: Column(
mainAxisAlignment : MainAxisAlignment.spaceEvenly,
children : [
Row(
mainAxisAlignment : MainAxisAlignment.spaceEvenly,
mainAxisSize : MainAxisSize.max,
children : cities.map((city){
return RaisedButton(
child : Text(city),
onPressed : (){
setState((){
this.stackIndex = cities.indexOf(city);
});
}
);
}).toList()
),
IndexedStack(
index : stackIndex,
children: cities.map((city){
return yourStreamBuilder(city);
}).toList()
),
])
),
);
}
Widget yourStreamBuilder(String city){
//you can use your custom stream here
//Stream stream = Firestore.instance.collection('users').where('myCity', isEqualTo: city).snapshots();
​
​
return Text(city);//replace this with your streamBuilder
}
}
​
int stackIndex = 0;
final List<String> cities =[
'Stockholm',
'Malmö',
'Uppsala',
'Västerås',
'Örebro',
'Linköping',
'Helsingborg',
'Jönköping',
'Norrköping',
'Lund',
'Umeå',
'Gävle',
'Södertälje',
'Borås',
'Huddinge',
'Eskilstuna',
'Nacka',
'Halmstad',
'Sundsvall',
'Södertälje',
'Växjö',
'Karlstad',
'Haninge',
'Kristianstad',
'Kungsbacka',
'Solna',
'Järfälla',
'Sollentuna',
'Skellefteå',
'Kalmar',
'Varberg',
'Östersund',
'Trollhättan',
'Uddevalla',
'Nyköping',
'Skövde',
];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment : MainAxisAlignment.spaceEvenly,
children: <Widget>[
Row(
mainAxisAlignment : MainAxisAlignment.spaceEvenly,
mainAxisSize : MainAxisSize.max,
children: cities.map((city) {
return OutlineButton(
child: Text(city),
onPressed: (){
setState(() {
this.stackIndex = cities.indexOf(city);
});
},
);
}).toList()
),
IndexedStack(
index: stackIndex,
children: cities.map((city){
return myStreamBuilder(city);
})
)
],
),
),
);
}
Widget myStreamBuilder(String city){
Stream stream = Firestore.instance.collection('users').where('myCity', isEqualTo: city).snapshots();
return Text(city);
}
}

Flutter Error: 'indexOf(child) > index': is not true. (StreamBuilder,PageView)

I'm trying to create a screen that is contained within a pageview, that also contains a page view for part of the screen.
To acheive this I have an unlimited page view for the whole page itself, then every page has a header view, with a bottom half that has a page view with 3 possible options. I have this pretty much working, however, the pages I am using I would like a StreamBuilder... This is where the issue is caused.
class DiaryPage extends StatefulWidget {
#override
State<StatefulWidget> createState() => _DiaryPage();
}
class _DiaryPage extends State<DiaryPage> with TickerProviderStateMixin {
DiaryBloc _diaryBloc;
TabController _tabController;
PageController _pageController;
#override
void initState() {
_diaryBloc = BlocProvider.of<DiaryBloc>(context);
_diaryBloc.init();
_tabController = TabController(length: 3, vsync: this);
_pageController = PageController(initialPage: _diaryBloc.initialPage);
super.initState();
}
#override
void dispose() {
_diaryBloc.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Flexible(
child: PageView.builder(
controller: _pageController,
itemBuilder: (BuildContext context, int position) {
return _buildPage(_diaryBloc.getDateFromPosition(position));
},
itemCount: _diaryBloc.amountOfPages,
),
);
}
Widget _buildPage(DateTime date) {
return Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[_getHeader(date), _getTabBody()],
);
}
Widget _getHeader(DateTime date) {
return Card(
child: SizedBox(
width: double.infinity,
height: 125,
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.fromLTRB(8, 16, 8, 0),
child: Text(
'${DateFormat('EEEE').format(date)} ${date.day} ${DateFormat('MMMM').format(date)}',
style: Theme.of(context).textTheme.subtitle,
textScaleFactor: 1,
textAlign: TextAlign.center,
),
),
Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
IconButton(
icon: const Icon(Icons.chevron_left),
onPressed: () => {
_pageController.previousPage(
duration: Duration(milliseconds: 250),
curve: Curves.ease)
},
),
const Expanded(child: LinearProgressIndicator()),
IconButton(
icon: const Icon(Icons.chevron_right),
onPressed: () => {
_pageController.nextPage(
duration: Duration(milliseconds: 250),
curve: Curves.ease)
},
),
],
),
Container(
height: 40.0,
child: DefaultTabController(
length: 3,
child: Scaffold(
backgroundColor: Colors.white,
appBar: TabBar(
controller: _tabController,
unselectedLabelColor: Colors.grey[500],
labelColor: Theme.of(context).primaryColor,
tabs: const <Widget>[
Tab(icon: Icon(Icons.pie_chart)),
Tab(icon: Icon(Icons.fastfood)),
Tab(icon: Icon(Icons.directions_run)),
],
),
),
),
),
],
),
),
);
}
Widget _getTabBody() {
return Expanded(
child: TabBarView(
controller: _tabController,
children: <Widget>[
_getOverviewScreen(),
_getFoodScreen(),
_getExerciseScreen(),
],
),
);
}
// TODO - this seems to be the issue, wtf and why
Widget _getBody() {
return Flexible(
child: StreamBuilder<Widget>(
stream: _diaryBloc.widgetStream,
initialData: _diaryBloc.buildEmptyWidget(),
builder: (BuildContext context, AsyncSnapshot<Widget> snapshot) {
return snapshot.data;
},
),
);
}
Widget _getExerciseScreen() {
return Text("Exercise Screen"); //_getBody();
}
Widget _getFoodScreen() {
return Text("Food Screen"); //_getBody();
}
Widget _getOverviewScreen() {
return _getBody();
}
}
As you can see, there are three widgets being returned as part of the sub page view, 2 of them are Text Widgets which show correctly, but the StreamBuilder, which is populated correctly with another Text Widget seems to give me the red screen of death. Any ideas?
Fixed the problem, it was related to the StreamBuilder being wrapped in a Flexible rather than a column. I then added column to have a mainAxisSize of max... Seemed to work.
For custom ListView/PageView
In my case, I wanted to clear the list of my listview. In a custom ListView/PageView, the findChildIndexCallback will find the element's index after i.e. a reordering operation, but also when you clear the list.
yourList.indexWhere()unfortunately returns -1 when it couldn't find an element. So, Make sure to return null in that case, to tell the callback that the child doesn't exist anymore.
...
findChildIndexCallback: (Key key) {
final ValueKey<String> valueKey = key as ValueKey<String>;
final data = valueKey.value;
final index = images.indexWhere((element) => element.id == data);
//important here:
if (index > 0 ) return index;
else return null;
},