How to properly play audio file using just_audio - flutter - flutter

Am working on a music app were am suppose to stream audio and also play my locally stored files.
Am using flutter and am not good on it, Actually am learning.
What i wanted is to be helped on how i can play a file properly, The intention is to play a file once provided to the player path. bellow is my code.
import 'package:myApp/shared/MusicProgressIndicator.dart';
import 'package:myApp/just_audio.dart';
import 'package:myApp/app.dart';
class SingleSongScreen extends StatefulWidget {
static const String id = "SingleSongScreen";
static var song = {};
SingleSongScreen({Key key}) : super(key: key);
#override
_SingleSongScreenState createState() => _SingleSongScreenState();
}
class _SingleSongScreenState extends State<SingleSongScreen> with TickerProviderStateMixin {
Duration currentDuration = Duration(milliseconds: 1000);
bool showRemaining = false;
AnimationController controller;
#override
void initState() {
controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 5),
)..addListener(() {
setState(() {});
});
controller.repeat(reverse: true);
super.initState();
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
final player = AudioPlayer();
var song = SingleSongScreen.song;
var songPath = app.base+"play.php?file="+song['song'];
print(song);
print(songPath);
player.stop();
player.setUrl(songPath).then((play){
player.play();
print("Playing now");
});
return Window(
backgroundColor: backgroundColor,
header: Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconicButton(
icon: backIcon,
iconColor: Colors.grey,
transparent: true,
circledIcon: false,
size: 40,
onClicked: () {
goBack(context);
},
),
Favicon(onClicked: () {
goto(page: MainScreen.id, context: context);
}),
],
),
),
body: [
VerticalSpacer(
h: 20,
),
Container(
height: 250,
child: Carousel(
boxFit: BoxFit.contain,
autoplay: carouselAutoPlay,
animationCurve: Curves.fastOutSlowIn,
animationDuration: Duration(milliseconds: 1000),
dotSize: 6.0,
dotIncreasedColor: Color(0xFF33A3FF),
dotBgColor: Colors.transparent,
dotPosition: DotPosition.bottomCenter,
dotVerticalPadding: 10.0,
showIndicator: true,
indicatorBgPadding: 7.0,
images: images(),
),
)
],
footer: Container(
child: Column(
children: [
Container(
child: Marquee(
child: Label(
song['title'],
bold: true,
),
),
),
Container(
child: Marquee(
child: Label(
song['artist'],
bold: true,
color: Colors.grey,
),
),
),
// ACTION ICONS SECTION
VerticalSpacer(
h: 25,
),
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
VerticalIconicButton(
icon: FontAwesomeIcons.fileDownload,
label: "Download",
iconColor: Colors.grey,
textColor: Colors.grey,
scale: 1.2,
onClicked: (){
var downloadURL = songPath.replaceAll("play.php", "download.php");
app app2 = new app();
toast("Downloading a file "+song['song']);
app2.fetchServer({'method':'download','file':song['song']}).then((fileInfo){
if(fileInfo['status']=="success"){
toast("Download complete "+song['song']);
app2.saveDownload(song['song'],song['artist'],fileInfo['file']);
}else{
toast("There was an error while downloading a file");
}
print("downloaded file is");
print(fileInfo);
});
},
),
VerticalIconicButton(
icon: Icons.favorite_border_outlined,
label: "Favourite",
iconColor: Colors.grey,
textColor: Colors.grey,
scale: 1.2,
),
VerticalIconicButton(
icon: FontAwesomeIcons.readme,
label: "Lyrics",
iconColor: Colors.grey,
textColor: Colors.grey,
scale: 1.2,
onClicked: () {
goto(page: LyricsScreen.id, context: context, args: lyric);
},
),
],
),
),
// SONG PLAY PROGRESS INDICATOR
VerticalSpacer(),
MusicProgressIndicator(
progress: currentDuration,
buffered: Duration(milliseconds: 2000),
total: Duration(milliseconds: 5000),
showTotalRemainingTime: showRemaining,
onSeek: (duration) {
setState(() {
currentDuration = duration;
showRemaining = !showRemaining;
});
// _player.seek(duration);
},
),
// MUSIC PLAY CONTROLLER ACTION BUTTON
VerticalSpacer(
h: 20,
),
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconicButton(
icon: Icons.repeat_one,
iconColor: Colors.grey,
transparent: true,
circledIcon: false,
size: 50,
),
Container(
child: Row(
children: [
Container(
child: Center(
child: IconicButton(
icon: Icons.skip_previous_rounded,
iconColor: Colors.white,
transparent: true,
circledIcon: false,
size: 50,
),
),
),
SizedBox(width: 15),
IconicButton(
icon: Icons.pause,
iconColor: Colors.grey,
iconCircleColor: Colors.blue,
size: 50,
onClicked: (){
toast("Playing file");
player.stop();
player.setUrl(songPath).then((play){
player.play();
print("Playing now");
});
},
),
SizedBox(
width: 15,
),
Container(
child: Center(
child: IconicButton(
icon: Icons.skip_next_rounded,
iconColor: Colors.white,
transparent: true,
circledIcon: false,
size: 50,
),
),
),
],
),
),
IconicButton(
icon: Icons.queue_music_outlined,
iconColor: Colors.grey,
transparent: true,
circledIcon: false,
size: 50,
onClicked: () {
goto(page: AllSongsScreen.id, context: context);
},
),
],
),
),
// BOTTTOM SPACE
VerticalSpacer(
h: 30,
),
],
),
),
);
}
void toast(String message){
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(message.toString()),
duration: const Duration(seconds: 1),
action: SnackBarAction(
label: 'OK',
onPressed: () { },
),
)
);
}
}
The code which i have used to play sound, is producing
Annoying sound, something a double sounds or waves crossing
It's allowing playing of multiple sounds.
Note: I purchased the template.
Thanks, Please help.

