How can I use streamProvider with StateNotifier together? - flutter

I have WebSocket service which I want to use with StreamProvider. When I used it with simple StreamProvider and tried to get data it worked well as it was described in docs, but my issue is a little bit complicated: I want to make it reactive and change seconds (I get DateTime from WebSocket). So I found out that in riverpod we can use state with StateNotifier and using it I can change state. So, I decided to combine two kinds of providers (in docs it says I can easily do it), but when I placed StreamProvider inside StateNotifier, it stopped getting requests to website and retrieve data. How can I solve this issue?
My full code looks like this:
final todosProvider =
StateNotifierProvider<TickingTime, String?>((ref) => TickingTime());
class TickingTime extends StateNotifier<String?> {
static String? time;
static var websockets;
TickingTime() : super(time) {
Timer.periodic(const Duration(seconds: 1), (timer) {
state = time;
});
print('NOT HEHE1');
websockets = StreamProvider((ref) async* {
print('NOT HEHE');
final httpConnectionOptions = HttpConnectionOptions(
accessTokenFactory: () => SharedPreferenceService().loginWithToken(),
skipNegotiation: true,
transport: HttpTransportType.WebSockets);
final connectionValue = HubConnectionBuilder()
.withUrl(
'http://securelink/link',
options: httpConnectionOptions,
)
.build();
print('NOT HEHE2');
await connectionValue.start();
if (connectionValue.state == HubConnectionState.Connected) {
await connectionValue.invoke('GetCurrentDateTime').then((value) {
time = value as String;
DateTime dateTime = DateTime.parse(time ?? 'no time');
time = DateFormat('HH:mm:ss').format(dateTime);
});
yield time;
}
;
ref.onDispose(() {
connectionValue.onclose(({error}) {
throw Exception('NOT HEHE, YOU KNOW');
});
});
});
}
}
UI:
class MyApp extends HookConsumerWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
var time = ref.watch(todosProvider);
return MaterialApp(
home: Scaffold(
body: Center(
child: Text(time ?? 'NOTHING TO SHOW'),
),
),
);
data I get from WebSocket:
14:25:34

Related

make hubconnection.start() only once in the whole app

