I put together a sample app that implements an username and password field using streams with verification using validation transforms. I used RxDart for this and I hooked up the "Login" button to enable/disable based on 2 streams results being true.
bloc.dart
Stream<bool> get submitValidWithCombineLatestStream =>
CombineLatestStream([email, password], (list) => true);
login_page.dart
Widget submitButton(Bloc bloc) {
return StreamBuilder(
stream: bloc.submitValidWithCombineLatestStream,
builder: (_, snapshot) {
return RaisedButton(
color: Colors.deepPurpleAccent,
child: Text(
'Submit',
style: TextStyle(color: Colors.white),
),
onPressed: !snapshot.hasData ? null : bloc.submit,
);
},
);
}
I have coded the same sample app using flutter_bloc but I'm trying to figure out how to enable/disable the "Login" button using whatever bloc has for CombineLatestStream. Does anyone know how to do this?
I realize that this is total overkill but I'm trying to figure this out using this simple example and I'm not sure how to access all the cool RxDart functionality once you convert to bloc (flutter_bloc). I'd like to have the best of both worlds WITHOUT importing/using RxDart.
Is that possible?
An example of what I’m looking for would be:
I’m building a form that requires 3 different backend calls. I’d prefer to show the progress indicator while all 3 calls are working and block until all 3 calls return. I’d like to do that via CombineLatestStreams since all 3 return Steam Futures.
Problem in your main bloc class,check out this github code,
https://github.com/hoc081098/flutter_validation_login_form_BLoC_pattern_RxDart
In short like this,you can use CombineLatestStream
you can set in your main bloc class
like this,
return LoginBloc._(
emailChanged: emailS.add,
passwordChanged: passwordS.add,
submitLogin: () => submitLoginS.add(null),
emailError$: emailError$,
passwordError$: passwordError$,
isLoading$: isLoadingS.stream,
message$: message$,
dispose: DisposeBag([...subjects, message$.connect()]).dispose,
);
This is how you combine streams with bloc using RxDart, you have emit new values if there is something new in listener, that's all.
You might create listener whenever you want to do calls to your backend.
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:rxdart/rxdart.dart';
class TestState {
final bool isValid;
TestState(this.isValid);
}
class TestCubit extends Cubit<TestState> {
final List<StreamSubscription> _ss = [];
final _emailSubject = BehaviorSubject<String>.seeded(null);
final _passwordSubject = BehaviorSubject<String>.seeded(null);
TestCubit() : super(TestState(false)) {
_ss.add(_subscribeToEmailAndPassword());
}
StreamSubscription _subscribeToEmailAndPassword() {
return Rx.combineLatest([_emailSubject, _passwordSubject], (list) {
if (list[0] != null && list[1] != null) {
return true;
}
return false;
}).listen((value) {
emit(TestState(value));
});
}
#override
Future<void> close() async {
_ss.forEach((e) async => await e.cancel());
await super.close();
}
void emailFieldChange(String email) {
_emailSubject.add(email);
}
void passwordFieldChange(String password) {
_passwordSubject.add(password);
}
}
Related
import 'dart:convert';
import 'package:get/get.dart';
import '../../../functions/functions.dart';
import '../models/user_details_modal.dart';
class UserListController extends GetxController {
var data = [];
List<UserDetails> userDetails = <UserDetails>[].obs;
// var userDetails = <UserDetails>[].obs;
// RxList<UserDetails> userDetails = <UserDetails>[].obs;
// RxList<UserDetails> userDetails = RxList();
Future<void> fetchData() async {
String apipage = "getData.php";
Map mappeddata = {};
final response = await sendServerData(apipage, mappeddata);
if (response.statusCode == 200) {
final responseBody = jsonDecode(response.body.toString());
userDetails = userDetailsFromJson(json.encode(responseBody));
print(userDetails);
update();
} else {
Get.snackbar(
'Server Connection Failed', 'Please try again after some time');
}
}
#override
void onInit() {
fetchData();
super.onInit();
}
}
Above code is my controller,
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/user_list_controller.dart';
import '../models/user_details_modal.dart';
class UserListView extends GetView<UserListController> {
#override
final UserListController controller = Get.put(UserListController());
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('ListView'),
centerTitle: true,
),
body: Obx(
() => controller.userDetails.isEmpty
? Center(
child: Text("Loading data..."),
)
: ListView.builder(
itemCount: controller.userDetails.length,
itemBuilder: (context, index) {
final UserDetails item = controller.userDetails[index];
return buildListTile(item);
},
),
),
);
}
Widget buildListTile(UserDetails item) {
print("userName: ${item.userName}");
print("userEmail: ${item.userEmail}");
return ListTile(
title: Text(item.userName),
subtitle: Text(item.userEmail),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () {},
),
);
}
}
This is my view,but it's throwing this error -
"[Get] the improper use of a GetX has been detected.
You should only use GetX or Obx for the specific widget that will be updated.
If you are seeing this error, you probably did not insert any observable variables into GetX/Obx
or insert them outside the scope that GetX considers suitable for an update
(example: GetX => HeavyWidget => variableObservable).
If you need to update a parent widget and a child widget, wrap each one in an Obx/GetX."
i'm a beginner in flutter,so please help me to find the issue here.
I have updated the code,i made some changes in controller part declaring that List,now that prev issue is solved!
But the problem is no data showing up on first load (using Get.to() to navigate to this from another page) but when i hot reload/reload this page it shows those data.
What's the problem?
if you want to use reactive approach using Obx() or Getx(), you need to make your variables observable. You can do this simply by adding .obs to the variable. This makes your variables as Rx type.
There are other ways to make your variables observable, but this is the simplest one.
in the controller:
var data = [].obs;
List<UserDetails> userDetails = [].obs;
Also, you don't need to use update() for this approach.
And whenever you want to change the value or use it in your view
you should access the value like this.
in the view inside the Obx: ( in the code above, no need to use .value for the list)
E.x)
bool isLoading = controller.loading.value
Hope this helped :)
I am using stream builder for fetching api in my app, what I am trying to achieve is to get notification or print message in the console every time the stream gives a new entry. I have heard there are packages for notifications but I don't know where exactly to write function that notifies me about new entry. the below is my code. please help.
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
void main() async {
runApp(MaterialApp(home: PeriodicRequester(),));
}
class PeriodicRequester extends StatelessWidget {
Stream<http.Response> getRandomNumberFact() async* {
yield* Stream.periodic(Duration(seconds: 5), (_) {
return http.get(Uri.parse("https://script.google.com/macros/s/AKfycbwhbpF4ZxuMUcTZZvObAqvE1pAbEfPt7gZHRV1vVp8PuKt39-ouOm-kQJ1U1LtlEwV-/exec"));
}).asyncMap((event) async => await event);
}
#override
Widget build(BuildContext context) {
return StreamBuilder<http.Response>(
stream: getRandomNumberFact(),
builder: (context, snapshot) => snapshot.hasData
? Center(child: Text(snapshot.data!.body))
: CircularProgressIndicator(),
);
}
}
It depends on the data you are recieving. A stream builder may rebuild on multiple occasions including on network state change. so you can't add it directly to the build method. For example if its returning a list of items, then you can create a variable and update this variable with the length of the response. for example if the length is 5 in the first response. Update the variable to 5 and on next instance from stream you may get a 6 then check if the existing value is lesser than the current length. Then perform your custom action here (print or notification) and update the length.
I have app where I am using Bloc and Hive.
main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final appDocumentDirectory =
await path_provider.getApplicationDocumentsDirectory();
Hive.init(appDocumentDirectory.path);
runApp(
const MyApp(),
);
}
On MyApp widget registered MultiRepositoryProvider
return MultiRepositoryProvider(
providers: [
RepositoryProvider(create: (context) => AccountService()),
],
child: MultiBlocProvider(
providers: [
BlocProvider<AccountBloc>(
create: (context) => AccountBloc(context.read<AccountService>()),
),
],
child: MaterialApp(
home: const AppPage(),
),
),
);
AppPage Contains bottomNavigationBar and some pages
account.dart
class AccountService {
late Box<Account> _accounts;
AccountService() {
init();
}
Future<void> init() async {
Hive.registerAdapter(AccountAdapter());
_accounts = await Hive.openBox<Account>('accounts');
}
On appPage have BlocBuilder
BlocBuilder<AccountBloc, AccountState>(
builder: (context, state) {
if (state.accountStatus == AccountStatus.loading) {
return const CircularProgressIndicator();
} else if (state.accountStatus == AccountStatus.error) {
Future.delayed(Duration.zero, () {
errorDialog(context, state.error);
});
}
return SingleChildScrollView(....
When app first loaded I receive LateInitializationError that late Box <Account> _accounts from account Repository not initialized. But as soon as I navigate to another page and go back, the Box <Account> _accounts are initialized and the data appears.
How can I avoid this error and initialize the Hive box on application load?
Can you try this? I think you need to await Hive init function
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final appDocumentDirectory =
await path_provider.getApplicationDocumentsDirectory();
await Hive.init(appDocumentDirectory.path);
runApp(
const MyApp(),
);
}
It's been like 7 months, but if you are still looking for an answer, not sure if it's optimal but below should work.
My understanding on the issue you are having is that the reason why there is that "LateInitializationError" is because that your init function call in your constructor is asynchronously invoked without await for its result. As a result, there is a possibility that when you are calling functions on the box, the initialisation is not yet finished. When you navigate to another page and go back, the function init run happened to be finished. Hence, the error is gone. The complexity here is that constructor can not be marked as async for you to use that await keyword. Since you are using bloc, one possible workaround is to call the init function of your repo when bloc is in init state.
For demo purpose I defined below bloc states and events,
you can absolutely change them based on your needs.
// bloc states
abstract class AccountState{}
class InitState extends AccountState{}
class LoadedState extends AccountState{
LoadedState(this.accounts);
final List<Account> accounts;
}
class LoadingErrorState extends AccountState{}
//bloc events
abstract class AccountEvent {}
class InitEvent extends AccountEvent {}
... // other events
in your bloc logic you can call the init function from you repo on InitEvent
class AccountBloc extends Bloc<AccountEvent, AccountState> {
AccountBloc(this.repo) : super(InitState()) {
on<InitEvent>((event, emit) async {
await repo.init();
emit(LoadedState(account: repo.getAccounts()));
});
...// define handlers for other events
}
final AccountRepository repo;
}
in your service class you can remove the init from the constructor like:
class AccountService {
late Box<Account> _accounts;
AccountService();
Future<void> init() async {
Hive.registerAdapter(AccountAdapter());
_accounts = await Hive.openBox<Account>('accounts');
}
List<Account> getAccounts(){
return _accounts.values.toList();
}
}
Then in your bloc builder, you can add init event to your bloc when the state is InitState as below:
BlocBuilder<AccountBloc, AccountState>(
builder: (context, state) {
if (state is InitState) {
context.read<AccountBloc>.add(InitEvent());
return const CircularProgressIndicator();
} else if (state is LoadingErrorState) {
Future.delayed(Duration.zero, () {
errorDialog(context, state.error);
});
}
else if (state is LoadedState){
return SingleChildScrollView(....
}
Also, FYI, you can if you want the init to be called when the object of your account service is instantiated, you can take a look at below answer:
https://stackoverflow.com/a/59304510/16584569
However, you still going to need to await for the initialisation of your service. One possible way is just do it in your main function and pass down to your app, but it makes the structure of your code messy and when you want to swap to another repo, you need to remember to change code in main function as well.
I'm using flutter_redux and google_sign_in and I want to route from the Login page to a different page after logging in.
I am handling the API call to Google using middleware that dispatches a LoggedInSuccessfully action. I know I can use Navigator.pushNamed(context, "/routeName") for the actual routing, but I am new to both Flutter and Redux and my problem is that I just don't know where to call this.
The code below is for the GoogleAuthButtonContainer, which is my guess as to where the routing should be. The GoogleAuthButton is just a plain widget with the actual button and layout.
Any help appreciated, thanks!
#override
Widget build(BuildContext context) {
return new StoreConnector<AppState, _ViewModel>(
converter: _ViewModel.fromStore,
builder: (BuildContext context, _ViewModel vm) {
return new GoogleAuthButton(
buttonText: vm.buttonText,
onPressedCallback: vm.onPressedCallback,
);
},
);
}
}
class _ViewModel {
final String buttonText;
final Function onPressedCallback;
_ViewModel({this.onPressedCallback, this.buttonText});
static _ViewModel fromStore(Store<AppState> store) {
return new _ViewModel(
buttonText:
store.state.currentUser != null ? 'Log Out' : 'Log in with Google',
onPressedCallback: () {
if (store.state.currentUser != null) {
store.dispatch(new LogOut());
} else {
store.dispatch(new LogIn());
}
});
}
}
You can achieve this by 3 different ways:
Call the navigator directly (without using the ViewModel), but this is not a clean solution.
Set the navigatorKey and use it in a Middleware as described here
And another solution is what I've explained here which is passing the Navigator to the Action class and use it in the Middleware
So to sum up, you probably want to use a Middleware to do the navigation.
I'm new to the flutter world and mobile app development and struggling with how I should pass user data throughout my app.
I've tried several things, but none seem great and I'm sure there are best practice patterns I should be following.
Because it makes examples easier, I'm using firebase for authentication.
I currently have a separate route for logging in. Once I'm logged in I want the User model in most views for checking permissions on what to show, displaying user info in the drawer, etc...
Firebase has an await firebaseAuth.currentUser(); Is it best practice to call this everywhere you might need the user? and if so, where is the best spot to place this call?
The flutter codelab shows a great example of authenticating users before allowing writes. However, if the page needs to check auth to determine what to build, the async call can't go in the build method.
initState
One method I've tried is to override initState and kick off the call to get the user. When the future completes I call setState and update the user.
FirebaseUser user;
#override
void initState() {
super.initState();
_getUserDetail();
}
Future<Null> _getUserDetail() async {
User currentUser = await firebaseAuth.currentUser();
setState(() => user = currentUser);
}
This works decent but seems like a lot of ceremony for each widget that needs it. There is also a flash when the screen loads without the user and then gets updated with the user upon the future's completion.
Pass the user through the constructor
This works too but is a lot of boilerplate to pass the user through all routes, views, and states that might need to access them. Also, we can't just do popAndPushNamed when transitioning routes because we can't pass a variable to it. We have to change routes similar to this:
Navigator.push(context, new MaterialPageRoute(
builder: (BuildContext context) => new MyPage(user),
));
Inherited Widgets
https://medium.com/#mehmetf_71205/inheriting-widgets-b7ac56dbbeb1
This article showed a nice pattern for using InheritedWidget. When I place the inherited widget at the MaterialApp level, the children aren't updating when the auth state changed (I'm sure I'm doing it wrong)
FirebaseUser user;
Future<Null> didChangeDependency() async {
super.didChangeDependencies();
User currentUser = await firebaseAuth.currentUser();
setState(() => user = currentUser);
}
#override
Widget build(BuildContext context) {
return new UserContext(
user,
child: new MaterialApp(
title: 'TC Stream',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new LoginView(title: 'TC Stream Login', analytics: analytics),
routes: routes,
),
);
}
FutureBuilder
FutureBuilder also seems like a decent option but seems to be a lot of work for each route. In the partial example below, _authenticateUser() is getting the user and setting state upon completion.
#override
Widget build(BuildContext context) {
return new FutureBuilder<FirebaseUser>(
future: _authenticateUser(),
builder: (BuildContext context, AsyncSnapshot<FirebaseUser> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return _buildProgressIndicator();
}
if (snapshot.connectionState == ConnectionState.done) {
return _buildPage();
}
},
);
}
I'd appreciate any advice on best practice patterns or links to resources to use for examples.
I'd recommend investigating inherited widgets further; the code below shows how to use them with asynchronously updating data:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(new MaterialApp(
title: 'Inherited Widgets Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new Scaffold(
appBar: new AppBar(
title: new Text('Inherited Widget Example'),
),
body: new NamePage())));
}
// Inherited widget for managing a name
class NameInheritedWidget extends InheritedWidget {
const NameInheritedWidget({
Key key,
this.name,
Widget child}) : super(key: key, child: child);
final String name;
#override
bool updateShouldNotify(NameInheritedWidget old) {
print('In updateShouldNotify');
return name != old.name;
}
static NameInheritedWidget of(BuildContext context) {
// You could also just directly return the name here
// as there's only one field
return context.inheritFromWidgetOfExactType(NameInheritedWidget);
}
}
// Stateful widget for managing name data
class NamePage extends StatefulWidget {
#override
_NamePageState createState() => new _NamePageState();
}
// State for managing fetching name data over HTTP
class _NamePageState extends State<NamePage> {
String name = 'Placeholder';
// Fetch a name asynchonously over HTTP
_get() async {
var res = await http.get('https://jsonplaceholder.typicode.com/users');
var name = json.decode(res.body)[0]['name'];
setState(() => this.name = name);
}
#override
void initState() {
super.initState();
_get();
}
#override
Widget build(BuildContext context) {
return new NameInheritedWidget(
name: name,
child: const IntermediateWidget()
);
}
}
// Intermediate widget to show how inherited widgets
// can propagate changes down the widget tree
class IntermediateWidget extends StatelessWidget {
// Using a const constructor makes the widget cacheable
const IntermediateWidget();
#override
Widget build(BuildContext context) {
return new Center(
child: new Padding(
padding: new EdgeInsets.all(10.0),
child: const NameWidget()));
}
}
class NameWidget extends StatelessWidget {
const NameWidget();
#override
Widget build(BuildContext context) {
final inheritedWidget = NameInheritedWidget.of(context);
return new Text(
inheritedWidget.name,
style: Theme.of(context).textTheme.display1,
);
}
}
I prefer to use Services with Locator, using Flutter get_it.
Create a UserService with a cached data if you like:
class UserService {
final Firestore _db = Firestore.instance;
final String _collectionName = 'users';
CollectionReference _ref;
User _cachedUser; //<----- Cached Here
UserService() {
this._ref = _db.collection(_collectionName);
}
User getCachedUser() {
return _cachedUser;
}
Future<User> getUser(String id) async {
DocumentSnapshot doc = await _ref.document(id).get();
if (!doc.exists) {
log("UserService.getUser(): Empty companyID ($id)");
return null;
}
_cachedUser = User.fromDocument(doc.data, doc.documentID);
return _cachedUser;
}
}
Then create create a Locator
GetIt locator = GetIt.instance;
void setupLocator() {
locator.registerLazySingleton(() => new UserService());
}
And instantiate in main()
void main() {
setupLocator();
new Routes();
}
That's it! You can call your Service + cachedData everywhere using:
.....
UserService _userService = locator<UserService>();
#override
void initState() {
super.initState();
_user = _userService.getCachedUser();
}
I crashed into another problem because of this problem you can check it out here
So the solution I came up with is a bit untidy,I created a separate Instance dart page and imported it to every page.
GoogleSignInAccount Guser = googleSignIn.currentUser;
FirebaseUser Fuser;
I stored the user there on login and checked on every StateWidget if it was null
Future<Null> _ensureLoggedIn() async {
if (Guser == null) Guser = await googleSignIn.signInSilently();
if (Fuser == null) {
await googleSignIn.signIn();
analytics.logLogin();
}
if (await auth.currentUser() == null) {
GoogleSignInAuthentication credentials =
await googleSignIn.currentUser.authentication;
await auth.signInWithGoogle(
idToken: credentials.idToken,
accessToken: credentials.accessToken,
);
}
This is my old code I did cleaned it up on my current app but I don't have that code now in handy. Just check out for null user and log it in again
I did it for most of the Firebase instances too because I have more than 3 pages on my app and Inherited Widgets was just too much work
You can use the GetX package to check whether or not the user is logged in, get user data and have it accessible throughout your app
For my lazy mathod,
i just create new file like userdata.dart and then put any variable on it for example like dynamic Profile = null
inside userdata.dart
//only put this or anything u want.
dynamic Profile = null;
at startingpage.dart
//import that file
import '../userdata.dart';
class startingpage extends ...{
...
//set data to store..
Profile = 'user profile';
...
}
to use the data just declare and use in
anotherpage.dart
//import that file
import '../userdata.dart';
class anotherpage extends...{
...
}
class .. State ...{
...
//set the data to variable
dynamic userdata = Profile;
print('this is my lazy pass data' + userdata.toString());
...
}