Skip to content

Commit

Permalink
🚀 feat: URL起動に失敗した場合にSnackBarでその旨を表示する
Browse files Browse the repository at this point in the history
Issue #145
  • Loading branch information
susatthi committed May 30, 2022
1 parent d8bede4 commit 4bba849
Show file tree
Hide file tree
Showing 14 changed files with 705 additions and 226 deletions.
2 changes: 2 additions & 0 deletions build.yaml
Expand Up @@ -10,12 +10,14 @@ targets:
include:
- lib/domain/**/*.dart
- lib/**/json_object/**/*.dart
- lib/**/state/*.dart
- lib/**/*state.dart
json_serializable:
generate_for:
include:
- lib/domain/**/*.dart
- lib/**/json_object/**/*.dart
- lib/**/state/*.dart
- lib/**/*state.dart
fast_i18n:
options:
Expand Down
26 changes: 26 additions & 0 deletions lib/config/app.dart
Expand Up @@ -7,6 +7,8 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../localizations/strings.g.dart';
import '../presentation/common/state/launch_url_state.dart';
import '../utils/logger.dart';
import 'router.dart';
import 'theme.dart';

Expand Down Expand Up @@ -40,10 +42,33 @@ class _GitHubSearchApp extends ConsumerWidget {

@override
Widget build(BuildContext context, WidgetRef ref) {
final scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();

// URL起動状態を監視してエラーが起きたらSnackBarを表示する
// どの画面でURL起動してもここで一括でエラーハンドリングできるようにしている
ref.listen<LaunchUrlState>(
launchUrlStateProvider,
(previous, next) {
logger.i(
'Changed LaunchUrlState: '
'status = ${next.status.name}, url = ${next.urlString}',
);
if (next.status == LaunchUrlStatus.error) {
// エラーの場合はSnackBar表示をする
scaffoldMessengerKey.currentState!.showSnackBar(
SnackBar(
content: Text(i18n.cantLaunchUrl(url: next.urlString!)),
),
);
}
},
);

final theme = ref.watch(themeProvider);
if (home != null) {
// テスト用
return MaterialApp(
scaffoldMessengerKey: scaffoldMessengerKey,
debugShowCheckedModeBanner: false,
localizationsDelegates: GlobalMaterialLocalizations.delegates,
supportedLocales: LocaleSettings.supportedLocales,
Expand All @@ -57,6 +82,7 @@ class _GitHubSearchApp extends ConsumerWidget {

final router = ref.watch(routerProvider);
return MaterialApp.router(
scaffoldMessengerKey: scaffoldMessengerKey,
debugShowCheckedModeBanner: false,
routerDelegate: router.routerDelegate,
routeInformationParser: router.routeInformationParser,
Expand Down
4 changes: 3 additions & 1 deletion lib/localizations/strings.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/localizations/strings.i18n.json
Expand Up @@ -3,6 +3,7 @@
"asc": "Asc",
"bestMatch": "Best match",
"canSearchRepos": "You can search repositories",
"cantLaunchUrl": "Can't launch $url",
"desc": "Desc",
"error": "Error",
"errorOccurred": "An error has occurred",
Expand Down
1 change: 1 addition & 0 deletions lib/localizations/strings_ja.i18n.json
Expand Up @@ -3,6 +3,7 @@
"asc": "昇順",
"bestMatch": "ベストマッチ",
"canSearchRepos": "リポジトリを検索できます",
"cantLaunchUrl": "$url を開くことができませんでした",
"desc": "降順",
"error": "エラー",
"errorOccurred": "エラーが発生しました",
Expand Down
21 changes: 12 additions & 9 deletions lib/presentation/common/components/hyperlink_text.dart
Expand Up @@ -3,44 +3,47 @@
// found in the LICENSE file.

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

import '../state/launch_url_state.dart';

/// ハイパーリンク表示するテキスト
class HyperlinkText extends StatelessWidget {
class HyperlinkText extends ConsumerWidget {
const HyperlinkText({
super.key,
required this.text,
this.url,
this.padding = EdgeInsets.zero,
this.onTap,
});

/// 表示するテキスト
final String text;

/// URL
final String? url;

/// パディング
final EdgeInsetsGeometry padding;

/// テキストタップ時のイベント
final VoidCallback? onTap;

/// アンカー表示時のテキストカラー
static const anchorColor = Colors.blueAccent;

@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final child = Padding(
padding: padding,
child: Text(text),
);

// タップイベントが無ければアンカー表示しない
if (onTap == null) {
// URLが無ければアンカー表示しない
if (url == null) {
return child;
}

return MouseRegion(
cursor: SystemMouseCursors.click,
child: InkWell(
onTap: onTap,
onTap: () => ref.read(launcherProvider)(url!),
child: DefaultTextStyle.merge(
style: const TextStyle(
color: anchorColor,
Expand Down
104 changes: 104 additions & 0 deletions lib/presentation/common/state/launch_url_state.dart
@@ -0,0 +1,104 @@
// Copyright 2022 susatthi All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.

import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:url_launcher/url_launcher.dart';

import '../../../utils/logger.dart';

part 'launch_url_state.freezed.dart';

/// URL起動のステータス
enum LaunchUrlStatus {
/// 起動前
wating,

/// 起動できた
success,

/// 起動できなかった
error;
}

/// URL起動状態
///
/// URL起動に成功したかどうかのステータスを持つ
@freezed
class LaunchUrlState with _$LaunchUrlState {
const factory LaunchUrlState({
String? urlString,
@Default(LaunchMode.platformDefault) LaunchMode mode,
@Default(LaunchUrlStatus.wating) LaunchUrlStatus status,
}) = _LaunchUrlState;
}

/// URL起動状態のプロバイダー
///
/// `ref.listen` することでURL起動状態を監視できる
final launchUrlStateProvider = StateProvider<LaunchUrlState>(
(ref) => const LaunchUrlState(),
);

/// URL起動モードプロバイダー
final launchModeProvider = Provider(
(ref) => LaunchMode.inAppWebView,
);

/// URL起動メソッドプロバイダー
///
/// UI側でこのメソッドを使ってURL起動をする
final launcherProvider = Provider(
(ref) {
final notifier = ref.read(launchUrlStateProvider.notifier);
final mode = ref.read(launchModeProvider);
return (String urlString) async {
// 状態を起動前に更新する
notifier.state = LaunchUrlState(
urlString: urlString,
mode: mode,
);

try {
final url = Uri.parse(urlString);
final result = await launchUrl(url, mode: mode);
if (result) {
logger.i('Successful launch: url = $urlString');
} else {
logger.w('Failure launch: url = $urlString');
}

// 結果に応じて状態を更新する
notifier.update(
(state) => state.copyWith(
status: result ? LaunchUrlStatus.success : LaunchUrlStatus.error,
),
);
} on FormatException catch (e, s) {
logger.e('Can\'t parse url: url = $urlString', e, s);
notifier.update(
(state) => state.copyWith(
status: LaunchUrlStatus.error,
),
);
} on PlatformException catch (e, s) {
logger.w('Failure launch: url = $urlString', e, s);
notifier.update(
(state) => state.copyWith(
status: LaunchUrlStatus.error,
),
);
// ignore: avoid_catching_errors
} on ArgumentError catch (e, s) {
logger.w('Failure launch: url = $urlString', e, s);
notifier.update(
(state) => state.copyWith(
status: LaunchUrlStatus.error,
),
);
}
};
},
);

0 comments on commit 4bba849

Please sign in to comment.