Flutter redux: typedef vs viewmodel property - flutter

I have a question about flutter_redux.
I have seen that there are two ways for passing a function to a presentational component by StoreConnector:
with typedef
as a viewmodel property
What is the difference, if any, beetween this two pieces of code?
Piece 1:
class ExampleContainer extends StatelessWidget {
ExampleContainer({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return StoreConnector<AppState, _ViewModel>(
converter: _ViewModel.fromStore,
builder: (context, vm) {
return ExampleScreen(
exampleAction: vm.exampleAction,
);
},
);
}
}
class _ViewModel {
final Function() exampleAction;
_ViewModel({this.exampleAction});
static _ViewModel fromStore(Store<AppState> store) {
return _ViewModel(exampleAction: () {
store.dispatch(ExampleAction());
}
);
}
}
Piece 2:
typedef ExampleCallback = Function();
class ExampleContainer extends StatelessWidget {
ExampleContainer({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return StoreConnector<AppState, ExampleCallback>(
converter: (Store<AppState> store) {
return () {
store.dispatch(ExampleAction());
};
},
builder: (BuildContext context, ExampleCallback exampleCallback) {
return ExampleScreen(
exampleAction: exampleCallback,
);
},
);
}
}

Using a ViewModel allows you to pass through more than one object from your converter.

Related

ValueListenableBuilder builder method not called after updating Notifier value

I am trying to update the notifier value from parent widget whereas ValueListenableBuilder is defined in a child widget but the builder is not calling after changing the value.
Here is the parent widget code in which I have declared two child widgets as StatefulWidget and also declared a static object of Notifier class. I am calling the method updateMenuItemList from secondChild() widget like this HotKeysWidget.of(context)!.updateMenuItemList(currentCat!['items']); to update the list of firstChild() widget :
class HotKeysWidget extends StatefulWidget {
static HotKeysWidgetState? of(BuildContext context) =>
context.findAncestorStateOfType<HotKeysWidgetState>();
#override
State<StatefulWidget> createState() {
// TODO: implement createState
return HotKeysWidgetState();
}
}
class HotKeysWidgetState extends State<HotKeysWidget> {
static DealsNotifier appValueNotifier = DealsNotifier();
updateMenuItemList(List<Food> list) {
appValueNotifier.updateMenuList(list);
}
#override
Widget build(BuildContext context) {
return Container(child: Column(children: [
firstChild(),
secondChild(),
],
),
);
}
}
Here is my Notifier class:
class DealsNotifier {
ValueNotifier<List<Food>> dealList = ValueNotifier([]);
ValueNotifier<List<Food>> menuitemList = ValueNotifier([]);
ValueNotifier<List<Map<String,dynamic>>> categoryList = ValueNotifier([]);
void updateDealsList(List<Food> list) {
dealList.value = list;
print('DEAL LIST IN CLASS: ${dealList}');
}
void updateMenuList(List<Food> list) {
menuitemList.value = list;
print('PRICE CHANGE: ${menuitemList.value[2].price}');
print('MENU ITEM LIST IN CLASS: ${menuitemList}');
}
void updateCategoryList(List<Map<String,dynamic>> catList) {
categoryList.value = catList;
print('DEAL LIST IN CLASS: ${categoryList}');
}
List<Food> getDealList() {
return dealList.value;
}
List<Food> getMenuitemList() {
return menuitemList.value;
}
List<Map<String,dynamic>> getCategoryList() {
return categoryList.value;
}
}
And this is the child widget named as firstChild() in parent code. Here the ValueListenerBuilder is declared:
class firstChild extends StatefulWidget {
#override
State<StatefulWidget> createState() {
// TODO: implement createState
return firstChildState();
}
}
class firstChildState extends State<firstChild> {
#override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: HotKeysWidgetState.appValueNotifier.menuitemList,
builder: (context, List<Food> value, widget)
{
print('MENUITEM LIST UPDATED: ${value}');
return HotkeysMenuItemsWidget(
key: menuItemsKey,
currentMenu:currentCat != null ? value : [],
);
},
);
}
}
class secondChild extends StatefulWidget {
#override
State<StatefulWidget> createState() {
// TODO: implement createState
return secondChildState();
}
}
class secondChildState extends State<secondChild> {
#override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: (){
HotKeysWidget.of(context)!.updateMenuItemList([]);
},
child: Text(
'UPDATE',
maxLines: 2,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 12,
),
),
);
}
}
Anyone help me with this issue please.
Thanks in advance
While there's still not enough code shared to fully reproduce your situation, I can offer some suggestions.
The state portion of StatefulWidgets are private by default for a reason. You shouldn't make them public just to access variables that are inside there are several other to access outside classes within widgets.
So anytime you're doing something like this
class firstChild extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return firstChildState();
}
}
class firstChildState extends State<firstChild> {
#override
...
Just stick to the default syntax of a StatefulWidget and also classes should be in UpperCamelCase with the first letter capitalized.
class FirstChild extends StatefulWidget {
const FirstChild({Key? key}) : super(key: key);
#override
State<FirstChild> createState() => _FirstChildState();
}
class _FirstChildState extends State<FirstChild> {
#override
Widget build(BuildContext context) {
...
If you find yourself wanting to edit this default syntax its a clue that you need to find a better way to achieve whatever it is you're trying to do. If you're need to access a function that is declared in a Widget from outside that Widget, then that function should be declared somewhere else.
All that being said, unless you need setState, initState or another of the lifecycle functions, then you don't need a StatefulWidget to begin with. All those classes can be Stateless.
An easy way to make that DealsNotifier class globally accessible without a full on state management solution is to make it a static class.
class DealsNotifier {
static ValueNotifier<List<Food>> dealList = ValueNotifier([]);
static ValueNotifier<List<Food>> menuitemList = ValueNotifier([]);
static ValueNotifier<List<Map<String, dynamic>>> categoryList =
ValueNotifier([]);
static void updateDealsList(List<Food> list) {
dealList.value = list;
print('DEAL LIST IN CLASS: ${dealList}');
}
static void updateMenuList(List<Food> list) {
menuitemList.value = list;
print('PRICE CHANGE: ${menuitemList.value[2].price}');
print('MENU ITEM LIST IN CLASS: ${menuitemList}');
}
static void updateCategoryList(List<Map<String, dynamic>> catList) {
categoryList.value = catList;
print('DEAL LIST IN CLASS: ${categoryList}');
}
static List<Food> getDealList() {
return dealList.value;
}
static List<Food> getMenuitemList() {
return menuitemList.value;
}
static List<Map<String, dynamic>> getCategoryList() {
return categoryList.value;
}
}
Then when you need to pass in the valueListenable you access via DealsNotifier.menuitemlist and its always the same instance.
return ValueListenableBuilder(
valueListenable: DealsNotifier.menuitemList,
builder: (context, List<Food> value, widget) {
print('MENUITEM LIST UPDATED: ${value}');
return HotkeysMenuItemsWidget(
key: menuItemsKey,
currentMenu: currentCat != null ? value : [],
);
},
);
Here's the Stateless version of all those classes and wherever you need the UI update you can use ValueListenableBuilder and pass in DealsNotifier.whicheverVariableYouWantToListenTo in the valueListenable. Then call whichever relevant method from the DealsNotifier class ie. DealsNotifier.updateMenuList([]).
And you didn't share your HotkeysMenuItemsWidget but if that's where you're looking to see the change in the UI, then that is where the ValueListenableBuilder should be. Its currently too high up in the widget tree all it needs to do is re-render the list in that Widget, you don't need/want an entire re-build of the HotkeysMenuItemsWidget from a parent widget.
class FirstChild extends StatelessWidget {
const FirstChild({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return ValueListenableBuilder( // this should be inside HotkeysMenuItemsWidget
valueListenable: DealsNotifier.menuitemList,
builder: (context, List<Food> value, widget) {
print('MENUITEM LIST UPDATED: ${value}');
return HotkeysMenuItemsWidget(
key: menuItemsKey,
currentMenu: currentCat != null ? value : [],
);
},
);
}
}
class SecondChild extends StatelessWidget {
const SecondChild({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
DealsNotifier.updateMenuList([]);
},
child: Text(
'UPDATE',
maxLines: 2,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 12,
),
),
);
}
}
class HotKeysWidget extends StatelessWidget {
const HotKeysWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
FirstChild(),
SecondChild(),
],
),
);
}
}

