PageView is consuming all gestures with embedded webview in flutter - flutter

I have an embedded web view that is nested alongside a pageview builder which controls my three js animations by sending javascript to the embedded web view.
import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_museum/app/data/bloc/artifacts.dart';
import 'package:flutter_museum/app/domain/models/artifact_model.dart';
import 'package:flutter_museum/app/ui/discover/widgets/animatedCardTutorial.dart';
import 'package:flutter_museum/app/ui/discover/widgets/focusPointList.dart';
import 'package:flutter_museum/app/ui/discover/widgets/questionMarkButton.dart';
import 'package:flutter_museum/app/ui/discover/widgets/threeJSViewer.dart';
import 'package:flutter_museum/app/ui/widgets/assetOrCachedNetworkImage.dart';
import 'package:flutter_museum/app/ui/widgets/circularBackButton.dart';
import 'package:flutter_museum/app/ui/widgets/simpleVideo.dart';
import 'package:flutter_museum/app/ui/widgets/videoPlayerControllers.dart';
double deviceHeight = 0.0;
double deviceWidth = 0.0;
class ArtifactView extends StatefulWidget {
ArtifactView({
Key? key,
}) : super(key: key);
#override
State<ArtifactView> createState() => _ArtifactViewState();
}
class _ArtifactViewState extends State<ArtifactView> {
bool _fullscreen = false;
AbstractVideoPlayerController? videoPlayerController;
Timer? _restoreUiTimer;
int? startUpCount;
bool shouldShowTutorial = false;
void startFullScreen() {
if (!_fullscreen) {
setState(() {
_fullscreen = true;
_restoreUiTimer?.cancel();
_restoreUiTimer = Timer(Duration(milliseconds: 1500), endFullScreen);
});
}
}
void endFullScreen() {
setState(() {
_fullscreen = false;
});
}
void hideTutorial() {
setState(() {
shouldShowTutorial = false;
});
}
#override
void initState() {
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
super.initState();
}
#override
void dispose() {
SystemChrome.setPreferredOrientations(DeviceOrientation.values);
videoPlayerController?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
// hide status bar when displaying fullscreen artifact. (otherwise show status bar)
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: _fullscreen ? [] : SystemUiOverlay.values);
return BlocConsumer<ArtifactsBloc, ArtifactsState>(
listener: (context, state) {},
builder: (context, state) {
Artifact artifact = state.hasArtifact ? state.currentArtifact : Artifact.empty;
//List<FocusPoint> focusPoints = state.hasArtifact ? state.focusPoints : [];
//String modelFilePath = state.modelPath;
//bool modelIsAsset = isAssetUrl(modelFilePath);
if (videoPlayerController == null && artifact.uuid == 'ironman_artifact') {
// EasterEgg // TODO - have Kodi add the polka.mp3 file to our CDN and reference that below instead of userbob.com
videoPlayerController = createVideoPlayerController('https://userbob.com/motb/polka.mp3', useChewie: true);
videoPlayerController?.initialize();
videoPlayerController?.play();
}
// FIXME - trying to always display background sized to device's dimensions so that the background
// doesn't shift when switching to full-screen mode. The code below will prevent the background from
// shifting after the first time we've gone into full-screen mode. However, the very first time
// their will be a slight shift.
//!FIXME - We should look into the overlfow widdget to let the scaffold know we intend to let this widget be larger than the screen.
var windowSize = MediaQuery.of(context).size;
if (windowSize.height > deviceHeight) deviceHeight = windowSize.height;
if (windowSize.width > deviceWidth) deviceWidth = windowSize.width;
return Center(
child: Container(
height: deviceHeight,
width: deviceWidth,
child: Stack(
children: [
if (videoPlayerController != null)
SimpleVideo(
placeholderImage: 'asset://images/discover/washingtonrevelations.webp',
videoPlayerController: videoPlayerController!,
),
if (artifact.backgroundUrl.isNotEmpty)
Container(
height: deviceHeight,
width: deviceWidth,
child: AssetOrCachedNetworkImage(imageUrl: artifact.backgroundUrl, fit: BoxFit.cover),
),
Container(
decoration: BoxDecoration(
gradient: RadialGradient(
radius: 0.9,
colors: [
Colors.transparent,
Colors.transparent,
Colors.black38,
],
),
),
height: deviceHeight,
width: deviceWidth,
),
Listener(
onPointerDown: (PointerDownEvent e) {
startFullScreen();
},
child: GestureDetector(
onPanEnd: ((details) {
print('ended');
}),
child: ThreeJSViewer(
modelFilePath: state.modelPath,
initScale: artifact.cameraZoom.toInt(),
autoRotateSpeed: 2,
state: state,
),
),
),
Align(
alignment: Alignment.topLeft,
child: Padding(
padding: EdgeInsets.only(top: 35),
child: CircularBackButton(
outterPadding: EdgeInsets.fromLTRB(10, 10, 0, 0),
onTap: () {
AutoRouter.of(context).pop();
},
),
),
),
AnimatedPositioned(
bottom: 60 - (_fullscreen ? 250 : 0.0),
duration: Duration(milliseconds: 300),
child: GestureDetector(
child: FocusPointList(
state: state,
),
),
),
],
),
),
);
},
);
}
}
finally here is my WebView plus widget
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_museum/app/data/bloc/artifact/artifacts_state.dart';
import 'package:internet_connection_checker/internet_connection_checker.dart';
import 'package:webview_flutter_plus/webview_flutter_plus.dart';
// ignore: must_be_immutable
class ThreeJSViewer extends StatefulWidget {
final String? modelFilePath;
final String? modelURLPath;
final double? autoRotateSpeed;
final int initScale;
final ArtifactsState state;
final bool? debug;
final void Function(PointerMoveEvent)? onPointerMove;
ThreeJSViewer({
Key? key,
this.modelFilePath,
this.onPointerMove,
this.modelURLPath,
required this.initScale,
this.autoRotateSpeed,
required this.state,
this.debug,
}) : super(key: key);
#override
State<ThreeJSViewer> createState() => _ThreeJSViewerState();
}
class _ThreeJSViewerState extends State<ThreeJSViewer> {
int progress = 0;
WebViewPlusController? controller;
bool isRendered = false;
#override
Widget build(BuildContext context) {
return FutureBuilder(
builder: (context, snapshot) {
if (snapshot.data == true) {
return Stack(
children: [
if (progress < 100 && isRendered == true)
Align(
alignment: Alignment.center,
child: Container(
height: 100,
width: 100,
child: CircularProgressIndicator(
value: progress.toDouble(),
),
),
),
WebViewPlus(
initialCookies: [],
gestureNavigationEnabled: true,
zoomEnabled: false,
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>[
new Factory<OneSequenceGestureRecognizer>(
() {
print('GestureRecognizer created');
return new EagerGestureRecognizer();
},
),
].toSet(),
backgroundColor: Colors.transparent,
javascriptMode: JavascriptMode.unrestricted,
javascriptChannels: ({
JavascriptChannel(
name: 'JavascriptChannel',
onMessageReceived: (message) {
if (message == "200") {
setState(() {
isRendered = true;
});
}
log(message.message);
},
)
}),
onProgress: (_progress) {
setState(() {
progress = _progress;
});
},
onWebViewCreated: (_controller) {
controller = _controller;
_controller.loadUrl("assets/renderer/index.html");
},
onPageFinished: (value) {
Future.delayed(Duration(milliseconds: 100), () {
this.widget.state.controller = controller?.webViewController;
this.widget.state.controller!.runJavascript(
'init("${this.widget.modelFilePath ?? this.widget.modelURLPath}", ${this.widget.initScale},${this.widget.initScale},${this.widget.initScale}, ${this.widget.autoRotateSpeed ?? 2}, ${this.widget.debug ?? false})',
);
});
},
onPageStarted: (value) {
log(value);
},
onWebResourceError: (value) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error loading model. Please make sure you have internet connection.'),
),
);
log(value.description);
},
),
],
);
} else {
return Center(
child: Container(
child: DefaultTextStyle(
style: TextStyle(color: Colors.white, fontSize: 25),
child: Text(
"no internet connection",
),
),
),
);
}
},
future: InternetConnectionChecker().hasConnection,
);
}
}
My WebView is able to accept all gesture events when I initially rotate or zoom the three js model, but when I swipe a card on the pageview builder it consumes whatever gesture event for the duration of the widget lifetime.
finally here are the pageview cards
import 'package:flutter/material.dart';
import 'package:flutter_museum/app/data/bloc/artifact/artifacts_state.dart';
import 'package:flutter_museum/app/domain/models.dart';
import 'package:flutter_museum/app/ui/discover/views/focusPointView.dart';
import 'package:flutter_museum/app/ui/discover/widgets/focusPointCard.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter_museum/app/data/bloc/artifacts.dart';
class FocusPointList extends StatefulWidget {
final ArtifactsState state;
const FocusPointList({Key? key, required this.state}) : super(key: key);
#override
State<FocusPointList> createState() => _FocusPointListState();
}
class _FocusPointListState extends State<FocusPointList> {
#override
Widget build(BuildContext context) {
int _centeredFocusPointIndex = 0;
ArtifactsState _state = this.widget.state;
List<FocusPoint> focusPoints = _state.hasArtifact ? _state.focusPoints : [];
List artifactFocusPointsList = focusPoints
.map(
(fp) => FocusPointCard(
publisher: fp.publisher ?? '',
subtitle: fp.subtitle ?? '',
title: fp.title,
imageUrl: fp.thumbnailImageUrl,
externalLink: fp.externalLink,
cardOnTap: () => _handleTapOnCard(context: context, focusPoint: fp),
),
)
.toList();
return Container(
height: 160,
width: MediaQuery.of(context).size.width,
child: PageView.builder(
itemCount: artifactFocusPointsList.length,
controller: PageController(viewportFraction: 0.75),
onPageChanged: (int index) {
setState(() {
_centeredFocusPointIndex = index;
FocusPoint afp = focusPoints[index];
bool isPointsEmpty = afp.x + afp.y + afp.z != 0;
if (index < focusPoints.length - 1 && isPointsEmpty) {
_centeredFocusPointIndex = index;
_state.controller?.runJavascript('tweenCamera(${afp.x}, ${afp.y}, ${afp.z}, 2000);');
}
});
},
itemBuilder: (_, index) {
return Transform.scale(
scale: index == _centeredFocusPointIndex ? 1 : 0.9,
child: artifactFocusPointsList[index],
);
},
),
);
}
void _handleTapOnCard({required BuildContext context, required FocusPoint focusPoint}) {
if (focusPoint.externalLink != null) {
launch(focusPoint.externalLink!);
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FocusPointView(
title: focusPoint.title,
subtitle: focusPoint.subtitle ?? '',
publisher: focusPoint.publisher ?? '',
imageUrl: focusPoint.detailImageUrl ?? '',
description: focusPoint.description ?? '',
),
),
);
}
}
}

