Migrate ChangeNotifier from provider to hooks_riverpod - flutter

I want to move my entire project from provider to riverpod. But I’m stuck at this point.
class EditQuestionScreen extends StatelessWidget {
EditQuestionScreen({this.question, this.answers});
final QuestionModel question;
final List<AnswerModel> answers;
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => QuestionProvider(
question: this.question,
answers: this.answers),
child: Container());
}
}
This is my provider widget for subwidgets. It's initialized only once. How can I write this class as a HookWidget with riverpod?

Quick note - with Provider or Riverpod, you don't really want/need to name the thing you are providing thingProvider. You aren't providing a provider, you're providing a thing if that makes sense.
I did my best to fill in the gaps for the rest of the code you didn't provide, so hopefully, this will help:
class QuestionModel {
QuestionModel(this.id);
final int id;
}
class AnswerModel {
AnswerModel(this.id);
final int id;
}
class QuestionWithAnswers {
QuestionWithAnswers(this.question, this.answers);
final QuestionModel question;
final List<AnswerModel> answers;
}
class QuestionAnswerNotifier extends ChangeNotifier {
QuestionAnswerNotifier(this.qwa);
final QuestionWithAnswers qwa;
QuestionModel get question => qwa.question;
List<AnswerModel> get answers => qwa.answers;
addAnswer(AnswerModel answer) {
qwa.answers.add(answer);
notifyListeners();
}
}
final questionProvider =
ChangeNotifierProvider.family<QuestionAnswerNotifier, QuestionWithAnswers>(
(ref, qwa) => QuestionAnswerNotifier(qwa));
class EditQuestionScreen extends HookWidget {
EditQuestionScreen({
#required QuestionModel question,
#required List<AnswerModel> answers,
Key key,
}) : qwa = QuestionWithAnswers(question, answers),
super(key: key);
final QuestionWithAnswers qwa;
#override
Widget build(BuildContext context) {
final provider = useProvider(questionProvider(qwa));
// Use data from provider to render your UI, for example:
return Container(
child: Column(
children: <Widget>[
Text('${provider.question}\n${provider.answers}'),
RaisedButton(
onPressed: () => provider.addAnswer(AnswerModel(5)),
child: Icon(Icons.add),
)
],
),
);
}
}
There are a few things to note here.
In Riverpod, family is how we pass parameters to providers.
QuestionWithAnswers class bundles the models you want to pass to the provider through the extra parameter provided by family.
The provider's name is suffixed with Provider, rather than the thing it is providing being named as such.
We are providing the ChangeNotifier, so that is what is returned when called useProvider.

Related

How do I set my state when the widget builds with riverpod?

