rrousselGit / riverpod

A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
https://riverpod.dev
MIT License
6.19k stars 948 forks source link

Consumer not updating MaterialApp home at widget rebuild #250

Closed abibiano closed 3 years ago

abibiano commented 3 years ago

I have an authentication StateNotifier class and StateNotifierProvider and have wrapped the MaterialApp widget into a Consumer to rebuild the complete app/widget tree when user is no longer authenticated.

When I update the state of the authenticationStateNotifierProvider from another page/widget and I debug, I can see the build method of the consumer wrapping the App is triggered and the state is updated (print(state)) but the home page and widget tree is not rebuilt.

I’m migrating this app from using provider package and with the provider package it works.

Sample code using Riverpot + StateNotifier

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(ProviderScope( child: AppWidget()));
}

class AppWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer(
      builder: (context, watch, child) {
        final state = watch(authenticationStateNotifierProvider.state);
        print(state);
        return MaterialApp(
          title: 'Test',
          home: (state is Authenticated) ? SalesOrderListPage() : LoginPage(),
        );
      },
    );
  }
}

Sample code using provider

class AppWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: providers,
      child: Consumer<AuthenticationProvider>(
          builder: (context, authenticationProvider, _) {
        return MaterialApp(
          title: 'Test',
          home: authenticationProvider.isAuth
              ? PedidosVentaScreen()
              : LoginScreen(),
          ),
        },
      ),
    );
  }
}
iamarnas commented 3 years ago

@abibiano Hi. In your situation it would look like this.

//MaterialApp... 
child: Consumer(
          builder: (context, watch, child) {
            final state = watch(weatherStateNotifierProvider.state);

            if (state is WeatherLoading) {
              return buildLoading();
            } else if (state is WeatherLoaded) {
              return buildWeatherPage(context, state.weather);
            }
          },
        ),
//..... 

And if you want only read data you can use StatelessWidget and use context.read(myProvider).value;. You don't need use Consumer to read data.

abibiano commented 3 years ago

@iamarnas thanks for your answer but I don't know where to put your code. In my current code I don't have a child property:

class AppWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer(
      builder: (context, watch, child) {
        final state = watch(authenticationStateNotifierProvider.state);
        print(state);
        return MaterialApp(
          title: 'Test',
          home: (state is Authenticated) ? SalesOrderListPage() : LoginPage(),
        );
      },
    );
  }
}

and I really don't know why this code is not working, because the state is updated and code inside build method is executed but home change is not displayed.

iamarnas commented 3 years ago

@abibiano home: == child:

abibiano commented 3 years ago

@iamarnas OK, I understant. I already tried this solution before and it works until I do a Navigator.pushReplacementNamed(context, ...) from my drawer for example to display a new page.

When I do the pushReplacementNamed then Consumer widget below my MaterialApp is deleted and updates to the state are no longer watched.

Do you have any suggestion how to avoid this problem?

iamarnas commented 3 years ago

@abibiano If you want return MaterialApp use then ConsumerWidget instead StatelessWidget. Consumer builder uses inside a widget tree. I'm bad at explaining but I hope you understood what I mean.

abibiano commented 3 years ago

yes I understand, but if I replace the complete app StatelessWidget with a ConsumerWidget I have the same problema as with my initial code. Changes on the state don't force to rebuild the APP

iamarnas commented 3 years ago

yes I understand, but if I replace the complete app StatelessWidget with a ConsumerWidget I have the same problema as with my initial code. Changes on the state don't force to rebuild the APP

And you return as in the example it is the same problem?

return watch(myProvider).didComplete ? SignInPage() : HomePage();
iamarnas commented 3 years ago

When I do the pushReplacementNamed then Consumer widget below my MaterialApp is deleted and updates to the state are no longer watched.

Do you have any suggestion how to avoid this problem?

If you destroy provider when you navigate to another page. You can override them with ProvidedScope. You can find example in the Marvel source in the documentation.

abibiano commented 3 years ago

This is my code with ConsumerWidget. I think It's Ok. In fact if I print the state too debug I can see it gets updated inside the builder but the widget tree is not rebuild.

class AppWidget extends ConsumerWidget {
  @override
  Widget build(context, watch) {
    final state = watch(authenticationStateNotifierProvider.state);

    return MaterialApp(
      title: 'TEST',

      home: (state is Authenticated) ? SalesOrderListPage() : LoginPage(),
      onGenerateRoute: AppRouteGenerator.generateRoute,
    );
  }
}

I think the problem is not with the provider because everything is fine. It seems the problem is with the MateriaApp that the home parameters update is not forcing a widget rebuild.

iamarnas commented 3 years ago

@abibiano I'm not sure but my first example should work.

@abibiano look at this example. Here is little advanced but in your situation you need only Consumer

abibiano commented 3 years ago

Yes. Your first example works until I do a pushReplacementNamed on the app. After the pushReplacementNamed the consumer disapear on the widget tree. I will investigate more about this.