You have called the setState() method in the addListener of controller and directly called the player.stop() and player.play() methods inside the widget build.
Before calling the stop or play methods, check whether the player is not null and is not playing
if(player.playing){
}
And you should not initialize your player inside the widget build. It is not advisable.

For avoid playing multiple audios simultaneously, maybe you need think about making the AudioPlayer a SINGLETON. The code above show me that every time you enter that page, there will be a new AudioPlayer created associate with and you are not dispose it on dispose(). Here's some code I wrote for my flutter podcast open source project.
a podcast case using just audio package

Related

Can't convert setState to BloC

I am developing an audio player application using Flutter, I am using on_audio_query package to get audio files from device storage, and just_audio package for the audio player.
when I created the project, in the audio player I used setState to handle state management, and now, I want to convert to bloc pattern but when I tried to do that I faced a lot of issues and couldn't fix them.
I will attach the code of the audio player and also basic cubit code and if someone guided me or showed me how to convert I would appreciate it.
I'm sorry if the code is confusing but I don't know how to add it correctly.
audio player code
import 'package:just_audio/just_audio.dart';
import 'package:dorosi/shared/ui/my_icon.dart';
import 'package:dorosi/shared/ui/my_text.dart';
import 'package:flutter/material.dart';
import 'package:on_audio_query/on_audio_query.dart';
class AudioPlayerWithUrl extends StatefulWidget {
final SongModel songModel;
const AudioPlayerWithUrl({required this.songModel, Key? key})
: super(key: key);
#override
State<AudioPlayerWithUrl> createState() => _AudioPlayerWithUrlState();
}
class _AudioPlayerWithUrlState extends State<AudioPlayerWithUrl> {
final audioPlayer = AudioPlayer();
bool isPlaying = false;
Duration duration = const Duration();
Duration position = const Duration();
bool isPressed = false;
#override
void initState() {
super.initState();
setAudio();
playAudio();
}
Future setAudio() async {
audioPlayer.setLoopMode(LoopMode.off);
audioPlayer.setAudioSource(
AudioSource.uri(
Uri.parse(widget.songModel.uri!),
),
);
isPlaying = true;
audioPlayer.durationStream.listen((audioDuration) {
setState(() {
duration = audioDuration!;
});
});
audioPlayer.positionStream.listen((audioPosition) {
setState(() {
position = audioPosition;
});
});
}
#override
void dispose() {
audioPlayer.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const MyText(
writtenText: 'Now Playing',
textSize: 23,
textColor: Colors.black,
),
leading: IconButton(
onPressed: () {
Navigator.pop(context);
},
icon: const MyIcon(
icon: Icons.arrow_back,
iconColor: Colors.black,
)),
centerTitle: true,
backgroundColor: Colors.transparent,
elevation: 0,
actions: [
IconButton(
onPressed: () {},
icon: const MyIcon(
icon: Icons.more_vert,
iconColor: Colors.black,
),
),
const SizedBox(width: 10)
],
),
body: SafeArea(
child: SingleChildScrollView(
child: Column(
children: [
const SizedBox(height: 15),
const CircleAvatar(
backgroundColor: Color(0xFFc61104),
radius: 100,
child: MyIcon(
icon: Icons.music_note,
iconSize: 100,
iconColor: Colors.white,
),
),
const SizedBox(
height: 15,
),
const SizedBox(height: 15),
MyText(
writtenText: widget.songModel.title,
textSize: 24,
textWeight: FontWeight.bold,
),
const SizedBox(height: 4),
MyText(
writtenText: widget.songModel.album!,
textSize: 20,
),
const SizedBox(height: 10),
Slider(
activeColor: Colors.orange,
inactiveColor: Colors.black87,
min: 0,
max: duration.inSeconds.toDouble(),
value: position.inSeconds.toDouble(),
onChanged: (value) async {
isPlaying = true;
final position = Duration(seconds: value.toInt());
await audioPlayer.seek(position);
await audioPlayer.play();
}),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText(
writtenText: formatTime(position),
textWeight: FontWeight.bold),
TextButton(
style: TextButton.styleFrom(primary: Colors.black),
onPressed: () {
setState(() {
showRemainingTime();
isPressed = !isPressed;
});
},
child: isPressed
? showRemainingTime()
: MyText(
writtenText: ' ${formatTime(duration)}',
textWeight: FontWeight.bold,
),
),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircleAvatar(
backgroundColor: const Color(0xFFc61104),
radius: 23,
child: IconButton(
onPressed: () async {
if (position >= const Duration(seconds: 10)) {
seekTo(position.inSeconds - 10);
} else {
setState(() {
seekTo(const Duration(seconds: 0).inSeconds);
isPlaying = false;
});
pauseAudio();
}
},
icon: const MyIcon(
icon: Icons.settings_backup_restore,
iconSize: 30,
iconColor: Colors.white,
)),
),
const SizedBox(width: 40),
CircleAvatar(
backgroundColor: const Color(0xFFc61104),
radius: 35,
child: IconButton(
icon: Icon(
isPlaying ? Icons.pause : Icons.play_arrow,
color: Colors.white,
),
iconSize: 50,
onPressed: () {
setState(() {
if (isPlaying) {
pauseAudio();
} else {
playAudio();
}
isPlaying = !isPlaying;
});
},
),
),
const SizedBox(width: 40),
CircleAvatar(
radius: 23,
backgroundColor: const Color(0xFFc61104),
child: IconButton(
onPressed: () async {
if (position < duration - const Duration(seconds: 10)) {
seekTo(position.inSeconds + 10);
} else {
setState(() {
seekTo(duration.inSeconds);
isPlaying = false;
});
pauseAudio();
}
},
icon: const MyIcon(
icon: Icons.forward_10,
iconSize: 30,
iconColor: Colors.white,
)),
),
],
),
const SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(width: 40),
ElevatedButton(
style: ElevatedButton.styleFrom(primary: Colors.orange),
onPressed: () {
setState(() {
if (audioPlayer.speed == 1) {
adjustAudioSpeed();
debugPrint('${audioPlayer.speed}');
} else if (audioPlayer.speed == 1.25) {
adjustAudioSpeed2();
} else if (audioPlayer.speed == 1.5) {
adjustAudioSpeed3();
} else if (audioPlayer.speed == 1.75) {
adjustAudioSpeed4();
} else if (audioPlayer.speed == 2) {
setAudioNormalSpeed();
}
});
},
child: MyText(
writtenText: '${audioPlayer.speed}',
textSize: 18,
textColor: Colors.black,
)),
const SizedBox(width: 40),
],
)
],
),
)),
);
}
Widget showRemainingTime() {
return MyText(
writtenText: '- ${formatTime(duration - position)}',
textWeight: FontWeight.bold,
);
}
seekTo(int seconds) {
audioPlayer.seek(Duration(seconds: seconds));
}
playAudio() {
audioPlayer.play();
}
pauseAudio() {
audioPlayer.pause();
}
setAudioNormalSpeed() {
audioPlayer.setSpeed(1);
}
adjustAudioSpeed() {
audioPlayer.setSpeed(1.25);
}
adjustAudioSpeed2() {
audioPlayer.setSpeed(1.5);
}
adjustAudioSpeed3() {
audioPlayer.setSpeed(1.75);
}
adjustAudioSpeed4() {
audioPlayer.setSpeed(2);
}
playNextAudio() {
audioPlayer.seekToNext();
}
playPreviousAudio() {
audioPlayer.seekToPrevious();
}
String formatTime(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final hours = twoDigits(duration.inHours);
final minutes = twoDigits(duration.inMinutes.remainder(60));
final seconds = twoDigits(duration.inSeconds.remainder(60));
return [
if (duration.inHours > 0) hours,
minutes,
seconds,
].join(':');
}
}
player cubit code
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:meta/meta.dart';
part 'player_state.dart';
class PlayerCubit extends Cubit<PlayerState> {
static PlayerCubit get(context) => BlocProvider.of(context);
PlayerCubit() : super(PlayerInitialState());
}
player state code
part of 'player_cubit.dart';
#immutable
abstract class PlayerState {}
class PlayerInitialState extends PlayerState {}
class PlayerPlayingState extends PlayerState {}
class PlayerPauseState extends PlayerState {}

