Open Flutter dropdown programmatically - flutter

I am trying to build a searchable dropdown which will load value from service on every button click. So, for that I have encapsulated DropDownButton and TextField in Stack Widget.
On keypress we get response from api, so far so good. But after getting data from api dropdown was not opening. After digging a bit I came to know it was not opening because we need to manually tap it to open, but since its in stack and second children is TextField I can't tap it.
But opening DropDownButton button programmatically is not possible.
So I tried second solution from https://stackoverflow.com/a/59499191/10423593 but it didn't work.
Below is my code without solution from stackoverflow.
import 'package:flutter/material.dart';
import 'package:giphy/services/gifs_service.dart';
import 'package:giphy/shared/autocomplete.dart';
class TestDropDown extends StatefulWidget {
// const TestDropDown({Key? key}) : super(key: key);
#override
_TestDropDownState createState() => _TestDropDownState();
}
class _TestDropDownState extends State<TestDropDown> {
final GifyService _service = GifyService();
final TextEditingController _gifSearchController = TextEditingController();
List<SearchData> _dropDownResult = <SearchData>[];
GlobalKey key = GlobalKey();
// T? _findChildWidgetFromKey<T extends Widget>(
// BuildContext? context, T childWidget) {
// T? detector;
// context!.visitChildElements((element) {
// if (element.widget == childWidget) {
// detector = element.widget as T;
// }
// });
// return detector;
// }
Widget _buildDropDown(List<SearchData> searchData) => DropdownButton<String>(
isExpanded: true,
key: key,
onChanged: (String? value) {},
items: searchData
.map(
(e) => DropdownMenuItem<String>(
child: Text(e.name ?? ''),
value: e.name ?? '',
),
)
.toList(),
);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
_buildDropDown(_dropDownResult),
Container(child: () {
if (_dropDownResult.length > 0) {
_buildDropDown(_dropDownResult);
}
}()),
TextField(
controller: _gifSearchController,
decoration: InputDecoration(
border: OutlineInputBorder(
borderSide: BorderSide(width: 0.5),
borderRadius: BorderRadius.circular(21),
),
),
onChanged: (String value) async {
AutoComplete result = await _service.getSearchKeywords(value);
setState(() {
_dropDownResult = result.data;
});
},
),
],
),
);
}
}

If you are looking to autocomplete the text field then you can use
autocomplete_textfield package.
Or if you want to build it on your own then you can try a different approach by using container instead of dropdown menu.

After trying some packages. I wasn't able to do flutter autocomplete based on api calls.
So decided to try some other approach and used Overlay in flutter
import 'dart:async';
import 'package:flutter/material.dart';
class TestDropDown extends StatefulWidget {
#override
_TestDropDownState createState() => _TestDropDownState();
}
class _TestDropDownState extends State<TestDropDown> {
late OverlayEntry _overlayEntry;
Timer? _debounce;
_showOverlay(BuildContext context) {
OverlayState? state = Overlay.of(context);
final RenderBox? box = key.currentContext!.findRenderObject() as RenderBox;
Size size = box!.size;
Offset position = box.localToGlobal(Offset.zero);
_overlayEntry = OverlayEntry(
builder: (context) => Positioned(
top: size.height + position.dy,
left: position.dx,
child: Card(
child: Container(
height: 200,
width: size.width,
decoration: BoxDecoration(
border: Border.all(),
),
child: Column(
children: [
Container(
width: size.width,
child: IconButton(
onPressed: () {
if (_overlayEntry.mounted) {
_overlayEntry.remove();
}
},
icon: Icon(Icons.close),
alignment: Alignment.topRight,
),
),
Expanded(
child: ListView(
children: []..addAll(List.generate(
200, (index) => Text(index.toString()))),
),
),
],
)),
),
),
);
state!.insert(_overlayEntry);
}
final key = GlobalKey();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(50),
child: Column(
children: [
TextField(
key: key,
onChanged: (String searchText) {
if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 500), () {
print(searchText);
});
},
),
ElevatedButton(
onPressed: () {
_showOverlay(context);
},
child: Text('Press Me'),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
if (_overlayEntry.mounted) {
_overlayEntry.remove();
}
},
),
);
}
}

try using: https://pub.dev/packages/dropdown_search
Step 1: you request all data
Step 2: using the library you can searching item

Related

How to send a data from listview screen to form screen using flutter

