Obx - Rebuilding a list on variable change - flutter

I am trying to make a stories progress bar.
I have a PageView that shows a new video per page, and a progress bar on top that should show the progress of each PageView when I scroll to it.
The problem is the stories bar only updates the very first progress bar. I need them all to keep listening to the updates.
Video progress code:
RxDouble getVideoProgress(int storyBarIndex) {
if (storyBarIndex < pageIndex.value) {
return 1.0.obs;
} else if (storyBarIndex == pageIndex.value) {
final properVideoController = Get.put(
ProperVideoController(video: pageVideo!.value),
tag: ValueKey(pageVideo!.value.uid + '_' + user.authUid + 'tt')
.toString());
return properVideoController.videoProgress;
} else {
return 0.0.obs;
}
}
ListView code:
Widget build(BuildContext context) {
return GetBuilder<ProfileViewController>(
init: ProfileViewController(),
builder: (controller) => Container(
width: Get.width,
child: LayoutBuilder(
builder: ((context, constraints) => Column(
children: [
Container(
height: 4,
child: ListView.builder(
itemCount: itemCount,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return _ProgressBarStep(
width: constraints.maxWidth / 6.2,
index: index,
);
}),
),
],
))),
),
);
}
Single Story bar widget
Widget build(BuildContext context) {
return GetBuilder<ProfileViewController>(
init: ProfileViewController(),
builder: (controller) {
return Padding(
padding: const EdgeInsets.only(right: 0.0),
child: Obx(
() {
return LinearPercentIndicator(
barRadius: Radius.circular(20),
animation: true,
width: width,
lineHeight: 9.0,
percent: controller.getVideoProgress(index).value,
// ignore: deprecated_member_use
linearStrokeCap: LinearStrokeCap.roundAll,
backgroundColor: TabooColors.darkBlueGrey,
progressColor: TabooColors.red,
);
},
));
});
}

You may remove the GetBuilder widget and just use the Obx it would work. Need to add Get.put(ProfileViewController()); above the Widget build() method. I hope this might help you. Please go through the docs: https://pub.dev/packages/get
When we use GetBuilder then we also need to call the update() method in GetxController class to update the UI

Related

Last element is hidden behind BottomAppBar with ReorderableListView

I'm using a ReorderableListView builder in combination with a Bottom App Bar with a FAB.
I set extendBody because I want the FAB notch to be transparent when scrolling up/down the list. However with a ReorderableListView I cannot scroll down to the last list item because the list item is hidden behind the Bottom App Bar.
Here's a demonstration:
This is not the case with a regular ListView as you can see in the code example below and on this picture right here. This would be my intended behaviour because the last list item is fully visible:
Important: It's not a solution to just set extendBody to false because there would be no transparent notch when scrolling. The scrolling behaviour itself is correct for ReorderableListView and ListView and looks like this when scrolling:
Is there a way to access the last list element with a Reorderable List View similar than with a List View?
Here's a code example with both versions:
import 'package:flutter/material.dart';
void main() {
runApp(SO());
}
class SO extends StatelessWidget {
List<String> myList = ["a", "b", "c", "d", "e", "f", "g", "h"];
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Builder(
builder: (context) => Scaffold(
extendBody: true,
appBar: AppBar(),
body:
// CANNOT scroll up to the last element
ReorderableListView.builder(
itemCount: myList.length,
itemBuilder: (BuildContext context, int index) {
return Container(
key: ValueKey(myList[index]),
height: 150,
color: Colors.green,
child: Center(child: Text('Entry ${myList[index]}')),
);
},
onReorder: (oldIndex, newIndex){
if (newIndex > oldIndex) newIndex --;
final item = myList.removeAt(oldIndex);
myList.insert(newIndex, item);
},
),
/*
// CAN scroll up to the last element
ListView.builder(itemCount: myList.length,
itemBuilder: (BuildContext context, int index) {
return Container(
key: ValueKey(myList[index]),
height: 150,
color: Colors.green,
child: Center(child: Text('Entry ${myList[index]}')),
);
},
),
*/
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
floatingActionButton: FloatingActionButton(
onPressed: () {},
),
bottomNavigationBar: BottomAppBar(
shape: const CircularNotchedRectangle(),
notchMargin: 18,
color: Colors.blue,
child: Container(
height: 60,
),
),
)
)
);
}
}
You can add padding like this:
ReorderableListView.builder(
padding: EdgeInsets.only(bottom: kBottomNavigationBarHeight + 16),//<--- add this
itemCount: myList.length,
itemBuilder: (BuildContext context, int index) {
return Container(
key: ValueKey(myList[index]),
height: 150,
color: Colors.green,
child: Center(child: Text('Entry ${myList[index]}')),
);
},
onReorder: (oldIndex, newIndex) {
if (newIndex > oldIndex) newIndex--;
final item = myList.removeAt(oldIndex);
myList.insert(newIndex, item);
},
),
Add a bottom edge inset. An inset would add padding inside the control without losing your transparent notch behaviour.
This can be done with something like -
ReorderableListView.builder(
padding(EdgeInsets.only(bottom: 60.0)),
...
)
This is based on the documentation here though I think it is a weird parameter naming as padding suggests offet but documentation says this as inset. Hence I hope this works for you.
Add dynamic padding to the ReorderableListview as below
ReorderableListView.builder(
padding: EdgeInsets.only(bottom: kBottomNavigationBarHeight + 15.0), // add this
itemCount: myList.length,
itemBuilder: (BuildContext context, int index) {
return Container(
key: ValueKey(myList[index]),
height: 150,
color: Colors.green,
child: Center(child: Text('Entry ${myList[index]}')),
);
},
onReorder: (oldIndex, newIndex){
if (newIndex > oldIndex) newIndex --;
final item = myList.removeAt(oldIndex);
myList.insert(newIndex, item);
},
),
kBottomNavigationBarHeight
get the height of bottom navigation bar and add as padding..

