Flutter FutureProvider Value Not Updating In Builder Method - flutter

The Problem
I am building a basic app in Flutter that gets the user's location and displays nearby places in a swipe-card format similar to Tinder. I managed to implement geolocation however when using FutureProvider/Consumer I'm experiencing a weird bug where the user's relative distance to the place is overwritten with the first distance value in the card deck. Although I am new to flutter and the Provider package, I believe there is a simple fix to this.
Side note: After searching around on Google, I attempted to use FutureProvider.value() to prevent the old value from updating but had no luck.
Thank you in advance for any assistance or direction!
A Quick Demo
Packages Used
card_swipe.dart
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:provider/provider.dart';
import 'package:swipe_stack/swipe_stack.dart';
import '../services/geolocator_service.dart';
import '../models/place.dart';
class CardSwipe extends StatelessWidget {
#override
Widget build(BuildContext context) {
final _currentPosition = Provider.of<Position>(context);
final _placesProvider = Provider.of<Future<List<Place>>>(context);
final _geoService = GeoLocatorService();
return FutureProvider(
create: (context) => _placesProvider,
child: Scaffold(
backgroundColor: Colors.grey[300],
body: (_currentPosition != null)
? Consumer<List<Place>>(
builder: (_, places, __) {
return (places != null)
? Column(
children: [
SizedBox(height: 10.0),
Container(
margin: EdgeInsets.only(top: 120.0),
height: 600,
child: SwipeStack(
children: places.map((place) {
return SwiperItem(builder:
(SwiperPosition position,
double progress) {
return FutureProvider(
create: (context) =>
_geoService.getDistance(
_currentPosition.latitude,
_currentPosition.longitude,
place.geometry.location.lat,
place.geometry.location.lng),
child: Consumer<double>(
builder: (_, distance, __) {
return (distance != null)
? Center(
child: Card(
child: Container(
height: 200,
width: 200,
child: Center(
child: Column(
mainAxisAlignment:
MainAxisAlignment
.center,
children: [
Text(place.name),
Text(
'${(distance / 1609).toStringAsFixed(3)} mi'), // convert meter to mi
],
),
),
),
),
)
: Container();
}),
);
});
}).toList(),
visibleCount: 3,
stackFrom: StackFrom.Top,
translationInterval: 6,
scaleInterval: 0.03,
onEnd: () => debugPrint("onEnd"),
onSwipe: (int index, SwiperPosition position) =>
debugPrint("onSwipe $index $position"),
onRewind:
(int index, SwiperPosition position) =>
debugPrint("onRewind $index $position"),
),
),
],
)
: Center(
child: CircularProgressIndicator(),
);
},
)
: Center(
child: CircularProgressIndicator(),
),
),
);
}
}
geolocator_service.dart
import 'package:geolocator/geolocator.dart';
class GeoLocatorService {
final geolocator = Geolocator();
Future<Position> getLocation() async {
return await geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
locationPermissionLevel: GeolocationPermission.location,
);
}
Future<double> getDistance(
double startLat, double startLng, double endLat, double endLng) async {
return await geolocator.distanceBetween(startLat, startLng, endLat, endLng);
}
}
place.dart
Quick note: Place class does import a custom class called geometry.dart however this is purely for structuring the Place object and I'm certain it doesn't affect the bug. Therefore, it has been omitted.
import './geometry.dart';
class Place {
final String name;
final Geometry geometry;
Place(this.name, this.geometry);
Place.fromJson(Map<dynamic, dynamic> parsedJson)
: name = parsedJson['name'],
geometry = Geometry.fromJson(
parsedJson['geometry'],
);
}

You have to add a key to the SwiperItem with some unique value (like the name of the place) since currently flutter thinks that the widget has stayed the same so the Consumer gets the state of the old topmost widget.
By adding the key you tell flutter that you removed the topmost widget and the new topmost is in fact the second widget

Related

Floor plan in flutter

