jr00n
jr00n

Reputation: 471

StreamProvider with RiverPod not working (try to migrate from Provider)

I'm trying to understand RiverPod by migrating my simple FireStore auth Provider example to RiverPod.

This is my AuthenticationService:

import 'package:firebase_auth/firebase_auth.dart';

class AuthenticationService {
  final FirebaseAuth _firebaseAuth;
  AuthenticationService(this._firebaseAuth);

  // with StreamProvider we listen to these changes
  Stream<User> get authStateChanges => _firebaseAuth.authStateChanges();

  Future<String> signIn({String email, String password}) async {
    try {
      await _firebaseAuth.signInWithEmailAndPassword(
          email: email, password: password);
      return 'Signed in';
    } on FirebaseAuthException catch (e) {
      return e.message;
    }
  }

  Future<String> signUp({String email, String password}) async {
    try {
      await _firebaseAuth.createUserWithEmailAndPassword(
          email: email, password: password);
      return 'Signed up ';
    } on FirebaseAuthException catch (e) {
      return e.message;
    }
  }

  Future<void> signOut() async {
    await _firebaseAuth.signOut();
  }
}

In main.dart I made 2 providers so I can use the service and listen to the property inside of the AuthenticationService

import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:meditatie_app/authentication_service.dart';
import 'package:meditatie_app/home_page.dart';
import 'package:meditatie_app/signin_page.dart';
import 'package:provider/provider.dart';

Future<void> main() async {
  // initalize Firebase and before that we need to initialize the widgets.
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        // Normal provider to serve the AuthenticationService in the widgettree
        // so the login form can use this provider to use .singIn()
        Provider<AuthenticationService>(
          create: (_) => AuthenticationService(FirebaseAuth.instance),
        ),
        // also a StreamProvider that serves the AuthenticationSerivce authStateChanges
        // this stream is updated by the FirebaseAuth package when users signin or out
        // this provider use context.read<AuthenticationService> to find the
        // provider dived here above
        StreamProvider(
          create: (context) =>
              context.read<AuthenticationService>().authStateChanges,
        )
      ],
      child: MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        home: AuthenticationWrapper(),
      ),
    );
  }
}

class AuthenticationWrapper extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final firebaseUser = context.watch<User>();
    if (firebaseUser != null) {
      return HomePage();
    }
    return SignInPage();
  }
}

Here the SingIn page:

import 'package:flutter/material.dart';
import 'package:meditatie_app/authentication_service.dart';
import 'package:provider/provider.dart';

class SignInPage extends StatelessWidget {
  final TextEditingController emailController = TextEditingController();
  final TextEditingController passwordController = TextEditingController();

...
          RaisedButton(
            onPressed: () {
              // Sign in code
              context.read<AuthenticationService>().signIn(
                    email: emailController.text.trim(),
                    password: passwordController.text.trim(),
                  );
            },
...

This works fine with normal Provider, but I can't get it to work with RiverPod

What I did was:

These providers I made global in providers.dart

import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_riverpod/all.dart';

import 'authentication_service.dart';

final authenticationSeriviceProvider =
    Provider((ref) => AuthenticationService(FirebaseAuth.instance));
final authStateChangeProvider = StreamProvider.autoDispose<User>((ref) {
  return ref
      .watch(authenticationSeriviceProvider)
      .authStateChanges;
});

Is this correct? The authStateChangeProvider is using the authenticationSeriviceProvider

When is use it like:

import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:meditatie_app/home_page.dart';
import 'package:meditatie_app/signin_page.dart';
import 'package:flutter_riverpod/all.dart';
import 'providers.dart';

Future<void> main() async {
  // initialize Firebase and before that we need to initialize the widgets.
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(
    // riverpod needs at toplevel a Provider container
    // for storing state of different providers.
    ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: AuthenticationWrapper(),
    );
  }
}

// Riverpods ConsumerWidget
// which can consume a provider
// rebuild if the value of the provider changes
class AuthenticationWrapper extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final firebaseUser = watch(authStateChangeProvider);
    if (firebaseUser != null) {
      return HomePage();
    }
    return SignInPage();
  }
}

My 'firebaseUser' is not a User anymore, but an AsyncValue

When I change it to:

class AuthenticationWrapper extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final User firebaseUser = watch(authStateChangeProvider).data?.value;
    if (firebaseUser != null) {
      return HomePage();
    }
    return SignInPage();
  }
}

It is working, but what am I doing wrong that I now work with AsyncValue

Upvotes: 3

Views: 4314

Answers (3)

1housand
1housand

Reputation: 616

Another way I found was to use it the way this tutorial did, but with the new riverpod changes:

import 'dart:async';

import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_shopping_list/repositories/auth_repository.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

final authControllerProvider = StateNotifierProvider<AuthController, User?>(
  (ref) => AuthController(ref.read)..appStarted(),
);

class AuthController extends StateNotifier<User?> {
  final Reader _read;

  StreamSubscription<User?>? _authStateChangesSubscription;

  AuthController(this._read) : super(null) {
    _authStateChangesSubscription?.cancel();
    _authStateChangesSubscription = _read(authRepositoryProvider)
        .authStateChanges
        .listen((user) => state = user);
  }

  @override
  void dispose() {
    _authStateChangesSubscription?.cancel();
    super.dispose();
  }

  void appStarted() async {
    final user = _read(authRepositoryProvider).getCurrentUser();
    if (user == null) {
      await _read(authRepositoryProvider).signInAnonymously();
    }
  }
}

And then I used it like this:

@override
  Widget build(BuildContext context, WidgetRef ref) {
    User? user = ref.watch<User?>(authControllerProvider);
    return user != null
        ? MaterialApp(
            title: 'My App',
            builder: (context, child) => _Unfocus(child: child!),
            home: MainNavigation(),
            debugShowCheckedModeBanner: false,
          )
        : const MaterialApp(
            title: 'My App,
            home: LoginPage(),
            debugShowCheckedModeBanner: false,
          );
  }

Upvotes: 0

Alex Hartford
Alex Hartford

Reputation: 6010

See the documentation.

You should use AsyncValue's exposed states to decide what to render. Your code could look something like the following:

class AuthenticationWrapper extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    return watch(authStateChangeProvider).when(
      data: (user) => user == null ? SignInPage() : HomePage(),
      loading: () => CircularProgressIndicator(),
      error: (err, stack) => SignInPage(),
    );
  }
}

So adjust your return logic to what you'd like for the data, loading, and error states, but this should give you a general idea on how to use AsyncValue.

Upvotes: 1

EdwynZN
EdwynZN

Reputation: 5611

Expanding the previous answer AsyncValue<T> is a sealed class, think of it as StreamBuilder in Flutter having AsyncSnapshot<T> which wraps the value returned from the stream and gives you options to check if its connecting, waiting, withError or withData. In your case

class AuthenticationWrapper extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    return watch(authStateChangeProvider).when(
      data: (user) => user == null ? SignInPage() : HomePage(),
      loading: () => CircularProgressIndicator(),
      error: (err, stack) => SignInPage(),
    );
  }
}

should handle all the options, now when loading it will show a progress indicator, if there is an error (connection, bad result, etc) it will display the SignInPage, and finally when there is a value you still will need to check if the value returned from the Stream is null (As far as I understand Firebase returns null when there is no user signed in, it doesn't mean the stream is empty) and display the right widget if its null or not.

Just like Provider, after retrieving the user you still have to do the logic with that

Upvotes: 2

Related Questions