I am trying to send a data from ontap listview screen to form screen like image below. I have searched many references on google but I can't find any references that can help me, if you can provide solutions or references, I will greatly appreciate it.
enter image description here
This is my sample code (ListPage Screen) :
const ListPage({Key? key}) : super(key: key);
#override
State<ListPage> createState() => _ListPageState();
}
class _ListPageState extends State<ListPage> {
TextEditingController textFieldController = TextEditingController();
var _controller = TextEditingController();
late bool searching, error;
var data;
late String query;
String dataurl = "https://www.something.co.id/mobile/search_data.php";
#override
void initState() {
searching = true;
error = false;
query = "";
super.initState();
}
void getSuggestion() async {
//get suggestion function
var res = await http
.post((Uri.parse(dataurl + "?query=" + Uri.encodeComponent(query))));
//in query there might be unwant character so, we encode the query to url
if (res.statusCode == 200) {
setState(() {
data = json.decode(res.body);
//update data value and UI
});
} else {
//there is error
setState(() {
error = true;
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
toolbarHeight: AppLayout.getHeight(100),
automaticallyImplyLeading: false,
title: searchField(),
backgroundColor: Styles.background,
elevation: 0.0,
),
body: SingleChildScrollView(
child: Container(
alignment: Alignment.center,
child: data == null
? Container(
padding: EdgeInsets.all(20),
child: searching
? Text("Please wait")
: Text("Search any location")
//if is searching then show "Please wait"
//else show search peopels text
)
: Container(
child: searching
? showSearchSuggestions()
: Text("Find any location"),
)
// if data is null or not retrived then
// show message, else show suggestion
),
),
);
}
Widget showSearchSuggestions() {
List suggestionlist = List.from(data["data"].map((i) {
return SearchSuggestion.fromJSON(i);
}));
//serilizing json data inside model list.
return Column(
children: suggestionlist.map((suggestion) {
return InkResponse(
// onTap: () {
// //when tapped on suggestion
// print(suggestion.id); //pint student id
// },
child: GestureDetector(
onTap: () {
_sendDataBack(context);
},
child: SizedBox(
width: double.infinity, //make 100% width
child: Card(
child: Container(
decoration: BoxDecoration(color: Styles.background),
padding: EdgeInsets.all(15),
child: Text(
suggestion.name,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
),
),
),
);
}).toList(),
);
}
// get the text in the TextField and send it back to the FirstScreen
void _sendDataBack(BuildContext context) {
String textToSendBack = textFieldController.text;
Navigator.pop(context, textToSendBack);
}
Widget searchField() {
//search input field
return Container(
height: 50,
child: TextField(
controller: _controller,
autofocus: true,
style: Styles.textStyle,
decoration: InputDecoration(
hintStyle: TextStyle(color: Styles.colorDeepGrey),
hintText: "Search Location...",
prefixIcon: Icon(Icons.search),
suffixIcon: _controller.text.length > 0
? IconButton(
onPressed: () {
_controller.clear();
setState(() {});
},
icon: Icon(Icons.cancel, color: Colors.grey))
: null,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Styles.colorLightBlack.withOpacity(0.20),
width: 2,
),
borderRadius: BorderRadius.circular(4),
), //under line border, set OutlineInputBorder() for all side border
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Styles.primaryColor,
width: 1,
),
borderRadius: BorderRadius.circular(4),
), // focused border color
), //decoration for search input field
onChanged: (value) {
query = value; //update the value of query
getSuggestion(); //start to get suggestion
},
),
);
}
}
//serarch suggestion data model to serialize JSON data
class SearchSuggestion {
String id, name;
SearchSuggestion({required this.id, required this.name});
factory SearchSuggestion.fromJSON(Map<String, dynamic> json) {
return SearchSuggestion(
id: json["id"],
name: json["name"],
);
}
}
Sample Code NextPage Screen :
class NextPage extends StatefulWidget {
#override
_NextPageState createState() => _NextPageState();
}
class _NextPageState extends State<NextPage> {
int _currentStep = 0;
StepperType stepperType = StepperType.vertical;
String text = 'Text';
var _controller = TextEditingController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text('Flutter Stepper Demo'),
centerTitle: true,
),
body: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Stepper(
type: stepperType,
physics: ScrollPhysics(),
currentStep: _currentStep,
onStepTapped: (step) => tapped(step),
onStepContinue: continued,
onStepCancel: cancel,
steps: <Step>[
//Form Pengirim
Step(
title: new Text('Location'),
content: Column(
children: <Widget>[
SizedBox(
height: 50,
child: TextField(
onTap: () {
_awaitReturnValueFromSecondScreen(context);
},
controller: _controller,
autofocus: true,
onChanged: (text) {
setState(() {});
},
style: Styles.textStyle,
textInputAction: TextInputAction.next,
decoration: InputDecoration(
hintText: 'Location',
contentPadding:
EdgeInsets.only(left: 15, right: 15),
hintStyle: TextStyle(color: Styles.colorDeepGrey),
suffixIcon: _controller.text.length > 0
? IconButton(
onPressed: () {
_controller.clear();
setState(() {});
},
icon: Icon(Icons.cancel,
color: Colors.grey))
: null,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color:
Styles.colorLightBlack.withOpacity(0.20),
width: 2,
),
borderRadius: BorderRadius.circular(4),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Styles.primaryColor,
width: 1,
),
borderRadius: BorderRadius.circular(4),
),
),
),
),
],
),
isActive: _currentStep >= 0,
state: _currentStep >= 0
? StepState.complete
: StepState.disabled,
),
],
),
),
],
),
),
);
}
void _awaitReturnValueFromSecondScreen(BuildContext context) async {
// start the SecondScreen and wait for it to finish with a result
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DataAlamat(),
));
// after the SecondScreen result comes back update the Text widget with it
setState(() {
text = result;
});
}
tapped(int step) {
setState(() => _currentStep = step);
}
continued() {
_currentStep < 2 ? setState(() => _currentStep += 1) : null;
}
cancel() {
_currentStep > 0 ? setState(() => _currentStep -= 1) : null;
}
}
Pass the tapped item value to the next page via named parameter of other page class.
class ListPage extends StatelessWidget {
const ListPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: 10,
itemBuilder: (context, index) {
return ListTile(
onTap: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return NextPage(value: index);
},
));
},
title: Text(index.toString()),
);
},
),
);
}
}
class NextPage extends StatelessWidget {
final int value;
const NextPage({Key? key, required this.value}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text(value.toString()),
),
);
}
}
Example in ListView screen, you have a variable called List<String> listLocations. Your ListView widget be like:
ListView.builder(
itemCount: listLocations.length,
itemBuilder: (context, index) {
return InkWell(
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) {
return SecondScreen(listLocations[index]);
},
)),
child: ...
);
}
}
And your SecondScreen is a StatefulWidget (well it is a Form screen, so it would be Stateful, not Stateless, use TextEditingController in Form widget):
import 'package:flutter/material.dart';
class SecondScreen extends StatefulWidget {
final String location;
SecondScreen(this.location, {Key? key}) : super(key: key);
#override
State<SecondScreen> createState() => _SecondScreenState();
}
class _SecondScreenState extends State<SecondScreen> {
var _textEditingController = TextEditingController();
#override
void initState() {
_textEditingController.text = widget.location;
super.initState();
}
#override
void dispose() {
_textEditingController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container();
}
}
You need to pass the location value in init state, and don't forget to dispose it.

FLUTTER : How to only rebuild the top page in stack flutter