To preface, I am completely new to riverpod so my apologies if I get some terminology wrong.
I have an edit feature that I'm implementing with riverpod that I'm migrating over to from flutter_blocs. Basically, with the bloc implementation, I am fetching my data from my server and when the widget builds, the BlocConsumer will emit an event to set my data from the server into my bloc state so that I can display and edit it in a TextInput.
Bloc implementation:
BlocConsumer<JournalBloc, JournalState>(
bloc: BlocProvider.of<JournalBloc>(context)
..add(
SetModelState(
title: journalEntry.title,
rating: journalEntry.rating.toDouble(),
),
),
builder: (context, state) {
return Column(
children: [
TextInput(
labelText: 'label',
value: state.editEntryTitle.value,
onChanged: (title) => context.read<JournalBloc>().add(EditJournalEntryTitleChanged(title: title))
)
],
);
}
Now with Riverpod, where I'm stuck on is I don't know how to set my values from my server into my state when the widget renders. I have my controller, and I have my providers.
My controller:
class EditJournalEntryController extends StateNotifier<EditJournalEntryState> {
final JournalService journalService;
EditJournalEntryController({required this.journalService})
: super(EditJournalEntryState());
void setSelectedJournalType(JournalType journalType) {
state = state.copyWith(selectedJournalType: journalType);
}
void setRating(int rating) {
state = state.copyWith(rating: rating);
}
}
final editJournalEntryController =
StateNotifierProvider<EditJournalEntryController, EditJournalEntryState>((ref) {
final journalService = ref.watch(journalServiceProvider);
return EditJournalEntryController(journalService: journalService);
});
My state:
class EditJournalEntryState implements Equatable {
final AsyncValue<void> value;
final JournalType? selectedJournalType;
final int? rating;
bool get isLoading => value.isLoading;
EditJournalEntryState({
this.selectedJournalType,
this.rating,
this.value = const AsyncValue.data(null),
});
EditJournalEntryState copyWith({
MealType? selectedJournalType,
int? rating,
AsyncValue? value,
}) {
return EditJournalEntryState(
selectedJournalType: selectedJournalType ?? this.selectedJournalType,
rating: rating ?? this.rating,
value: value ?? this.value,
);
}
#override
List<Object?> get props => [selectedJournalType, rating, value];
#override
bool? get stringify => true;
}
What I have tried
In my UI, I am referencing my controller and then using my notifier to set my state:
class EditJournal extends StatelessWidget {
const EditFoodJournal({
Key? key,
required this.journalEntry, // coming from server in a few parents above
}) : super(key: key);
final JournalEntry journalEntry;
#override
Widget build(BuildContext context) {
return Consumer(
builder: ((context, ref, child) {
final state = ref.watch(editJournalEntryController);
final notifier = ref.read(editJournalEntryController.notifier);
notifier.setSelectedJournalType(journalEntry.type)
notifier.setRating(journalEntry.rating)
But for obvious reasons I get this error:
At least listener of the StateNotifier Instance of 'EditFoodJournalEntryController' threw an exception
when the notifier tried to update its state.
The exceptions thrown are:
Tried to modify a provider while the widget tree was building.
If you are encountering this error, chances are you tried to modify a provider
in a widget life-cycle, such as but not limited to:
- build
- initState
- dispose
- didUpdateWidget
- didChangeDepedencies
Modifying a provider inside those life-cycles is not allowed, as it could
lead to an inconsistent UI state. For example, two widgets could listen to the
same provider, but incorrectly receive different states.
To fix this problem, you have one of two solutions:
- (preferred) Move the logic for modifying your provider outside of a widget
life-cycle. For example, maybe you could update your provider inside a button's
onPressed instead.
- Delay your modification, such as by encasuplating the modification
in a `Future(() {...})`.
This will perform your upddate after the widget tree is done building.
Ideally I want to render that value from state and not from the variable I have.
I feel like I've ran into a wall. Is there an ideal way of handling with this?
you are trying to modify a provider while the widget tree is building.
final notifier = ref.read(editJournalEntryController.notifier);
notifier.setSelectedJournalType(journalEntry.type)//here
notifier.setRating(journalEntry.rating)//here
this will cause the issue, you need to initialize the editJournalEntryController with an initial state you can do it by .family provider modifier
Edit:
Something like this
final editJournalEntryController = StateNotifierProvider.family<
EditJournalEntryController,
EditJournalEntryState,
JournalEntry>((ref, journalEntry) {
final journalService = ref.watch(journalServiceProvider);
return EditJournalEntryController(journalService: journalService,journalentry: journalEntry);
});
EditJournalEntryController will be
class EditJournalEntryController extends StateNotifier<EditJournalEntryState> {
final JournalService journalService;
final JournalEntry journalentry;
EditJournalEntryController({required this.journalService,required this.journalentry})
: super(EditJournalEntryState(selectedJournalType: journalentry.type,rating: journalentry.rating));
void setSelectedJournalType(String journalType) {
state = state.copyWith(selectedJournalType: journalType);
}
void setRating(int rating) {
state = state.copyWith(rating: rating);
}
}
will be called like
class EditFoodJournal extends StatelessWidget {
const EditFoodJournal({
Key? key,
required this.journalEntry, // coming from server in a few parents above
}) : super(key: key);
final JournalEntry journalEntry;
#override
Widget build(BuildContext context) {
return Consumer(builder: (context, ref, child) {
final state = ref.watch(editJournalEntryController(journalEntry));
return Container();
});
}
}

Read nested widget/class properties value in flutter

I'm building a simple app with lots of nested widgets/classes from different specialised files
list of files:
main.dart -> the menu file used to start the activity
"Activity()"
group_widgets.dart -> the file that contains the custom widget
"CustomWidget()"
file_a.dart -> the file that uses the custom widgets
inside the "Activity()"
other.dart -> other files that needs to manage data changed in CustomWidget()
inside main.dart:
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const Activity(),
));
},
inside group_widgets.dart:
class CustomWidget extends StatefulWidget {
const CustomWidget({Key? key}) : super(key: key);
#override
State<CustomWidget> createState() => _CustomWidgetState();
}
class _CustomWidgetState extends State<CustomWidget> {
var _boolean = false;
bool switchBoolean(bool state) => !state;
#override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => {
setState(() {
_boolean = switchBoolean(_boolean);
})
},
child: Container(
color: _boolean == true ? Colors.green : Colors.red,
),
);
}
}
inside file_a.dart
class Activity extends StatefulWidget {
const Activity({Key? key}) : super(key: key);
#override
State<Activity> createState() => _ActivityState();
}
class _ActivityState extends State<Activity> {
#override
Widget build(BuildContext context) {
bool boolean = true;
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
CustomWidget(),
Text('Here where to show the variable from CustomWidget'
'and prove I can retrieve it')
],
),
),
);
}
}
inside other.dart
if ( booleanFromCustomWidget == true) {
Something ...
}
What is the best practice to achieve it?
I've read a lot here but nothing seems to well fit my needing.
Just comment if my request is not as clear as it seems to me))
Please correct me if I am wrong, but if you want to access data from parent widgets from inside their descendants (children or even nested children) you can either pass them down via parameter arguments:
Child(int age, String name);
And then accept it in the new file, where the Child widget lives, via its constructor:
class Child {
String name;
int age;
// Constructor
Child(String passedName, int passedAge) {
this.name = passedName;
this.age = passedAge;
}
}
Inside the parent.dart you then have to import the children.dart to use it.
Or use a popular package like the provider package: https://pub.dev/packages/provider
This allows you to store data containers, which you can access basically anywhere in your code. Feel free to google it & watch some tutorials to get started, as it is the preferred approach to avoid passing data to widget which really do not care about the passed parameters.
Note: You can transfer the idea to output the String data like in your example code above.
you can use a state manager like provider, or bloc
At the top level, you set up the data services