i'm trying to come up with best way to draw a floor plan in flutter, something like these images, but it would be for regals in one concrete shop, instead of plan of shopping centre with multiple shops.
floor plan 1
floor plan 2
i decided rectangles would be sufficient enough for now and i have multiple ideas on how to execute, but no idea which one is the best. or maybe there is even better one i have not thought of
1. using custom painter
regals have attributes: ax, ay, bx, by, so they go from point a (left bottom) to b (right upper)
code like this
final rect = Rect.fromPoints(
Offset(regal.ax.toDouble(), size.height - regal.ay.toDouble()),
Offset(regal.bx.toDouble(), size.height - regal.by.toDouble()),
);
this is good because it is flexible, there is pretty much unlimited range of options, but using CustomPainter is a bit buggy in my case, alongside with Transform and GestureDetector it bugs out sometimes and instead of clicking on "buttons" you need to track where user clicked, ehm, tapped.
2. using gridView?
i dont have thought this thru as much as first option, but big plus would be using styled buttons as regals, instead of rectangles.
possible problems would be button sizing, if one regal would be times bigger than others.
regal attributes would be order on x axis, order on y axis, x flex (for example 3 as 3 times of base size), y flex
i think i have not thought of the best solution yet.
what would it be?
Here is a quick playground using a Stack of Regals who are just Containers in this quick implementation under 250 lines of code.
Click the FloatActionButton to create random Regal. Then, you can define the position of each Regal and its Size, within the limit of the Floor Plan and Max/min Regal Size.
In this quick implementation, the position of a Regal can be defined both with Gestures or Sliders; while its size can only be defined using the sliders.
Package Dependencies
Riverpod (Flutter Hooks flavor) for State Management
Freezed for Domain classes immutability
Full Source Code (222 lines)
import 'dart:math' show Random;
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
part '66478145.floor_plan.freezed.dart';
void main() {
runApp(
ProviderScope(
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
home: HomePage(),
),
),
);
}
class HomePage extends HookWidget {
#override
Widget build(BuildContext context) {
final regals = useProvider(regalsProvider.state);
return Scaffold(
body: Center(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Stack(
children: [
Container(
width: kFloorSize.width,
height: kFloorSize.height,
color: Colors.amber.shade100),
...regals
.map(
(regal) => Positioned(
top: regal.offset.dy,
left: regal.offset.dx,
child: GestureDetector(
child: RegalWidget(regal: regal),
),
),
)
.toList(),
],
),
const SizedBox(width: 16.0),
RegalProperties(),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read(regalsProvider).createRegal(),
child: Icon(Icons.add),
),
);
}
}
class RegalWidget extends HookWidget {
final Regal regal;
const RegalWidget({Key key, this.regal}) : super(key: key);
#override
Widget build(BuildContext context) {
final _previousOffset = useState<Offset>(null);
final _refOffset = useState<Offset>(null);
return GestureDetector(
onTap: () => context.read(selectedRegalIdProvider).state = regal.id,
onPanStart: (details) {
_previousOffset.value = regal.offset;
_refOffset.value = details.localPosition;
},
onPanUpdate: (details) => context.read(regalsProvider).updateRegal(
regal.copyWith(
offset: _previousOffset.value +
details.localPosition -
_refOffset.value),
),
child: Container(
width: regal.size.width,
height: regal.size.height,
color: regal.color,
),
);
}
}
class RegalProperties extends HookWidget {
#override
Widget build(BuildContext context) {
final regal = useProvider(selectedRegalProvider);
return Padding(
padding: EdgeInsets.all(16.0),
child: regal == null
? Text('Click a Regal to start')
: Form(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('WIDTH'),
Slider(
min: kRegalMinSize.width,
max: kRegalMaxSize.width,
value: regal.size.width,
onChanged: (value) => context
.read(regalsProvider)
.updateRegal(
regal.copyWith(size: Size(value, regal.size.height)),
),
),
const SizedBox(height: 16.0),
Text('HEIGHT'),
Slider(
min: kRegalMinSize.height,
max: kRegalMaxSize.height,
value: regal.size.height,
onChanged: (value) => context
.read(regalsProvider)
.updateRegal(
regal.copyWith(size: Size(regal.size.width, value)),
),
),
const SizedBox(height: 16.0),
Text('LEFT'),
Slider(
min: 0,
max: kFloorSize.width - regal.size.width,
value: regal.offset.dx,
onChanged: (value) =>
context.read(regalsProvider).updateRegal(
regal.copyWith(
offset: Offset(value, regal.offset.dy)),
),
),
const SizedBox(height: 16.0),
Text('TOP'),
Slider(
min: 0,
max: kFloorSize.height - regal.size.height,
value: regal.offset.dy,
onChanged: (value) =>
context.read(regalsProvider).updateRegal(
regal.copyWith(
offset: Offset(regal.offset.dx, value)),
),
),
],
),
),
);
}
}
final selectedRegalIdProvider = StateProvider<String>((ref) => null);
final selectedRegalProvider = Provider<Regal>((ref) {
final selectedId = ref.watch(selectedRegalIdProvider).state;
final regals = ref.watch(regalsProvider.state);
return regals.firstWhereOrNull((regal) => regal.id == selectedId);
});
final regalsProvider =
StateNotifierProvider<RegalsNotifier>((ref) => RegalsNotifier());
class RegalsNotifier extends StateNotifier<List<Regal>> {
final Size floorSize;
final Size maxSize;
RegalsNotifier({
this.floorSize = const Size(600, 400),
this.maxSize = const Size(100, 100),
List<Regal> state,
}) : super(state ?? []);
void createRegal() {
state = [...state, Regal.random];
print(state.last);
}
void updateRegal(Regal updated) {
state = state.map((r) => r.id == updated.id ? updated : r).toList();
}
}
#freezed
abstract class Regal implements _$Regal {
const factory Regal({
String id,
Color color,
Offset offset,
Size size,
}) = _Regal;
static Regal get random {
final rnd = Random();
return Regal(
id: DateTime.now().millisecondsSinceEpoch.toString(),
color: Color(0xff555555 + rnd.nextInt(0x777777)),
offset: Offset(
rnd.nextDouble() * (kFloorSize.width - kRegalMaxSize.width),
rnd.nextDouble() * (kFloorSize.height - kRegalMaxSize.height),
),
size: Size(
kRegalMinSize.width +
rnd.nextDouble() * (kRegalMaxSize.width - kRegalMinSize.width),
kRegalMinSize.height +
rnd.nextDouble() * (kRegalMaxSize.height - kRegalMinSize.height),
),
);
}
}
// CONFIG
const kFloorSize = Size(600, 400);
const kRegalMinSize = Size(10, 10);
const kRegalMaxSize = Size(200, 200);