I have an issue with rebuilding pages in the stack with Flutter
This is all my users and have added the search to the top appBar.
But it works with conditions to see if it is widgets there then get height of the widgets that is being fixed underneath the appBar...But that happens asynchronously.
So when firstLoad it works but when I call setState it then rebuilds all the pages in the stack and with that it looks like this
This is how it looks after a set state. The problem i saw is that the previous pages have an influence. I couldn't find a good viable solution to this. Will explain my architecture.
I have a page Layout that is a container wrapper for all my pages that has it's appBar styles and just sends through the children. But that is the page Layout wrapper that is being rebuild every time the a set States happen
HOW I GET MY WIDGET SIZE
HOW I IMPLEMENTED IT
It goes in the else with the other pages in the stack.. I tried putting it in the initState but it never goes inside because it is used in the other pages in the stack
I only need an implementation to rebuild the TOP page in the stack.
PAGE LAYOUT
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:flutterweb/constants.dart';
import 'package:flutterweb/controllers/channel_controller.dart';
import 'package:flutterweb/controllers/user_controller.dart';
import 'package:flutterweb/main.dart';
import 'package:flutterweb/models/channels_model.dart';
import 'package:flutterweb/utils/functions.dart';
import 'package:flutterweb/views/channels/func.dart';
import 'package:flutterweb/views/home/home.dart';
import 'package:flutterweb/views/menu/permissions/choose_assign_group.dart';
import 'package:flutterweb/widgets/builders/KNetworkFadeImage.dart';
import 'package:flutterweb/widgets/builders/kPopups.dart';
import 'package:flutterweb/widgets/drawerDara.dart';
import 'package:get/get.dart';
class CustomAppBar extends StatefulWidget {
final String title;
final List<Map<String, dynamic>>? topTabs;
final TabController? topTabController;
final List<Widget>? children;
final List<Widget>? childrenFixed;
final Function? leftActionFunction;
final Icon? leftActionIcon;
final Drawer? drawer;
final Function? logOutPressed;
final bool showOptionsMenu;
final Widget? optionMenu;
final Widget? bottomNavigationBar;
final String? backGroundImage;
final ScrollController? scrollController;
CustomAppBar({
required this.title,
this.topTabs,
this.topTabController,
this.leftActionFunction,
this.leftActionIcon,
this.children,
this.childrenFixed,
this.drawer,
this.logOutPressed,
this.showOptionsMenu = false,
this.optionMenu,
this.bottomNavigationBar,
this.backGroundImage,
this.scrollController,
});
#override
_CustomAppBarState createState() => _CustomAppBarState();
}
double app_content_height = 0;
double fixedWidgetSize = 0;
String prevTitle = "";
class _CustomAppBarState extends State<CustomAppBar>
with SingleTickerProviderStateMixin {
final GlobalKey<ScaffoldState> scaffoldkey = GlobalKey<ScaffoldState>();
#override
void initState() {
super.initState();
}
#override
void dispose() {
super.dispose();
}
_toggleAnimation() {
scaffoldkey.currentState!.openDrawer();
}
double _getAppBarSize() {
double fixedHeightInclude = fixedWidgetSize;
if (widget.topTabs != null) {
fixedHeightInclude += 100;
} else if (widget.title == "") {
fixedHeightInclude += 0;
} else {
fixedHeightInclude += 60;
}
return fixedHeightInclude;
}
#override
Widget build(BuildContext context) {
// if (widget.title != global_title) return SizedBox();
List<Widget> arr = [];
Widget arrView = SizedBox();
double statusBar = 0;
double _width = MediaQuery.of(context).size.width;
Widget? fixedChild = SizedBox();
if ((widget.childrenFixed?.length ?? 0) > 1) {
fixedChild = WidgetSize(
child: Column(children: widget.childrenFixed!),
onChange: (Size size) {
fixedWidgetSize = 0;
setState(() {
fixedWidgetSize = size.height;
});
kPrint("fixedWidgetSize ${size.height}");
},
);
} else {
fixedWidgetSize = 0;
}
// Widget? fixedChild = (widget.childrenFixed?.length ?? 0) > 1
// ? WidgetSize(
// child: Column(children: widget.childrenFixed!),
// onChange: (Size size) {
// fixedWidgetSize = 0;
// setState(() {
// fixedWidgetSize = size.height;
// });
// kPrint("fixedWidgetSize ${size.height}");
// },
// )
// : SizedBox();
app_content_height =
MediaQuery.of(context).size.height - _getAppBarSize() - statusBar;
if (widget.title != "") {
arr.add(
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
flex: 2,
child: widget.showOptionsMenu == true
? IconButton(
icon: const Icon(Icons.menu, color: Colors.white),
onPressed: () => _toggleAnimation(),
)
: IconButton(
icon: widget.leftActionIcon ??
const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () {
if (widget.leftActionFunction != null) {
widget.leftActionFunction!();
} else {
if (Navigator.canPop(context)) {
Get.back();
}
}
},
),
),
Expanded(flex: 2, child: SizedBox()),
Expanded(
flex: 10,
child: Center(
child: Text(
widget.title,
style: const TextStyle(color: Colors.white, fontSize: 24.0),
),
),
),
Expanded(
flex: 4,
child: widget.logOutPressed != null
? IconButton(
icon: const Icon(Icons.power_settings_new_outlined,
color: Colors.white),
onPressed: () {
widget.logOutPressed!();
},
)
: widget.optionMenu ?? Container(),
),
],
),
);
}
if (widget.topTabs != null) {
List<Widget> tempTopBar = [];
List<Widget> tempTopView = [];
for (var i = 0; i < widget.topTabs!.length; i++) {
String key = widget.topTabs![i].keys
.toString()
.replaceAll("(", "")
.replaceAll(")", "");
Widget value = widget.topTabs![i][key];
tempTopBar.add(Tab(text: key));
tempTopView.add(SingleChildScrollView(child: value));
}
arr.add(
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Center(
child: Container(
height: 30,
child: DefaultTabController(
length: widget.topTabs!.length,
child: TabBar(
labelPadding: widget.topTabs!.length == 2
? const EdgeInsets.symmetric(horizontal: 40.0)
: const EdgeInsets.symmetric(horizontal: 16.0),
controller: widget.topTabController,
indicatorSize: TabBarIndicatorSize.tab,
indicator: CircleTabIndicator(color: Colors.white, radius: 4),
isScrollable: true,
labelColor: Colors.white,
tabs: tempTopBar,
),
),
),
),
),
);
// arr.add(child);
arrView = Container(
width: _width,
height: app_content_height,
child: TabBarView(
controller: widget.topTabController,
children: tempTopView,
),
);
}
if (widget.children != null) {
arrView = Container(
width: _width,
height: app_content_height,
child: ListView(
// controller: widget.scrollController ?? ScrollController(),
children: widget.children!,
),
);
}
_getStatus() {
if (statusBar > 0) {
Color color = AppColors.kBlue;
return Container(
height: Get.height * 0.03,
width: Get.width,
color: color,
child: const Center(
child: Text(
"",
style: const TextStyle(color: Colors.black),
),
),
);
} else {
return const SizedBox();
}
}
return SafeArea(
child: Material(
child: Stack(
children: [
Scaffold(
resizeToAvoidBottomInset:
true, //That the keyboard shows correctly
extendBodyBehindAppBar: true,
key: scaffoldkey,
appBar: PreferredSize(
preferredSize: Size.fromHeight(
_getAppBarSize()), // here the desired height
child: Container(
decoration: kAppBarBoxDecorations,
child: Column(
children: [
Column(
children: arr,
),
fixedChild,
],
),
),
),
drawer: Drawer(
child: ListView(
children: <Widget>[
UserAccountsDrawerHeader(
accountName: InkWell(
onTap: () {
Channels element =
ChannelController.to.gSelectedChannel.value;
getChannelRoles(element);
},
child: Text(
"${ChannelController.to.gSelectedChannel.value.rolDesc} >",
),
),
accountEmail: Text(
UserController.to.gUserModel.value.email.toString()),
currentAccountPicture: GestureDetector(
child: const CircleAvatar(
backgroundImage: NetworkImage(
"https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500"),
),
onTap: () => print("Current User")),
decoration: const BoxDecoration(
image: DecorationImage(
fit: BoxFit.fill,
image: NetworkImage(
"${URLS.keyBaseUrl}/assets/images/background/background7.jpg")),
),
),
ListTile(
title: const Text("Home"),
trailing: const Icon(Icons.home),
onTap: () =>
Get.toNamed(Home.router, preventDuplicates: false),
),
ListTile(
title: const Text("Menu Permissions"),
trailing: const Icon(Icons.home),
onTap: () => Get.toNamed(ChooseAssignGroup.router,
preventDuplicates: false),
),
const Divider(
thickness: 1.0,
),
drawerData(),
const Divider(
thickness: 1.0,
),
ListTile(
title: const Text("Close"),
trailing: const Icon(Icons.cancel),
onTap: () => Navigator.of(context).pop(),
),
ListTile(
title: const Text("Log Out"),
trailing: const Icon(Icons.logout),
onTap: () => UserController.to.logOutUser(),
),
],
),
),
body: Container(
decoration: widget.backGroundImage != null
? BoxDecoration(
color: Colors.black.withOpacity(0.9),
image: DecorationImage(
fit: BoxFit.cover,
colorFilter: ColorFilter.mode(
Colors.black.withOpacity(0.2), BlendMode.dstATop),
image: NetworkImage(widget.backGroundImage!),
),
)
: BoxDecoration(color: Colors.grey.shade400),
child: Center(
child: Container(
constraints: BoxConstraints(maxWidth: 800),
padding: EdgeInsets.only(
left: 15.0, right: 15.0, top: _getAppBarSize()),
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: arrView,
),
),
),
),
bottomNavigationBar: widget.bottomNavigationBar,
),
],
),
),
);
}
}
class CircleTabIndicator extends Decoration {
final BoxPainter _painter;
CircleTabIndicator({required Color color, required double radius})
: _painter = _CirclePainter(color, radius);
#override
BoxPainter createBoxPainter([onChanged()?]) => _painter;
}
class _CirclePainter extends BoxPainter {
final Paint _paint;
final double radius;
_CirclePainter(Color color, this.radius)
: _paint = Paint()
..color = color
..isAntiAlias = true;
#override
void paint(Canvas canvas, Offset offset, ImageConfiguration cfg) {
final Offset circleOffset =
offset + Offset(cfg.size!.width / 2, cfg.size!.height - radius);
canvas.drawCircle(circleOffset, radius, _paint);
}
}
class WidgetSize extends StatefulWidget {
final Widget child;
final Function onChange;
const WidgetSize({
required this.onChange,
required this.child,
});
#override
_WidgetSizeState createState() => _WidgetSizeState();
}
class _WidgetSizeState extends State<WidgetSize> {
#override
Widget build(BuildContext context) {
SchedulerBinding.instance.addPostFrameCallback(postFrameCallback);
return Container(
key: widgetKey,
child: widget.child,
);
}
var widgetKey = GlobalKey();
var oldSize;
void postFrameCallback(_) {
var context = widgetKey.currentContext;
if (context == null) return;
var newSize = context.size;
if (oldSize == newSize) return;
oldSize = newSize;
widget.onChange(newSize);
}
}
ALL VERIFIED USERS
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutterweb/controllers/channel_controller.dart';
import 'package:flutterweb/controllers/user_controller.dart';
import 'package:flutterweb/main.dart';
import 'package:flutterweb/models/user_model.dart';
import 'package:flutterweb/thirdParty/googleSignin.dart';
import 'package:flutterweb/utils/functions.dart';
import 'package:flutterweb/views/menu/permissions/menu_assign.dart';
import 'package:flutterweb/widgets/builders/kPopups.dart';
import 'package:flutterweb/widgets/buttons/KIconOnlyButton.dart';
import 'package:flutterweb/widgets/builders/KNetworkFadeImage.dart';
import 'package:flutterweb/widgets/builders/customAppBar.dart';
import 'package:flutterweb/widgets/buttons/KButton.dart';
import 'package:flutterweb/constants.dart';
import 'package:flutterweb/widgets/cards/KStudentInfoCard.dart';
import 'package:flutterweb/widgets/cards/kStudentCard.dart';
import 'package:flutterweb/widgets/input/KInputBar.dart';
import 'package:flutterweb/widgets/input/KTextField.dart';
import 'package:flutterweb/widgets/static/kLabel.dart';
import 'package:flutterweb/widgets/text/kInfo.dart';
import 'package:get/get.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
class AllVerifiedUsers extends StatefulWidget {
static const String router = "/allVerifiedUsers";
AllVerifiedUsers({Key? key}) : super(key: key);
#override
_MyPageState createState() => _MyPageState();
}
// The controller for the ListView
late ScrollController _controller;
class _MyPageState extends State<AllVerifiedUsers> {
// The controller for the ListView
late ScrollController _controllerTest;
int _page = 1;
final int _limit = 20;
bool _hasNextPage = true;
bool _isFirstLoadRunning = false;
bool _isLoadMoreRunning = false;
List<UserModel> _posts = [];
String searchVal = "";
void _firstLoad() async {
setState(() {
_isFirstLoadRunning = true;
});
try {
List<UserModel> lUserMode = await ChannelController.to
.fetchUsersChannels(_limit, _page, searchVal);
setState(() {
_posts = lUserMode;
});
} catch (err) {
kPrint('Something went wrong');
}
setState(() {
_isFirstLoadRunning = false;
});
}
void _loadMore() async {
if (_isFirstLoadRunning == false &&
_isLoadMoreRunning == false &&
_controller.position.extentAfter < 300) {
setState(() {
_isLoadMoreRunning = true; // Display a progress indicator at the bottom
});
_page += 1; // Increase _page by 1
try {
List<UserModel> lUserMode = await ChannelController.to
.fetchUsersChannels(_limit, _page, searchVal);
if (lUserMode.isNotEmpty) {
setState(() {
_hasNextPage = true;
_posts.addAll(lUserMode);
});
} else {
// This means there is no more data
// and therefore, we will not send another GET request
setState(() {
_hasNextPage = false;
});
}
} catch (err) {
print('Something went wrong!');
}
setState(() {
_isLoadMoreRunning = false;
});
}
}
#override
void initState() {
super.initState();
_firstLoad();
_controller = ScrollController()..addListener(_loadMore);
_controllerTest = ScrollController()
..addListener(() => {kPrint("CustomView Scroll")});
}
#override
void dispose() {
super.dispose();
_controller.removeListener(_loadMore);
}
#override
Widget build(BuildContext context) {
global_title = "All Verified Users";
return CustomAppBar(
title: "All Verified Users",
scrollController: _controllerTest,
childrenFixed: [
kAddSpace(2),
CustomInputBar(
inverse: true,
title: "Search",
onChanged: (String value) {
if (value == "") {
_firstLoad();
return;
}
setState(() {
searchVal = value;
_posts = [];
});
_loadMore();
},
),
],
children: [
kAddSpace(2),
KLabel(
label: "Choose Verified User",
),
kAddSpace(2),
_isFirstLoadRunning
? const Center(
child: CircularProgressIndicator(),
)
: Column(
children: [
SizedBox(
height: app_content_height,
// width: Get.width,
child: ListView.builder(
shrinkWrap: true,
controller: _controller,
itemCount: _posts.length,
itemBuilder: (_, index) {
UserModel item = _posts[index];
return KStudentCard(
imgUrl: "",
onPressed: () {
ChannelController.to.gSelectedMenuUserModel.value =
item;
Get.toNamed(MenuAssign.router);
},
name: "${item.name} ${item.surname}",
);
},
),
),
// when the _loadMore function is running
if (_isLoadMoreRunning == true)
const Padding(
padding: EdgeInsets.only(top: 10, bottom: 40),
child: Center(
child: CircularProgressIndicator(),
),
),
// When nothing else to load
if (_hasNextPage == false)
Container(
padding: const EdgeInsets.only(top: 30, bottom: 40),
color: Colors.amber,
child: const Center(
child: Text('You have fetched all of the content'),
),
),
],
),
kAddSpace(2),
],
);
}
}
Thank you
As I understand it, the idea is a fixed TextArea and a scrollable List beneath. A solution that works without computing any height would be:
final items = List<String>.generate(1000, (i) => 'Item $i');
Widget build(BuildContext context) {
return Scaffold(
body: Column(children: [
//
// TEXTBOX
//
Container(
color: Colors.red,
child: Padding(
padding: const EdgeInsets.all(16),
child: TextFormField(
// controller: controller,
))),
const SizedBox(height: 16),
//
// LIST
//
Expanded(
child: ListView.builder(
shrinkWrap: true, // IMPORTANT
itemCount: items.length,
itemBuilder: (context, index) {
return Text(items[index]);
})),
]),
);
}

