Flutter: Update class variable when global variable changes with MVVM - flutter

I have my app setup in such a way that a SessionRepository provider wraps the MaterialApp. This repository allows me to track data about the session throughout the app.
#override
Widget build(BuildContext context) {
return Provider<SessionRepository>(
create: (_) => SessionRepository()),
child: MaterialApp(
...
),
);
}
In one of my screens, I create a view model as I'm following MVVM architecture, and use the SessionRepository to initialize some view model variables.
Widget build(BuildContext context) {
final session = Provider.of<SessionRepository>(context, listen: false);
return Provider<TestViewModel>(
create: (context) => TestViewModel(session),
child: ...
);
}
In my view model this is happening:
class TestViewModel{
final SessionRepository session;
final var foo;
final var bar;
TestViewModel(this.session) : foo = session.foo, bar = session.bar;
}
My question is that whenever I update the session variables, I also want the view model to store the updated variables. I can't think of any way that this could be done automatically when updating the session, any inputs?

If you want to notify your view , when SessionRepository property change , and recreate your viewmodel with new values, SessionRepository must implement ChangeNotifier. Check out the ChangeNotifierProvider samples.

Related

What is the efficient way to pass arguments to a Riverpod provider each time it gets initialized in Flutter?

I am currently trying to create an instance of a widget's state (ChangeNotifier) using a global auto-disposable ChangeNotifierProvider. The notifier instance takes in a few arguments to initialize each time the UI is built from scratch.
Let's assume we have the following simple state (or notifier):
class SomeState extends ChangeNotifier {
int _someValue;
SomeState({required int initialValue})
: _someValue = initialValue;
int get someValue => _someValue;
set someValue(int someValue) {
_someValue = someValue;
notifyListeners();
}
}
I used to use the Provider package before switching to Riverpod, where this could've easily been done like so:
class SomeWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
// Passing 2 into state initializer, which may be
// obtained from a different state, but not necessarily.
create: (_) => SomeState(initialValue: 2),
builder: (context, child) => Consumer<SomeState>(
builder: (context, state, child) {
// Will print 2, as it's currently the default value.
return Text('${state.someValue}');
},
),
);
}
}
So with Provider, you can manually call to SomeState constructor with arbitrary arguments when the state is being set up (i.e. provided). However, with Riverpod, it doesn't seem as intuitive to me, mainly because the provider is made to be declared globally:
static final someProvider = ChangeNotifierProvider.autoDispose((ref) => SomeState(2));
Which would end up being used like so:
class SomeWidget extends ConsumerWidget {
#override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(someProvider);
return Text('${state.someValue}');
}
}
However, with this approach I can't pass parameters like I did in the example using Provider. I also don't want to use the family modifier because I would need to pass the same parameter each time I read/watch the state, even if it's already created.
If it helps, in my current situation I am trying to pass a function (say String Function()? func) into my state on initialization. It's also not feasible to depend on a different provider in this case which would provide such function.
How could I replicate the same functionality in the Provider example, but with Riverpod?
P.S. Apologies if code has syntax errors, as I hand-typed this and don't have an editor with me at the moment. Also, this is my first post so apologies for lack of clarity or format.
Use provider overrides with the param that you need:
First, let's ensure the ProviderScope in the root of the widget-tree.
// Root
ProviderScope(
child: MaterialApp(...)
)
After, create another one in some widget:
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
someProvider.overrideWithProvider(
ChangeNotifierProvider.autoDispose((ref) => SomeState(5)),
),
],
child: Consumer(
builder: (context, ref, child) {
final notifier = ref.watch(someProvider);
final value = notifier.someValue;
return Text('$value'); // shows 5 instead of 2
}
),
);
}
If you do not want to use family then you can put value in another way by combining two providers.
final someValue = StateProvider((ref) => 0);
final someProvider = ChangeNotifierProvider.autoDispose((ref) {
final value = ref.watch(someValue);
return SomeState(value);
});
class SomeState extends ChangeNotifier {
int _someValue;
SomeState(int initialValue) : _someValue = initialValue;
int get someValue => _someValue;
set someValue(int someValue) {
_someValue = someValue;
notifyListeners();
}
}
USAGE:
// From everywhere you can put new value to your ChangeNotifier.
ref.read(someValue.notifier).state++;
But in your case, it's better to use the `family method. It's cleaner and less complicated.

how do you share data between viewmodels in Flutter Provider state management?

