flutter_inappwebview로 hybrid app 만들기

JuhyunKim·2022년 11월 4일
2

flutter

목록 보기
2/5
post-thumbnail

velog를 시작하고 나서 내 글 보는 재미가 생겼는데 맨날 삼성인터넷으로 들어가려니 불편하길래.. inappwebview로 간단하게 velog 어플을 만들어봤다. 출시할 것도 아니고.. 내 벨로그 수정하고 구경하기 편하려고 만든거라 정말 간단하게만 만들었다!

flutter의 webview 라이브러리로 가장 많이 쓰는게 flutter_webview랑 flutter_inappwbeview인데 flutter_inappwebview 쪽이 좀 더 자유도가 높은 편이고, flutter_webview는 새 창이 뜨도록 하는게 어렵다고한다. 반응형 웹이라고 해도 그대로 사용하려면 꽤 노력이 필요한데 flutter_inappwebview는 그 부분을 좀 더 편리하게 도와주는듯.
이번 어플은 간단히 만들거라 둘 중 어느 것을 사용하든 상관없긴한데 나는 전에 flutter_inappwebview 만들었던 경험이 있어 그 코드 그대로 응용하고자 flutter_inappwebview로 사용했다.



1. splash screen 만들기

앱을 시작하는 splash screen. 사실 없어도 무방하지만 있으면 내가 좋아하므로 그냥 넣어봤다.
우선 로고로 사용할 png 파일이 필요하다.
다시 강조하지만 배포용도 아니고 디자이너도 아니기 때문에 내 velog 로고를 캡쳐해서 png로 저장해줬다.

저장한 파일을 [project]/assets 안에 넣어준다.


그리고 pubspec.yaml에 asset 경로 추가.



splash_screen.dart

import 'dart:async';

import 'package:flutter/material.dart';

class SplashScreen extends StatefulWidget {
  const SplashScreen({Key? key}):super(key:key);

  @override
  State<SplashScreen> createState() => _SplashScreenState();
}

class _SplashScreenState extends State<SplashScreen> {

  @override
  void initState() {
    super.initState();
    Timer(const Duration(seconds:2), () {
      Navigator.of(context).pushReplacementNamed('/velog');
    });
  }

  @override
  Widget build(BuildContext context){
    return Scaffold(
      body: Center(
        child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children:[
              Container(
                  margin: const EdgeInsets.symmetric(horizontal:50),
                  child: Image.asset('assets/jhkim_velog_logo.png')
              ),
            ]
        ),
      ),
    );
  }
}

간단하게 이미지만 나오는 코드로 작성.
2초 후 velog 페이지로 넘어가게 해두었다.

조금 심심한 splash screen 완성~



2. web view screen 만들기

web view를 사용하려면 준비과정이 필요하다.

flutter pub get flutter_inappwebview

우선 터미널에서 flutter_inappwebview 패키지부터 pub get 하고 시작.

flutter_inappwebview
https://pub.dev/packages/flutter_inappwebview

위의 공식 페이지를 들어가 보면 사용조건이 나와있다.

Requirements

Dart sdk: ">=2.14.0 <3.0.0"
Flutter: ">=3.0.0"
Android: minSdkVersion 17 and add support for androidx (see AndroidX Migration to migrate an existing app)
iOS: --ios-language swift, Xcode version >= 14

나는 일단 android로 제작할 예정이라 android 조건만 맞춰줬다.
flutter 버전 같은 경우 flutter 3.0은 flutter 2.0의 null-safety처럼 업그레이드 시 오류가 발생하거나 그러진 않아서 업그레이드 하는 것을 추천한다.


    defaultConfig {
        applicationId "com.jhkim.jhkimvelog"
        minSdkVersion 17
        targetSdkVersion flutter.targetSdkVersion
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }

[porject]/android/app/build.gradle 파일에 가서 minSdkVersion을 17로 잡아준다.


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.jhkim.jhkimvelog">
    <uses-permission android:name="android.permission.INTERNET"/>
   <application
        android:label="jhkim.log"
        android:name="${applicationName}"
        ...

