How to update the ParentWidget of ModalBottomSheet using setState() in Flutter? - flutter

In my SingleDayCalendarView() I have a button which calls a showModalBottomSheet() with AddShiftBottomSheet as its child :
class SingleDayCalendarView extends StatelessWidget {
...
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
enableDrag: true,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(25),
topRight: Radius.circular(25),
),
),
builder: (BuildContext context) {
return AddShiftBottomSheet(
dayOfTheShift: id,
);
});
},
Then inside AddshiftBottomSheet, which is a stateful widget in another file, I call another showModalBottomSheet to show a TimePicker
class AddShiftBottomSheet extends StatefulWidget {
#override
Widget build(BuildContext context) {
DateTime _startDateTime;
...
Text("${DateFormat("HH:mm").format(_startDateTime)}",
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return Container(
height: 200,
color: Colors.white,
child: CupertinoDatePicker(
onDateTimeChanged: (dateTime) {
setState(() {
_startDateTime = dateTime;
});
print(_startDateTime);
},
The problem is that when I change the time with the TimePicker, the Text() which should display the _startDateTime, doesn't change and keeps displaying its initial value.
With print statement I see that the variable _startDateTime it's changing as it should and that setState its triggered, but nothing happens.
One strange behavior: if I but the _startDateTime variable between:
class AddShiftBottomSheet extends StatefulWidget {
#override
_AddShiftBottomSheetState createState() => _AddShiftBottomSheetState();
}
//here
DateTime = _startDateTime;
class _AddShiftBottomSheetState extends State<AddShiftBottomSheet> {
everything works.

Remove DateTime _startDateTime; from build method and define it in class scope:
class AddShiftBottomSheet extends StatefulWidget {
DateTime _startDateTime;
#override
Widget build(BuildContext context) {
...
When you call setState build method is called again, where you've defined _startDateTime.

Related

How to pass generic provider to child?

I want to create a universal alert that I will use several times in my app.
class SelectIconAlertDialogWidget extends StatelessWidget {
const SelectIconAlertDialogWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final model = Provider.of<IncomeViewModel>(context, listen: true).state;
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40),
),
Problem is that I can not figure out how to pass type of parent ChangeNotifier
showDialog<String>(
context: context,
builder: (_) =>
ChangeNotifierProvider<IncomeViewModel>.value(
value: view,
child: const SelectIconAlertDialogWidget(),
),
);
I have a lot of ViewModels that will use this alert so dont want to repeat this code and write generic Alert that I can use with any ViewModel. How can I achieve this?
Create some abstract class with methods or fields you want to have for all your view models you use for that dialog
abstract class AbstractViewModel{
void doStuff();
}
Implement this class for your view models
class MyViewModel1 implements AbstractViewModel{
#override
void doStuff (){ print("from MyViewModel1");}
}
Add type parameter for your dialog class
class SelectIconAlertDialogWidget<T extends AbstractViewModel> extends StatelessWidget {
const SelectIconAlertDialogWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
// and you can use generic type T like this:
final model = Provider.of<T>(context, listen: true);
model.doStuff();
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40),
),
And call it passing type parameter SelectIconAlertDialogWidget<MyViewModel1>()
showDialog<String>(
context: context,
builder: (_) =>
ChangeNotifierProvider<MyViewModel1>.value(
value: view,
child: const SelectIconAlertDialogWidget<MyViewModel1>(),
),
);
On build method of your dialog widget it will print "from MyViewModel1". Hope you got the concept of abstraction.

Can I use a whole widget/screen as modal bottom sheet?

I have a login screen, which I want to be accessible at different points throughout the app. Can I use that screen as a modal bottom sheet like this?
example from airbnb
Yes you can, the below code snippet is used to show bottomsheet:-
bottomSheetForSignIn(BuildContext context)
{
showModalBottomSheet(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(topLeft: Radius.circular(25),topRight: Radius.circular(25)),
),
isScrollControlled: true,
context: context,
builder: (context){
return SignIn();
}
);
}
and then create a stateful widget signin returning a container with a property height(to control like in how much part of screen you want to show bottom sheet)
Code for it will look like:-
class SignIn extends StatefulWidget {
const SignIn({Key? key}) : super(key: key);
#override
_SignInState createState() => _SignInState();
}
class _SignInState extends State<SignIn> {
#override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.of(context).size.height*0.9,//to control height of bottom sheet
child: Column( //your design from here onwards
children: [
],
),
);
}
}
And whenever you want to open bottom sheet just make a call to:-
bottomSheetForSignIn(context);

Getx argumentsbeing cleared after using showDialog() in Flutter

The used Getx Arguments are cleared after the showDialog method is executed.
_someMethod (BuildContext context) async {
print(Get.arguments['myVariable'].toString()); // Value is available at this stage
await showDialog(
context: context,
builder: (context) => new AlertDialog(
//Simple logic to select between two buttons
); // get some Confirmation to execute some logic
print(Get.arguments['myVariable'].toString()); // Variable is lost and an error is thrown
Also I would like to know how to use Getx to show snackbars without losing the previous arguments as above.
One way to do this is to duplicate the data into a variable inside the controller and make a use from it instead of directly using it from the Get.arguments, so when the widget tree rebuild, the state are kept.
Example
class MyController extends GetxController {
final myArgument = ''.obs;
#override
void onInit() {
myArgument(Get.arguments['myVariable'] as String);
super.onInit();
}
}
class MyView extends GetView<MyController> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: Center(child: Obx(() => Text(controller.myArgument()))),
),
);
}
}
UPDATE
Since you are looking for solution without page transition, another way to achieve that is to make a function in the Controller or directly assign in from the UI. Like so...
class MyController extends GetxController {
final myArgument = 'empty'.obs;
}
class MyView extends GetView<MyController> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Obx(() => Text(controller.myArgument())),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
controller.myArgument(Get.arguments['myVariable'] as String);
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(controller.myArgument()); // This should work
}
}
UPDATE 2 (If you don't use GetView)
class MyController extends GetxController {
final myArgument = 'empty'.obs;
}
class MyView extends StatelessWidget {
final controller = Get.put(MyController());
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Obx(() => Text(controller.myArgument())),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
controller.myArgument(Get.arguments['myVariable'] as String);
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(controller.myArgument()); // This should work
}
}
UPDATE 3 (NOT RECOMMENDED)
If you really really really want to avoid using Controller at any cost, you can assign it to a normal variable in a StatefulWidget, although I do not recommend this approach since it was considered bad practice and violates the goal of the framework itself and might confuse your team in the future.
class MyPage extends StatefulWidget {
const MyPage({ Key? key }) : super(key: key);
#override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
String _myArgument = 'empty';
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Text(_myArgument),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
setState(() {
_myArgument = Get.arguments['myVariable'] as String;
});
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(_myArgument); // This should work
}
}

