Flutter Riverpod StateNotifierProvider: only watch changes for part of a model - flutter

I have a model like this:
class TimerModel {
const TimerModel(this.timeLeft, this.buttonState);
final String timeLeft;
final ButtonState buttonState;
}
enum ButtonState {
initial,
started,
paused,
finished,
}
And here is the StateNotifierProvider:
class TimerNotifier extends StateNotifier<TimerModel> {
TimerNotifier() : super(_initialState);
static const int _initialDuration = 10;
static final _initialState = TimerModel(
_durationString(_initialDuration),
ButtonState.initial,
);
final Ticker _ticker = Ticker();
StreamSubscription<int> _tickerSubscription;
void start() {
if (state.buttonState == ButtonState.paused) {
_tickerSubscription?.resume();
state = TimerModel(state.timeLeft, ButtonState.started);
} else {
_tickerSubscription?.cancel();
_tickerSubscription =
_ticker.tick(ticks: _initialDuration).listen((duration) {
state = TimerModel(_durationString(duration), ButtonState.started);
});
_tickerSubscription.onDone(() {
state = TimerModel(state.timeLeft, ButtonState.finished);
});
state =
TimerModel(_durationString(_initialDuration), ButtonState.started);
}
}
static String _durationString(int duration) {
final String minutesStr =
((duration / 60) % 60).floor().toString().padLeft(2, '0');
final String secondsStr =
(duration % 60).floor().toString().padLeft(2, '0');
return '$minutesStr:$secondsStr';
}
void pause() {
_tickerSubscription?.pause();
state = TimerModel(state.timeLeft, ButtonState.paused);
}
void reset() {
_tickerSubscription?.cancel();
state = _initialState;
}
#override
void dispose() {
_tickerSubscription?.cancel();
super.dispose();
}
}
class Ticker {
Stream<int> tick({int ticks}) {
return Stream.periodic(Duration(seconds: 1), (x) => ticks - x - 1)
.take(ticks);
}
}
I can listen for all changes in state like this:
final timerProvider = StateNotifierProvider<TimerNotifier>((ref) => TimerNotifier());
However I want to make another provider that only listens for changes in the ButtonState. This doesn't work:
final buttonProvider = StateProvider<ButtonState>((ref) {
return ref.watch(timerProvider.state).buttonState;
});
because it still returns all the state changes.
This also doesn't work:
final buttonProvider = StateProvider<ButtonState>((ref) {
return ref.watch(timerProvider.state.buttonState);
});
Because the state object doesn't have a buttonState property.
How do I only watch buttonState changes?

Using watch gives a new state whenever the watched state changes. So can solve the problem in two parts like so:
final _buttonState = Provider<ButtonState>((ref) {
return ref.watch(timerProvider.state).buttonState;
});
Using this provider will cause a rebuild every time the timerProvider.state changes. However, the trick is to do the following:
final buttonProvider = Provider<ButtonState>((ref) {
return ref.watch(_buttonState);
});
Since _buttonState will be the same for most of the timerProvider.state changes, watching _buttonState will only cause rebuilds when _buttonState actually changes.
Thanks to this post for showing the answer. That post also indicates that there will be a simplified syntax soon:
final buttonState = ref.watch(timerProvider.state.select((state) => state.buttonState));

Related

How to notify a stream to update in Flutter

