Is there a way to precache Lottie animations in flutter? - flutter

I have a lottie animation that I'm using as a loading screen:
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:lottie/lottie.dart';
class Loading extends StatelessWidget {
final String loadingText;
Loading({this.loadingText});
Widget build(BuildContext context) {
return Container(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (loadingText != null) _buildLoadingText(loadingText),
_buildAnimation(),
],
),
);
}
Widget _buildLoadingText(String text) {
return Text(
loadingText,
style: GoogleFonts.poppins(
textStyle:
TextStyle(fontWeight: FontWeight.w500, color: Colors.black)),
);
}
Widget _buildAnimation() {
return Lottie.asset('assets/lottie/heartbeat_loading.json',
width: 300, repeat: true, animate: true);
}
}
And I'm using it like this for when my app initially loads:
_determineHome() {
return StreamBuilder(
stream: AppBlocContainer().authenticationBloc().loggedIn,
builder: (context, AsyncSnapshot<AuthenticationStatus> snapshot) {
// return Loading();
return AnimatedSwitcher(
duration: Duration(seconds: 2),
child: !snapshot.hasData
? Loading(
loadingText: 'Loading...',
)
: _buildSecondChild(snapshot.data));
},
);
This works, except that the lottie animation is being loaded maybe a second or two too late, which by the time the lottie asset loads, it's too late and the page has already transitioned.
I was wondering since I did was able to precache my SVG images in my main() by doing this:
Future.wait([
precachePicture(
ExactAssetPicture(
SvgPicture.svgStringDecoder, 'assets/svg/login.svg'),
null,
),
precachePicture(
ExactAssetPicture(
SvgPicture.svgStringDecoder, 'assets/svg/logo.svg'),
null,
),
precachePicture(
ExactAssetPicture(
SvgPicture.svgStringDecoder, 'assets/svg/signup_panel_1.svg'),
null,
)
]);
Would I be able to do the same thing with lottie?

what I found is that lottie package in flutter, itself got a built in auto cache.(want it or not it will cache the animation into ram)
method A(own made):
so you basically need to call Lottie.asset('assets/Logo.json') once and the second time you will be able to load it without delay by calling Lottie.asset('assets/Logo.json') again because it was cached at the first time.
so this can be used as a cheat cache method, for example you can load it first in splash screen like below:
either by wrapping with a SizedBox and giving 0 height/width or using Visibility widget with visible property of false but maintainState to true, so its not visible but still will be in widget tree and load into ram.
Visibility(
visible: false,
maintainSize: false,
maintainState: true,
maintainAnimation: false,
maintainInteractivity: false,
child: Row(
children: [
Lottie.asset('assets/Logo.json'),
],
),
),
next time where you need just call Lottie.asset('assets/Logo.json') and it will load without delay.
method B:
this is mentioned by a guy called Pante on github.
class LottieCache {
final Map<String, LottieComposition> _compositions = {};
/// Caches the given [LottieAsset]s.
Future<void> add(String assetName) async {
_compositions[assetName] = await AssetLottie(assetName).load();
}
Widget load(String assetName, Widget fallback) {
final composition = _compositions[assetName];
return composition != null ? Lottie(composition: composition) : fallback;
}
}
there are 2 ways here:
1)use AssetLottie(assetName).load() method; provide the LottieCache object in order to access its load method where ever you need.
2)completly neglect load method and the composition way(you can comment the load method);because AssetLottie(assetName).load() loads the animation into ram just as the same as Lottie.asset('assets/Logo.json')
so just calling its add method will load animation into ram and you can use it without delay next time where you need.
method C:
this is a custom loading method mentioned in lottie documentation for flutter.
class _MyWidgetState extends State<MyWidget> {
late final Future<LottieComposition> _composition;
#override
void initState() {
super.initState();
_composition = _loadComposition();
}
Future<LottieComposition> _loadComposition() async {
var assetData = await rootBundle.load('assets/LottieLogo1.json');
return await LottieComposition.fromByteData(assetData);
}
#override
Widget build(BuildContext context) {
return FutureBuilder<LottieComposition>(
future: _composition,
builder: (context, snapshot) {
var composition = snapshot.data;
if (composition != null) {
return Lottie(composition: composition);
} else {
return const Center(child: CircularProgressIndicator());
}
},
);
}
}