How to 'setState' only to the appBar icons instead of 'notifyListeners'

I have an appBar with one icon, this icon has a number which have to be updated after I change somethings in the app. I was using notifyListeners(), but this command is cleaning a list I need so I have to update that number in appbar without notifyListeners().
I tried to call SetState but it doesn't worked.. is there a way to update only the app bar?
In provider which I include more items:
void setBadge() {
_number = number;
notifyListeners(); // this line I dropped out
}
App bar Icon widget:
class AppBarWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<Cart>(
child: IconButton(
icon: Icon(Icons.shopping_bag_outlined),
onPressed: () async {
Navigator.of(context).pushNamed(ROUTE_CART);
},
),
builder: (_, cart, child) {
return BagBadge(
child: child,
value: cart.isEmpty ? '' : cart.number.toString(),
);
},
);
}
}
BagBadge:
class BagBadge extends StatelessWidget {
final Widget child;
final String value;
BagBadge({
#required this.child,
#required this.value,
});
#override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.center,
children: <Widget>[
child,
if (value != '')
Positioned(
right: value.length < 4 ? 20 : 10,
top: 30,
child: Container(
padding: EdgeInsets.all(value.length < 4 ? 2 : 3),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Theme.of(context).accentColor
),
constraints: BoxConstraints(
minHeight: 16,
minWidth: 16,
),
child: Text(
value,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
)
],
);
}
}
Edit: this would work only if you use a stateful widget. With stateless widget the change won't be shown.
You can try something like this:
import 'package:flutter/material.dart';
class AppBarWidget extends StatefulWidget {
#override
_AppBarWidgetState createState() => _AppBarWidgetState();
}
class _AppBarWidgetState extends State<AppBarWidget> {
int _appBarValue = 0;
#override
Widget build(BuildContext context) {
return Consumer<Cart>(
child: IconButton(
icon: Icon(Icons.shopping_bag_outlined),
onPressed: () async {
Navigator.of(context).pushNamed(ROUTE_CART);
},
),
builder: (_, cart, child) {
return BagBadge(
child: child,
value: _appBarValue == 0 ? '' : '$appBarValue',
);
},
);
}
}
setAppBarValue(int value) {
setState(() { _appBarValue = value; });
}
}
Whenever you want to change the value, just call the setAppBarValue() function.

