How to make rating bar non clickable in flutter - flutter

How to make default flutter ratingbar non clickable ?
I have to disable rating option once user give feedback. How do I do it.
RatingBar(
itemSize: 35,
initialRating: 0,
glowColor: Colors.transparent,
direction: Axis.horizontal,
allowHalfRating: false,
tapOnlyMode: false,
itemCount: 5,
itemPadding: const EdgeInsets.symmetric(horizontal: 0.0),
ratingWidget: RatingWidget(
full: Image.asset(img_star_rating_fill, width: 25.w, height: 25.h),
// full: const Icon(Icons.star, color:yellow_FFC800),
half: Image.asset(img_star_rating_fill, width: 25.w, height: 25.h),
// half: const Icon(Icons.star_half, color:yellow_FFC800,),
empty:
Image.asset(img_star_rating_empty, width: 25.w, height: 25.h),
),
// empty: const Icon(Icons.star_outline, color:gray_868590,)),
onRatingUpdate: (value) {
setState(() {
_ratingValue = value;
printData(
'Rating to consultation booking ID', _ratingValue.toString());
controller
.callRateConsultationAPI(
widget.i,
controller.pastBookingList[widget.i].id.toString(),
value.toString())
.then((value) {
setState(() {
// ratingBar.setFocusable(false);
});
});
});
})

You can set ignoreGestures to true, like this:
RatingBar(
ignoreGestures: true, // <---- add this
itemSize: 35,
initialRating:0,
glowColor: Colors.transparent,
direction: Axis.horizontal,
allowHalfRating: false,
...
)

IgnorePointer(child:RatingWidget)

Related

How to show tool tip below the slider enable all time- Flutter

I need SfRangeSlider to display the tooltip below the slider instead of the top. As per here discussion here able to display the tooltip below the slider. It is displayed only when we change the slider. I need to display the tooltip below the slider all the time.
Widget _buildThumbIcon(TextEditingController controller) {
return Transform.translate(
// Here 20 is thumb diameter and 5 is spacing between thumb and text.
offset: const Offset(0, 25),
child: OverflowBox(
maxWidth: 150,
child: TextField(
textAlign: TextAlign.center,
decoration:
const InputDecoration(border: InputBorder.none, isDense: true),
controller: controller,
),
),
);
}
SfRangeSlider(
activeColor: primary,
inactiveColor: grey_e1,
min: 1.0,
max: 12.0,
showTicks: true,
showLabels: false,
stepSize: 1,
// interval: 1,
//minorTicksPerInterval: 1,
startThumbIcon: _buildThumbIcon(_rangeStartController),
endThumbIcon: _buildThumbIcon(_rangeEndController),
values: _values,
onChangeStart: (SfRangeValues newValues) {
_rangeStartController.text = _getFormattedText(newValues.start);
_rangeEndController.text = _getFormattedText(newValues.end);
},
onChanged: (SfRangeValues newValues) {
setState(() {
_rangeStartController.text = _getFormattedText(newValues.start);
_rangeEndController.text = _getFormattedText(newValues.end);
_values = newValues;
});
},
onChangeEnd: (SfRangeValues newValues) {
_rangeStartController.text = "";
_rangeEndController.text = "";
},
)

How to validate a form that has cards to select from and the user should obligatorily select one of them?

I have a code whereby the genders of a pet is listed in two separate cards and when the user taps on one of them, it changes color to indicate that it has been selected and is saved in the database. However, the app is letting the user continue to the next page without choosing any one of the values. I want to do a validation whereby the user will have to choose one of the cards to be able to move forward. How can I do this please?
Here is my code:
Expanded(
child: GridView.count(
crossAxisCount: 2,
primary: false,
scrollDirection: Axis.vertical,
children: List.generate(petGenders.length, (index) {
return GestureDetector(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0)),
color:
selectedIndex == index ? primaryColor : null,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
petGenders[petKeys[index]],
SizedBox(
height: 15.0,
),
Text(
petKeys[index],
style: TextStyle(
color: selectedIndex == index
? Colors.white
: null,
fontSize: 18.0,
fontWeight: FontWeight.w600),
),
],
),
),
),
onTap: () {
setState(() {
widget.pet.gender = petKeys[index];
selectedIndex = index;
});
});
}),
),
),
The model:
Map<String, Image> genders() => {
"Male":
Image(image: AssetImage('Assets/images/male.png'), width: 50),
"Female":
Image(image: AssetImage('Assets/images/female.png'), width: 50)
};
Take one variable
bool isGenderSelected = false;
Then change its value to true on tap of card like
onTap: () {
setState(() {
isGenderSelected = true;
widget.pet.gender = petKeys[index];
selectedIndex = index;
});
});
Now check if it's true then only allow the user to go next page or show some message to the user
Scenario like this, I prefer using nullable selectedValue. In this case, I will create nullable int to hold and switch between selection.
int? selectedIndex;
And using color will be like
color: selectedIndex==index? SelectedColor:null,
you can replace null with inactive color.
For validation part, do null check on selectedIndex .
if(selectedIndex!=null){.....}