I am trying to implement pagination in my Flutter app but this is the first time I have done it. My idea was to create a Stream of data that updates each time the user reaches the bottom of a list. I have failed to get this working. My current code has the logic for getting the new data and adding it to the existing data, but right now it's not even returning the first range of data so the snapshots are empty. As for the pagination functionality, I tried to use a ChangeNotifier to notify the Stream to update, but I don't know if that is working. Please take a look at the code below and let me know what should be changed.
The DataProvider class:
class DataProvider extends ChangeNotifier{
DataProvider() : super();
static var changeController = ChangeNotifier();
static void reload() {
changeController.notifyListeners();
}
static int lastRange = 0;
static List data = [];
static Stream<List> paginatedUsersAndPosts() async* {
List<UserSearchResult> usersList = data.first;
List<Post> postsList = data.last;
print('Notifier');
changeController.addListener(() async {
print('Change notified, getting data');
List<int> range() {
if (lastRange == 0) {
lastRange = 10;
return [0, 10];
} else {
// Example 0, 10 => 11, 20
int newMin = lastRange + 1;
int newMax = lastRange + 10;
lastRange = newMax;
return [newMin, newMax];
}
}
List<Map<String, dynamic>> postsDocs = await Supabase.db
.from('posts')
.select()
.order('date', ascending: false)
.range(range().first, range().last);
List<Post> newPostsList =
postsDocs.map((postDoc) => Post.fromJson(postDoc)).toList();
newPostsList.forEach((post) async {
postsList.add(post);
if (usersList.where((u) => u.uid == post.uid).isNotEmpty) {
Map<String, dynamic> userDoc =
await Supabase.db.from('profiles').select().single();
ProfileInfoObject profileInfo = ProfileInfoObject.fromJson(userDoc);
print('New profile: $profileInfo');
Profile profile = Profile(profileInfo, []);
profile.posts.add(post);
List blockedUsers = userDoc['blockedUsers'] as List;
UserSearchResult user = (UserSearchResult(
profile, userDoc['uid'].toString(), blockedUsers));
usersList.add(user);
}
});
});
yield [usersList, postsList];
}
}
The main widget that uses the stream:
class FloatingTabBarView extends StatefulWidget {
const FloatingTabBarView({Key? key}) : super(key: key);
#override
State<FloatingTabBarView> createState() => _FloatingTabBarViewState();
}
class _FloatingTabBarViewState extends State<FloatingTabBarView> {
#override
void initState() {
PermissionsService.checkPermissions(context);
DataProvider.reload();
super.initState();
}
Stream<List> stream = DataProvider.paginatedUsersAndPosts();
return StreamBuilder<List>(
stream: stream,
builder: (context, AsyncSnapshot<List<dynamic>> snapshot) {
...
});
}
#override
Widget build(BuildContext context) {
return floatingTabBarPageView();
}
}
Please take a look at pull_to_refresh package. This allows to implement pull to refresh and incrementally load data. Use the RefreshController to update when the data is refreshed or loaded.
Instantiate RefreshController
Wrap the ListView with SmartRefresher
Implement the onLoading callback to fetch data incrementally
Update the RefreshController on completion or error.
The package has a good usage example, do check it out.
The issue was the way I was creating the stream. Since I made the Stream a function, there was no way for me to call that function to reload. Instead, I moved the code to a static void that I can call from any page, created a StreamController, and used controller.add(data) at the end of the void which allows me to update the stream with the new data.

Riverpod trigger rebuild when update state with freezed copywith even if nothing changed

