Problems with flutter bloc/State Management-How to pass objects between pages? - flutter

My internship boss asked me to solve a problem which I am not being able to do so far.
I have been given an ready application. This is basically flattered attendance app.
Now let me tell you clearly where I'm facing the issue.When the user click the student button,It takes the student to a class page,And in the class pledge the user or the student has to select the class by onTap event-One tapped and selected the class.the You, the user, goes to another page where the user will need to tap again to finally go to the check in Page-meaing another OnTap event./
Now my boss wants to get rid of the two OnTap events in the in between.He wants me to directly grow from student button to Student Item Page without losing the contents of the object that was passed. Please note I just show the important codes here scrapping the UI codes because it will become long.Please see if you can understand the codes.
The app uses bloc/statemanagement system.
Student Button-From here starts the flow(only important codes here)
final User user;
SelectUser({this.user});
Expanded(
child: GestureDetector(
onTap: () {
final ClassroomRepository classroomRepository =
ClassroomRepository(
classroomApiClient: ClassroomApiClient(
httpClient: http.Client(),
),
);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BlocProvider(
create: (context) => ClassroomBloc(
classroomRepository: classroomRepository),
child: ClassList(user: this.user),//user object is passed to the ClassList
)));
},
child: Image.asset('assets/logos/studentbutton.png'),
)
The above is passing the user object to ClassList. Below is the full code for ClassList:
class ClassList extends StatelessWidget {
final User user ;
ClassList({this.user});
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: BlocBuilder<ClassroomBloc, ClassroomState>(
builder: (context, state) {
if (state is ClassroomEmpty) {
print('load');
BlocProvider.of<ClassroomBloc>(context)
.add(FetchClassroom(schoolId: this.user.id));
return Center(child: Text('Please Wait'));
} else if (state is ClassroomLoading) {
return Center(child: CircularProgressIndicator());
} else if (state is ClassroomLoaded) {
final classrooms = state.classrooms;
return ListView.builder(
itemCount: classrooms.length,
itemBuilder: (context, position) {
return ClassItem(classroom: classrooms[position]);//GENERATES UI WITH ONTAP EVENT IN THE NEXT PAGE.
},
);
} else {
return Text(
'Something went wrong!',
style: TextStyle(color: Colors.red),
);
}
},
),
));
}
}
Now , it will go to Class ItemPage to perform the first onTap. You can see in the above code(return ClassItem(classroom: classrooms[position]) passing classroom object from repo)
Below is the full code for Class Item Page. HERE , I AM TRYING TO GET RID OF THIS ON TAP BUTTON WITHOUT LOSING THE CONTENT OF THE OBJECT THAT IS PASSED BUT FAILED REPEATEDLY.
import 'package:attendanceapp/blocs/student/blocs.dart';
import 'package:attendanceapp/repositories/student/repositories.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:attendanceapp/models/classroom.dart';
import 'package:attendanceapp/widgets/student/student.dart';
class ClassItem extends StatelessWidget {
final Classroom classroom ;
ClassItem({this.classroom});
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: (){
final VisitorRepository studentRepository = VisitorRepository(
visitorApiClient: StudentApiClient(
httpClient: http.Client(),
),
);
Navigator.push(context,
MaterialPageRoute(builder: (context) =>
BlocProvider(
create: (context) => StudentBloc(studentRepository:studentRepository),
child: Student(classroom: classroom),
)
)
);
},//tapping on the card
child: Card(
margin: EdgeInsets.all(20),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Text(classroom.classroomName,style: TextStyle(fontSize: 30))
)
),
);
}
}
From ClassItem Page , we go to the StudentPage passing ClassRooms as object.//Shows the Appbar and generates UI by returning the next page. Now this is the Page I don’t want to show but only pass the objects.
class Student extends StatelessWidget {
final Classroom classroom ;
Student({this.classroom});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar(
backgroundColor: Colors.amber[600],
title: Text(classroom.classroomName),
),
body:
Center(
child: BlocBuilder<StudentBloc, StudentState>(
builder: (context, state) {
if (state is StudentEmpty) {
print(this.classroom);
BlocProvider.of<StudentBloc>(context).add(FetchStudent(classId: this.classroom.id));
return Center(child: Text('Please Wait'));
} else if (state is StudentLoading) {
return Center(child: CircularProgressIndicator());
} else if (state is StudentLoaded) {
final students = state.students;
return ListView.builder(
itemCount: students.length,
itemBuilder: (context, position) {
return StudentItem(student:students[position], classroom: classroom,);
},
);
} else {
return Text(
'Something went wrong!',
style: TextStyle(color: Colors.red),
);
}
},
),
));
}
}
Finally, we go from StudentButton Page to StudeDetails Page. Below IS THE SECOND onTap in the studentItem page which I want to get rid. But failed …I want to go From Student Button Page to STUDENT ITEM PAGE without two OnTap evens and Not losing the content of the object. I have been trying for 7 days,but failed. I told my internship boss it is possible because each student has a class but he said do it any how.
Below is the next page we were talking about in the previous page.
I just need to show StudentDetails page starting from studentButton without performing the two onTaps. I have struggling with this for about 7 days, but still couldn’t go from studentButton to studentDetails page skipping the intermittent pages.
class StudentItem extends StatelessWidget {
final Student student;
final Classroom classroom;
StudentItem({this.student, this.classroom});
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
final StudentAttendanceRepository attendanceRepository =
StudentAttendanceRepository(
studentAttendanceClient: StudentAttendanceClient(
httpClient: http.Client(),
),
);
Navigator.push(
context,
MaterialPageRoute(
settings: RouteSettings(name: '/studentDetail'),
builder: (context) => BlocProvider(
create: (context) => StudentAttendanceBloc(studentAttendanceRepository: attendanceRepository),
child: StudentDetail(student: student,),
),
),
).then((value){
if(value){
BlocProvider.of<StudentBloc>(context).add(FetchStudent(classId: this.classroom.id));
}
});
},
////UI CODES HERE…NOT PASTED HERE BECAUSE TOO LONG
}

