Flutter - Firebase Dynamic Link is not caught by onLink but open the app on iOS - ios

Everything works fine on android but on ios when the app is already opened clicking the link takes the app in the foreground but the onLink method is not call.
Link:
https://<url>/?link=<link>&apn=<apn>&ibi=<bundle>&isi=<isi>
Package:
firebase_dynamic_links: ^0.6.3
Code
import 'package:firebase_dynamic_links/firebase_dynamic_links.dart';
import 'package:flutter/material.dart';
class DynamicLinksService {
Future handleDynamicLinks(BuildContext context) async {
final PendingDynamicLinkData data =
await FirebaseDynamicLinks.instance.getInitialLink();
await _handleDynamicLink(context, data);
FirebaseDynamicLinks.instance.onLink(
onSuccess: (PendingDynamicLinkData dynamicLinkData) async {
await _handleDynamicLink(context, dynamicLinkData);
}, onError: (OnLinkErrorException e) async {
print('Dynamic link failed: ${e.message}');
});
}
Future _handleDynamicLink(
BuildContext context, PendingDynamicLinkData data) async {
final Uri deepLink = data?.link;
if (deepLink != null) {
print('_handleDeepLink | deepLink $deepLink');
await _doSomething(context, deepLink.toString());
} else {
print('no deepLink');
}
}
}

From my experimentation, onLink is not called on iOS however you can call getInitialLink() and it will contain the link. I'm uncertain if this is by design or a bug, but it seems to be across a few versions.
Example service code:
Future<Uri> retrieveDynamicLink(BuildContext context) async {
try {
final PendingDynamicLinkData data = await FirebaseDynamicLinks.instance.getInitialLink();
final Uri deepLink = data?.link;
return deepLink;
} catch (e) {
print(e.toString());
}
return null;
}
Widget snippet
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed){
final _timerLink = Timer(
const Duration(milliseconds: 1000),
() async {
final auth = Provider.of<FirebaseAuthService>(context, listen: false);
final link = await auth.retrieveDynamicLink(context);
_handleLink(link);
},
);
}
}
Make sure to add the WidgetsBindingObserver
class _SignInPageState extends State<SignInPage> with TickerProviderStateMixin, WidgetsBindingObserver{

I'm not sure why this works but you'd first have to call FirebaseMessaging.instance.getInitialMessage() at least once before your onLink callback is activated by Firebase.
Not sure if this is by design or a bug.
Let me know if this works.

Related

Open the youtube app from flutter app on iOS

I basically want to open a specific youtube video from my app, when a button is pressed. If the youtube app is installed on the user's device, then the video should be opened in the youtube app (and not in the browser or a separate webview).
I used the url_launcher package for that, and it works fine on android. However on iOS the youtube app is not opened even if it is installed, instead a separate web window is opened, where the corresponding youtube url is shown as a webpage.
I thought, that I could override this behaviour like so:
_launchURL() async {
if (Platform.isIOS) {
if (await canLaunch('youtube://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw')) {
await launch('youtube://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw');
} else {
if (await canLaunch('https://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw')) {
await launch('https://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw');
} else {
throw 'Could not launch https://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw';
}
}
} else {
const url = 'https://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw';
if (await canLaunch(url)) {
await launch(url);
} else {
throw 'Could not launch $url';
}
}
}
but it didn’t work. In case you wonder, I use the following imports:
import 'dart:io' show Platform;
import 'package:url_launcher/url_launcher.dart';
I am pretty sure, the youtube:// URL-Scheme works (launches the YouTube app), because I tested it on third party apps (Launch Center Pro and Pythonista).
The last thing I was not able to test, is if the Platform.isIOS is really true on my IPhone.
Is there a working way, to open the YouTube App from flutter?
I fixed it. I had to set forceSafariVC: false, because it is true on default, which causes the url to be opened inside a sort of webview inside the app.
_launchURL() async {
if (Platform.isIOS) {
if (await canLaunch('youtube://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw')) {
await launch('youtube://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw', forceSafariVC: false);
} else {
if (await canLaunch('https://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw')) {
await launch('https://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw');
} else {
throw 'Could not launch https://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw';
}
}
} else {
const url = 'https://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw';
if (await canLaunch(url)) {
await launch(url);
} else {
throw 'Could not launch $url';
}
}
This is actually documented in the url_launcher docs, but somewhat hidden...
You don't have to have all that if/else clauses. The most important thing to take into consideration is that whether the device has the YouTube app or not, regardless of the O.S (and remember to define your function as Future because of the async):
Future<void> _launchYoutubeVideo(String _youtubeUrl) async {
if (_youtubeUrl != null && _youtubeUrl.isNotEmpty) {
if (await canLaunch(_youtubeUrl)) {
final bool _nativeAppLaunchSucceeded = await launch(
_youtubeUrl,
forceSafariVC: false,
universalLinksOnly: true,
);
if (!_nativeAppLaunchSucceeded) {
await launch(_youtubeUrl, forceSafariVC: true);
}
}
}
}
The thing to highlight here to avoid several if/else si the attribute universalLinksOnly set to true.
I have solved the issue. You can try the below code:
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
void main() => runApp(const HomePage());
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: MyApp(),
);
}
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Future<void>? _launched;
#override
Widget build(BuildContext context) {
const String toLaunch = 'https://www.youtube.com/watch?v=WcZ8lTRTNM0';
return Scaffold(
appBar: AppBar(title: const Text('Flutter Demo')),
body: Center(
child: ElevatedButton(
onPressed: () => setState(() {
_launched = _launchInBrowser(toLaunch);
}),
child: const Text('Launch in browser'),
),
), );
}
Future<void> _launchInBrowser(String url) async {
if (!await launch(
url,
forceSafariVC: true,
forceWebView: false,
headers: <String, String>{'my_header_key': 'my_header_value'},
)) {
throw 'Could not launch $url';
}
}
}
Using same package:
https://pub.dev/packages/url_launcher
Here is latest working example. Most answers above is outdated or using deprecated package. Default mode is LaunchMode.platformDefault. Change to LaunchMode.externalApplication will open youtube app. Hope this helps
Future<dynamic> openUrl(String url, callback) async {
try {
if (await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication)) {
callback(true);
} else {
toastMessage('#1: Could not launch $url');
callback(false);
}
} catch (e) {
toastMessage('#2: Could not launch $url');
callback(false);
}
}

