Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The member 'state' can only be used within instance members of subclasses of 'package:state_notifier/state_notifier.dart' #52

Closed
zgramming opened this issue Jul 16, 2020 · 20 comments
Labels
documentation Improvements or additions to documentation

Comments

@zgramming
Copy link

I follow source code from documentation :

Documentation example

class Counter extends StateNotifier<int> {
  Counter(): super(0);

  void increment() => state++;
}

final counterProvider = StateNotifierProvider((ref) => Counter());

// ...

@override
Widget build(BuildContext context) {
  // Obtains Counter without listening to its state.
  // Will not cause the button to rebuild when the counter changes.
  final Counter counter = useProvider(counterProvider);

  return RaisedButton(
    onPressed: () => counter.increment(),
    child: Text('increment'),
  );
}

Then i want custom it , when i click button i want show circularprogressindicator , after 3 second showing the text. So i already custom the example documentation to this :

ProviderLoading.dart

import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:state_notifier/state_notifier.dart';

class IsLoading extends StateNotifier<bool> {
  IsLoading() : super(false);
  void toggleLoading(bool value) => state = value;
}

final isLoadingProvider = StateNotifierProvider((ref) => IsLoading());

Main.dart


class TestingApp extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final IsLoading isLoading = useProvider(isLoadingProvider);
    return Scaffold(
      body: Container(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Center(
              child: isLoading.state ? CircularProgressIndicator() : Text('This Is Your Data'),
            ),
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: FlatButton(
                  onPressed: () => showLoading(isLoading), child: Text('Toggle Loading')),
            ),
          ],
        ),
      ),
    );
  }

  void showLoading(IsLoading isLoading) async {
    isLoading.toggleLoading(true);
    print("Proses");
    await Future.delayed(const Duration(seconds: 3));
    print("Done");
    isLoading.toggleLoading(false);
  }
}

But i got this warning and although i click button , it not showing the circularprogressindicator:

The member 'state' can only be used within instance members of subclasses of 'package:state_notifier/state_notifier.dart'.dart(invalid_use_of_protected_member)

I missed something ?

@zgramming zgramming added the documentation Improvements or additions to documentation label Jul 16, 2020
@rrousselGit
Copy link
Owner

As said by StateNotifierProvider, if you want to listen to the state of a StateNotifier, use "useProvider(myStateNotifierProvider.state)"

@zgramming
Copy link
Author

zgramming commented Jul 16, 2020

@rrousselGit but i still confused , i should make 2 variable to define state what i want listen and method from StateNotifier ?
something like :

final IsLoading isLoading = useProvider(isLoadingProvider); // it for method toggleLoading();
final bool loading = useProvider(isLoadingProvider.state); // it for listen state

Because documentation example i attach above , not showing to listen state and only show call method counter.increment()

@rrousselGit
Copy link
Owner

Yes, that is correct.

Although the counter example does counter.read(context).increment()

@zgramming
Copy link
Author

It's more clear for me. But it's have option/possible make it more simple ? something like i only make 1 variable final loading = useProvider(loadingProvider) . and this variable can use for calling method and listen state ?

final loading = userProvider(loadingProvider);

//Listen state
Text(loading.state);

//Calling method 
loading.toggleLoading(value);

with above approach , will make warning like my thread . Because make 2 variable with almost same name , make me confused.

@rrousselGit
Copy link
Owner

No this is expected

It allows reading the notifier without listening to the state, to avoid pointless rebuilds.

@edisonywh
Copy link

edisonywh commented Jul 20, 2020

Mhm, I am trying to do exactly that (one for reading and one for methods), but I can't seem to get the method way to work?

final StateNotifierProvider themeProvider = StateNotifierProvider((ref) {
  return ThemeManager();
});

class ThemeManager extends StateNotifier<ThemeData> {
  ThemeManager() : super(darkTheme);

  void setTheme(ThemeData theme) async {
    state = theme;
  }
}

Then now when I try to obtain toggle setTheme, I retrieve it like so:

final ThemeManager themeManager = useProvider(themeProvider);