How to update state within a custom tab bar with flutter

I'm trying to create a custom TabBar which navigates only forward with a button click.
im using an enum to manage the state and couple of class objects to manage items for Tabs.
What result I expected
VS
What result I've got
The code I worked out is as follows. I am still trying to figure out a way of managing the expected outcome with my code implementation. if anyone can help out will be amazing.
Object classes
class CategoryModel {
final String? title;
final eCategory category;
// final eCategoryState? catState;
CategoryModel({this.title, required this.category});
}
class CategoryStateModel {
final CategoryModel? tItem;
late eTabState? tState;
CategoryStateModel({this.tItem, this.tState});
}
The code portion
class _ContributeTabScreenState extends State<ContributeTabScreen>
with SingleTickerProviderStateMixin {
eButtonState _submitBtnState = eButtonState.bActive;
eButtonState _continueBtnState = eButtonState.bActive;
int pageNo = 0;
TabController? _tabController;
late List<eTabState>? _tabState = [];
late List<CategoryModel>? _categoryModelList = [];
late List<Tab>? _tabList = [];
late List<CategoryStateModel>? _catStateList = [];
#override
void initState() {
_categoryModelList = widget.catList;
_assignTabs();
_tabController = new TabController(
vsync: this,
length: _categoryModelList!.length, //3,
);
super.initState();
// print(_categoryModelList![0].title.toString());
}
List<Tab> _assignTabs() {
for (var item = 0; item < _categoryModelList!.length; item++) {
//
if (item != 0) {
for (var t in _categoryModelList!) {
_catStateList!.add(
new CategoryStateModel(tItem: t, tState: eTabState.tUnSelected));
}
} else {
for (var t in _categoryModelList!) {
_catStateList!.add(
new CategoryStateModel(tItem: t, tState: eTabState.tSelected));
}
}
//
_tabList!.add(
new Tab(
child: _tabItem(_catStateList![item]),
// child: _tabItem(_categoryModelList![item]),
),
);
}
return _tabList!;
}
void _goBack() {
Navigator.of(context).pop();
}
//the onPressed call back I manage the forward move of a tabbar item + tabview
void forwardTabPage() {
if (pageNo >= 0 && pageNo < _categoryModelList!.length) {
setState(() {
// });
// setState(() {
pageNo = pageNo + 1;
// _catStateList![pageNo - 1].tState = eTabState.tCompleted;
// _tabState![pageNo - 1] = _catStateList![pageNo - 1].tState!;
});
_tabController!.animateTo(pageNo);
}
}
...rest of the code
//the Tabbar item
_tabItem(CategoryStateModel item) => Container(
width: 140.0,
alignment: Alignment.center,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
item.tItem!.title!,
style: tBody4.copyWith(color: CustomColors.mDarkBlue),
),
item.tState == _catStateList![pageNo].tState //eTabState.tCompleted
? SizedBox(
width: 8.0,
)
: SizedBox(
width: 0.0,
),
item.tState == _catStateList![pageNo].tState //eTabState.tCompleted
? CircleAvatar(
radius: 12.0,
backgroundColor: CustomColors.green600,
child: Icon(
Icons.check_outlined,
size: 20.0,
),
)
: SizedBox(
width: 0.0,
),
],
),
);
continueBtn() => ButtonWidget(
btnColor: CustomColors.green600,
borderColor: CustomColors.green600,
textColor: CustomColors.mWhite,
text: "Continue",
eButtonType: eButtonType.bText,
eButtonState: _continueBtnState,
onPressed: () {
forwardTabPage();
// _tabState = eTabState.tCompleted;
},
);
submitBtn() => ButtonWidget(
btnColor: CustomColors.green600,
borderColor: CustomColors.green600,
textColor: CustomColors.mWhite,
text: "Submit",
eButtonType: eButtonType.bText,
eButtonState: _submitBtnState,
onPressed: () {},
);
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: _categoryModelList!.length,
child: Scaffold(
appBar: AppBar(
toolbarHeight: 60.0,
leadingWidth: 100.0,
leading: GestureDetector(
onTap: _goBack,
child: Container(
child: Row(
children: [
SizedBox(
width: 16.0,
),
const Icon(
Icons.arrow_back_ios,
color: CustomColors.grey700,
size: 24.0,
),
Flexible(
child: Text(
"Back",
style: tButtonSmall,
),
),
],
),
),
),
backgroundColor: Colors.white,
elevation: 0.0,
bottom: PreferredSize(
preferredSize: Size.fromHeight(50.0),
child: IgnorePointer(
child: TabBar(
controller: _tabController,
isScrollable: false,
enableFeedback: false,
padding: EdgeInsets.only(bottom: 8.0),
indicatorSize: TabBarIndicatorSize.label,
indicator:
// _tabState == eTabState.tSelected
// ?
BoxDecoration(
borderRadius: BorderRadius.circular(40.0),
color: CustomColors.green300,
border: Border.all(color: CustomColors.mGreen, width: 2),
),
tabs: _tabList!
),
),
),
),
body: Container(
child: TabBarView(
controller: _tabController,
physics: NeverScrollableScrollPhysics(),
children: [
Container(
color: CustomColors.grey600.withOpacity(0.2),
child: Center(
child: Text("Home Tab View"),
),
),
Center(
child: Text("Map Tab View"),
),
],
),
),
persistentFooterButtons: [
Container(
width: MediaQuery.of(context).size.width,
// height: 40,
padding: EdgeInsets.symmetric(horizontal: 24.0),
margin: EdgeInsets.symmetric(vertical: 16.0),
child: (_categoryModelList!.length == 1 ||
pageNo == _categoryModelList!.length - 1)
? submitBtn()
: continueBtn(),
)
],
),
);
}
enum classes
enum eCategoryState {
cSelected,
cUnSelected,
cEnded,
}
enum eTabState {
tSelected,
tUnSelected,
tCompleted,
}
enum eCategory {
cAccess,
cAmenities,
}
I don't know if I understand what you mean or not. but try this. TabBar has a isScrollable property. set it to true