그리고 <uses-permission android:name="android.permission.INTERNET"/>을 꼭 넣어줘야 한다.


velog_screen.dart

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';

class InAppWebViewScreen extends StatefulWidget {
  const InAppWebViewScreen({Key? key}):super(key:key);

  @override
  State<InAppWebViewScreen> createState() => _InAppWebViewScreenState();
}

class _InAppWebViewScreenState extends State<InAppWebViewScreen> {
  final GlobalKey webViewKey = GlobalKey();
  Uri myUrl = Uri.parse("https://velog.io/@jhkim0122");
  late final InAppWebViewController webViewController;
  late final PullToRefreshController pullToRefreshController;
  double progress = 0;

  @override
  void initState() {
    super.initState();

    pullToRefreshController = (kIsWeb
        ? null
        : PullToRefreshController(
      options: PullToRefreshOptions(color: Colors.blue,),
      onRefresh: () async {
        if (defaultTargetPlatform == TargetPlatform.android) {
          webViewController.reload();
        } else if (defaultTargetPlatform == TargetPlatform.iOS || defaultTargetPlatform == TargetPlatform.macOS) {
          webViewController.loadUrl(urlRequest: URLRequest(url: await webViewController.getUrl()));}
        },
      ))!;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: SafeArea(
            child: WillPopScope(
                onWillPop: () => _goBack(context),
                child: Column(children: <Widget>[
                  progress < 1.0
                      ? LinearProgressIndicator(value: progress, color: Colors.blue)
                      : Container(),
                  Expanded(
                      child: Stack(children: [
                        InAppWebView(
                          key: webViewKey,
                          initialUrlRequest: URLRequest(url: myUrl),
                          initialOptions: InAppWebViewGroupOptions(
                            crossPlatform: InAppWebViewOptions(
                                javaScriptCanOpenWindowsAutomatically: true,
                                javaScriptEnabled: true,
                                useOnDownloadStart: true,
                                useOnLoadResource: true,
                                useShouldOverrideUrlLoading: true,
                                mediaPlaybackRequiresUserGesture: true,
                                allowFileAccessFromFileURLs: true,
                                allowUniversalAccessFromFileURLs: true,
                                verticalScrollBarEnabled: true,
                                userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.122 Safari/537.36'
                            ),
                            android: AndroidInAppWebViewOptions(
                                useHybridComposition: true,
                                allowContentAccess: true,
                                builtInZoomControls: true,
                                thirdPartyCookiesEnabled: true,
                                allowFileAccess: true,
                                supportMultipleWindows: true
                            ),
                            ios: IOSInAppWebViewOptions(
                              allowsInlineMediaPlayback: true,
                              allowsBackForwardNavigationGestures: true,
                            ),
                          ),
                          pullToRefreshController: pullToRefreshController,
                          onLoadStart: (InAppWebViewController controller, uri) {
                            setState(() {myUrl = uri!;});
                          },
                          onLoadStop: (InAppWebViewController controller, uri) {
                            setState(() {myUrl = uri!;});
                          },
                          onProgressChanged: (controller, progress) {
                            if (progress == 100) {pullToRefreshController.endRefreshing();}
                            setState(() {this.progress = progress / 100;});
                          },
                          androidOnPermissionRequest: (controller, origin, resources) async {
                            return PermissionRequestResponse(
                                resources: resources,
                                action: PermissionRequestResponseAction.GRANT);
                          },
                          onWebViewCreated: (InAppWebViewController controller) {
                            webViewController = controller;
                          },
                          onCreateWindow: (controller, createWindowRequest) async{
                            showDialog(
                              context: context, builder: (context) {
                              return AlertDialog(
                                content: SizedBox(
                                  width: MediaQuery.of(context).size.width,
                                  height: 400,
                                  child: InAppWebView(
                                    // Setting the windowId property is important here!
                                    windowId: createWindowRequest.windowId,
                                    initialOptions: InAppWebViewGroupOptions(
                                      android: AndroidInAppWebViewOptions(
                                        builtInZoomControls: true,
                                        thirdPartyCookiesEnabled: true,
                                      ),
                                      crossPlatform: InAppWebViewOptions(
                                          cacheEnabled: true,
                                          javaScriptEnabled: true,
                                          userAgent: "Mozilla/5.0 (Linux; Android 9; LG-H870 Build/PKQ1.190522.001) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36"
                                      ),
                                      ios: IOSInAppWebViewOptions(
                                        allowsInlineMediaPlayback: true,
                                        allowsBackForwardNavigationGestures: true,
                                      ),
                                    ),
                                    onCloseWindow: (controller) async{
                                        if (Navigator.canPop(context)) {
                                          Navigator.pop(context);
                                        }
                                    },
                                  ),
                                ),);
                            },
                            );
                            return true;
                          },
                        )
                      ]))
                ])
            )
        )
    );
  }

  Future<bool> _goBack(BuildContext context) async{
    if(await webViewController.canGoBack()){
      webViewController.goBack();
      return Future.value(false);
    }else{
      return Future.value(true);
    }
  }
}