Flutter remove OverlayEntry if touch outside

I have a CustomDropDown, done with a OverlayEntry. The problem is that I have a StatefulWidget for that, which I place in my Screen simply like that:
CustomDropDownButton(
buttonLabel: 'Aus Vorauswahl wählen',
options: [
'1',
'2',
'3',
'4',
],
),
Now inside that CustomDropDownButton I can simply call floatingDropdown.remove(); where ever I want but how can I call that from a Parent-Widget?? I hope you understand my problem. Right now the only way to remove the overlay is by pressing the DropDownButton again, but it should be removed everytime the user taps outside the actual overlay.
I am quite lost here so happy for every help! Let me know if you need any more details!
This is the code for my CustomDropDownButton if that helps:
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../../constants/styles/colors.dart';
import '../../../constants/styles/text_styles.dart';
import '../../../services/size_service.dart';
import 'drop_down.dart';
class CustomDropDownButton extends StatefulWidget {
String buttonLabel;
final List<String> options;
CustomDropDownButton({
required this.buttonLabel,
required this.options,
});
#override
_CustomDropdownState createState() => _CustomDropdownState();
}
class _CustomDropdownState extends State<CustomDropDownButton> {
late GlobalKey actionKey;
late double height, width, xPosition, yPosition;
bool _isDropdownOpened = false;
int _selectedIndex = -1;
late OverlayEntry floatingDropdown;
#override
void initState() {
actionKey = LabeledGlobalKey(widget.buttonLabel);
super.initState();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
key: actionKey,
onTap: () {
setState(() {
if (_isDropdownOpened) {
floatingDropdown.remove();
} else {
findDropdownData();
floatingDropdown = _createFloatingDropdown();
Overlay.of(context)!.insert(floatingDropdown);
}
_isDropdownOpened = !_isDropdownOpened;
});
},
child: Container(
height: scaleWidth(50),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(width: 1.0, color: AppColors.black),
),
color: AppColors.white,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: scaleWidth(10),
),
Text(
widget.buttonLabel,
style: AppTextStyles.h5Light,
),
Spacer(),
_isDropdownOpened
? SvgPicture.asset(
'images/icons/arrow_down_primary.svg',
width: scaleWidth(21),
)
: SvgPicture.asset(
'images/icons/arrow_up_primary.svg',
width: scaleWidth(21),
),
SizedBox(
width: scaleWidth(10),
),
],
),
),
);
}
void findDropdownData() {
RenderBox renderBox =
actionKey.currentContext!.findRenderObject()! as RenderBox;
height = renderBox.size.height;
width = renderBox.size.width;
Offset? offset = renderBox.localToGlobal(Offset.zero);
xPosition = offset.dx;
yPosition = offset.dy;
}
OverlayEntry _createFloatingDropdown() {
return OverlayEntry(builder: (context) {
return Positioned(
left: xPosition,
width: width,
top: yPosition + height,
height: widget.options.length * height + scaleWidth(5),
child: DropDown(
itemHeight: height,
options: widget.options,
onOptionTap: (selectedIndex) {
setState(() {
widget.buttonLabel = widget.options[selectedIndex];
_selectedIndex = selectedIndex;
floatingDropdown.remove();
_isDropdownOpened = !_isDropdownOpened;
});
},
selectedIndex: _selectedIndex,
),
);
});
}
}
1. Return a ListView instead GestureDetector
2. Under Listview use that GestureDetector containing DropDown as one of the children.
3. Add another children(widgets) as GestureDetector and set onTap of each one as:
GestureDetector(
onTap: () {
if(isDropdownOpened){
floatingDropDown!.remove();
isDropdownOpened = false;
}
},
child: Container(
height: 200,
color: Colors.black,
),
)
In short you have to add GestureDetector to the part wherever you want the tapping should close overlay entry
** Full Code **
//This is to close overlay when you navigate to another screen
#override
void dispose() {
// TODO: implement dispose
floatingDropDown!.remove();
super.dispose();
}
Widget build(BuildContext context) {
return ListView(
children: [
Padding(
padding: EdgeInsets.all(20),
child: GestureDetector(
key: _actionKey,
onTap: () {
setState(() {
if (isDropdownOpened) {
floatingDropDown!.remove();
} else {
findDropDownData();
floatingDropDown = _createFloatingDropDown();
Overlay.of(context)!.insert(floatingDropDown!);
}
isDropdownOpened = !isDropdownOpened;
});
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), color: Colors.orangeAccent),
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row(
children: <Widget>[
Text(
widget.text,
style: TextStyle(color: Colors.white, fontSize: 20),
),
Spacer(),
Icon(
Icons.arrow_drop_down,
color: Colors.white,
),
],
),
),
),
),
GestureDetector(
onTap: () {
if(isDropdownOpened){
floatingDropDown!.remove();
isDropdownOpened = false;
}
},
child: Container(
height: 200,
color: Colors.black,
),
)
],
);
}
Let me know whether it helped or not
Listen to full screen onTapDown gesture and navigation event.
The screen' s gesture event:
#override
Widget build(BuildContext context) {
return RawGestureDetector(
gestures: {
PenetrableTapRecognizer: GestureRecognizerFactoryWithHandlers<PenetrableTapRecognizer>(
() => PenetrableTapRecognizer(),
(instance) {
instance.onTapDown = (_) => _handleGlobalGesture();
},
),
},
behavior: HitTestBehavior.opaque,
child: Scaffold(
),
);
}
void _handleGlobalGesture {
// insert or remove the popup menu
// a bool flag maybe helpful
}
class PenetrableTapRecognizer extends TapGestureRecognizer {
#override
void rejectGesture(int pointer) {
acceptGesture(pointer);
}
}