How to change indicator manually in flutter introduction screen

I want to know how to move between pages in flutter introduction screen. To clarify the problem, I want to go back a page when skip button is pressed.
So far I have done this:
class _IntroPageState extends State<IntroPage> {
int currentIndex = 0;
void _onIntroEnd(context) {
getIt<IntroLocalDataSource>().setIntroSeenState(true);
Navigator.of(context).pushReplacementNamed(SignInPage.id);
}
#override
Widget build(BuildContext context) {
final strings = Languages.of(context);
final bodyStyle = Theme.of(context)
.textTheme
.subtitle2
?.copyWith(fontSize: k16TextFontSize);
final titleStyle = Theme.of(context)
.textTheme
.headline1
?.copyWith(fontSize: k20TextFontSize);
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Flexible(
child: Container(
child: IntroductionScreen(
onChange: (index) {
setState(() {
currentIndex = index;
});
},
rtl: true,
globalBackgroundColor: Colors.transparent,
showNextButton: false,
rawPages: strings.introScreenTitles.asMap().entries.map((entry) {
int idx = entry.key;
String val = entry.value;
return Center(
child: IntroCardWidget(
index: idx,
title: val,
bodyStyle: bodyStyle,
titleStyle: titleStyle,
image: Assets.introImages[idx],
description: strings.introScreenDescriptions[idx],
));
}).toList(),
showDoneButton: false,
curve: Curves.fastLinearToSlowEaseIn,
showSkipButton: currentIndex != 0,
skip: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
primary: kLightTextColor,
shape: CircleBorder(),
),
onPressed: () {
setState(() {
currentIndex--; // <<--------- This does not work
});
},
child: Icon(
Icons.keyboard_arrow_left_rounded,
color: Theme.of(context).primaryColor,
)),
dotsDecorator: DotsDecorator(
color: kLightTextColor.withOpacity(.15),
activeSize: Size(20, 10),
activeShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25.0)),
activeColor: kLightTextColor),
controlsPadding: kIsWeb
? const EdgeInsets.all(12.0)
: const EdgeInsets.symmetric(
vertical: Spacings.paddingSm,
horizontal: Spacings.paddingXs),
),
),
),
Container(
constraints: BoxConstraints(
maxHeight: Spacings.margin5Xl, minHeight: Spacings.margin5Xl),
margin: EdgeInsets.only(bottom: Spacings.margin5Xl),
child: currentIndex != 3
? Container()
: ElevatedButton(
onPressed: () => _onIntroEnd(context),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: Spacings.padding5Xl,
vertical: Spacings.paddingSm),
child: Text(
strings.goToVerificationPageBtnText,
style: TextStyle(
fontSize: k16TextFontSize, color: kDarkTextColor),
),
),
style: ElevatedButton.styleFrom(
elevation: 1,
primary: kLightTextColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(Spacings.radiusLg),
),
),
),
),
],
);
}
But the page does not change. It seems to make sense, as the index is not passed to the IntroductionScreen widget. So I was wondering how to navigate in these pages as will.
I found a way to do what I wanted.
It could be done with using keys & calling IntroductionScreenState's methods.
After going through the source code of library, it seems it is changing pages using this public method:
Future<void> animateScroll(int page) async {
setState(() => _isScrolling = true);
await _pageController.animateToPage(
max(min(page, getPagesLength() - 1), 0),
duration: Duration(milliseconds: widget.animationDuration),
curve: widget.curve,
);
if (mounted) {
setState(() => _isScrolling = false);
}
}
and also has these methods for going forward & backward:
void next() => animateScroll(_currentPage.round() + 1);
void previous() => animateScroll(_currentPage.round() - 1);
From this point, it was just a matter of calling the methods of this state when needed. This could be done using keys:
GlobalKey<IntroductionScreenState> _introScreenKey =
GlobalKey<IntroductionScreenState>();
IntroductionScreen(
key: _introScreenKey,
skip: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
primary: kLightTextColor,
shape: CircleBorder(),
),
onPressed: () {
_introScreenKey.currentState?.previous(); // <<----- This here
},
child: Icon(
Icons.keyboard_arrow_left_rounded,
color: Theme.of(context).primaryColor,
)),
)
And ofcourse it is pretty easy to navigate directly to any page using the animateScroll().
Since you're using navigator, you should give a try to Navigator.pop()
onPressed: () { Navigator.pop(context); }
here you can find some examples