우선 추가한 기능으로는 화면을 아래로 당겼을 때 새로고침이 되도록 pullToRefreshController를 넣어줬다.
그리고 webview는 어플 입장에서 사실상 페이지 하나라서 뒤로가기를 하면 바로 어플이 꺼진다. 뒤로가기 했을을 때 webview 속 사이트의 이전 화면이 나오도록 WillPopScope를 사용해서 webViewController로 지정해줬다.

정상적으로 잘 된다. 편안~



3. 앱 아이콘 추가하기

우선 icon으로 사용할 이미지들이 필요한데 구글에 app icon generator 검색하면 잔뜩 나온다.

App Icon Generator
https://appicon.co/

내가 이용한 사이트는 여긴데 ios icon bundle도 만들어줘서 한 번에 만들기 좋다.
icon으로 사용할 png 이미지를 업로드해서 만들어준다.


그럼 이런 폴더를 다운받을 수 있는데 android 버전만 만들거고 배포는 안 할 예정이라 android 폴더만 사용할 것이다.


android 폴더 안에 보면 이렇게 5가지 폴더가 나온다.

그 파일들을 이제 [project]/android/app/main/res 폴더에 넣어준다.
그러고 새로 어플을 깔아주면 바뀐 로고를 확인할 수 있다.

velog 로고 검색해서 나온걸로 사용했는데 상업용이 아니니까 괜찮겠지...? velog에서 지금 사용중인 연두색 로고보다 검은색이 예뻐서 이걸로 사용했다.



4. apk 만들기

flutter build apk --split-per-abi

터미널에서 build apk 를 사용하면 apk가 생성된다.
--split-per-abi 를 사용하면 abi별로 apk가 따로 생성되는데 용량을 조금 줄일 수 있다.
이런 간단한 앱에서는 굳이 사용하지 않아도 괜찮겠지만 습관이 되어서 그냥 저렇게 빌드해줬다.

빌드가 완료되면 [project]\build\app\outputs\flutter-apk\app-armeabi-v7a-release.apk 경로의 apk를 다운받아서 설치하면 완료!


마무리

프로젝트는 깃허브에 올려놓았는데, 앱 아이콘, 앱 라벨, splash 이미지, 웹 링크만 수정하면 다른 웹으로 연결되도록 사용할 수 있다.

프로젝트 깃허브
https://github.com/jhkim0122/jhkim_velog

apk 다운로드 링크 (2022.11.08 버그 수정)
jhkim_velog.apk


어플 만드는 데에는 30분 정도 걸린 것 같은데 글 쓰는데엔 2시간은 걸린 것 같다.. 벨로그 쉽지않구만..




다음글 > flutter_inappwebview 파일 업로드, 파일 탐색기 안 열릴 때 해결 방법 (flutter_inappwebview file provider authority)

1개의 댓글

comment-user-thumbnail
2023년 12월 3일

안녕하세요. 개발 공부중입니다.
올려주신 자료 보면서 공부중인데요. 파일 다운로드 방법을 어려워서요.
git 소스 링크 다시 공유 가능하신가요?

답글 달기