Flutter Retrigger Staggered Animation on ListView

I am using flutter_staggered_animations in my app for my listView. It is working quite nice when starting the app.
Problem:
I want the animation to be triggered if I change the child-widget of the listView or even just the itemCount. So what I need is a rebuild of the staggeredList.
But how can I do that? I tried simply changing the child or itemCount with setState. But that is triggering an animation...
Couldn't find anything on this. Let me know if you need more info!
I use pretty much the exact code from the example:
#override
Widget build(BuildContext context) {
return Scaffold(
body: AnimationLimiter(
child: ListView.builder(
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 375),
child: SlideAnimation(
verticalOffset: 50.0,
child: FadeInAnimation(
child: YourListChild(),
),
),
);
},
),
),
);
}
You can provide a new key on AnimationLimiter, and it will recreate the AnimationLimiter,
AnimationLimiter(
key: ValueKey("$itemCount"),
child: ListView.builder(
class STA extends StatefulWidget {
const STA({super.key});
#override
State<STA> createState() => _STAState();
}
class _STAState extends State<STA> {
int itemCount = 5;
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
itemCount++;
setState(() {});
},
),
body: AnimationLimiter(
key: ValueKey("$itemCount"),
child: ListView.builder(
itemCount: itemCount,
itemBuilder: (BuildContext context, int index) {
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 375),
child: SlideAnimation(
verticalOffset: 50.0,
child: FadeInAnimation(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
height: 50,
color: index.isEven ? Colors.amber : Colors.purple,
),
),
),
),
);
},
),
),
);
}
}
Because the widget AnimationLimiter of this package is prevent you reanimate when setState
Problem is if you remove that widget, ListView will be reanimate when you scroll back to old position, that will be ugly
If you still want animation like you want, I recommend you to write your custom AnimatedList, I have do one which AnimatedList like this :
AnimatedList(
key: _listKey,
itemBuilder: (_, index, animation) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(-0.5, 0.0),
end: const Offset(0.0, 0.0),
).animate(animation),
child: YourItemWidget(),
);
},
)
then you can use insert function to add single item animate to list :
_listKey.currentState?.insertItem

Flutter Card Swipe Animation with forward and reverse methode without a loop

