Flutter - getx controller not updated when data changed - flutter

I am developing an app that has a bottomnavitaionbar with five pages. I use getx. In first page, i am listing data. My problem is that, when i changed data(first page in bottomnavigationbar) manually from database and thn i pass over pages, came back to first page i could not see changes.
Controller;
class ExploreController extends GetxController {
var isLoading = true.obs;
var articleList = List<ExploreModel>().obs;
#override
void onInit() {
fetchArticles();
super.onInit();
}
void fetchArticles() async {
try {
isLoading(true);
var articles = await ApiService.fetchArticles();
if (articles != null) {
//articleList.clear();
articleList.assignAll(articles);
}
} finally {
isLoading(false);
}
update();
}
}
and my UI;
body: SafeArea(
child: Column(
children: <Widget>[
Header(),
Expanded(
child: GetX<ExploreController>(builder: (exploreController) {
if (exploreController.isLoading.value) {
return Center(
child: SpinKitChasingDots(
color: Colors.deepPurple[600], size: 40),
);
}
return ListView.separated(
padding: EdgeInsets.all(12),
itemCount: exploreController.articleList.length,
separatorBuilder: (BuildContext context, int index) {

thanks to #Baker for the right answer. However, if you have a list and in viewModel and want to update that list, just use the list.refresh() when the list updated
RxList<Models> myList = <Models>[].obs;
when add or insert data act like this:
myList.add(newItem);
myList.refresh();

GetX doesn't know / can't see when database data has changed / been updated.
You need to tell GetX to rebuild when appropriate.
If you use GetX observables with GetX or Obx widgets, then you just assign a new value to your observable field. Rebuilds will happen when the obs value changes.
If you use GetX with GetBuilder<MyController>, then you need to call update() method inside MyController, to rebuild GetBuilder<MyController> widgets.
The solution below uses a GetX Controller (i.e. TabX) to:
hold application state:
list of all tabs (tabPages)
which Tab is active (selectedIndex)
expose a method to change the active/visible tab (onItemTapped())
OnItemTapped()
This method is inside TabX, the GetXController.
When called, it will:
set which tab is visible
save the viewed tab to the database (FakeDB)
rebuild any GetBuilder widgets using update()
void onItemTapped(int index) {
selectedIndex = index;
db.insertViewedPage(index); // simulate database update while tabs change
update(); // ← rebuilds any GetBuilder<TabX> widget
}
Complete Example
Copy/paste this entire code into a dart page in your app to see a working BottomNavigationBar page.
This tabbed / BottomNavigationBar example is taken from
https://api.flutter.dev/flutter/material/BottomNavigationBar-class.html
but edited to use GetX.
import 'package:flutter/material.dart';
import 'package:get/get.dart';
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',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyTabHomePage(),
);
}
}
class FakeDB {
List<int> viewedPages = [0];
void insertViewedPage(int page) {
viewedPages.add(page);
}
}
/// BottomNavigationBar page converted to GetX. Original StatefulWidget version:
/// https://api.flutter.dev/flutter/material/BottomNavigationBar-class.html
class TabX extends GetxController {
TabX({this.db});
final FakeDB db;
int selectedIndex = 0;
static const TextStyle optionStyle =
TextStyle(fontSize: 30, fontWeight: FontWeight.bold);
List<Widget> tabPages;
#override
void onInit() {
super.onInit();
tabPages = <Widget>[
ListViewTab(db),
Text(
'Index 1: Business',
style: optionStyle,
),
Text(
'Index 2: School',
style: optionStyle,
),
];
}
/// INTERESTING PART HERE ↓ ************************************
void onItemTapped(int index) {
selectedIndex = index;
db.insertViewedPage(index); // simulate database update while tabs change
update(); // ← rebuilds any GetBuilder<TabX> widget
// ↑ update() is like setState() to anything inside a GetBuilder using *this*
// controller, i.e. GetBuilder<TabX>
// Other GetX controllers are not affected. e.g. GetBuilder<BlahX>, not affected
// by this update()
// Use async/await above if data writes are slow & must complete before updating widget.
// This example does not.
}
}
/// REBUILT when Tab Page changes, rebuilt by GetBuilder in MyTabHomePage
class ListViewTab extends StatelessWidget {
final FakeDB db;
ListViewTab(this.db);
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: db.viewedPages.length,
itemBuilder: (context, index) =>
ListTile(
title: Text('Page Viewed: ${db.viewedPages[index]}'),
),
);
}
}
class MyTabHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
Get.put(TabX(db: FakeDB()));
return Scaffold(
appBar: AppBar(
title: const Text('BottomNavigationBar Sample'),
),
body: Center(
/// ↓ Tab Page currently visible - rebuilt by GetBuilder when
/// ↓ TabX.onItemTapped() called
child: GetBuilder<TabX>(
builder: (tx) => tx.tabPages.elementAt(tx.selectedIndex)
),
),
/// ↓ BottomNavBar's highlighted/active item, rebuilt by GetBuilder when
/// ↓ TabX.onItemTapped() called
bottomNavigationBar: GetBuilder<TabX>(
builder: (tx) => BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.business),
label: 'Business',
),
BottomNavigationBarItem(
icon: Icon(Icons.school),
label: 'School',
),
],
currentIndex: tx.selectedIndex,
selectedItemColor: Colors.amber[800],
onTap: tx.onItemTapped,
),
),
);
}
}