Related

Flutter Looping Endlessly when adding a FutureBuilder Inside another FutureBuilder

I have a Stop Watch timer in a Flutter Screen which works as required in the beginning inside a FutureBuilder but when I add another futurebuilder to add information from an API it keeps looping indefinitely:
Initially when I opened the screen it was looping indefinelty then when I adjusted the code it looped endlessly only when I clicked the Start button, it continued looping even after clicking the stop button.
Here is the dart file:
class Screen extends StatefulWidget {
#override
_ScreenState createState() => _ScreenState();
}
class _ScreenState extends State<Screen> {
bool isStartButtonDisabled = false;
bool isStopButtonDisabled = true;
Stopwatch _stopwatch = Stopwatch();
String _elapsedTime = "00:00:00";
late Timer _timer;
void _startStopwatch() {
print("void _startStopwatch() { Starting stopwatch");
_stopwatch.start();
setState(() {
isStartButtonDisabled = true;
isStopButtonDisabled = false;
});
_timer = Timer.periodic(
const Duration(seconds: 1),
(Timer timer) {
if (mounted) {
setState(() {
_elapsedTime = _stopwatch.elapsed.toString().split(".")[0];
});
}
},
);
}
#override
void dispose() {
_timer?.cancel();
super.dispose();
}
Here is ui
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView(
children: [
Container(
child: Column(
children: [
GFButton(
onPressed: isStartButtonDisabled ? null : startWorkout,
text: "Start Workout",
),
Text(
_elapsedTime,
),
Card(
child: Padding(
padding: const EdgeInsets.all(18.0), //change here
child: Column(
children: [
FutureBuilder<List<Model_No_1>>(
future: futureModel_No_1,
builder: (BuildContext context,
AsyncSnapshot<List<Model_No_1>> snapshot) {
if (snapshot.hasData) {
return Column(
children: List.generate(snapshot.data!.length,
(int index) {
String Some_VariableName =
snapshot.data![index].name;
return Column(
children: [
Text(
snapshot.data![index].name,
),
Builder(builder: (context) {
return Container(
child: SingleChildScrollView(
child: Column(
children: [
Card(
child: Row(
children: [
Expanded(
child: FutureBuilder<
Get_Old_Model_No>(
future: () {
final Map<String,dynamic> arguments =ModalRoute.of(context)!.settings.arguments as Map<String,dynamic>;final int id =arguments['id'] ??
0;print("This is the id $id");return APIService.Get_Old_Model_No(id);}(),builder:(context, snapshot) {print("Snapshot data:${snapshot.data}");print("Snapshot error:${snapshot.error}");
if (snapshot.hasData) {
return Column(
children: [
// Text(snapshot
// .data
// ?.endDate),
],
);
} else if (snapshot
.hasError) {
return Text(
"${snapshot.error}");
}
return CircularProgressIndicator();
},),),],),),
for (var breakdown in snapshot.data![index].breakdowns)
Form(child: Expanded(child: Column(children: [TextFormField(
keyboardType:TextInputType.number,
onChanged:(value) {
final int?parsedValue =
int.tryParse(value);if (parsedValue !=
null) {setState(
() {variable1 =parsedValue;});} else {}
},),],),),),
When I add the Expanded, which runs the APIService.Get_Old_Model_No(id);
My question is why is the indefinete looping happening? How can I fix it ?
i cannot reproduce your problem from your provided code, but having multiple encapsuled futureBuilders shouldn't be a problem.
Any chance that the setState gets called from within one of your future(Builder)s?
That would trigger a rebuild, reloading the future and then setting the state again, triggering a rebuilt, and so on
When you call setState the build method is called, this triggers the FutureBulder to rebuild, thereby calling the future again.
What I see happening in your case is that once the _startStopwatch is called, the timer is started and it calls setState every second. I can't see your stop function, but it seems you are not canceling the Timer.periodic when you stop the stopWatch.
To solve your problem, you need to manage your state in such a way that only the necessary widget rebuild when the state changes. To achieve this you will need more than setState. See a simple example below using ValueNotifier and ValueListenableBuilder.
class Screen extends StatefulWidget {
#override
_ScreenState createState() => _ScreenState();
}
class _ScreenState extends State<Screen> {
ValueNotifier<bool> isStartButtonDisabled = ValueNotifier(false);
ValueNotifier<bool> isStopButtonDisabled = ValueNotifier(true);
Stopwatch _stopwatch = Stopwatch();
ValueNotifier<String> _elapsedTime = ValueNotifier("00:00:00");
late Timer _timer;
void _startStopwatch() {
print("void _startStopwatch() { Starting stopwatch");
_stopwatch.start();
isStartButtonDisabled.value = true;
isStopButtonDisabled.value = false;
_timer = Timer.periodic(
const Duration(seconds: 1),
(Timer timer) {
if (mounted) {
_elapsedTime.value = _stopwatch.elapsed.toString().split(".")[0];
}
},
);
}
#override
void dispose() {
_timer?.cancel();
super.dispose();
}
Now for the widgets that need to rebuild when the state changes, just use ValueListenableBuilder, See below.
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView(
children: [
Container(
child: Column(
children: [
ValueListenableBuilder<bool>(valueListenable: isStartButtonDisabled, builder: (context, value, child)=> GFButton(
onPressed:value ? null : startWorkout,
text: "Start Workout",
)),
ValueListenableBuilder<String>(valueListenable: _elapsedTime, builder: (context, value, child)=> Text(
value,
)),
In this way when _elapsedTime changes, only the Text widget is rebuilt, similarly, when isStartButtonDisabled changes, only GFButton is rebuilt.
Also remember to cancel the Timer when you stop the StopWatch.
You are defining and calling a function in your build method with is getting called every time you set state, which is occuring at every tick of your timer.
() {
final Map<String,dynamic> arguments = ModalRoute.of(context)!.settings.arguments as Map<String,dynamic>;final
int id =arguments['id'] ?? 0;
print("This is the id $id");
return APIService.Get_Old_Model_No(id);
}()

Updating a Slider based on video position

Summarize the problem
I need help finding a Flutter construct which will allow me to repeatedly check the return value of an API function call for getting the current time of a playing video (position). My goal is to update the Slider() to the position of the current video. I am using plugin flutter_vlc_player for playing back videos.
The most important part of the code below is the videoPlayerController.getPosition() function call. I need a way to repeatedly call this function and get the latest value. This is what I am struggling with.
The SeekBar class instantiated is the Slider() I am updating.
I think I am close to a solution as StreamBuilder is meant to update based on events. Also if I perform hot refresh of app after playing a video, the Slider updates once.
What I am seeing is the stream function is called twice but returns null each time because the video isn't playing yet. I need the stream function to be called while the video is playing.
I/flutter (29465): snapshot: null
I/flutter (29465): snapshot: null
One last thing: videoPlayerController.getPosition() is a Future.
Describe what you've tried:
I tried using StreamBuilder() and FutureBuilder() but I got the same results. The current position is only fetched twice when I need it to be continuously fetched during video playback. I checked the Flutter documentation on StreamBuilder but their example only shows when there is one item to be grabbed and not multiple. I need to rebuild the Slider() widget based on the value returned from function repeatedly.
Show some code:
VlcPlayerController videoPlayerController = VlcPlayerController.network(
'rtsp://ip_addr:8554/test',
hwAcc: HwAcc.FULL,
autoPlay: true,
options: VlcPlayerOptions(
video: VlcVideoOptions(),
rtp: VlcRtpOptions(['--rtsp-tcp'],),
extras: ['--h264-fps=30']
),
);
await Navigator.push(context,
MaterialPageRoute(builder: (context) =>
Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.blueAccent,
title: const Text("Playback")),
body: Center(
child: Column(
children: [
VlcPlayer(
controller: videoPlayerController,
aspectRatio: 16/9,
placeholder: const Center(child: CircularProgressIndicator()),
),
StatefulBuilder(builder: (context, setState) {
return Row(
children: [
TextButton(
child: Icon(isPlaying ? Icons.play_arrow : Icons.pause),
style: ButtonStyle(backgroundColor: MaterialStateProperty.all<Color>(Colors.blueAccent),
foregroundColor: MaterialStateProperty.all<Color>(Colors.white)),
onPressed: () {
setState(() {
if(videoPlayerController.value.isPlaying)
{
isPlaying = true;
videoPlayerController.pause();
}
else {
isPlaying = false;
videoPlayerController.play();
}
});
}
),
Text("${videoPlayerController.value.position.inMinutes}:${videoPlayerController.value.position.inSeconds}",
style: const TextStyle(color: Colors.white)),
],
);
}),
StreamBuilder<Duration>(
stream: Stream.fromFuture(videoPlayerController.getPosition()),
builder: (BuildContext context, AsyncSnapshot <Duration> snapshot) {
Duration position = snapshot.data ?? const Duration();
print('snapshot: ${snapshot.data?.inSeconds.toDouble()}');
return Column(
children: [
SeekBar(
duration: const Duration(seconds: 5),
position: position,
onChangeEnd: (newPosition)
{
videoPlayerController.seekTo(newPosition);
},
)
],
);
}
),
]
)
),
)
)
);
Thank you for reading and help. I am still learning Flutter/Dart so any references to helpful classes will be great.
Is there a reason you want to use StreamBuilder in this case? You can use a StatefulWidget and add a listener to the controller. Then update the position inside that listener.
Use this to add listener:
videoPlayerController.addListener(updateSeeker);
Also make sure to remove the listener in dispose method:
videoPlayerController.removeListener(updateSeeker);
Here is the updateSeeker method:
Future<void> updateSeeker() async {
final newPosition = await videoPlayerController.getPosition();
setState(() {
position = newPosition;
});
}
Here is an example of a widget that plays the video and shows its position in a Text widget:
class VideoPlayer extends StatefulWidget {
const VideoPlayer({Key? key}) : super(key: key);
#override
_VideoPlayerState createState() => _VideoPlayerState();
}
class _VideoPlayerState extends State<VideoPlayer> {
final videoPlayerController = VlcPlayerController.network(url);
var position = Duration.zero;
#override
void initState() {
super.initState();
videoPlayerController.addListener(updateSeeker);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
VlcPlayer(
aspectRatio: 16 / 9,
controller: videoPlayerController,
),
Text(position.toString()),
],
),
);
}
Future<void> updateSeeker() async {
final newPosition = await videoPlayerController.getPosition();
setState(() {
position = newPosition;
});
}
#override
void dispose() {
videoPlayerController.removeListener(updateSeeker);
super.dispose();
}
}

Flutter when ListView.builder is refreshed it goes back from the beggining and not from the maxScrollExtent point

I am new in Flutter and I am facing a problem.
What I have so far is that I am getting the data from a web service asynchronously (Json). So in the build method I use a FutureBuilder widget for Scaffold's body argument. And its 'builder' argument (of FutureBuilder widget) returns a ListView.builder to show the data.
Also I use ScrollController for scrolling.
Its time I reach the maximum scroll point I call once again the service to get the next page of data and so on...
The problem is that when I "change page" by scrolling it brings the data correctly, but it starts from the very first line of data and not from the point where i ve reached the maxScrollExtent point.
So if the first 'snapshot' has 25 rows and the second has 15, when I go to the second it starts from row 1 and not from row 26 although the total amount of data row is 40 correctly .
As a result i don't have a smooth scrolling. I have been stuck for enough time to this point and i do not know what I am missing. Do I have to use Page Storage keys (i saw video from flutter team but i haven't found an edge to my problem). Any hints? A sample of code follows.
class _MyAppState extends State<MyApp> {
final ScrollController _controller = ScrollController();
#override
void initState() {
super.initState();
_controller.addListener(_scrollListener);
}
void _scrollListener() {
if (_controller.offset >= _controller.position.maxScrollExtent &&
!_controller.position.outOfRange) {
if (nextDataSet != null) {
print('nextDataSet = ' + nextDataSet);
setState(() {
inDataSet = nextDataSet;
});
} else {
print('nextDataSet = NULL');
}
}
Future<Post> fetchPost([String dataSet]) async {
...
return Post.fromJson(json.decode(utf8.decode(response.bodyBytes)));
}
Widget build(BuildContext context) {
return MaterialApp(
home: Builder(
builder: (context) => Scaffold(
...
body: FutureBuilder<Post>(
future: fetchPost(inDataSet),
builder: _buildList,
),
),
),
);
}
Widget _buildList(BuildContext context, AsyncSnapshot snapshot) {
... /*code that eventually fills the lists of strings
progressivePartyListSec, progressivePartyListThird*/
...
return ListData(controller: _controller, listEidosPerigr:
progressivePartyListSec, listPerigr: progressivePartyListThird );
}
}
class ListData extends StatelessWidget {
final ScrollController controller;
final List<String> listEidosPerigr;
final List<String> listPerigr;
ListData({this.controller,
this.listEidosPerigr,
this.listPerigr});
#override
Widget build(BuildContext context) {
return ListView.builder(
controller: controller,
itemCount: rowsSelected,
itemBuilder: (context, i) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
removeAllHtmlTags(listEidosPerigr[i]),
style: TextStyle(
fontWeight: FontWeight.bold, color: Colors.blue),
),
new RichText(
text: new TextSpan(
text: listPerigr[i].replaceAllMapped('<BR>', (Match m) => '').replaceAllMapped('<b>', (Match m) => '').replaceAllMapped('</b>', (Match m) => ''),
style: DefaultTextStyle.of(context).style,
),
),
]);
});
}
}
after some research i found a solution in the following link: Flutter: Creating a ListView that loads one page at a time
Thanks a lot AbdulRahman AlHamali.
Basically, on the above code that i have posted, i used another argument for the ListView.builder and it is the following key: PageStorageKey('offset'),
Finally as AbdulRahman writes on his article i used the KeepAliveFutureBuilder as a wrapper of FutureBuilder in other worlds in my build method i did...
body: KeepAliveFutureBuilder(
//child: FutureBuilder<Post>(
future: fetchPost(inDataSet),
builder: _buildList,
//),
),

Flutter FutureBuilder refresh when TextField value changes

The _futureData is to used for the FutureBuilder after retrieving value from the _loadPhobias() function.
entry_screen.dart
Future _futureData;
final TextEditingController _textEditingController = TextEditingController();
_loadPhobias() function does not seem to have any problem.
entry_screen.dart
Future<List<String>> _loadPhobias() async =>
await rootBundle.loadString('assets/phobias.txt').then((phobias) {
List _listOfAllPhobias = [];
List<String> _listOfSortedPhobias = [];
_textEditingController.addListener(() {
...
});
return _listOfSortedPhobias;
});
#override
void initState() {
super.initState();
_futureData = _loadPhobias();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: TextField(
// When the value is changed, the value returned from the _loadPhobias will also change. So I want the FutureBuilder to be rebuilt.
onChanged: (text) { setState(() => _futureData = _loadPhobias()) },
),
),
body: FutureBuilder(
future: _futureData,
builder: (context, snapshot) {
return snapshot.hasData
? ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) => Column(
children: <Widget>[
PhobiasCard(sentence: snapshot.data[index]),
)
],
))
: Center(
child: CircularProgressIndicator(),
);
},
),
),
);
}
This is the error that I got:
FlutterError (setState() callback argument returned a Future.
The setState() method on _EntryScreenState#51168 was called with a closure or method that returned a Future. Maybe it is marked as "async".
Instead of performing asynchronous work inside a call to setState(), first execute the work (without updating the widget state), and then synchronously update the state inside a call to setState().)
The first thing to note, you mentioned that you want to rebuild your app every time there's a change in the text. For that to happen, you should use StreamBuilder instead. FutureBuilder is meant to be consumed once, it's like a fire and forget event or Promise in JavaScript.
Here's to a good comparison betweenStreamBuilder vs FutureBuilder.
This is how you would refactor your code to use StreamBuilder.
main.dart
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyAppScreen(),
);
}
}
class MyAppScreen extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return MyAppScreenState();
}
}
class MyAppScreenState extends State<MyAppScreen> {
StreamController<List<String>> _phobiasStream;
final TextEditingController _textEditingController = TextEditingController();
void _loadPhobias() async =>
await rootBundle.loadString('lib/phobia.txt').then((phobias) {
List<String> _listOfSortedPhobias = [];
for (String i in LineSplitter().convert(phobias)) {
for (String t in _textEditingController.text.split('')) {
if (i.split('-').first.toString().contains(t)) {
_listOfSortedPhobias.add(i);
}
}
}
_phobiasStream.add(_listOfSortedPhobias);
});
#override
void initState() {
super.initState();
_phobiasStream = StreamController<List<String>>();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: TextField(
controller: _textEditingController,
onChanged: (text) {
print("Text $text");
_loadPhobias();
},
),
),
body: StreamBuilder(
stream: _phobiasStream.stream,
builder: (context, snapshot) {
return snapshot.hasData
? Container(
height: 300,
child: ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
print("Data ${snapshot.data[index]}");
return Text(snapshot.data[index]);
},
),
)
: Center(
child: CircularProgressIndicator(),
);
},
),
);
}
}
As seen in the code above, I eliminated unnecessary text change callbacks inside the for a loop.
lib/phobia.txt
test1-test2-test3-test4-test5
Let me know if this is the expected scenario.
Hope this helps.
The solution can be inferred in the third line of the error message:
Instead of performing asynchronous work inside a call to setState(), first execute the work (without updating the widget state), and then synchronously update the state inside a call to setState().)
So this means you'll have to perform the operation before refreshing the widget. You can have a temporary variable to hold the result of the asynchronous work and use that in your setState method:
onChanged: (text) {
setState(() => _futureData = _loadPhobias())
},
Could be written as:
onChanged: (text) async {
var phobias = _loadPhobias();
setState(() {
_futureData = phobias;
});
},

Flutter - trigger navigation when Provider variable changes

I'm trying to show a splash screen on initial app startup until I have all of the data properly retrieved. The retrieval is done by a class called "ProductData" As soon as it's ready, I want to navigate from the splash page to the main screen of the app.
Unfortunately, I can't find a good way to trigger a method that runs that kind of Navigation and listens to a Provider.
This is the code that I'm using to test this idea. Specifically, I want to run the command Navigator.pushNamed(context, 'home'); when the variable shouldProceed becomes true. Unfortunately, the code below gives me the error, "setState() or markNeedsBuild() called during build."
import 'package:catalogo/firebase/ProductData.dart';
import 'package:flutter/material.dart';=
import 'package:provider/provider.dart';
class RouteSplash extends StatefulWidget {
#override
_RouteSplashState createState() => _RouteSplashState();
}
class _RouteSplashState extends State<RouteSplash> {
bool shouldProceed = false;
#override
Widget build(BuildContext context) {
shouldProceed =
Provider.of<ProductData>(context, listen: true).shouldProceed;
if (shouldProceed) {
Navigator.pushNamed(context, 'home'); <-- The error occurs when this line is hit.
} else {
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
}
}
Is there a better way to navigate to a page based on listening to the results of a provider?
Instead of trying to navigate to a new view what you should do is display the loading splash screen if you are still waiting for data and once that changes display your main home view, like this:
import 'package:catalogo/firebase/ProductData.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Main extends StatefulWidget {
#override
_MainState createState() => _MainState();
}
class _MainState extends State<Main> {
bool shouldProceed = Provider.of<ProductData>(context, listen: true).shouldProceed;
#override
Widget build(BuildContext context) {
if(shouldProceed){
return Home();
}else{
return RouteSplash();
}
}
}
Use BlocListener as in this example:
BlocListener(
bloc: BlocProvider.of<DataBloc>(context),
listener: (BuildContext context, DataState state) {
if (state is Success) {
Navigator.of(context).pushNamed('/details');
}
},
child: BlocBuilder(
bloc: BlocProvider.of<DataBloc>(context),
builder: (BuildContext context, DataState state) {
if (state is Initial) {
return Text('Press the Button');
}
if (state is Loading) {
return CircularProgressIndicator();
}
if (state is Success) {
return Text('Success');
}
if (state is Failure) {
return Text('Failure');
}
},
}
)
Source: https://github.com/felangel/bloc/issues/201
I think I have a solution that does what the OP wants. If you make your splash screen to be Stateful, then you can add a PostFrameCallback. This avoids any problems with Navigator being called when build is running. Your callback can then call whatever routine Provider needs to read the data. This read data routine can be passed a further callback which contains the Navigator command.
In my solution I've added a further callback so that the splash screen is visible for at least one second (you can choose what duration you think reasonable here). Unfortunately, this creates a race condition, so I need to import the synchronized package to avoid problems.
import 'package:flutter/material.dart';
import 'package:reflect/utils/constants.dart';
import 'category_screen.dart';
import 'package:provider/provider.dart';
import 'package:reflect/data_models/app_prefs.dart';
import 'dart:async';
import 'dart:core';
import 'package:synchronized/synchronized.dart';
class LoadingScreen extends StatefulWidget {
static const id = 'LoadingScreen';
#override
_LoadingScreenState createState() => _LoadingScreenState();
}
class _LoadingScreenState extends State<LoadingScreen> {
bool readPrefsDone = false;
bool timeFinished = false;
Lock _lock = Lock();
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
Provider.of<AppPrefs>(context, listen: false).readPrefs(readDone);
Timer(Duration(seconds: 1), () {
timerDone();
});
});
}
void timerDone() async {
_lock.synchronized(() {
if (readPrefsDone) {
pushMainScreen();
}
timeFinished = true;
});
}
void readDone() {
_lock.synchronized(() {
if (timeFinished) {
pushMainScreen();
}
readPrefsDone = true;
});
}
void pushMainScreen() {
Navigator.pushReplacement(
context,
PageRouteBuilder(
pageBuilder: (context, animation, animation2) => CategoryScreen(),
transitionDuration: Duration(seconds: 1),
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: Colors.white,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Hero(
tag: kFishLogoTag,
child: Image(
image: AssetImage('assets/fish_logo.png'),
),
),
SizedBox(
height: 30,
),
Text(
'Reflect',
style: TextStyle(
fontSize: 30,
color: Color(0xFF0000cc),
fontWeight: FontWeight.bold,
),
),
],
),
),
));
}
}
Anyone else facing this issue can use this code
Future.delayed(Duration.zero, () => Navigate.toView(context));
This navigates to the other screen without build errors