can i implement my own card swip animation ?
what i want is something similar to this package here. but the slides should shown from the top like so: (and without a loop)
my code:
final controller = SwiperController();
List<Widget> _pages = [
Container(color: Colors.blue),
Container(color: Colors.black),
];
#override
Widget build(BuildContext context) {
return Scaffold(
body: new Swiper(
loop: false,
controller: SwiperController(),
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 40.0),
child: _pages[index],
);
},
itemCount: _pages.length,
itemWidth: 400.0,
itemHeight: 700.0,
layout: SwiperLayout.TINDER,
),
);
}
}
can you suggest me a way to reach that and thanks.
My editor warns me about this:
The library 'package:flutter_swiper/flutter_swiper.dart' is legacy, and shouldn't be imported into a null safe library.
So I guess you should search for a up-to-date, null-safe package which does the same.
And for the vertical layout that you want to achieve... Can't you just rotate the whole swiper widget by 90 degrees?
Like so:
#override
Widget build(BuildContext context) {
return Scaffold(
body: RotatedBox(
quarterTurns: 1,
child: Swiper(
loop: false,
controller: SwiperController(),
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 40.0),
child: _pages[index],
);
},
itemCount: _pages.length,
itemWidth: 400.0,
itemHeight: 700.0,
layout: SwiperLayout.TINDER,
),
),
);
}

How to display progress indicator in flutter when a certain condition is true?

I have widget which return CircularProgressIndicator()
it shows on the circular mark upper left of screen.
However I want to put this as overlay and put at the center of screen.
I am checking widget list but I cant find what Widget should I use as overlay.
On which layer should I put this on??
For now my code is like this ,when loading it shows CircularProgressIndicator instead of ListView
However I want to put CircularProgressIndicator() on ListView
Widget build(BuildContext context) {
if(loading) {
return CircularProgressIndicator();
}
return ListView.builder(
controller: _controller,
itemCount: articles.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(articles[index]),
);
},
);
}
Thank you very much for answers.
I solve with stack Widget like this below.
At first I try to use overlay, but I bumped into some errors.
So, I use simply stack.
#override
Widget build(BuildContext context) {
Widget tempWidget = new CircularProgressIndicator();
if(loading) {
tempWidget = new CircularProgressIndicator();
}
else {
tempWidget = new Center();//EmptyWidget
}
return Stack(
children: <Widget>[
ListView.builder(
controller: _controller,
itemCount: articles.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(articles[index].title),
onTap: () => onTapped(context,articles[index].url),
);
},
),
Center(
child: tempWidget
),
]
);
}
Stack(
children: <Widget>[
ListView.builder(
itemBuilder: (BuildContext context, int index) {
return Container(
height: 100,
color: Colors.red,
);
},
itemCount: 10,
),
Center(
child: CircularProgressIndicator(),
),
],
)
Stack(
children: <Widget>[
ListView.builder(
itemBuilder: (BuildContext context, int index) {
return Container(
height: 100,
color: Colors.red,
);
},
itemCount: 10,
),
isLoading? Container(child: Center(
child: CircularProgressIndicator(),
)): Container(), //if isLoading flag is true it'll display the progress indicator
],
)
or you can use futureBuilder or streamBuilder when loading data from somewhere and you want to change the ui depending on the state
To overlay or position items on top of each other you would usually use a Stack widget or Overlay as described here. For your usecase I would recommend checking out the modal progress hud package.

Dismissible and FutureBuilder do not work together