But I get error of:

The argument type 'StateNotifierProvider<StateNotifier>' can't be assigned to the parameter type 'ProviderListenable'.

What am I doing wrong here?

EDIT: Nevermind, looks like I just had to remove the type annotation for:

- final StateNotifierProvider themeProvider = StateNotifierProvider((ref) {
+ final themeProvider = StateNotifierProvider((ref) {
  return ThemeManager();
});

@rrousselGit
Copy link
Owner

As I mentioned, the syntax is "useProvider(themeProvider.state)"

@zgramming
Copy link
Author

@rrousselGit i don't know it's should create new issue or not , i got this warning again but in FutureProvider i want know this is best practice or not.

I have simple Http get request to show all user from server, and i using manage it using FutureProvider

Api

class UserGoogleApi {
  Future<List<UserGoogleModel>> getAllUser() async {
    final result = await reusableRequestServer.requestServer(() async {
      final response =
          await http.get('${appConfig.baseApiUrl}/${appConfig.userGoogleController}/getAllUser');
      final Map<String, dynamic> responseJson = json.decode(response.body);
      if (responseJson['status'] == 'ok') {
        final List list = responseJson['data'];
        final listUser = list.map((e) => UserGoogleModel.fromJson(e)).toList();
        return listUser;
      } else {
        throw responseJson['message'];
      }
    });
    return result;
  }
}

User Provider

class UserProvider extends StateNotifier<UserGoogleModel> {
  UserProvider([UserGoogleModel state]) : super(UserGoogleModel());
   
Future<UserGoogleModel> searchUserByIdOrEmail({
    String idUser,
    String emailuser,
    String idOrEmail = 'email_user',
  }) async {
    final result = await _userGoogleApi.getUserByIdOrEmail(
      idUser: idUser,
      emailUser: emailuser,
      idOrEmail: idOrEmail,
    );
    UserGoogleModel temp;
    for (var item in result) {
      temp = item;
    }
    state = UserGoogleModel(
      idUser: temp.idUser,
      createdDate: temp.createdDate,
      emailUser: temp.emailUser,
      imageUser: temp.emailUser,
      nameUser: temp.nameUser,
      tokenFcm: temp.tokenFcm,
      listUser: state.listUser,
    );

    return temp;
  }

  Future<List<UserGoogleModel>> showAllUser() async {
    final result = await _userGoogleApi.getAllUser();
    state.listUser = result;
    return result;
  }
}

final userProvider = StateNotifierProvider((ref) => UserProvider());

final showAllUser = FutureProvider.autoDispose((ref) async {
  final usrProvider = ref.read(userProvider);
  final result = await usrProvider.showAllUser();
  return result;
});

After that setup, i simply can call showAllUser like this :

   Consumer((ctx, read) {
              final provider = read(showAllUser);
              return provider.when(
                data: (value) {
                  return ListView.builder(
                    itemCount: value.length,
                    shrinkWrap: true,
                    itemBuilder: (BuildContext context, int index) {
                      final result = value[index];
                      return Text(result.nameUser);
                    },
                  );
                },
                loading: () => const CircularProgressIndicator(),
                error: (error, stackTrace) => Text('Error $error'),
              );
            }),

it's no problem if http request don't have required parameter, but i got problem if my http request required parameter. I don't know how to handle this.

Let's say , i have another http get to show specific user from id user or email user. Then API look like :

User Provider

final showSpecificUser = FutureProvider.autoDispose((ref) async {
  final usrProvider = ref.read(userProvider);

  final result = await usrProvider.searchUserByIdOrEmail(
    idOrEmail: 'id_user',
    idUser: usrProvider.state.idUser, //  => warning on "state"
  );
  return result;
});

When i access idUser from userProvider using usrProvider.state.idUser , i got this warning again The member 'state' can only be used within instance members of subclasses of 'package:state_notifier/state_notifier.dart'.

How can i get idUser from userProvider ? and i want know this is best practice using FutureProvider ?

@rrousselGit
Copy link
Owner

rrousselGit commented Jul 22, 2020 via email

@zgramming
Copy link
Author

zgramming commented Jul 22, 2020

@rrousselGit ref.read(userProvider.state); give me error :

The argument type 'StateNotifierStateProvider<UserGoogleModel>' can't be assigned to the parameter type 'ProviderBase<ProviderDependency<dynamic>, Object>'.

I think method read on Consumer and FutureProvider it's different.

read on Consumer accept Res Function<Res>(ProviderBase<ProviderDependencyBase, Res>)
read on FutureProviduer accept ProviderBase<ProviderDependency<T>, Object>

@rrousselGit
Copy link
Owner

Oh my bad, that's for the next changes

For now you can do ref.read(userProvider).addListener((state) {...}

@zgramming
Copy link
Author

zgramming commented Jul 22, 2020

@rrousselGit i have 2 problem with this approach .

  1. Value obtained from listener always null , although i'm sure it's have value.

UserProvider

final showSpecificUser = FutureProvider.autoDispose((ref) async {
  UserGoogleModel temp;
  final usrProvider = ref.read(userProvider);
  usrProvider.addListener((state) async {
    final result = await usrProvider.searchUserByIdOrEmail(emailuser: state.emailUser);
    // print('listenern RESULT ${result.nameUser} || ${result.emailUser} || ${result.idUser}');
    temp = result;
    print('Temp RESULT ${result.nameUser} || ${result.emailUser} || ${result.idUser}');
  });

  return temp;
});

And i simple call it like this

     Consumer((ctx, read) {
              final provider = read(showSpecificUser);
              return provider.when(
                data: (value) {
                  if (value == null) {
                    return Text('Not have data');
                  }
                  // print('specific user ${value.nameUser}');
                  return Text(value.nameUser ?? 'null');
                },
                loading: () => const CircularProgressIndicator(),
                error: (error, stackTrace) => Text(error.toString()),
              );
            }),

Temp Result

tanya rousel

tanya rousel 2

2.With addListener((state)async{}) it will always listen ,although i already used FutureProvider.autoDisposed.

Although already used FutureProvider.autoDisposed , after logout it's still listen old value. please see below gif.
tanya rousel 3

@AlexHartford
Copy link
Contributor

Can't you just add a getter for state in your class that extends StateNotifier? Assuming that your FutureProvider is being properly disposed after each use that should be a suitable workaround until the new changes to Riverpod are live. I did a quick test to see and it does work. Make sure you define a getter like this and don't override the default defined by StateNotifier.

class A extends StateNotifier<B> {
...
  static final provider = StateNotifierProvider((ref) => A());

  getState() => state;
...
}
final provider = FutureProvider.autoDispose((ref) async {
    final a = ref.read(A.provider);
    final t = a.getState();
    print(t);
});

Not ideal but seems like a fine workaround. I believe the intention of state being inaccessible outside is to ensure state manipulations are handled by the StateNotifier itself, so using a getter in the meantime wouldn't be the end of the world.

@zgramming
Copy link
Author

@AlexHartford interesting, i will try it .

@rrousselGit
Copy link
Owner

Not necessarily a good practice, as the value may change over time.
You may want a StreamProvider for that reason too

@zgramming
Copy link
Author

@rrousselGit based on my above code , what's the problem with my code ?

@rrousselGit
Copy link
Owner

If "t" changes,this will not print the new value

@zgramming
Copy link
Author

@rrousselGit I think best option it's waiting you implement what you mention on it

@Tameflame
Copy link

How would I listen to state changes without using a hookwidget? Is the below correct?

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Search Sellers")),
      body: Consumer(
        builder: (context, watch, child) {
          var location = watch(mapApiProvider.state); // mapApiProvider is a StateNotifierProvider
          var myLatLong = {
            location.userLocation.latitude,
            location.userLocation.longitude
          };
          return Center(
              child: Text("You're location is ${myLatLong.toString()}"));
        },
      ),
    );
  }

@rrousselGit
Copy link
Owner

Yes this is correct

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

5 participants