How to get the index value whilst not getting duplicates within ListView.builder?

I'm trying to show the week's budget spending & timeline only when the user has put in the spending information.
This is the expected result and all seems to be working nicely until I add in more than just one spending in each week.
Here's what happens:
The problem that I understand is that the ListView.builder gets the date that is between "Initial" and "End" and builds the widgets. So because there's 2 spending between those dates, then it builds 2 of those widgets. The problem is I just can't seem to figure out a way to show them without duplication.
Here's my code:
Widget build(BuildContext context) {
return ListView.builder(
shrinkWrap: true,
controller: ScrollController(),
itemCount: snapshot.data!.length,
padding: const EdgeInsets.only(bottom: 8),
itemBuilder: (context, index) {
final spending = snapshot.data![index];
DateTime spendingDate = DateTime.parse(spending.date);
var initial =
DateTime(initialDate.year, initialDate.month, initialDate.day - 1);
var end = DateTime(endDate.year, endDate.month, endDate.day + 1);
return spendingDate.isAfter(initial) && spendingDate.isBefore(end)
? Column(
children: [
WeekDivider(label: label, dateEstimation: dateEstimation),
WeeklySpendingStream(
color: color,
snapshot: snapshot,
initialDate: initialDate,
endDate: endDate,
),
],
)
: const SizedBox();
},
);
}
WeeklySpendingStreamCode:
Widget build(BuildContext context) {
return ListView.builder(
shrinkWrap: true,
controller: ScrollController(),
itemCount: snapshot.data!.length,
padding: const EdgeInsets.only(bottom: 8),
itemBuilder: (context, index) {
final spending = snapshot.data![index];
DateTime spendingDate = DateTime.parse(spending.date);
var initial =
DateTime(initialDate.year, initialDate.month, initialDate.day - 1);
var end = DateTime(endDate.year, endDate.month, endDate.day + 1);
if (spendingDate.isAfter(initial) && spendingDate.isBefore(end)) {
return SwipeActionCell(
editModeOffset: 0,
fullSwipeFactor: 0.50,
key: ObjectKey(snapshot.data![index]),
trailingActions: [
SwipeAction(
performsFirstActionWithFullSwipe: true,
color: Colors.transparent,
content: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30),
color: Colors.red,
),
child: getIconButton(Colors.red, IconlyBold.delete)),
onTap: (handler) async {
handler(true);
await Future.delayed(const Duration(milliseconds: 100));
snapshot.data!.removeAt(index);
SpendingDatabaseHelper.instance.removeMethod(spending.id!);
},
),
],
child: SpendingCard(
beneficiary: spending.beneficiary,
budgetSpent: currency.format(int.parse(spending.budgetSpent)),
date: DateFormat("dd-MM-yyyy")
.format(DateTime.parse(spending.date)),
colorValue: color,
),
);
} else {
return const SizedBox();
}
},
);
}
The output of snapshot.data:
[
{id: 8, budgetName: 🍣 Food & Beverage, beneficiary: ddd, budgetSpent: 1, date: 2022-02-21},
{id: 7, budgetName: 🍣 Food & Beverage, beneficiary: dfgvsd, budgetSpent: 1, date: 2022-02-14},
{id: 4, budgetName: 🍣 Food & Beverage, beneficiary: ddd, budgetSpent: 1, date: 2022-02-11},
{id: 10, budgetName: 🍣 Food & Beverage, beneficiary: ddd, budgetSpent: 1, date: 2022-02-11},
{id: 5, budgetName: 🍣 Food & Beverage, beneficiary: asxasd, budgetSpent: 1, date: 2022-02-06}
]
Would really appreciate any suggestions/ideas of how this can be resolved.
Thanks in advance!
Nevermind, I somehow found the solution by getting the data using ".where" and checking whether it is empty or not. It worked nicely.
var initial = DateTime(initialDate.year, initialDate.month, initialDate.day - 1);
var end = DateTime(endDate.year, endDate.month, endDate.day + 1);
final spending = snapshot.data!.where((s) =>
DateTime.parse(s.date).isAfter(initial) &&
DateTime.parse(s.date).isBefore(end));
return spending.isNotEmpty ||
DateTime.now().isAfter(initial) && DateTime.now().isBefore(end)
? spending.isEmpty
? Column(
children: [
WeekDivider(label: label, dateEstimation: dateEstimation),
const Padding(
padding: EdgeInsets.only(top: 8, bottom: 16),
child: Text(
'No spending this week',
style: kCaption,
),
),
],
)
: ListView(
shrinkWrap: true,
controller: ScrollController(),
padding: const EdgeInsets.only(bottom: 8),
children: [
Column(
children: [
WeekDivider(label: label, dateEstimation: dateEstimation),
WeeklySpendingStream(
color: color,
snapshot: snapshot,
initialDate: initialDate,
endDate: endDate,
),
],
)
],
)
: const SizedBox();
}
I think your problem raised because of you just extract the database file without filtering so do this use .toSet() or assign the extracted data to a variable as a map or Set and use .toSet().