Can I use Dismissible without actually dismissing the widget?

I'm trying to make a widget that can be swiped to change the currently playing song in a playlist. I'm trying to mimic how other apps do it by letting the user swipe away the current track and the next one coming in. Dismissible is so close to what I actually want. It has a nice animation and I can easily use the onDismissed function to handle the logic. My issue is that Dismissible actually wants to remove the widget from the tree, which I don't want.
The widget I'm swiping gets updated with a StreamBuilder when the song changes, so being able to swipe away the widget to a new one would be perfect. Can I do this or is there a better widget for my needs?
Here's the widget I'm working on:
class NowPlayingBar extends StatelessWidget {
const NowPlayingBar({
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return StreamBuilder<ScreenState>(
stream: _screenStateStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
final screenState = snapshot.data;
final queue = screenState.queue;
final mediaItem = screenState.mediaItem;
final state = screenState.playbackState;
final processingState =
state?.processingState ?? AudioProcessingState.none;
final playing = state?.playing ?? false;
if (mediaItem != null) {
return Container(
width: MediaQuery.of(context).size.width,
child: Dismissible(
key: Key("NowPlayingBar"),
onDismissed: (direction) {
switch (direction) {
case DismissDirection.startToEnd:
AudioService.skipToNext();
break;
case DismissDirection.endToStart:
AudioService.skipToPrevious();
break;
default:
throw ("Unsupported swipe direction ${direction.toString()} on NowPlayingBar!");
}
},
child: ListTile(
leading: AlbumImage(itemId: mediaItem.id),
title: mediaItem == null ? null : Text(mediaItem.title),
subtitle: mediaItem == null ? null : Text(mediaItem.album),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (playing)
IconButton(
onPressed: () => AudioService.pause(),
icon: Icon(Icons.pause))
else
IconButton(
onPressed: () => AudioService.play(),
icon: Icon(Icons.play_arrow)),
],
),
),
),
);
} else {
return Container(
width: MediaQuery.of(context).size.width,
child: ListTile(
title: Text("Nothing playing..."),
));
}
} else {
return Container(
width: MediaQuery.of(context).size.width,
// The child below looks pretty stupid but it's actually genius.
// I wanted the NowPlayingBar to stay the same length when it doesn't have data
// but I didn't want to actually use a ListTile to tell the user that.
// I use a ListTile to create a box with the right height, and put whatever I want on top.
// I could just make a container with the length of a ListTile, but that value could change in the future.
child: Stack(
alignment: Alignment.center,
children: [
ListTile(),
Text(
"Nothing Playing...",
style: TextStyle(color: Colors.grey, fontSize: 18),
)
],
));
}
},
);
}
}
Here's the effect that I'm going for (although I want the whole ListTile to get swiped, not just the song name): https://i.imgur.com/ZapzpJS.mp4
This can be done by using the confirmDismiss callback instead of the onDismiss callback. To make sure that the widget never actually gets dismissed, you need to return false at the end of the function.
Dismissible(
confirmDismiss: (direction) {
...
return false;
}
)