Flutter: Unhandled Exception: Bad state: Cannot add new events after calling close (NOT SAME CASE)

I am trying to use the BLoC pattern to manage data from an API and show them in my widget. I am able to fetch data from API and process it and show it, but I am using a bottom navigation bar and when I change tab and go to my previous tab, it returns this error:
Unhandled Exception: Bad state: Cannot add new events after calling close.
I know it is because I am closing the stream and then trying to add to it, but I do not know how to fix it because not disposing of the publish subject will result in a memory leak.
I know maybe this question is almost the same as this question.
But I have implemented it and it doesn't work in my case, so I make questions with a different code and hope someone can help me in solving my case. I hope you understand, Thanks.
Here is my BLoC code:
import '../resources/repository.dart';
import 'package:rxdart/rxdart.dart';
import '../models/meals_list.dart';
class MealsBloc {
final _repository = Repository();
final _mealsFetcher = PublishSubject<MealsList>();
Observable<MealsList> get allMeals => _mealsFetcher.stream;
fetchAllMeals(String mealsType) async {
MealsList mealsList = await _repository.fetchAllMeals(mealsType);
_mealsFetcher.sink.add(mealsList);
}
dispose() {
_mealsFetcher.close();
}
}
final bloc = MealsBloc();
Here is my UI code:
import 'package:flutter/material.dart';
import '../models/meals_list.dart';
import '../blocs/meals_list_bloc.dart';
import '../hero/hero_animation.dart';
import 'package:dicoding_submission/src/app.dart';
import 'detail_screen.dart';
class DesertScreen extends StatefulWidget {
#override
DesertState createState() => new DesertState();
}
class DesertState extends State<DesertScreen> {
#override
void initState() {
super.initState();
bloc.fetchAllMeals('Dessert');
}
#override
void dispose() {
bloc.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: getListDesert()
);
}
getListDesert() {
return Container(
color: Color.fromRGBO(58, 66, 86, 1.0),
child: Center(
child: StreamBuilder(
stream: bloc.allMeals,
builder: (context, AsyncSnapshot<MealsList> snapshot) {
if (snapshot.hasData) {
return _showListDessert(snapshot);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
return Center(child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white)
));
},
),
),
);
}
Widget _showListDessert(AsyncSnapshot<MealsList> snapshot) => GridView.builder(
itemCount: snapshot == null ? 0 : snapshot.data.meals.length,
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
child: Card(
elevation: 2.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(5))),
margin: EdgeInsets.all(10),
child: GridTile(
child: PhotoHero(
tag: snapshot.data.meals[index].strMeal,
onTap: () {
showSnackBar(context, snapshot.data.meals[index].strMeal);
Navigator.push(
context,
PageRouteBuilder(
transitionDuration: Duration(milliseconds: 777),
pageBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) =>
DetailScreen(
idMeal: snapshot.data.meals[index].idMeal),
));
},
photo: snapshot.data.meals[index].strMealThumb,
),
footer: Container(
color: Colors.white70,
padding: EdgeInsets.all(5.0),
child: Text(
snapshot.data.meals[index].strMeal,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.bold, color: Colors.deepOrange),
),
),
),
),
);
},
);
}
If you need the full source code, this is the repo with branch submission-3
bloc.dispose(); is the problem.
Since the bloc is initialised outside your UI code, there is no need to dispose them.
Why are you instantiating your bloc on the bloc class?
You must add your bloc instance somewhere in your widget tree, making use of a InheritedWidget with some Provider logic. Then in your widgets down the tree you would take that instance and access its streams. That is why this whole process it is called 'lifting up the state'.
That way, your bloc will always be alive when you need it, and the dispose would still be called sometime.
A bloc provider for example:
import 'package:flutter/material.dart';
abstract class BlocBase {
void dispose();
}
class BlocProvider<T extends BlocBase> extends StatefulWidget {
BlocProvider({
Key key,
#required this.child,
#required this.bloc,
}) : super(key: key);
final T bloc;
final Widget child;
#override
State<StatefulWidget> createState() => _BlocProviderState<T>();
static T of<T extends BlocBase>(BuildContext context) {
final type = _typeOf<_BlocProviderInherited<T>>();
_BlocProviderInherited<T> provider = context
.ancestorInheritedElementForWidgetOfExactType(type)
?.widget;
return provider?.bloc;
}
static Type _typeOf<T>() => T;
}
class _BlocProviderState<T extends BlocBase> extends State<BlocProvider<T>> {
#override
Widget build(BuildContext context) {
return new _BlocProviderInherited(
child: widget.child,
bloc: widget.bloc
);
}
#override
void dispose() {
widget.bloc?.dispose();
super.dispose();
}
}
class _BlocProviderInherited<T> extends InheritedWidget {
_BlocProviderInherited({
Key key,
#required Widget child,
#required this.bloc
}) : super(key: key, child: child);
final T bloc;
#override
bool updateShouldNotify(InheritedWidget oldWidget) => false;
}
It makes use of a combination of InheritedWidget (to be available easily down the widget tree) and StatefulWidget (so it can be disposable).
Now you must add the provider of some bloc somewhere into your widget tree, that is up to you, I personally like to add it between the routes of my screens.
In the rout of my MaterialApp widget:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'MyApp',
onGenerateRoute: _routes,
);
}
Route _routes(RouteSettings settings) {
if (settings.isInitialRoute)
return MaterialPageRoute(
builder: (context) {
final mealsbloc = MealsBloc();
mealsbloc.fetchAllMeals('Dessert');
final homePage = DesertScreen();
return BlocProvider<DesertScreen>(
bloc: mealsbloc,
child: homePage,
);
}
);
}
}
With the help of routes, the bloc was created 'above' our homePage. Here I can call wherever initialization methods on the bloc I want, like .fetchAllMeals('Dessert'), without the need to use a StatefulWidget and call it on initState.
Now obviously for this to work your blocs must implements the BlocBase class
class MealsBloc implements BlocBase {
final _repository = Repository();
final _mealsFetcher = PublishSubject<MealsList>();
Observable<MealsList> get allMeals => _mealsFetcher.stream;
fetchAllMeals(String mealsType) async {
MealsList mealsList = await _repository.fetchAllMeals(mealsType);
_mealsFetcher.sink.add(mealsList);
}
#override
dispose() {
_mealsFetcher.close();
}
}
Notice the override on dispose(), from now on, your blocs will dispose themselves, just make sure to close everything on this method.
A simple project with this approach here.
To end this, on the build method of your DesertScreen widget, get the available instance of the bloc like this:
var bloc = BlocProvider.of<MealsBloc>(context);
A simple project using this approach here.
For answers that resolve my problem, you can follow the following link: This
I hope you enjoy it!!