setState from child widget (drawer) not updating a widget above it in the tree (FloatingAppBar)

I have a flutter project with a FloatingSearchBar from this library (pub.dev). I have four buttons, one of which is a GestureDetector (which is not really important but explains some behind scenes info).
This all looks like this:
or this:
or one other variation.
These buttons all work fine, although I've spent a lot of time getting them that way. They are toggleable, and their Icons are decided by a variable that can be updated using setState.
There's also, however, this layout:
which should be able to be toggled on and off by this switch/button inside the menu:
It too just updates the offlineMode variable inside a setState. But this setState doesn't update the search bar until I force the bar to update by clicking any of the buttons.
Why is setState not working in this particular circumstance?
Code for search bar:
Widget buildFloatingSearchBar(AsyncSnapshot snapshot) {
const bool isPortrait = true;
return DescribedFeatureOverlay( //Don't worry about these I don't think
featureId: 'search',
tapTarget: Icon(Icons.search),
title: Text('Search'),
description: Text(
'Tap the bar at the top to open the search interface, where you can search for GeoTags by name, approximate location or by author.'),
overflowMode: OverflowMode.extendBackground,
backgroundColor: Theme.of(context).primaryColor,
targetColor: Colors.white,
textColor: Colors.white,
child: FloatingSearchBar(
borderRadius: BorderRadius.all(
Radius.circular(50),
),
progress: loadingSearch,
title: offlineMode //Checking offlineMode variable Should update with setState
? Row(
children: [
Text(
'Geotagger',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
Text(
' - Offline Mode',
style: TextStyle(
//fontWeight: FontWeight.bold,
fontSize: 15,
),
),
],
)
: null,
backgroundColor: Theme.of(context).backgroundColor,
hint: 'Find a GeoTag...', //Shown if offlineMode is off (title is null)
transitionDuration: const Duration(milliseconds: 800),
transitionCurve: Curves.easeInOut,
physics: const BouncingScrollPhysics(),
axisAlignment: isPortrait ? 0.0 : -1.0,
openAxisAlignment: 0.0,
maxWidth: isPortrait ? 600 : 500,
height: compassExpanded ? 62 : 48,
debounceDelay: const Duration(milliseconds: 1500),
onQueryChanged: (inQuery) {
String query = inQuery.trim();
if (offlineMode || query == null || query == '' || query == '#') {
return;
}
if (query.startsWith('#')) {}
setState(() {
loadingSearch = true;
});
// Call your model, bloc, controller here.
Future.delayed(const Duration(seconds: 3), () {
setState(() {
loadingSearch = false;
});
});
},
// Specify a custom transition to be used for
// animating between opened and closed stated.
transition: ExpandingFloatingSearchBarTransition(),
accentColor: Theme.of(context).accentColor,
actions: [
FloatingSearchBarAction.searchToClear(
showIfClosed: false,
),
FloatingSearchBarAction( //This one is to check current GPS state
showIfOpened: false,
child: StreamBuilder<Object>(
stream: Stream.periodic(Duration(seconds: 5), (x) => x),
builder: (context, _) {
return FutureBuilder(
future: Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.medium,
timeLimit: Duration(seconds: 5),
),
builder: (context, compassDir) {
if (compassDir.hasError) {
//print('failure');
return Padding(
padding: const EdgeInsets.only(
right: 7,
bottom: 2,
),
child: Icon(
Icons.wrong_location,
color: Colors.red,
),
);
} else {
return Container(width: 0, height: 0);
}
});
}),
),
FloatingSearchBarAction(
showIfOpened: false,
child: DescribedFeatureOverlay(
featureId: 'toggleLocationSnapping',
tapTarget: Icon(Icons.control_camera),
title: Text('Location Snapping'),
description: Text(
'This button toggles the map mode between snapping (default) and free. When the icon shown is a circle with a line across it, tap it to switch to free mode, and you\'ll be able to move, zoom and rotate the map freely. In snapping mode, you will be snapped to your current location and orientation, and you\'ll also be unable to pan, rotate or zoom (usually).'),
//overflowMode: OverflowMode.extendBackground,
backgroundColor: Theme.of(context).primaryColor,
targetColor: Colors.white,
textColor: Colors.white,
child: CircularButton(
icon: Icon(moveMapToUser
? Icons.location_disabled
: Icons.my_location),
onPressed: () {
setState(() {
moveMapToUser = !moveMapToUser;
rotateMapToUser = false;
});
},
),
),
),
FloatingSearchBarAction(
showIfOpened: false,
child: Visibility(
visible: moveMapToUser,
child: DescribedFeatureOverlay(
featureId: 'rotationAndNorth',
tapTarget: Icon(Icons.screen_rotation),
title: Text('Rotation & Panning Mode'),
description: Text(
'This button has three states. When showing an upward facing arrow in free mode, tap it to orientate the map north, and remain in free mode. When showing a lock symbol in snapping mode, tap it to renable automatic rotation and prevent zooming. When showing an unlock symbol in snapping mode, tap it to allow rotation and zooming, but still prevent panning away from your current location.'),
//overflowMode: OverflowMode.extendBackground,
backgroundColor: Theme.of(context).primaryColor,
targetColor: Colors.white,
textColor: Colors.white,
child: CircularButton(
icon: Icon(rotateMapToUser ? Icons.lock_open : Icons.lock),
onPressed: () {
setState(() {
rotateMapToUser = !rotateMapToUser;
if (!rotateMapToUser) {
controller.rotate(0);
}
});
},
),
),
),
),
FloatingSearchBarAction(
showIfOpened: false,
child: Visibility(
visible: !moveMapToUser,
child: CircularButton(
icon: Icon(Icons.north),
onPressed: () {
setState(() {
controller.rotate(0);
});
},
),
),
),
FloatingSearchBarAction(
showIfOpened: false,
child: Visibility(
visible: !offlineMode,
child: DescribedFeatureOverlay(
featureId: 'refresh',
tapTarget: Icon(Icons.refresh),
title: Text('Refresh'),
description: Text(
'Tap this button to search the area visible on your device for GeoTags after panning the map.'),
//overflowMode: OverflowMode.extendBackground,
backgroundColor: Theme.of(context).primaryColor,
targetColor: Colors.white,
textColor: Colors.white,
child: CircularButton(
icon: const Icon(Icons.refresh),
onPressed: () {
//setState(() {
paintGeoTags(
true,
() => setState(() {
loadingSearch = true;
}),
() => setState(() {
loadingSearch = false;
}));
//});
}),
),
),
),
FloatingSearchBarAction( //Profile Pic icon
showIfOpened: !compassExpanded, //Don't worry about compassExpanded
child: Visibility(
visible: !offlineMode, //Should update on setState
child: DescribedFeatureOverlay(
featureId: 'profile',
tapTarget: Icon(Icons.account_circle),
title: Text('Public Profile'),
description: Text(
'Tap this button to view your public profile, and view information such as rank, total points and various other information. You can manage your profile by tapping the settings cog icon in that screen, or by choosing Manage My Profile from the menu. The colored border represents the color of your current rank, and the point will always face north. You can hold down on this icon to toggle the visibility of the compass point and to expand or reduce the height of the search bar. \n\nAfter you\'ve tapped the icon above to move to the next button introduction, tap the menu button in the top left, and your introduction will continue there!'),
overflowMode: OverflowMode.extendBackground,
backgroundColor: Theme.of(context).primaryColor,
targetColor: Colors.white,
textColor: Colors.white,
child: Hero(
tag: 'profileImage',
child: GestureDetector(
onTap: () => Navigator.pushNamed(
context,
ProfileScreen.routeName,
arguments: ScreenArguments(
globals.userData.displayName,
//'test user',
FirebaseFirestore.instance
.doc('users/' + globals.userData.uid),
true,
),
),
onLongPressStart: (e) {
setState(() {
compassExpanding = true;
});
},
onLongPressEnd: (e) => {
setState(() {
compassExpanded = !compassExpanded;
compassExpanding = false;
}),
//showLogoutAlert(context)
},
child: StreamBuilder<Object>(
stream: FlutterCompass.events,
builder: (context, compassDir) {
if (snapshot.hasError) {
return Center(
child: Text('Error: ${snapshot.error}'));
} else {
if (snapshot.data == null) {
return Container();
}
}
return Padding(
padding: const EdgeInsets.only(
left: 4,
),
child: Container(
width: 41,
height: 41,
transform: Matrix4Transform()
.rotateDegrees(
double.parse(((compassDir.data) ?? 0)
.toString()) *
-1 +
45,
origin: Offset(41 / 2, 41 / 2))
.matrix4,
decoration: ShapeDecoration(
//shape: BoxShape.circle,
shape: CustomRoundedRectangleBorder(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(21),
bottomRight: Radius.circular(21),
topRight: Radius.circular(21),
topLeft: Radius.circular(
compassExpanded ? 0 : 21),
),
leftSide: BorderSide(
width: 2,
color: changeMedalColor(
snapshot.data.points),
),
bottomLeftCornerSide: BorderSide(
width: 2,
color: changeMedalColor(
snapshot.data.points),
),
rightSide: BorderSide(
width: 2,
color: changeMedalColor(
snapshot.data.points),
),
bottomRightCornerSide: BorderSide(
width: 2,
color: changeMedalColor(
snapshot.data.points),
),
bottomSide: BorderSide(
width: 2,
color: changeMedalColor(
snapshot.data.points),
),
topSide: BorderSide(
width: 2,
color: changeMedalColor(
snapshot.data.points),
),
topRightCornerSide: BorderSide(
width: 2,
color: changeMedalColor(
snapshot.data.points),
),
),
color: //!compassExpanding
/*?*/ changeMedalColor(
snapshot.data.points)
//: Theme.of(context).accentColor,
/*border: Border.all(
width: 2,
color: changeMedalColor(snapshot.data.points),
),*/
),
//duration: Duration(milliseconds: 250),
child: SizedBox(
child: !compassExpanding
? Transform.rotate(
angle: vmath.radians(double.parse(
((compassDir.data) ?? 0)
.toString()) -
45),
child: CircleAvatar(
minRadius: 1000,
maxRadius: 5000,
backgroundImage: NetworkImage(
globals.userData.photoURL,
),
backgroundColor: changeMedalColor(
snapshot.data.points),
),
)
: Transform.rotate(
angle: vmath.radians(double.parse(
((compassDir.data) ?? 0)
.toString()) -
45),
child: Icon(Icons.expand,
color: Colors.white),
),
),
),
);
}),
),
),
),
),
),
],
builder: (context, transition) {
return ClipRRect(
//borderRadius: BorderRadius.circular(8),
child: Material(
color: Colors.white,
//elevation: 4.0,
child: !offlineMode
? Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Search for a GeoTag by name, approximate location. To search by author, use \'#\' in front of the search query.',
textAlign: TextAlign.center,
),
),
SizedBox(height: 15),
Column(
mainAxisSize: MainAxisSize.min,
children: Colors.accents.map((color) {
return Container(height: 112, color: color);
}).toList(),
),
],
)
: Column(
children: [
Center(
child: Icon(
Icons.cloud_off,
size: 40,
),
),
SizedBox(height: 10),
Text(
'You\'re in Offline Mode',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
Text(
'Whilst in Offline Mode, the search feature is disabled.'),
],
),
),
);
},
),
);
}
Code inside drawer to update variable:
DescribedFeatureOverlay(
featureId: 'offlineMode',
tapTarget: Icon(Icons.file_download),
title: Text('Offline Mode'),
description: Text(
'One of the most important and complex features of this app. Allows for fully offline functioning, including downloading large areas of maps. You can see more detail about it\'s features and drawbacks by tapping this menu item after the introduction is complete.'),
overflowMode: OverflowMode.extendBackground,
backgroundColor: Theme.of(widget.mainContext).primaryColor,
targetColor: Colors.white,
textColor: Colors.white,
child: ListTile(
title: Text(offlineMode
? 'Offline Manager'
: (internetConnected
? 'Enable Offline Mode'
: 'Offline Mode Disabled')),
onTap: offlineMode
? () {
Navigator.pushNamed(
widget.mainContext, OfflineManagerScreen.routeName);
}
: (internetConnected //Don't worry about internetConnected
? () {
showAlert(
context,
widget.mainContext,
'Offline Mode',
<Widget>[
//Some text widgets
],
'Agree & Enable Offline Mode',
'Cancel', () {
StorageManager.saveData('offlineMode', 'true');
setState(() {
offlineMode = true; //Should Update
});
if (!Directory(globals.saveDir.path + '/tiles')
.existsSync()) {
Directory(globals.saveDir.path + '/tiles')
.createSync(recursive: true);
}
});
}
: null),
leading: Icon(offlineMode
? Icons.offline_pin
: (internetConnected
? Icons.file_download
: Icons.cloud_off)),
trailing: Switch(
value: offlineMode,
onChanged: offlineMode && internetConnected
? (bool newVal) {
showAlert(
context,
widget.mainContext,
'Disable Offline Mode',
<Widget>[
const Text(
'Disabling Offline Mode will delete all saved tiles, and resync information with the server. Any GeoTags tagged will be uploaded, and the appropriate point value will be added to your account. Are you sure you want to disable Offline Mode?',
textAlign: TextAlign.justify,
),
],
'Disable',
'Stay Offline', () {
setState(() {
offlineMode = false; //Should update
});
StorageManager.saveData('offlineMode', 'false');
Directory(globals.saveDir.path + '/tiles')
.deleteSync(recursive: true);
setState(() {
totalSize = 0;
filesList = [];
sizeList = [];
});
});
}
: null,
activeColor: Theme.of(widget.mainContext).primaryColorLight,
),
),
),
If you haven't already realised, this is my first big Flutter project, so tips about other things are also appreciated!
Widget is updating on inner widget. but all state is not being changed.
you should pass state to all your inner widgets like this (in showalert in your case)
StatefulBuilder(builder: (context, newState) {
return
showAlert(
context,
widget.mainContext,
'Offline Mode',
<Widget>[
//Some text widgets
],
'Agree & Enable Offline Mode',
'Cancel', () {
StorageManager.saveData('offlineMode', 'true');
newState(() {
offlineMode = true; //Should Update
});
if (!Directory(globals.saveDir.path + '/tiles')
.existsSync()) {
Directory(globals.saveDir.path + '/tiles')
.createSync(recursive: true);
}
});
});
Please try this! I didnt test, but the logic is you should pass the state.
Maybe you should wrap listtile with new state..