You don't need GetBuilder here, as its not meant for observable variables. Nor do you need to call update() in the fetchArticles function as that's only for use with GetBuilder and non observable variables.
So you had 2 widgets meant to update UI (GetBuilder and Obx) both following the same controller and all you need is just the OBX. So Rahuls answer works, or you can leave the Obx in place, get rid of of the GetBuilder and declare and initialize a controller in the beginning of your build method.
final exploreController = Get.put(ExploreController());
Then use that initialized controller in your OBX widget as the child of your Expanded.
Obx(() => exploreController.isLoading.value
? Center(
child:
SpinKitChasingDots(color: Colors.deepPurple[600], size: 40),
)
: ListView.separated(
padding: EdgeInsets.all(12),
itemCount: exploreController.articleList.length,
separatorBuilder: (BuildContext context, int index) {},
),
)

GetX< ExploreController >(builder: (controller) {
if (controller.isLoading.value) {
return Center(
child: SpinKitChasingDots(
color: Colors.deepPurple[600], size: 40),);
}
return ListView.separated(
padding: EdgeInsets.all(12),
itemCount: controller.articleList.length,
separatorBuilder: (BuildContext context, int index) {});
});

If you change the value in the database 'manually', you need a STREAM to listen to the change on the database.
You can't do:
var articles = await ApiService.fetchArticles();
You need to do something like this:
var articles = await ApiService.listenToArticlesSnapshot();
The way you explained is like if you need the data to refresh after navigating to another page and clicking on a button, then navigating to first page (GetBuilder) OR automatically adds data from the within the first page (Obx). But your case is simple, just retrieve the articles SNAPSHOT, then in the controller onInit, subscribe to the snapshot with the bindStream method, and eventually use the function ever() to react to any change in the observable articleList.
Something like this:

create
final exploreController = Get.put(ExploreController());
Add
init: ExploreController();
body: SafeArea(
child: Column(
children: <Widget>[
Header(),
Expanded(
child: GetX<ExploreController>(builder: (exploreController) {
*** here ***
init: ExploreController();
if (exploreController.isLoading.value) {
return Center(
child: SpinKitChasingDots(
color: Colors.deepPurple[600], size: 40),
);
}
return ListView.separated(
padding: EdgeInsets.all(12),
itemCount: exploreController.articleList.length,
separatorBuilder: (BuildContext context, int index) {

using GetxBuilder approch on ui side and where you want update simple called built in function update();

The simplest way I could.
In the controller create an obs (var indexClick = 1.obs;)
On each Tile test the selected==index...;
On the click of each item change the indexClick sequentially
return Obx(() {
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
ListTile(
leading: const Icon(Icons.dns),
title: const Text('Menu1'),
selected: controller.indexClick.value==1?true:false,
onTap: () {
controller.indexClick.value=1;
Navigator.pop(context);
},
),
ListTile(
leading: const Icon(Icons.search),
title: const Text('Menu2'),
selected: controller.indexClick.value==2?true:false,
onTap: () {
controller.indexClick.value=2;
Navigator.pop(context);
},
),

Related

What can I do to make my ListView stop incrementing the data every time I open it?

My first Flutter project, which is a tricycle booking system, has just begun. Using the ListView widget, I wanted to display all of the active passengers that are saved in my Firebase Database. However, when I attempted to display it and place it in a List, all functions are working fine at first click. When you click the button to view the ListView a second time, all of the saved data are replicated. The list continues after my third click and grows by three. The image below illustrates what takes place when I repeatedly click on the ListView.
These are the blocks of code that are utilized for this functionality:
CODE for Functionality
retrieveOnlinePassengersInformation(List onlineNearestPassengersList) async
{
dList.clear();
DatabaseReference ref = FirebaseDatabase.instance.ref().child("passengers");
for(int i = 0; i<onlineNearestPassengersList.length; i++)
{
await ref.child(onlineNearestPassengersList[i].passengerId.toString())
.once()
.then((dataSnapshot)
{
var passengerKeyInfo = dataSnapshot.snapshot.value;
dList.add(passengerKeyInfo);
print("passengerKey Info: " + dList.toString());
});
}
}
CODE for the UI
body: ListView.builder(
itemCount: dList.length,
itemBuilder: (BuildContext context, int index)
{
return GestureDetector(
onTap: ()
{
setState(() {
chosenPassengerId = dList[index]["id"].toString();
});
Navigator.pop(context, "passengerChoosed");
},
child: Card(
color: Colors.grey,
elevation: 3,
shadowColor: Colors.green,
margin: EdgeInsets.all(8.0),
child: ListTile(
leading: Padding(
padding: const EdgeInsets.only(top: 2.0),
child: Icon(
Icons.account_circle_outlined,
size: 26.sp,
color: Color(0xFF777777),
),
),
title: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Row(
children: [
Text(
dList[index]["first_name"] + " " + dList[index]["last_name"],
style: TextStyle(
fontFamily: "Montserrat",
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
Icon(
Icons.verified_rounded,
color: Color(0xFF0CBC8B),
size: 22.sp,
),
],
),
],
),
),
),
);
},
),
Expected Result:
Actual Result AFTER CLICKING MANY TIMES:
Made a demo for you how to call function once on load
class CustomWidgetName extends StatefulWidget {
const CustomWidgetName({Key? key}) : super(key: key);
#override
State<CustomWidgetName> createState() => _CustomWidgetNameState();
}
class _CustomWidgetNameState extends State<CustomWidgetName> {
List? dList = [];
void myDataFunction() async {
// do your data fetch and add to dList
final newList = [];
setState(() {
dList = newList;
});
}
#override
void initState() {
super.initState();
myDataFunction(); // Call your async function here
}
#override
Widget build(BuildContext context) {
return Scaffold();
}
}
Try this solution.
Update SelectNearestActiveDriversScreen() like this:
class SelectNearestActiveDriversScreen extends StatefulWidget
{
DatabaseReference? referenceRideRequest;
final List list;
SelectNearestActiveDriversScreen({this.referenceRideRequest, required this.list});
#override
State<SelectNearestActiveDriversScreen> createState() => _SelectNearestActiveDriversScreenState();
}
In homepage.dart, declare List dList = [];, then change line 378 like this:
Navigator.push(context, MaterialPageRoute(builder: (c)=> SelectNearestActiveDriversScreen(list: dList)));
In SelectNearestActiveDriversScreen(), replace all dList with widget.list.
Finally, if you are using variables in a specific file declare them in that file(not in another file) or pass them in the constructor of the class / file / widget /screen you are calling.
If you would rather use global variables and state managers go for packages like GetX.

Need help chasing down: Exception while building using Provider in Flutter

I'm trying to learn Flutter and become more acquainted with passing data around. So i have this very simple app here that is a sort of complicated version of this: Provider version flutter starter demo
Like I said I'm trying to get acquainted, and I'm a relatively green dev. I'm creating this demo to learn StateManagement as well as Persistence.
My goal with this post, is to get help to fix this issue and also know what I'm doing wrong.
So far I have tried moving a few things around and some typical searches but can't seem to figure out specifically what I'm doing wrong here compared with others who are getting the same error.
The app works fine, exactly as expected, no crash and as far as my green grass eyes can tell my code is structured exactly like the Flutter example (with respect to the Provider StateManagement). However I'm getting this error in the console:
======== Exception caught by foundation library ====================================================
The following assertion was thrown while dispatching notifications for Keeper:
setState() or markNeedsBuild() called during build.
This _InheritedProviderScope<Keeper> widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.
The widget on which setState() or markNeedsBuild() was called was: _InheritedProviderScope<Keeper>
value: Instance of 'Keeper'
listening to value
The widget which was currently being built when the offending call was made was: Consumer<Keeper>
dirty
dependencies: [_InheritedProviderScope<Keeper>]
Page 1
class ScreenOne extends StatelessWidget {
static const String id = 'screen_one';
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
'Page One',
),
),
backgroundColor: Colors.grey.shade200,
body: Container(
child: SafeArea(
child: Padding(
padding: EdgeInsets.all(20.0),
child: Center(
child: Column(
children: [
CustomTextBoxes(title: 'Screen One'),
Consumer<Keeper>(
builder: (_, keeper, child) => Text(
'${keeper.pageOneValue}',
style: TextStyle(color: Colors.grey, fontSize: 20.0),
),
),
CustomTextBoxes(title: 'Screen Two'),
Consumer<Keeper>(
builder: (_, keeper, child) => Text(
'${keeper.pageTwoValue}',
style: TextStyle(color: Colors.grey, fontSize: 20.0),
),
),
CustomTextBoxes(title: 'Total'),
Consumer<Keeper>(
builder: (_, keeper, child) => Text(
'${keeper.addCounters()}',
style: TextStyle(color: Colors.grey, fontSize: 20.0),
),
),
SizedBox(
height: 20.0,
),
CustomButton(
text: 'Screen 2',
function: () {
Navigator.pushNamed(context, ScreenTwo.id);
},
),
],
),
),
),
),
),
floatingActionButton: CustomFloatingButton(
function: () {
var counter = context.read<Keeper>();
counter.incrementCounterOne();
},
),
);
}
}
"Keeper"
class Keeper with ChangeNotifier {
int pageOneValue = 0;
int pageTwoValue = 0;
int totalValue = 0;
void incrementCounterOne() {
pageOneValue += 1;
notifyListeners();
}
void incrementCounterTwo() {
pageTwoValue += 1;
notifyListeners();
}
int addCounters() {
totalValue = pageOneValue + pageTwoValue;
notifyListeners();
return totalValue;
}
}
Main
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => Keeper(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(primarySwatch: Colors.blue),
initialRoute: ScreenOne.id,
routes: {
ScreenOne.id: (context) => ScreenOne(),
ScreenTwo.id: (context) => ScreenTwo()
},
);
}
}
Your problem is in calling addCounters() inside your build method. addCounters() calls notifyListeners() that triggers a setState().
This cannot be perform within your build function. It can only be performed later at the request of a User action. For example, inside the onPressed of a button as you do for the incrementCounterOne().
Instead of computing and storing the total value of your two counters, you could use a getter:
Keeper:
class Keeper with ChangeNotifier {
int pageOneValue = 0;
int pageTwoValue = 0;
int get totalValue => pageOneValue + pageTwoValue;
void incrementCounterOne() {
pageOneValue += 1;
notifyListeners();
}
void incrementCounterTwo() {
pageTwoValue += 1;
notifyListeners();
}
}
ScreenOne:
Consumer<Keeper>(
builder: (_, keeper, child) => Text(
'${keeper.totalValue}', // addCounters()}',
style: TextStyle(color: Colors.grey, fontSize: 20.0),
),
),

Is there a way to update BottomNavigationBarItems?

I am new flutter and been trying to work with the BottomNavigationBar. Thing is, i made the bar as i required but i need it to update its items when a Switch is set to true but can't manage to find a work around.
I have two List<BottomNavigationBarItems> that have the two different navBar items which i assign to a third List that contain the active one depending on the switch state. This variable is the one setting the items in my navbarItem but a setState() doesn't seems to re build the navBar.
Is there a way to update the items or do i have to make my own kind of navBar with other widgets ?
Non Ready Items
Ready Items
List<BottomNavigationBarItem> nonReadyBottomItems = [
//some items
];
List<BottomNavigationBarItem> readyBottomItems = [
some other items
];
List<BottomNavigationBarItem> = nonReadyBottomItems;
Scaffold(
body: Center(
child: Switch(
value: switchConnect,
onChanged: (bool boolean) {
setState(() {
switchConnect = boolean;
});
}),
),
bottomNavigationBar: BottomNavigationBar(
onTap: (int i) {
setState(() {
pageIndex = i;
if (switchConnect) {
activeItems = readyBottomItems;
} else if (!switchConnect) {
activeItems = nonReadyBottomItems;
}
});
},
currentIndex: pageIndex,
type: BottomNavigationBarType.fixed,
items: activeItems,
),
);
Yes, You create state full Bottom Navigation Bar for change the state of widgets.
Open bottom sheet
InkWell(
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) {
return ModalBottomSheet(
);
});
})
Stateful bottom sheet
class ModalBottomSheet extends StatefulWidget {
#override
_ModalBottomSheetState createState() => _ModalBottomSheetState();
}
class _ModalBottomSheetState extends State<ModalBottomSheet>
{
#override
Widget build(BuildContext context) {
// TODO: implement build
return Wrap(
children: <Widget>[
Container(
margin:
EdgeInsets.only(left: 10.0, right: 10.0, top: 15.0, bottom: 15.0),
child: Column(
Widgets(),
)
)
],
);
}
}
I actually found out why the setState() wasn't working. Seems like the there was some problem with the assignment of the activeItems variable so it wasn't changing the bar since there was nothing new.
So setState actually work on a BottomNavBar !

SetState in Flutter

I have dynamically created a list view by getting data from an API and putting it into a list of objects, however, when I open the screen, which is opened by pressing a button on another screen, the only way to get the ListView to show on screen is to do a hot refresh. I've tried setting the state in the init_state function but it doesn't seem to be working.
Thanks for any help :)
class _PlanetListState extends State<PlanetList> {
List<Planet> planets = [];
String urlWeb = 'https://swapi.dev/api/planets/';
Future getData(String url) async {
var response = await http.get(url);
if (response.statusCode == 200) {
var data = json.decode(response.body);
Planet obj = Planet(
name: data['name'],
rotationPeriod: data['rotation_period'],
orbitalPeriod: data['orbital_period'],
diameter: data['diameter'],
climate: data['climate'],
gravity: data['gravity'],
terrain: data['terrain'],
population: data['population']
);
planets.add(obj);
print(planets);
}
}
#override
void initState() {
for (var i=0;i<62;i++){
getData('$urlWeb$i/');
}
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Planets"),
backgroundColor: Colors.amber[800],
),
body: Container(
color: Colors.black,
child: ListView.builder(
itemCount: planets.length,
itemBuilder: (BuildContext context, int index){
return Card(
color: Colors.amber[600],
margin:EdgeInsets.all(8),
child: ListTile(
leading: CircleAvatar(
backgroundColor: getColor(planets[index].climate),
backgroundImage: AssetImage('assets/planet.png'),
),
contentPadding: EdgeInsets.all(5),
title: Text('$index: ${planets[index].name}'),
subtitle:Text(planets[index].climate),
),
);
},
),
),
);
}
}
What's happening is that your screen is loading before your data are added to the list, this is because your getData function is async. So when your screen first loads the list is empty, and as you may know in order to show changes on the screen you must do a setState(), this is why when you hot refresh the changes appear.
Now you have multiple options here, first you can take a look at the FutureBuilder, this is a widget that would build twice, once before your Future is resolved and once afterwards, meaning it "auto refreshes". In this case your list of data would become the Future type and your ListView would be wrapped with a FutureBuilder so it's built after the data are ready.
Another option would be to do a setState() every time you add an item to the List of objects you have.