Question about Flutter State and retrieving variables from State vs StatefulWidget

Here's the context:
In my app, users can create a question, and all questions will be displayed on a certain page. This is done with a ListView.builder whose itemBuilder property returns a QuestionTile.
The problem:
If I create a new question, the text of the new question is (usually) displayed as the text of the previous question.
Here's a picture of me adding three questions in order, "testqn123", "testqn456", "testqn789", but all are displayed as "testqn123".
Hot restarting the app will display the correct texts for each question, but hot reloading wont work.
In my _QuestionTileState class, if I change the line responsible for displaying the text of the question on the page, from
child: Text(text)
to
child: Text(widget.text)
the issue will be resolved for good. I'm not super familiar with how hot restart/reload and state works in flutter, but can someone explain all of this?
Here is the code for QuestionTile and its corresponding State class, and the line changed is the very last line with words in it:
class QuestionTile extends StatefulWidget {
final String text;
final String roomName;
final String roomID;
final String questionID; //
QuestionTile({this.questionID, this.text, this.roomName, this.roomID});
#override
_QuestionTileState createState() => _QuestionTileState(text);
}
class _QuestionTileState extends State<QuestionTile> {
final String text;
int netVotes = 0;
bool expand = false;
bool alreadyUpvoted = false;
bool alreadyDownvoted = false;
_QuestionTileState(this.text);
void toggleExpansion() {
setState(() => expand = !expand);
}
#override
Widget build(BuildContext context) {
RoomDbService dbService = RoomDbService(widget.roomName, widget.roomID);
final user = Provider.of<User>(context);
print(widget.text + " with questionID of " + widget.questionID);
return expand
? ExpandedQuestionTile(text, netVotes, toggleExpansion)
: Card(
elevation: 10,
child: Padding(
padding: const EdgeInsets.fromLTRB(10, 7, 15, 7),
child: GestureDetector(
onTap: () => {
Navigator.pushNamed(context, "/ChatRoomPage", arguments: {
"question": widget.text,
"questionID": widget.questionID,
"roomName": widget.roomName,
"roomID": widget.roomID,
})
},
child: new Row(
// crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Column(
// the stack overflow functionality
children: <Widget>[
InkWell(
child: alreadyUpvoted
? Icon(Icons.arrow_drop_up,
color: Colors.blue[500])
: Icon(Icons.arrow_drop_up),
onTap: () {
dynamic result = dbService.upvoteQuestion(
user.uid, widget.questionID);
setState(() {
alreadyUpvoted = !alreadyUpvoted;
if (alreadyDownvoted) {
alreadyDownvoted = false;
}
});
},
),
StreamBuilder<DocumentSnapshot>(
stream: dbService.getQuestionVotes(widget.questionID),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
} else {
// print("Current Votes: " + "${snapshot.data.data["votes"]}");
// print("questionID: " + widget.questionID);
return Text("${snapshot.data.data["votes"]}");
}
},
),
InkWell(
child: alreadyDownvoted
? Icon(Icons.arrow_drop_down,
color: Colors.red[500])
: Icon(Icons.arrow_drop_down),
onTap: () {
dbService.downvoteQuestion(
user.uid, widget.questionID);
setState(() {
alreadyDownvoted = !alreadyDownvoted;
if (alreadyUpvoted) {
alreadyUpvoted = false;
}
});
},
),
],
),
Container(
//color: Colors.red[100],
width: 290,
child: Align(
alignment: Alignment.centerLeft,
child: Text(text)), // problem solved if changed to Text(widget.text)
),
}
}
You can wrap your UI with a Stream Builder, this will allow the UI to update every time any value changes from Firestore.
Since you are using an item builder you can wrap the widget that is placed with the item builder.
That Should update the UI

state change not prompting ondidchange

I have a page which is connected to appstate and viewmodel ,
what i'm doing in this page is picking an image from gallery and then sending it to the server , when i send i prompt a message to the user so he can see the loading ,
when the upload epic ends there is an action than goes to the reducer which change the state so the ui in the page will change ,
the reducer receives it and return a state , but the onDidChange not prompt right after ,
here's some code:
page build method :
#override
Widget build(BuildContext context) {
return new StoreConnector<AppState, AddRefundViewModel>(
converter: addRefundConverter,
onInitialBuild: (AddRefundViewModel vm) => {vm.doInitializeDto()},
builder: (context, vm) {
return MaterialApp(
title: 'Test',
home: Scaffold(
body: Column(
children: <Widget>[
Container(
child: new Column(
children: <Widget>[
RefundHeader1(context, vm),
RefundHeader(context, vm),
],
),
decoration: new BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerRight,
end: Alignment.bottomLeft,
colors: [primary, primaryGradient]),
),
width: MediaQuery.of(context).size.width * 1,
height: MediaQuery.of(context).size.width * 0.4,
),
//Text(vm.c),
refundRowSum(vm),
refundRowDate(context, vm),
refundRowCurrent(context, vm),
refundRowRefund(context, vm),
refundRowAtm(context, vm),
Expanded(child: Container(
child: GestureDetector(onTap: () {
//print("tap");}
// myFocusNode.dispose();},
if (myFocusNode != null) {
FocusScope.of(context).requestFocus(new FocusNode());
}
}),
)),
proceedMessage(context, vm),
],
),
bottomNavigationBar: BottomAppBar(
child: proceedButton(context, vm),
),
resizeToAvoidBottomPadding: false,
));
},
onDidChange: (vm) {
print('---!!!!-----<<<<<<<<<<<<<< onDidChange >>>>>>>>>>>-----!!!------');
if (vm.addRefundDto.tmpFileStat == TmpFileStat.BadExt) {
showSendReceiptDialog2(context, TmpFileStat.BadExt);
vm.doInitTmpFileStat();
} else if (vm.addRefundDto.tmpFileStat == TmpFileStat.TooLarge) {
showSendReceiptDialog2(context, TmpFileStat.TooLarge);
vm.doInitTmpFileStat();
}
else if (vm.addRefundDto.uploadRefundStatus ==
RefundUpload.Success ||vm.addRefundDto.uploadRefundStatus ==
RefundUpload.Started ||
vm.addRefundDto.uploadRefundStatus == RefundUpload.Failed) {
Navigator.pop(context);
showSendReceiptDialog(context, vm.addRefundDto.uploadRefundStatus);
}
},
);}
reducer :
case GotRefund:
print('----=== GotRefund add new refund reducer ===-----');
AddRefundDto refundDto = state.refundDto;
refundDto.uploadRefundStatus=RefundUpload.Success;
state.refundDto= refundDto;
return new AddRefundState(
fileDto: null,
byteImage: null,
refundDto: state.refundDto,
);
ADDED ViewModel :
import 'package:flutter/widgets.dart';
import 'package:iai/models/add_refund_dto.dart';
import 'package:iai/store/ui/ui_state.dart';
import 'package:iai/theme/images.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:iai/models/currency.dart';
class AddRefundViewModel {
bool fileAttachment;
DateTime pickedDate;
double amount;
bool isAtm;
bool canProceed;
AddRefundDto addRefundDto;
//final List<String> refundList;
final Map<String, AssetImage> refundList;
final Map<String, IconData> currencyList;
final List<Currency> currencyList2;
Function saveCurrentViewModel;
Function sendDtoToServer;
Function openGallery;
Function openCamera;
Function doInitializeDto;
Function doRemoveTmpImage;
Function doInitTmpFileStat;
final Function search;
final String searchTerm;
final bool showSearch;
AddRefundViewModel({
this.openCamera,
this.openGallery,
this.addRefundDto,
this.search,
this.searchTerm,
this.showSearch,
this.doInitializeDto,
this.doRemoveTmpImage,
this.doInitTmpFileStat,
this.refundList,
this.sendDtoToServer,
this.saveCurrentViewModel,
this.pickedDate,
this.amount,
this.currencyList,
this.isAtm,
this.canProceed,
this.currencyList2,
this.fileAttachment,
});
}
thanks to everyone that can contribute
Can you also post your AddRefundViewModel class in here? I think the on onDidChange is called based on those variables.

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.