I am learning MVVM with the Provider package in Flutter.
I am creating a screen using StatelessWidget.
I have come to the point where I need to cache the widget, where should I store it?
StatelessWidget is immutable, so it cannot have variables in its properties.
MVVM's ViewModel cannot depend on a View, so it cannot have a Widget instance.
In the above question, I used Widget as an example, but there are other things I would like to store in View variables, such as GlobalKey.
The test code is as follows The purpose is to reference _needCacheWidget and _key later, but
I get a warning in the comment section
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
Future<void> main() async {
runApp(MultiProvider(
providers: [
ChangeNotifierProvider<TestViewModel>(create: (context) => TestViewModel()),
],
child: S1(),
));
}
class TestViewModel extends ChangeNotifier {
var _count = 0;
int get count {
return _count;
}
void increment() {
this._count += 1;
notifyListeners();
}
}
/// This class (or a class that this class inherits from) is marked as '#immutable',
/// but one or more of its instance fields aren't final: S1._needCacheWidget, S1._key (Documentation)
class S1 extends StatelessWidget {
Widget? _needCacheWidget = null;
GlobalKey? _key = null;
Widget _getNeedCacheWidget() {
GlobalKey key = _key ?? GlobalKey();
return _needCacheWidget ?? Text("need cache widget", key: key);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("text"),
),
body: Column(
children: [
Consumer<TestViewModel>(
builder: (context, testViewModel, child) {
return Text("count-${testViewModel.count}");
},
),
_getNeedCacheWidget(),
],
),
floatingActionButton: Consumer<TestViewModel>(
builder: (context, testViewModel, child) {
return FloatingActionButton(
onPressed: () {
testViewModel.increment();
print("globalKey is $_key"); // I want to refer to GlobalKey of NeedCacheWidget here.
},
child: const Icon(Icons.add),
);
},
),
),
);
}
}
Related
I am using ValueListenableBuilder to update my UI based on the data provided to it. I am initializing the ValueNotifier with value. But when I tried to read that value it returns nothing.
This is my Notifier class code:
class AppValueNotifier
{
ValueNotifier<List<Food>> valueNotifier = ValueNotifier([]);
void updateDealsList(List<Food> list)
{
valueNotifier.value=list;
print('DEAL LIST IN CLASS: ${ valueNotifier.value}');
}
List<Food> getDealList()
{
return valueNotifier.value;
}
}
In a separate widget I am initializing the value like this:
class HomeWidgetState extends State<HomeWidget> {
AppValueNotifier appValueNotifier = AppValueNotifier();
.
.
.
assignList(List<Food> dealList)
{
appValueNotifier.updateDealsList(dealList);
}
..
..
.
}
Now in another widget class I am building my UI with this data like this:
AppValueNotifier appValueNotifier = AppValueNotifier();
Widget buildList()
{
return ValueListenableBuilder(
valueListenable: appValueNotifier.valueNotifier,
builder: (context, List<Food> value, widget) {
print(
'DEAL LIST: ${appValueNotifier.getDealList()}');
return DealsWidget(
key: menuItemsKey,
updateList: (oldIndex, newIndex, newList) {},
currentMenu: value,
menuItemNodes: [],
changeCellColor: (color, catid) {},
);
},
);
}
But it is returning empty list instead. Not that list which is being initialized at the start.
Anyone help me what is the issue here:
Thanks in advance
You should be able to initialize your ValueNotifier list either in the constructor or based on an action (i.e. a button click, as shown below). Notice how I'm providing the AppValueNotifier service using the Provider pattern, and one widget triggers the action while a separate widget listens to the changes being made.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
Provider(
create: (context) => AppValueNotifier(),
child: MyApp()
)
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Column(
children: [
TriggerWidget(),
Expanded(
child: MyWidget(),
)
]
),
),
);
}
}
class Food {
final String name;
Food({ required this.name });
}
class AppValueNotifier
{
ValueNotifier<List<Food>> valueNotifier = ValueNotifier([]);
void updateDealsList(List<Food> list)
{
valueNotifier.value = list;
print('DEAL LIST IN CLASS: ${ valueNotifier.value}');
}
List<Food> getDealList()
{
return valueNotifier.value;
}
}
class TriggerWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
AppValueNotifier appValueNotifier = Provider.of<AppValueNotifier>(context, listen: false);
return TextButton(
child: const Text('Add Items!'),
onPressed: () {
appValueNotifier.updateDealsList([
Food(name: 'Food 1'),
Food(name: 'Food 2'),
]);
},
);
}
}
class MyWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
AppValueNotifier appValueNotifier = Provider.of<AppValueNotifier>(context, listen: false);
return ValueListenableBuilder(
valueListenable: appValueNotifier.valueNotifier,
builder: (context, List<Food> value, widget) {
var list = value;
return ListView.builder(
itemCount: list.length,
itemBuilder: (context, index) {
return Text(list[index].name);
}
);
},
);
}
}
You get this as output:
Also checkout this Gist you can run on DartPad.dev to check out how it works.
I'm trying to implement Provider state management on counter application to understand Provider's functionality better. I have added two buttons with respect to two different text widget. So, now whenever I click any of the two widget both the Text widgets get update and give same value. I want both the widgets independent to each other.
I have used ScopedModel already and got the desire result but now I want to try with provider.
Image Link : https://i.stack.imgur.com/ma3tR.png
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
print("====Home Page Rebuilt====");
return Scaffold(
appBar: AppBar(
title: Text("HomePage"),
),
body: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
//crossAxisAlignment:CrossAxisAlignment.center,
children: [
Consumer<CounterModel>(
builder: (context, value, child) {
return CustomWidget(
number: value.count.toString(),
);
},
),
Consumer<CounterModel>(
builder: (context, value, child) {
return CustomWidget(
number: value.count.toString(),
);
},
),
],
)),
);
}
}
class CustomWidget extends StatelessWidget {
final String number;
const CustomWidget({Key key, this.number}) : super(key: key);
#override
Widget build(BuildContext context) {
print("====Number Page Rebuilt====");
return ButtonBar(
alignment: MainAxisAlignment.center,
children: [
Consumer<CounterModel>(
builder: (context, value, child) {
return Text(
value.count.toString(),
style: Theme.of(context).textTheme.headline3,
);
},
),
FlatButton(
color: Colors.blue,
onPressed: () =>
Provider.of<CounterModel>(context, listen: false).increment(),
child: Text("Click"),
),
],
);
}
}
If you want them independent from each other, then you need to differentiate them somehow. I have a bit of a different style to implement the Provider and it hasn't failed me yet. Here is a complete example.
You should adapt your implementation to something like this:
Define your provider class that extends ChangeNotifier in a CounterProvider.dart file
import 'package:flutter/material.dart';
class CounterProvider extends ChangeNotifier {
/// You can either set an initial value here or use a UserProvider object
/// and call the setter to give it an initial value somewhere in your app, like in main.dart
int _counter = 0; // This will set the initial value of the counter to 0
int get counter => _counter;
set counter(int newValue) {
_counter = newValue;
/// MAKE SURE YOU NOTIFY LISTENERS IN YOUR SETTER
notifyListeners();
}
}
Wrap your app with a Provider Widget like so
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
/// don't forget to import it here too
import 'package:app/CounterProvider.dart';
void main() {
runApp(
MaterialApp(
initialRoute: '/root',
routes: {
'/root': (context) => MyApp(),
},
title: "Your App Title",
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
/// Makes data available to everything below it in the Widget tree
/// Basically the entire app.
ChangeNotifierProvider<CounterProvider>.value(value: CounterProvider()),
],
child: MaterialApp(
home: HomeScreen(),
),
);
}
}
Access and update data anywhere in the app
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
/// MAKE SURE TO IMPORT THE CounterProvider.dart file
import 'package:app/CounterProvider.dart';
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
CounterProvider counterProvider;
#override
Widget build(BuildContext context) {
/// LISTEN TO THE CHANGES / UPDATES IN THE PROVIDER
counterProvider = Provider.of<CounterProvider>(context);
return Scaffold(
appBar: AppBar(
title: Text("HomePage"),
),
body: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
//crossAxisAlignment:CrossAxisAlignment.center,
children: [
_showCounterButton(1),
_showCounterButton(2),
],
),
),
);
}
Widget _showCounterButton(int i) {
return ButtonBar(
alignment: MainAxisAlignment.center,
children: [
Text(
i == 1
? counterProvider.counter1.toString()
: counterProvider.counter2.toString(),
style: Theme.of(context).textTheme.headline3,
),
FlatButton(
color: Colors.blue,
onPressed: () {
/// UPDATE DATA IN THE PROVIDER. BECAUSE YOU're USING THE SETTER HERE,
/// THE LISTENERS WILL BE NOTIFIED AND UPDATE ACCORDINGLY
/// you can do this in any other file anywhere in the Widget tree, as long as
/// it it beneath the main.dart file where you defined the MultiProvider
i == 1
? counterProvider.counter1 += 1
: counterProvider.counter2 += 1;
setState(() {});
},
child: Text("Click"),
),
],
);
}
}
If you want, you can change the implementation a bit. If you have multiple counters, for multiple widgets, then just create more variables in the CounterProvider.dart file with separate setters and getters for each counter. Then, to display/update them properly, just use a switch case inside the _showCounterButton() method and inside the onPressed: (){ switch case here, before setState((){}); }.
Hope this helps and gives you a better understanding of how Provider works.
I am learning Mobx Flutter and would like to have an observer showing modification of a field in a class.
When using an int instead of a custom class it is working.
So i suspect I am not declaring properly the class in the store
Here is the code of my store
import 'package:mobx/mobx.dart';
// generated file
part 'bikeModel.g.dart';
class Cell {
String description;
String value;
String unit;
Cell({this.description, this.value, this.unit});
}
class BikeData = _BikeData with _$BikeData;
abstract class _BikeData with Store {
Timer _timerSimu;
#observable
int cadence = 0;
#observable
Cell cello = Cell(description: 'desc', value: 'oo', unit: 'km/h');
#action
startSimul() {
int _tick = 0;
cadence++;
cello.value = cadence.toString();
_timerSimu = Timer.periodic(Duration(seconds: 1), (timer) {
print('Screen simu is ticking...$_tick');
_tick++;
cadence++;
});
}
#action
stopSimu() {
_timerSimu.cancel();
}
}
and here is the main code
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx_first/bikeModel.dart';
import 'globals.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: 'MobX',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage();
#override
Widget build(BuildContext context) {
BikeData store = BikeData();
return GestureDetector(
onPanUpdate: (details) {
if (details.delta.dx > 0) {
// Right swipe
print('this is a right swipe');
} else {
// left swipe
print('this is a left swipe');
}
},
child: Scaffold(
appBar: AppBar(
title: Text('MobX Test'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Observer(
builder: (_) => Text('cello.value ${store.cello.value}')),
Observer(builder: (_) => Text('cadence ${store.cadence}')),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: store.startSimul,
tooltip: 'Change',
child: Icon(Icons.add),
),
),
);
}
}
cadence is changing everything second on the screen but not cello.value
What is the proper way to declare cello observable?
Problem is that you are just changing value of object item(value). You have to completely change object, then only mobx find value is change.
Replace your following line
cello.value = cadence.toString();
With following code:
cello = Cell(
description: cello.description,
value: cadence.toString(),
unit: cello.unit);
import 'package:flutter/material.dart';
import 'dart:async';
void main() => runApp(MyApp());
//Using Bloc
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: bloc.darkThemeEnabled,
initialData: false,
builder: (context, snapshot) => MaterialApp(
theme: snapshot.data ? ThemeData.dark() : ThemeData.light(),
home: HomePage(snapshot.data)),
);
}
}
class HomePage extends StatelessWidget {
final bool darkThemeEnabled;
HomePage(this.darkThemeEnabled);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Dynamic Theme"),
),
body: Center(
child: Text("Hello World"),
),
drawer: Drawer(
child: ListView(
children: <Widget>[
ListTile(
title: Text("Dark Theme"),
trailing: Switch(
value: darkThemeEnabled,
onChanged: bloc.changeTheme,
),
)
],
),
),
);
}
}
class Bloc {
final _themeController = StreamController<bool>();
get changeTheme => _themeController.sink.add;
get darkThemeEnabled => _themeController.stream;
}
final bloc = Bloc();
1.A warning says to Close instances of dart.core.sink
2.Why dart.core.sink is used in flutter?
3.How can I solve this error
4.Its error documentation redirects me to this website link
5.I don't know how to use these methods in flutter please guide me
dart.core.sink is an interface that is implemented by Stream.
The warning is showing, because the dart compiler wants you to .close() your instance of a Stream. In this case that is your final _themeController = StreamController<bool>().
If you want to fix the warning, add
void dispose() {
_themeController.close();
}
to your Bloc class.
Just adding the method is not doing much, since it's not called. So you should change your main() method to call bloc.dispose() after runApp(MyApp()).
That error occur when missing close StreamController.
Simple way to fix:
Create abstract class:
abstract class Bloc {
void dispose();
}
Your bloc class implements Bloc, now you can close StreamController in dispose:
class ColorBloc implements Bloc {
// streams of Color
StreamController streamListController = StreamController<Color>.broadcast();
// sink
Sink get colorSink => streamListController.sink;
// stream
Stream<Color> get colorStream => streamListController.stream;
// function to change the color
changeColor() {
colorSink.add(getRandomColor());
}
// Random Colour generator
Color getRandomColor() {
Random _random = Random();
return Color.fromARGB(
_random.nextInt(256),
_random.nextInt(256),
_random.nextInt(256),
_random.nextInt(256),
);
}
// close Stream
#override
void dispose() {
streamListController.close();
}
}
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();