I am using the flutter_riverpod package. I'm using it for a search field and it's showing an error when searching for something. I asked a question about the error but couldn't fix it. Is there any other way to create a search field in Flutter using the flutter_riverpod package?
Here is some code:
User interface code:
TextFormField(
…
onChanged: (search) => controller.updateSearch(search),
onSaved: (search) {
search == null ? null : controller.updateSearch(search);
},
),
itemBuilder: (context, index) {
if (mounted) {
return name.contains(state.search) ? ListTile(title: Text(name)) : Container();
}
return Container();
},
Controller code:
class Controller extends StateNotifier<State> {
Controller() : super(State());
void updateSearch(String search) => state = state.copyWith(search: search);
}
final controllerProvider = StateNotifierProvider.autoDispose< Controller, State>((ref) {
return Controller();
});
State code:
class State {
State({this.search = "", this.value = const AsyncValue.data(null)});
final String search;
final AsyncValue<void> value;
bool get isLoading => value.isLoading;
State copyWith({String? search, AsyncValue<void>? value}) {
return State(search: search ?? this.search, value: value ?? this.value);
}
}
Is there something wrong with the code above? If yes, what is the way to create the seach field using the flutter_riverpod package? If not, why am I getting the error message (go to this question to see the error message)?
If I can't create a search field using the flutter_riverpod package in Flutter, how can I create a search field (I hope maybe without using any package and without using setState function)?
Feel free to comment if you need more information!
How to fix this error? I would appreciate any help. Thank you in advance!
Update:
We can discuss in this room.
this may not answer directly the question but it may give another variation on how to restructure the code.
To track the loading state, from the controller:
class Controller extends StateNotifier<AsyncValue<void>> {
Controller({required this.repository}) : super(AsyncData(null));
//this depends on where your search api is located at.
//for this example i use repository because it is mostly where its is.
final SeachRepository repository
Future<void> submitSearch(String search) async {
state = const AsyncLoading();
//`.guard` is used to handle possible errors from your API so no need
// to use `try/catch`
state = await AsyncValue.guard(()=> repository.submitSearch(search));
}
}
final controllerProvider = StateNotifierProvider.autoDispose< Controller, AsyncValue<void>>((ref) {
return Controller();
});
You can use your controller from your UI like this:
final controller = ref.watch(controllerProvider.notifier);
TextFormField(
…
onChanged: (search) => ref.read(searchValueProvider.notifier).state = search,
onSaved: (search) {
search == null ? null : controller.submitSearch(search);
},
),
And for the loading state:
//your loading state
final state = ref.watch(controllerProvider);
//to handle passible error:
ref.listen<AsyncValue>(controllerProvider, (_, state)=> showAlertDialog(context));
itemBuilder: (context, index) {
if (mounted) {
return name.contains(state.search) ? ListTile(title: Text(name)) : Container();
}
return state.isLoading ? CircularProgressIndicator() : Container();
},
for the search variable, you can store it in a StateProvider for easy access:
final searchValueProvider = StateProvider.autodispose((ref)=> '');
Try the following code:
class FooSearchRiverpod extends ConsumerWidget {
final controller = StreamController<String>();
late final provider = obtainProvider(controller);
#override
Widget build(BuildContext context, WidgetRef ref) {
AsyncValue<List<String>> list = ref.watch(provider);
return Column(
children: [
TextField(
onChanged: controller.add,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.search), hintText: 'search...'),
),
Expanded(
child: list.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, stack) => Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Card(elevation: 4, child: Text(err.toString())),
),
),
data: (list) => AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
child: ListView.builder(
itemCount: list.length,
itemBuilder: (ctx, i) => ListTile(
key: UniqueKey(),
title: Text(list[i]),
onTap: () => debugPrint('onTtap: ${list[i]}'),
),
),
),
),
),
],
);
}
obtainProvider(StreamController<String> controller) {
return StreamProvider.autoDispose<List<String>>((ref) => controller.stream
.debounce(const Duration(milliseconds: 1000))
.asyncMap(_getAPI));
}
final list = ['foo', 'foo 2', 'bar', 'bar 2', 'spam'];
Future<List<String>> _getAPI(String query) async {
debugPrint('_getAPI: |$query|');
if (query.isEmpty) return [];
return list.where((s) => s.contains(query)).toList();
}
}
Related
I am a little bit confused on why a GraphQL query returning a list of items does not get updated. Say I have a list of three items, then I add another item and rebuild the widget. The widget still only shows the first three items, even though I can see that new item has been created in the database. I am unsure if this is a cache problem. I have tried to fetch data from the network only, but that does not work either.
The client used in the GraphQLProvider is instantiated like this:
Future<ValueNotifier<GraphQLClient>> _getClient() async {
final HttpLink httpLink = HttpLink(
Constants.apiURL,
defaultHeaders: {
'X-Parse-Application-Id': Constants.kParseApplicationId,
'X-Parse-Client-Key': Constants.kParseClientKey,
},
);
// initialize Hive and wrap the default box in a HiveStore
Directory directory = await pathProvider.getApplicationDocumentsDirectory();
final store = await HiveStore.open(path: directory.path);
return ValueNotifier(
GraphQLClient(
cache: GraphQLCache(store: store),
link: httpLink,
),
);
}
And the page looks like this. When a new forum post is created, setstate() is called and the widget rebuilds. However, the line List<dynamic> forumEntries = result.data?["getForumEntries"]; still returns the old list of data without the new entry. I have the same problem in a few other places as well.
class FeedWidget extends StatefulWidget {
const FeedWidget({Key? key}) : super(key: key);
#override
State<FeedWidget> createState() => _FeedWidgetState();
}
class _FeedWidgetState extends State<FeedWidget> {
final TextEditingController controller = TextEditingController();
void _createForumPost() async {
Map<String, dynamic> inputVariables = {
"questionText": controller.text,
};
GraphQLClient client = GraphQLProvider.of(context).value;
await client.query(
QueryOptions(
document: gql(GraphQLQueries.createForumPost),
variables: inputVariables,
),
);
setState(() {
controller.text = "";
});
}
#override
Widget build(BuildContext context) {
return Query(
options: QueryOptions(
fetchPolicy: FetchPolicy.networkOnly,
document: gql(GraphQLQueries.getForumEntries),
),
builder: (QueryResult result,
{VoidCallback? refetch, FetchMore? fetchMore}) {
if (result.hasException) {
return Text(result.exception.toString());
}
if (result.isLoading) {
return const Center(child: CircularProgressIndicator());
}
List<dynamic> forumEntries = result.data?["getForumEntries"];
return Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Row(
children: [
Expanded(
child: TextField(
controller: controller,
keyboardType: TextInputType.multiline,
maxLines: null,
autocorrect: false,
decoration: InputDecoration(
fillColor: Theme.of(context).colorScheme.surface,
labelText: "Content",
filled: true,
border: InputBorder.none,
),
),
),
const Padding(padding: EdgeInsets.symmetric(horizontal: 3)),
CustomIconButton(
padding: EdgeInsets.zero,
icon: const Icon(Icons.send),
onPressed: () => _createForumPost(),
),
],
),
const Padding(padding: EdgeInsets.only(bottom: 10)),
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: forumEntries.length,
itemBuilder: (BuildContext context, int index) {
Map<String, dynamic> entry = forumEntries[index];
return ForumEntryWidget(entry);
},
),
),
],
);
},
);
}
}
I was stuck on this and solved it easily using refetch functions returned by Query. As your CustomIconButton are inside the Query, you can replace _createForumPost() with refetch(). In my case I use the RefreshIndicator creating a function Future<void> which calls the refetch method.
#override
Widget build(BuildContext context) {
// resp = ListView();
return Scaffold(
body: Query(
options: QueryOptions(
document: gql(documentQ),
fetchPolicy: FetchPolicy.noCache,
cacheRereadPolicy: CacheRereadPolicy.ignoreAll,
optimisticResult: true),
builder: (QueryResult result,
{Refetch? refetch, FetchMore? fetchMore}) {
if (result.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (result.data == null) {
return const Center(child: Text('Nenhum tour por perto'));
}
return _toursView(result, refetch);
},
),
);
}
RefreshIndicator _toursView(QueryResult result, refetch) {
// print(result.data?['tourAll']);
Future<void> refresh(refetch) {
return refetch();
}
final toursList = result.data?['tourAll'];
return RefreshIndicator(
onRefresh: () => refresh(refetch),
child: ...
here is the code from which I am able to get document ids in collection from cloud firestore
class _MyHomePageState extends State<MyHomePage> {
final userRef =
FirebaseFirestore.instance.collection('/Exam');
#override
Widget build(BuildContext context) {
return Container(
child: TextButton(
child: Text('press'),
onPressed: () {
userRef.get().then((snapshot) {
snapshot.docs.forEach((doc) {
print(doc.id);
});
});
},
),
);
}
}
I want to convert above code into future builder and map it in drop down button. I convert the above code as follow. Is it correct or not?
FutureBuilder(
future: db.collection('/Exam').get(),
builder:
(BuildContext context, AsyncSnapshot asyncSnapshot) {
if (asyncSnapshot.hasError)
return Text("Error: ${asyncSnapshot.error}");
if (!asyncSnapshot.hasData)
return const CircularProgressIndicator();
return DropdownButton<String>(
isExpanded: true,
items: asyncSnapshot.data!.docs
.map(
(snap) => DropdownMenuItem(
value: snap.id,
child: Text(
snap.id.toString(),
),
),
)
.toList(),
value: _selectedexam,
onChanged: (String? newValue) {
setState(() {
_selectedexam = newValue!;
_selectedsemester = null;
print(_selectedexam);
});
},
);
}),
If I run above code I get following errors
you need to type cast future builder as below
return FutureBuilder<QuerySnapshot>
I created this StateNotifier with Riverpod in Flutter which Returns the Object DocumentsList
class TripStateNotifier extends StateNotifier<List<DocumentList>> {
TripStateNotifier() : super([]);
void getDocuments() async {
final res = await db.listDocuments(collectionId: '6286c0f1e7b7a5760baa');
state = res as List<DocumentList>;
}
}
final TripState = StateNotifierProvider((ref) => TripStateNotifier());
And this ConsumerWidget whicht gets the data
ref.watch(TripState)!.when(
data: (list) {
//Handeling if no data is found
if(list == null || (list.documents?.isEmpty ?? true)) return Center(child: Text("Yet you didn´t add any destinations to your trip", style: TextStyle(color: Colors.black.withOpacity(0.5)))); return ListView.builder(
itemCount: list.total,
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemBuilder: (context, index) {
return mytrip_card(
location: list.documents[index].data['location'], date: list.documents[index].data['date']
);
},
);
},
error: (e, s) => Text(e.toString()),
loading: () => const CircularProgressIndicator(),
),
My Problem is that my IDE Outputs following Error in regards to the when in ref.watch(TripState)!.when
The method 'when' isn't defined for the type 'Object'.
Intrestingly enought my Old Soulution with Future Provider worked why does this not?
Old solution:
final TripProvider = FutureProvider((ref)
async {
debugPrint('test');
final res = await db.listDocuments(collectionId: '6286c0f1e7b7a5760baa');
return res;
});
The method when is only available for the object type AsyncValue which can be provided by a FutureProvider or a StreamProvider.
Now that you are using a StateNotifierProvider, you won't be able to use when anymore, I would recommend you to create a "state" class to use with your TripStateNotifier.
Code sample:
final tripStateProvider = StateNotifierProvider<TripStateNotifier, TripState>(
(ref) => TripStateNotifier());
class TripStateNotifier extends StateNotifier<TripState> {
TripStateNotifier() : super(const TripState());
void getDocuments() async {
// Trigger the loading state
state = state.copyWith(isLoading: true);
try {
final res = await db.listDocuments(collectionId: '6286c0f1e7b7a5760baa');
// Update the state with your newly fetched results
state = state.copyWith(
isLoading: false,
documents: res as List<DocumentList>,
);
} catch (e) {
// Manage received error
state = state.copyWith(
isLoading: false,
hasError: true,
);
}
}
}
#immutable
class TripState {
final bool isLoading;
final bool hasError;
final List<DocumentList> documents;
const TripState({
this.documents = const [],
this.isLoading = false,
this.hasError = false,
});
int get total => documents.length;
TripState copyWith({
List<DocumentList>? documents,
bool? isLoading,
bool? hasError,
}) {
return TripState(
documents: documents ?? this.documents,
isLoading: isLoading ?? this.isLoading,
hasError: hasError ?? this.hasError,
);
}
}
class MyWidget extends ConsumerWidget {
#override
Widget build(BuildContext context, WidgetRef ref) {
final currentState = ref.watch(tripStateProvider);
final controller = ref.read(tripStateProvider.notifier);
if (currentState.isLoading) return const CircularProgressIndicator();
if (currentState.documents.isEmpty) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Yet you didn´t add any destinations to your trip",
style: TextStyle(
color: Colors.black.withOpacity(0.5),
),
),
TextButton(
onPressed: controller.getDocuments,
child: const Text('Refresh'),
),
],
),
);
}
return ListView.builder(
itemCount: currentState.total,
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemBuilder: (context, index) {
return MyTripCard(
location: currentState.documents[index].data['location'],
date: currentState.documents[index].data['date'],
);
},
);
}
}
Try the full example on DartPad
Define StateNotifier like this
final tripState = StateNotifierProvider<TripStateNotifier, List<DocumentList>>((ref) {
return TripStateNotifier();
});
Access to documents like this
List<DocumentList> trips = ref.watch(tripState);
return ListView.builder(
itemCount: trips.length,
itemBuilder: (context, index) {
...
}
);
See StateNotifierProvider for details.
I'm refactoring my app to GetX state management for less boilerplate code.
I make the Controller and the API provider (code below).
But when I want to refresh the data (Manually too) it won't change.
home_page.dart
class HomeUI extends GetView<HomeController> {
...
GetX<HomeController>(
initState: (state) => Get.find<HomeController>().getAll(),
builder: (_) {
return _.goalList.length < 1 ||
_.goalList == null
? Center(
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
Text('0 goals found, please wait',
style: Theme.of(context)
.textTheme
.headline6
.copyWith(
color: kTextColor))
],
))
: ListView.builder(
itemBuilder: (context, index) {
GoalModel goalModel =
GoalModel.fromMap(
_.goalList[index]);
return ListTile(
title: Text(goalModel.text),
subtitle:
Text(goalModel.updated_at),
);
});
}
home_controller.dart
class HomeUI extends GetView<HomeController> {
...
class HomeController extends GetxController {
final MyRepository repository = MyRepository();
final _goalsList = RxList();
get goalList => this._goalsList.value;
set goalList(value) => this._goalsList.value = value;
getAll() {
repository.getAll().then((data) {
this.goalList = data;
update();
});
}
delete(id) {
repository.delete(id).then((message) {
this.goalList;
return message;
});
}
add(goal) {
repository.add(goal).then((data) {
this.goalList = data;
});
}
edit(editedItem, text, achievementDate) {
repository.edit(editedItem, text, achievementDate).then((data) {
this.goalList = data;
});
}
}
goals_repository.dart
class MyRepository {
final MyApiClient apiClient = MyApiClient();
getAll() {
return apiClient.getAll();
}
delete(id) {
return apiClient.deleteGoal(id);
}
edit(editedItem, text, achievementDate) {
return apiClient.updateGoal(editedItem, text, achievementDate);
}
add(goal) {
return apiClient.postGoal(goal);
}
}
api.dart (getAll() method)
getAll() async {
try {
var _token = await _sharedPrefsHelper.getTokenValue();
var response = await httpClient.get(baseUrl, headers: {
'Authorization': 'Bearer $_token',
});
if (response.statusCode == 200) {
print('json decode response is: ${json.decode(response.body)}');
return json.decode(response.body);
} else
print('erro -get');
} catch (error) {
print(error);
}
}
I followed this article to make the implementation:
getx_pattern
After updating manually your list, do:
this._goalsList.refresh()
After that your UI will be updated
Just Wrap the ListView.builder list with Obx or Getx. For widgets that are not in the list, you can wrap them individually with obx or getx.
Example:
Obx(() => ListView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: item.length,
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
return Card()...
},
),
),
Obs Getx variables are only observed within an Obx or Getx as stated above. You need to wrap them up. Just be careful not to use Obx / Getx when there are no variables observed inside, as it will generate an error.
This answer is for #mjablecnik's comment:
class Other extends StatelessWidget {
final Counter c = Get.find();
final _chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
final Random _rnd = Random();
/* ---------------------------------------------------------------------------- */
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Obx(() => ListView.builder(
scrollDirection: Axis.vertical,
padding: EdgeInsets.all(10),
itemCount: c.testList.length,
itemBuilder: (context, index) => Card(
color: Colors.amber[600],
child: Padding(
padding: const EdgeInsets.all(10),
child: Center(
child: Text('${c.testList[index]}'),
),
),
),
)),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () => c.addToList(getRandomString(15)),
),
);
}
/* ---------------------------------------------------------------------------- */
// source: https://stackoverflow.com/questions/61919395/how-to-generate-random-string-in-dart
String getRandomString(int length) => String.fromCharCodes(Iterable.generate(
length, (_) => _chars.codeUnitAt(_rnd.nextInt(_chars.length))
)
);
}
Update 1:
Another little change I did was for the controller:
class Counter extends GetxController {
var count = 0.obs;
var testList = <String>['test1', 'test2'].obs;
/* ---------------------------------------------------------------------------- */
void incremenent() => count++;
/* ---------------------------------------------------------------------------- */
void addToList(String item) {
print('adding: $item');
testList.add(item);
}
}
I am calling login api on button click, I am able to get response from server but on clicking on button it doesn't show progress bar. I am using BLoC pattern for this. Here is the code,
import 'package:flutter/material.dart';
import '../blocs/bloc.dart';
import '../blocs/provider.dart';
import '../models/login_response.dart';
class LoginScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Provider(
child: new Scaffold(
body: Container(
child: LoginForm(),
),
),
);
}
}
class LoginForm extends StatefulWidget {
// since its a stateful widget we need to create state for it.
const LoginForm({Key key}) : super(key: key);
#override
_LoginFormState createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
#override
Widget build(BuildContext context) {
return Form(
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 50),
),
// Start creating widget here.
emailField(),
passwordField(),
Container(margin: EdgeInsets.only(top: 25.0)),
submitButton()
],
),
);
}
Widget emailField() {
return StreamBuilder(
stream: bloc.email,
builder: (context, snapshot) {
return TextField(
onChanged: bloc.changeEmail,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'you#example.com',
labelText: 'Email Address',
errorText: snapshot.error
),
);
}
);
}
Widget passwordField() {
return StreamBuilder(
stream: bloc.password,
builder: (context, snapshot) {
return TextField(
onChanged: bloc.changePassword,
obscureText: true,
decoration: InputDecoration(
labelText: 'Please enter your password',
hintText: 'Password',
errorText: snapshot.error
),
);
},
);
}
Widget submitButton() {
return StreamBuilder(
stream: bloc.submitValid,
builder: (context, snapshot) {
return RaisedButton(
onPressed:() => showWidgetForNetworkCall(context),
// onPressed: () {
// // Do submit button action.
// showWidgetForNetworkCall(context);
// // callLoginApi();
// },
child: const Text('Login'),
textColor: Colors.white,
color: Colors.blueAccent,
);
},
);
}
// Loading Widget
Widget _buildLoadingWidget() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("Loading data from API...", textDirection: TextDirection.ltr), CircularProgressIndicator()
],
),
);
}
// // Error Widget
Widget _buildErrorWidget(String error) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("Loading error data from API...", textDirection: TextDirection.ltr), CircularProgressIndicator()
],
),
);
}
// show server data
showServerData() {
print(" Servr >>>>>> Data : ");
}
Widget showWidgetForNetworkCall(BuildContext context) {
bloc.loginSubmit();
return StreamBuilder(
stream: bloc.loginSubject.stream,
builder: (context, AsyncSnapshot<LoginResponse>snapshot){
if (snapshot.hasData) {
return showServerData();
} else if (snapshot.hasError) {
return _buildErrorWidget(snapshot.error);
} else {
return _buildLoadingWidget();
}
},
);
}
}
This is my login_screen.dart. And my bloc class for api call is:
postData() async {
LoginResponse response = await _repository.postData(_loginResource);
_subject.sink.add(response);
}
I am able to parse json api, but not able to get the response of my model i.e, 'LoginResponse' in login_screen.dart class and also the CircularProgressBar doesn't show when api is called on button click.
Code of the BLoC class is :
import 'dart:async';
import 'package:rxdart/rxdart.dart';
import 'validators.dart';
import '../models/login_response.dart';
import '../repository/login_repository.dart';
import '../resources/login_resource.dart';
class Bloc extends Object with Validators {
final LoginRepository _repository = LoginRepository();
final BehaviorSubject<LoginResponse> _subject =
BehaviorSubject<LoginResponse>();
LoginResource _loginResource = LoginResource();
final _email = BehaviorSubject<String>(); // Declaring variable as private
final _password = BehaviorSubject<String>(); // Declaring variable as private
// Add data to stream (Its like setter)
Stream<String> get email => _email.stream.transform(validateEmail);
Stream<String> get password =>
_password.stream.transform(validatePassword);
Stream<bool> get submitValid => Observable.combineLatest2(email, password, (e, p) => true);
// Change data. For retrieveing email value.
Function(String) get changeEmail => _email.sink.add;
Function(String) get changePassword => _password.sink.add;
loginSubmit() {
_loginResource.email = "bar1";
_loginResource.password = "bar2";
postData();
}
postData() async {
LoginResponse response = await _repository.postData(_loginResource);
_subject.sink.add(response);
}
dispose() {
_email.close();
_password.close();
_subject.close();
}
BehaviorSubject<LoginResponse> get loginSubject => _subject;
}
final bloc = Bloc();
Kindly let me know what I am missing. Thanks in advance :)
Well here we go. I make some changes in your UI layer and in BLoC class with order to accomplish what you're asking for. I will firstly show the pieces of code that I insert and explain what I was think when I wrote it and after all I will paste the entire source code will all changes. Maybe you can use the concept that I had used to adapt the source code to your needs. All code has comments so please read it will help you a lot.
First of all I create an enum to represent the status of the login process and a class that holds the login process status and a message about it. Both are part of your UI layer.
/// NON_LOGIN: means that login is not happening
/// LOGGIN: means that login is happening
/// LOGIN_ERROR: means that something is wrong with login
/// LOGIN_SUCCESS: the login process was a success.
enum LoginStatus { NON_LOGIN, LOGGING, LOGIN_SUCCESS, LOGIN_ERROR }
class LoginState {
final LoginStatus status;
final String message;
LoginState({this.status, this.message});
}
In _LoginFormState class inside build method I inserted a StreamBuilder that will show and hide the progressbar when the login is happening or show an error widget.
#override
Widget build(BuildContext context) {
return Form(
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 50),
),
// Start creating widget here.
emailField(),
passwordField(),
Container(margin: EdgeInsets.only(top: 25.0)),
submitButton(),
StreamBuilder<LoginState>(
stream: bloc.loginStateStream,
builder: (context, AsyncSnapshot<LoginState> snapshot){
if ( !snapshot.hasData )
return Container();
switch(snapshot.data.status){
case LoginStatus.LOGGING:
return _buildLoadingWidget();
case LoginStatus.LOGIN_ERROR:
return _buildErrorWidget(snapshot.data.message);
case LoginStatus.LOGIN_SUCCESS:
// Here you can go to another screen after login success.
return Center(child: Text("${snapshot.data.message}"),);
case LoginStatus.NON_LOGIN:
default:
return Container();
}
},
),
],
),
);
}
And the last change in your UI layer was in submitButton method the only change was in onPress event of your button now it calls bloc.loginSubmit method.
return RaisedButton(
onPressed:() => bloc.loginSubmit(), // the only change
child: const Text('Login'),
textColor: Colors.white,
color: Colors.blueAccent,
);
Now all the changes are in BLoC class. Basically I created a new subject for handling the state changes of login process using LoginStatus enum and LoginState class and tell to view what widget must be showed to user.
//The subject and a get method to expose his stream
final PublishSubject<LoginState> _loginStateSubject = new PublishSubject();
Observable<LoginState> get loginStateStream => _loginStateSubject.stream;
All the login state changes handling I wrote inside postData method.
postData() async {
// this call will change the UI and a CircularProgressBar will be showed.
changeLoginState(state: LoginState( status: LoginStatus.LOGGING, message: "logging") );
// waiting for login response!
LoginResponse response = await _repository.postData(_loginResource);
print(response); // just to text debug your response.
//Here you can verify if the login process was successfully or if there is
// some kind of error based in your LoginResponse model class.
// avoiding write this logic in UI layer.
if(response.hasError){
changeLoginState(state: LoginState(status: LoginStatus.LOGIN_ERROR,
message: response.errorMessage)
);
// and after 1.5 seconds we make the error message disappear from UI.
// you can do this in UI layer too
Future.delayed(Duration(milliseconds: 1500), (){
// you can pass null to state property, will make the same effect
changeLoginState(state: LoginState(status: LoginStatus.NON_LOGIN)); });
}
else {
changeLoginState(state: LoginState(status:
LoginStatus.LOGIN_SUCCESS, message: "Login Success"));
}
//_subject.sink.add(response);
}
With this approach you avoid send to your UI layer objects from you model layer like LoginResponse class objects and this kind of concept makes your code more clean and do not broken MVC pattern and your UI layer holds only layout code.
Make some tests, I didn't, adapt to your needs and comment if you need something I will answer when I can.
The entire source code:
/// NON_LOGIN: means that login is not happening
/// LOGGIN: means that login is happening
/// LOGIN_ERROR: means that something is wrong with login
/// LOGIN_SUCCESS: the login process was a success.
///
enum LoginStatus { NON_LOGIN, LOGGING, LOGIN_SUCCESS, LOGIN_ERROR }
class LoginState {
final LoginStatus status;
final String message;
LoginState({this.status, this.message});
}
class LoginScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Provider(
child: new Scaffold(
body: Container(
child: LoginForm(),
),
),
);
}
}
class LoginForm extends StatefulWidget {
// since its a stateful widget we need to create state for it.
const LoginForm({Key key}) : super(key: key);
#override
_LoginFormState createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
#override
Widget build(BuildContext context) {
return Form(
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 50),
),
// Start creating widget here.
emailField(),
passwordField(),
Container(margin: EdgeInsets.only(top: 25.0)),
submitButton(),
StreamBuilder<LoginState>(
stream: bloc.loginStateStream,
builder: (context, AsyncSnapshot<LoginState> snapshot){
if ( !snapshot.hasData )
return Container();
switch(snapshot.data.status){
case LoginStatus.LOGGING:
return _buildLoadingWidget();
case LoginStatus.LOGIN_ERROR:
return _buildErrorWidget(snapshot.data.message);
case LoginStatus.LOGIN_SUCCESS:
// Here you can go to another screen after login success.
return Center(child: Text("${snapshot.data.message}"),);
case LoginStatus.NON_LOGIN:
default:
return Container();
}
},
),
],
),
);
}
Widget emailField() {
return StreamBuilder(
stream: bloc.email,
builder: (context, snapshot) {
return TextField(
onChanged: bloc.changeEmail,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'you#example.com',
labelText: 'Email Address',
errorText: snapshot.error
),
);
}
);
}
Widget passwordField() {
return StreamBuilder(
stream: bloc.password,
builder: (context, snapshot) {
return TextField(
onChanged: bloc.changePassword,
obscureText: true,
decoration: InputDecoration(
labelText: 'Please enter your password',
hintText: 'Password',
errorText: snapshot.error
),
);
},
);
}
Widget submitButton() {
return StreamBuilder(
stream: bloc.submitValid,
builder: (context, snapshot) {
return RaisedButton(
onPressed:() => bloc.loginSubmit(),
child: const Text('Login'),
textColor: Colors.white,
color: Colors.blueAccent,
);
},
);
}
// Loading Widget
Widget _buildLoadingWidget() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("Loading data from API...", textDirection: TextDirection.ltr), CircularProgressIndicator()
],
),
);
}
// // Error Widget
Widget _buildErrorWidget(String error) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("Loading error data from API...", textDirection: TextDirection.ltr), CircularProgressIndicator()
],
),
);
}
/*
// show server data
showServerData() {
print(" Servr >>>>>> Data : ");
}
Widget showWidgetForNetworkCall() {
return StreamBuilder(
stream: bloc.loginSubject.stream,
builder: (context, AsyncSnapshot<LoginResponse>snapshot){
if (snapshot.hasData) {
return showServerData();
} else if (snapshot.hasError) {
return _buildErrorWidget(snapshot.error);
} else {
return _buildLoadingWidget();
}
},
);
}*/
}
class Bloc extends Object with Validators {
//final BehaviorSubject<LoginResponse> _subject = BehaviorSubject<LoginResponse>();
//BehaviorSubject<LoginResponse> get loginSubject => _subject;
final LoginRepository _repository = LoginRepository();
final PublishSubject<LoginState> _loginStateSubject = new PublishSubject();
Observable<LoginState> get loginStateStream => _loginStateSubject.stream;
LoginResource _loginResource = LoginResource();
final _email = BehaviorSubject<String>(); // Declaring variable as private
final _password = BehaviorSubject<String>(); // Declaring variable as private
// Add data to stream (Its like setter)
Stream<String> get email => _email.stream.transform(validateEmail);
Stream<String> get password => _password.stream.transform(validatePassword);
Stream<bool> get submitValid => Observable.combineLatest2(email, password, (e, p) => true);
// Change data. For retrieveing email value.
Function(String) get changeEmail => _email.sink.add;
Function(String) get changePassword => _password.sink.add;
void changeLoginState({LoginState state } ) => _loginStateSubject.sink.add(state);
loginSubmit() {
_loginResource.email = "bar1";
_loginResource.password = "bar2";
postData();
}
postData() async {
// this call will change the UI and a CircularProgressBar will be showed.
changeLoginState(state: LoginState( status: LoginStatus.LOGGING, message: "logging") );
// waiting for login response!
LoginResponse response = await _repository.postData(_loginResource);
print(response); // just to text debug your response.
//Here you can verify if the login process was successfully or if there is
// some kind of error based in your LoginResponse model class.
if(response.hasError){
changeLoginState(state: LoginState(status: LoginStatus.LOGIN_ERROR,
message: response.errorMessage)
);
// and after 1.5 seconds we make the error message disappear from UI.
// you can do this in UI layer too
Future.delayed(Duration(milliseconds: 1500), (){
// you can pass null to state property, will make the same effect
changeLoginState(state: LoginState(status: LoginStatus.NON_LOGIN)); });
}
else {
changeLoginState(state: LoginState(status:
LoginStatus.LOGIN_SUCCESS, message: "Login Success"));
}
//_subject.sink.add(response);
}
dispose() {
_loginStateSubject.close();
_email.close();
_password.close();
//_subject.close();
}
}
final bloc = Bloc();