So to access the data on different pages, you can pass the data to your event from page 1 and then in the bloc, you can assign this data to one of the state class. Now on page 2 or page 3, wherever you want to access the data, you can use BlocListener or BlocBuilder and access the data using the state object.

Related

How can I use multiple cubits and emit states from both in the same screen?

I need to rebuild the body of the home screen when I add new task to get all the tasks including the last task added, I tried many solutions from many resources but I didn't find an solution.
here the home screen widget which build list view of all tasks.
Home screen widget
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocConsumer<TasksCubit, TasksStates>(
listener: (context, state) {},
builder: (context, state) {
TasksCubit cubit = TasksCubit.get(context);
return Scaffold(
body: ListView.separated(
itemBuilder: (context, index) {
return BuildTaskItem(
task: cubit.tasksList[index],
);
},
separatorBuilder: (context, index) => Divider(
thickness: 1,
),
itemCount: cubit.tasksList.length,
),
);
},
);
}
}
that the add screen which I can add task using insert into database(), when I add task and hit the button which trigger the function, it emits the add task success state and emits get tasks success but it isn't refresh the home screen with the new task
add task widget
Widget build(BuildContext context) {
return BlocConsumer<TasksOperationsCubit, TasksOperationsStates>(
listener: (context, state) {},
builder: (context, state) {
TasksOperationsCubit cubit = TasksOperationsCubit.get(context);
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(20.0),
child: SingleChildScrollView(
child: Form(
key: formKey,
child: Column(
children: [
defaultMaterialButton(
color: Colors.pink,
text: 'Add Task',
textTheme: ButtonTextTheme.primary,
height: 50.0,
textSize: 25.0,
minWidth: double.infinity,
pressed: () {
if (formKey.currentState!.validate()) {
TaskEntity task = TaskEntity(
task: titleController.text,
date: dateController.text,
time: timeController.text,
piriority: piriorityController.text,
category: category,
);
cubit.insertIntoDatabase(task);
}
},
here the method that I add task into the database from the tasks operations cubit
tasks operations cubit
class TasksOperationsCubit extends Cubit<TasksOperationsStates> {
TasksCubit tasksCubit;
static TasksOperationsCubit get(context) => BlocProvider.of(context);
void insertIntoDatabase(TaskEntity task) {
insertTaskUsecase.todoRepository.insertTaskIntoDatabase(task).then((value) {
value.fold((failure) {
emit(InsertIntoDatabaseErrorState(error: failure.toString()));
}, (unit) {
emit(InsertIntoDatabaseSuccessState());
tasksCubit.getAllTasks();
});
});
}
}
here the get all tasks method form the the tasks cubit
tasks cubit
class TasksCubit extends Cubit<TasksStates> {
static TasksCubit get(context) => BlocProvider.of(context);
List<TaskEntity> tasksList = [];
List<TaskEntity> getAllTasks() {
LoadingGetAllTasksState();
getAllTasksUsecase.todoRepository.getAllTasksFromDatabase().then((value) {
value.fold((failure) {
emit(GetAllTasksErrorState(error: failure.toString()));
}, (tasks) {
tasksList = List.from(tasks);
print(tasks.toString());
emit(GetAllTasksSuccessState());
});
});
return tasksList;
}
}
I wish someone can help me finding a solution, and thanks all.
emit a cubit event from task adding widget after task is added, listen to that state and build home screen using that, make sure state is changing, if state is not changing create a dummy state and switch between state.
use listenWhen and buildWhen of bloc consumer in home widget for more convenience.
I am tried so many times until finally I found the answer
all what I had to do just nest the bloc builder method at the home screen and create the proper implementations.
and here what I did
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: _buildAppbar(context),
body: BlocConsumer<TasksCubit, TasksStates>(
listener: (context, state) {},
builder: (context, state) {
return BlocConsumer<TasksOperationsCubit, TasksOperationsStates>(
listener: (operationsContext, operationsState) {
if (operationsState is InsertIntoDatabaseSuccessState ||
operationsState is DeleteTaskItemSuccessState ||
operationsState is UpdateTaskItemSuccessState) {
TasksCubit.get(context).getAllTasks();
}
},
builder: (operationsContext, operationsState) {
return _buildTasks(state.tasks);
},
);
},
),
);
}

How to automatically show an alert dialog without pressing a button in Flutter?

I implemented the alert dialog in the initstate() method but Init state is only called once. In my case I want the alert to appear automatically every time a variable value changes for exemple. ( I need it to suddenly pop up during using the app)
You could use a ValueNotifier and a ValueListenableBuilder so that every time the value in the ValueNotifier changes, the ValueListenableBuilder rebuilds and shows a dialog, like so:
class MyWidget extends StatelessWidget {
ValueNotifier<int> dialogTrigger = ValueNotifier(0);
#override
Widget build(BuildContext context) {
return Column(
children: [
TextButton(
onPressed: () {
var random = Random();
dialogTrigger.value = random.nextInt(100);
},
child: const Text('Click me and change a value')
),
Expanded(
child: ValueListenableBuilder(
valueListenable: dialogTrigger,
builder: (ctx, value, child) {
Future.delayed(const Duration(seconds: 0), () {
showDialog(
context: ctx,
builder: (ctx) {
return AlertDialog(
title: const Text('Dialog'),
content: Text('Hey! I got a $value'),
);
});
});
return const SizedBox();
})
)
]
);
}
}
(Again, I'm only adding a button to change the value, not to launch the dialog. That way you can programmatically change the value, which eventually launches the dialog). See if that works for your purposes.

how to implement scoped model to navigation screen when scoped model initialized in the previous screen and not in myApp()

im trying to use scoped model in 2 screens in the app and i dont want to run the app with scoped model, i just initialize scoped model in first screen and it worked but i got an error in the second screen where i navigate to it. so what do i do?
first i call the first screen from the pre screen like this
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ServiceDetails(model: new CartModel()),
)
);
then in ServiceDetails i didnt get any error
so in build widget
#override
Widget build(BuildContext context) {
return ScopedModel(
model: widget.model,
child: Scaffold(...)
);
}
and i have a cart button which on tap:
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => new Cart()
),
);
},
Cart class:
class Cart extends StatefulWidget {
#override
_CartState createState() => _CartState();
}
class _CartState extends State<Cart> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Your Cart")),
body: ScopedModel.of<CartModel>(context, rebuildOnChange: true).cart.length == 0 ?
Container(
alignment: Alignment.center,
child: Text("Your cart is empty!",style: new TextStyle(color: Colors.grey))
) :
Container(
padding: EdgeInsets.only(top:15),
child: cartListView()
)
);
}
Widget cartListView(){
return ScopedModelDescendant<CartModel>(
builder: (context, child, model) {
return ListView.builder(
shrinkWrap: true,
itemCount: ScopedModel.of<CartModel>(context,rebuildOnChange: true).total,
itemBuilder: (context, index) {
return Container(
child: Image.asset(model.cart[index].fs.image)
)
}
)})}
}
so when i enter cart page i got an error
can't find the correct scoped model
anyone help plz