Related

Audio not starting when loading Just Audio player

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.

make a pagination with flutter and bloc

ServiceRepo
import 'package:service_youtube/data/service_model.dart';
import 'package:http/http.dart';
class ServicesRepo {
final _url = "http://osamastartup.osamamy-class.com/api/services";
Future<List<Datum>> getServices(int page) async {
final response = await get(Uri.parse("$_url?page=$page"));
final services = serviceFromJson(response.body);
return services.data;
}
}
the _url and everything is correct and return data without any problem
ServiceBloc:
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:service_youtube/blocs/service_bloc/service_event.dart';
import 'package:service_youtube/blocs/service_bloc/service_state.dart';
import '../../data/service_repo.dart';
class ServiceBloc extends Bloc<ServiceEvent, ServiceState> {
final ServicesRepo repo;
int page = 1;
bool isFetching = false;
ServiceBloc(this.repo) : super(ServiceLoadingState()) {
on<LoadServicesEvent>((event, emit) async {
emit(ServiceLoadingState());
try {
final services = await repo.getServices(page);
emit(ServiceLoadedState(services: services));
page++;
} catch(e) {
emit(ServiceErrorState(msg: e.toString()));
}
});
}
}
surly i have all states and events , and there are no problem with them,
ServiceView
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:service_youtube/blocs/internet_bloc/internet_bloc.dart';
import 'package:service_youtube/blocs/service_bloc/service_bloc.dart';
import 'package:service_youtube/blocs/service_bloc/service_event.dart';
import 'package:service_youtube/data/service_model.dart';
import '../blocs/internet_bloc/internet_state.dart';
import '../blocs/service_bloc/service_state.dart';
class ServiceView extends StatelessWidget {
ServiceView({Key? key}) : super(key: key);
final List<Datum> _services = [];
final ScrollController _scrollController = ScrollController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Services App"),),
body: Container(
padding: EdgeInsets.all(5),
margin: EdgeInsets.all(5),
child: Center(
child: BlocListener<ConnectedBloc, ConnectedState>(
listener: (context, state) {
if (state is ConnectedSucessState) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Internet Connected')));
} else if (state is ConnectedFailureState) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Internet Lost')));
}
},
child: BlocBuilder<ServiceBloc, ServiceState>(
builder: (context, state) {
if(state is ServiceLoadingState && _services.isEmpty) {
return const CircularProgressIndicator();
} else if (state is ServiceLoadedState) {
_services.addAll(state.services);
context.read<ServiceBloc>().isFetching = false;
return ListView.separated(
controller: _scrollController
..addListener(() {
if (_scrollController.offset ==
_scrollController.position.maxScrollExtent &&
!context.read<ServiceBloc>().isFetching) {
context.read<ServiceBloc>()
..isFetching = true
..add(LoadServicesEvent());
}
}),
itemCount: _services.length,
separatorBuilder: (context, index) => const SizedBox(height: 50),
itemBuilder: (context, index) {
return ListTile(
title: Text(_services[index].name,),
subtitle: Text(_services[index].info,),
leading: CircleAvatar(
radius: 50,
backgroundImage: NetworkImage(_services[index].image),
),
);
},
);
} else if (state is ServiceErrorState && _services.isEmpty) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
onPressed: () {
context.read<ServiceBloc>()
..isFetching = true
..add(LoadServicesEvent());
},
icon: const Icon(Icons.refresh),
),
const SizedBox(height: 15),
Text(state.msg, textAlign: TextAlign.center),
],
);
} else {
return Container();
}
},
),
),
),
),
);
}
}
the data is appear without any problem , but after i scroll show me just a blank screen, without any error message? where is the problem?

