這篇教學會帶你用 Flutter 為 example.com 建立一個專屬的 APP。你只要會一點基本程式操作,就能跟著做出自己的「網站 APP」。我們會一步一步介紹 Flutter(Dart 語言)、WebView 的概念,並帶你了解建立專案、設定 APP 圖示、管理套件、修改 Android 權限的一些重點,最後會完整講解 main.dart
的寫法,觀念點通了,速度就快了!
什麼是 Flutter?什麼是 Dart?
Flutter 是 Google 推出的跨平台 UI 框架,只要寫一次程式,就能同時產生 Android、iOS、Web 的 APP。它的語言叫做 Dart,語法跟 JavaScript、Java 有點像,很適合用來快速開發漂亮的手機 APP。拿來開發簡易的 Android APP 又快又有效率,稍作修改馬上也能開發 iOS 專用的 APP,不過本文不對這部分解說。
什麼是 WebView?
WebView 就是「把網站畫面包進 APP 裡」的一種方式。APP 會像瀏覽器一樣顯示你指定的網址,你的網站怎麼更新、APP 裡就跟著變。不需重寫 APP 內容,維護也超方便。
1. 開始建立 Flutter 專案
首先你要先安裝好 Flutter 開發環境。安裝好後,在終端機輸入下列指令建立新專案(請把 my_webapp
換成你要的專案名稱):
flutter create my_webapp
進入專案資料夾:
cd my_webapp
2. 製作與放置 APP 圖示
APP 一定要有自己的圖示。可以用 AppIcon 工具 或 Figma、Photoshop 製作 APP 圖示。
接著,把生成好的 Android 圖示,依照目錄分類放進 android/app/src/main/res/
目錄底下,會有 mipmap-hdpi
、mipmap-mdpi
等子目錄(每個子目錄都是不同解析度用的)。
3. 管理套件(pubspec.yaml)
接著我們要用到兩個重要套件:
- flutter_inappwebview:WebView 元件,比官方的 webview 更強大。
- url_launcher:用來處理跳轉外部瀏覽器的情境。
打開 pubspec.yaml
,加入以下內容(版本請查詢最新):
dependencies:
flutter:
sdk: flutter
flutter_inappwebview: ^6.0.0
url_launcher: ^6.1.11
加完後執行:
flutter pub get
4. 修改 Android 權限(AndroidManifest.xml)
要讓 APP 能存取網路,一定要在 android/app/src/main/AndroidManifest.xml
中 <application> 標籤前面加入:
<uses-permission android:name="android.permission.INTERNET" />
這樣 APP 才能正確載入網頁。
5. 編寫主程式 lib/main.dart
以下是完整的主程式範例,裡面會帶你理解每個部份的用法:
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:url_launcher/url_launcher.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
if (Platform.isAndroid) {
await AndroidInAppWebViewController.setWebContentsDebuggingEnabled(true);
}
runApp(
MaterialApp(
theme: ThemeData(
primarySwatch: Colors.deepBlue,
primaryColor: Colors.cyan[800],
scaffoldBackgroundColor: Colors.black,
canvasColor: Colors.black,
cardColor: Colors.grey[850],
),
home: const MyApp(),
debugShowCheckedModeBanner: false,
),
);
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final GlobalKey webViewKey = GlobalKey();
InAppWebViewController? webViewController;
String url = "";
double progress = 0;
InAppWebViewGroupOptions options = InAppWebViewGroupOptions(
crossPlatform: InAppWebViewOptions(
useShouldOverrideUrlLoading: true,
mediaPlaybackRequiresUserGesture: false,
incognito: false,
cacheEnabled: true,
),
android: AndroidInAppWebViewOptions(
useHybridComposition: true,
thirdPartyCookiesEnabled: true,
),
ios: IOSInAppWebViewOptions(
allowsInlineMediaPlayback: true,
sharedCookiesEnabled: true,
),
);
late PullToRefreshController pullToRefreshController;
@override
void initState() {
super.initState();
pullToRefreshController = PullToRefreshController(
options: PullToRefreshOptions(color: Colors.cyan[800]),
onRefresh: () async {
if (Platform.isAndroid) {
webViewController?.reload();
} else if (Platform.isIOS) {
webViewController?.loadUrl(
urlRequest: URLRequest(url: await webViewController?.getUrl()),
);
}
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Stack(
children: [
InAppWebView(
key: webViewKey,
initialUrlRequest: URLRequest(
url: WebUri("https://example.com/"),
),
initialOptions: options,
pullToRefreshController: pullToRefreshController,
onWebViewCreated: (controller) {
webViewController = controller;
},
onLoadStart: (controller, url) {
setState(() {
this.url = url.toString();
});
},
androidOnPermissionRequest:
(controller, origin, resources) async {
return PermissionRequestResponse(
resources: resources,
action: PermissionRequestResponseAction.GRANT,
);
},
shouldOverrideUrlLoading: (controller, navigationAction) async {
var uri = navigationAction.request.url!;
final whitelist = ['example.com'];
if (["http", "https"].contains(uri.scheme)) {
bool isWhitelisted = whitelist.any((host) {
return uri.host == host || uri.host.endsWith('.$host');
});
if (isWhitelisted) {
return NavigationActionPolicy.ALLOW;
} else {
if (await canLaunchUrl(uri)) {
await launchUrl(
uri,
mode: LaunchMode.externalApplication,
);
}
return NavigationActionPolicy.CANCEL;
}
}
// 其它協定一律允許
return NavigationActionPolicy.ALLOW;
},
onLoadStop: (controller, url) async {
pullToRefreshController.endRefreshing();
setState(() {
this.url = url.toString();
});
},
onLoadError: (controller, url, code, message) {
pullToRefreshController.endRefreshing();
},
onProgressChanged: (controller, progress) {
if (progress == 100) {
pullToRefreshController.endRefreshing();
}
setState(() {
this.progress = progress / 100;
});
},
onUpdateVisitedHistory: (controller, url, androidIsReload) {
setState(() {
this.url = url.toString();
});
},
onConsoleMessage: (controller, consoleMessage) {
debugPrint(consoleMessage.toString());
},
),
// 載入進度條
if (progress < 1.0)
Positioned(
top: 0,
left: 0,
right: 0,
child: LinearProgressIndicator(value: progress),
),
],
),
),
);
}
}
程式解說
- main():Flutter 入口點。設定 APP 主題顏色、啟用 WebView Debug(方便 Android 開發)。
- MyApp:主畫面 Widget,採用 StatefulWidget 可隨時更新網頁狀態。
- InAppWebViewGroupOptions:這裡設定 WebView 行為,像是否啟用快取、允許多媒體自動播放、Cookie 管理等等。
- PullToRefreshController:提供下拉重新整理功能,符合 APP 使用習慣。
- build():APP 畫面結構,主體就是一個
InAppWebView
,顯示https://example.com/
。 - onWebViewCreated:WebView 建立時把控制器記錄下來。
- shouldOverrideUrlLoading:網址白名單判斷,只有 example.com 的網頁才會在 APP 內開,其它網站會跳外部瀏覽器。
- onLoadStart/onLoadStop/onLoadError/onProgressChanged:這幾個是監控網頁載入狀態、控制進度條的。
- androidOnPermissionRequest:Android 網頁如有麥克風、相機等權限會自動允許(你可自行加強權限控制)。
- 進度條:當網頁載入未完成,顯示在畫面頂端。
6. 設定金鑰(Key)相關事項
當你打算將 Flutter APP 發佈到 Google Play,必須要有一組簽章金鑰來進行 AAB 檔案的簽名。這部分主要與 Android 相關。
你可以使用下列指令產生自己的金鑰(假設你已經安裝好 keytool
,通常隨 Java JDK 一起安裝):
keytool -genkey -v -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my-key-alias
這條指令會要求你輸入密碼並建立一個 my-release-key.jks
檔案(可自行改名),記得密碼和別名(alias)要牢記且妥善儲存。
然後將金鑰檔放在專案根目錄下,接著在 android/app
目錄內建立或編輯 key.properties
檔案,內容如下:
storePassword=你的密碼
keyPassword=你的密碼
keyAlias=my-key-alias
storeFile=../my-release-key.jks
最後,編輯 android/app/build.gradle
,在 android { ... }
區塊加上:
signingConfigs {
release {
if (project.hasProperty('KEYSTORE_PASSWORD')) {
storeFile file(KEYSTORE_FILE)
storePassword KEYSTORE_PASSWORD
keyAlias KEY_ALIAS
keyPassword KEY_PASSWORD
} else if (project.hasProperty('storePassword')) {
storeFile file(storeFile)
storePassword storePassword
keyAlias keyAlias
keyPassword keyPassword
}
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled false
shrinkResources false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
這樣每次你打包就會自動使用你設定好的金鑰檔案進行簽章,讓你的 APP 可以安全上架到 Google Play。
如果你還沒要上架,可以先跳過這一步,日後再補也沒關係,但千萬記得「金鑰檔要備份好」,一旦遺失,未來更新 APP 會遇到大麻煩!
7. 執行與除錯
現在可以連接 Android 手機或用模擬器,直接執行:
flutter run
如果有遇到問題,可參考終端機訊息或 Flutter 官網尋求協助。
最後可以把 APP 打包並上架 Google Play!
AAB(上架 Google Play 專用)與 APK 的打包指令如下:
flutter build appbundle --release
flutter build apk --release
總結
透過 Flutter + WebView,你可以超快速地將自己的網站包成原生 APP,而且後續維護只要管理網站本身就好,非常適合 MVP、內部系統、活動型網站使用。如果你想進階,還可以整合更多原生功能或客製 UI。歡迎試試看,開啟你的 Flutter APP 開發之路吧!
留言區 / Comments
萌芽論壇