De: Arnas notifications@github.com Enviado: Wednesday, December 16, 2020 6:15:06 PM Para: rrousselGit/river_pod river_pod@noreply.github.com Cc: Alex Bibiano alex@bibiano.es; Mention mention@noreply.github.com Asunto: Re: [rrousselGit/river_pod] Consumer not updating MaterialApp home at widget rebuild (#250)

@abibianohttps://github.com/abibiano I'm not sure but my first example should work.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHubhttps://github.com/rrousselGit/river_pod/issues/250#issuecomment-746660714, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AAMQNRECDASGQ2PLO73ADXLSVDTJVANCNFSM4U6BE7OA.

rrousselGit commented 3 years ago

Could you provide a full example?

The snippet you gave cannot be executed and there is no obvious issue with it

iamarnas commented 3 years ago

@abibiano I can accept that here is not issue with Riverpod. I have tested you StateNotifier and I have no issue and work very well. Here's what my little app looks like:

// View.
class App extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final state = watch(authenticationNotifier.state);

    return MaterialApp(
      home: state is Unauthenticated ? LoginScreen() : HomeScreen(),
    );
  }
}

// Controller.
final authenticationNotifier = StateNotifierProvider((ref) => AuthenticationNotifier());

class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
  AuthenticationNotifier() : super(Unauthenticated());

  void toggle() {
    state = state is Unauthenticated ? Authenticated() : Unauthenticated();
  }
}

// State.
abstract class AuthenticationState {}

class Authenticated extends AuthenticationState {}

class Unauthenticated extends AuthenticationState {}
abibiano commented 3 years ago

@iamarnas I have created a very simple example to demostrate the issue on abibiano/riverpod_test

If you test the app you will see state management works between login and home page as expected with your code, but as soon as you navigate to the second screen form the home page and press the logout button, state is changed on the App widged but widget tree is not updated.

Here the code for reference:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/all.dart';

void main() {
  runApp(ProviderScope(child: MyApp()));
}

class MyApp extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final state = watch(authenticationNotifier.state);
    print(state);
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: state is Unauthenticated ? LoginScreen() : HomeScreen(),
      onGenerateRoute: (RouteSettings settings) {
        if (settings.name == '/second')
          return MaterialPageRoute(builder: (_) => SecondScreen());
        else
          return MaterialPageRoute(builder: (_) => HomeScreen());
      },
    );
  }
}

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('HomeScreen'),
      ),
      body: Column(
        children: [
          MaterialButton(
            child: Text('Logout'),
            onPressed: () => context.read(authenticationNotifier).toggle(),
          ),
          MaterialButton(
            child: Text('Second'),
            onPressed: () => Navigator.pushReplacementNamed(
              context,
              '/second',
            ),
          ),
        ],
      ),
    );
  }
}

class SecondScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('SecondScreen'),
      ),
      body: MaterialButton(
        child: Text('Logout'),
        onPressed: () => context.read(authenticationNotifier).toggle(),
      ),
    );
  }
}

class LoginScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('LoginScreen'),
      ),
      body: MaterialButton(
        child: Text('Login'),
        onPressed: () => context.read(authenticationNotifier).toggle(),
      ),
    );
  }
}

// Controller.
final authenticationNotifier =
    StateNotifierProvider((ref) => AuthenticationNotifier());

class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
  AuthenticationNotifier() : super(Unauthenticated());

  void toggle() {
    state = state is Unauthenticated ? Authenticated() : Unauthenticated();
  }
}

// State.
abstract class AuthenticationState {}

class Authenticated extends AuthenticationState {}

class Unauthenticated extends AuthenticationState {}
iamarnas commented 3 years ago

@abibiano Hi, It's nothing to do with providers. You replace your HomeScreen with SecondScreen and you provider work in the background of SecondScreen. You need just navigate back to the LoginScreen when user click logout button and of course update state of AuthenticationNotifier

Your SecondScreen would look like that:

class SecondScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('SecondScreen'),
      ),
      body: Center(
        child: MaterialButton(
          child: Text('Logout'),
          onPressed: () {
            Navigator.pushReplacementNamed(context, '/'); // <- Just add this.
            context.read(authenticationNotifier).logout(); // <- Update state.
          },
        ),
      ),
    );
  }
}
abibiano commented 3 years ago

Lot of thanks for your time. You are right!

jozes commented 2 years ago

@iamarnas Excellent answer. I was struggling for a few hours with almost the same problem where consumer was not rebuilding the MaterialApp when the user logged in again and the MaterialApp.home definition was not respected. The problem was caused in logout function where I pushed a route to loginScreen what was completely unnecessary and causing the problem. The definition of home in MaterialApp was such that loginScreen is displayed whenever the user is not authenticated, otherwise homeScreen. When I read your answer the problem was immediately solved. Thanks for pointing that out.

MultiProvider( [ .... ChangeNotifierProvider.value(value: Auth()), ] ), child: Consumer(builder: (context, auth, _) { return MaterialApp( ... home: auth.isAuthenticated ? HomePage() : LoginPage(), ... }