Storing certain value in Widget build / Flutter

I've a question:
In my Widget build(BuildContext context), I want to store a certain value,
final userName = book.owner
(book is the reference to the certain value from Firestore)
But it's done not in the right way to my lack of knowledge. I'd appreciate if someone could guide through that.
Thank you in advance!
Snippet of my code
class BookView extends StatefulWidget {
final Book book;
BookView({Key key, #required this.book}) : super(key: key);
DatabaseMethods databaseMethods = new DatabaseMethods();
var userName;
#override
_BookViewState createState() => _BookViewState(book);
}
class _BookViewState extends State<BookView> {
Book book;
_BookViewState(this.book);
String userName;
#override
void initState() {
userName = book.owner;
super.initState();
}
// final Book book;
createChatroomAndStartConversation({var userName}) {
if (userName != Constants.myName) {
String roomId = getChatRoomId(userName, Constants.myName);
List<String> users = [userName, Constants.myName];
Map<String, dynamic> chatRoomMap = {
"Users": users,
"roomId": roomId,
};
DatabaseMethods().createChatRoom(roomId, chatRoomMap);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ConversationScreen(roomId, userName)),
);
} else {
print("You cannot send msg to your self");
}
}
#override
Widget build(BuildContext context) {
//widget.book;
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
...
FlatButton(
child: Text(
"Get contact with",
style: TextStyle(color: Colors.white),
),
color: Colors.blue,
onPressed: () {
createChatroomAndStartConversation(
userName: userName);
...
}
Snippet of Value not in range: 1
getChatRoomId(String a, String b) {
if (a.substring(0, 1).codeUnitAt(0) > b.substring(0, 1).codeUnitAt(0)) {
return "$b\_$a";
} else {
return "$a\_$b";
}
}
It's not a good practice to store any data in build() method, because this method is invoked too many times to do the such kind of move. Consider using StatefulWidget to store any state you have in the widget, for the very beginning. When you use this widget, you can define this all in such way:
class YourWidget extends StatefulWidget {
#override
_YourWidgetState createState() => _YourWidgetState();
}
class _YourWidgetState extends State<YourWidget> {
String userName;
#override
void initState() {
userName = book.owner;
super.initState()
}
#override
Widget build(BuildContext context) {
return Container(child: Text(userName),);
}
}
Here, in initState() you can retrieve value from book and set it to userName. But for more complex and bigger applications, consider using StateManagement solutions and some kind of architectural patterns i.e. Riverpod, Provider, MobX, BLoC.. Because changing the state via setState() method will cause rebuilding whole child widget tree, which could freeze whole UI in complex app.
UPD to 'Snippet of my code':
According to your code, if you are using a 'book' from Widget, not its state - use widget.book, in such way you have access to widget members, because of this you don't need a constructor of state. So, due to these changes, your code might looks like:
class BookView extends StatefulWidget {
final Book book;
BookView({Key key, #required this.book}) : super(key: key);
// You DON'T need this here, because you are retrieving these methods
// inside your state via DatabaseMethods constructor
DatabaseMethods databaseMethods = DatabaseMethods();
#override
_BookViewState createState() => _BookViewState(book);
}
class _BookViewState extends State<BookView> {
String userName;
#override
void initState() {
// Using widget.book to retrieve Book object from state's widget
userName = widget.book.owner;
super.initState();
}
createChatroomAndStartConversation({var userName}) {
if (userName != Constants.myName) {
String roomId = getChatRoomId(userName, Constants.myName);
// Also, it's just a recommendation, try to omit local variables types
// because they are already known with List type (String). Also, this
// all is about chatRoomMap
var users = <String>[userName, Constants.myName];
final chatRoomMap = <String, dynamic>{
"Users": users,
"roomId": roomId,
};
DatabaseMethods().createChatRoom(roomId, chatRoomMap);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ConversationScreen(roomId, userName)),
);
} else {
print("You cannot send msg to your self");
}
}
#override
Widget build(BuildContext context) {
// your widgets here
}
}
UPD 2:
Second trouble and issue with 'Snippet of Value not in range: 1'. I could to reproduce it with given value of 'a' as empty string. So, your function invocation is like getChatRoomId('', 'user123'), because of empty 'userName', substring function can't take values from range [0, 1), so exception is raised.

keeping repository instance "alive" with bloc

I am still with my first bloc based app, adding features. While previously, I stored some of my page specific data with the bloc class, for the last feature, I now moved most variables into its repository. I already feared that the instance of calling the repository gets lost, afterwards, which now proved true.
Is there a proper, easy way to make the instance persistent?
I know of inherited widgets, however, I have not yet figured out how to implement this and my question around this unfortunately remained unanswered. It would be great, if someone could point me to some direction!
In general, my idea was to have the api dealing with local files and online data, the repository with frequently re-used data (session data, presented data etc) and helper variables within the bloc. So when the UI requests data, the bloc asks the repository which will either return a value stored in a variable or request a value from the api.
This is, how the strucuture basically looks like (hope I have not missed anything significant)
void main() async {
final UserRepository userRepository = UserRepository(); // <===== userRepository initialized
runApp(MyApp(userRepository: UserRepository()));
}
class MyApp extends StatelessWidget {
MyApp({Key key, this.userRepository}) : assert(userRepository != null), super(key: key);
final UserRepository userRepository;
#override
Widget build(BuildContext context) {
return BlocProvider<UserBloc>( <====== userBloc injection to top of widget tree
create: (_) => UserBloc(userRepository: userRepository)..add(AppStarted()),
child: App(),
);
}
}
// =================================================== APP WITH ROUTES
class App extends StatelessWidget {
App({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return CupertinoApp(
routes: {
'/': (_) => HomePage(),
'feature 1': (_) => HomePage(),
},
);
}
}
// =================================================== LANDING PAGE WITH MAIN MENU
class HomePage extends StatefulWidget {
HomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
]);
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text('MathUup'),
),
child: SafeArea(
child: CupertinoButton(
child: Text('Feature 1',
onPressed: () => Navigator.pushNamed(context, 'feature 1'),
),)));
}}
// =================================================== BLOC
class UserBloc extends Bloc<UserEvent, UserState> {
UserBloc({this.userRepository}) : super(AppInitial());
final UserRepository userRepository;
...
final user = await userRepository.getActiveUserData(userId);
final lastSessionData = await userRepository.getLastSession(userId);
...
}
// =================================================== REPOSITORY
class UserRepository {
UserRepository();
final UserApiClient achievementsApiClient = UserApiClient();
final SessionsApiClient sessionsApiClient = SessionsApiClient();
UserSession activeUserSession;
User activeUserData;
Future<String> getLastUserId() async {
final lastUserId = await sessionsApiClient.getLastUserId();
return lastUserId;
}
Future<UserSession> getActiveUser() async {
if (activeUserSession == null) {
activeUserSession = await sessionsApiClient.getLastUser();
}
return activeUserSession;
}
}
This line is creating and initializing your user repository:
final UserRepository userRepository = UserRepository(); // <===== userRepository initialized
However, this line is not passing that repository, it's creating a new repository, ignoring the one you just initialized:
runApp(MyApp(userRepository: UserRepository()));
I think you meant to use the variable you already have:
runApp(MyApp(userRepository: userRepository));