Flutter - Text undo and redo button

hi i have been searching the internet for how to create redo and undo buttons and connect them to a flutter TextField, but so far i haven't found anything. I hope someone knows how to do this, I hope for your help.
You may have a look at undo or replay_bloc packages.
Or, you may just try to implement the feature in your own project and fine-tune it to your specific requirements.
Here is a draft implementation of such a feature.
It supports undo, redo and reset.
I used the following packages:
Flutter Hooks, as an alternative to StatefulWidgets
Hooks Riverpod, for State Management
Freezed, for immutability
Easy Debounce, to denounce the history of changes
You'll find the full source code at the end of this post. but, here are a few important highlights:
Structure of the solution:
App
A MaterialApp encapsulated inside a Riverpod ProviderScope
HomePage
A HookWidget maintaining the global state: uid of the selected quote and editing, whether or not we display the Form.
QuoteView
Very basic display of the selected Quote.
QuoteForm
This form is used to modify the selected quote. Before (re)building the form, we check if the quote was changed (this happens after a undo/reset/redo) and if so, we reset the values (and cursor position) of the fields that changed.
UndoRedoResetWidget
This Widget provides three buttons to trigger undo / reset and redo on our `pendingQuoteProvider. The undo and redo buttons also display the number of undo and redo available.
pendingQuoteProvider
This is a family StateNotifierProvider (check here for more info on family providers), it makes it easy and simple to track changes per quote. It even keeps the tracked changes even when you navigate from one quote to other quotes and back. You will also see that, inside our PendingQuoteNotifier, I debounce the changes for 500 milliseconds to decrease the number of states in the quote history.
PendingQuoteModel
This is the State Model of our pendingQuoteProvider. It's made of a List<Quote> history as well as an index for the current position in history.
Quote
Basic class for our Quotes, made of uid, text, author, and year.
Full source code
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:easy_debounce/easy_debounce.dart';
part '66288827.undo_redo.freezed.dart';
// APP
void main() {
runApp(
ProviderScope(
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Undo/Reset/Redo Demo',
home: HomePage(),
),
),
);
}
// HOMEPAGE
class HomePage extends HookWidget {
#override
Widget build(BuildContext context) {
final selected = useState(quotes.keys.first);
final editing = useState(false);
return Scaffold(
body: SingleChildScrollView(
child: Container(
padding: EdgeInsets.all(16.0),
alignment: Alignment.center,
child: Column(
children: [
Wrap(
children: quotes.keys
.map((uid) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4.0,
vertical: 2.0,
),
child: ChoiceChip(
label: Text(uid),
selected: selected.value == uid,
onSelected: (_) => selected.value = uid,
),
))
.toList(),
),
const Divider(),
ConstrainedBox(
constraints: BoxConstraints(maxWidth: 250),
child: QuoteView(uid: selected.value),
),
const Divider(),
if (editing.value)
ConstrainedBox(
constraints: BoxConstraints(maxWidth: 250),
child: QuoteForm(uid: selected.value),
),
const SizedBox(height: 16.0),
ElevatedButton(
onPressed: () => editing.value = !editing.value,
child: Text(editing.value ? 'CLOSE' : 'EDIT'),
)
],
),
),
),
);
}
}
// VIEW
class QuoteView extends StatelessWidget {
final String uid;
const QuoteView({Key key, this.uid}) : super(key: key);
#override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('“${quotes[uid].text}”', textAlign: TextAlign.left),
Text(quotes[uid].author, textAlign: TextAlign.right),
Text(quotes[uid].year, textAlign: TextAlign.right),
],
);
}
}
// FORM
class QuoteForm extends HookWidget {
final String uid;
const QuoteForm({Key key, this.uid}) : super(key: key);
#override
Widget build(BuildContext context) {
final quote = useProvider(
pendingQuoteProvider(uid).state.select((state) => state.current));
final quoteController = useTextEditingController();
final authorController = useTextEditingController();
final yearController = useTextEditingController();
useEffect(() {
if (quoteController.text != quote.text) {
quoteController.text = quote.text;
quoteController.selection =
TextSelection.collapsed(offset: quote.text.length);
}
if (authorController.text != quote.author) {
authorController.text = quote.author;
authorController.selection =
TextSelection.collapsed(offset: quote.author.length);
}
if (yearController.text != quote.year) {
yearController.text = quote.year;
yearController.selection =
TextSelection.collapsed(offset: quote.year.length);
}
return;
}, [quote]);
return Form(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
UndoRedoResetWidget(uid: uid),
TextFormField(
decoration: InputDecoration(
labelText: 'Quote',
),
controller: quoteController,
keyboardType: TextInputType.multiline,
maxLines: null,
onChanged: (value) =>
context.read(pendingQuoteProvider(uid)).updateText(value),
),
TextFormField(
decoration: InputDecoration(
labelText: 'Author',
),
controller: authorController,
onChanged: (value) =>
context.read(pendingQuoteProvider(uid)).updateAuthor(value),
),
TextFormField(
decoration: InputDecoration(
labelText: 'Year',
),
controller: yearController,
onChanged: (value) =>
context.read(pendingQuoteProvider(uid)).updateYear(value),
),
],
),
);
}
}
// UNDO / RESET / REDO
class UndoRedoResetWidget extends HookWidget {
final String uid;
const UndoRedoResetWidget({Key key, this.uid}) : super(key: key);
#override
Widget build(BuildContext context) {
final pendingQuote = useProvider(pendingQuoteProvider(uid).state);
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
_Button(
iconData: Icons.undo,
info: pendingQuote.hasUndo ? pendingQuote.nbUndo.toString() : '',
disabled: !pendingQuote.hasUndo,
alignment: Alignment.bottomLeft,
onPressed: () => context.read(pendingQuoteProvider(uid)).undo(),
),
_Button(
iconData: Icons.refresh,
disabled: !pendingQuote.hasUndo,
onPressed: () => context.read(pendingQuoteProvider(uid)).reset(),
),
_Button(
iconData: Icons.redo,
info: pendingQuote.hasRedo ? pendingQuote.nbRedo.toString() : '',
disabled: !pendingQuote.hasRedo,
alignment: Alignment.bottomRight,
onPressed: () => context.read(pendingQuoteProvider(uid)).redo(),
),
],
);
}
}
class _Button extends StatelessWidget {
final IconData iconData;
final String info;
final Alignment alignment;
final bool disabled;
final VoidCallback onPressed;
const _Button({
Key key,
this.iconData,
this.info = '',
this.alignment = Alignment.center,
this.disabled = false,
this.onPressed,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onPressed,
child: Stack(
children: [
Container(
width: 24 + alignment.x.abs() * 6,
height: 24,
decoration: BoxDecoration(
color: Colors.black12,
border: Border.all(
color: Colors.black54, // red as border color
),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(alignment.x == -1 ? 10.0 : 0.0),
topRight: Radius.circular(alignment.x == 1 ? 10.0 : 0.0),
bottomRight: Radius.circular(alignment.x == 1 ? 10.0 : 0.0),
bottomLeft: Radius.circular(alignment.x == -1 ? 10.0 : 0.0),
),
),
),
Positioned.fill(
child: Align(
alignment: Alignment(alignment.x * -.5, 0),
child: Icon(
iconData,
size: 12,
color: disabled ? Colors.black38 : Colors.lightBlue,
),
),
),
Positioned.fill(
child: Align(
alignment: Alignment(alignment.x * .4, .8),
child: Text(
info,
style: TextStyle(fontSize: 6, color: Colors.black87),
),
),
),
],
),
).showCursorOnHover(
disabled ? SystemMouseCursors.basic : SystemMouseCursors.click);
}
}
// PROVIDERS
final pendingQuoteProvider =
StateNotifierProvider.family<PendingQuoteNotifier, String>(
(ref, uid) => PendingQuoteNotifier(quotes[uid]));
class PendingQuoteNotifier extends StateNotifier<PendingQuoteModel> {
PendingQuoteNotifier(Quote initialValue)
: super(PendingQuoteModel().afterUpdate(initialValue));
void updateText(String value) {
EasyDebounce.debounce('quote_${state.current.uid}_text', kDebounceDuration,
() {
state = state.afterUpdate(state.current.copyWith(text: value));
});
}
void updateAuthor(String value) {
EasyDebounce.debounce(
'quote_${state.current.uid}_author', kDebounceDuration, () {
state = state.afterUpdate(state.current.copyWith(author: value));
});
}
void updateYear(String value) {
EasyDebounce.debounce('quote_${state.current.uid}_year', kDebounceDuration,
() {
state = state.afterUpdate(state.current.copyWith(year: value));
});
}
void undo() => state = state.afterUndo();
void reset() => state = state.afterReset();
void redo() => state = state.afterRedo();
}
// MODELS
#freezed
abstract class Quote with _$Quote {
const factory Quote({String uid, String author, String text, String year}) =
_Quote;
}
#freezed
abstract class PendingQuoteModel implements _$PendingQuoteModel {
factory PendingQuoteModel({
#Default(-1) int index,
#Default([]) List<Quote> history,
}) = _PendingModel;
const PendingQuoteModel._();
Quote get current => index >= 0 ? history[index] : null;
bool get hasUndo => index > 0;
bool get hasRedo => index < history.length - 1;
int get nbUndo => index;
int get nbRedo => history.isEmpty ? 0 : history.length - index - 1;
PendingQuoteModel afterUndo() => hasUndo ? copyWith(index: index - 1) : this;
PendingQuoteModel afterReset() => hasUndo ? copyWith(index: 0) : this;
PendingQuoteModel afterRedo() => hasRedo ? copyWith(index: index + 1) : this;
PendingQuoteModel afterUpdate(Quote newValue) => newValue != current
? copyWith(
history: [...history.sublist(0, index + 1), newValue],
index: index + 1)
: this;
}
// EXTENSIONS
extension HoverExtensions on Widget {
Widget showCursorOnHover(
[SystemMouseCursor cursor = SystemMouseCursors.click]) {
return MouseRegion(cursor: cursor, child: this);
}
}
// CONFIG
const kDebounceDuration = Duration(milliseconds: 500);
// DATA
final quotes = {
'q_5374': Quote(
uid: 'q_5374',
text: 'Always pass on what you have learned.',
author: 'Minch Yoda',
year: '3 ABY',
),
'q_9534': Quote(
uid: 'q_9534',
text: "It’s a trap!",
author: 'Admiral Ackbar',
year: "2 BBY",
),
'q_9943': Quote(
uid: 'q_9943',
text: "It’s not my fault.",
author: 'Han Solo',
year: '7 BBY',
),
};