Flutter infinite loop when StreamBuilder inside LayoutBuilder

So i am making a page with a LayoutBuilder as described here
Inside the LayoutBuilder i put a StreamBuilder with a TextField powered by the bloc class SignupFormBloc. The stream is a BehaviorSubject
When someone put something in the input it trigger the onChanged function which is the sink for my stream. So i add the value in the stream then i pass the value in a StreamTransformer to validate the value and then i let the StreamBuilder to build the TextField again with an error message(if value not valid).
This is were the problem starts.
When i click on the TextField and enter something it starts an infinite loop like this:
The StreamBuilder sees the new value in the stream
The StreamBuilder try to rebuild TextField
Some how this triggers the LayoutBuilder builder function
The LayoutBuilder builder function builds again the StreamBuilder
StreamBuilder find a value in stream(because of the BehaviorSubject)
and all start again from the first bulled in an endless loop
Hint: If i change the BehaviorSubject to a PublishSubject everything is ok
Hint 2: If i remove the StreamBuilder completely and just let a blank TextField, you can see that in every entry the LayoutBuilder builder function run. Is that a normal behavior?
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
SignupFormBloc _signupFormBloc;
#override
void initState() {
super.initState();
_signupFormBloc = SignupFormBloc();
}
#override
Widget build(BuildContext context) {
print('Build Run!!!!!');
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: LayoutBuilder(
builder: (BuildContext context, BoxConstraints viewportConstraints) {
print('Layout Builder!!!');
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: IntrinsicHeight(
child: StreamBuilder<String>(
stream: _signupFormBloc.emailStream,
builder: (context, AsyncSnapshot<String> snapshot) {
return TextField(
onChanged: _signupFormBloc.onEmailChange,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'Email',
contentPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 18),
filled: true,
fillColor: Colors.white,
errorText: snapshot.error,
border: new OutlineInputBorder(
borderSide: BorderSide.none
),
),
);
}
),
),
),
);
},
)
);
}
#override
void dispose() {
_signupFormBloc?.dispose();
super.dispose();
}
}
class SignupFormBloc {
///
/// StreamControllers
///
BehaviorSubject<String> _emailController = BehaviorSubject<String>();
///
/// Stream with Validators
///
Observable<String> get emailStream => _emailController.stream.transform(StreamTransformer<String,String>.fromHandlers(handleData: (email, sink){
final RegExp emailExp = new RegExp(r"^[a-zA-Z0-9_.+-]+#[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$");
if (!emailExp.hasMatch(email) || email.isEmpty){
print('has error');
sink.addError('Email format is invalid');
} else {
sink.add(email);
}
}));
///
/// Sinks
///
Function(String) get onEmailChange => _emailController.sink.add;
void dispose() {
_emailController.close();
}
}
This happens because of a misuse of streams.
The culprit is this line:
Observable<String> get emailStream => _emailController.stream.transform(...);
The issue with this line is that it creates a new stream every time.
This means that bloc.emailStream == bloc.emailStream is actually false.
When combined with StreamBuilder, it means that every time something asks StreamBuilder to rebuild, the latter will restart the listening process from scratch.
Instead of a getter, you should create the stream once inside the constructor body of your BLoC:
class MyBloc {
StreamController _someController;
Stream foo;
MyBloc() {
foo = _someController.stream.transform(...);
}
}