Riverpod : Alternate way of overriding initState inside ConsumerWidget

What is the solution of initializing things inside consumerWidget as because the initState method is not overridable here?
Riverpod v2.1.3
You can use ConsumerStatefulWidget and ConsumerState
final helloWorldProvider = Provider((_) => 'Hello world');
class RiverpodExample extends ConsumerStatefulWidget {
const RiverpodExample({super.key});
#override
ConsumerState<RiverpodExample> createState() => _RiverpodExampleState();
}
class _RiverpodExampleState extends ConsumerState<RiverpodExample> {
#override
void initState() {
super.initState();
final value = ref.read(helloWorldProvider);
print(value); // Hello world
}
#override
Widget build(BuildContext context) {
final value = ref.watch(helloWorldProvider);
return Text(value); // Hello world
}
}
I'm not totally sure how to answer your question as I have not worked with ConsumerWidget. I'd presume the idea is to keep most of your state in providers.
However, I would like to recommend using hooks_riverpod alongside flutter_hooks (same developer).
This makes keeping state local to the widget simple and also provides easy access to providers.
For example:
class Example extends HookWidget {
const Example({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
final test = useProvider(Test.provider());
final controller = useTextEditingController();
final loading = useState(false);
final buttonText = useState('Change me!');
return Column(
children: [
TextField(controller: controller),
if (!loading) RaisedButton(
onPressed: () async {
loading.value = true;
await Future.delayed(const Duration(seconds: 1));
buttonText.value = controller.text;
loading.value = false;
}
child: Text(buttonText.value),
),
if (loading) const CircularProgressIndicator(),
// Do something with providers, etc.
],
),
);
}
Just a quick example, but there are plenty of resources (flutter_hooks, hooks_riverpod) to help you along. Also, check out examples from the developer on riverpod hooks usage.
I may be late but with the upcoming 1.0.0 of Riverpod you will be able to use https://pub.dev/documentation/flutter_riverpod/1.0.0-dev.2/flutter_riverpod/ConsumerStatefulWidget-class.html and https://pub.dev/documentation/flutter_riverpod/1.0.0-dev.2/flutter_riverpod/ConsumerState-class.html, which does exactly what you want.