Does buildSuggestions of showSearch method run continuously?

I'm new in flutter and I have a function to search working with API,and i have created a function called _fetchData() inside StatefulWidget:
_fetchData() async {
print("fetchData Run");
print(widget.query);
this.setState((){
isLoading=true;
});
final response = await http.get(URL+'search/keyword?api_key='+API_KEY+'&query='+widget.query);
if (response.statusCode == 200) {
print(response.body);
var list = json.decode(response.body)["results"] as List;
keywordList = list.map<Keyword>((json)=>Keyword.fromJson(json)).toList();
this.setState((){
isLoading=false;
});
} else {
throw Exception('Failed to load photos');
}
}
and i put my StatefulWidget in buildSuggestions like:
class SearchHome extends SearchDelegate {
#override
List<Widget> buildActions(BuildContext context) {
//widget
}
#override
Widget buildLeading(BuildContext context) {
//widget
}
#override
Widget buildResults(BuildContext context) {
//widget
}
#override
Widget buildSuggestions(BuildContext context) {
//SuggestKeyword is my statefullwidget with _fetchData() function inside
return SuggestKeyword(query: query);
}
}
but i'm confuse why _fetchData() keep running continuously, that's mean my apps keep hit an API, i think it's not good so i want to avoid it
I know i can put SuggestKeyword inside buildResults, but these method need user to submit/enter the keyboard to run, i want to search while the user is still typing, but buildSuggestions keep running even when user is not typing,
am i doing something wrong? any suggestion would appreciated!

Flutter Shared Preferences Auth FIle

I'm trying to write an auth file, with a list of finals with shared preferences values in it. I could import that auth file in my other files and i could get like the name or email without importing shared preferences in every file.
It would probably look way smoother and cleaner.
I thought something like this would have worked but it didn't
/// ------------Auth------------ ///
final email = getEmail();
getEmail() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getString('email');
}
Does anybody have any idea how to do this?
Greetings,
Jente
I assume you want to use the method in multiple files. The problem with your code is that the getEmail method is marked async that means it will have to return a Future. Think about it like this, when you mark a method as async it means it will return something (or finish executing) in the near future. When ? Well you don't know exactly when, so you'll need to get "notified" when the method is "done", that's why you'll use a Future. Something like this:
Future<String> getEmail() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getString('email');
}
class ThisIsTheClassWhereYouWantToUseTheFunction {
//let's say you have a method in your class called like the one below (it can be any other name)
void _iNeedTheEmailToPrintIt() {
//this is the way you can call the method in your classes, this class is just an example.
getEmail().then((thisIsTheResult){ // here you "register" to get "notifications" when the getEmail method is done.
print("This is the email $thisIsTheResult");
});
}
}
you can define a class Auth or much better a scoped_model.
Here's a class implementation
class Auth {
get email() {
final SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getString('email');
}
set email(String em) {
final SharedPreferences prefs = await SharedPreferences.getInstance();
pref.setString('email', em);
}
}
and now you can call it in your widgets :)
Try this;
make dart file (Filename and Class Name ShareUtils)
add follow Code
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:async';
class ShareUtils {
static ShareUtils _instance;
SharedPreferences ShareSave;
factory ShareUtils() => _instance ?? new ShareUtils._();
ShareUtils._();
void Instatce() async {
ShareSave = await SharedPreferences.getInstance();
}
Future<bool> set(key, value) async {
return ShareSave.setString(key, value);
}
Future<String> get(key) async {
return ShareSave.getString(key);
}
}
2.Add main.dart
class MyApp extends StatelessWidget {
static ShareUtils shareUtils;
#override
Widget build(BuildContext context) {
ThemeData mainTheme = new ThemeData(
);
shareUtils = new ShareUtils();
shareUtils.Instatce();
MaterialApp mainApp = new MaterialApp(
title: "Your app",
theme: mainTheme,
home: new SplashPage(),
debugShowCheckedModeBanner: true,
routes: <String, WidgetBuilder>{
"RegisterPage": (BuildContext context) => new RegisterPage(),
"HomePage": (BuildContext context) => new HomePage(),
},
);
return mainApp;
}
}
3.SET
void UserInfo(code, token) async{
await MyApp.shareUtils.set("token", token);
await MyApp.shareUtils.set("code", code);
await Navigator.of(context).pushNamed("HomePage");
}
4.GET
Future NextPage() async {
MyApp.shareUtils.get("token").then((token) {
print(token);
if (token == null || token == "") {
Navigator.of(context).popAndPushNamed("RegisterPage");
} else {
Navigator.of(context).popAndPushNamed("HomePage");
}
});
}
Hope to help.