I am using the singalR package in order to use websocket in my application. During the writing of my program, I was advised to use await hubconnection.start only once and throughout the whole application. Is it possible? I thought that I could run this task somehow in the background and not make every websocket request start every time I want to use some kind of request. Is this possible or is this the wrong idea?
now I an trying to split up the whole thing and found out that in initState we can create method which will be start first before all others code in app, so I decided to split it up like that(in educational purpose I declaired connection setting globally in my file)
final httpConnectionOptions = HttpConnectionOptions(
accessTokenFactory: () => SharedPreferenceService().loginWithToken(),
skipNegotiation: true,
transport: HttpTransportType.WebSockets);
final hubConnection = HubConnectionBuilder()
.withUrl(
'http://secureLink/mysocket',
options: httpConnectionOptions,
)
.build();
this is my class:
class TestClass extends StatefulWidget {
bool? gpsenabled;
TestClass({Key? key}) : super(key: key);
#override
State<TestClass> createState() => _TestClassState();
}
class _TestClassState extends State<TestClass> {
void checkGPS() async {
if (hubConnection.state == HubConnectionState.Connected) {
await hubConnection.invoke('GPSEnable').then((value) {
setState(() {
widget.gpsenabled = value as bool;
});
print(widget.gpsenabled);
Future.delayed(Duration(seconds: 5));
checkGPS();
});
}
}
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((_) {
hubConnection.start();
print('connection first');
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Column(

Flutter Custom State Management

What I am trying to achieve is a small custom state management solution that I believe is powerful enough to run small and large apps. The core is based on the ValueNotifier and ValueListenable concepts in flutter. The data can be accessed anywhere in the app with out context since I am storing the data like this:
class UserData {
static ValueNotifier<DataLoader<User>> userData =
ValueNotifier(DataLoader<User>());
static Future<User> loadUserData() async {
await Future.delayed(const Duration(seconds: 3));
User user = User();
user.age = 23;
user.family = 'Naoushy';
user.name = 'Anass';
return user;
}
}
So by using UserData.userData you can use the data of the user whenever you want. Everything works fine until I encountered a problem of providing a child to my custom data consumer that rebuilds the widget when there is a new event fired. The DataLoader class looks like this:
enum Status { none, hasError, loading, loaded }
class DataLoader<T> {
Status status = Status.none;
T? data;
Object? error;
bool get hasError => error != null;
bool get hasData => data != null;
}
which is very simple. Now the class for consuming the data and rebuilding looks like this:
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:testing/utils/dataLoader/data_loader.dart';
class DataLoaderUI<T> extends StatefulWidget {
final ValueNotifier<DataLoader<T>> valueNotifier;
final Widget noneStatusUI;
final Widget hasErrorUI;
final Widget loadingUI;
final Widget child;
final Future<T> future;
const DataLoaderUI(
{Key? key,
required this.valueNotifier,
this.noneStatusUI = const Text('Data initialization has not started'),
this.hasErrorUI = const Center(child: Text('Unable to fetch data')),
this.loadingUI = const Center(
child: CircularProgressIndicator(),
),
required this.child,
required this.future})
: super(key: key);
#override
State<DataLoaderUI> createState() => _DataLoaderUIState();
}
class _DataLoaderUIState extends State<DataLoaderUI> {
Future startLoading() async {
widget.valueNotifier.value.status = Status.loading;
widget.valueNotifier.notifyListeners();
try {
var data = await widget.future;
widget.valueNotifier.value.data = data;
widget.valueNotifier.value.status = Status.loaded;
widget.valueNotifier.notifyListeners();
} catch (e) {
log('future error', error: e.toString());
widget.valueNotifier.value.error = e;
widget.valueNotifier.value.status = Status.hasError;
widget.valueNotifier.notifyListeners();
}
}
#override
void initState() {
super.initState();
log('init state launched');
if (!widget.valueNotifier.value.hasData) {
log('reloading or first loading');
startLoading();
}
}
//AsyncSnapshot asyncSnapshot;
#override
Widget build(BuildContext context) {
return ValueListenableBuilder<DataLoader>(
valueListenable: widget.valueNotifier,
builder: (context, dataLoader, ui) {
if (dataLoader.status == Status.none) {
return widget.noneStatusUI;
} else if (dataLoader.status == Status.hasError) {
return widget.hasErrorUI;
} else if (dataLoader.status == Status.loading) {
return widget.loadingUI;
} else {
return widget.child;
}
});
}
}
which is also simple yet very effective. since even if the initState function is relaunched if the data is already fetched the Future will not relaunch.
I am using the class like this:
class TabOne extends StatefulWidget {
static Tab tab = const Tab(
icon: Icon(Icons.upload),
);
const TabOne({Key? key}) : super(key: key);
#override
State<TabOne> createState() => _TabOneState();
}
class _TabOneState extends State<TabOne> {
#override
Widget build(BuildContext context) {
return DataLoaderUI<User>(
valueNotifier: UserData.userData,
future: UserData.loadUserData(),
child: Text(UserData.userData.value.data!.name??'No name'));
}
}
The error is in this line:
Text(UserData.userData.value.data!.name??'No name'));
Null check operator used on a null value
Since I am passing the Text widget as an argument with the data inside it. Flutter is trying to pass it but not able to since there is no data yet so its accessing null values. I tried with a normal string and it works perfectly. I looked at the FutureBuilder widget and they use a kind of builder and also the ValueLisnableBuilder has a builder as an arguement. The problem is that I am not capable of creating something like it for my custom solution. How can I just pass the child that I want without having such an error and without moving the ValueLisnable widget into my direct UI widget?
I have found the solution.
Modify the DataLoaderUI class to this:
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:testing/utils/dataLoader/data_loader.dart';
class DataLoaderUI<T> extends StatefulWidget {
final ValueNotifier<DataLoader<T>> valueNotifier;
final Widget noneStatusUI;
final Widget hasErrorUI;
final Widget loadingUI;
final Widget Function(T? snapshotData) child;
final Future<T> future;
const DataLoaderUI(
{Key? key,
required this.valueNotifier,
this.noneStatusUI = const Text('Data initialization has not started'),
this.hasErrorUI = const Center(child: Text('Unable to fetch data')),
this.loadingUI = const Center(
child: CircularProgressIndicator(),
),
required this.child,
required this.future})
: super(key: key);
#override
State<DataLoaderUI<T>> createState() => _DataLoaderUIState<T>();
}
class _DataLoaderUIState<T> extends State<DataLoaderUI<T>> {
Future startLoading() async {
widget.valueNotifier.value.status = Status.loading;
widget.valueNotifier.notifyListeners();
try {
var data = await widget.future;
widget.valueNotifier.value.data = data;
widget.valueNotifier.value.status = Status.loaded;
widget.valueNotifier.notifyListeners();
} catch (e) {
log('future error', error: e.toString());
widget.valueNotifier.value.error = e;
widget.valueNotifier.value.status = Status.hasError;
widget.valueNotifier.notifyListeners();
}
}
#override
void initState() {
super.initState();
log('init state launched');
if (!widget.valueNotifier.value.hasData) {
log('reloading or first loading');
startLoading();
}
}
//AsyncSnapshot asyncSnapshot;
#override
Widget build(BuildContext context) {
return ValueListenableBuilder<DataLoader<T>>(
valueListenable: widget.valueNotifier,
builder: (context, dataLoader, ui) {
if (dataLoader.status == Status.none) {
return widget.noneStatusUI;
} else if (dataLoader.status == Status.hasError) {
return widget.hasErrorUI;
} else if (dataLoader.status == Status.loading) {
return widget.loadingUI;
} else {
return widget.child(dataLoader.data);
}
});
}
}
and use it like this:
DataLoaderUI<User>(
valueNotifier: UserData.userData,
future: UserData.loadUserData(),
child: (user) {
return Text(user!.name ?? 'kk');
});
Take a look at my version of the same sort of state management approach here: https://github.com/lukehutch/flutter_reactive_widget

Future Provider Stuck In loading state

I am using a future provider to display a login page on load and then a loading indicator on loading. Here is my future provider
final loginProvider = FutureProvider.family((ref, UserInput input) =>
ref.read(authRepositoryProvider).doLogin(input.email, input.password));
In my UI I have this....
class LoginScreen extends HookWidget {
final TextEditingController emailEditingController = TextEditingController();
final TextEditingController passwordEditingController =
TextEditingController();
#override
Widget build(BuildContext context) {
var userInput =
UserInput(emailEditingController.text, passwordEditingController.text);
final login = useProvider(loginProvider(userInput));
return login.when(
data: (user) => Login(emailEditingController, passwordEditingController),
loading: () => const ProgressIndication(),
error: (error, stack) {
if (error is DioError) {
return Login(emailEditingController, passwordEditingController);
} else {
return Login(emailEditingController, passwordEditingController);
}
},
);
}
}
here is my doLogin function.
#override
Future<dynamic> doLogin(String email, String password) async {
try {
final response = await _read(dioProvider)
.post('$baseUrl/login', data: {'email': email, 'password': password});
final data = Map<String, dynamic>.from(response.data);
return data;
} on DioError catch (e) {
return BadRequestException(e.error);
} on SocketException {
return 'No Internet Connection';
}
}
I would like to know why it's stuck in the loading state. Any help will be appreciated.
First off, family creates a new instance of the provider when given input. So in your implementation, any time your text fields change, you're generating a new provider and watching that new provider. This is bad.
In your case, keeping the UserInput around for the sake of accessing the login state doesn't make a lot of sense. That is to say, in this instance, a FamilyProvider isn't ideal.
The following is an example of how you could choose to write it. This is not the only way you could write it. It is probably easier to grasp than streaming without an API like Firebase that handles most of that for you.
First, a StateNotifierProvider:
enum LoginState { loggedOut, loading, loggedIn, error }
class LoginStateNotifier extends StateNotifier<LoginState> {
LoginStateNotifier(this._read) : super(LoginState.loggedOut);
final Reader _read;
late final Map<String, dynamic> _user;
static final provider =
StateNotifierProvider<LoginStateNotifier, LoginState>((ref) => LoginStateNotifier(ref.read));
Future<void> login(String email, String password) async {
state = LoginState.loading;
try {
_user = await _read(authRepositoryProvider).doLogin(email, password);
state = LoginState.loggedIn;
} catch (e) {
state = LoginState.error;
}
}
Map<String, dynamic> get user => _user;
}
This allows us to have manual control over the state of the login process. It's not the most elegant, but practically, it works.
Next, a login screen. This is as barebones as they get. Ignore the error parameter for now - it will be cleared up in a moment.
class LoginScreen extends HookWidget {
const LoginScreen({Key? key, this.error = false}) : super(key: key);
final bool error;
#override
Widget build(BuildContext context) {
final emailController = useTextEditingController();
final passwordController = useTextEditingController();
return Column(
children: [
TextField(
controller: emailController,
),
TextField(
controller: passwordController,
),
ElevatedButton(
onPressed: () async {
await context.read(LoginStateNotifier.provider.notifier).login(
emailController.text,
passwordController.text,
);
},
child: Text('Login'),
),
if (error) Text('Error signing in'),
],
);
}
}
You'll notice we can use the useTextEditingController hook which will handle disposing of those, as well. You can also see the call to login through the StateNotifier.
Last but not least, we need to do something with our fancy new state.
class AuthPage extends HookWidget {
const AuthPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final loginState = useProvider(LoginStateNotifier.provider);
switch (loginState) {
case LoginState.loggedOut:
return LoginScreen();
case LoginState.loading:
return LoadingPage();
case LoginState.loggedIn:
return HomePage();
case LoginState.error:
return LoginScreen(error: true);
}
}
}
In practice, you're going to want to wrap this in another widget with a Scaffold.
I know this isn't exactly what you asked, but thought it might be helpful to see another approach to the problem.

How can I recall a futureprovider (riverpod) on build

I have a simple page, which shows a product, and i am trying to use riverpod, and futureprovider, but the future is only called the first time i go to the page?
final productInfo = FutureProvider<Map<String, dynamic>>((ref) async {
final response =
await http.get('https://www.api.com/$clickedID');
final content = json.decode(response.body) as Map<String, dynamic>;
return content;
});
With the recent Riverpod, you can do ref.refresh(userProvider) to refresh the provider.
Please use
final userProvider = StreamProvider<User>(
final response =
await http.get('https://www.api.com/$clickedID');
final content = json.decode(response.body) as Map<String, dynamic>;
return content;
);
now you can listen to the provider using watch,
AsyncValue<User> user = watch(userProvider);
No inbuilt functionality, but you can override the value with the same future at a later time.
recallProductInfoFutureProvider() {
final response = http.get('https://www.api.com/$clickedID');
final content = json.decode(response.body) as Map<String, dynamic>;
productInfo.overrideWithValue(content)
}
Consider using with StreamProvider and watching inside Consumer to update UI on overrideWithValue.
There are a two options.
ref.refresh(provider)
ref.invalidate(provider)
Note that if you are using a AsyncValue, the old result will be returned when you refresh the FutureProvider and AsyncValue.isRefreshing will be set to true:
final notificationRepositoryProvider = FutureProvider<bool?>((ref) async {
Future<bool> approveDocument() => Future.delayed(Duration(seconds: 1), () => Future.value(Random().nextBool()));
return approveDocument();
});
class HomeView extends ConsumerStatefulWidget {
const HomeView({Key? key}) : super(key: key);
#override
HomeViewState createState() => HomeViewState();
}
class HomeViewState extends ConsumerState<HomeView> {
#override
Widget build(BuildContext context) {
AsyncValue<bool?> rejectResponse = ref.watch(notificationRepositoryProvider);
return ElevatedButton(
onPressed: () {
// ref.refresh(notificationRepositoryProvider);
ref.watch(notificationRepositoryProvider).;
},
child: rejectResponse.when(
loading: () => const CircularProgressIndicator(
color: Colors.white,
),
skipLoadingOnRefresh: false,
error: (err, stack) => Text('Error'),
data: (data) => Text('Yes: $data'),
));
}
}

How to access data in Bloc's state from another bloc

I am developing a Flutter application using Bloc pattern. After success authentication, UserSate has User object. In all other Blocs, I need to access User object in UserState. I tried with getting UserBloc on other Bloc's constructor parameters and accessing User object. But it shows that User object is null. Anyone have a better solution?
class SectorHomeBloc extends Bloc<SectorHomeEvent, SectorHomeState> {
final OutletRepository outletRepository;
UserBloc userBloc;
final ProductRepository productRepository;
final ProductSubCategoryRepository productSubCategoryRepository;
final PromotionRepository promotionRepository;
final ProductMainCategoryRepository mainCategoryRepository;
SectorHomeBloc({
#required this.outletRepository,
#required this.userBloc,
#required this.productSubCategoryRepository,
#required this.productRepository,
#required this.promotionRepository,
#required this.mainCategoryRepository,
});
#override
SectorHomeState get initialState => SectorHomeLoadingState();
#override
Stream<SectorHomeState> mapEventToState(SectorHomeEvent event) async* {
try {
print(userBloc.state.toString());
LatLng _location = LatLng(
userBloc.state.user.defaultLocation.coordinate.latitude,
userBloc.state.user.defaultLocation.coordinate.longitude);
String _token = userBloc.state.user.token;
if (event is GetAllDataEvent) {
yield SectorHomeLoadingState();
List<Outlet> _previousOrderedOutlets =
await outletRepository.getPreviousOrderedOutlets(
_token, _location, event.orderType, event.sectorId);
List<Outlet> _featuredOutlets =
await outletRepository.getFeaturedOutlets(
_token, _location, event.orderType, event.sectorId);
List<Outlet> _nearestOutlets = await outletRepository.getOutletsNearYou(
_token, _location, event.orderType, event.sectorId);
List<Product> _newProducts = await productRepository.getNewItems(
_token, _location, event.orderType, event.sectorId);
List<Product> _trendingProducts =
await productRepository.getTrendingItems(
_token, _location, event.orderType, event.sectorId);
List<Promotion> _promotions = await promotionRepository
.getVendorPromotions(_token, event.sectorId);
yield SectorHomeState(
previousOrderedOutlets: _previousOrderedOutlets,
featuredOutlets: _featuredOutlets,
nearByOutlets: _nearestOutlets,
newItems: _newProducts,
trendingItems: _trendingProducts,
promotions: _promotions,
);
}
} on SocketException {
yield SectorHomeLoadingErrorState('could not connect to server');
} catch (e) {
print(e);
yield SectorHomeLoadingErrorState('Error');
}
}
}
The print statement [print(userBloc.state.toString());] in mapEventToState method shows the initial state of UserSate.
But, at the time of this code executing UserState is in UserLoggedInState.
UPDATE (Best Practice):
please refer to the answer here enter link description here
so the best way for that is to hear the changes of another bloc inside the widget you are in, and fire the event based on that.
so what you will do is wrap your widget in a bloc listener and listen to the bloc you want.
class SecondPage extends StatelessWidget {
const SecondPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return BlocListener<FirstBloc, FirstBlocState>(
listener: (context, state) {
if(state is StateFromFirstBloc){
BlocProvider.of<SecondBloc>(context).add(SecondBlocEvent());}//or whatever you want
},
child: ElevatedButton(
child: Text('THIS IS NEW SCREEN'),
onPressed: () {
BlocProvider.of<SecondBloC>(context).add(SecondBloCEvent());
},
),
);
}
}
the lovely thing about listener is that you can listen anywhere to any bloc and do whatever you want
here is the official documentation for it
OLD WAY (NOT Recommended)
there is an official way to do this as in the documentation, called Bloc-to-Bloc Communication
and here is the example for this as in the documentation
class MyBloc extends Bloc {
final OtherBloc otherBloc;
StreamSubscription otherBlocSubscription;
MyBloc(this.otherBloc) {
otherBlocSubscription = otherBloc.listen((state) {
// React to state changes here.
// Add events here to trigger changes in MyBloc.
});
}
#override
Future<void> close() {
otherBlocSubscription.cancel();
return super.close();
}
}
sorry for the late update for this answer and thanks to #MJ studio
The accepted answer actually has a comment in the above example in the official docs saying "No matter how much you are tempted to do this, you should not do this! Keep reading for better alternatives!"!!!
Here's the official doc link, ultimately one bloc should not know about any other blocs, add methods to update your bloc and these can be triggered from blocListeners which listen to changes in your other blocs: https://bloclibrary.dev/#/architecture?id=connecting-blocs-through-domain
class MyWidget extends StatelessWidget {
const MyWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return BlocListener<WeatherCubit, WeatherState>(
listener: (context, state) {
// When the first bloc's state changes, this will be called.
//
// Now we can add an event to the second bloc without it having
// to know about the first bloc.
BlocProvider.of<SecondBloc>(context).add(SecondBlocEvent());
},
child: TextButton(
child: const Text('Hello'),
onPressed: () {
BlocProvider.of<FirstBloc>(context).add(FirstBlocEvent());
},
),
);
}
}