It is kind of a complex problem but I'll do my best to explain it.
My project utilizes a sqflite database. This particular page returns a list of Dismissible widgets according to the data in the database. This is how I read the data:
class TaskListState extends State<TaskList> {
DBProvider dbProvider = new DBProvider();
Future<List<Task>> allTasks;
#override
void initState() {
allTasks = dbProvider.getAllTasks();
super.initState();
}
void update(){
setState(() {
allTasks = dbProvider.getAllTasks();
});
}
//build
}
The TaskList widget returns a page with a FutureBuilder, which builds a ListView.builder with the data from the database. The ListView builds Dismissible widgets. Dismissing the Dismissible widgets updates a row in the database and reads the data again to update the list.
build method for TaskListState
#override
Widget build(BuildContext context) {
return ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: <Widget>[
//other widgets such as a title for the list
),
FutureBuilder(
future: allTasks,
builder: (context, snapshot){
if(snapshot.hasError){
return Text("Data has error.");
} else if (!snapshot.hasData){
return Center(
child: CircularProgressIndicator(),
);
} else {
return pendingList(Task.filterByDone(false, Task.filterByDate(Datetime.now, snapshot.data))); //filters the data to match current day
}
},
),
//other widgets
],
);
}
The pendingList
Widget pendingList(List<Task> tasks){
//some code to return a Text widget if "tasks" is empty
return ListView.separated(
separatorBuilder: (context, index){
return Divider(height: 2.0);
},
itemCount: tasks.length,
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemBuilder: (context, index){
return Dismissible(
//dismissible backgrounds, other non-required parameters
key: Key(UniqueKey().toString()),
onDismissed: (direction) async {
Task toRemove = tasks[index]; //save the dismissed task for the upcoming operations
int removeIndex = tasks.indexWhere((task) => task.id == toRemove.id);
tasks.removeAt(removeIndex); //remove the dismissed task
if(direction == DismissDirection.endToStart) {
rateTask(toRemove).then((value) => update()); //rateTask is a function that updates the task, it is removed from the list
}
if(direction == DismissDirection.startToEnd) {
dbProvider.update(/*code to update selected task*/).then((value) => update());
}
},
child: ListTile(
//ListTile details
),
);
},
);
}
Here is the problem (might be a wrong interpretation I'm still kind of new):
Dismissing a widget essentially removes it from the list. After the user dismisses a task, the task is "visually" removed from the list and the update() method is called, which calls setState(). Calling setState() causes the FutureBuilder to build again, but the dbProvider.getAllTasks() call is not completed by the time the FutureBuilder builds again. Therefore, the FutureBuilder passes the old snapshot, which causes the ListView to build again with the Task that just was dismissed. This causes the dismissed ListTile to appear momentarily after being dismissed, which looks creepy and wrong.
I have no idea how to fix this. Any help would be appreciated.
I was having the exact same issue, I was using sqflite which works with Futures so I ended up using the FutureBuilder alongside Dismissible for my ListView. The dismissed list item would remove then reappear for a frame then disappear again. I came across this question :
https://groups.google.com/forum/#!topic/flutter-dev/pC48MMVKJGc
which suggests removing the list item from the snapshot data itself:
return FutureBuilder<List<FolderModel>>(
future: Provider.of<FoldersProvider>(context).getFolders(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, i) {
final folder = snapshot.data[i];
return Dismissible(
onDismissed: (direction) {
snapshot.data.removeAt(i); // to avoid weird future builder issue with dismissible
Provider.of<FoldersProvider>(context, listen: false).deleteFolder(folder.id);
},
background: Card(
margin: EdgeInsets.symmetric(vertical: 8),
elevation: 1,
child: Container(
alignment: AlignmentDirectional.centerStart,
color: Theme.of(context).accentColor,
child: Padding(
padding: EdgeInsets.fromLTRB(15.0, 0.0, 0.0, 0.0),
child: Icon(
Icons.delete,
color: Colors.white,
),
),
),
),
key: UniqueKey(),
direction: DismissDirection.startToEnd,
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
margin: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
elevation: 1,
child: ListTile(
title: Text(folder.folderName),
leading: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons.folder,
color: Theme.of(context).accentColor,
),
],
),
subtitle: folder.numberOfLists != 1
? Text('${folder.numberOfLists} items')
: Text('${folder.numberOfLists} item'),
onTap: () {},
),
),
);
},
);
},
);
and low and behold, it worked! Minimal changes to the code :)
Found a workaround for this by not using FutureBuilder and calling setState after the query is completed.
Instead of Future<List<Task>>, the state now contains a List<Task> which is declared as null.
class TaskListState extends State<TaskList> {
DBProvider dbProvider = new DBProvider();
DateTime now = DateTime.now();
List<Task> todayTasks;
//build
}
The update() function was changed as follows
void update() async {
Future<List<Task>> futureTasks = dbProvider.getByDate(now); //first make the query
futureTasks.then((data){
List<Task> tasks = new List<Task>();
for(int i = 0; i < data.length; i++) {
print(data[i].name);
tasks.add(data[i]);
}
setState(() {
todayTasks = tasks; //then setState and rebuild the widget
});
});
}
This way I the widget does not rebuild before the future is completed, which was the problem I had.
I removed the FutureBuilder completely, the Listview.builder just builds accordingly to the List stored in state.
#override
Widget build(BuildContext context) {
if(todayTasks == null) {
update();
return Center(
child: CircularProgressIndicator(),
);
} //make a query if the list has not yet been initialized
return ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: <Widget>[
//other widgets
pendingList(Task.filterByDone(false, todayTasks)),
],
);
}
This approach completely solved my problem, and I think its better than using a FutureBuilder in case the Future must be completed before the widget builds again.