How to pass generic provider to child? - flutter

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.

Related

Turning Flutter Arguments into Usable Variables on another dart file

My final goal is to have a userName variable from one dart file transferred to the other dart file.
Firebase Authentication will only take the email and password inputs so I need to pass the userName variable as an argument into another file that is called after the users email has been verified.
I have been trying to find videos and documentation online, most of what I found is trying to put the data into a list (which I would like to avoid). I don't understand the "this." getter function in flutter yet, I don't know if it's necessary to solve this problem. Let me know if there's anything I can clarify, I hope I'm overlooking something simple.
Dart File #1
onPressed: () => signUp(_email, _password).then((_) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => Verify(_userName)));
}),
Dart File #2
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
class Verify extends StatefulWidget {
const Verify(String _userName, {Key? key}) : super(key: key);
#override
_VerifyState createState() => _VerifyState();
}
class _VerifyState extends State<Verify> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child:
Text('Here is your variable $_userName please verify'),
),
);
}
I guess you're asking about passing arguments(any object) between different screens.
You can do this easily by passing it in RouteSettings, you can pass any object (String, int, map) and then fetch it in the build method of another Screen.
class MyWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return TextButton(
onPressed: () => Navigator.push(
context, MaterialPageRoute(
builder: (context) => HomeScreen(),
settings: const RouteSettings(arguments: 'username')),), //arguments
child: Text('Hello, World!',
style: Theme.of(context).textTheme.headline4,
),
);
}
}
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
final args = ModalRoute.of(context)!.settings.arguments as String; //arguments
return TextButton(
onPressed: () {},
child: Text(args,
style: Theme.of(context).textTheme.headline4,
),
);
}
}

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);

How to update the ParentWidget of ModalBottomSheet using setState() in 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.

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!!

How to share the bloc between contexts

I'm trying to access the bloc instance created near the root of my application after navigating to a new context with showDialog(). However, if I try getting the bloc like I usually do, by getting it from the context like _thisBlocInstance = BlocProvider.of<ThisBlocType>(context), I get an error that indicates there is no bloc provided in this context.
I assume this is because the showDialog() builder method assigns a new context to the widgets in the dialog that don't know about the Bloc I am trying to find, which was instantiated as soon as the user logs in:
#override
Widget build(BuildContext context) {
_authBloc = BlocProvider.of<AuthBloc>(context);
_accountBloc = AccountBloc(authBloc: _authBloc);
return BlocProvider(
bloc: _accountBloc,
....
There is a button in the corner that displays a dialog:
#override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.all(18.0),
child: FloatingActionButton(
onPressed: () => showDialog(
context: context,
builder: (newContext) => EventDialog(),
).then(
(val) => print(val)
),
child: Icon(Icons.add),
),
),
);
}
And in the EventDialog, I try to find the bloc with the context again:
#override
void build(BuildContext context) {
_accountBloc = BlocProvider.of<AccountBloc>(context);
_userMenuItems = _accountBloc.usersInAccount
.map((user) => DropdownMenuItem(
child: Text(user.userName),
value: user.userId,
))
.toList();
}
And this fails, with an error 'the getter bloc was called on null', or, there is no bloc of that type attached to this context.
Is there some way to access the bloc just from the context after using showDialog(), or otherwise navigating to a new context?
This is the bloc provider class:
import 'package:flutter/material.dart';
//This class is a generic bloc provider from https://www.didierboelens.com/2018/08/reactive-programming---streams---bloc/
//it allows easy access to the blocs by ancestor widgets and handles calling their dispose method
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
_BlocProviderState<T> createState() => _BlocProviderState<T>();
static T of<T extends BlocBase>(BuildContext context){
final type = _typeOf<BlocProvider<T>>();
BlocProvider<T> provider = context.ancestorWidgetOfExactType(type);
return provider.bloc;
}
static Type _typeOf<T>() => T;
}
class _BlocProviderState<T> extends State<BlocProvider<BlocBase>>{
#override
void dispose(){
widget.bloc.dispose();
super.dispose();
}
#override
Widget build(BuildContext context){
return widget.child;
}
}
abstract class BlocBase {
void dispose();
}
The best way I found to access the original bloc in a new context is by passing a reference to it to a new bloc that manages the logic of the new context. In order to keep the code modular, each bloc shouldn't control more than one page worth of logic, or one thing (e.g. log-in state of the user). So, when I create a new screen/context with showDialog(), I should also have a new bloc that deals with the logic in that screen. If I need a reference to the original bloc, I can pass it to the constructor of the new bloc via the dialog widget's constructor, so any information in the original bloc can still be accessed by the new bloc/context:
child: FloatingActionButton(
onPressed: () => showDialog(
context: context,
builder: (newContext) => NewEventDialog(
accountBloc: BlocProvider.of<AccountBloc>(context),
),
).then((event) => eventsBloc.addEvent(event)),
...
class NewEventDialog extends StatelessWidget {
final AccountBloc accountBloc;
NewEventBloc _newEventBloc;
NewEventDialog({this.accountBloc}) : assert(accountBloc != null);
#override
Widget build(BuildContext context) {
_newEventBloc = NewEventBloc(accountBloc: accountBloc);
return BlocProvider(
bloc: _newEventBloc,
...
The last answer is okay but it can be simplified, that is just transfering Bloc to its child widget.
#override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.all(18.0),
child: FloatingActionButton(
onPressed: () => showDialog(
context: context,
builder: (newContext) => EventDialog((
accountBloc: BlocProvider.of<AccountBloc>(context),
),
).then(
(val) => print(val)
),
child: Icon(Icons.add),
),
),
);
}
class NewEventDialog extends StatelessWidget {
final AccountBloc accountBloc;
NewEventDialog({this.accountBloc}) : assert(accountBloc != null);
#override
void build(BuildContext context) {
_accountBloc = accountBloc;
_userMenuItems = _accountBloc.usersInAccount
.map((user) => DropdownMenuItem(
child: Text(user.userName),
value: user.userId,
))
.toList();
}
So far I find this problem occurs when going to widget via page routing. We can transfer the Bloc widget to widget to avoid this problem.