how to know whether the screen is in front or background - flutter

i want to know the screen(StatelessWidget) is in front or background.
for now, a screen in background shows AlertDialog, but i want to show the dialog only when the screen is in front.
is there any way to do that?
here's my code using hooks_riverpod.
i omitted some lines because of "mostly code" warning.
final _provider = provider801x;
class MyPage801x extends HookConsumerWidget {
MyPage801x({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
final String? message = ref.watch(_provider).message;
if (message != null) {
ref.read(_provider).done();
showResult(context, message); //don't want to show when in background.
}
return Scaffold(
appBar: AppBar(
title: Text('main screen'),
),
body: Container(),
);
}
Future<void> showResult(BuildContext context, String message) async {
Future.delayed(Duration(milliseconds: 500), () async {
await showOKOnlyDialog(context, message: message);
});
}
}
final provider801x = ChangeNotifierProvider.autoDispose((ref) => Notifier801x(ref));
class Notifier801x extends ChangeNotifier {
Notifier801x(this._ref) {
_iapManager = IAPManager4Android();
_refreshDataset();
_iapListener = _iapManager.onChangedIAP.listen((IAPTransactionStateEnum state) async {
_message = state.message! + ' #801x';
notifyListeners();
});
}
String? _message = null;
String? get message => _message;
void done() {
_message = null;
}
void _refreshDataset() {
//fetch data
notifyListeners();
}
}

this code worked!
if (ModalRoute.of(context)?.isCurrent ?? false) {
showResult(context, message);
}
I got this answer from the page below. thank you all!
Flutter | How to know if widget is on top of navigation stack (visible)

You could use mounted for this case.
mounted is true if the widget is visible to the user or technically on the top of the stack of screens otherwise will be false.
So, you can use it like this way
if(mounted){
//do your stuff like show dialog or snackbar
}

Use a static string (or DI ) in main function , set it to class name in build function of any screen. check this variable to know which screen is visible.

Related

Android back button action

I'm dealing with back actions. I'm not able to achieve good results with WillPopScope() (it's only called with top app return button but not called with android back button).
In my app, I have several pages and when android back button is pressed, I don't see the previous page.
For example, I have main-page1-page2-page3
If I'm in page 3 and press Android back button I return to page1, not to page2. In other cases it returns to main...How it is possible? Is there a way to define de pages "order"?
EDIT
This is my code that I shared in a previous question.
class Calendario1 extends StatelessWidget {
final List listaini;
Calendario1(this.listaini);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: "ATGapp",
home:Cal1(listaini: listaini),
);
}
}
class Cal1 extends StatefulWidget {
final List listaini;
Cal1({Key? key,required this.listaini}) : super(key: key);
#override
///
_Cal1State createState() => _Cal1State();
}
class _Cal1State extends State<Cal1> {
#override
void initState() {
getImage(path1);
super.initState();
}
String url_1 = '';
getImage(String path1) async {
//String url='';
final ref1 = FirebaseStorage.instance.ref().child(path1);
var url1 = await ref1.getDownloadURL();
setState(() => url_1 = url1);
}
final FirebaseStorage storage =
FirebaseStorage.instance;
String path1 = 'fondos/mons.jpeg';
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: ()async{
print('Bck button pressed');
return false;
},
child: Scaffold(
body: Column(...//and so on
add Navigator.pop() inside onWillPop and return true.
onWillPop: () async {
// do something here
return true;
},
return new WillPopScope(
onWillPop: _willPopCallback,
child: new Scaffold(
//then the rest of your code...
Thank your for all your help.
The final solution has been a new structure of the app pages. Until now, every page was a different dart file and with this structure, the behaviour of willpopscope and any other solutions didn't work for me.
Now, all pages are in the same file ordered by routes definitions in MaterialApp. Now, I'm able to catch the android back button click and work with it.
I hope this can be helpful for others.

Trying to use Hivedb with a multi state nav bar app

I'm trying to use the hive db to store simple objects in an app that has 3 main pages, selected with a nav bar in the following form (following closely the example from the flutter docs).
/// determine body widget ie page to be rendered
int _pageIndex = 0;
/// list of body widgets
static const List<Widget> _pageOption = [
KeyList(), //index 0
PersonalKey(), //index 1
Crypt(), //index 2
];
/// [index] tells body of scaffold what widget to render
void _changePage(int index) {
setState(() {
_pageIndex = index;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
widget.title,
style: const TextStyle(color: cyan),
),
),
body: Center(
child: _pageOption.elementAt(_pageIndex),
),
I'm not sure how to get the hive box to open / work with each page. for instance a Box would need to used in the KeyList() page. I tried passing the box through state but this is warned against and didn't work.
I tried varies combinations of an async main function and an overriden innitState function in _KeyListState() and now I'm not getting any widgets to render.
main function (from main.dart)
void main() async {
await Hive.initFlutter();
await Hive.openBox<ContactKey>('contacts');
Hive.registerAdapter(ContactKeyAdapter());
runApp(const MyApp());
}
reference from key_list.dart
class _KeyListState extends State<KeyList> {
late final Box contactBox;
#override
void initState() {
super.initState();
contactBox = Hive.box("contacts");
// makeKeyList();
}
#override
void dispose() {
Hive.close();
super.dispose();
}
final List<ContactKey> _keys = []; // = [
// ContactKey(contactName: "cade", publicKey: "123")
// ];
void addKey(String name, String key) async {
ContactKey newContact = ContactKey(contactName: name, publicKey: key);
setState(() {
contactBox.add(newContact);
makeKeyList();
});
}
void makeKeyList() {
if (contactBox.isNotEmpty) {
for (var i = 0; i < contactBox.length; i++) {
_keys.add(contactBox.getAt(i));
}
}
}
#override
Widget build(BuildContext context) {
return Stack(
...
I'm not getting any errors or warnings but when the app is running I get the error "each child must be laid out only once" A google search on this error made it seem like a flutter bug, but if I remove the Hive code my app renders again.
I'm pretty lost right now and if anyone has any tips or sample apps that use a nav bar with hive they'd be greatly appreciated!
Thanks

Flutter GetX state management initial null value

This is what I'm trying to achieve using flutter GetX package but not working properly.
I have a Firestore document, if the document is changed I want to call an api and keep the data up to date as observable.
The code below seems to work but initial screen shows null error then it shows the data.
I don't know how I can make sure both fetchFirestoreUser() and fetchApiData() (async methods) returns data before I move to the home screen.
GetX StateMixin seems to help with async data load problem but then I don't know how I can refresh the api data when the firestore document is changed.
I'm not sure if any other state management would be best for my scenario but I find GetX easy compared to other state management package.
I would very much appreciate if someone would tell me how I can solve this problem, many thanks in advance.
Auth Controller.
class AuthController extends SuperController {
static AuthController instance = Get.find();
late Rx<User?> _user;
FirebaseAuth auth = FirebaseAuth.instance;
var _firestoreUser = FirestoreUser().obs;
var _apiData = ProfileUser().obs;
#override
void onReady() async {
super.onReady();
_user = Rx<User?>(auth.currentUser);
_user.bindStream(auth.userChanges());
//get firestore document
fetchFirestoreUser();
//fetch data from api
fetchApiData();
ever(_user, _initialScreen);
//Refresh api data if firestore document has changed.
_firestoreUser.listen((val) {
fetchApiData();
});
}
Rx<FirestoreUser?> get firestoreUser => _firestoreUser;
_initialScreen(User? user) {
if (user == null) {
Get.offAll(() => Login());
} else {
Get.offAll(() => Home());
}
}
ProfileUser get apiData => _apiData.value;
void fetchFirestoreUser() async {
Stream<FirestoreUser> firestoreUser =
FirestoreDB().getFirestoreUser(_user.value!.uid);
_firestoreUser.bindStream(firestoreUser);
}
fetchApiData() async {
var result = await RemoteService.getProfile(_user.value!.uid);
if (result != null) {
_apiData.value = result;
}
}
#override
void onDetached() {}
#override
void onInactive() {}
#override
void onPaused() {}
#override
void onResumed() {
fetchApiData();
}
}
Home screen
class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
child: Obx(() =>
Text("username: " + AuthController.instance.apiData.username!))),
),
);
}
}
To be honest, I never used GetX so I'm not too familiar with that syntax.
But I can see from your code that you're setting some mutable state when you call this method:
fetchApiData() async {
var result = await RemoteService.getProfile(_user.value!.uid);
if (result != null) {
_apiData.value = result;
}
}
Instead, a more robust solution would be to make everything reactive and immutable. You could do this by combining providers if you use Riverpod:
final authStateChangesProvider = StreamProvider.autoDispose<User?>((ref) {
final authService = ref.watch(authRepositoryProvider);
return authService.authStateChanges();
});
final apiDataProvider = FutureProvider.autoDispose<APIData?>((ref) {
final userValue = ref.watch(authStateChangesProvider);
final user = userValue.value;
if (user != null) {
// note: this should also be turned into a provider, rather than using a static method
return RemoteService.getProfile(user.uid);
} else {
// decide if it makes sense to return null or throw and exception when the user is not signed in
return Future.value(null);
}
});
Then, you can just use a ConsumerWidget to watch the data:
#override
Widget build(BuildContext context, WidgetRef ref) {
// this will cause the widget to rebuild whenever the auth state changes
final apiData = ref.watch(apiDataProvider);
return apiData.when(
data: (data) => /* some widget */,
loading: () => /* some loading widget */,
error: (e, st) => /* some error widget */,
);
}
Note: Riverpod has a bit of a learning curve (worth it imho) so you'll have to learn it how to use it first, before you can understand how this code works.
Actually the reason behind this that you put your controller in the same page that you are calling so in the starting stage of your page Get.put() calls your controller and because you are fetching data from the API it takes a few seconds/milliseconds to get the data and for that time your Obx() renders the error. To prevent this you can apply some conditional logic to your code like below :
Obx(() => AuthController.instance.apiData != null ? Text("username: " + AuthController.instance.apiData.username!) : CircularProgressIndicator())) :

Future Provider Stuck In loading state

I am using a future provider to display a login page on load and then a loading indicator on loading. Here is my future provider
final loginProvider = FutureProvider.family((ref, UserInput input) =>
ref.read(authRepositoryProvider).doLogin(input.email, input.password));
In my UI I have this....
class LoginScreen extends HookWidget {
final TextEditingController emailEditingController = TextEditingController();
final TextEditingController passwordEditingController =
TextEditingController();
#override
Widget build(BuildContext context) {
var userInput =
UserInput(emailEditingController.text, passwordEditingController.text);
final login = useProvider(loginProvider(userInput));
return login.when(
data: (user) => Login(emailEditingController, passwordEditingController),
loading: () => const ProgressIndication(),
error: (error, stack) {
if (error is DioError) {
return Login(emailEditingController, passwordEditingController);
} else {
return Login(emailEditingController, passwordEditingController);
}
},
);
}
}
here is my doLogin function.
#override
Future<dynamic> doLogin(String email, String password) async {
try {
final response = await _read(dioProvider)
.post('$baseUrl/login', data: {'email': email, 'password': password});
final data = Map<String, dynamic>.from(response.data);
return data;
} on DioError catch (e) {
return BadRequestException(e.error);
} on SocketException {
return 'No Internet Connection';
}
}
I would like to know why it's stuck in the loading state. Any help will be appreciated.
First off, family creates a new instance of the provider when given input. So in your implementation, any time your text fields change, you're generating a new provider and watching that new provider. This is bad.
In your case, keeping the UserInput around for the sake of accessing the login state doesn't make a lot of sense. That is to say, in this instance, a FamilyProvider isn't ideal.
The following is an example of how you could choose to write it. This is not the only way you could write it. It is probably easier to grasp than streaming without an API like Firebase that handles most of that for you.
First, a StateNotifierProvider:
enum LoginState { loggedOut, loading, loggedIn, error }
class LoginStateNotifier extends StateNotifier<LoginState> {
LoginStateNotifier(this._read) : super(LoginState.loggedOut);
final Reader _read;
late final Map<String, dynamic> _user;
static final provider =
StateNotifierProvider<LoginStateNotifier, LoginState>((ref) => LoginStateNotifier(ref.read));
Future<void> login(String email, String password) async {
state = LoginState.loading;
try {
_user = await _read(authRepositoryProvider).doLogin(email, password);
state = LoginState.loggedIn;
} catch (e) {
state = LoginState.error;
}
}
Map<String, dynamic> get user => _user;
}
This allows us to have manual control over the state of the login process. It's not the most elegant, but practically, it works.
Next, a login screen. This is as barebones as they get. Ignore the error parameter for now - it will be cleared up in a moment.
class LoginScreen extends HookWidget {
const LoginScreen({Key? key, this.error = false}) : super(key: key);
final bool error;
#override
Widget build(BuildContext context) {
final emailController = useTextEditingController();
final passwordController = useTextEditingController();
return Column(
children: [
TextField(
controller: emailController,
),
TextField(
controller: passwordController,
),
ElevatedButton(
onPressed: () async {
await context.read(LoginStateNotifier.provider.notifier).login(
emailController.text,
passwordController.text,
);
},
child: Text('Login'),
),
if (error) Text('Error signing in'),
],
);
}
}
You'll notice we can use the useTextEditingController hook which will handle disposing of those, as well. You can also see the call to login through the StateNotifier.
Last but not least, we need to do something with our fancy new state.
class AuthPage extends HookWidget {
const AuthPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final loginState = useProvider(LoginStateNotifier.provider);
switch (loginState) {
case LoginState.loggedOut:
return LoginScreen();
case LoginState.loading:
return LoadingPage();
case LoginState.loggedIn:
return HomePage();
case LoginState.error:
return LoginScreen(error: true);
}
}
}
In practice, you're going to want to wrap this in another widget with a Scaffold.
I know this isn't exactly what you asked, but thought it might be helpful to see another approach to the problem.

Flutter - Show only one Dialog at the time

I am working on a flutter application using several dialogs for several purposes.
In our code, there are some cases where the user can open a Dialog. Inside this dialog, there are some buttons that will also open another dialog. It results with 2 dialogs on top of each other and with a very dark background screen.
What we would like to do is to only display one dialog at the time. How can we achieve that ?
Here is a simple code to illustrate our issue:
class MyScreen extends StatelessWidget {
#override
build(BuildContext context) {
return FlatButton(
child: Text('Button'),
onPressed: () async {
final resultDialog = await showDialog<ResultDialog1>(
context: context,
builder: (BuildContext context) => MyFirstDialog(),
);
// Do some stuff with the result, so this part of the tree cannot be destroyed
},
);
}
}
class MyFirstDialog extends StatelessWidget {
#override
build(BuildContext context) {
return FlatButton(
child: Text('Button in first dialog'),
onPressed: () async {
final resultDialog = await showDialog<ResultDialog2>(
context: context,
builder: (BuildContext context) => MySecondDialog(), // <- This will appear on top of the first dialog
);
// Do some stuff with the result, so this part of the tree cannot be destroyed
},
);
}
}
class MySecondDialog extends StatelessWidget {
#override
build(BuildContext context) {
return Text('Second Dialog');
}
}
let me give you a widget for that
class MultiDialog extends StatefulWidget {
final Widget child;
const MultiDialog({Key key, this.child}) : super(key: key);
#override
_MultiDialogState createState() => _MultiDialogState();
static void addDialog(
{#required BuildContext context, #required Widget dialog}) {
assert(context != null, "the context cannot be null");
assert(dialog != null, "the dialog cannot be null");
context.findAncestorStateOfType<_MultiDialogState>()._addDialog(dialog);
}
static void remove({#required BuildContext context}) {
assert(context != null, "the context cannot be null");
context.findAncestorStateOfType<_MultiDialogState>()._remove();
}
}
class _MultiDialogState extends State<MultiDialog> {
final _allDialogs = <Widget>[];
void _addDialog<T>(Widget dialog) {
assert(dialog != null, "The dialog cannot be null");
setState(() {
_allDialogs.add(dialog);
});
}
void _remove() {
if (_allDialogs.isEmpty) {
print("No dialogs to remove");
return;
}
setState(() {
_allDialogs.removeLast();
});
}
#override
Widget build(BuildContext context) {
return Stack(
children: [
widget.child,
if (_allDialogs.isNotEmpty) _allDialogs.last,
],
);
}
}
and when you want to add a dialog just call
MultiDialog.addDialog(
context: context,
dialog: AlertDialog(),
);
call Navigator.pop to remove the dialog, if there is another dialog which you pushed exist it will be shown, you can further pop them all with results, PS:this code isn't tested, let me know in the comments if this works for you
call MultiDialog.remove(context:context) to pop the visible dialog and bring back the previous dialog,
and if you receive a error that the addDialog is called on null, its because how flutter works, after MultiDialog use a Builder to introduce a new context use it call showDialog,
PS:ABOVE CODE IS TESTED
i made a stream out of the events that cause the dialog to pop up and used rx darts exhaust map to wait for the result (i was already using rxdart)
dialogEventStream
.exhaustMap((_) => maybeShowDialog().asStream())
.listen((_) {});
Future<bool> maybeShowOfflineModeDialog() async {
final isOfflineModeEnabled = await _sharedPreferencesService.isOfflineModeEnabled();
if (!isOfflineModeEnabled) {
final isLoginOffline = await _navigationService.showDialog(NoConnectionDialog());
if (isLoginOffline == true) {
await _sharedPreferencesService.setIsOfflineModeEnabled(isOfflineModeEnabled: true);
return await _navigationService.pushReplacement(AppShellOffline.routeName) ?? true;
} else {
return false;
}
}
return false;
}
something like this