I want to create a custom widget in which when I click a button,
it expands to a widget something like this [ - 0 + ]
if the user taps '-' or '+', the button should remain expanded and should decrement or increment the value for the respective taps. The current value should be reflected.
if the user does not tap anything after expansion, the widget should collapse to its previous state after some x duration (say 2 seconds) from the previous tap.
Here's is my code
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatefulWidget {
#override
MyWidgetState createState() => MyWidgetState();
}
class MyWidgetState extends State<MyWidget> {
bool isExpanded = false;
int quantity = 0;
int duration = 3;
#override
Widget build(BuildContext context) {
return !isExpanded
? ElevatedButton(
child: Text('Expand (current: $quantity)'),
onPressed: expansionHandler)
: Card(
elevation: 5.0,
child: Container(
color: Colors.blue,
width: 200,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
icon: const Icon(Icons.remove),
onPressed: decrementValue),
Text('$quantity'),
IconButton(
icon: const Icon(Icons.add),
onPressed: incrementValue),
])));
}
void incrementValue() {
setState(() {
duration += 3;
quantity++;
});
}
void decrementValue() {
setState(() {
duration += 3;
quantity--;
quantity = quantity < 0 ? 0 : quantity;
});
}
void expansionHandler() async {
setState(() {
isExpanded = true;
});
await Future.delayed(Duration(seconds: duration+3)/*not sure if this can work or not*/, () {
setState(() {
isExpanded = false;
duration = 2;
});
});
}
}
DartPad Link: https://dartpad.dev/?id=c22ea14c649cc2858f6d09270a62a0d5&null_safety=true
Test this Widget. I comment down the duration changes for fast testing.
I think the problem with expansionHandler with Future that it is activated when you click the 1st time. On second and next click, it generates a new instance instead of replacing the current one.
According to you question, we to check the current state of Timer, where 'Future.delay' can't handle in this case.
Hope this is what you need.
class MyWidget extends StatefulWidget {
#override
MyWidgetState createState() => MyWidgetState();
}
class MyWidgetState extends State<MyWidget> {
bool isExpanded = false;
int quantity = 0;
int duration = 3;
Timer? timer;
setUpTimer() async {
print("startTimer");
setState(() {
isExpanded = true;
});
timer = await Timer.periodic(Duration(seconds: duration), (timer) {
setState(() {
isExpanded = false;
duration = 2;
});
timer.cancel();
print("done with time $duration");
});
}
_reset() {
if (timer != null && timer!.isActive) timer!.cancel();
setUpTimer();
}
#override
void dispose() {
if (timer != null && timer!.isActive) timer!.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return !isExpanded
? ElevatedButton(
child: Text('Expand (current: $quantity)'),
onPressed: _reset,
)
: Card(
elevation: 5.0,
child: Container(
color: Colors.blue,
width: 200,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
icon: const Icon(Icons.remove),
onPressed: decrementValue),
Text('$quantity'),
IconButton(
icon: const Icon(Icons.add),
onPressed: incrementValue,
),
],
),
),
);
}
void incrementValue() {
setState(() {
// duration += 3;
///for fast testing
quantity++;
});
_reset();
}
void decrementValue() {
setState(() {
// duration += 3;
quantity--;
quantity = quantity < 0 ? 0 : quantity;
});
}
}
Related
I'm new to flutter and very new to riverpod. I've just been helped with some code to use a countdown clock that can then be viewed on multiple pages using Riverpod.
here is the Riverpod State Notifier.
final countDownControllerProvider = StateNotifierProvider.family
.autoDispose<CountdownController, Duration, Duration>(
(ref, initialDuration) {
return CountdownController(initialDuration);
});
class CountdownController extends StateNotifier<Duration> {
Timer? timer;
final Duration initialDuration;
CountdownController(this.initialDuration) : super(initialDuration) {
stopTimer();
}
void startTimer() {
timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (state == Duration.zero) {
timer.cancel();
} else {
if (mounted) {
state = state - const Duration(seconds: 1);
} else {
timer.cancel();
}
}
});
}
}
Currently, the input for the time to display on the countdown clock is inputted when you call CountdownController. (the class with startTimer function inside it).
the problem I'm having is if I want to call startTimer(), I need to reinput the time to display which is a problem if I'm stopping and starting the clock.
how would I move the time input from a parameter of the CountdownController class, into a function inside the class that I can then call on when needed so I don't have to set it when starting/stopping the clock?
and what would that code look like?
thanks so much
I didn't test it. If you need to save duration to state, consider making the state a data class.
EDIT: tested.
import 'dart:math';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/material.dart';
import 'dart:async';
void main() {
runApp(const ProviderScope(child: App()));
}
final countDownControllerProvider =
StateNotifierProvider.autoDispose<CountdownController, Timer?>((ref) {
return CountdownController(ref);
});
final counterProvider = StateProvider((_) => 0);
final intervalProvider = StateProvider((_) => Duration(seconds: 1));
class CountdownController extends StateNotifier<Timer?> {
CountdownController(this.ref) : super(null);
final Ref ref;
void startTimer() {
state?.cancel();
state = Timer.periodic(ref.read(intervalProvider), (timer) {
ref.read(counterProvider.notifier).state++;
});
}
void stopTimer() {
state?.cancel();
}
void accelerate(double multiplier) {
final duration = ref.read(intervalProvider);
ref.read(intervalProvider.notifier).state = Duration(
milliseconds: (duration.inMilliseconds * (1 / multiplier)).floor(),
);
startTimer();
}
void speedUp() {
accelerate(sqrt2);
}
void speedDown() {
accelerate(1 / sqrt2);
}
}
class App extends ConsumerWidget {
const App();
#override
Widget build(BuildContext context, WidgetRef ref) {
final counter = ref.watch(counterProvider);
final controller = ref.watch(countDownControllerProvider.notifier);
final timer = ref.watch(countDownControllerProvider);
return MaterialApp(
home: Scaffold(
body: SafeArea(
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Text("$counter", style: Theme.of(context).textTheme.headlineLarge),
SizedBox(height: 24),
Row(children: [
SizedBox(width: 24),
Expanded(
child: ElevatedButton(
onPressed: controller.startTimer,
child: Text(timer == null ? "start" : "stop"),
),
),
SizedBox(width: 24),
Expanded(
child: ElevatedButton(
onPressed: controller.speedUp,
child: Text("+"),
),
),
SizedBox(width: 24),
Expanded(
child: ElevatedButton(
onPressed: controller.speedDown,
child: Text("-"),
),
),
SizedBox(width: 24),
]),
]),
),
),
);
}
}
I wrote a code like this:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_sms_inbox/flutter_sms_inbox.dart';
class Inbox extends StatefulWidget {
const Inbox({Key? key}) : super(key: key);
#override
State<Inbox> createState() => _InboxState();
}
SmsQuery query = new SmsQuery();
List<SmsMessage> AllMessages = [];
Timer? _timer;
class _InboxState extends State<Inbox> {
#override
void initState() {
GetAllMessages();
_timer = Timer.periodic(const Duration(seconds: 1), (Timer t) {
GetAllMessages();
print("refreshed");
print(AllMessages); // output: []
});
super.initState();
}
Future<void> GetAllMessages() async {
Future.delayed(Duration.zero, () async {
List<SmsMessage> messages = await query.querySms(
kinds: [SmsQueryKind.inbox],
count: 50,
);
setState(() {
AllMessages = messages;
});
});
}
#override
void dispose() {
_timer?.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Page(),
),
);
}
}
Widget Page() {
if (AllMessages == null) {
return Text("No message");
} else {
Column(
children: [
const SizedBox(height: 25),
Container(
child: Column(
children: AllMessages.map((msg) {
return Container(
child: Card(
child: ListTile(
leading: Text("${msg.body}"),
title: Text("${msg.address}"),
),
),
);
}
).toList(),
),
),
],
);
}
return SizedBox();
}
I'm trying to access and list SMS's on the phone. I save the SMS in the list called AllMessages. If there is no SMS, I want it to show a Text named No message but it doesn't.
It shows blank as in the picture:
Why could this be? How can I solve the problem? Thanks in advance for your help.
I am currently writing an app using Flutter and Dart. On a button onPressed event I would like to invoke an action that executes after timeLeft seconds unless it is cancelled by correctly entering a pin. Additionally, I would like to use the value timeLeft in a Text widget.
This would require a structure with the following functionality:
executing a function after an x amount of seconds
this function should execute unless some event (e.g. entering a pin correctly) has occurred.
the timeLeft value should be accessible to be used in a Text widget and should update as the timer progresses.
I am wondering how to do this according to flutter's and dart's best practices. For state management I am using the provider pattern so preferably this approach is compatible with the provider pattern.
This is what I have tried so far:
class Home extends ChangeNotifier {
int secondsLeft = 10;
void onPressedEmergencyButton(BuildContext context) {
countDown();
showDialog<void>(
context: context,
builder: (context) {
return ScreenLock(
title: Text(
"Sending message in ${context.read<Home>().secondsLeft} seconds"),
correctString: '1234',
canCancel: false,
didUnlocked: () {
Navigator.pop(context);
},
);
},
);
}
void countDown() {
Future.delayed(const Duration(seconds: 1), () {
secondsLeft =- 1;
notifyListeners();
if (secondsLeft <= 0) {
// Do something
return;
}
});
}
}
You can use CancelableOperation from async package.
Simplifying code-snippet and about _cancelTimer(bool) , this bool used to tell widget about true = time end, and on cancel false like _cancelTimer(false);, rest are described on code-comments.
class TS extends StatefulWidget {
const TS({Key? key}) : super(key: key);
#override
State<TS> createState() => _TSState();
}
class _TSState extends State<TS> {
Timer? _timer;
final Duration _refreseRate = const Duration(seconds: 1);
CancelableOperation? _cancelableOperation;
Duration taskDuration = const Duration(seconds: 5);
bool isSuccess = false;
_initTimer() {
if (_cancelableOperation != null) {
_cancelTimer(false);
}
_cancelableOperation = CancelableOperation.fromFuture(
Future.delayed(Duration.zero),
).then((p0) {
_timer = Timer.periodic(_refreseRate, (timer) {
setState(() {
taskDuration -= _refreseRate;
});
if (taskDuration <= Duration.zero) {
/// task complete on end of duration
_cancelTimer(true);
}
});
}, onCancel: () {
_timer?.cancel();
setState(() {});
});
}
_cancelTimer(bool eofT) {
// cancel and reset everything
_cancelableOperation?.cancel();
_timer?.cancel();
_timer = null;
taskDuration = const Duration(seconds: 5);
isSuccess = eofT;
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (isSuccess)
Container(
height: 100,
width: 100,
color: Colors.green,
),
if (_timer != null)
Text("${taskDuration.inSeconds}")
else
const Text("init Timer"),
],
),
),
floatingActionButton: Row(
mainAxisSize: MainAxisSize.min,
children: [
FloatingActionButton(
child: const Text("init"),
onPressed: () {
_initTimer();
},
),
FloatingActionButton(
child: const Text("Cancel"),
onPressed: () {
_cancelTimer(false);
},
),
],
),
);
}
}
You can use the Timer class to run a function after a set Duration. It doesn't give you the time remaining, but you can calculate it yourself.
Here is a quick implementation I put together:
import 'dart:async';
import 'package:flutter/material.dart';
void main() async {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.light(),
home: const Scaffold(
body: Center(
child: Countdown(),
),
),
);
}
}
class Countdown extends StatefulWidget {
const Countdown({Key? key}) : super(key: key);
#override
_CountdownState createState() => _CountdownState();
}
class _CountdownState extends State<Countdown> {
bool active = false;
Timer? timer;
Timer? refresh;
Stopwatch stopwatch = Stopwatch();
Duration duration = const Duration(seconds: 5);
_CountdownState() {
// this is just so the time remaining text is updated
refresh = Timer.periodic(
const Duration(milliseconds: 100), (_) => setState(() {}));
}
void start() {
setState(() {
active = true;
timer = Timer(duration, () {
stop();
onCountdownComplete();
});
stopwatch
..reset()
..start();
});
}
void stop() {
setState(() {
active = false;
timer?.cancel();
stopwatch.stop();
});
}
void onCountdownComplete() {
showDialog(
context: context,
builder: (context) => const AlertDialog(
title: Text('Countdown was not stopped!'),
),
);
}
int secondsRemaining() {
return duration.inSeconds - stopwatch.elapsed.inSeconds;
}
#override
void dispose() {
timer?.cancel();
refresh?.cancel();
stopwatch.stop();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
if (active) Text(secondsRemaining().toString()),
if (active)
TextButton(onPressed: stop, child: const Text('Stop'))
else
TextButton(onPressed: start, child: const Text('Start')),
],
);
}
}
I gave up trying to find the reason setState() is not running build method. I have a ChoiceChip which has a callback function. Debug showed me that I actually receive the selected chip at the callback but setState() is not updating the ui. I have spent all day trying to understand why setState() is not running the build() method. Here is my code
class SocialStoryCategory extends StatefulWidget {
final Function(String) onMenuItemPress;
SocialStoryCategory({Key key, #required this.onMenuItemPress}) : sup er(key: key);
#override
_SocialStoryCategoryState createState() => _SocialStoryCategoryState();
}
class _SocialStoryCategoryState extends State<SocialStoryCategory> {
int _value = 0;
List<String> categoryList;
#override
Widget build(BuildContext context) {
categoryList = [
NoomeeLocalizations.of(context).trans('All'),
NoomeeLocalizations.of(context).trans('Communication'),
NoomeeLocalizations.of(context).trans('Behavioral'),
NoomeeLocalizations.of(context).trans('ADL'),
NoomeeLocalizations.of(context).trans('Other')
];
return Wrap(
spacing: 4,
children: List<Widget>.generate(5, (int index) {
return Theme(
data: ThemeData.from(
colorScheme: ColorScheme.light(primary: Colors.white)),
child: Container(
child: ChoiceChip(
elevation: 3,
selectedColor: Colors.lightBlueAccent,
label: Text(categoryList.elementAt(index)),
selected: _value == index,
onSelected: (bool selected) {
setState(() {
_value = selected ? index : null;
if (categoryList.elementAt(_value) == "All") {
widget.onMenuItemPress("");
} else {
widget.onMenuItemPress(categoryList.elementAt(_value));
}
});
},
),
),
);
}).toList());
}
}
Here is the place where I get the callback
class SocialStoriesHome extends StatefulWidget {
#override
_SocialStoriesHomeState createState() => _SocialStoriesHomeState();
}
class _SocialStoriesHomeState extends State<SocialStoriesHome>
with TickerProviderStateMixin {
String title;
TabController _tabController;
int _activeTabIndex = 0;
String _defaultStoryCategory;
_goToDetailsPage() {
Navigator.of(context).pushNamed("parent/storyDetails");
}
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
#override
void initState() {
super.initState();
_tabController = TabController(vsync: this, length: 2);
_defaultStoryCategory = '';
}
#override
Widget build(BuildContext context) {
return BaseWidget<SocialStoryViewModel>(
model: new SocialStoryViewModel(
socialStoriesService: Provider.of(context),
),
onModelReady: (model) =>
model.fetchDefaultStoriesByCategory(_defaultStoryCategory),
builder: (context, model, child) => DefaultTabController(
length: 2,
child: Scaffold(
body: model.busy
? Center(child: CircularProgressIndicator())
: Container(
child: Column(
children: <Widget>[
new SocialStoryCategory(
onMenuItemPress: (selection) {
setState(() {
_defaultStoryCategory = selection;
});
},
),
Expanded(
child: ListView(
children: getStories(model.socialStories),
),
),
],
),
),
);
}
}
List<Widget> getStories(List<SocialStoryModel> storyList) {
List<Widget> list = List<Widget>();
for (int i = 0; i < storyList.length; i++) {
list.add(Padding(
padding: const EdgeInsets.all(8.0),
child: Template(
title: storyList[i].name,
subTitle: storyList[i].categories,
hadEditIcon: false),
));
}
return list;
}
Finally I have found the solution.
I have simply replaced
new SocialStoryCategory(onMenuItemPress: (selection) {
setState(() {
_defaultStoryCategory = selection;
});
},
),
to
new SocialStoryCategory(
onMenuItemPress: (selection) {
model.fetchDefaultStoriesByCategory(selection);
},
),
my viewModel extend change notifier and build a child as consumer so I totally understand why it works. But I still do not understand why the previous version was not working. I will feel happy again only when you explain me the problem,
when I click on IconButton() to delete All items from the list movies I can't see that change until I reopen the page again...
anyone know how I could fix
this my infoPage(("class B")):
class InfoPage extends StatefulWidget {
int id;
int pageId;
InfoPage(this.id,this.pageId);
#override
_InfoPageState createState() => _InfoPageState(id,pageId);
}
class _InfoPageState extends State<InfoPage> {
var db = DatabaseHelper();
String title = "";
String about = "";
String rate = "";
String date = "";
int id;
int pageId;
_InfoPageState(this.id,this.pageId);
#override
void initState() {
super.initState();
if(pageId == 1){
_getMovie();
}
}
void _getMovie() async {
Movie thisMovie = await db.getMovie(id);
setState(() {
title = thisMovie.name;
about = thisMovie.description;
rate = thisMovie.rate;
date = thisMovie.date;
});
}
_deleteMovie() async{
await db.deleteMovie(id);
Navigator.pop(context);
setState(() {
CardsListViewState(pageId).deleteAllList();
});
}
#override
Widget build(BuildContext context) {
Navigator.canPop(context);
return Scaffold(
body: Container(
child: Column(
children: <Widget>[
Container(
margin: EdgeInsets.only(bottom: 10),
child:Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Expanded(
child: Container(),
),
Container(
margin: EdgeInsets.only(right: 10),
child: IconButton(
icon: Icon(Icons.delete,color: Color(0xffFC4D4D),),
onPressed: (){
_deleteMovie();
}
)
)
],
),
)
],
),
),
);
}
}
and this my CardsListView(("class A"))
class CardsListView extends StatefulWidget {
int whereComeFrom;
CardsListView(this.whereComeFrom);
#override
CardsListViewState createState() => CardsListViewState(whereComeFrom);
}
class CardsListViewState extends State<CardsListView> {
int whereComeFrom;
CardsListViewState(this.whereComeFrom);
var db = DatabaseHelper();
List mainList = [];
final List<Movie> movies = <Movie>[];
deleteAllList() async{
await db.deleteMovies();
setState(() {
movies.clear();
});
}
#override
void initState() {
super.initState();
_readUnites();
if(whereComeFrom == 1){
mainList = movies;
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body:
GridView.count(
crossAxisCount: 3,
addAutomaticKeepAlives: true,
childAspectRatio: (1/1.5),
children: List.generate(mainList.length, (index){
return CardUnite(mainList[index].name,mainList[index].id,whereComeFrom);
})
),
);
}
You can use a callback function from the parent class supplied to the child class.
Remember that functions are first class objects in Dart.
Just pass in a function that calls setState to the child and have the child call that function.