Flutter - How can I add a circular loading indicator to my button?

I have a Flutter code. instead of showing nothing when the submit button is clicked, I want to show the circular loading indicator when the button is clicked so to keep the user busy but I'm having a challenge to convert a tutorial I have that does that to a work with my code.
Here is the tutorial:
...
children: <Widget>[
new Padding(
padding: const EdgeInsets.all(16.0),
child: new MaterialButton(
child: setUpButtonChild(),
onPressed: () {
setState(() {
if (_state == 0) {
animateButton();
}
});
},
elevation: 4.0,
minWidth: double.infinity,
height: 48.0,
color: Colors.lightGreen,
),
)
],
Widget setUpButtonChild() {
if (_state == 0) {
return new Text(
"Click Here",
style: const TextStyle(
color: Colors.white,
fontSize: 16.0,
),
);
} else if (_state == 1) {
return CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
);
} else {
return Icon(Icons.check, color: Colors.white);
}
}
void animateButton() {
setState(() {
_state = 1;
});
Timer(Duration(milliseconds: 1000), () {
setState(() {
_state = 2;
});
});
Timer(Duration(milliseconds: 3300), () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => AnchorsPage(),
),
);
});
}
Here's my code. All I want to do is to display the CircularProgressIndicator when the system is performing the HTTP request.
And here is my code where I want to use the CircularProgressIndicator:
Center(
child:
RaisedButton(
padding: EdgeInsets.fromLTRB(80, 10, 80, 10),
color: Colors.green,
child: setUpButtonChild(),
onPressed: () {
setState(()async {
_state = 1;
var toSubmit = {
"oid": EopOid,
"modifiedBy": user['UserName'].toString(),
"modifiedOn": DateTime.now().toString(),
"submitted": true,
"submittedOn": DateTime.now().toString(),
"submittedBy": user['UserName'].toString()
};
for (EopLine i in selectedEops) {
var item = {
"oid": i.oid.toString(),
"quantityCollected": i.quantityCollected,
"modifiedBy": user['UserName'].toString(),
"modifiedOn": DateTime.now().toString(),
};
await http
.put(
"http://api.ergagro.com:112/UpdateEopLine",
headers: {
'Content-Type': 'application/json'
},
body: jsonEncode(item))
.then((value) async {
if (selectedEops.indexOf(i) ==
selectedEops.length - 1) {
await http
.put(
"http://api.ergagro.com:112/SubmitEop",
headers: {
'Content-Type':
'application/json'
},
body: jsonEncode(toSubmit))
.then((value) {
print('${value.statusCode} submitted');
Navigator.pop(context);
});
}
});
}
_state = 2;
});
//Navigator.of(context).push(MaterialPageRoute(
//builder: (context) =>
//StartScanPage(widget.dc_result)));
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
),
),
If you're using a button with the icon() constructor (icon + text), you can swap the icon with the CircularProgressIndicator when the button state changes. It works because both the icon and the indicator are widgets:
return ElevatedButton.icon(
onPressed: _isLoading ? null : _onSubmit,
style: ElevatedButton.styleFrom(padding: const EdgeInsets.all(16.0)),
icon: _isLoading
? Container(
width: 24,
height: 24,
padding: const EdgeInsets.all(2.0),
child: const CircularProgressIndicator(
color: Colors.white,
strokeWidth: 3,
),
)
: const Icon(Icons.feedback),
label: const Text('SUBMIT'),
);
Live Demo
You can copy paste run full code below
You can directly use package https://pub.dev/packages/progress_indicator_button
or reference it's source code
You can pass AnimationController to http job and use controller.forward and reset
code snippet
void httpJob(AnimationController controller) async {
controller.forward();
print("delay start");
await Future.delayed(Duration(seconds: 3), () {});
print("delay stop");
controller.reset();
}
...
ProgressButton(
borderRadius: BorderRadius.all(Radius.circular(8)),
strokeWidth: 2,
child: Text(
"Sample",
style: TextStyle(
color: Colors.white,
fontSize: 24,
),
),
onPressed: (AnimationController controller) async {
await httpJob(controller);
}
working demo
full code
import 'package:flutter/material.dart';
import 'package:progress_indicator_button/progress_button.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
void httpJob(AnimationController controller) async {
controller.forward();
print("delay start");
await Future.delayed(Duration(seconds: 3), () {});
print("delay stop");
controller.reset();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
width: 200,
height: 60,
child: ProgressButton(
borderRadius: BorderRadius.all(Radius.circular(8)),
strokeWidth: 2,
child: Text(
"Sample",
style: TextStyle(
color: Colors.white,
fontSize: 24,
),
),
onPressed: (AnimationController controller) async {
await httpJob(controller);
},
),
),
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
You can also use ternary operator to output based on some _isLoading state variable and make use of CircularProgressIndicator(), of course this is a simple solution without using any third party libraries.
#override
Widget build(BuildContext context) {
return TextButton(
onPressed: () {},
child: Container(
padding: const EdgeInsets.all(10),
child: _isLoading
? SizedBox(
height: 25,
width: 25,
child: CircularProgressIndicator(),
)
: Text('ORDER NOW'),
),
);
}