Flutter Dynamic Theming - dart

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

Related

Flutter - RepaintBoundary causes state reset of StatefulWidget

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);
}
}

Undefined name with hero widget in flutter

I'm trying to work with hero widget .. every thing working fine.. my problem the tag for hero should be unique .. for the main scaffold i can make it unique by using the id from my api .. but i can't pass this id to the second Scaffold ... it become undefined .. how i can defined it ,,,
My Code is
import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:json/add.dart';
Future<List> getData() async {
String url = 'http://192.168.0.57:4000/api/contacts';
http.Response response = await http.get(url);
return json.decode(response.body);
}
List data;
void main() async {
data = await (getData());
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
routes: <String, WidgetBuilder>{
'/Add': (BuildContext context) => new Add(),
},
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
#override
State<StatefulWidget> createState() {
// TODO: implement createState
return HomePageState();
}
}
class HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
// TODO: implement build
return new MaterialApp(
title: "Test",
home: new Scaffold(
appBar: AppBar(
centerTitle: true,
title: new Text("Chat"),
),
body: new Center(
child: new ListView.builder(
itemCount: data.length,
itemBuilder: (BuildContext context, int position) {
return new ListTile(
title: new Text('${data[position]['name']}'),
subtitle: new Text('${data[position]['email']}'),
leading: new InkWell(
onTap: () {
Navigator.push(context,
MaterialPageRoute(builder: (BuildContext context) {
return HeroPage();
}));
},
child: Hero(
tag: "${data[position]['id']}",
child: new CircleAvatar(
child: new Text("${data[position]['name'][0]}"),
),
),
),
onTap: () {},
);
}),
),
),
);
}
}
class HeroPage extends StatefulWidget {
#override
State<StatefulWidget> createState() {
// TODO: implement createState
return HeroPageState();
}
}
class HeroPageState extends State<HeroPage> {
#override
Widget build(BuildContext context) {
// TODO: implement build
return new Scaffold(
appBar: AppBar(),
body: Hero(
tag: "${data[position]['id']}",
child: new Container(
color: Colors.blueAccent,
),
),
);
}
}
You can Pass the Position(Int) with help of Class Constructors.
class HeroPage extends StatefulWidget {
final int position;
final List data;
HeroPage({this.position,this.data});
#override
State<StatefulWidget> createState() {
// TODO: implement createState
return HeroPageState();
}
}
class HeroPageState extends State<HeroPage> {
#override
Widget build(BuildContext context) {
// TODO: implement build
return new Scaffold(
appBar: AppBar(),
body: Hero(
tag: "${widget.data[widget.position]['id']}",
child: new Container(
color: Colors.blueAccent,
),
),
);
}
}
Call the page like in your InkWell onTap::
Navigator.push(context,
MaterialPageRoute(builder: (BuildContext context) {
return HeroPage(position: position,data: data);
In the another page, try to wrap you hero in a ListView.builder, but the trick is only to set 1 in the itemCount param, with this, you can manipulate to show only one and get the correct tag

Flutter close a Dialog inside a condition

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);
}

How to listen to Drawer open/close animation in Flutter

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

Flutter Listview in stateless widget with initial offset

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.

Resources