I am new to flutter Provider state management. If each view has its own viewmodel, how do you share data between viewmodels?
Lets say you have a viewmodel with a list in it, in one screen you want to show the list, and in another screen you want to show the list count?
You can place ViewModels on the top of widget tree, before MaterialApp
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: <SingleChildWidget>[
ChangeNotifierProvider<ViewModel1>(create: (_) => ViewModel1()),
ChangeNotifierProvider<ViewModel2>(create: (_) => ViewModel2()),
],
child: MaterialApp( ... );
);
}
}
so you can access both viewmodels everywhere

How to create a dependency for ChangeNotifierProvider and make it wait to complete?

I have ChangeNotifierProvider object that uses data stored sqflite asset database which need to be loaded at the beginning as future. The problem is that ChangeNotifierProvider doesn't wait for future operation to complete. I tried to add a mechanism to make ChangeNotifierProvider wait but couldn't succeed. (tried FutureBuilder, FutureProvider, using all together etc...)
Note : FutureProvider solves waiting problem but it doesn't listen the object as ChangeNotifierProvider does. When I use them in multiprovider I had two different object instances...
All solutions that I found in StackOverflow or other sites don't give a general solution or approach for this particular problem. (or I couldn't find) I believe there must be a very basic solution/approach and decided to ask for your help. How can I implement a future to this code or how can I make ChangeNotifierProvider wait for future?
Here is my summary code;
class DataSource with ChangeNotifier {
int _myId;
List _myList;
int get myId => _myId;
List get myList => _myList;
void setMyId(int changeMyId) {
_myId = changeMyId;
notifyListeners();
}
.... same setter code for myList object.
DataSource(){initDatabase();}
Future<bool> initDatabase() {
.... fetching data from asset database. (this code works properly)
return true;
}
}
main.dart
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<DataSource>(
create: (context) => DataSource(),
child: MaterialApp(
home: HomePage(),
),
);
}
}
Following code and widgets has this code part (it works fine)
return Consumer<DataSource>(
builder: (context, myDataSource, child) {.......
There are multiple ways that you can achieve. The main point of it is that you should stick to reactive principle rather than trying to await the change. Say for example, you could change the state of boolean value inside the DataSource class when the ajax request changes
class DataSource extends ChangeNotifier{
bool isDone = false;
Future<bool> initDatabase(){
//Do Whatever
isDone = true;
notifyListeners();
}
}
Then you could listen to this change in the build method like so
Widget build(BuildContext ctx){
bool isDone = Provider.of<DataSource>(context).isDone;
if(isDone){
// render result
}else{
// maybe render loading
}
}

Flutter Authentication and Data Persistence with Provider Architecture

I come from a HTML/CSS/JQUERY for frontend background and mostly I have been a Nodejs backend developer.
However a project I took over was based on HTML/CSS/JQuery and PHP and I am doing quite well in that until they needed a mobile application.
So my choice was flutter since it was hot at that moment.
I have mastered flutter basics and took provider as my state management and also incorporated logic inside it.
However I have 2 issue that I am not being able to figure out.
One is state persistence in provider even when app is forced closed.
Maintain login persistence unless logged out and clear state if logged out.
For 1. I haven't been able to figure out a concrete solution except Application Storage but then I would have to write alot of code more to store all the data and class into it.
For 2. I have done some experimental method as below
runApp(
ChangeNotifierProvider(
create: (_) => UserRepository(), // Here is my auth state provider
child: MyApp(),
),
);
In UserRepository class user data like user id are stored in SharedPreferences which checks if there is userdata is stored or not hence Status becomes Authenticated or Uninitialized per condition.
Below is a Stateful widget that sets the status with respect to UserRepository
class _MyAppState extends State<MyApp> {
ProjectRepository projectRepository = ProjectRepository();
UIKeypadRepository uiKeypadRepository = UIKeypadRepository();
PODRepository podRepository = PODRepository();
Status currentStatus;
//Also there are more Change notifier class here that manages state of various features of the app
void initProviders() {
projectRepository = ProjectRepository();
uiKeypadRepository = UIKeypadRepository();
podRepository = PODRepository();
}
#override
void initState() {
currentStatus = Provider.of<UserRepository>(context, listen: false).status;
//if not logged out current status is authenticated
super.initState();
}
#override
Widget build(BuildContext context) {
return Consumer(builder: (context, UserRepository authStatus, child) {
if (currentStatus != Status.Authenticated) {
initProviders(); // this function is called and all changenotifier classes are reinitialized
}
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: projectRepository),
ChangeNotifierProvider.value(value: uiKeypadRepository),
ChangeNotifierProvider.value(value: podRepository),
],
//instead of create I used value so if classes are initialized initial values are taken else existing values are made available to below the widget tree
child: MaterialApp(
home: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},
child: LandingPage())),
);
});
}
}
You can use authStateChanges() and StreamBuilder to get the authentication status.
Reference