I thought Riverpod will only trigger rebuild if the state value is different but turn out it rebuild every time when state is set although the value is the same. Is that true?
The case is as below
#Freezed(genericArgumentFactories: true)
class Model with _$Model {
const factory Model({required int id}) = _Model;
}
class Manager {
static StateProvider<Model> modelProvider =
StateProvider<Model>((ref) => Model(id: 1));
Manager() {
Stream.periodic(Duration(seconds: 1)).take(1000).listen((event) {
ref.read(modelProvider.notifier).update((state) {
var cloneState = state.copyWith();
print("${state == cloneState}"); //This print true
return cloneState;
});
});
}
}
class TestWidget extends ConsumerWidget {
const TestWidget();
#override
Widget build(BuildContext context, WidgetRef ref) {
var model = ref.watch(Manager.modelProvider);
print("model change......................"); //print every second
return Text(model.id.toString());
}
}
It showed that the TestWidget was rebuilt every seconds but I thought it shouldn't as the state is the same although I set it again.
Am I missing something? Thanks.
Riverpod by default doesn't rely on == but identical to filter updates.
The reasoning is that == can be quite inefficient if your model becomes large.
But this causes the behavior you described: If two objects have the same content but use a different instance, listeners will be notified.
It's not considered a problem though, as there is no value in doing:
state = state.copyWith();
It's all about using identical(old, current) under the hood to compare states. identical presents for itself the following:
/// Check whether two references are to the same object.
///
/// Example:
/// ```dart
/// var o = new Object();
/// var isIdentical = identical(o, new Object()); // false, different objects.
/// isIdentical = identical(o, o); // true, same object
/// isIdentical = identical(const Object(), const Object()); // true, const canonicalizes
/// isIdentical = identical([1], [1]); // false
/// isIdentical = identical(const [1], const [1]); // true
/// isIdentical = identical(const [1], const [2]); // false
/// isIdentical = identical(2, 1 + 1); // true, integers canonicalizes
/// ```
external bool identical(Object? a, Object? b);
Here is a complete copy-run example:
void main() => runApp(const ProviderScope(child: MyApp()));
#Freezed(genericArgumentFactories: true)
class Model with _$Model {
const factory Model({required int id}) = _Model;
}
class Manager {
static StateProvider<Model> modelProvider = StateProvider<Model>((ref) {
Stream.periodic(const Duration(seconds: 1)).take(1000).listen((event) {
ref.read(modelProvider.notifier).update((state) {
final cloneState = state.copyWith();
// const cloneState = Model(id: 1); //The print true in both cases
print("${state == cloneState}"); //This print true
print("identical: ${identical(state, cloneState)}"); //This print false
return cloneState;
});
});
return const Model(id: 1);
});
}
class MyApp extends ConsumerWidget {
const MyApp();
#override
Widget build(BuildContext context, WidgetRef ref) {
var model = ref.watch(Manager.modelProvider);
print("model change......................"); //print every second
return MaterialApp(home: Text(model.id.toString()));
}
}
I modified the example a little, but kept the essence the same. Only by applying `const' can we achieve the absence of rebuilds.

How to update difference Time using Provider on flutter?

I have class:
class JWSModel extends ChangeNotifier {
String ketCounterShalat = "";
String diff = "";
void streamDifferentTime() {
DateTime now = DateTime.now();
if (now.isBefore(dhuhrTime) && now.isAfter(_dhuha)) {
ketCounterShalat = "Menuju dhuhur";
diff = now.difference(dhuhrTime).toString();
} else if (now.isBefore(asrTime) && now.isAfter(dhuhrTime)) {
ketCounterShalat = "Menuju ashar";
diff = now.difference(asrTime).toString();
} else if (now.isBefore(maghribTime) && now.isAfter(asrTime)) {
ketCounterShalat = "Menuju maghrib";
diff = now.difference(maghribTime).toString();
} else if (now.isBefore(ishaTime) && now.isAfter(maghribTime)) {
ketCounterShalat = "Menuju isya";
diff = now.difference(ishaTime).toString();
} else if (now.isBefore(sunriseTime) && now.isAfter(fajrTime)) {
diff = now.difference(sunriseTime).toString();
ketCounterShalat = "Menuju terbit";
} else if (now.isBefore(_dhuha) && now.isAfter(sunriseTime)) {
diff = now.difference(_dhuha).toString();
ketCounterShalat = "Menuju dhuha";
} else {
if (fajrTime.day == now.day) {
diff = now.difference(fajrTime).toString();
} else if (fajrTime.day != now.day) {
diff = now.difference(fajrTime.add(const Duration(days: 1))).toString();
}
ketCounterShalat = "Menuju subuh";
notifyListeners();
}
String get counterShalat => diff;
String get ketCounter => ketCounterShalat;
}
next, in the widget I created
class JWS extends StatefulWidget {
const JWS({Key? key}) : super(key: key);
#override
State<JWS> createState() => _JWSState();
}
class _JWSState extends State<JWS> {
JWSModel jws = JWSModel();
Future<void> getJWS() async {
jws.init();
}
#override
void initState() {
super.initState();
getJWS();
Timer.periodic(const Duration(seconds: 1), (timer) {
jws.streamDifferentTime();
});
}
#override
Widget build(BuildContext context) {
final state = context.watch<JWSModel>();
log(state.counterShalat);
Why not success? I want to update difference Time, I use a timer to update it. I don't want to use setState({}); because I think it will be hard to update every 1 second.
Update: if i log in class JWSModel success.
But, i call in class JWS nothing appears.
i want to get update different time but I don't want to use setState({});
or is there the right code for me to use.
class _JWSState extends State<JWS> {
#override
void initState() {
super.initState();
Timer.periodic(const Duration(seconds: 1), (timer) {
context.read<JWSModel>().streamDifferentTime();
});
}
#override
Widget build(BuildContext context) {
final state = context.watch<JWSModel>();
return Scaffold ( appBar: AppBar(),
body: Text(state.counterShalat,
style:TextStyle(color:Colors.blue)),
);
}
when any update from notifylistener, the value should be updated too.

Flutter BlocConsumer doesn't listen to state change when searching

I have been battling with this flutter bloc problem. I am currently using flutter Bloc 7.0.1. The BlocConsumer doesn't listen to the state changes at all. Anytime I enter values inside the search field, event is been called and state is yielded but the listener fail to listen to state changes.
This issue is really driving me mad.
STATE
part of 'people_bloc.dart';
#immutable
abstract class PeopleState {}
class PeopleInitial extends PeopleState {}
class PeopleLoadingState extends PeopleState {
#override
List<Object?> get props => [];
}
class SearchLoadingState extends PeopleState {
#override
List<Object?> get props => [];
}
BLOC
List<SearchPeopleResponseData> people = [];
#override
Stream<PeopleState> mapEventToState(
PeopleEvent event,
) async* {
if (event is SearchPeopleEvent) {
yield SearchLoadingState();
try {
var token = await getToken();
//print(token);
SearchPeopleResponse responseData =
await client.getPeople(token!, event.term);
if (responseData.status == 200) {
yield GetSearchResultState(getPeopleResponse: responseData);
} else {
yield PeopleErrorState(message: responseData.msg);
print("loadingE");
}
} catch (e) {
//print("error msg here ${e.toString()}");
PeopleErrorState(message: e.toString());
}
}
EVENT
part of 'people_bloc.dart';
#immutable
abstract class PeopleEvent {
const PeopleEvent();
}
class GetPeopleEvent extends PeopleEvent {
final String term;
GetPeopleEvent({required this.term});
#override
List<Object> get props => [term];
}
class SearchPeopleEvent extends PeopleEvent {
final String term;
SearchPeopleEvent({required this.term});
#override
List<Object> get props => [term];
}
VIEW
Widget build(BuildContext context) {
return BlocConsumer<PeopleBloc, PeopleState>(
listener: (context, state) {
print("Listener has been called");
if (state is GetSearchResultState) {
loading = false;
print("Result Found in view");
} else if (state is SearchLoadingState) {
loading = true;
print("Search loading");
} else if (state is PeopleLoadingState) {
loading = true;
}
See screenshot

How to access Riverpod StateNotifier state outside build without ConsumerWidget or HookWidget?

I have this class:
class BeatCounter extends StateNotifier<int> {
BeatCounter() : super(8);
int get counter => state;
void increment() {
if (state < 16) {
state++;
print('State $state');
} else
return;
}
void decrement() {
if (state > 1) {
state--;
} else
return;
}
}
final beatCounterProvider = StateNotifierProvider((ref) => BeatCounter());
And want to access the state inside a class that extends a StatefullWidget that I don't want (/know how to) change. So I can't use 'with' to extend 'ConsumerWidget' or 'HookWidget'.
How do I get the state in this class?
class ChordsTrack extends BaseWidget {
ChordsTrack({Key key, #required this.sample}) : super(key: key);
final SOUND_SAMPLE sample;
#override
_ChordsTrackState createState() => _ChordsTrackState();
}
class _ChordsTrackState extends BaseState<ChordsTrack> {
MultitrackChordBassBoxCreator multitrackBox =
MultitrackChordBassBoxCreator();
List<bool> _data = List.generate(***BeatCounter().state***, (i) => false);
#override
void on<Signal>(Signal signal) {
setState(() => _data = AudioEngine.trackdata[widget.sample]);
}
...
}
Newbie question, I know, but would really appreciate some help.
I solved it this way.
class BeatCounter extends StateNotifier<int> {
BeatCounter() : super(8);
static int counter = 8; //*
void increment() {
if (state < 16) {
state++;
counter++; //*
} else
return;
}
void decrement() {
if (state > 1) {
state--;
counter--;//*
} else
return;
}
}
Is there a better option?
Just use context.read(beatCounterProvider) in your initState.
class _ChordsTrackState extends BaseState<ChordsTrack> {
MultitrackChordBassBoxCreator multitrackBox =
MultitrackChordBassBoxCreator();
List<bool> _data;
#override
void initState(){
super.initState();
_data = List.generate(context.read(beatCounterProvider).state, (i) => false);
}
#override
void on<Signal>(Signal signal) {
setState(() => _data = AudioEngine.trackdata[widget.sample]);
}
...
}