Flutter pagination loading the same data as in page one when scrolling

I'm building a list of news from an api that has next page results as in the image attached.
The api has only two pages with 10 list items each page.
Data is being passed to the widget. My problem is that when I scroll down the view, it loads the same 10 list items from page one.
This is the api I'm using enter link description here
Rest API
//newsModal.dart
class NewsNote {
String banner_image;
String title;
String text;
String sport;
NewsNote(this.banner_image, this.title, this.text, this.sport);
NewsNote.fromJson(Map<String, dynamic> json) {
banner_image = json['banner_image'];
title = json['title'];
text = json['text'];
sport = json['sport'];
}
}
//page news
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:jabboltapp/models/newsModal.dart';
class JabNews extends StatefulWidget {
#override
_JabNewsState createState() => _JabNewsState();
}
class _JabNewsState extends State<JabNews> {
ScrollController _scrollController = ScrollController();
bool isLoading = false;
String url = "https://jabbolt.com/api/news";
List<NewsNote> _newsNotes = List<NewsNote>();
Future<List<NewsNote>> fetchNewsNotes() async {
if (!isLoading) {
setState(() {
isLoading = true;
});
var response = await http.get(url);
var newsNotes = List<NewsNote>();
if (response.statusCode == 200) {
url = jsonDecode(response.body)['next'];
var newsNotesJson = json.decode(response.body)["results"];
for (var newsNoteJson in newsNotesJson) {
newsNotes.add(NewsNote.fromJson(newsNoteJson));
}
setState(() {
isLoading = false;
_newsNotes.addAll(newsNotes);
});
} else {
setState(() {
isLoading = false;
});
}
return newsNotes;
}
}
#override
void initState() {
fetchNewsNotes().then((value) {
setState(() {
_newsNotes.addAll(value);
});
});
this.fetchNewsNotes();
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
fetchNewsNotes();
}
});
}
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
Widget _buildProgressIndicator() {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Opacity(
opacity: isLoading ? 1.0 : 00,
child: CircularProgressIndicator(),
),
),
);
}
Widget _buildList() {
return ListView.builder(
itemBuilder: (BuildContext context, int index) {
if (index == _newsNotes.length) {
return _buildProgressIndicator();
} else {
return Padding(
padding: EdgeInsets.all(8.0),
child: Card(
child: ListTile(
title: Text((_newsNotes[index].title)),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(_newsNotes[index])));
},
),
),
);
}
},
controller: _scrollController,
itemCount: _newsNotes.length,
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: dGrey,
appBar: AppBar(
title: Text(
"News",
style: TextStyle(
color: textGrey,
fontFamily: 'bison',
fontSize: 32.0,
letterSpacing: 1.2,
),
),
backgroundColor: Colors.transparent,
elevation: 0,
),
body: Container(
child: _buildList(),
),
);
}
}
You need to add the page number concatenation in the URL
https://jabbolt.com/api/news?page=2