How to create Login-Wall Views in Flutter

I am working on an app in Flutter and I'm pretty new to it/Dart. I already created the login, signup etc and everything works perfectly fine. Now I want to create a "Login-Wall" Template for every View that needs the user to be logged in. If the user is not logged in, he should be returned to the LoginView, if the api-call is still loading, it should not show anything but a loading screen called LoadingView(). I started by creating a Stateful Widget called AuthorizedLayout:
class AuthorizedLayout extends StatefulWidget {
final Widget view;
AuthorizedLayout({this.view});
_AuthorizedLayoutState createState() => new _AuthorizedLayoutState();
}
The state utilizes a Future Builder as follows:
Widget build(BuildContext context) {
return FutureBuilder<User>(
future: futureToken,
builder: (BuildContext context, AsyncSnapshot<User> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return NoConnectionView();
case ConnectionState.active:
case ConnectionState.waiting:
return LoadingView();
case ConnectionState.done:
if(snapshot.data != null) {
print("User Data loaded");
return widget.view;
} else
return LoginView();
}
},
);
}
As you can see, it should load the userdata, and when it's finished it should return the view. The futureToken represents the Future that will return the User-Object from the server after an api-request. In any other case it should show the Loading/Error/Login Page.
I'm calling it like this:
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).backgroundColor,
body: AuthorizedLayout(
view: DashboardView(),
),
);
}
In the Build method of the Dashboard view I have a "print('Dashboard View');". The problem I have is that in the output the 'Dashboard View' is printed before the 'User Data Loaded'. That means I can't access the loaded user data in that view. This means that this solution does not work the way I intended it to.
Now for my question: Is there any way I can build this "Login-Wall" and pass the user data to every view that is inside the login wall? I hope the code I posted explains the idea I'm trying to go for.
Is there any way I can build this "Login-Wall" and pass the user data to every view that is inside the login wall?
Absolutely! At a basic level, you're talking about state management. Once a user logs into your app, you want to store that user data so that it's accessible to any widget within the widget tree.
State management in Flutter is a hotly-debated topic and while there are a ton of options, there is no defacto state management technique that fits every app. That said, I'd start simple. One of the simplest and most popular options is the scoped_model package.
You can read all of the details here, but the gist is that it provides utilities to pass a data model from a parent widget to its descendants.
First, install the package.
Second, you'll want to create a model that can hold the user data that you want to be accessible to any widget in the tree. Here's a trivial example of what that might look like:
// user_model.dart
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
class UserModel extends Model {
dynamic _userData;
void setUserData(dynamic userData) {
_userData = userData;
}
String getFirstName() {
return _userData['firstName'];
}
static UserModel of(BuildContext context) =>
ScopedModel.of<UserModel>(context);
}
Next, we'll need to make an instance of this UserModel available to all widgets. A contrived way of doing this would be to wrap your entire app in a ScopedModel. Example below:
// main.dart
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'login_view.dart';
import 'user_model.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ScopedModel<UserModel>(
model: UserModel(),
child: MaterialApp(
theme: ThemeData.light(),
home: LoginView(),
),
);
}
}
In the above code, we're wrapping our entire instance of MaterialApp in a ScopedModel<UserModel>, which will give every widget in the application access to the User model.
In your login code, you could then do something like the following when your login button is pressed:
onPressed() async {
// authenticate your user...
var userData = await someApiCall();
// set the user data in our model
UserModel.of(context).setUserData(userData);
// go to the dashboard
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DashboardView(),
),
);
}
Last but not least, you can then access that user data through the UserModel like so:
// dashboard_view.dart
import 'package:flutter/material.dart';
import 'package:scoped_model_example/user_model.dart';
class DashboardView extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: Text(
UserModel.of(context).getFirstName(),
),
),
],
);
}
}
Check out the docs on scoped_model for more details. If you need something more advanced, there are a number of other state management patterns in Flutter such as BloC, Redux, Mobx, Provider and more.
So I just got what was happening. I was passing the already-built widget to the AuthorizedView. What I actually had to pass was a Builder instead of a Widget.
class AuthorizedLayout extends StatefulWidget {
final Builder viewBuilder;
AuthorizedLayout({this.viewBuilder});
_AuthorizedLayoutState createState() => new _AuthorizedLayoutState();
}
Calling it like this:
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).backgroundColor,
body: AuthorizedLayout(
viewBuilder: Builder(builder: (context) => DashboardLayout()),
),
);
}
Note that I recalled the final variable to viewBuilder instead of view, compared to the example above.
This will actually build the widget AFTER the userdata is loaded.