Can you skip an action widget in the foreground to another widget?

I use Lottie above GoogleMap but I have problem I can't move GoogleMap In the frame of the picture
For example, when I use my fingers to move inside the map
It works fine but doesn't work at the edges of the Lottie
Can i skip Lottie to move map as if it does not exist
that my code
Stack(
children: [
GoogleMap(
padding: EdgeInsets.only(bottom: bottomPosition),
myLocationButtonEnabled: false,
initialCameraPosition: _cameraPosition,
myLocationEnabled: true,
onCameraMoveStarted: () {
setState(() {
_controllerAnimation.forward(from: 0.0);
});
},
onCameraIdle: () {
setState(() {
_controllerAnimation.forward(from: 0.65);
});
},
onMapCreated: (GoogleMapController controller) async {
_controller.complete(controller);
_googleMapController = controller;
},
),
Positioned(
top: (mapHeight - iconSize - 180) / 2,
right: (mapWidth - iconSize - 60) / 2,
child: Lottie.asset("assets/json_assets/expanding_marker.json",
width: 128,
height: 160,
repeat: false,
controller: _controllerAnimation, onLoaded: (composition) {
_controllerAnimation.duration = composition.duration;
_controllerAnimation.forward(from: 0.50);
_controllerAnimation.stop(canceled: true);
})
),
])
If there is no way to solve, Is there another idea .
Try this:
Positioned(
top: (mapHeight - iconSize - 180) / 2,
right: (mapWidth - iconSize - 60) / 2,
child:
IgnorePointer(ignoring: true, child:
Lottie.asset("assets/json_assets/expanding_marker.json",
width: 128,
height: 160,
repeat: false,
controller: _controllerAnimation, onLoaded: (composition) {
_controllerAnimation.duration = composition.duration;
_controllerAnimation.forward(from: 0.50);
_controllerAnimation.stop(canceled: true);
})
),),
])

Searchable SliverGrid Rendering Wrong Items