Flutter : How to get information about which picture in the grid has been selected?

How to get information about which picture in the grid has been selected in flutter. Please check my code!
I really need an answer to this. Please help me. I am looking forward to hearing from all of you.
Getting image and put it into a grid.
ImageEvalation.dart
class ImageEvaluation extends StatefulWidget {
#override
_ImageEvaluationState createState() => _ImageEvaluationState();
}
class _ImageEvaluationState extends State<ImageEvaluation> {
File _selectedFile;
bool _inProcess = false;
String colorCode = '#33695d';
getImage(ImageSource source, BuildContext context) async {
this.setState(() {
_inProcess = true;
});
// File image = await ImagePicker.pickImage(source: source);
final _picker = ImagePicker();
PickedFile image = await _picker.getImage(source: source);
if (image != null) {
// Remove crop attribute if we don't want to resize the image
File cropped = await ImageCropper.cropImage(
sourcePath: image.path,
aspectRatio: CropAspectRatio(ratioX: 1, ratioY: 1),
compressQuality: 100, // 100 means no compression
maxWidth: 700,
maxHeight: 700,
compressFormat: ImageCompressFormat.jpg,
androidUiSettings: AndroidUiSettings(
toolbarColor: HexColor(colorCode),
toolbarTitle: "RPS Cropper",
statusBarColor: HexColor(colorCode),
backgroundColor: Colors.white,
//toolbarWidgetColor: HexColor(colorCode),
activeControlsWidgetColor: HexColor(colorCode),
//dimmedLayerColor: HexColor(colorCode),
cropFrameColor: HexColor(colorCode),
cropGridColor: HexColor(colorCode),
),
);
this.setState(() {
_selectedFile = cropped;
_inProcess = false;
//_showDelete = true;
});
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => UploadScreen(
image: _selectedFile,
),
),
);
} else {
this.setState(() {
_inProcess = false;
});
}
}
#override
Widget build(BuildContext context) {
return _inProcess
? Loading()
: Scaffold(
body: StreamProvider<List<ImageProperty>>.value(
value: User_DatabaseService().pictureData,
child: SingleChildScrollView(
child: Center(
child: Column(
children: <Widget>[
Text('GridView'),
PictureLinkGrid(),
Text('Image Evulation'),
MaterialButton(
onPressed: () {
getImage(ImageSource.camera, context);
},
color: Colors.deepOrange,
child: Text(
'NEXT',
style: TextStyle(color: Colors.white),
),
),
],
),
),
),
),
);
}
}
Make a grid for my images.
PictureGrid.dart
class PictureLinkGrid extends StatefulWidget {
#override
_PictureLinkGridState createState() => _PictureLinkGridState();
}
class _PictureLinkGridState extends State<PictureLinkGrid> {
#override
Widget build(BuildContext context) {
final pictureData = Provider.of<List<ImageProperty>>(context) ?? [];
final neededPicture = [];
final demoPicture = [];
int count = 0;
// get Demo Picture
pictureData.forEach((picture) {
if (picture.title.contains('demo')) {
demoPicture.add(picture);
}
});
// get Needed Picture
pictureData.forEach((picture) {
if (picture.display_count < 10 && !picture.title.contains('demo')) {
print('${picture.title} is NOT null');
neededPicture.add(picture);
} else {
print('${picture.title} is null');
}
});
// fill in the empty picture
count = 0;
while (neededPicture.length < 9) {
neededPicture.add(demoPicture[count]);
count++;
}
return GridView.builder(
//itemCount: neededPicture.length,
itemCount: neededPicture.length,
shrinkWrap: true,
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
itemBuilder: (BuildContext context, int index) {
print(
'Picture title in picturelink grid: ${neededPicture[index].title}');
return TouchableWebImageCard(imagePath: neededPicture[index].url);
});
}
}
Make ImageCard which can be clicked and unchecked.
TouchableWebImageCard.dart
class TouchableWebImageCard extends StatefulWidget {
String imagePath;
TouchableWebImageCard({#required this.imagePath});
//
#override
_TouchableWebImageCardState createState() =>
_TouchableWebImageCardState(imagePath);
}
class _TouchableWebImageCardState extends State<TouchableWebImageCard> {
// To pass parameters
double width;
double height;
String imagePath;
_TouchableWebImageCardState(this.imagePath);
//
bool isChecked = false;
double sigmaX = 0.0;
double sigmaY = 0.0;
double showBorder = 0;
//
checkIcon() {
return isChecked
? Center(
child: Icon(
Icons.check_circle,
color: Colors.white,
size: 50,
),
)
: Container();
}
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Center(
child: SpinKitCircle(
color: HexColor('#33695d'),
size: 50,
),
),
Center(
child: InkWell(
child: Stack(
children: <Widget>[
FadeInImage.memoryNetwork(
placeholder: kTransparentImage,
image: imagePath,
),
checkIcon(),
],
),
onTap: () {
print('Image Path: $imagePath');
if (isChecked != true) {
setState(
() {
showBorder = 4.0;
isChecked = !isChecked;
},
);
} else {
setState(
() {
showBorder = 0.0;
isChecked = !isChecked;
},
);
}
},
),
),
],
);
}
}
In your TouchableWebImageCard constructor add another variable int _index;
So will know which image you have selected.
Also, the "right" way to make a constructor is:
class TouchableWebImageCard extends StatefulWidget {
TouchableWebImageCard({#required this.imagePath, this._index});
String imagePath;
int _index;

Flutter appear The method 'findRenderObject' was called on null

I have a need recently,I need to measure the distance between the child elements in the sliver and the top, but always prompt that findrendereobject is empty。
I can't even try widgetsbinding.instance.addpostframecallback
Console error:
════════ Exception caught by scheduler library
═════════════════════════════════════════════════════
The following
NoSuchMethodError was thrown during a scheduler callback: The method
'findRenderObject' was called on null. Receiver: null Tried calling:
findRenderObject()
Please Everybody look what happened,why can't measuring height~thank you very much!!!
My Code:
class GoodsDetailPage extends StatefulWidget {
final IndexVOS bean;
GoodsDetailPage(this.bean);
#override
_GoodsDetailPageState createState() => _GoodsDetailPageState();
}
class _GoodsDetailPageState extends State<GoodsDetailPage> with SingleTickerProviderStateMixin{
int productId;
int areaId;
String serviceTime;
ProductInfoBean productInfoBean;
String _selectName;
Norms norms;
double screenWidth;
bool _isShow = false;
TabController _tabController;
List<String> tabList;
ScrollController _scrollController = ScrollController();
var globalKeyOne = GlobalKey();
var globalKeyTwo = GlobalKey();
var globalKeyThree = GlobalKey();
var oneY = 0.0;
var twoY = 0.0;
var threeY = 0.0;
ProductModel model = ProductModel();
GoodsSpecModel _specModel = GoodsSpecModel();
#override
void initState() {
productId = widget.bean.productId;
areaId = widget.bean.areaId;
serviceTime = widget.bean.serviceTimeBegin;
tabList = [
"商品",
"评价",
"详情",
];
_tabController = TabController(
length: tabList.length,
vsync: this,
);
_scrollController.addListener(() {
var of = _scrollController.offset;
setState(() {
_isShow = of >= screenWidth-50;
});
if (of > threeY - oneY) {
_tabController.animateTo(2);
}else if (of > twoY - oneY) {
_tabController.animateTo(1);
} else {
_tabController.animateTo(0);
}
print("滚动了$of one=${twoY - oneY}=two=${threeY - oneY}");
});
WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
super.initState();
}
//等待界面渲染完成
void _afterLayout(_){
oneY = getY(globalKeyOne.currentContext);
twoY = getY(globalKeyTwo.currentContext);
threeY = getY(globalKeyThree.currentContext);
}
static double getY(BuildContext buildContext) {
final RenderBox box = buildContext.findRenderObject();
//final size = box.size;
final topLeftPosition = box.localToGlobal(Offset.zero);
return topLeftPosition.dy;
}
Future _request() async{
model.requestGoodsDetail(productId: productId,areaId: areaId,serviceTime: serviceTime);
}
#override
Widget build(BuildContext context) {
screenWidth = MediaQuery.of(context).size.width;
return Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
child: ProviderWidget<ProductModel>(
model: model,
onModelReady: (model) => model.requestGoodsDetail(productId: productId,areaId: areaId,serviceTime: serviceTime),
builder: (context, model, child) {
if (model.busy) {
return ViewStateBusyWidget();
}
if (model.error) {
return ViewStateErrorWidget(error: model.viewStateError, onPressed: _request);
}
return Column(
children: <Widget>[
Expanded(child: _body()),
_bottom()
],
);
}
),
)
);
}
Widget _body(){
var _normalBack = Image.asset(ImagePath.normalBack,height: dp40,width: dp40);
var _detailBack = Image.asset(ImagePath.detailBack,height: dp60,width: dp60);
var _normalShare = Image.asset(ImagePath.detailShareSmall,height: dp40,width: dp60);
var _detailShare = Image.asset(ImagePath.detailShare,height: dp60,width: dp140);
var _normalDot = Image.asset(ImagePath.threeDot,height: dp40,width: dp40);
var _detailDot = Image.asset(ImagePath.detailDot,height: dp60,width: dp60);
return CustomScrollView(
controller: _scrollController,
slivers: <Widget>[
SliverAppBar(
key: globalKeyOne,
title: _isShow?_topTabTitle():Container(),
centerTitle: true,
expandedHeight: screenWidth,
floating: false,
pinned: true,
snap: false,
elevation: 0.5,
leading: IconButton(
icon: _isShow?_normalBack:_detailBack,
onPressed: () => pop(),
),
actions: <Widget>[
GestureDetector(
child: _isShow?_normalShare:_detailShare,
onTap: (){
print("分享");
},
),
SizedBox(width: dp24),
GestureDetector(
child: _isShow?_normalDot:_detailDot,
onTap: _showPopup,
),
SizedBox(width: dp30),
],
flexibleSpace: FlexibleSpaceBar(
background: GoodsDetailBannerWidget(model),
),
),
SliverList(
delegate: SliverChildListDelegate([
_activityImage(),
GoodsDetailInfoWidget(model),
gap20,
GoodsDetailOtherWidget(model,serviceTime),
GoodsDetailCategoryWidget(click: _clickSpec,name: _selectName),
gap20,
Column(
key: globalKeyTwo,
children: <Widget>[
GoodsDetailCommentWidget(model),
gap20,
],
),
Container(
key: globalKeyThree,
child: GoodsDetailDescWidget(model),
)
]),
),
],
);
}
Widget _bottom(){
return GoodsDetailBottomWidget(
clickBuy: (){
if(_selectName == null){
_clickSpec();
return;
}
routePush(ConfirmOrderPage(productInfoBean: model.productInfoBean,norms: norms,count: _specModel.countNumber));
},
);
}
Widget _activityImage(){
return Container(
width: double.infinity,
height: dp80,
child: Image.asset(ImagePath.goodsActivity),
);
}
Widget _topTabTitle(){
return Container(
height: dp60,
child: TabBar(
controller: _tabController,
labelColor: ColorConfig.themeGreen,
unselectedLabelColor: ColorConfig.C09,
labelStyle: StyleConfig.green_36Style,
unselectedLabelStyle: StyleConfig.normalTitle36Style,
indicatorColor: ColorConfig.themeGreen,
indicatorSize: TabBarIndicatorSize.label,
indicatorWeight: 2,
isScrollable: true,
labelPadding: EdgeInsets.symmetric(horizontal: dp20),
tabs: tabList.map((item) {
return Tab(
text: item,
);
}).toList(),
onTap: (index){
switch(index){
case 0:
_scrollController.jumpTo(0);
_tabController.animateTo(0);
break;
case 1:
_scrollController.jumpTo(twoY - oneY);
_tabController.animateTo(1);
break;
case 2:
_scrollController.jumpTo(threeY - oneY);
_tabController.animateTo(2);
break;
}
},
),
);
}
void _showPopup(){
showPopupWindow(
context,
gravity: KumiPopupGravity.rightTop,
underStatusBar: true,
underAppBar: true,
offsetX: -dp30,
offsetY: -dp20,
childFun: (pop){
return Container(
key: GlobalKey(),
child: GoodsDetailPopupMenuWidget(),
);
}
);
}
void _clickSpec(){
_specModel.initData(model.normInfoBean.norms);
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return GoodsSpecSelectDialog(
model: _specModel,
onSelected: (bean, count) {
norms = bean;
setState(() {
_selectName = "已选:"+bean.normName + ",数量:" + count.toString();
});
},
);
},
);
}
#override
void dispose() {
_specModel.dispose();
_scrollController.dispose();
_tabController.dispose();
super.dispose();
}
}
Your global keys are attached to the widgets in _body() method but this method is just called when model.busy = false and model.error = false. It means globalKeyOne.currentContext will be null when model.busy = true || model.error = true. Your _afterLayout(...) is called in all the cases, that's why it fails with NPE.
use widgetsbinding.instance.addpostframecallback
if you put the method getPosition in the widgetsbinding..... then remember to add this in the parameter of getPosition(_)
for some reason this works
Here is the Sample Code
import 'package:flutter/material.dart';
class Test extends StatefulWidget {
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
GlobalKey _akey = GlobalKey();
var top;
var left;
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
height: 200,
width: 300,
key: _akey,
color: Colors.blue,
),
),
);
}
_afterLayout(_) {
_getPosition();
}
_getPosition() async {
final RenderBox rb = _akey.currentContext.findRenderObject();
var position = rb.localToGlobal(Offset.zero);
top = position.dy;
left = position.dx;
print('$left , $top');
}
}
try putting it in build Function not initState()