Related
I'm new to Flutter and followed this tutorial https://suragch.medium.com/background-audio-in-flutter-with-audio-service-and-just-audio-3cce17b4a7d to setup Just Audio and audioservice. I'm building my app for Android at the moment.
I'm having 3 issues:
The main problem is that when I use context.go (from GoRouter) to move to the player screen, audio should start playing, but it doesn't, I have to manually press the Play button.
Additionaly when I load a playlist and press Next, it also doesn't start playing automatically.
Finally, sometimes if I leave the player for other screen and come back, I can't press the Play button.
Here are the 3 files I'm using:
Player screen:
import 'package:audio_service/audio_service.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:podfic_app/screens/chapterlist_screen.dart';
import 'package:podfic_app/utils/api_service.dart';
import './just_audio/notifiers/play_button_notifier.dart';
import './just_audio/notifiers/progress_notifier.dart';
import './just_audio/page_manager.dart';
import './just_audio/services/service_locator.dart';
import 'package:audio_video_progress_bar/audio_video_progress_bar.dart';
class Player extends StatefulWidget {
const Player({super.key});
#override
State<Player> createState() => _PlayerState();
}
class _PlayerState extends State<Player> {
#override
void initState() {
super.initState();
getIt<PageManager>().init();
}
#override
void dispose() {
getIt<PageManager>().dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
// startPlaying();
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 25.0),
child: Column(
children: [
// const SizedBox(height: 10),
// back button and menu button
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.pop()),
const Text('Listening to'),
IconButton(
icon: const Icon(Icons.playlist_play_outlined),
onPressed: () => context.push('/chapters')),
],
),
const SizedBox(height: 25),
// cover art, artist name, song name
const CurrentMetadata(),
const SizedBox(height: 30),
const SizedBox(height: 20),
// linear bar
const AudioProgressBar(),
const SizedBox(height: 30),
// previous song, pause play, skip next song
Align(
alignment: Alignment.topCenter,
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(0),
child: Stack(children: [
Container(
child: const ChangeSpeedButton(),
),
Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
PreviousSongButton(),
PlayButton(),
NextSongButton(),
],
),
),
]),
),
),
],
),
),
),
);
}
}
// class CurrentSongTitle extends StatelessWidget {
// const CurrentSongTitle({Key? key}) : super(key: key);
// #override
// Widget build(BuildContext context) {
// final pageManager = getIt<PageManager>();
// return ValueListenableBuilder<String>(
// valueListenable: pageManager.currentSongTitleNotifier,
// builder: (_, title, __) {
// return Padding(
// padding: const EdgeInsets.only(top: 8.0),
// child: Text(title, style: const TextStyle(fontSize: 40)),
// );
// },
// );
// }
// }
class AddRemoveSongButtons extends StatelessWidget {
const AddRemoveSongButtons({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final pageManager = getIt<PageManager>();
return Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
FloatingActionButton(
onPressed: pageManager.add,
child: const Icon(Icons.add),
),
FloatingActionButton(
onPressed: pageManager.remove,
child: const Icon(Icons.remove),
),
],
),
);
}
}
class AudioProgressBar extends StatelessWidget {
const AudioProgressBar({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final pageManager = getIt<PageManager>();
return ValueListenableBuilder<ProgressBarState>(
valueListenable: pageManager.progressNotifier,
builder: (_, value, __) {
return ProgressBar(
progress: value.current,
buffered: value.buffered,
total: value.total,
onSeek: pageManager.seek,
);
},
);
}
}
class AudioControlButtons extends StatelessWidget {
const AudioControlButtons({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
height: 60,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: const [
PreviousSongButton(),
PlayButton(),
NextSongButton(),
],
),
);
}
}
class PreviousSongButton extends StatelessWidget {
const PreviousSongButton({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final pageManager = getIt<PageManager>();
return ValueListenableBuilder<bool>(
valueListenable: pageManager.isFirstSongNotifier,
builder: (_, isFirst, __) {
return IconButton(
icon: const Icon(Icons.skip_previous),
iconSize: 32,
onPressed: (isFirst) ? null : pageManager.previous,
);
},
);
}
}
class PlayButton extends StatelessWidget {
const PlayButton({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final pageManager = getIt<PageManager>();
return ValueListenableBuilder<ButtonState>(
valueListenable: pageManager.playButtonNotifier,
builder: (_, value, __) {
switch (value) {
case ButtonState.loading:
return Container(
margin: const EdgeInsets.all(8.0),
width: 48.0,
height: 48.0,
child: const CircularProgressIndicator(),
);
case ButtonState.paused:
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(200)),
child: IconButton(
icon: const Icon(Icons.play_arrow),
iconSize: 48,
color: Colors.white,
onPressed: pageManager.play,
));
case ButtonState.playing:
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(200)),
child: IconButton(
icon: const Icon(Icons.pause),
iconSize: 48,
color: Colors.white,
onPressed: pageManager.pause,
));
}
},
);
}
}
class NextSongButton extends StatelessWidget {
const NextSongButton({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final pageManager = getIt<PageManager>();
return ValueListenableBuilder<bool>(
valueListenable: pageManager.isLastSongNotifier,
builder: (_, isLast, __) {
return IconButton(
icon: const Icon(Icons.skip_next),
iconSize: 32,
onPressed: (isLast) ? null : pageManager.next,
);
},
);
}
}
class ChangeSpeedButton extends StatefulWidget {
const ChangeSpeedButton({Key? key}) : super(key: key);
#override
State<ChangeSpeedButton> createState() => _ChangeSpeedButtonState();
}
class _ChangeSpeedButtonState extends State<ChangeSpeedButton> {
var selectedValue = 1.0;
#override
Widget build(BuildContext context) {
final pageManager = getIt<PageManager>();
return DropdownButtonHideUnderline(
child: DropdownButton(
icon: const Visibility(
visible: false, child: Icon(Icons.arrow_downward)),
value: selectedValue
.toString(), //we set a value here depending on the button pressed, and call the pagemanager method assing it the value.
items: dropdownItems,
onChanged: (value) {
setState(() {
selectedValue = double.parse(value.toString());
});
pageManager.changeSpeed(selectedValue);
}),
);
}
List<DropdownMenuItem<String>> get dropdownItems {
List<DropdownMenuItem<String>> menuItems = [
const DropdownMenuItem(value: "0.75", child: Text("0.75x")),
const DropdownMenuItem(value: "1.0", child: Text("1.0x")),
const DropdownMenuItem(value: "1.25", child: Text("1.25x")),
const DropdownMenuItem(value: "1.5", child: Text("1.5x")),
const DropdownMenuItem(value: "1.75", child: Text("1.75x")),
const DropdownMenuItem(value: "2.0", child: Text("2.0x")),
];
return menuItems;
}
}
class CurrentMetadata extends StatelessWidget {
const CurrentMetadata({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final pageManager = getIt<PageManager>();
return ValueListenableBuilder<MediaItem?>(
valueListenable: pageManager.currentSongMetadataNotifier,
builder: (context, mediaItem, child) {
if (mediaItem == null) {
return SizedBox.shrink();
}
return Column(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network((mediaItem.artUri ?? '').toString()),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(mediaItem.extras!['chapterName'],
overflow: TextOverflow.fade,
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(fontWeight: FontWeight.w700)),
const SizedBox(height: 6),
Text(
mediaItem.title,
overflow: TextOverflow.fade,
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 6),
Text(
'By ${mediaItem.artist}',
overflow: TextOverflow.fade,
style: Theme.of(context).textTheme.titleSmall,
),
],
),
IconButton(
icon: const Icon(Icons.favorite),
color: Colors.red,
onPressed: () {}),
],
),
)
],
);
// Text(mediaItem.album ?? ''),
// Text(mediaItem.displaySubtitle ?? ''),
},
);
}
}
PageManager.dart
import 'package:flutter/foundation.dart';
import 'package:podfic_app/models/getpodficlist_model.dart';
import 'package:podfic_app/models/series_model.dart';
import 'package:podfic_app/notifiers/chapterObject_notifier.dart';
import 'package:podfic_app/utils/api_service.dart';
import 'notifiers/play_button_notifier.dart';
import 'notifiers/progress_notifier.dart';
import 'notifiers/repeat_button_notifier.dart';
import 'services/playlist_repository.dart';
import 'package:audio_service/audio_service.dart';
import 'services/service_locator.dart';
class PageManager {
// Listeners: Updates going to the UI
final currentSongMetadataNotifier = ValueNotifier<MediaItem?>(null);
final playlistNotifier = ValueNotifier<List<String>>([]);
final progressNotifier = ProgressNotifier();
final repeatButtonNotifier = RepeatButtonNotifier();
final isFirstSongNotifier = ValueNotifier<bool>(true);
final playButtonNotifier = PlayButtonNotifier();
final isLastSongNotifier = ValueNotifier<bool>(true);
final isShuffleModeEnabledNotifier = ValueNotifier<bool>(false);
final _audioHandler = getIt<AudioHandler>();
List<Chapter>? chapterObject = getIt<ChapterObject>().chapterObject.value;
// Events: Calls coming from the UI
void init() async {
await loadPlaylist();
_listenToChangesInPlaylist();
_listenToPlaybackState();
_listenToCurrentPosition();
_listenToBufferedPosition();
_listenToTotalDuration();
_listenToChangesInSong();
// _startPlaying();
}
Future<void> loadPlaylist() async {
final mediaItems = seriesItems.chapterList?.map((chapter) {
final _podficInfo = chapter.podficInfo;
return MediaItem(
id: _podficInfo.id.toString(),
artUri: Uri.parse(_podficInfo.coverArt!),
artist: seriesItems.author,
title: seriesItems.title!,
extras: {
'url': _podficInfo.url,
'chapterName':
"Chapter ${_podficInfo.chapterNumber}: ${_podficInfo.title}"
},
);
}).toList() ??
[];
_audioHandler.addQueueItems(mediaItems);
}
// void _setInitialPlaylist() {
// final _seriesInfo = getIt<SeriesInfo>();
// final _playlist = SeriesInfo(chapterList: chapterItems);
// }
void play() => _audioHandler.play();
void pause() => _audioHandler.pause();
void seek(Duration position) => _audioHandler.seek(position);
void previous() => _audioHandler.skipToPrevious();
void next() => _audioHandler.skipToNext();
void repeat() {}
void shuffle() {}
void add() async {
//don't need this for now.
final songRepository = getIt<PlaylistRepository>();
final song = await songRepository.fetchAnotherSong();
final mediaItem = MediaItem(
id: song['id'] ?? '',
album: song['album'] ?? '',
title: song['title'] ?? '',
extras: {'url': song['url']},
);
_audioHandler.addQueueItem(mediaItem);
}
void remove() {
//don't need this for now.
final lastIndex = _audioHandler.queue.value.length - 1;
if (lastIndex < 0) return;
_audioHandler.removeQueueItemAt(lastIndex);
}
void dispose() {
_audioHandler.stop();
}
void _listenToChangesInPlaylist() {
_audioHandler.queue.listen((playlist) {
if (playlist.isEmpty) {
playlistNotifier.value = [];
currentSongMetadataNotifier.value = '' as MediaItem?;
} else {
final newList = playlist.map((item) => item.title).toList();
playlistNotifier.value = newList;
}
_updateSkipButtons();
});
}
void _listenToPlaybackState() {
_audioHandler.playbackState.listen((playbackState) {
final isPlaying = playbackState.playing;
final processingState = playbackState.processingState;
if (processingState == AudioProcessingState.loading ||
processingState == AudioProcessingState.buffering) {
playButtonNotifier.value = ButtonState.loading;
} else if (!isPlaying) {
playButtonNotifier.value = ButtonState.paused;
} else if (processingState != AudioProcessingState.completed) {
playButtonNotifier.value = ButtonState.playing;
logger.i('Reproduciendo');
} else {
_audioHandler.seek(Duration.zero);
_audioHandler.pause();
}
});
}
void _listenToCurrentPosition() {
AudioService.position.listen((position) {
final oldState = progressNotifier.value;
progressNotifier.value = ProgressBarState(
current: position,
buffered: oldState.buffered,
total: oldState.total,
);
});
}
void _listenToBufferedPosition() {
_audioHandler.playbackState.listen((playbackState) {
final oldState = progressNotifier.value;
progressNotifier.value = ProgressBarState(
current: oldState.current,
buffered: playbackState.bufferedPosition,
total: oldState.total,
);
});
}
void _listenToTotalDuration() {
_audioHandler.mediaItem.listen((mediaItem) {
final oldState = progressNotifier.value;
progressNotifier.value = ProgressBarState(
current: oldState.current,
buffered: oldState.buffered,
total: mediaItem?.duration ?? Duration.zero,
);
});
}
void _listenToChangesInSong() {
_audioHandler.mediaItem.listen((mediaItem) {
currentSongMetadataNotifier.value = mediaItem;
_updateSkipButtons();
});
}
void _updateSkipButtons() {
final mediaItem = _audioHandler.mediaItem.value;
final playlist = _audioHandler.queue.value;
if (playlist.length < 2 || mediaItem == null) {
isFirstSongNotifier.value = true;
isLastSongNotifier.value = true;
} else {
isFirstSongNotifier.value = playlist.first == mediaItem;
isLastSongNotifier.value = playlist.last == mediaItem;
}
}
void changeSpeed(value) {
_audioHandler.setSpeed(value);
}
// void _startPlaying(){
// _audioHandler.
// }
}
// class PlaySpeedNotifier extends ValueNotifier {
// PlaySpeedNotifier() : super(initialSpeed);
// static const initialSpeed = 1.0;
// var speed = initialSpeed;
// void changeSpeed(value) {
// speed = value;
// _audio
// }
// }
Audiohandler.dart
import 'package:audio_service/audio_service.dart';
import 'package:just_audio/just_audio.dart';
import 'package:podfic_app/utils/api_service.dart';
Future<AudioHandler> initAudioService() async {
return await AudioService.init(
builder: () => MyAudioHandler(),
config: const AudioServiceConfig(
androidNotificationChannelId: 'com.mycompany.myapp.audio',
androidNotificationChannelName: 'FanPods',
androidNotificationOngoing: true,
androidStopForegroundOnPause: true,
),
);
}
class MyAudioHandler extends BaseAudioHandler {
final _player = AudioPlayer();
final _playlist = ConcatenatingAudioSource(children: []);
MyAudioHandler() {
loadEmptyPlaylist();
_notifyAudioHandlerAboutPlaybackEvents();
_listenForDurationChanges();
_listenForCurrentSongIndexChanges();
}
Future<void> loadEmptyPlaylist() async {
try {
await _player.setAudioSource(_playlist);
} catch (e) {
logger.i('algo paso con loadEemptyplaylist');
}
}
// Future<void> startPlaying() => _player.setAudioSource(_playlist);
#override
Future<void> addQueueItems(List<MediaItem> mediaItems) async {
// manage Just Audio
final audioSource = mediaItems.map(_createAudioSource);
_playlist.addAll(audioSource.toList());
// notify system
final newQueue = queue.value..addAll(mediaItems);
queue.add(newQueue);
logger.i(newQueue);
}
UriAudioSource _createAudioSource(MediaItem mediaItem) {
return AudioSource.uri(
Uri.parse(mediaItem.extras!['url']),
tag: mediaItem,
);
}
#override
Future<void> play() => _player.play();
#override
Future<void> pause() => _player.pause();
void _notifyAudioHandlerAboutPlaybackEvents() {
_player.playbackEventStream.listen((PlaybackEvent event) {
final playing = _player.playing;
playbackState.add(playbackState.value.copyWith(
controls: [
MediaControl.skipToPrevious,
if (playing) MediaControl.pause else MediaControl.play,
MediaControl.skipToNext,
],
systemActions: const {
MediaAction.seek,
},
androidCompactActionIndices: const [0, 1, 3],
processingState: const {
ProcessingState.idle: AudioProcessingState.idle,
ProcessingState.loading: AudioProcessingState.loading,
ProcessingState.buffering: AudioProcessingState.buffering,
ProcessingState.ready: AudioProcessingState.ready,
ProcessingState.completed: AudioProcessingState.completed,
}[_player.processingState]!,
playing: playing,
updatePosition: _player.position,
bufferedPosition: _player.bufferedPosition,
speed: _player.speed,
queueIndex: event.currentIndex,
));
});
}
#override
Future<void> seek(Duration position) => _player.seek(position);
void _listenForDurationChanges() {
_player.durationStream.listen((duration) {
final index = _player.currentIndex;
final newQueue = queue.value;
if (index == null || newQueue.isEmpty) return;
final oldMediaItem = newQueue[index];
final newMediaItem = oldMediaItem.copyWith(duration: duration);
newQueue[index] = newMediaItem;
queue.add(newQueue);
mediaItem.add(newMediaItem);
});
}
#override
Future<void> skipToNext() => _player.seekToNext();
#override
Future<void> skipToPrevious() => _player.seekToPrevious();
void _listenForCurrentSongIndexChanges() {
_player.currentIndexStream.listen((index) {
final playlist = queue.value;
if (index == null || playlist.isEmpty) return;
mediaItem.add(playlist[index]);
logger.i(playlist);
});
}
#override
Future<void> addQueueItem(MediaItem mediaItem) async {
// manage Just Audio
final audioSource = _createAudioSource(mediaItem);
_playlist.add(audioSource);
// notify system
final newQueue = queue.value..add(mediaItem);
queue.add(newQueue);
}
#override
Future<void> removeQueueItemAt(int index) async {
// manage Just Audio
_playlist.removeAt(index);
// notify system
final newQueue = queue.value..removeAt(index);
queue.add(newQueue);
}
#override
Future<void> skipToQueueItem(int index) async {
if (index < 0 || index >= queue.value.length) return;
if (_player.shuffleModeEnabled) {
index = _player.shuffleIndices![index];
}
_player.seek(Duration.zero, index: index);
}
#override
Future<void> stop() async {
await _player.dispose();
return super.stop();
}
}
I tried including pageManager.play in the init section of Player and PageManager, but nothing happens. I think I need to set the audiosource as a concatenatingAudioSource, but I created a method to execute it and passed it _playlist, running it under _listenForCurrentSongIndexChanges(), but nothing happened.
Realized the play method was executing before the init method finished, so simple delay worked.
I have a view with a list of job applications,
when you click on one of the job applications, it sends the context to the next page, where you can edit the job application.
when I click a job application, it sends its context to the next page I think. it then populates the local variables I think.
the goal is to update the job application on the server, when the change is recorded, I should get the new data back from the server, and it should then update the local variables. right now I only have the button 'submit inquiry' to make the code easy to follow.
Below is the view that has all job applications
class JobApplicationsView extends StatefulWidget {
const JobApplicationsView({Key? key}) : super(key: key);
#override
_JobApplicationsViewState createState() => _JobApplicationsViewState();
}
class _JobApplicationsViewState extends State<JobApplicationsView> {
late final FirebaseCloudStorage _jobsService;
String get userId => AuthService.firebase().currentUser!.id;
#override
void initState() {
_jobsService = FirebaseCloudStorage();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Your job applications'),
actions: [
PopupMenuButton<MenuAction>(
onSelected: (value) async {
switch (value) {
case MenuAction.logout:
final shouldLogout = await showLogOutDialog(context);
if (shouldLogout) {
await AuthService.firebase().logOut();
Navigator.of(context).pushNamedAndRemoveUntil(
loginRoute,
(_) => false,
);
}
}
},
itemBuilder: (context) {
return const [
PopupMenuItem<MenuAction>(
value: MenuAction.logout,
child: Text('Log out'),
),
];
},
)
],
),
body: StreamBuilder(
stream: _jobsService.allJobApplications(userId),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
case ConnectionState.active:
if (snapshot.hasData) {
final allJobApplications =
snapshot.data as Iterable<CloudJobApplication>;
return JobApplicationsListView(
allowScroll: false,
jobApplications: allJobApplications,
onTap: (job) {
Navigator.of(context).pushNamed(
myJobApplicationsRoute,
arguments: job,
);
},
);
} else {
return const CircularProgressIndicator();
}
default:
return const CircularProgressIndicator();
}
},
),
);
}
}
the function used to get all job applications.
final jobApplication = FirebaseFirestore.instance.collection('job application');
Stream<Iterable<CloudJobApplication>> allJobApplications(userId) =>
jobApplication.snapshots().map((event) => event.docs
.map((doc) => CloudJobApplication.fromSnapshot(doc))
.where((jobApplication) => jobApplication.jobApplicatorId == userId));
here is the view that allows the job application to be edited:
The function 'getExistingJobApplication' is where the issue might be.
class JobApplicationView extends StatefulWidget {
const JobApplicationView({Key? key}) : super(key: key);
#override
_JobApplicationViewState createState() => _JobApplicationViewState();
}
// https://youtu.be/VPvVD8t02U8?t=90350
class _JobApplicationViewState extends State<JobApplicationView> {
CloudJobApplication? _jobApplication;
late final FirebaseCloudStorage _cloudFunctions;
final _formKey = GlobalKey<FormState>();
final currentUser = AuthService.firebase().currentUser!;
// state varibles
String _jobApplicationState = 'default';
String? _jobApplicationSubState;
String? _jobCreatorId;
String? _jobApplicatorId;
String? _jobApplicationEstimate;
late Timestamp? _jobApplicationStartDate;
late Timestamp? _jobApplicationEndDate;
late final TextEditingController _jobDescriptionController;
bool? _formFieldsEditable;
#override
void initState() {
super.initState();
_cloudFunctions = FirebaseCloudStorage();
_jobDescriptionController = TextEditingController();
}
Future<CloudJobApplication> getExistingJobApplication(
BuildContext context) async {
// if the local state variables are default, use the information from the context
// if the local state variables are not default, that means an update has occur on the server. fetch the new data from the server
var widgetJobApplication;
if (_jobApplicationState == 'default') {
log('using job application from context');
widgetJobApplication = context.getArgument<CloudJobApplication>();
_jobApplication = widgetJobApplication;
_jobApplicationState =
widgetJobApplication?.jobApplicationState as String;
_jobApplicationEstimate = widgetJobApplication.jobApplicationEstimate;
_jobDescriptionController.text =
widgetJobApplication?.jobApplicationDescription as String;
_jobApplicationStartDate = widgetJobApplication.jobApplicationStartDate;
_jobApplicationEndDate = widgetJobApplication.jobApplicationEndDate;
_jobCreatorId = widgetJobApplication.jobCreatorId;
_jobApplicatorId = widgetJobApplication.jobApplicatorId;
_formFieldsEditable = _jobApplicationState == jobApplicationStateNew ||
_jobApplicationState == jobApplicationStateOpen
? true
: false;
} else {
log('using job application from server');
widgetJobApplication =
_cloudFunctions.getJobApplication(_jobApplication!.documentId);
}
return widgetJobApplication;
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('update job application'),
actions: [],
),
body: FutureBuilder(
future: getExistingJobApplication(context),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.done:
return Form(
key: _formKey,
child: ListView(padding: const EdgeInsets.all(32.0), children: [
JobApplicationStateVariablesWidget(
jobApplicationState: _jobApplicationState),
const Divider(
height: 20,
thickness: 5,
indent: 0,
endIndent: 0,
color: Colors.blue,
),
ElevatedButton(
style: ElevatedButton.styleFrom(
textStyle: const TextStyle(fontSize: 13),
backgroundColor: false ? Colors.black : Colors.blue,
),
// HERE IS BUTTON AT PLAY
onPressed: () async {
setState(() {
_jobApplicationState = jobApplicationStatePending;
});
await _cloudFunctions.updateJobApplicationColumn(
documentId: _jobApplication?.documentId as String,
fieldNameColumn: jobApplicationStateColumn,
fieldNameColumnValue: jobApplicationStatePending);
},
child: const Text('Submit inquiry'),
),
]),
);
default:
return const CircularProgressIndicator();
}
},
),
);
}
}
The way its set up right now works but in a crappy way...
for example: if the job was closed, then the job application should become closed. the way I have the code right now wont work with that apprach because I am hitting the local state variables directly
I am working on a web app where users can post stuffs and make them more accessible by associating the posts with tags. so my idea is similar to stackoverflow's way of giving tags to posts, I am creating a Textfield with which will accept only few tags(string values) which I will create from a list and users can put them in their post. But I aint getting how to implement this as textfield has only few keyboardtypes... and I what I want to achieve is if I entered a value from a that list then it should act like a chip text(tag).
or Is there any other way to do this,
your help is appreciated,
thank you
Yes, there is. you can use the flutter_tagging package on the PUB
It has supports for Web
The gif below explains what you want to achieve
You can find an implementation of a Chip Input Field type widget here:
Latest: https://gist.github.com/slightfoot/c6c0f1f1baca326a389a9aec47886ad6
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
// See: https://twitter.com/shakil807/status/1042127387515858949
// https://github.com/pchmn/MaterialChipsInput/tree/master/library/src/main/java/com/pchmn/materialchips
// https://github.com/BelooS/ChipsLayoutManager
void main() => runApp(ChipsDemoApp());
class ChipsDemoApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primaryColor: Colors.indigo,
accentColor: Colors.pink,
),
home: DemoScreen(),
);
}
}
class DemoScreen extends StatefulWidget {
#override
_DemoScreenState createState() => _DemoScreenState();
}
class _DemoScreenState extends State<DemoScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Material Chips Input'),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
decoration: const InputDecoration(hintText: 'normal'),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ChipsInput<AppProfile>(
decoration: InputDecoration(prefixIcon: Icon(Icons.search), hintText: 'Profile search'),
findSuggestions: _findSuggestions,
onChanged: _onChanged,
chipBuilder: (BuildContext context, ChipsInputState<AppProfile> state, AppProfile profile) {
return InputChip(
key: ObjectKey(profile),
label: Text(profile.name),
avatar: CircleAvatar(
backgroundImage: NetworkImage(profile.imageUrl),
),
onDeleted: () => state.deleteChip(profile),
onSelected: (_) => _onChipTapped(profile),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
},
suggestionBuilder: (BuildContext context, ChipsInputState<AppProfile> state, AppProfile profile) {
return ListTile(
key: ObjectKey(profile),
leading: CircleAvatar(
backgroundImage: NetworkImage(profile.imageUrl),
),
title: Text(profile.name),
subtitle: Text(profile.email),
onTap: () => state.selectSuggestion(profile),
);
},
),
),
),
],
),
);
}
void _onChipTapped(AppProfile profile) {
print('$profile');
}
void _onChanged(List<AppProfile> data) {
print('onChanged $data');
}
Future<List<AppProfile>> _findSuggestions(String query) async {
if (query.length != 0) {
return mockResults.where((profile) {
return profile.name.contains(query) || profile.email.contains(query);
}).toList(growable: false);
} else {
return const <AppProfile>[];
}
}
}
// -------------------------------------------------
const mockResults = <AppProfile>[
AppProfile('Stock Man', 'stock#man.com', 'https://d2gg9evh47fn9z.cloudfront.net/800px_COLOURBOX4057996.jpg'),
AppProfile('Paul', 'paul#google.com', 'https://mbtskoudsalg.com/images/person-stock-image-png.png'),
AppProfile('Fred', 'fred#google.com',
'https://media.istockphoto.com/photos/feeling-great-about-my-corporate-choices-picture-id507296326'),
AppProfile('Bera', 'bera#flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
AppProfile('John', 'john#flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
AppProfile('Thomas', 'thomas#flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
AppProfile('Norbert', 'norbert#flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
AppProfile('Marina', 'marina#flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
];
class AppProfile {
final String name;
final String email;
final String imageUrl;
const AppProfile(this.name, this.email, this.imageUrl);
#override
bool operator ==(Object other) =>
identical(this, other) || other is AppProfile && runtimeType == other.runtimeType && name == other.name;
#override
int get hashCode => name.hashCode;
#override
String toString() {
return 'Profile{$name}';
}
}
// -------------------------------------------------
typedef ChipsInputSuggestions<T> = Future<List<T>> Function(String query);
typedef ChipSelected<T> = void Function(T data, bool selected);
typedef ChipsBuilder<T> = Widget Function(BuildContext context, ChipsInputState<T> state, T data);
class ChipsInput<T> extends StatefulWidget {
const ChipsInput({
Key key,
this.decoration = const InputDecoration(),
#required this.chipBuilder,
#required this.suggestionBuilder,
#required this.findSuggestions,
#required this.onChanged,
this.onChipTapped,
}) : super(key: key);
final InputDecoration decoration;
final ChipsInputSuggestions findSuggestions;
final ValueChanged<List<T>> onChanged;
final ValueChanged<T> onChipTapped;
final ChipsBuilder<T> chipBuilder;
final ChipsBuilder<T> suggestionBuilder;
#override
ChipsInputState<T> createState() => ChipsInputState<T>();
}
class ChipsInputState<T> extends State<ChipsInput<T>> implements TextInputClient {
static const kObjectReplacementChar = 0xFFFC;
Set<T> _chips = Set<T>();
List<T> _suggestions;
int _searchId = 0;
FocusNode _focusNode;
TextEditingValue _value = TextEditingValue();
TextInputConnection _connection;
String get text => String.fromCharCodes(
_value.text.codeUnits.where((ch) => ch != kObjectReplacementChar),
);
bool get _hasInputConnection => _connection != null && _connection.attached;
void requestKeyboard() {
if (_focusNode.hasFocus) {
_openInputConnection();
} else {
FocusScope.of(context).requestFocus(_focusNode);
}
}
void selectSuggestion(T data) {
setState(() {
_chips.add(data);
_updateTextInputState();
_suggestions = null;
});
widget.onChanged(_chips.toList(growable: false));
}
void deleteChip(T data) {
setState(() {
_chips.remove(data);
_updateTextInputState();
});
widget.onChanged(_chips.toList(growable: false));
}
#override
void initState() {
super.initState();
_focusNode = FocusNode();
_focusNode.addListener(_onFocusChanged);
}
void _onFocusChanged() {
if (_focusNode.hasFocus) {
_openInputConnection();
} else {
_closeInputConnectionIfNeeded();
}
setState(() {
// rebuild so that _TextCursor is hidden.
});
}
#override
void dispose() {
_focusNode?.dispose();
_closeInputConnectionIfNeeded();
super.dispose();
}
void _openInputConnection() {
if (!_hasInputConnection) {
_connection = TextInput.attach(this, TextInputConfiguration());
_connection.setEditingState(_value);
}
_connection.show();
}
void _closeInputConnectionIfNeeded() {
if (_hasInputConnection) {
_connection.close();
_connection = null;
}
}
#override
Widget build(BuildContext context) {
var chipsChildren = _chips
.map<Widget>(
(data) => widget.chipBuilder(context, this, data),
)
.toList();
final theme = Theme.of(context);
chipsChildren.add(
Container(
height: 32.0,
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
text,
style: theme.textTheme.subhead.copyWith(
height: 1.5,
),
),
_TextCaret(
resumed: _focusNode.hasFocus,
),
],
),
),
);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
//mainAxisSize: MainAxisSize.min,
children: <Widget>[
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: requestKeyboard,
child: InputDecorator(
decoration: widget.decoration,
isFocused: _focusNode.hasFocus,
isEmpty: _value.text.length == 0,
child: Wrap(
children: chipsChildren,
spacing: 4.0,
runSpacing: 4.0,
),
),
),
Expanded(
child: ListView.builder(
itemCount: _suggestions?.length ?? 0,
itemBuilder: (BuildContext context, int index) {
return widget.suggestionBuilder(context, this, _suggestions[index]);
},
),
),
],
);
}
#override
void updateEditingValue(TextEditingValue value) {
final oldCount = _countReplacements(_value);
final newCount = _countReplacements(value);
setState(() {
if (newCount < oldCount) {
_chips = Set.from(_chips.take(newCount));
}
_value = value;
});
_onSearchChanged(text);
}
int _countReplacements(TextEditingValue value) {
return value.text.codeUnits.where((ch) => ch == kObjectReplacementChar).length;
}
#override
void performAction(TextInputAction action) {
_focusNode.unfocus();
}
void _updateTextInputState() {
final text = String.fromCharCodes(_chips.map((_) => kObjectReplacementChar));
_value = TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: text.length),
composing: TextRange(start: 0, end: text.length),
);
_connection.setEditingState(_value);
}
void _onSearchChanged(String value) async {
final localId = ++_searchId;
final results = await widget.findSuggestions(value);
if (_searchId == localId && mounted) {
setState(() => _suggestions = results.where((profile) => !_chips.contains(profile)).toList(growable: false));
}
}
}
class _TextCaret extends StatefulWidget {
const _TextCaret({
Key key,
this.duration = const Duration(milliseconds: 500),
this.resumed = false,
}) : super(key: key);
final Duration duration;
final bool resumed;
#override
_TextCursorState createState() => _TextCursorState();
}
class _TextCursorState extends State<_TextCaret> with SingleTickerProviderStateMixin {
bool _displayed = false;
Timer _timer;
#override
void initState() {
super.initState();
_timer = Timer.periodic(widget.duration, _onTimer);
}
void _onTimer(Timer timer) {
setState(() => _displayed = !_displayed);
}
#override
void dispose() {
_timer.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return FractionallySizedBox(
heightFactor: 0.7,
child: Opacity(
opacity: _displayed && widget.resumed ? 1.0 : 0.0,
child: Container(
width: 2.0,
color: theme.primaryColor,
),
),
);
}
}
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,
I am trying to build a steam of posts (think twitter or instagram type posts) that a user is able to scroll through. As they are scrolling, they can click one of the posts and navigate to a new page. When they go navigate back from that page, I want them to remain at the same position on the position that they were previously on within the ListView.
PROBLEM
I can not currently keep the stream widget from rebuilding and returning to the scroll position. I know that one of the solutions to this is to include a key; however, I have tried to including the key in the ListView.builder, but it has not worked.
QUESTION
Where should I include the key? Am I using the right type of key?
class Stream extends StatefulWidget {
Stream({Key key, this.user}) : super(key: key);
final User user;
#override
_StreamState createState() => new _StreamState(
user: user
);
}
class _StreamState extends State<Stream> {
_StreamState({this.user});
final User user;
Firestore _firestore = Firestore.instance;
List<DocumentSnapshot> _posts = [];
bool _loadingPosts = true;
int _per_page = 30;
DocumentSnapshot _lastPosts;
ScrollController _scrollController = ScrollController();
bool _gettingMorePosts = false;
bool _morePostsAvailable = true;
_getPosts() async {
Query q = _firestore
.collection('posts')
.document(user.user_id)
.collection('posts')
.orderBy("timePosted", descending: true)
.limit(_per_page);
setState(() {
_loadingPosts = true;
});
QuerySnapshot querySnapshot = await q.getDocuments();
_posts = querySnapshot.documents;
if (_posts.length == 0) {
setState(() {
_loadingPosts = false;
});
}
else {
_lastPosts = querySnapshot.documents[querySnapshot.documents.length - 1];
setState(() {
_loadingPosts = false;
});
}
}
_getMorePosts() async {
if (_morePostsAvailable == false) {
return;
}
if (_gettingMorePosts == true) {
return;
}
if (_posts.length == 0) {
return;
}
_gettingMorePosts = true;
Query q = _firestore
.collection('posts')
.document(user.user_id)
.collection('posts')
.orderBy("timePosted", descending: true)
.startAfter([_lastPosts.data['timePosted']]).limit(_per_page);
QuerySnapshot querySnapshot = await q.getDocuments();
if (querySnapshot.documents.length == 0) {
_morePostsAvailable = false;
}
if(querySnapshot.documents.length > 0) {
_lastPosts = querySnapshot.documents[querySnapshot.documents.length - 1];
}
_posts.addAll(querySnapshot.documents);
setState(() {});
_gettingMorePosts = false;
}
#override
void initState() {
super.initState();
_getPosts();
_scrollController.addListener(() {
double maxScroll = _scrollController.position.maxScrollExtent;
double currentScroll = _scrollController.position.pixels;
double delta = MediaQuery.of(context).size.height * 0.25;
if (maxScroll - currentScroll < delta) {
_getMorePosts();
}
});
}
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
new Expanded(
child: _loadingPosts == true
? Container(
child: Center(
child: Text(" "),
),
)
: Container(
child: Center(
child: _posts.length == 0
? Center(
child: Text("Follow friends", style: TextStyle(fontSize: 15),),
)
: ListView.builder(
key: widget.key,
controller: _scrollController,
itemCount: _posts.length,
itemBuilder: (BuildContext ctx, int index) {
return new Widget(
//paramenters to build the post widget here
);
}),
),
),
),
],
);
}
One thing to note, since I don't want to return all pages (due to Firestore expenses calling so many posts), the build logic is created such that more posts are loaded upon scroll. I realize this may impact it.
Short answer:
You need to provide key to your ListView.builder like this:
ListView.builder(
key: PageStorageKey("any_text_here"),
// ...
)
Long answer:
You can see that when you come back from screen 2 to screen 1, the item 30 remains on the top.
Sorry it was difficult to reproduce your code due to limited availability to the variables you're using. I created a simple example to demonstrate what you're looking for.
Full code:
void main() => runApp(MaterialApp(home: HomePage()));
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView.builder(
key: PageStorageKey("any_text_here"), // this is the key you need
itemCount: 50,
itemBuilder: (_, i) {
return ListTile(
title: Text("Item ${i}"),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => DetailPage(index: i))),
);
},
),
);
}
}
class DetailPage extends StatefulWidget {
final int index;
const DetailPage({Key key, this.index}) : super(key: key);
#override
_DetailPageState createState() => _DetailPageState();
}
class _DetailPageState extends State<DetailPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Text(
"You clicked ${widget.index}",
style: Theme.of(context).textTheme.headline,
),
),
);
}
}
This kind of key works:
key: PageStorageKey('Your Key Name'),