Vintage appMaker의 Tech Blog

[Flutter] Web App에서 javascript와 연동 본문

Source code or Tip/Flutter & Dart

[Flutter] Web App에서 javascript와 연동

VintageappMaker 2022. 11. 13. 11:40

Flutter 3.0으로 오면서 멀티플랫폼으로 Flutter는 안정적으로 변했다. 특히 Web app에서 PWA(Progressive Web App)을 만든다면 Flutter는 정말 괜찮은 선택 중에 하나이다. 그런데, Fluttrer로 Web App을 만들다보면 기존의 라이브러리들이 javascript로 되어있는 경우가 있는데, 이럴 때는 Javacript와 interface를 해야 한다. 

 

1. javascript를 web 폴더에 작성한다. (app.js)

flutter 프로젝트의 web 폴더안의 index.html이 있는 곳에 원하는 자바스크립트를 작성하여 코딩을 한다. 

 

[app.js]

window.state = {
    reserved1: '전역변수처럼 사용가능'
}

// 호출예제
// 이 파일에서 apiTest와 같은 구조로
// WebPage에 전달해주면 된다. 
function apiTest(jsonParam, onCallback) {
  // 파라메터처리 {"id" : 숫자번호}
  console.log(jsonParam);
  var obj = JSON.parse(jsonParam);
  console.log(obj);
  
  // id라는 값이 넘어온다. 
  var num = obj.id;
  var url = 'https://jsonplaceholder.typicode.com/posts/'+ num;

  callServerTest(url, onCallback)
}

// 네트워크 api 호출
function callServerTest(url, onCallback) {
  fetch(url)
    .then(response => response.json())
    .then(json => {
      
      // 결과값 전달
      var data = JSON.stringify(json, null, 4);
      onCallback(data)
      
    });
}

 

2. index.html을 수정한다.

flutter 프로젝트의 web 폴더안의 index.html을 위와 같이 수정한다. 

 

3. flutter 소스에서 javascript를 호출한다. 

import 'dart:js' as js;
를 한 후,  js.context.callMethod()를 통해 javascrip 함수를 호출한다. 
app.js에서 정의한 함수를 호출하는데, 예제에는 결과물을 callback으로 받아처리하는 구조로
정의되어 있다. 

 

4. 빌드한다. 

flutter build web --release

5. 주의점

최종 릴리즈를 하려면 build한 web 폴더에 작성한 js 파일을 복사해주어야 한다.

그리고 js 파일을 수정한 것이 웹에서 적용되지 않았을 경우가 종종 발생하는데, 

이 때에는 chrome에서 ctrl + shift + r을 통해 강력하게 갱신을 해야 한다. 

 

6. 전체소스

[app.js 소스]는 위에 기술되어 있으므로 Flutter의 메인소스만 기술한다. 

[main.dart]

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'dart:js' as js;

void main() {
  runApp(PWATesterApp());
}

class PWATesterApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      scrollBehavior: DeskScrollBehavior(),
      debugShowCheckedModeBanner: false,
      title: 'PWA Javascript Interface',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MainPage(),
    );
  }
}

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

  @override
  _MainPageState createState() => new _MainPageState();
}

class _MainPageState extends State<MainPage> {
  TextEditingController ctrlTextFunction = TextEditingController();
  TextEditingController ctrlTextParam = TextEditingController();

  var resultText = "";

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Container(
      color: Color(0xffaa2344),
      width: double.infinity,
      height: double.infinity,
      padding: EdgeInsets.all(58.0),
      child: Container(
        child: SingleChildScrollView(
          child: Column(
            children: [...buildMainWidgets()],
          ),
        ),
      ),
    ));
  }

  // Main 화면
  List buildMainWidgets() {
    var inputColor = Colors.deepOrange;
    var title = Text(
      "Javascript Interface",
      style: TextStyle(fontSize: 42, color: inputColor, decorationThickness: 3),
    );
    var inputText = TextField(
        style: TextStyle(color: Colors.white),
        controller: ctrlTextFunction,
        decoration: InputDecoration(
            suffixIcon: IconButton(
              color: Colors.white,
              onPressed: ctrlTextFunction.clear,
              icon: Icon(Icons.clear),
            ),
            enabledBorder:
                OutlineInputBorder(borderSide: BorderSide(color: inputColor)),
            focusedBorder:
                OutlineInputBorder(borderSide: BorderSide(color: inputColor)),
            labelText: '함수명',
            hintText: "app.js에 정의한 함수명",
            hintStyle: TextStyle(color: Colors.grey),
            labelStyle: TextStyle(color: inputColor)));

    var inputText2 = TextField(
        style: TextStyle(color: Colors.grey),
        controller: ctrlTextParam,
        keyboardType: TextInputType.multiline,
        maxLines: null,
        decoration: InputDecoration(
            suffixIcon: IconButton(
              color: Colors.white,
              onPressed: ctrlTextParam.clear,
              icon: Icon(Icons.clear),
            ),
            enabledBorder:
                OutlineInputBorder(borderSide: BorderSide(color: inputColor)),
            focusedBorder:
                OutlineInputBorder(borderSide: BorderSide(color: inputColor)),
            labelText: 'json 포멧 파라메터',
            hintText: "예: {\"id\":1}",
            hintStyle: TextStyle(color: Colors.white),
            labelStyle: TextStyle(color: inputColor)));

    var responseText = Container(
      constraints: BoxConstraints(minWidth: 100, maxWidth: 500),
      child: SingleChildScrollView(
        scrollDirection: Axis.horizontal,
        child: Text(
          "$resultText",
          style: TextStyle(color: Colors.yellowAccent),
        ),
      ),
    );

    return [
      title,
      Card(
        elevation: 4.0,
        color: Colors.transparent,
        child: Container(
          constraints: BoxConstraints(minWidth: 100, maxWidth: 500),
          decoration: BoxDecoration(
              color: Colors.amber, borderRadius: BorderRadius.circular(10.0)),
          padding: EdgeInsets.all(15),
          child: Column(
            children: [
              SizedBox(
                height: 30,
              ),
              inputText,
              SizedBox(
                height: 20,
              ),
              inputText2,
              SizedBox(
                height: 20,
              )
            ],
          ),
        ),
      ),
      SizedBox(
        height: 20,
      ),
      ElevatedButton(
        style: ButtonStyle(
          backgroundColor: MaterialStateProperty.all(Colors.redAccent),
          padding: MaterialStateProperty.all(EdgeInsets.all(20)),
        ),
        onPressed: () {
          callJavascriptAPI();
        },
        child: Text('전송하기'),
      ),
      SizedBox(
        height: 20,
      ),
      responseText
    ];
  }

  // javascript 호출
  void callJavascriptAPI() {
    setState(() {
      resultText = "";
    });
    js.context.callMethod('${ctrlTextFunction.text}', [
      '${ctrlTextParam.text}',
      (s) {
        setState(() {
          resultText = s;
        });
      }
    ]);
  }
}

// 웹과 desktop에서 모바일처럼 터치 스크롤 지원하기 위함
class DeskScrollBehavior extends MaterialScrollBehavior {
  @override
  Set<PointerDeviceKind> get dragDevices => {
        PointerDeviceKind.touch,
        PointerDeviceKind.mouse,
      };
}

 

 

 

Comments