Flutter - How to get value from shared preferences in a non-async method

I am trying to get some values saved in the SharedPreferences from a getter method of a class. But SharedPreferences.getInstance() returns a Future. Is there a way to obtain the SharedPreferences object in a non-async getter methods, for example:
import 'package:shared_preferences/shared_preferences.dart';
class MyClass {
get someValue {
return _sharedPreferencesObject.getString("someKey");
}
}
Is there something in Dart that is similar to .Result property in C#, for example getSomethingAsync().Result (https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1.result?view=netframework-4.7.2)?
You can use FutureBuilder()
SharedPreferences sharedPrefs;
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _getPrefs(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return YourFinalWidget();
}
return CircularProgressIndicator(); // or some other widget
},
);
}
Future<void> _getPrefs() async{
sharedPrefs = await SharedPreferences.getInstance();
}
You can do it in initState() and after this call setState() to update your build() method. Other way is to use FutureBuilder()
SharedPreferences sharedPrefs;
#override
void initState() {
super.initState();
SharedPreferences.getInstance().then((prefs) {
setState(() => sharedPrefs = prefs);
});
}
I suggest you use GetStorage
https://pub.dev/packages/get_storage
you will be able to get values without awaiting for them
main.dart
await GetStorage.init();
runApp(MyApp());
storage.dart
static bool isFirstOpen() {
final box = GetStorage();
return box.read("isFirstOpen") ?? true;
}
static setFirstOpen(bool isopen) {
final box = GetStorage();
box.write("isFirstOpen", isopen);
}
You can do something like this
Create a separate shared preferences service
Take in the SharedPreferences value in the constructor
You can then do get call without async, await
Example
class SharedPreferencesService {
final SharedPreferences _prefs;
const SharedPreferencesService(this._prefs);
Future<void> setString(String key, String value) async {
await _prefs.setString(key, value);
}
String? getString(String key) {
if (containsKey(key)) {
return _prefs.getString(key);
} else {
return null;
}
}
bool containsKey(String key) {
return _prefs.containsKey(key);
}
}
The only async call in SharedPreferences is to get the initial instance. This can be reused across your application. main can be async. So you can just await the SharedPreferences instance in main:
late SharedPreferences prefs;
main() async {
prefs = await SharedPreferences.getInstance();
runApp(App());
}
Now you can use prefs anywhere without resorting to async code. SharedPreferences will serve as a nonblocking write-through cache, with write operations running asynchronously in the background.
You can call a function on initState() and save SharedPreferences in a variable, so you can use in a non-async method, like this:
SharedPreferences prefs;
Future<void> loadPrefs() async {
prefs = await SharedPreferences.getInstance();
}
#override
void initState(){
super.initState();
loadPrefs();
}

Flutter How to make it automatically - (navigate to login) when http status code is 401

Here is my previous codes.
class mainScreen extends StatefulWidget
{
Api api = new Api();
override
Widget build(BuildContext context) {
return new MaterialButton(
onPressed: () async{
new Future.then(
api.doSthA(),onError: (e) {
Navigator.of(context).pushNamed("/login"); //this is repeat
})
new Future.then(
api.doSthB(),onError: (e) {
Navigator.of(context).pushNamed("/login"); //this is repeat
}
)
}
);
}
}
class Api
{
Future<dynamic> doSthA() async{
return http
.post(
"url"
)
.then((http.Response res) {
if(res.body.statusCode == 401){
throw new Exception("401");
}else{
return _decoder.convert(res);
}
}
}
Future<dynamic> doSthB() async{
similar with doSthA
}
}
I want it simplify to
new MaterialButton(
onPressed: () async{
new Future.then(api.doSthA())...
new Future.then(api.doSthB())...
}
auto execute Navigator to login when api return 401.
Because Navigator must need widget's content.so I have no idea how to let it integrate with Api class.I want make Navigator be part of Api manager.
Try this when you want to navigate to login
Navigator.pushNamedAndRemoveUntil(
context,
'/login', (_) => false);
Put this in build method
Api api = Api(context: context)
Api class
class Api
{
BuildContext context;
Api({this.context});
}
ApiCall Method
if(res.statusCode == 401){
Navigator.pushNamedAndRemoveUntil(
context,
'/login', (_) => false);
return null;
}

Resources