This is a question mostly about the best practice, I'm new to flutter dev and to the BLoC architecture, to summarize I have an app that has an app_bloc which handles user authentication (firebase login), it's pretty simple, has two states Authenticated and Unauthenticated.
My app renders pages depending on whether user is authenticated or not, either login page or home page.
import 'package:authentication_repository/authentication_repository.dart';
import 'package:flow_builder/flow_builder.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:majstor/app/bloc/app_bloc.dart';
import 'package:majstor/app/routes/routes.dart';
import 'package:majstor/theme.dart';
class App extends StatelessWidget {
const App({
Key? key,
required AuthenticationRepository authenticationRepository,
}) : _authenticationRepository = authenticationRepository,
super(key: key);
final AuthenticationRepository _authenticationRepository;
#override
Widget build(BuildContext context) {
return RepositoryProvider.value(
value: _authenticationRepository,
child: BlocProvider(
create: (_) => AppBloc(
authenticationRepository: _authenticationRepository,
),
child: const AppView(),
),
);
}
}
class AppView extends StatelessWidget {
const AppView({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
/// try context.select here maybe?
return MaterialApp(
home: FlowBuilder<AppStatus>(
state: context.select((AppBloc bloc) => bloc.state.status),
onGeneratePages: onGenerateAppViewPages,
),
);
}
}
import 'package:flutter/widgets.dart';
import 'package:majstor/app/app.dart';
import 'package:majstor/home/home.dart';
import 'package:majstor/login/login.dart';
List<Page> onGenerateAppViewPages(AppStatus state, List<Page<dynamic>> pages) {
switch (state) {
case AppStatus.authenticated:
return [HomePage.page()];
case AppStatus.unauthenticated:
default:
return [LoginPage.page()];
}
}
Now what I'm having a hard time wrapping my head around is this, in my home page case, I'd like to further 'split' my users into roles, there will be two roles stored on a Firebase collection with the user's corresponding id on the same document, and for each role a different page entirely should be built, I'm not sure where exactly would I do this role check, I could technically in the home page below, query the database by using the user id, and getting the role and then similarly as i did with the login and home, use a flowbuilder to generate different page depending on role? But is that a good practice? I kind of feel like I should be separating the database logic elsewhere, but as I'm new to this kind of development I don't know where I should be doing this to build a scalable app.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:majstor/app/app.dart';
import 'package:majstor/home/home.dart';
import 'package:majstor/utils/databaseservice.dart';
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
static Page page() => const MaterialPage<void>(child: HomePage());
#override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
final user = context.select((AppBloc bloc) => bloc.state.user);
Database db = Database();
db.readData(user.id);
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
actions: <Widget>[
IconButton(
key: const Key('homePage_logout_iconButton'),
icon: const Icon(Icons.exit_to_app),
onPressed: () => context.read<AppBloc>().add(AppLogoutRequested()),
)
],
),
body: Align(
alignment: const Alignment(0, -1 / 3),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Avatar(photo: user.photo),
const SizedBox(height: 4),
Text(user.email ?? '', style: textTheme.headline6),
const SizedBox(height: 4),
Text(user.name ?? '', style: textTheme.headline5),
],
),
),
);
}
}
Do I query the database in the file above, and do the checks there or, is there a way to sort of separate my logic further? Should I be using BloCs for roles aswell? or should I be somehow integrating roles into my existing BloC? Hope I was clear enough with my explanation.
So I'm not a Flutter developer, but I might be able to offer some advice based on my general experience and some research I did for your question. And I'm assuming your concept of Bloc's align's somewhat with this.
I'd like to further 'split' my users into roles ... I'm not sure where
exactly would I do this role check
I would make a Bloc (logic class) that represents a users authentication and authorization state, which for the sake of discussion we'll call UserAuthBloc.
As your other Bloc's go doing stuff they can refer to the UserAuthBloc, whenever they need to make a decision based on the users authentication status or role.
Using BlocProvider to implement dependency injection is generally wise, and you can also use it to provide the UserAuthBloc to widgets that need to make decisions based on the users authentication status and role membership.
Do I query the database in the file above, and do the checks there or,
is there a way to sort of separate my logic further? Should I be using
BloCs for roles as well?
The database querying for user authentication and authorization (roles) should be done exclusively through the UserAuthBloc.
The checks are done at the Bloc level, at least as much as possible. If you are doing checks in the UI, then try and limit them to reusable widgets so that there's less code to change if you need to makes changes.
Keeping authentication and authorization (roles) in the same Bloc makes sense to me in most cases, because the roles depend on the user being authenticated.
Related
I am working on a flutter app and I want to update the widgets of a page without pushing new updates on the play store. I have researched a bit on this topic and found that I can use firebase remote config. My idea is to use firebase remote config to fetch the widgets and show them on the app so that I don't need to publish frequent updates for small changes.
For Example, the below code is used to show a circular loading bar:
import 'package:flutter/material.dart';
class LoadingWidget extends StatelessWidget {
const LoadingWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: const CircularProgressIndicator(
color: Colors.black,
),
);
}
}
But now even for the smallest change, let it be changes in color, alignment, or size I need to push a new update to the play store and users need to install the new update in order to reflect the changes.
I want to store the flutter widget in firebase remote config (JSON format) then in the app, it will fetch the JSON data from remote config and show the new changes.
Once your RemoteConfig, you need do add key-value pair to match your needs:
key : value
color : black
Thenm fetch the value:
myRemoteColor = remoteConfig.getString('color')
Now, your myRemoteColor has a string with value black.
Since you already know that Remote Config only stores primitive values, the problem becomes how to encode and decode a Flutter widget into a string.
I doubt that is going to be simple though, as it'd mean that your application would need to compile the JSON/string into a binary. You'd essentially be implementing something akin to Flutter's Hot Reload for your remote clients, something that'd be pretty cool but is not a built-in feature.
More feasible is to control specific widgets that you already have in your code, as for example Rubens answered. In a recent sample app I used Remote Config to enable/disable a new feature (a process known as feature flagging) with:
class _MyHomePageState extends State<MyHomePage> {
final FirebaseRemoteConfig remoteConfig;
_MyHomePageState(this.remoteConfig);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: DefaultTabController(
length: remoteConfig.getBool("chat_enabled") ? 3 : 2,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [
const Tab(icon: Icon(Icons.games_outlined)),
const Tab(icon: Icon(Icons.swap_vert_circle_outlined)),
if (remoteConfig.getBool("chat_enabled")) const Tab(icon: Icon(Icons.chat_bubble_outlined)),
],
),
title: Text(widget.title),
),
body: TabBarView(
children: [
const MyGame(key: Key('game')),
const Leaderboard(key: Key('leaderboard')),
if (remoteConfig.getBool("chat_enabled")) const Messageboard(key: Key('messageboard')),
],
),
),
),
);
}
I'm building a Flutter app with Firebase and Riverpod.
Until the main page is displayed a user has to perform several steps to get there (e.g. sign in, validate email, get activated by admin, upload documents). For each of these steps i show a specific page or widget which is determined in AppRouterWidget (see below).
The problem i have is that i need at least 2 different providers to cover all possible states, since some aspects belong to the Firebase user in Authentication area and the others to the user account in Firebase's database (collection 'account'), which is of course only available if the user has logged in.
I can cover the authentication part, but i have no clue how i can add the user account part, which should be accessible by watching accountStreamProvider.
This is what i currently have working:
final accountStreamProvider = StreamProvider((ref) {
final database = ref.watch(databaseProvider)!;
return database.accountStream();
});
class AppRouterWidget extends ConsumerWidget {
const AppRouterWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
final authStateChanges = ref.watch(authStateChangesProvider);
return authStateChanges.when (
data: (user) => _data(context, user, ref),
loading: () => const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
),
error: (_, __) => const Scaffold(
body: Center(
child: Text('Error'),
),
),
);
}
Widget _data(BuildContext context, User? user, WidgetRef ref) {
// user is the auth user
if (user == null) {
// either login or signup
return const AuthPage();
} else {
// logged in. now check which step we have to show
if (!user.emailVerified) {
return const VerifyEmailPage();
} else {
// these are the account specific data
//final accountAsyncValue = ref.watch(accountStreamProvider);
//if (accountAsyncValue.hasValue || !accountAsyncValue.value!.isActiv) {
// return const WeCallYouWidget();
//}
}
return Container();
}
}
}
I guess that i need 2 listeners in build() and both would call _data when triggered, but i don't know exactly how to do this.
Thanks a lot for some insights.
I'd be tempted to move all the logic for deciding which page to show outside your widget.
One way to do this would be to create a StateNotifier<PageState> subclass (PageState could be a Freezed class or an enumeration) that takes all the repositories/data sources you need as arguments, subscribes to all the streams as needed, and computes the output state that the widget can watch as:
final pageState = ref.watch(pageStateProvider);
return pageState.when(
auth: () => ....
verifyEmail: () => ...
uploadDocuments: () => ...
// and so on
);
As a result i took bizz84 advice and moved all logic into a separate class which now holds all needed listeners.
Whenever a new event happens it can react to that notification and determine the new page state which will be used to show the correct page.
This is my first question here and I hope I’m not making it too complex.
So, I’m a junior programmer and I’ve start learning flutter, firebase and riverpod a couple of months ago and I’m stuck in a specific project.
For part of the app, what I need to do is something very similar to WhatsApp:
A screen with all the user chats,
This screen (or part of it) should update every time a chat gets a new message, showing the last message snippet and turning into bold if not read,
The shown chats should change between unarchived and archived, depending on a button in the UI.
Couldn’t have picked up an easier starting project, right? ;) (now every time I look at WhatsApp I say wow!)
Regarding firebase/firestore I’m fetching 2 different collections for this:
the sub-collection ‘chats’ within the ‘user_chats’ collection: where I get all the chat Ids plus it’s status (if this chat is archived and if the last message was read), for the current user,
the main ‘chats’ collection, where I have the main info of each chat.
At this moment I’m doing this:
In the chats_screen (UI) I’m fetching my chat provider: userChatsProvider
(note: this is just an example of the UI implementation. I have another implementation for it, but as long I get the chatName and lastMsgContent updated for each chat, perfect.)
class ChatsScreen extends ConsumerWidget {
const ChatsScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
(…)
return Scaffold(
appBar: (…) // not relevant for this question
body: Center(
child: Consumer(
builder: (BuildContext context, WidgetRef ref, Widget? child) {
return ref.watch(userChatsProvider).when(
loading: () => const CircularProgressIndicator(),
error: (err, st) => Center(child: Text(err.toString())),
data: (chatData) {
return Column(
children: [
// button to get archived chats
child: TextButton(
child: (…)
onPressed: () {}),
),
Expanded(
child: ListView.builder(
itemCount: chatData.length,
itemBuilder: (ctx, index) => Row(
children: [
Text (chatData[index].chatName!),
Text (chatData[index].lastMsgContent!),
]
),
),
),
]
);
}
);
}
)
)
);
}
}
In the chats_provider (provider) I’m fetching the 2 repository providers and joining them into a specific model I’ve created for this screen:
final userChatsProvider = FutureProvider.autoDispose<List<ChatScreenModel>>((ref) async {
const String userId = ‘XXX’; // this will be substituted by a dynamic variable with the current user Id
try {
final List<UserChat> userChats =
await ref.watch(userChatsRepositoryProvider).get(userId: userId);
final List<String> chatIdList = userChats.map<String>((e) => e.id).toList();
final List<Chat> chats =
await ref.watch(chatsRepositoryProvider).get(chatIds: chatIdList);
// ref.maintainState = true;
return toChatScreenModel(chats, userChats);
} on Exception catch (e) {
throw const CustomException();
}
});
In the repositories I’m fetching the firestorm collections I mentioned above. Here’s an example:
final userChatsRepositoryProvider =
Provider<UserChatsRepository>((ref) => UserChatsRepository(ref.read));
class UserChatsRepository {
final Reader _read;
const UserChatsRepository(this._read);
Future<List<UserChat>> get({required String userId}) async {
try {
final snap = await _read(firebaseFirestoreProvider)
.collection('users_chats/$userId/chats')
.get();
// Maybe this step is not necessary, but I’ve decided to transform the data into temporary models, before sending to provider
List<UserChat> userChats =
snap.docs.map((doc) => UserChat.fromJson(doc.data(), doc.id)).toList();
return userChats;
} on FirebaseException catch (e) {
throw CustomException(message: e.message);
}
}
}
And by the way, this is the model I’m sending to the UI:
class ChatScreenModel {
final String? id;
final String? chatName;
final String? chatImage;
final String? lastMsgContent;
final String? lastMsgDate;
final ChatType? chatType;
final bool? archived;
final bool? msgRead;
ChatScreenModel({
this.id,
this.chatName,
this.chatImage,
this.lastMsgContent,
this.lastMsgDate,
this.chatType,
this.archived,
this.msgRead,
});
Problems with this implementation:
I’m getting the user chats in the screen, but they don’t update since I’m not using a stream. So I get a snapshot, but it will only update if I leave and enter that chats_screen again. And it would be important to have it updating with a stream.
I’m showing all the chats, and not a filtered list with only the unarchived chats.
Also, related with the previous point, I still don’t have the archived button working, to only show the archived chats.
I’ve lost many, many hours trying to understand how I could implement a stream provider and a state notifier provider in this workflow.
Tried many combinations, but without success.
Can anyone help me understand how to do this?
Priority: transform these providers into stream providers (so it updates the UI constantly).
Nice to have: also include the archived/unarchived dynamic to filter the chats that appear and be able to switch between them.
Thanks a lot. :)
I am creating a cross platform flutter app and started developing on the Web version. I have used provider throughout to pass various pieces of data around my app and thus far it works well.
However, when I run my app on Android, the same provider I have used to pull data from Firestore doesn't register any values. When I run on web, I get 2 when I print the length of my list of values whereas on Android, I get 0.
I appreciate there isn't a great deal of information here, but i'm wondering if anyone else has experienced the same issue? And if so, how did you resolve it? Thanks.
UPDATE - added code
Here is how I access the stream from firestore:
class OurDatabase {
final CollectionReference customCollection =
FirebaseFirestore.instance.collection('myCollection');
Stream<List<CustomClass>> get customitems {
return customCollection.snapshots().map(
(QuerySnapshot querySnapshot) => querySnapshot.docs
.map(
(document) => CustomClass.fromFirestore(document),
)
.toList(),
);
}
Here is my main.dart:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(
MultiProvider(
providers: [
StreamProvider<List<CustomClass>>.value(
value: OurDatabase().customitems,
initialData: <CustomClass>[],
),
],
child: MaterialApp(theme: OurTheme().buildTheme(), home: OurHomePage()),
),
);
}
I then access the custom list here:
class OurHeadlineList extends StatelessWidget {
#override
Widget build(BuildContext context) {
final List<CustomClass> items =
Provider.of<List<CustomClass>>(context);
print(items .length);
return Container();
}
}
I have swapped out provider for a stream builder and that works - I guess the problem lies with provider?
Like I mentioned previously, when I run on Chrome, the provider works perfectly. But when I run on Android emulator, it doesn't pick up any of the values. I am able to log into firebase through both platforms which confuses me even more. Thoughts?
I am using Firebase cloud messaging for notifications, and i want to show a dialog or snackbar once i receive a notification when i am inside the application, my problem is that i am initializing the firebase configuration at the top of my widget tree (Splash screen once the app is starting)
_fireBaseMessaging.configure(
onMessage: (Map<String, dynamic> message) async {
dynamic data = message['data'];
................ // Adding a snackbar/alertdialog here doesn't work
},
);
obviously if i set a dialog or snackbar it won't show since i need the context of my current page, is there any way to get the current context?
I also tried putting it inside the build widget of my splash screen but still the dialog isn't showing once i am on another page.
#override
Widget build(BuildContext context) {
_fireBaseMessaging.configure(
onMessage: (Map<String, dynamic> message) async {
print("onMessage: $message");
dynamic data = message['data'];
if (data['id'] == '1') {
newPro = true;
} else if (data['id'] == '2') {
print("THIS WORKS!!!");
showDialog(
context: context,
builder: (context) => AlertDialog(
content: ListTile(
title: Text("TEST"),
subtitle: Text("TEST"),
),
actions: <Widget>[
FlatButton(
child: Text("OK"),
onPressed: () => Navigator.pop(context),
)
],
));
}
},
);
I had the exact same issue, but I found a brilliant thread on GitHub. Basically, you can create a navigatorKey and pass that in to MaterialApp, and then use that navigatorKey to change route.
See how in this thread: https://github.com/brianegan/flutter_redux/issues/5#issuecomment-361215074
I ended up using Overlay support:
https://pub.dev/packages/overlay_support
It is basically called at the very beginning of my tree just like wrapping providers at the main.dart, it worked like a charm, nothing else worked at all!
Also here is a tutorial that helped me a lot:
https://medium.com/flutter-community/in-app-notifications-in-flutter-9c1e92ea10b3
Because it makes me uncomfortable to have the answer embedded in a link, here is the answer (credit to xqwzts on Github).
Use a GlobalKey you can access from anywhere to navigate:
Create the key:
final GlobalKey<NavigatorState> navigatorKey = new GlobalKey<NavigatorState>();
Pass it to your App:
new MaterialApp(
title: 'MyApp',
onGenerateRoute: generateRoute,
navigatorKey: navigatorKey,
);
Push routes:
navigatorKey.currentState.pushNamed('/someRoute');
An elegant solution to this problem is to use GlobalKey. That'll let you find the current BuildContext and do things with it.
You make a file called eg. global.dart looking like this:
import 'package:flutter/material.dart';
class GlobalVariable {
static final GlobalKey<NavigatorState> navState = GlobalKey<NavigatorState>();
}
You use this in your main() and MaterialApp() like this:
import 'global.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'fcm.dart'; // My Firebase Cloud Messaging code
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'screens/welcome_screen.dart';
void main() {
print('Running main()');
WidgetsFlutterBinding.ensureInitialized();
Firebase.initializeApp();
initializeFcm('', GlobalVariable.navState); // Sending the global key when initializing Firebase Cloud Messaging
FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: WelcomeScreen(),
navigatorKey: GlobalVariable.navState, // Putting the global key in the MaterialApp
);
}
}
Then, in the file that handles Firebase Cloud Messaging, which I've named fcm.dart, you'll be able to use the GlobalKey to find the current context and use it, for example like this:
import 'package:blackbox/global.dart';
import 'online_screens/game_hub_screen.dart';
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
void initializeFcm(String token, GlobalKey myGlobalKey) async {
print('Initializing Firebase Cloud Messaging...');
await Firebase.initializeApp();
FirebaseMessaging.onMessageOpenedApp.listen((remoteMsg) {
// Using the currentContext found with GlobalKey:
Navigator.push(GlobalVariable.navState.currentContext, MaterialPageRoute(builder: (context) {
return GameHubScreen();
}));
});
}
do the initializing inside a build method of your first widget in the tree ! which normally it called an App widget and it is StateLess StateFull widget and inside the build method you have access to the BuildContext