I have my own StatelessWidget with a ListView. I want it's state to be managed by parent StatefulWidget.
The behaviour I desire is that if I change a value, listView scrolls (or even jumps - it doesn't matter) to that value.
I thought that if I create stateless widget every time parent's setState() method is being invoked, the scrollController with initialOffset would make the list "move" but it doesn't. What is worth mentioning is that on first build initialOffset works as it should.
Here is example code of my problem:
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: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 5;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new MyClass(_counter),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
child: new Icon(Icons.add),
),
);
}
}
class MyClass extends StatelessWidget {
final int extraValue;
final ScrollController scrollController;
MyClass(this.extraValue):
scrollController = new ScrollController(initialScrollOffset: extraValue*50.0);
#override
Widget build(BuildContext context) {
return new ListView.builder(
itemExtent: 50.0,
itemCount: 100,
controller: scrollController,
itemBuilder: (BuildContext context, int index) {
if (index != extraValue)
return new Text(index.toString());
else
return new Text("EXTRA" + index.toString());
});
}
}
I'm not sure if it's a bug or my mistake.
Any ideas might be helpful :)
EDIT:
Inspired by Ian Hickson's answer I have solution to my problem:
void _incrementCounter() {
setState(() {
_counter++;
myClass.scrollController.animateTo(_counter*50.0, duration: new Duration(seconds: 1), curve: new ElasticOutCurve());
});
}
The initial offset is... the initial offset. Not the current offset. :-)
You can cause the offset to change by calling methods on the ScrollController, like animateTo.
Related
I have a preview widget that loads data after a user tap. This state (already tapped or not) should not be lost while scrolling (the preview is located in a list) or navigating through other screen.
The scrolling is solved by adding AutomaticKeepAliveClientMixin which saves the state when scrolling away.
Now i also need to wrap the preview widget (actually a more complex widget that contains the preview) with a RepaintBoundary, to be able to make a "screenshot" of this widget alone.
Before i wrap the widget with a RepaintBoundary, the state is saved both while scrolling and navigating to another screen.
After i add the RepaintBoundary the scrolling still works but for navigation the state is reset.
How can i wrap a Stateful widget that should hold its state with a RepaintBoundary?
Code is a simplified example of my implementation with the same problem.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
MyApp({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
final title = 'Test';
return MaterialApp(
title: title,
home: Scaffold(
appBar: AppBar(
title: Text(title),
),
body: TestList(40),
),
);
}
}
class TestList extends StatefulWidget {
final int numberOfItems;
TestList(this.numberOfItems);
#override
_TestListState createState() => _TestListState();
}
class _TestListState extends State<TestList> {
#override
Widget build(BuildContext context) {
print('_TestListState build.');
return ListView.builder(
itemCount: widget.numberOfItems,
itemBuilder: (context, index) {
return RepaintBoundary(
key: GlobalKey(),
child: Preview()
);
},
);
}
}
class Preview extends StatefulWidget {
#override
_PreviewState createState() => _PreviewState();
}
class _PreviewState extends State<Preview> with AutomaticKeepAliveClientMixin {
bool loaded;
#override
void initState() {
super.initState();
print('_PreviewState initState.');
loaded = false;
}
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
print('_PreviewState build.');
if(loaded) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => NewScreen()),
);
},
child: ListTile(
title: Text('Loaded. Tap to navigate.'),
leading: Icon(Icons.visibility),
),
);
} else {
return GestureDetector(
onTap: () {
setState(() {
loaded = true;
});
},
child: ListTile(
title: Text('Tap to load.'),
),
);
}
}
}
class NewScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('New Screen')),
body: Center(
child: Text(
'Navigate back and see if loaded state is gone.',
style: TextStyle(fontSize: 14.0),
),
),
);
}
}
Take a look at RepaintBoundary.wrap, it assigns the RepaintBoundary widget a key based on its child or childIndex so state is maintained:
class _TestListState extends State<TestList> {
#override
Widget build(BuildContext context) {
print('_TestListState build.');
return ListView.builder(
itemCount: widget.numberOfItems,
itemBuilder: (context, index) {
return RepaintBoundary.wrap(
Preview(),
index,
);
},
);
}
}
https://api.flutter.dev/flutter/widgets/RepaintBoundary/RepaintBoundary.wrap.html
EDIT: As per the below comments, it looks like this solution would break the screenshot ability so you'd have to store the list of children widgets in your state like so:
class _TestListState extends State<TestList> {
List<Widget> _children;
#override
void initState() {
super.initState();
_children = List.generate(
widget.numberOfItems,
(_) => RepaintBoundary(
key: GlobalKey(),
child: Preview(),
));
}
#override
Widget build(BuildContext context) {
print('_TestListState build.');
return ListView(children: _children);
}
}
Im new to flutter and dart so i have created simple counter flutter app , explained in article.
But when stream does not update widget when subject adds values.
can someone help me to find the issue.
my main widget class
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: 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> {
CounterBloc _counterBloc = new CounterBloc(initialCount: 0);
#override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Counter(),
floatingActionButton: FloatingActionButton(
onPressed: _counterBloc.increment,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
and Counter widget
class Counter extends StatefulWidget {
#override
_CounterState createState() => _CounterState();
}
class _CounterState extends State<Counter> {
CounterBloc _counterBloc = new CounterBloc(initialCount: 1);
#override
void dispose() {
_counterBloc.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: _counterBloc.counterObservable,
builder: (context, snapshot) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'${snapshot.hasData}',
style: Theme.of(context).textTheme.display1,
),
],
),
),
);
}
}
and here is my bloc class
class CounterBloc {
int initialCount =
1; //if the data is not passed by paramether it initializes with 0
BehaviorSubject<int> _subjectCounter;
CounterBloc({this.initialCount}) {
_subjectCounter = new BehaviorSubject<int>.seeded(
this.initialCount); //initializes the subject with element already
}
Observable<int> get counterObservable => _subjectCounter.stream;
void increment() {
initialCount++;
print(initialCount);
_subjectCounter.add(initialCount);
}
void decrement() {
initialCount--;
_subjectCounter.add(initialCount);
}
void dispose() {
_subjectCounter.close();
}
}
Can some one help me to find the issue.
Thanks.
You are using two separate instances of CounterBloc in MyHomePage and Counter classes. A simple solution would be to pass the CounterBloc of MyHomePage to Counter:
MyHomePage
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
MyHomePageState
class _MyHomePageState extends State<MyHomePage> {
CounterBloc _counterBloc = CounterBloc(initialCount: 0);
#override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Counter(
bloc: _counterBloc,
),
floatingActionButton: FloatingActionButton(
onPressed: _counterBloc.increment,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
Counter
class Counter extends StatefulWidget {
Counter({Key key, this.bloc}) : super(key: key);
final CounterBloc bloc;
#override
_CounterState createState() => _CounterState();
}
CounterState
class _CounterState extends State<Counter> {
CounterBloc _counterBloc;
#override
void dispose() {
_counterBloc.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
CounterBloc _counterBloc = widget.bloc;
return StreamBuilder(
stream: _counterBloc.counterObservable,
builder: (context, snapshot) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'${snapshot.data}',
style: Theme.of(context).textTheme.display1,
),
],
),
),
);
}
}
But in the long run, rather than passing your Bloc explicitly as a parameter, you should use a BlocProvider, which will implicitly assigns the instance of the parent classes' Bloc to your children.
What is the best way to go about dynamically changing the theme of a Flutter app? For example, if the user changes the color to red, I want the theme to instantly be changed to red. I can't find anything very helpful online except one guy said to use the BLOC pattern, which I am not familiar with it. I'd like to hear your guys thoughts on the issue. Thanks!
My current code structure:
var themeData = ThemeData(
fontFamily: 'Raleway',
primaryColor: Colors.blue,
brightness: Brightness.light,
backgroundColor: Colors.white,
accentColor: Colors.blue);
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: Constants.appName,
theme: themeData,
home: CheckAuth(), //CheckAuth returns MyHomePage usually
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title, #required this.uid}) : super(key: key);
final String title;
final String uid;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
...build and stuff
}
You can use InhertedWidget if you like (instead of BLOC) - Basically it is used to access parent widget anywhere from the tree.
So what you should do is
create InheritedWidget, somewhere in top of tree [from where you want the effect of theme to take place]
wrap it around Theme widget
expose a method to switch theme, by passing the ThemeData you want to replace it with.
Here is some code:
import 'package:flutter/material.dart';
var themeData = ThemeData(
fontFamily: 'Raleway',
primaryColor: Colors.blue,
brightness: Brightness.light,
backgroundColor: Colors.white,
accentColor: Colors.blue
);
void main() {
runApp(
ThemeSwitcherWidget(
initialTheme: themeData,
child: MyApp(),
),
);
}
class ThemeSwitcher extends InheritedWidget {
final _ThemeSwitcherWidgetState data;
const ThemeSwitcher({
Key key,
#required this.data,
#required Widget child,
}) : assert(child != null),
super(key: key, child: child);
static _ThemeSwitcherWidgetState of(BuildContext context) {
return (context. dependOnInheritedWidgetOfExactType(ThemeSwitcher)
as ThemeSwitcher)
.data;
}
#override
bool updateShouldNotify(ThemeSwitcher old) {
return this != old;
}
}
class ThemeSwitcherWidget extends StatefulWidget {
final ThemeData initialTheme;
final Widget child;
ThemeSwitcherWidget({Key key, this.initialTheme, this.child})
: assert(initialTheme != null),
assert(child != null),
super(key: key);
#override
_ThemeSwitcherWidgetState createState() => _ThemeSwitcherWidgetState();
}
class _ThemeSwitcherWidgetState extends State<ThemeSwitcherWidget> {
ThemeData themeData;
void switchTheme(ThemeData theme) {
setState(() {
themeData = theme;
});
}
#override
Widget build(BuildContext context) {
themeData = themeData ?? widget.initialTheme;
return ThemeSwitcher(
data: this,
child: widget.child,
);
}
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeSwitcher.of(context).themeData,
home: CheckAuth(),
);
}
}
I have wrapped ThemeSwitcherWidget around MaterialApp so the effect is throughout the app (even when you push new route with Navigator).
Use ThemeSwitcher.of(context).switchTheme(themeData) anywhere below ThemeSwithcerWidget to change the theme.
In question's case it should call ThemeSwitcher.of(context).switchTheme(Theme.of(context).copyWith(primaryColor: Colors.red)) to switch primary color to red throught out the app, for eg. on some button click
EDIT: replaced inheritFromWidgetOfExactType -> dependOnInheritedWidgetOfExactType, since it is deprecated - as pointed by Phoca in comments.
Using provider package:
theme_changer.dart
var darkTheme = ThemeData.dark();
var lightTheme= ThemeData.light();
class ThemeChanger extends ChangeNotifier {
ThemeData _themeData;
ThemeChanger(this._themeData);
get getTheme => _themeData;
void setTheme(ThemeData theme) {
_themeData = theme;
notifyListeners();
}
}
main.dart
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => ThemeChanger(lightTheme)),
],
child: MaterialAppWithTheme(),
);
}
}
class MaterialAppWithTheme extends StatelessWidget {
#override
Widget build(BuildContext context) {
final theme = Provider.of<ThemeChanger>(context);
return MaterialApp(
theme: theme.getTheme,
home: FirstScreen(),
);
}
first_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import './theme_changer.dart'
class FirstScreen extends StatelessWidget{
#override
Widget build(BuildContext context){
var _themeProvider=Provider.of<ThemeChanger>(context);
return Scaffold(
appBar: AppBar(title:Text("First Screen"),),
body:Container(width:MediaQuery.of(context).size.width,
height:MediaQuery.of(context).size.height,
child:Center(
child:FlatButton(child:Text("Press me"). onPressed:(){
_themeProvider.setTheme(_themeProvider.getTheme==lightTheme?darkTheme:lightTheme);
})
),
),
);
}
}
This is how to implement the dynamic Theme changing in Your App:
1.You should Change your MyApp into Stateful widget to enable the class to rebuild again when the color changes:
var _primary = Colors.blue ; // This will hold the value of the app main color
var themeData = ThemeData(
fontFamily: 'Raleway',
primaryColor: _primary, // so when the rebuilds the color changes take effect
brightness: Brightness.light,
backgroundColor: Colors.white,
accentColor: Colors.blue);
void main() => runApp(new App());
class App extends StatefulWidget {
App({Key key,}) :
super(key: key);
#override
_AppState createState() => new _AppState();
static void setTheme(BuildContext context, Color newColor) {
_AppState state = context.ancestorStateOfType(TypeMatcher<_AppState>());
state.setState(() {
state._primary = newColor;
});
}
}
2.The static method setTheme will be the one responsible for color changing :
class _AppState extends State<App> {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: Constants.appName,
theme: themeData,
home: CheckAuth(), //CheckAuth returns MyHomePage usually
);
}
}
3.When You want to change the theme color from anywhere from your code call this method:
App.setTheme(context, Colors.blue);
You can change the theme using setState or ValueListenableBuilder dynamically without any extension.
import 'package:flutter/material.dart';
void main() {
runApp(const MaterialApp(
title: 'Flutter Basics',
home: StartScreen(),
));
}
class StartScreen extends StatefulWidget {
const StartScreen({Key? key}) : super(key: key);
#override
State<StartScreen> createState() => _StartScreenState();
}
class _StartScreenState extends State<StartScreen> {
#override
Widget build(BuildContext context) {
final notifier = ValueNotifier(ThemeController.type);
return ValueListenableBuilder(
valueListenable: notifier,
builder: (BuildContext context, ThemeType value, Widget? child) {
print(value.name);
return Theme(
data: ThemeController.data,
child: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
setState(() {
int i = (value.index + 1) % ThemeType.values.length;
ThemeController.select(ThemeType.values[i]);
});
},
child: const Text('Change Theme(setState)'),
),
ElevatedButton(
onPressed: () {
int i = (value.index + 1) % ThemeType.values.length;
ThemeController.select(ThemeType.values[i]);
notifier.value = ThemeController.type;
},
child: const Text('Change Theme(Notifier)'),
),
],
),
)),
);
},
);
}
}
enum ThemeType {
dark,
light,
system,
}
class ThemeController {
static ThemeType _type = ThemeType.system;
static ThemeType get type => _type;
static ThemeData _themeData = _getData(ThemeType.system);
static ThemeData get data => _themeData;
static ThemeData select(ThemeType type) {
_type = type;
_themeData = _getData(type);
return _themeData;
}
static ThemeData _getData(ThemeType themeType) {
Brightness brightness = WidgetsBinding.instance.window.platformBrightness;
ThemeType type = themeType == ThemeType.system
? ThemeType.values[brightness.index]
: themeType;
switch (type) {
case ThemeType.dark:
return ThemeData.dark();
case ThemeType.light:
return ThemeData.light();
default:
return _themeData;
}
}
}
An easy approach (to me) is to achieve this is to make use of Stream with InheritedWidget.
The basic idea is to use an InheritedWidget with a StreamController, and wrap your MaterialApp (or a subtree of your app) with a StreamBuilder which gets the Stream from the StreamController from the InheritedWidget.
A complete tested working code sample as follows:
import 'dart:async';
import 'package:flutter/material.dart';
ThemeData darkTheme = ThemeData(
colorSchemeSeed: Colors.amber,
brightness: Brightness.dark,
);
ThemeData lightTheme = ThemeData(
colorSchemeSeed: Colors.blue,
brightness: Brightness.light,
);
void main() {
runApp(
CustomTheme(
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return StreamBuilder<ThemeData>(
initialData: lightTheme,
stream: CustomTheme.of(context)!.streamController.stream,
builder: (context, snapshot) => MaterialApp(
theme: snapshot.data,
home: const HomeScreen(),
),
);
}
}
class CustomTheme extends InheritedWidget {
CustomTheme({Key? key, required this.child}) : super(key: key, child: child);
final Widget child;
final StreamController<ThemeData> streamController = StreamController();
static CustomTheme? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<CustomTheme>();
}
#override
bool updateShouldNotify(CustomTheme oldWidget) {
return oldWidget != this;
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
CustomTheme customTheme = CustomTheme.of(context)!;
return Scaffold(
appBar: AppBar(
title: const Text('Custom Theme Demo'),
),
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(
onPressed: () {
customTheme.streamController.add(darkTheme);
},
child: const Text('DARK'),
),
ElevatedButton(
onPressed: () {
customTheme.streamController.add(lightTheme);
},
child: const Text('LIGHT'),
),
],
),
),
);
}
}
I have used get plugin and used Get.changeThemeMode(ThemeMode.(dark/system/light)); it works perfectly for me
First u have to add the get plugin by following the installing guide
then in main change
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'Add Your Title',
debugShowCheckedModeBanner: false,
theme:_lightTheme,
darkTheme: _darkTheme,
home: login(),
);
}
}
ON Tap function
import 'package:get/get.dart';
onTap: () {
Get.changeThemeMode(ThemeMode.dark);
setState(() async {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (BuildContext context) => super.widget));
});
}
I have two button for both themes and onTap i have just add the line
Get.changeThemeMode(ThemeMode.dark) for dark mode ,
Get.changeThemeMode(ThemeMode.dark) for light mode
I am trying to create a countdown timer application where I can press a button to add more time while the timer is actively counting down just like every microwave oven where there is a button you can press to add an extra minute to it's run time while it's running without stopping anything.
import 'package:flutter/material.dart';
void main() {
runApp(new MaterialApp(
home: new MyApp(),
));
}
class Countdown extends AnimatedWidget {
Countdown({ Key key, this.animation }) : super(key: key, listenable: animation);
Animation<int> animation;
#override
build(BuildContext context){
return new Text(
animation.value.toString(),
style: new TextStyle(fontSize: 150.0),
);
}
}
class MyApp extends StatefulWidget {
State createState() => new _MyAppState();
}
class _MyAppState extends State<MyApp> with TickerProviderStateMixin {
AnimationController _controller;
static const int kStartValue = 4;
#override
void initState() {
super.initState();
_controller = new AnimationController(
vsync: this,
duration: new Duration(seconds: kStartValue),
);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
floatingActionButton: new FloatingActionButton(
child: new Icon(Icons.play_arrow),
onPressed: () => _controller.forward(from: 0.0),
),
body: new Container(
child: new Center(
child: new Countdown(
animation: new StepTween(
begin: kStartValue,
end: 0,
).animate(_controller),
),
),
),
);
}
}
This example from a similar question about timers makes sense to me and has been my jumping off point. I know I need to change the duration and replace the animation with a new one with the appropriate duration, but I never get anything close to the correct behavior I am looking for.
I think the below is what you're trying to accomplish. Tapping the FloatingActionButton adds 5 seconds.
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Countdown Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Countdown Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _timeRemaining = 10;
Timer _timer;
#override
void initState() {
_timer = Timer.periodic(Duration(seconds: 1), (Timer t) => _getTime());
super.initState();
}
#override
void dispose() {
_timer?.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Text('$_timeRemaining'),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
_timeRemaining += 5;
}),
);
}
void _getTime() {
setState(() {
_timeRemaining == 0 ? _timeRemaining = 0 : _timeRemaining--;
});
}
}
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