ChangeNotifierProvider not re-rendering the UI (Flutter,Dart,Provider)

I have a BaseView that contains ChangeNotifieProvider and Consumer which will be common to use anywhere. This Widget also receives Generic types of ViewModel. It has onModelReady that to be called inside init state.
Also using get_it for Dependency injection.
Issue: Whenever the user inserts a new entry and calls fetch data, data gets loaded but UI still remains as it is.
If I remove the ChangeNotifierProvider and use only Consumer then it's re-rendering UI in a proper way. But I cannot pass the onModelReady function that is to be called in initState()
:::::::::::CODE:::::::::::::::::::::::::
base_view.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:businesshub/injections/injection_container.dart';
import 'package:businesshub/features/views/viewmodels/base_model.dart';
class BaseView<T extends BaseModel> extends StatefulWidget {
const BaseView({
Key? key,
this.onModelReady,
required this.builder,
}) : super(key: key);
final Function(T)? onModelReady;
final Widget Function(BuildContext context, T model, Widget? child) builder;
#override
_BaseViewState<T> createState() => _BaseViewState();
}
class _BaseViewState<T extends BaseModel> extends State<BaseView<T>> {
T model = locator<T>();
#override
void initState() {
super.initState();
if (widget.onModelReady != null) {
widget.onModelReady!(model);
}
}
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<T>(
create: (context) => model,
child: Consumer<T>(
builder: widget.builder,
),
);
}
}
USING::::::::::::::::HERE::::::::::::::::::::::
class RecentBillBuilder extends StatelessWidget {
const RecentBillBuilder({
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return BaseView<SalesBillViewModel>(
onModelReady: (model) {
model.fetchAndSetSalesBills(currentUser!.uid);
model.searchController.clear();
},
builder: (ctx, model, _) {
if (model.state == ViewState.busy) {
return Center(
child: CircularProgressIndicator.adaptive(),
);
}
return model.bills.fold(
(l) => ResourceNotFound(title: l.message!),
(r) => (r.isEmpty)
? ResourceNotFound(title: "Sales Bills not created yet!")
: ListView.builder(
itemCount: min(r.length, 7),
shrinkWrap: true,
reverse: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (ctx, index) {
return RecentBillsCard(bill: r[index]);
},
),
);
},
);
}
}

Updating and animating an AnimatedList with Provider in Flutter

I'm able to successfully animate an AnimatedList's contents in Flutter when the list data is stored in the same component that owns the list widget (i.e., there's no rebuild happening when there's changes to the list data). I run into issues when I try to get the items for the list from a ChangeNotifier using Provider and Consumer.
The component that owns the AnimatedList, let's call it ListPage, is built with a Consumer<ListItemService>. My understanding is that ListPage is then rebuilt whenever the service updates the list data and calls notifyListeners(). When that happens, I'm not sure where within ListPage I could call AnimatedListState.insertItem to animate the list, since during the build the list state is still null. The result is a list that doesn't animate its contents.
I think my question boils down to "how do I manage state for this list if the contents are fetched and updated in real time?", and ideally I'd like to understand what's going on but I'm open to suggestions on how I should change this if this isn't the best way to approach the task.
Here's some code that illustrates the problem:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<AuthService>(
create: (_) => AuthService(),
),
ChangeNotifierProxyProvider<AuthService, ListItemService>(
create: (_) => ListItemService(),
update: (_, authService, listItemService) =>
listItemService!..update(authService),
),
],
child: MaterialApp(
home: HomePage(),
),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<ListItemService>(
builder: (context, listItemService, _) =>
ListPage(items: listItemService.items),
);
}
}
// Implementation details aren't really relevant, but
// this only updates if the user logs in or out.
class AuthService extends ChangeNotifier {}
class ListItemService extends ChangeNotifier {
List<Item> _items = [];
List<Item> get items => _items;
Future<void> update(AuthService authService) async {
// Method that subscribes to a Firestore snapshot
// and calls notifyListeners() after updating _items.
}
}
class Item {
Item({required this.needsUpdate, required this.content});
final String content;
bool needsUpdate;
}
class ListPage extends StatefulWidget {
const ListPage({Key? key, required this.items}) : super(key: key);
final List<Item> items;
#override
_ListPageState createState() => _ListPageState();
}
class _ListPageState extends State<ListPage> {
final GlobalKey<AnimatedListState> _listKey = GlobalKey();
late int _initialItemCount;
#override
void initState() {
_initialItemCount = widget.items.length;
super.initState();
}
void _updateList() {
for (int i = 0; i < widget.items.length; i++) {
final item = widget.items[i];
if (item.needsUpdate) {
// _listKey.currentState is null here if called
// from the build method.
_listKey.currentState?.insertItem(i);
item.needsUpdate = false;
}
}
}
#override
Widget build(BuildContext context) {
_updateList();
return AnimatedList(
key: _listKey,
initialItemCount: _initialItemCount,
itemBuilder: (context, index, animation) => SizeTransition(
sizeFactor: animation,
child: Text(widget.items[index].content),
),
);
}
}
You can use didUpdateWidget and check the difference between the old and new list. "Checking the difference" means looking at what has been removed vs added. In you case the Item widget should have something to be identified. You can use Equatable for example so that an equality between Items is an equality between their properties.
One other important aspect is that you are dealing with a list, which is mutable, but Widgets should be immutable. Therefore it is crucial that whenever you modify the list, you actually create a new one.
Here are the implementations details, the most interesting part being the comment of course (though the rendering is fun as well ;)):
import 'dart:async';
import 'dart:math';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<AuthService>(
create: (_) => AuthService(),
),
ChangeNotifierProxyProvider<AuthService, ListItemService>(
create: (_) => ListItemService(),
update: (_, authService, listItemService) => listItemService!..update(authService),
),
],
child: MaterialApp(
home: HomePage(),
),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Material(
child: SafeArea(
child: Consumer<ListItemService>(
builder: (context, listItemService, _) => ListPage(
// List.from is very important because it creates a new list instead of
// giving the old one mutated
items: List.from(listItemService.items),
),
),
),
);
}
}
// Implementation details aren't really relevant, but
// this only updates if the user logs in or out.
class AuthService extends ChangeNotifier {}
class ListItemService extends ChangeNotifier {
List<Item> _items = [];
List<Item> get items => _items;
Future<void> update(AuthService authService) async {
// Every 5 seconds
Timer.periodic(Duration(seconds: 5), (timer) {
// Either create or delete an item randomly
if (Random().nextDouble() > 0.5 && _items.isNotEmpty) {
_items.removeAt(Random().nextInt(_items.length));
} else {
_items.add(
Item(
needsUpdate: true,
content: 'This is item with random number ${Random().nextInt(10000)}',
),
);
}
notifyListeners();
});
}
}
class Item extends Equatable {
Item({required this.needsUpdate, required this.content});
final String content;
bool needsUpdate;
#override
List<Object?> get props => [content]; // Not sure you want to include needsUpdate?
}
class ListPage extends StatefulWidget {
const ListPage({Key? key, required this.items}) : super(key: key);
final List<Item> items;
#override
_ListPageState createState() => _ListPageState();
}
class _ListPageState extends State<ListPage> {
final _listKey = GlobalKey<AnimatedListState>();
// You can use widget if you use late
late int _initialItemCount = widget.items.length;
/// Handles any removal of [Item]
_handleRemovedItems({
required List<Item> oldItems,
required List<Item> newItems,
}) {
// If an [Item] was in the old but is not in the new, it has
// been removed
for (var i = 0; i < oldItems.length; i++) {
final _oldItem = oldItems[i];
// Here the equality checks use [content] thanks to Equatable
if (!newItems.contains(_oldItem)) {
_listKey.currentState?.removeItem(
i,
(context, animation) => SizeTransition(
sizeFactor: animation,
child: Text(oldItems[i].content),
),
);
}
}
}
/// Handles any added [Item]
_handleAddedItems({
required List<Item> oldItems,
required List<Item> newItems,
}) {
// If an [Item] is in the new but was not in the old, it has
// been added
for (var i = 0; i < newItems.length; i++) {
// Here the equality checks use [content] thanks to Equatable
if (!oldItems.contains(newItems[i])) {
_listKey.currentState?.insertItem(i);
}
}
}
// Here you can check any update
#override
void didUpdateWidget(covariant ListPage oldWidget) {
super.didUpdateWidget(oldWidget);
_handleAddedItems(oldItems: oldWidget.items, newItems: widget.items);
_handleRemovedItems(oldItems: oldWidget.items, newItems: widget.items);
}
#override
Widget build(BuildContext context) {
return AnimatedList(
key: _listKey,
initialItemCount: _initialItemCount,
itemBuilder: (context, index, animation) => SizeTransition(
sizeFactor: animation,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(widget.items[index].content),
),
),
);
}
}

