I just learn about how can i use Bloc in flutter applications and my simple app i have some separated view class as App and MainPage and i implemented simple Bloc pattern to handle click event on some widgets such as button
after running application my implemented bloc pattern only work once and show message in console and after click again that don't work to show message
my main.dart class
void main() {
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
title: Strings.appName,
theme: ThemeData(
primarySwatch: Colors.indigo,
),
home: App(),
));
}
class App extends StatefulWidget {
final HomeBloc homeBloc = HomeBloc();
#override
State<App> createState() => MainPage();
}
MainPage class:
class MainPage extends State<App> {
#override
void initState() {
super.initState();
}
HomeBloc get _homeBloc => widget.homeBloc;
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: Strings.appName,
theme: ThemeData(
primarySwatch: Colors.indigo,
),
home: BlocBuilder<HomeEvent,HomeState>(
bloc: _homeBloc,
builder: (BuildContext context, HomeState state) {
if (state is HandleDrawerMenuClick) {
_onWidgetDidBuild(() {
print("clicked on drawer menu");
});
}
return Scaffold(
body: Center(
child: RaisedButton(
child: Text('ddddddddd'),
onPressed: () {
_homeBloc.dispatch(OnDrawerMenuClicked());
},
),
),
);
}),
);
}
#override
void dispose() {
_homeBloc.dispose();
super.dispose();
}
void _onWidgetDidBuild(Function callback) {
WidgetsBinding.instance.addPostFrameCallback((_) {
callback();
});
}
}
HomeBloc class:
class HomeBloc extends Bloc<HomeEvent, HomeState> {
#override
HomeState get initialState => HomeState();
#override
Stream<HomeState> mapEventToState(HomeEvent event) async* {
if (event is OnDrawerMenuClicked) {
yield HandleDrawerMenuClick();
}
}
}
HomeEvent class:
class HomeEvent extends Equatable {
HomeEvent([List props = const []]) : super(props);
}
class OnDrawerMenuClicked extends HomeEvent {
OnDrawerMenuClicked() : super([]);
#override
String toString() => 'OnDrawerMenuClicked clicked';
}
HomeState class:
class HomeState extends Equatable{
HomeState([List props = const[]]):super(props);
}
class HandleDrawerMenuClick extends HomeState{
#override
String toString()=>'HandleDrawerMenuClick';
}
i think problem is on HandleDrawerMenuClick class because when i debug application, debug can go into if statement on this line:
if (event is OnDrawerMenuClicked) {
yield HandleDrawerMenuClick();
}
and i think twice click on button couldn't trigger yield HandleDrawerMenuClick();
This seems to be an intended behavior because blocs doesn't re-emit on the same state as mentioned on this GitHub issue ticket.
Related
I have two screens in my app.
Screen A runs a computationally expensive operation while opened, and properly disposes by cancelling animations/subscriptions to the database when dispose() is called to prevent memory leaks.
From Screen A, you can open another screen (Screen B).
When I use Navigator.pushNamed, Screen A remains in memory, and dispose() is not called, even though Screen B is now shown.
Is there a way to force disposal of Screen A when it is not in view?
Example code where first route is never disposed:
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
title: 'Navigation Basics',
home: FirstRoute(),
));
}
class FirstRoute extends StatefulWidget {
#override
_FirstRouteState createState() => _FirstRouteState();
}
class _FirstRouteState extends State<FirstRoute> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('First Route'),
),
body: RaisedButton(
child: Text('Open route'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
},
),
);
}
#override
void dispose() {
// Never called
print("Disposing first route");
super.dispose();
}
}
class SecondRoute extends StatefulWidget {
#override
_SecondRouteState createState() => _SecondRouteState();
}
class _SecondRouteState extends State<SecondRoute> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Route"),
),
body: RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go back!'),
),
);
}
#override
void dispose() {
print("Disposing second route");
super.dispose();
}
}
I know it's a bit late but I think you should override the deactivate method. Since we are changing the page we are not actually destroying it, that's why the dispose isn't being called.
If you'd like more information this page lists the lifecycle of the stateful widgets.
From the link:
'deactivate()' is called when State is removed from the tree, but it might be
reinserted before the current frame change is finished. This method exists basically
because State objects can be moved from one point in a tree to another.
call Navigator.pushReplacement when routing between first and second screen.
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
title: 'Navigation Basics',
home: FirstRoute(),
));
}
class FirstRoute extends StatefulWidget {
#override
_FirstRouteState createState() => _FirstRouteState();
}
class _FirstRouteState extends State<FirstRoute> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('First Route'),
),
body: RaisedButton(
child: Text('Open route'),
onPressed: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
},
),
);
}
#override
void dispose() {
// Never called
print("Disposing first route");
super.dispose();
}
}
class SecondRoute extends StatefulWidget {
#override
_SecondRouteState createState() => _SecondRouteState();
}
class _SecondRouteState extends State<SecondRoute> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Route"),
),
body: RaisedButton(
onPressed: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => FirstRoute()),
);
},
child: Text('Go back!'),
),
);
}
#override
void dispose() {
print("Disposing second route");
super.dispose();
}
}
Try this
In flutter new versions deactivate won't be called when you push a new widget on top of another widget. Also there is an open issue related to this topic on flutter github: https://github.com/flutter/flutter/issues/50147
The best way to handle this issue is to add RouteObserver<PageRoute> to your material app and override didPushNext and didPushNext functions.
There is a very helpful medium article related to this topic which you can find here: https://medium.com/koahealth/how-to-track-screen-transitions-in-flutter-with-routeobserver-733984a90dea
As Article said create your own RouteAwareWidget, you can add these two call backs to the fields of the widget:
didPopNext
didPushNext
class RouteAwareWidget extends StatefulWidget {
final Widget child;
final VoidCallback? didPopNext;
final VoidCallback? didPushNext;
const RouteAwareWidget({
Key? key,
required this.child,
this.didPopNext,
this.didPushNext,
}) : super(key: key);
#override
State<RouteAwareWidget> createState() => RouteAwareWidgetState();
}
class RouteAwareWidgetState extends State<RouteAwareWidget> with RouteAware {
#override
void didChangeDependencies() {
super.didChangeDependencies();
routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute);
}
#override
void dispose() {
routeObserver.unsubscribe(this);
super.dispose();
}
#override
void didPush() {}
#override
void didPopNext() {
dPrint('didPopNext');
widget.didPopNext == null ? null : widget.didPopNext!();
super.didPopNext();
}
#override
void didPushNext() {
dPrint('didPushNext');
widget.didPushNext == null ? null : widget.didPushNext!();
super.didPushNext();
}
#override
Widget build(BuildContext context) => widget.child;
}
Create a global RouteObserver<PageRoute> and add it to your material app:
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
MaterialApp(
navigatorObservers: [routeObserver],
debugShowCheckedModeBanner: false,
routes: _routes,
)
then in your routs you should wrap your routes with RouteAwareWidget and add custom function you want:
final _routes = {
HomePage.routeName: (context) => RouteAwareWidget(
child: const HomePage(),
didPushNext: () => sl<CameraBloc>().add(Dispose()),
didPopNext: () => sl<CameraBloc>().add(Init()),
),
MyQuestions.routeName: (context) => const RouteAwareWidget(
child: MyQuestions(),
),
};
didPushNext will be called when you push a widget on top of HomePage and didPopNext will be called when you pop the last widget above HomePage.
With Navigator.pushReplacement(), if using MaterialPageRoute, then setting
maintainState:false
will ensure that dispose() is called.
A light weight solution for a single route case is using a callback function triggered from the SecondRoute.
Trigger the callback from the WidgetsBinding.instance.addPostFrameCallback() within the initState() on the SecondRoute
More information on WidgetsBinding and when they run can be found here: Flutter: SchedulerBinding vs WidgetsBinding.
WidgetsBinding & SchedulerBinding will be printed only once as we called it initState(), but it will be called when build method finished it’s rendering.
import 'package:flutter/material.dart';
class FirstRoute extends StatefulWidget {
const FirstRoute({super.key});
#override
State<FirstRoute> createState() => _FirstRouteState();
}
class _FirstRouteState extends State<FirstRoute> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Screen A')),
body: Center(
child: TextButton(
child: const Text('Go to Screen B'),
onPressed: () async {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => SecondRoute(_callbackFn),
),
);
_secondRouteDone();
},
),
),
);
}
_callbackFn() {
print("Widget B Loaded, Free up memory, dispose things, etc.");
}
_secondRouteDone() {
print("SecondRoute Popped, Reinstate controllers, etc.");
}
}
class SecondRoute extends StatefulWidget {
final Function() notifyIsMountedFn;
const SecondRoute(this.notifyIsMountedFn, {super.key});
#override
State<SecondRoute> createState() => _SecondRouteState();
}
class _SecondRouteState extends State<SecondRoute> {
#override
void initState() {
super.initState();
// Notify FirstRoute after paint
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.notifyIsMountedFn();
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Screen B')),
);
}
}
Not only to call 'deactivate()' but also to use 'Navigator.pushReplacement()' for page moving is necessary. Not working if you are using 'Navigator.push()'.
i was struck here while making an application my code went like this
void main() {
runApp(Myapp());
}
class Myapp extends StatelessWidget {
bool s=false;
#override
Widget build(BuildContext context) {
return (MaterialApp(
debugShowCheckedModeBanner: false,
title: "haha app",
theme: ThemeData(primarySwatch: Colors.lime),
home: s ? HomeScreen(null) : LoginPage()));
}
}
the above code is of main.dart file
and this is my another file called Login.dart and the code goes like this
class LoginPage extends StatefulWidget {
#override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
Widget build(BuildContext context) {
return(some button ontap:(\\ on tap on this i have to change the bool s value in main.dart to true how to do that){
}
)
}
on tap the button the value s in main dart file should change to true but without navigator because we are not navigating here just a click.
please help me,
thanks in advance
You can use callbacks to communicate your widgets, like this
Create a method to get the callback , in this case : onChangeBool , pass the callback to your LoginPage Widget.
class Myapp extends StatelessWidget {
bool s=false;
onChangeBool(){
//change your var here
s = true;
//refresh the state
setState(() {
});
}
#override
Widget build(BuildContext context) {
return (MaterialApp(
debugShowCheckedModeBanner: false,
title: "haha app",
theme: ThemeData(primarySwatch: Colors.lime),
home: s ? HomeScreen(null) : LoginPage(onPressed: () => onChangeBool() ));
}
}
Receive the callBack , and call it when you press the button
class LoginPage extends StatefulWidget {
final VoidCallback onPressed;
LoginPage({this.onPressed});
#override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
Widget build(BuildContext context) {
return RaisedButton(
child: Text("button"),
onPressed: (){
widget.onPressed();
},
)
}
)
}
In case you want to pass Data, you can use ValueChanged callback , or if you want to pass complex data, create your own callback using typedef/
A sample using ValueChanged.
class Myapp extends StatelessWidget {
bool s=false;
receiveData(String data){
print("your text here : $data");
}
#override
Widget build(BuildContext context) {
return (MaterialApp(
debugShowCheckedModeBanner: false,
title: "haha app",
theme: ThemeData(primarySwatch: Colors.lime),
home: s ? HomeScreen(null) : LoginPage(onPressed: receiveData ));
}
}
class LoginPage extends StatefulWidget {
final ValueChanged<String> onPressed;
LoginPage({this.onPressed});
#override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
Widget build(BuildContext context) {
return RaisedButton(
child: Text("button"),
onPressed: (){
widget.onPressed("passing this data");
},
)
}
)
}
Just testing out flutter. The code sample below is a very simple flutter app. The problem is that I don't know how to call the setState() function inside the TestTextState class in order to change the text each time when the change button is pressed.
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Test app',
home: new Scaffold(
appBar: new AppBar(
title: new Text("Test"),
),
body: new Test(),
),
);
}
}
class Test extends StatelessWidget {
final TestText testText = new TestText();
void change() {
testText.text == "original" ? testText.set("changed") : testText.set("original");
}
#override
Widget build(BuildContext context) {
return new Column(
children: [
testText,
new RaisedButton(
child: new Text("change"),
onPressed: () => change(),
),
]
);
}
}
class TestText extends StatefulWidget {
String text = "original";
void set(String str) {
this.text = str;
}
#override
TestTextState createState() => new TestTextState();
}
class TestTextState extends State<TestText> {
#override
Widget build(BuildContext context) {
return new Text(this.widget.text);
}
}
I have approached this problem by initializing the _TestTextState as the final property of the TestText widget which allows to simply update the state when the change button is pressed. It seems like a simple solution but I'm not sure whether it's a good practice.
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Test app',
home: new Scaffold(
appBar: new AppBar(
title: new Text("Test"),
),
body: new Test(),
),
);
}
}
class Test extends StatelessWidget {
final _TestText text = new _TestText();
#override
Widget build(BuildContext context) {
return new Column(
children: [
text,
new RaisedButton(
child: new Text("change"),
onPressed: () => text.update(),
),
]
);
}
}
class TestText extends StatefulWidget {
final _TestTextState state = new _TestTextState();
void update() {
state.change();
}
#override
_TestTextState createState() => state;
}
class _TestTextState extends State<TestText> {
String text = "original";
void change() {
setState(() {
this.text = this.text == "original" ? "changed" : "original";
});
}
#override
Widget build(BuildContext context) {
return new Text(this.text);
}
}
thier is no way to do so. any how you have to convert your StatelessWidget to StatefulWidget.
Solution based on your existing code
class Test extends StatelessWidget {
final StreamController<String> streamController = StreamController<String>.broadcast();
#override
Widget build(BuildContext context) {
final TestText testText = TestText(streamController.stream);
return new Column(children: [
testText,
new RaisedButton(
child: Text("change"),
onPressed: () {
String text = testText.text == "original" ? "changed" : "original";
streamController.add(text);
},
),
]);
}
}
class TestText extends StatefulWidget {
TestText(this.stream);
final Stream<String> stream;
String text = "original";
#override
TestTextState createState() => new TestTextState();
}
class TestTextState extends State<TestText> {
#override
void initState() {
widget.stream.listen((str) {
setState(() {
widget.text = str;
});
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Text(widget.text);
}
}
But it's not the best idea - to use non-final field inside Stateful Widget
P.S.
You can also use this - scoped_model
I am trying to close a Dialog dynamically.
What I am actually trying to do is to change the content of the dialog depending on the information I have at the moment.
Starts with loading info and no button and after a few seconds could be an error with the OK button to close the Dialog Box.
class Dialogs{
loginLoading(BuildContext context, String type, String description){
var descriptionBody;
if(type == "error"){
descriptionBody = CircleAvatar(
radius: 100.0,
maxRadius: 100.0,
child: new Icon(Icons.warning),
backgroundColor: Colors.redAccent,
);
} else {
descriptionBody = new Center(
child: new CircularProgressIndicator(),
);
}
return showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context){
return AlertDialog(
title: descriptionBody,
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
Center(child: Text(description))
],
),
),
);
}
);
}
}
So after creating the instance os the dialog and opening it
Dialogs _dialog = new Dialogs();
_dialog.loginLoading(context, "loading", "loading...");
// Close the dialog code here
don't know how to do it
// Call again the AlertDialog with different content.
https://docs.flutter.io/flutter/material/showDialog.html
The dialog route created by this method is pushed to the root navigator. If the application has multiple Navigator objects, it may be necessary to call Navigator.of(context, rootNavigator: true).pop(result) to close the dialog rather than just Navigator.pop(context, result).
So any one of the below should work for you
Navigator.of(context, rootNavigator: true).pop(result)
Navigator.pop(context, result)
You don't need to close and reopen the dialog. Instead let flutter handle the dialog update. The framework is optimised for just that.
Here is a working example app that you can use as a starting point (just add your own Dialogs class):
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'MyApp',
home: Login(
child: Home(),
),
);
}
}
class Home extends StatefulWidget {
final Dialogs dialog = Dialogs();
#override
State<StatefulWidget> createState() => HomeState();
}
class HomeState extends State<Home> {
#override
void didChangeDependencies() {
super.didChangeDependencies();
Future.delayed(Duration(milliseconds: 50)).then((_) {
widget.dialog.loginLoading(
context,
LoginStateProvider.of(context).type,
LoginStateProvider.of(context).description,
);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Updating Dialog'),
),
body: Container(),
);
}
}
class Login extends StatefulWidget {
final Widget child;
Login({#required this.child});
#override
State<StatefulWidget> createState() => LoginState();
}
class LoginState extends State<Login> {
String type = 'wait';
String description = 'foo';
#override
void didChangeDependencies() {
super.didChangeDependencies();
Future.delayed(Duration(milliseconds: 2000)).then((_) {
setState(() {
type = 'error';
description = 'bar';
});
});
}
#override
Widget build(BuildContext context) {
return LoginStateProvider(widget.child, type, description);
}
}
class LoginStateProvider extends InheritedWidget {
final String type;
final String description;
LoginStateProvider(Widget child, this.type, this.description)
: super(child: child);
#override
bool updateShouldNotify(LoginStateProvider old) {
return type != old.type || description != old.description;
}
static LoginStateProvider of(BuildContext context) =>
context.inheritFromWidgetOfExactType(LoginStateProvider);
}
Being new to Flutter, I'm doing a learning exercise by re-creating my existing Android app. However I'm having trouble to produce a 'spinning, growing home icon', which should be animated in sync with the drawer open/close animation.
The desired drawer/home-icon behaviour looks like this:
I made this in Android by implementing
DrawerListener.onDrawerSlide(View drawerView, float slideOffset)
My naive approach to do this in Flutter, is to use a ScaleTransition and a RotationTransition that listen to the same Animation that opens/closes the Drawer.
I can see that ScaffoldState has a DrawerControllerState, but it is private.
final GlobalKey<DrawerControllerState> _drawerKey = new GlobalKey<DrawerControllerState>();
And even if I could somehow access the DrawerControllerState (which I don't know how), I then couldn't access _animationChanged() and _controller because both are private members of DrawerControllerState.
I feel that I'm coming at this in the wrong way, and that there is an better approach that's more natural to Flutter, that I'm unable to see.
Please can anyone describe the Flutter way of implementing this?
You can first refer to other people's replies on stackoverflow here
My solve:
get Drawer status on DrawerWidget
initState() : open drawer
dispose() : close drawer
Stream drawer status by DrawerService Provider
see full code
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:provider/provider.dart';
void main() {
runApp(
MultiProvider(
providers: [
Provider(create: (_) => DrawerService()),
],
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
DrawerService _drawerService;
String drawerStatus = 'close';
#override
void initState() {
super.initState();
_drawerService = Provider.of(context, listen: false);
_listenDrawerService();
}
_listenDrawerService() {
_drawerService.status.listen((status) {
if(status) {
drawerStatus = 'open';
} else {
drawerStatus = 'close';
}
setState(() { });
});
}
#override
Widget build(BuildContext context) {
Color bgColor = Colors.yellow;
if(drawerStatus == 'open') {
bgColor = Colors.red;
}
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
drawer: DrawerWidget(),
body: Container(
decoration: BoxDecoration(color: bgColor),
height: 300,
child: Center(child: Text(drawerStatus),),
),
);
}
}
class DrawerWidget extends StatefulWidget {
#override
_DrawerWidgetState createState() => _DrawerWidgetState();
}
class _DrawerWidgetState extends State<DrawerWidget> {
DrawerService _drawerService;
#override
void initState() {
super.initState();
_drawerService = Provider.of(context, listen: false);
_drawerService.setIsOpenStatus(true);
}
#override
Widget build(BuildContext context) {
return Drawer(
child: Center(child: Text('drawer'),),
);
}
#override
void dispose() {
super.dispose();
_drawerService.setIsOpenStatus(false);
}
}
class DrawerService {
StreamController<bool> _statusController = StreamController.broadcast();
Stream<bool> get status => _statusController.stream;
setIsOpenStatus(bool openStatus) {
_statusController.add(openStatus);
}
}
hope to help some body