How to pass a context.read to children widgets in Flutter? Potential BuildContext error issue

Background
I am trying to create a refresh button that floats on the right side of the screen that will refresh a listview
My Code
I am using the following class that will pull JSON data and create a listview for me via context.read<ThreadData>().fetchData; on first load.
class OpenThread extends StatelessWidget{
#override
Widget build(BuildContext context) {
context.read<ThreadData>().fetchData;
return Scaffold(
appBar: AppBar(...)
body: (...)
child:
IconButton(onPressed: () {
debugPrint('Pressed');
context.read<ThreadData>().fetchData;
}, icon: Icon(Icons.refresh))
);
}
}
In there is a reload button IconButton() which when pressed is perfectly reloading my data onPressed: () { debugPrint('Pressed'); context.read<ThreadData>().fetchData; }
My Problem
However, I've created a child widget called ThreadPageNav which I've added as a child to my body in the above OpenThread class as follows
Stack(
children: [
Container(...)
ThreadPageNav(),
The ThreadPageNav() widget looks like this
class ThreadPageNav extends StatelessWidget {
#override
Widget build(BuildContext context) {
return
Positioned( ...
child:
IconButton(onPressed: () {
debugPrint('Pressed');
context.read<ThreadData>().fetchData;
}, icon: Icon(Icons.refresh))
);
}
}
However, here the context.read<ThreadData>().fetchData; call doesn't work at all
I immediately get an error: The method 'read' isn't defined for the type 'BuildContext'.
In case it matters
Just in case it matters, the OpenThread and ThreadData are originally setup from a navigator push, with ThreadData doing the JSON request and data collection and OpenThread creating the listview and styles and displaying it on the screen as follows:
Navigator.push(
context,
new MaterialPageRoute(
builder: (context) => ChangeNotifierProvider(
create: (context) => ThreadData(
tVars: ThreadVars(
threadid: map['threadid'],
threadtitle: map['title'])
),
builder: (context, child)
{
return OpenThread(
tVars: ThreadVars(
threadid: map['threadid'],
threadtitle: map['title'])
);
},
),
)
);
Well #pskink nailed it above in the comments
I'm leaving this here just incase someone else stumbles on this problem and needs a solution
The answer is that I didn't include the ReadContext extension
All I had to do was add import 'package:provider/provider.dart'; at the top of my .dart file and it works perfectly

How to use provider to create a commit/discard changes pattern?

What would be a best practice for (provider based) state management of modal widgets in flutter, where when user makes an edit changes do not propagate to parent page until user confirms/closes modal widget. Optionally, user has a choice to discard the changes.
In a nutshell:
modal widget with OK and cancel actions, or
modal widget where changes are applied when modal is closed
Currently, my solution looks like this
Create a copy of the current state
Call flutter's show___() function and wrap widgets with a provider (using .value constructor) to expose copy of the state
If needed, update original state when modal widget is closed
Example of case #2:
Future<void> showEditDialog() async {
// Create a copy of the current state
final orgState = context.read<MeState>();
final tmpState = MeState.from(orgState);
// show modal widget with new provider
await showDialog<void>(
context: context,
builder: (_) => ChangeNotifierProvider<MeState>.value(
value: tmpState,
builder: (context, _) => _buildEditDialogWidgets(context)),
);
// update original state (no discard option to keep it simple)
orgState.update(tmpState);
}
But there are issues with this, like:
Where should I dispose tmpState?
ProxyProvider doesn't have .value constructor.
If temporary state is created in Provider's create: instead, how can I safely access that temporary state when modal is closed?
UPDATE: In my current app I have a MultiProvider widget at the top of widget tree, that creates and provides multiple filter state objects. Eg. FooFiltersState, BarFiltersState and BazFiltersState. They are separate classes because each these three extends either ToggleableCollection<T> extends ChangeNotifier or ToggleableCollectionPickerState<T> extends ToggleableCollection<T> class. An abstract base classes with common properties and functions (like bool areAllSelected(), toggleAllSelection() etc.).
There is also FiltersState extends ChangeNotifier class that contains among other things activeFiltersCount, a value depended on Foo, Bar and Baz filters state. That's why I use
ChangeNotifierProxyProvider3<
FooFiltersState,
BarFilterState,
BazFilterState,
FiltersState>
to provide FiltersState instance.
User can edit these filters by opening modal bottom sheet, but changes to filters must not be reflected in the app until bottom sheet is closed by taping on the scrim. Changes are visible on the bottom sheet while editing.
Foo filters are displayed as chips on the bottom sheet. Bar and baz filters are edited inside a nested dialog windows (opened from the bottom sheet). While Bar or Baz filter collection is edited, changes must be reflected only inside the nested dialog window. When nested dialog is confirmed changes are now reflected on bottom sheet. If nested dialog is canceled changes are not transferred to the bottom sheet. Same as before, these changes are not visible inside the app until the bottom sheet is closed.
To avoid unnecessary widget rebuilds, Selector widgets are used to display filter values.
From discussion with yellowgray, I think that I should move all non-dependent values out of proxy provider. So that, temp proxy provider can create new temp state object that is completely independent of original state object. While for other objects temp states are build from original states and passed to value constructors like in the above example.
1. Where should I dispose tmpState?
I think for your case, you don't need to worry about it. tmpState is like a temporary variabl inside function showEditDialog()
2. ProxyProvider doesn't have .value constructor.
It doesn't need to because it already is. ProxyProvider<T, R>: T is a provider that need to listen to. In your case it is the orgState. But I think the orgState won't change the value outside of this function, so I don't know why you need it.
3. If temporary state is created in Provider's create: instead, how can I safely access that temporary state when modal is closed?
you can still access the orgState inside _buildEditDialogWidgets and update it by context.read(). But I think you shouldn't use same type twice in the same provider tree (MeState)
Actually when I first see your code, I will think why you need to wrap tmpState as another provider (your _buildEditDialogWidgets contains more complicated sub-tree or something else that need to use the value in many different widget?). Here is the simpler version I can think of.
Future<void> showEditDialog() async {
// Create a copy of the current state
final orgState = context.read<MeState>();
// show modal widget with new provider
await showDialog<void>(
context: context,
builder: (_) => _buildEditDialogWidgets(context,MeState.from(orgState)),
);
}
...
Widget _buildEditDialogWidgets(context, model){
...
onSubmit(){
context.read<MeState>().update(updatedModel)
}
...
}
The simplest way is you can just provide a result when you pop your dialog and use that result when updating your provider.
import 'dart:collection';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
class Item {
Item(this.name);
String name;
Item clone() => Item(name);
}
class MyState extends ChangeNotifier {
List<Item> _items = <Item>[];
UnmodifiableListView<Item> get items => UnmodifiableListView<Item>(_items);
void add(Item item) {
if (item == null) {
return;
}
_items.add(item);
notifyListeners();
}
void update(Item oldItem, Item newItem) {
final int indexOfItem = _items.indexOf(oldItem);
if (newItem == null || indexOfItem < 0) {
return;
}
_items[indexOfItem] = newItem;
notifyListeners();
}
}
class MyApp extends StatelessWidget {
#override
Widget build(_) {
return ChangeNotifierProvider<MyState>(
create: (_) => MyState(),
builder: (_, __) => MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Builder(
builder: (BuildContext context) => Scaffold(
body: SafeArea(
child: Column(
children: <Widget>[
FlatButton(
onPressed: () => _addItem(context),
child: const Text('Add'),
),
Expanded(
child: Consumer<MyState>(
builder: (_, MyState state, __) {
final List<Item> items = state.items;
return ListView.builder(
itemCount: items.length,
itemBuilder: (_, int index) => GestureDetector(
onTap: () => _updateItem(context, items[index]),
child: ListTile(
title: Text(items[index].name),
),
),
);
},
),
),
],
),
),
),
),
),
);
}
Future<void> _addItem(BuildContext context) async {
final Item item = await showDialog<Item>(
context: context,
builder: (BuildContext context2) => AlertDialog(
actions: <Widget>[
FlatButton(
onPressed: () => Navigator.pop(context2),
child: const Text('Cancel'),
),
FlatButton(
onPressed: () => Navigator.pop(
context2,
Item('New Item ${Random().nextInt(100)}'),
),
child: const Text('ADD'),
),
],
),
);
Provider.of<MyState>(context, listen: false).add(item);
}
Future<void> _updateItem(BuildContext context, Item item) async {
final Item updatedItem = item.clone();
final Item tempItem = await showModalBottomSheet<Item>(
context: context,
builder: (_) {
final TextEditingController controller = TextEditingController();
controller.text = updatedItem.name;
return Container(
height: 300,
child: Column(
children: <Widget>[
Text('Original: ${item.name}'),
TextField(
controller: controller,
enabled: false,
),
TextButton(
onPressed: () {
updatedItem.name = 'New Item ${Random().nextInt(100)}';
controller.text = updatedItem.name;
},
child: const Text('Change name'),
),
TextButton(
onPressed: () => Navigator.pop(context, updatedItem),
child: const Text('UPDATE'),
),
TextButton(
onPressed: () => Navigator.pop(context, Item(null)),
child: const Text('Cancel'),
),
],
),
);
},
);
if (tempItem != null && tempItem != updatedItem) {
// Do not update if "Cancel" is pressed.
return;
}
// Update if "UPDATE" is pressed or dimissed.
Provider.of<MyState>(context, listen: false).update(item, updatedItem);
}
}