I have a SliverGrid. I have a search field. In my search field onChange event I have a function that searches my local sqlite db based on the keyword entered by the user returns the results and reassigns to a variable and calls notifyListeners(). Now my problem is for some weird reason whenever I search for an item the wrong item is rendered.
I checked the results from my functions by iterating over the list and logging the title and the overall count as well and the results were correct however my view always rendered the wrong items. Not sure how this is possible.
I also noticed something strange, whenever it rendered the wrong item and I went back to my code and hit save, triggering live reload, when I switched back to my emulator it now displayed the right item.
I have tried the release build on an actual phone and it's the same behaviour. Another weird thing is sometimes certain items will duplicate and show twice in my list while the user is typing.
This is my function that searches my sqlite db:
Future<List<Book>> searchBookshelf(String keyword) async {
try {
Database db = await _storageService.database;
final List<Map<String, dynamic>> rows = await db
.rawQuery("SELECT * FROM bookshelf WHERE title LIKE '%$keyword%'; ");
return rows.map((i) => Book.fromJson(i)).toList();
} catch (e) {
print(e);
return null;
}
}
This is my function that calls the above function from my viewmodel:
Future<void> getBooksByKeyword(String keyword) async {
books = await _bookService.searchBookshelf(keyword);
notifyListeners();
}
This is my actual view where i have the SliverGrid:
class BooksView extends ViewModelBuilderWidget<BooksViewModel> {
#override
bool get reactive => true;
#override
bool get createNewModelOnInsert => true;
#override
bool get disposeViewModel => true;
#override
void onViewModelReady(BooksViewModel vm) {
vm.initialise();
super.onViewModelReady(vm);
}
#override
Widget builder(BuildContext context, vm, Widget child) {
var size = MediaQuery.of(context).size;
final double itemHeight = (size.height) / 4.3;
final double itemWidth = size.width / 3;
var heading = Container(
margin: EdgeInsets.only(top: 35),
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Align(
alignment: Alignment.centerLeft,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Books',
textAlign: TextAlign.left,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w900),
),
Text(
'Lorem ipsum dolor sit amet.',
textAlign: TextAlign.left,
style: TextStyle(fontSize: 14),
),
],
),
),
);
var searchField = Container(
margin: EdgeInsets.only(top: 5, left: 15, bottom: 15, right: 15),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(15)),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 1.0,
spreadRadius: 0.0,
offset: Offset(2.0, 1.0), // shadow direction: bottom right
),
],
),
child: TextFormField(
decoration: InputDecoration(
border: InputBorder.none,
prefixIcon: Icon(
FlutterIcons.search_faw,
size: 18,
),
suffixIcon: Icon(
FlutterIcons.filter_fou,
size: 18,
),
hintText: 'Search...',
),
onChanged: (keyword) async {
await vm.getBooksByKeyword(keyword);
},
onFieldSubmitted: (keyword) async {},
),
);
return Scaffold(
body: SafeArea(
child: Container(
padding: EdgeInsets.only(left: 1, right: 1),
child: LiquidPullToRefresh(
color: Colors.amber,
key: vm.refreshIndicatorKey, // key if you want to add
onRefresh: vm.refresh,
showChildOpacityTransition: true,
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Column(
children: [
heading,
searchField,
],
),
),
SliverToBoxAdapter(
child: SpaceY(15),
),
SliverToBoxAdapter(
child: vm.books.length == 0
? Column(
children: [
Image.asset(
Images.manReading,
width: 250,
height: 250,
fit: BoxFit.contain,
),
Text('No books in your bookshelf,'),
Text('Grab a book from our bookstore.')
],
)
: SizedBox(),
),
SliverPadding(
padding: EdgeInsets.only(bottom: 35),
sliver: SliverGrid.count(
childAspectRatio: (itemWidth / itemHeight),
mainAxisSpacing: 20.0,
crossAxisCount: 3,
children: vm.books
.map((book) => BookTile(book: book))
.toList(),
),
)
],
),
))));
}
#override
BooksViewModel viewModelBuilder(BuildContext context) =>
BooksViewModel();
}
Now the reason I am even using SliverGrid in the first place is because I have a search field and a title above the grid and I want all items to scroll along with the page, I didn't want just the list to be scrollable.
I believe this odd behavior can be attributed to you calling vm.getBooksByKeyword() in onChanged. As this is an async method, there is no guarantee that the last result returned will be the result for the final text in the TextFormField. The reason you see the correct results after a live reload is because the method is being called again with the full text currently in the TextFormField.
The quickest way to verify this is to move the function call to onFieldSubmitted or onEditingComplete and see if it behaves correctly.
If you require calling the function with every change to the text, you will need to add a listener to the controller and be sure to only make the call after input has stopped for a specified amount of time, using a Timer, like so:
final _controller = TextEditingController();
Timer _timer;
...
_controller.addListener(() {
_timer?.cancel();
if(_controller.text.isNotEmpty) {
// only call the search method if keyword text does not change for 300 ms
_timer = Timer(Duration(milliseconds: 300),
() => vm.getBooksByKeyword(_controller.text));
}
});
...
#override
void dispose() {
// DON'T FORGET TO DISPOSE OF THE TextEditingController
_controller.dispose();
super.dispose();
}
...
TextFormField(
controller: controller,
...
);
So I found the problem and the solution:
The widget tree is remembering the list items place and providing the
same viewmodel as it had originally. Not only that it also takes every
item that goes into index 0 and provides it with the same data that
was enclosed on the Construction of the object.
Taken from here.
So basically the solution was to add and set a key property for each list item generated:
SliverPadding(
padding: EdgeInsets.only(bottom: 35),
sliver: SliverGrid(
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: (itemWidth / itemHeight),
mainAxisSpacing: 20.0,
),
delegate: SliverChildListDelegate(vm.books
.map((book) => BookTile(
key: Key(book.id.toString()), book: book))
.toList()),
),
)
And also here:
const BookTile({Key key, this.book}) : super(key: key, reactive: false);
My search works perfectly now. :)