I have this application that needs a pull to refresh functionality, so I placed the StreamBuilder Widget inside the RefreshIndicator Widget, but I don't know how to manually refresh the StreamBuilder when the onRefreshed event is triggered.
Having the stream as a state variable and resetting on pull on refresh will solve the problem.
In below code, I am resetting the stream on button press. Hope that helps you.
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return new _MyAppState();
}
}
class _MyAppState extends State<MyApp> {
var stream; // state variable
#override
void initState() {
super.initState();
stream = newStream(); // initial stream
}
Stream<String> newStream() =>
Stream.periodic(Duration(seconds: 1), (i) => "$i");
#override
Widget build(BuildContext context) {
var streamBuilder = StreamBuilder(
initialData: "0",
stream: stream,
builder: (context, snapshot) {
return new Text(snapshot.data);
});
return MaterialApp(
title: 'Trial',
home: Scaffold(
appBar: AppBar(title: Text('Stream builder')),
body: Column(
children: <Widget>[
streamBuilder,
FlatButton(
onPressed: () {
setState(() {
stream = newStream(); //refresh/reset the stream
});
},
child: Text("Reset"))
],
)));
}
}
Try to use this sample with rxdart package:
class _MyAppState extends State<MyApp> {
/// Controller for stream with `Object` (actually any) signal
final controller = PublishSubject<Object>();
/// Stream returning as transformed `Future` with Firebase data
Stream<List> get stream => controller.asyncMap(_onRefreshFirebase);
#override
void dispose() {
controller.close();
super.dispose();
}
#override
void initState() {
super.initState();
controller.add(Object()); // Activate stream
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Trial',
home: Scaffold(
appBar: AppBar(title: Text('Stream builder')),
body: Column(
children: <Widget>[
RefreshIndicator(
onRefresh: _onRefresh,
child: StreamBuilder<List>(
stream: stream,
builder: (context, snapshot) {
// use data from `snapshot`
return ListView.builder(...),
},
),
),
FlatButton(
child: Text('Reload'),
onPressed: () {
// Prepare new data and send it to stream
controller.add(Object());
}
),
],
),
);
}
}
}
/// Stream transformer from `event` to `List<dynamic>`
Future<List> _onRefreshFirebase(Object event) async {
// get data from Firebase
return [];
}
/// Callback when user pulls down
Future _onRefresh() async {
await _onRefreshFirebase(Object());
}
Related
late Future<Kategori> _futureArticles;
late Future<Article> _futureSummary;
and the API's
#override
void initState() {
_futureArticles = _newsService.getArticlesByCategory(widget.id);
_futureSummary = _newsService.getArticleById(widget.id);
super.initState();
}
and FutureBuilder
child: FutureBuilder<Kategori>(
future: _futureArticles,
builder: (BuildContext context, AsyncSnapshot<Kategori> snapshot) {
if (snapshot.hasData) {
final articles = snapshot.data?.data;
now with FutureArticles and with this structure everything works but I need an another json value from _futureSummary. Both API's has got same ID values, so I can get the json.summary value from second API. But how? I tried to use future.wait but it did not work.
Meanwhile I am using second APi on different page to get all informations of a spesific news.
What is the correct approach?
Not sure what you are trying to achieve. Do you want your Future builder to rebuild only when both futures completed? If so - try to combine both futures. Future.wait will wait for all Future objects you pass to complete, and return List of results:
Let me update my answer with the working demo - you can test it in DartPad. Note that the first Future will complete after 1 second (and write the log in the console), but the FutureBuilder will wait until the second Future is completed, and only then show the values from both.
import 'package:flutter/material.dart';
const Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: darkBlue,
),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
late Future<String> _futureArticles;
late Future<int> _futureSummary;
#override
void initState() {
_futureArticles = Future.delayed(const Duration(seconds:1), () {print("First is done"); return "First is done";});
_futureSummary = Future.delayed(const Duration(seconds:5), () => 10);
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder<List<dynamic>>(
future: Future.wait([_futureArticles, _futureSummary]),
builder: (BuildContext context, AsyncSnapshot<List<dynamic>> snapshot) {
if (snapshot.hasData) {
final articles = snapshot.data![0] as String;
final summary= snapshot.data![1] as int;
return Column(children:[
Text(articles),
Text('$summary')
]);
} else {
return const CircularProgressIndicator();
}
});
}
}
Back again. I refactored my code after the advice in this thread: Flutter Multiple Blocs and NamedRoutes
However, since moving my bloc from the main material app tree to the router page, the data isn't loading as snapshot is null.
The Router:
class AppRouter {
final _centresBloc = CentresBloc();
Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case routes.CentreSelectScreenRoute:
return MaterialPageRoute(
builder: (_) => BlocProvider(
bloc: _centresBloc,
child: CentreSelectScreen(),
),
);
default:
return MaterialPageRoute(builder: (context) => HomeScreen());
}
and the CentreSelectScreen class itself
class _CentreSelectScreenState extends State<CentreSelectScreen> {
#override
Widget build(BuildContext context) {
final _centresBloc = BlocProvider.of<CentresBloc>(context);
return Scaffold(
body: Container(
child: StreamBuilder<List<ClimbingCentre>>(
stream: _centresBloc.centres,
builder: (context, snapshot) {
print('snapshot == ${snapshot.data}'); //is always null now
if (snapshot.hasData) {
// If there are no centres (data), display this message.
if (snapshot.data.length == 0) {
return Text('No Centres listed');
}...
The blocprovider was originally in the centreselect screen class which all worked fine, but since moving it, it's not working, and I can't seem to figure out why.
Th blog itself seems to initialise properly when the app first loads, as it is printing out all the correct information. From CentresBloc:
void getCentres() async {
// Retrieve all the centres from the database
List<ClimbingCentre> centres = await ClimbDB.db.getCentres();
// Add all of the centres to the stream so we can grab them later from our pages
_inCentres.add(centres);
print('BLOC incentres is $centres'); //this works and prints all centres when the app first loads...
}
Any help most appreciated.
EDIT Adding CentresBloc Class
import 'dart:async';
import 'package:flutterapp/data/database.dart';
import 'package:flutterapp/models/centre_model.dart';
import 'bloc_provider.dart';
class CentresBloc implements BlocBase {
final _centresController = StreamController<List<ClimbingCentre>>.broadcast();
// Input stream. Add centres to the stream using this variable.
StreamSink<List<ClimbingCentre>> get _inCentres => _centresController.sink;
// Output stream. This one will be used within our pages to display the centres.
Stream<List<ClimbingCentre>> get centres => _centresController.stream;
CentresBloc() {
// Retrieve all the climbing centres on initialization
getCentres();
}
#override
void dispose() {
_centresController.close();
}
void getCentres() async {
// Retrieve all the centres from the database
List<ClimbingCentre> centres = await ClimbDB.db.getCentres();
// Add all of the centres to the stream so we can grab them later from our pages
_inCentres.add(centres);
print('CentreBloc _incentres is: $centres'); //this prints the correct centres when the app is first loaded
}
After a new route is pushed, StreamBuilder subscribes to the same stream _centresBloc.centres, but no new events are emitted on this stream, as it only happens once - during bloc's initialization in constructor. That's because Dart's default StreamController won't send previous events/values (including the last one) to stream's new subscribers.
However, you can use BehaviorSubject from rxdart library, which is based on StreamController, but it also stores the last value emitted and sends it to any new subscriber. Subjects are also always broadcast streams and can have initial (seed) value.
Just replace this:
final _centresController = StreamController<List<ClimbingCentre>>.broadcast();
with this:
final _centresController = BehaviorSubject<List<ClimbingCentre>>();
Full working code:
import 'dart:async';
import 'package:bloc_provider/bloc_provider.dart';
import 'package:flutter/material.dart';
import 'package:rxdart/subjects.dart';
void main() => runApp(MyApp2());
class CentresBloc implements Bloc {
final _centresController = BehaviorSubject<List<String>>();
// Input stream. Add centres to the stream using this variable.
StreamSink<List<String>> get _inCentres => _centresController.sink;
// Output stream. This one will be used within our pages to display the centres.
Stream<List<String>> get centres => _centresController.stream;
CentresBloc() {
// Retrieve all the climbing centres on initialization
getCentres();
}
void dispose() {
_centresController.close();
}
void getCentres() async {
// Retrieve all the centres from the database
List<String> centres = ['LIST'];
// Add all of the centres to the stream so we can grab them later from our pages
_inCentres.add(centres);
print('CentreBloc _incentres is: $centres'); //this prints the correct centres when the app is first loaded
}
}
class AppRouter {
final _centresBloc = CentresBloc();
Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case 'test':
return MaterialPageRoute(
builder: (_) => BlocProvider<CentresBloc>.fromBloc(
bloc: _centresBloc,
child: CentreSelectScreen(),
),
);
default:
return MaterialPageRoute(builder: (context) => HomeScreen());
}
}
void dispose() {
_centresBloc.dispose();
}
}
class MyApp2 extends StatefulWidget {
#override
_MyApp2State createState() => _MyApp2State();
}
class _MyApp2State extends State<MyApp2> {
final _router = AppRouter();
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
onGenerateRoute: _router.generateRoute,
);
}
#override
void dispose() {
_router.dispose();
super.dispose();
}
}
class CentreSelectScreen extends StatefulWidget {
#override
State<StatefulWidget> createState() => _CentreSelectScreenState();
}
class _CentreSelectScreenState extends State<CentreSelectScreen> {
#override
Widget build(BuildContext context) {
final _centresBloc = BlocProvider.of<CentresBloc>(context);
return Scaffold(
body: Container(
child: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
MaterialButton(
child: Text('push index'),
onPressed: () {
Navigator.pushReplacementNamed(context, '/');
},
),
StreamBuilder<List<String>>(
stream: _centresBloc.centres,
builder: (context, snapshot) {
print('snapshot == ${snapshot.data}'); //is always null now
if (snapshot.hasData) {
// If there are no centres (data), display this message.
if (snapshot.data.length == 0) {
return Text('No Centres listed');
} else {
return Text(snapshot.data.toString());
}
}
return Container();
}
),
],
),
),
),
),
);
}
}
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: MaterialButton(
onPressed: () => Navigator.pushReplacementNamed(context, 'test'),
child: Text('push test')
)
)
)
);
}
}
I am using a Selector which rebuilds when a data in Bloc changes. Which woks fine but when the data changes it reloads the whole tree not just the builder inside Selector.
In my case the selector is inside a StreamBuilder. I need this because the stream is connected to API. So inside the stream I am building some widget and One of them is Selector. Selector rebuilds widgets which is depended on the data from the Stream.
Here is My Code. I dont want the Stream to be called again and again. Also the Stream gets called because the build gets called every time selector widget rebuilds.
main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:provider_test/data_bloc.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MultiProvider(providers: [
ChangeNotifierProvider<DataBloc>(
create: (_) => DataBloc(),
)
], child: ProviderTest()),
);
}
}
class ProviderTest extends StatefulWidget {
#override
_ProviderTestState createState() => _ProviderTestState();
}
class _ProviderTestState extends State<ProviderTest> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: <Widget>[
Text("Outside Stream Builder"),
StreamBuilder(
stream: Provider.of<DataBloc>(context).getString(),
builder: (_, AsyncSnapshot<String> snapshot) {
if (snapshot.hasData) {
return Column(
children: <Widget>[
Text("Widget Generated by Stream Data"),
Text("Data From Strem : " + snapshot.data),
RaisedButton(
child: Text("Reload Select"),
onPressed: () {
Provider.of<DataBloc>(context, listen: false).changeValue(5);
}),
Selector<DataBloc, int>(
selector: (_, val) =>
Provider.of<DataBloc>(context, listen: false).val,
builder: (_, val, __) {
return Container(
child: Text(val.toString()),
);
}),
],
);
}
return Container();
},
)
],
),
);
}
}
bloc.dart
import 'package:flutter/foundation.dart';
class DataBloc with ChangeNotifier {
int _willChange = 0;
int get val => _willChange;
void changeValue(int val){
_willChange++;
notifyListeners();
}
Stream<String> getString() {
print("Stream Called");
return Stream.fromIterable(["one", "two", "three"]);
}
}
Also if I remove the StreamBuilder then the Selector acts like its suppose to. Why does StreamBuilder Rebuilds in this case? Is there anyway to prevent this?
Based on the code that you've shared, you can create a listener to your Stream on your initState that updates a variable that keeps the most recent version of your data, and then use that variable to populate your widgets. This way the Stream will only be subscribed to the first time the Widget loads, and not on rebuilds. I can't test it directly as I don't have your project. But please try it out.
Code example based on your code
class ProviderTest extends StatefulWidget {
#override
_ProviderTestState createState() => _ProviderTestState();
}
class _ProviderTestState extends State<ProviderTest> {
String _snapshotData;
#override
void initState() {
listenToGetString();
super.initState();
}
void listenToGetString(){
Provider.of<DataBloc>(context).getString().listen((snapshot){
setState(() {
_snapshotData = snapshot.data;
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: <Widget>[
Text("Outside Stream Builder"),
Column(
children: <Widget>[
Text("Widget Generated by Stream Data"),
Text("Data From Strem : " + _snapshotData),
RaisedButton(
child: Text("Reload Select"),
onPressed: () {
Provider.of<DataBloc>(context, listen: false).changeValue(5);
}
),
Selector<DataBloc, int>(
selector: (_, val) =>
Provider.of<DataBloc>(context, listen: false).val,
builder: (_, val, __) {
return Container(
child: Text(val.toString()),
);
}
),
],
)
],
),
);
}
}
I found the problem after reading this blog post here. I lacked the knowlwdge on how the Provider lib works and how its doing all the magic stuff out of Inherited widgets
The point and quote that solves this problem is. ( A quation from the blog post above)
When a Widget registers itself as a dependency of the Provider’s
InheritedWidget, that widget will be rebuilt each time a variation in
the “provided data” occurs (more precisely when the notifyListeners()
is called or when a StreamProvider’s stream emits new data or when a
FutureProvider’s future completes).
That means the variable that i am changing and the Stream that i am listning to, exists in the Same Bloc! that was the mistake. So when I change the val and call notifyListener() in a single bloc, all things reloads which is the default behaviour.
All I had to do to solve this problem is to make another Bloc and Abstract the Stream to that particular bloc(I think its a Good Practice also). Now the notifyListener() has no effect on the Stream.
data_bloc.dart
class DataBloc with ChangeNotifier {
int _willChange = 0;
String data = "";
int get val => _willChange;
void changeValue(int val){
_willChange++;
notifyListeners();
}
Future<String> getData () async {
return "Data";
}
}
stream_bloc.dart
import 'package:flutter/foundation.dart';
class StreamBloc with ChangeNotifier {
Stream<String> getString() {
print("Stream Called");
return Stream.fromIterable(["one", "two", "three"]);
}
}
And the problem is solved. Now the Stream will only be called if its invoked but not when the variable changes in the data_bloc
I created this code, what i want to happen is when i press on the button i want the piechart to re-render with the new values (which should be old values but the food value increased by 1)
I am using a piechart from pie_chart: 0.8.0 package.
Deposit is nothing but a pojo (String category and int deposit)
the bloc.dart contains a global instance of the bloc, a getter for the stream and initialization of a stream of type
Here's my code:
import 'package:flutter/material.dart';
import 'package:pie_chart/pie_chart.dart';
import 'bloc.dart';
import 'Deposit.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'bloc Chart',
theme: ThemeData(
primarySwatch: Colors.blueGrey,
),
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
Map<String, double> datamap = new Map();
#override
Widget build(BuildContext context) {
datamap.putIfAbsent("Food", () => 5);
datamap.putIfAbsent("transportation", () => 3);
return Scaffold(
appBar: AppBar(
title: Text("PieChart using blocs"),
),
body: Column(
children: <Widget>[
StreamBuilder<Deposit>(
stream: bloc.data, //A stream of Deposit data
builder: (context, snapshot) {
addDeposit(Deposit("Food", 1), datamap);
debugPrint("Value of food in map is: ${datamap["Food"]}");
return PieChart(dataMap: datamap);
}),
SizedBox.fromSize(
size: Size(20, 10),
),
RaisedButton(
onPressed: () {
bloc.add(Deposit("Food", 1)); //returns the stream.add
},
child: Icon(Icons.add),
),
],
),
);
}
void addDeposit(Deposit dep, Map<String, double> map) {
if (map.containsKey(dep.category)) {
map.update(dep.category, (value) => value + dep.price);
} else
map.putIfAbsent(dep.category, () => dep.price);
}
}
I think your problem is that the stream doesn't trigger new events. You don't have to close the stream to rebuild. I can't see anywhere in your code where you are triggering new events for the stream. Check below code to see a simple way how you can update a StatelessWidget using a StreamBuilder.
class CustomWidgetWithStream extends StatelessWidget {
final CustomBlock block = CustomBlock();
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
StreamBuilder(
stream: block.stream,
builder: (context, stream) {
return Text("${stream.data.toString()}");
}),
RaisedButton(
onPressed: () {
block.incrementNumber();
},
child: Text("Increment"),
)
],
);
}
}
class CustomBlock {
num counter = 10;
final StreamController<num> _controller = StreamController();
Stream<num> get stream => _controller.stream;
CustomBlock() {
_controller.onListen = () {
_controller.add(counter); // triggered when the first subscriber is added
};
}
void incrementNumber() {
counter += 1;
_controller.add(counter); // ADD NEW EVENT TO THE STREAM
}
dispose() {
_controller.close();
}
}
Although this is a working code snippet, I would strongly suggest to change your widget from StatelessWidget to StatefulWidget, for two reasons:
* if you go "by the book", if a widget changes the content by itself, then it's not a StatelessWidget, a stateless widget only displays data that is given to it. In your case, the widget is handling the tap and then decides what to do next and how to update itself.
* if you are using streams, in a stateful widget you can safely close the stream, as you can see in the above code, there's no safe way to close the stream. If you don't close the stream, there might be unwanted behaviour or even crashes.
This is my bloc file
import 'package:rxdart/rxdart.dart';
import 'package:testing/Deposit.dart';
class Bloc{
final _data = new BehaviorSubject<Deposit>();
Stream<Deposit> get data => _data.stream;
Function(Deposit) get add => _data.sink.add;
void dispose(){
_data.close();
}
}
Bloc bloc = new Bloc();
I have an intro screen for my app, but it shows every time I open the app,
I need to show that for the 1st time only.
How to do that?
//THIS IS THE SCREEN COMES 1ST WHEN OPENING THE APP (SPLASHSCREEN)
class SplashScreen extends StatefulWidget {
#override
_SplashScreenState createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
#override
void initState() {
super.initState();
//After 2seconds of time the Introscreen will e opened by bellow code
Timer(Duration(seconds: 2), () => MyNavigator.goToIntroscreen(context));
}
//The below code has the text to show for the spalshing screen
#override
Widget build(BuildContext context) {
return Scaffold(
body: new Center(
child: Text('SPLASH SCREEN'),
),
);
}
}
Every time this screen opens the intro screen with 2 seconds delay.
but I want for the first time only How to do that with sharedpreference??
Please add the required code.
If you wish to show the intro screen only for the first time, you will need to save locally that this user has already seen intro.
For such thing you may use Shared Preference. There is a flutter package for Shared Preference which you can use
EDITED:
Please refer to the below complete tested code to understand how to use it:
import 'dart:async';
import 'package:after_layout/after_layout.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
color: Colors.blue,
home: new Splash(),
);
}
}
class Splash extends StatefulWidget {
#override
SplashState createState() => new SplashState();
}
class SplashState extends State<Splash> with AfterLayoutMixin<Splash> {
Future checkFirstSeen() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
bool _seen = (prefs.getBool('seen') ?? false);
if (_seen) {
Navigator.of(context).pushReplacement(
new MaterialPageRoute(builder: (context) => new Home()));
} else {
await prefs.setBool('seen', true);
Navigator.of(context).pushReplacement(
new MaterialPageRoute(builder: (context) => new IntroScreen()));
}
}
#override
void afterFirstLayout(BuildContext context) => checkFirstSeen();
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new Center(
child: new Text('Loading...'),
),
);
}
}
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Hello'),
),
body: new Center(
child: new Text('This is the second page'),
),
);
}
}
class IntroScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('IntroScreen'),
),
body: new Center(
child: new Text('This is the IntroScreen'),
),
);
}
}
Thanks to Ben B for noticing the incorrect use of delay in initState. I had used a delay because sometimes the context is not ready immediately inside initState.
So now I have replaced that with afterFirstLayout which is ready with the context. You will need to install the package after_layout.
I was able to do without using after_layout package and Mixins and instead I have used FutureBuilder.
class SplashState extends State<Splash> {
Future checkFirstSeen() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
bool _seen = (prefs.getBool('seen') ?? false);
if (_seen) {
return HomeScreen.id;
} else {
// Set the flag to true at the end of onboarding screen if everything is successfull and so I am commenting it out
// await prefs.setBool('seen', true);
return IntroScreen.id;
}
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: checkFirstSeen(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
} else {
return MaterialApp(
initialRoute: snapshot.data,
routes: {
IntroScreen.id: (context) => IntroScreen(),
HomeScreen.id: (context) => HomeScreen(),
},
);
}
});
}
}
class HomeScreen extends StatelessWidget {
static String id = 'HomeScreen';
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Hello'),
),
body: new Center(
child: new Text('This is the second page'),
),
);
}
}
class IntroScreen extends StatelessWidget {
static String id = 'IntroScreen';
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('IntroScreen'),
),
body: new Center(
child: new Text('This is the IntroScreen'),
),
);
}
}
I always try to use minimum count of packages, because in future it can conflict with ios or android. So my simple solution without any package:
class SplashScreen extends StatefulWidget {
#override
_SplashScreenState createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
final splashDelay = 2;
#override
void initState() {
super.initState();
_loadWidget();
}
_loadWidget() async {
var _duration = Duration(seconds: splashDelay);
return Timer(_duration, checkFirstSeen);
}
Future checkFirstSeen() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
bool _introSeen = (prefs.getBool('intro_seen') ?? false);
Navigator.pop(context);
if (_introSeen) {
Navigator.pushNamed(context, Routing.HomeViewRoute);
} else {
await prefs.setBool('intro_seen', true);
Navigator.pushNamed(context, Routing.IntroViewRoute);
}
}
#override
Widget build(BuildContext context) {
//your splash screen code
}
}
Use shared_preferences:
Full code:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
var prefs = await SharedPreferences.getInstance();
var boolKey = 'isFirstTime';
var isFirstTime = prefs.getBool(boolKey) ?? true;
runApp(MaterialApp(home: isFirstTime ? IntroScreen(prefs, boolKey) : RegularScreen()));
}
class IntroScreen extends StatelessWidget {
final SharedPreferences prefs;
final String boolKey;
IntroScreen(this.prefs, this.boolKey);
Widget build(BuildContext context) {
prefs.setBool(boolKey, false); // You might want to save this on a callback.
return Scaffold();
}
}
class RegularScreen extends StatelessWidget {
Widget build(BuildContext context) => Scaffold();
}
I just had to do exactly the same thing, here's how I did it:
First, in my main method, I open the normal main page and the tutorial:
MaterialApp(
title: 'myApp',
onGenerateInitialRoutes: (_) => [MaterialPageRoute(builder: mainPageRoute), MaterialPageRoute(builder: tutorialSliderRoute)],
)
...and then I use a FutureBuilder to build the tutorial only if necessary:
var tutorialSliderRoute = (context) => FutureBuilder(
future: Provider.of<UserConfiguration>(context, listen: false).loadShowTutorial() // does a lookup using Shared Preferences
.timeout(Duration(seconds: 3), onTimeout: () => false),
initialData: null,
builder: (context, snapshot){
if (snapshot.data == null){
return CircularProgressIndicator(); // This is displayed for up to 3 seconds, in case data loading doesn't return for some reason...
} else if (snapshot.data == true){
return TutorialSlider(); // The Tutorial, implemented using IntroSlider()
} else {
// In case the tutorial shouldn't be shown, just return an empty Container and immediately pop it again so that the app's main page becomes visible.
SchedulerBinding.instance.addPostFrameCallback((_){Navigator.of(context).pop();});
return Container(width: 0, height: 0);
}
},
);
Also, I think the tutorial should be shown again in case the user does not finish it, so I set only set the variable showTutorial to false once the user has completed (or skipped) the tutorial:
class TutorialSlider extends StatefulWidget {
#override
State<StatefulWidget> createState() => TutorialSliderState();
}
class TutorialSliderState extends State<TutorialSlider> {
...
#override
Widget build(BuildContext context) => IntroSlider(
...
onDonePress: (){
Provider.of<UserConfiguration>(context, listen: false).setShowTutorial(false);
Navigator.of(context).pop();
}
);
}
I took a different approach. I agree with the other answers that you should save your isFirstRun status via SharedPreferences. The tricky part then is how to show the correct widget in such a way that when you hit back you close out of the app correctly, etc. I first tried doing this by launching a my SplashWidget while building my HomePageWidget, but this turned out to lead to some weird Navigator errors.
Instead, I wound up calling runApp() multiple times with my different widget as appropriate. When I need to close the SplashWidget, rather than pop it, I just call runApp() again, this time with my HomePageWidget as the child property. It is safe to call runApp() multiple times according to this issue, indeed even for splash screens.
So it looks something like this (simplified obviously):
Future<void> main() async {
bool needsFirstRun = await retrieveNeedsFirstRunFromPrefs();
if (needsFirstRun) {
// This is will probably be an async method but no need to
// delay the first widget.
saveFirstRunSeen();
runApp(child: SplashScreenWidget(isFirstRun: true));
} else {
runApp(child: HomePageWidget());
}
}
I have an isFirstRun property on SplashScreenWidget because I can launch it in two ways--once as a true splash screen, and once from settings so that users can see it again if they want. I then inspect that in SplashScreenWidget to determine how I should return to the app.
class SplashScreenWidget extends StatefulWidget {
final bool isFirstRun;
// <snip> the constructor and getState()
}
class _SplashScreenWidgetState extends State<SplashScreenWidget> {
// This is invoked either by a 'skip' button or by completing the
// splash screen experience. If they just hit back, they'll be
// kicked out of the app (which seems like the correct behavior
// to me), but if you wanted to prevent that you could build a
// WillPopScope widget that instead launches the home screen if
// you want to make sure they always see it.
void dismissSplashScreen(BuildContext ctx) {
if (widget.isFirstRun) {
// Then we can't just Navigator.pop, because that will leave
// the user with nothing to go back to. Instead, we will
// call runApp() again, setting the base app widget to be
// our home screen.
runApp(child: HomePageWidget());
} else {
// It was launched via a MaterialRoute elsewhere in the
// app. We want the dismissal to just return them to where
// they were before.
Navigator.of(ctx).pop();
}
}
}