Check / Uncheck ListTiles in ListView.builder() - Flutter

I have a ListView.builder(); in showModalBottomSheet();
Need to select / deselect multiple items on tap everything is well but need to close the modal and show it again to apply changes, another thing is the ListTiles sometimes duplicated more than once, function emptyList doesn't work well.
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:async';
import 'package:flutter/material.dart';
import 'book_details.dart' show BookDetails;
class Explore extends StatefulWidget {
#override
_ExploreState createState() => _ExploreState();
}
var _books,
_categories,
_arranges,
_currentCategory,
_selected,
_primeColor,
_currentFilter,
_isThereIsFilters,
_booksContainer,
_booksWithFilters,
_isLoading,
_noBooks,
_itemIcon;
final GlobalKey<ScaffoldState> _scaffoldKeyExplore =
new GlobalKey<ScaffoldState>();
List<String> _getCats = new List();
List<String> _getArrs = new List();
void _insertCategories() {
for (int i = 0; i < _categories.length; i++) {
_getCats.add(_categories[i]);
}
_getCats.sort();
}
void _insertArranges() {
for (int i = 0; i < _arranges.length; i++) {
_getArrs.add(_arranges[i]);
}
}
class _ExploreState extends State<Explore> with TickerProviderStateMixin {
onCatChange(String category) {
setState(() {
_currentCategory = category;
});
}
#override
void initState() {
super.initState();
_primeColor = Color.fromRGBO(239, 89, 39, 1.0);
_categories = ["أول", "ثاني", "ثالث", "رابع", "خامس"];
_arranges = ["أول", "ثاني", "ثالث", "رابع", "خامس"];
_currentFilter = _arranges[0];
_selected = [];
_isThereIsFilters = false;
}
void emptyList(List list) {
for (var i = 0; i < list.length; i++) {
list.remove(list[i]);
}
}
_showSheet(String type) {
switch (type) {
case "filters":
showModalBottomSheet(
context: _scaffoldKeyExplore.currentContext,
builder: (BuildContext context) {
return Directionality(
textDirection: TextDirection.rtl,
child: Container(
child: Column(children: <Widget>[
Expanded(
child: new ListView.builder(
itemCount: _getArrs[0] != null ? _getArrs.length : 0,
itemBuilder: (BuildContext context, int i) {
return new RadioListTile(
title: Text(_getArrs[i]),
value: _getArrs[i],
groupValue: _currentFilter,
onChanged: (val) {
setState(() {
_currentFilter = val;
});
});
}),
)
])),
);
});
break;
case "categories":
default:
showModalBottomSheet(
context: _scaffoldKeyExplore.currentContext,
builder: (BuildContext context) {
return Directionality(
textDirection: TextDirection.rtl,
child: Container(
child: Column(children: <Widget>[
Container(
color: _primeColor,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
IconButton(
icon: Icon(Icons.close, color: Colors.white),
onPressed: () {
emptyList(_selected);
//Navigator.pop(context);
//_showSheet(type);
}),
IconButton(
icon:
Icon(Icons.done_all, color: Colors.white),
onPressed: () {
if (_selected.length > 0) {
_getFilteredBooks(_selected);
setState(() {
_isThereIsFilters = true;
});
} else {
setState(() {
_isThereIsFilters = false;
});
}
Navigator.pop(context);
})
]),
),
Expanded(
child: new ListView.builder(
itemCount: _getCats != null ? _getCats.length : 0,
itemBuilder: (BuildContext context, int i) {
final _isSelected = _selected.contains(_getCats[i]);
return new ListTile(
leading: Icon(Icons.category),
trailing: _isSelected ? Icon(Icons.done) : null,
title: Text(_getCats[i]),
onTap: () {
setState(() {
_isSelected
? _selected.remove(_getCats[i])
: _selected.add(_getCats[i]);
});
//Navigator.pop(context);
//_showSheet(type);
});
}),
)
])),
);
});
break;
}
}
#override
Widget build(BuildContext context) {
return new Directionality(
textDirection: TextDirection.rtl,
child: new Scaffold(
key: _scaffoldKeyExplore,
appBar:
AppBar(title: Text("استكشاف"), elevation: 0.0, actions: <Widget>[
IconButton(
icon: Icon(Icons.category, color: _primeColor),
onPressed: () => _showSheet("categories")),
IconButton(
icon: Icon(Icons.filter_list, color: _primeColor),
onPressed: () => _showSheet("filters"))
]),
body: Center(child: Text("Nothing..."));
));
}
}
Thank you
need to close the modal and show it again to apply changes
This happens because the showModalBottomSheet's builder needs to be called again to reflect the changes.
In Flutter, StatefulWidgets should be able to rebuild any time the state changes - which is not the case here, because of the bottom sheet being shown.
Why did I run into this issue (on a meta level)?
Storing the state in StatefulWidgets is useful for saving UI state, but you quickly outgrow this technique if you want to store some "app state" or "data state" that is independent of the screen it's on.
It is finally time to fundamentally rethink your state management and settle on a full-fledged state management pattern that decouples the state from the widgets. Luckily, there are a few to choose from:
Making everything global, like you did above. This is generally not a good idea, as you break the contract of setState (state can be modified without the widgets being notified). Also, you break hot restart and stuff like that.
Using an InheritedWidget, where widgets below a root widget can access the same state.
Using a ScopedModel, which builds on top of that.
Using the infamous BLoC pattern, which also builds on top of the InheritedWidget, but adds some Stream-y stuff to make everything more reactive.
Probably many more.
Here is a great Youtube video about state management from Google I/O, where several patterns are being presented.
Anyways, are bottom sheets the right widget for the task ahead?
According to the Material Design spec, the modal bottom sheet is "an alternative to inline menus or simple dialogs on mobile, providing room for additional items, longer descriptions, and iconography".
More concrete, the showModalBottomSheet function is designed to show a widget that doesn't affect the parent over time, but rather - if at all - at a single point in time. That's why it returns a Future<T>, not a Stream<T>.
Be aware that you are trying to use the bottom sheet in a way that it's not intended to be used.
In your case, I'd recommend just using a new screen.