Flutter - How to make a FutureBuilder that resolves a Future ImageProvider

I'd like to eliminate the boilerplate code used to wait and then replace an ImageProvider placeholder with a final image.
The following works for an Image Widget.
class FutureImage extends StatelessWidget {
final Future<Image> futureImage;
final Image placeholder;
const FutureImage({Key key, this.futureImage, this.placeholder})
: super(key: key);
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: futureImage,
builder: (BuildContext context, AsyncSnapshot<Image> image) {
if (image.hasData) {
return image.data;
} else {
return placeholder;
}
},
);
}
}
However I need an ImageProvider (because it can be used in places where an Image cannot)
The following doesn't work because ImageProvider isn't a Widget:
class FutureImageProvider extends StatelessWidget {
final Future<ImageProvider> futureImageProvider;
final ImageProvider placeholder;
const FutureImageProvider(
{Key key, this.futureImageProvider, this.placeholder})
: super(key: key);
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: futureImageProvider,
builder: (BuildContext context, AsyncSnapshot<ImageProvider> image) {
if (image.hasData) {
return image.data;
} else {
return placeholder;
}
},
);
}
}

Flutter - InheritedModel still update child even if it does not match an aspect

Good day. I've watched a video about Flutter's InheritedModel and got interested on it. Unfortunately, I can't seems to make it work properly.
Summary: Need help how to properly implement InheritedModel.
Expected Code Output: Widget CountText should not be updated when updating count parameter in CountModel.
Actual Code Output: CountText still updates (I think this is due to that the parent widget is a StatefulWidget)
Details
I am trying to implement a Counter app using InheritedModel. Code below is my code
import 'package:flutter/material.dart';
class CountModel extends InheritedModel<String> {
final int count;
CountModel({ this.count, child }) : super(child: child);
#override
bool updateShouldNotify(CountModel oldWidget) {
if (oldWidget.count != count) {
return true;
}
return false;
}
#override
bool updateShouldNotifyDependent(InheritedModel<String> oldWidget, Set<String> dependencies) {
if (dependencies.contains('counter')) {
return true;
}
return false;
}
static CountModel of(BuildContext context, String aspect) {
return InheritedModel.inheritFrom<CountModel>(context, aspect: aspect);
}
}
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Counter',
theme: Theme.of(context),
home: Counter(),
);
}
}
class Counter extends StatefulWidget {
#override
CounterState createState() => CounterState();
}
class CounterState extends State<Counter> {
int count = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// title: Text("Counter"),
),
body: CountModel(
count: count,
child: CounterText()
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
++count;
});
},
child: Icon(Icons.add),
),
);
}
}
class CounterText extends StatelessWidget {
#override
Widget build(BuildContext context) {
CountModel model = CountModel.of(context, 'test');
return Text('Count: ${model.count}');
}
}
I have a CountModel as InheritedModel and a CountText widget which consumes the data from the CountModel. As you can see in the implementation of the CountText, it pass test when getting the CountModel. In my understanding, it should not be updated when the count value is updated in the CountModel. Unfortunately, this does not happen.
In short, you should use const.
Add const to CounterText constructor
class CounterText extends StatelessWidget {
const CounterText();
...
}
and use const when you create instance of CounterText() (const CounterText())
class CounterState extends State<Counter> {
...
#override
Widget build(BuildContext context) {
return Scaffold(
...
body: CountModel(..., child: const CounterText()),
...
);
}
}
And voila 🎉
I have described why this is happening here in details