Skip to content

Flutter結合GoogleSheet

Flutter結合GoogleSheet

簡介

  • 製作一個可以自訂TAG並將資料傳到Sheet的APP及網站

  • 展試圖

  • APP Alt text
  • WEB Alt text
  • Sheet Alt text

Code

  • GoogleSheetCode
function doGet(request) {
  var sheet_id = 'yoursheetID';
  var sheet_name = "yoursheetName-ex:worksheet1";
  var ss = SpreadsheetApp.openById(sheet_id);
  var sheet = ss.getSheetByName(sheet_name);
  var input = request.parameter.input;
  var tag = request.parameter.tag.toString();
  sheet.appendRow([input, tag]);
  return ContentService.createTextOutput(input,tag);

}

這段代碼是一個用於 Google Apps Script 中的函數 doGet。它的功能是處理從網絡請求中收到的參數,將這些參數寫入 Google Sheets 中的指定工作表,並返回一個文本輸出。

  • sheet_id 是 Google Sheets 的 ID,用於確定要寫入的目標工作表。
  • sheet_name 是工作表的名稱。
  • request 是函數接收到的網絡請求對象,其中包含了從客戶端發送的參數。
  • input 是從請求中獲取的一個參數,它是要寫入表格的輸入值。
  • tag 是另一個從請求中獲取的參數,它是與輸入值相關的標簽。
  • sheet.appendRow([input, tag]) 將輸入值和標簽寫入指定的工作表。
  • ContentService.createTextOutput(input,tag) 創建一個文本輸出,其中包含了輸入值和標簽。

這個函數的作用是接收客戶端發送的參數,將這些參數寫入 Google Sheets 中,並返回一個包含輸入值和標簽的文本輸出。


  • Flutter Code
main.dart
import 'package:flutter/material.dart';
import 'package:textfield_tags/textfield_tags.dart';
// import 'package:textfields/textfields.dart';
import 'sendData.dart';

void main() {
  runApp(const MainApp());
}

class MainApp extends StatelessWidget {
  const MainApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Scaffold(
        body: Home(),
      ),
    );
  }
}

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

  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  NetworkClient? _sendData;
  double? _distanceToField;
  TextfieldTagsController? _controller;
  TextEditingController? _msgcontroller;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _distanceToField = MediaQuery.of(context).size.width;
  }

  @override
  void dispose() {
    super.dispose();
    _controller!.dispose();
  }

  @override
  void initState() {
    super.initState();
    _controller = TextfieldTagsController();
    _msgcontroller = TextEditingController();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Tag2Sheet",
      home: Scaffold(
        appBar: AppBar(
          backgroundColor: Color.fromRGBO(214, 205, 189, 1),
          centerTitle: true,
          title: const Text('Enter a tag...'),
        ),
        body: Column(
          children: [
            Container(
              margin: const EdgeInsets.all(10.0),
              child: SimpleTextFieldWithBorder(
                label: "Input",
                bordercolor: Color.fromRGBO(214, 205, 189, 1),
                controller: _msgcontroller,
              ),
            ),
            TextFieldTags(
              textfieldTagsController: _controller,
              initialTags: const [
                'Python',
                'Flutter',
                'AI',
                'Tools',
                'Linux',
                'Git'
              ],
              textSeparators: const [' ', ','],
              letterCase: LetterCase.normal,
              validator: (String tag) {
                // if (tag == 'php') {
                //   return 'No, please just no';
                // } else if (_controller!.getTags!.contains(tag)) {
                //   return 'you already entered that';
                // }
                return null;
              },
              inputfieldBuilder:
                  (context, tec, fn, error, onChanged, onSubmitted) {
                return ((context, sc, tags, onTagDelete) {
                  return Padding(
                    padding: const EdgeInsets.all(10.0),
                    child: TextField(
                      controller: tec,
                      focusNode: fn,
                      decoration: InputDecoration(
                        isDense: true,
                        border: const OutlineInputBorder(
                          borderSide: BorderSide(
                            color: Color.fromRGBO(214, 205, 189, 1),
                            width: 3.0,
                          ),
                        ),
                        focusedBorder: const OutlineInputBorder(
                          borderSide: BorderSide(
                            color: Color.fromRGBO(214, 205, 189, 1),
                            width: 3.0,
                          ),
                        ),
                        helperText: 'Enter language...',
                        helperStyle: const TextStyle(
                          color: Color.fromRGBO(214, 205, 189, 1),
                        ),
                        hintText: _controller!.hasTags ? '' : "Enter tag...",
                        errorText: error,
                        prefixIconConstraints:
                            BoxConstraints(maxWidth: _distanceToField! * 0.74),
                        prefixIcon: tags.isNotEmpty
                            ? SingleChildScrollView(
                                controller: sc,
                                scrollDirection: Axis.horizontal,
                                child: Row(
                                    children: tags.map((String tag) {
                                  return Container(
                                    decoration: const BoxDecoration(
                                      borderRadius: BorderRadius.all(
                                        Radius.circular(20.0),
                                      ),
                                      color: Color.fromRGBO(214, 205, 189, 1),
                                    ),
                                    margin: const EdgeInsets.symmetric(
                                        horizontal: 5.0),
                                    padding: const EdgeInsets.symmetric(
                                        horizontal: 10.0, vertical: 5.0),
                                    child: Row(
                                      mainAxisAlignment:
                                          MainAxisAlignment.spaceBetween,
                                      children: [
                                        InkWell(
                                          child: Text(
                                            '#$tag',
                                            style: const TextStyle(
                                                color: Colors.white),
                                          ),
                                          onTap: () {
                                            print("$tag selected");
                                          },
                                        ),
                                        const SizedBox(width: 4.0),
                                        InkWell(
                                          child: const Icon(
                                            Icons.cancel,
                                            size: 14.0,
                                            color: Color.fromARGB(
                                                255, 233, 233, 233),
                                          ),
                                          onTap: () {
                                            onTagDelete(tag);
                                          },
                                        )
                                      ],
                                    ),
                                  );
                                }).toList()),
                              )
                            : null,
                      ),
                      onChanged: onChanged,
                      onSubmitted: onSubmitted,
                    ),
                  );
                });
              },
            ),
            Row(
              crossAxisAlignment: CrossAxisAlignment.center,
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                ElevatedButton(
                  style: ButtonStyle(
                    backgroundColor: MaterialStateProperty.all<Color>(
                      Color.fromRGBO(214, 205, 189, 1),
                    ),
                  ),
                  onPressed: () {
                    _controller!.clearTags();
                  },
                  child: const Text(
                    'CLEAR TAGS',
                    style: TextStyle(color: Colors.white),
                  ),
                ),
                ElevatedButton(
                  onPressed: () => NetworkClient(
                          _controller!.getTags!.toString(),
                          _msgcontroller!.text)
                      .sendData(),
                  child: Text("SEND",
                      style:
                          TextStyle(color: Colors.white, letterSpacing: 1.5)),
                  style: ButtonStyle(
                    backgroundColor: MaterialStateProperty.all<Color>(
                      Color.fromRGBO(214, 205, 189, 1),
                    ),
                  ),
                )
              ],
            ),
          ],
        ),
      ),
    );
  }
}

class SimpleTextFieldWithBorder extends StatelessWidget {
  const SimpleTextFieldWithBorder(
      {Key? key,
      this.controller,
      required this.label,
      required this.bordercolor,
      this.validator,
      this.autofocus,
      this.onChanged})
      : super(key: key);

  final TextEditingController? controller;
  final String? label;
  final Color? bordercolor;
  final String? Function(String?)? validator;
  final bool? autofocus;
  final Function(String)? onChanged;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.fromLTRB(10, 2, 10, 2),
      decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(5),
          border:
              Border.all(width: 3.0, color: bordercolor ?? Colors.blueAccent)),
      child: TextFormField(
        onChanged: onChanged,
        autofocus: autofocus ?? false,
        decoration: InputDecoration(
          border: InputBorder.none,
          labelText: label ?? "label",
          labelStyle: const TextStyle(
            color: Color.fromRGBO(214, 205, 189, 1),
          ),
        ),
        controller: controller,
        validator: validator,
      ),
    );
  }
}

main.dart 是一個使用 Flutter 框架編寫的 Dart 語言文件。它是一個應用程序的入口文件,負責定義應用程序的結構和行為。

主要組件和功能

  • MainApp 類: 定義了整個應用程序的主要外觀和功能。它是一個無狀態的小部件,即 StatelessWidget。
  • Home 類: 定義了應用程序的主頁。它是一個有狀態的小部件,即 StatefulWidget。
  • _HomeState 類: Home 類的狀態,管理了一些與狀態相關的數據和方法。
  • TextfieldTagsController: 管理文本標簽的控制器,可以用於添加、刪除和檢查標簽。
  • TextEditingController: 管理輸入框文本的控制器,可以用於監聽輸入框中的文本變化。
  • MediaQuery: 用於獲取設備屏幕的尺寸信息,通常用於根據屏幕尺寸調整布局。
  • TextFieldTags: 一個用於顯示和管理標簽的自定義小部件,可以添加、刪除和驗證標簽。
  • SimpleTextFieldWithBorder: 一個帶有邊框的簡單輸入框,用於用戶輸入文本。

主要流程和功能

  1. 在應用程序啟動時,main() 函數會運行 MainApp,將應用程序渲染為一個 MaterialApp,並將其顯示在屏幕上。
  2. MainApp 中的 Scaffold 小部件定義了應用程序的基本布局,包括應用欄和主體內容。
  3. 主體內容由 Home 類定義,其中包含一個帶有邊框的簡單輸入框和一個用於管理標簽的自定義小部件 TextFieldTags
  4. 用戶可以在輸入框中輸入標簽,然後點擊添加按鈕將其添加到標簽列表中。還可以刪除已添加的標簽。
  5. 標簽列表通過 TextFieldTags 自定義小部件動態顯示,用戶可以看到已添加的標簽,並且可以點擊標簽進行操作。
  6. 用戶還可以點擊清除按鈕來清除所有已添加的標簽。

sendData.dart
import 'package:http/http.dart' as http;

class NetworkClient {
  final String _controller;
  final String _msgcontroller;

  NetworkClient(this._controller, this._msgcontroller);

  Future<void> sendData() async {
    final url =
        "yourGoogleAppScriptsDeployID";
    final fullUrl = Uri.parse(url).replace(queryParameters: {
      "tag": _controller.toString(), // 將 _controller 的內容傳送出去
      "input": _msgcontroller.toString(), // 將 _msgcontroller 的內容傳送出去
    });
    print(fullUrl);
    final response = await http.get(fullUrl);
  }
}

網絡客戶端發送數據

在這個代碼片段中,我們定義了一個 NetworkClient 類,用於發送數據到網絡。主要功能是通過 HTTP 請求將數據發送到指定的 URL。

類成員和功能

  • _controller: 一個字符串類型的私有成員變量,存儲控制器的內容。
  • _msgcontroller: 一個字符串類型的私有成員變量,存儲消息控制器的內容。
  • sendData() 方法: 一個異步方法,用於發送數據到網絡。它構建一個 URL,將 _controller_msgcontroller 的內容作為參數添加到 URL 中,並使用 HTTP 請求發送該 URL。
    在這個示例中,sendData() 方法構建了一個 URL,並使用 HTTP 請求將數據發送到指定的 URL。這可以用於與服務器進行通信,傳輸數據或獲取響應。

佈署至Netlify

  1. 先使用flutter build web建立出可發布的WEB內容
  2. 將內容上傳至github的某一儲存庫(新的)
  3. 至Netlify
  4. Alt text
  5. Alt text
  6. Alt text
  7. 找到那一個儲存庫 Alt text
  8. Alt text
  9. 回到左邊導引欄Alt text
  10. 按下創建出的專案Alt text
  11. 修改網站設定Alt text
  12. 修改網址Alt text
  13. Alt text

function doGet(request) {
var sheet_id = 'yoursheetID';
var sheet_name = "yoursheetName-ex:worksheet1";
var ss = SpreadsheetApp.openById(sheet_id);
var sheet = ss.getSheetByName(sheet_name);
var input = request.parameter.input;
var tag = request.parameter.tag.toString();
sheet.appendRow([input, tag]);
return ContentService.createTextOutput(input,tag);
}
main.dart
import 'package:flutter/material.dart';
import 'package:textfield_tags/textfield_tags.dart';
// import 'package:textfields/textfields.dart';
import 'sendData.dart';

void main() {
  runApp(const MainApp());
}

class MainApp extends StatelessWidget {
  const MainApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Scaffold(
        body: Home(),
      ),
    );
  }
}

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

  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  NetworkClient? _sendData;
  double? _distanceToField;
  TextfieldTagsController? _controller;
  TextEditingController? _msgcontroller;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _distanceToField = MediaQuery.of(context).size.width;
  }

  @override
  void dispose() {
    super.dispose();
    _controller!.dispose();
  }

  @override
  void initState() {
    super.initState();
    _controller = TextfieldTagsController();
    _msgcontroller = TextEditingController();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Tag2Sheet",
      home: Scaffold(
        appBar: AppBar(
          backgroundColor: Color.fromRGBO(214, 205, 189, 1),
          centerTitle: true,
          title: const Text('Enter a tag...'),
        ),
        body: Column(
          children: [
            Container(
              margin: const EdgeInsets.all(10.0),
              child: SimpleTextFieldWithBorder(
                label: "Input",
                bordercolor: Color.fromRGBO(214, 205, 189, 1),
                controller: _msgcontroller,
              ),
            ),
            TextFieldTags(
              textfieldTagsController: _controller,
              initialTags: const [
                'Python',
                'Flutter',
                'AI',
                'Tools',
                'Linux',
                'Git'
              ],
              textSeparators: const [' ', ','],
              letterCase: LetterCase.normal,
              validator: (String tag) {
                // if (tag == 'php') {
                //   return 'No, please just no';
                // } else if (_controller!.getTags!.contains(tag)) {
                //   return 'you already entered that';
                // }
                return null;
              },
              inputfieldBuilder:
                  (context, tec, fn, error, onChanged, onSubmitted) {
                return ((context, sc, tags, onTagDelete) {
                  return Padding(
                    padding: const EdgeInsets.all(10.0),
                    child: TextField(
                      controller: tec,
                      focusNode: fn,
                      decoration: InputDecoration(
                        isDense: true,
                        border: const OutlineInputBorder(
                          borderSide: BorderSide(
                            color: Color.fromRGBO(214, 205, 189, 1),
                            width: 3.0,
                          ),
                        ),
                        focusedBorder: const OutlineInputBorder(
                          borderSide: BorderSide(
                            color: Color.fromRGBO(214, 205, 189, 1),
                            width: 3.0,
                          ),
                        ),
                        helperText: 'Enter language...',
                        helperStyle: const TextStyle(
                          color: Color.fromRGBO(214, 205, 189, 1),
                        ),
                        hintText: _controller!.hasTags ? '' : "Enter tag...",
                        errorText: error,
                        prefixIconConstraints:
                            BoxConstraints(maxWidth: _distanceToField! * 0.74),
                        prefixIcon: tags.isNotEmpty
                            ? SingleChildScrollView(
                                controller: sc,
                                scrollDirection: Axis.horizontal,
                                child: Row(
                                    children: tags.map((String tag) {
                                  return Container(
                                    decoration: const BoxDecoration(
                                      borderRadius: BorderRadius.all(
                                        Radius.circular(20.0),
                                      ),
                                      color: Color.fromRGBO(214, 205, 189, 1),
                                    ),
                                    margin: const EdgeInsets.symmetric(
                                        horizontal: 5.0),
                                    padding: const EdgeInsets.symmetric(
                                        horizontal: 10.0, vertical: 5.0),
                                    child: Row(
                                      mainAxisAlignment:
                                          MainAxisAlignment.spaceBetween,
                                      children: [
                                        InkWell(
                                          child: Text(
                                            '#$tag',
                                            style: const TextStyle(
                                                color: Colors.white),
                                          ),
                                          onTap: () {
                                            print("$tag selected");
                                          },
                                        ),
                                        const SizedBox(width: 4.0),
                                        InkWell(
                                          child: const Icon(
                                            Icons.cancel,
                                            size: 14.0,
                                            color: Color.fromARGB(
                                                255, 233, 233, 233),
                                          ),
                                          onTap: () {
                                            onTagDelete(tag);
                                          },
                                        )
                                      ],
                                    ),
                                  );
                                }).toList()),
                              )
                            : null,
                      ),
                      onChanged: onChanged,
                      onSubmitted: onSubmitted,
                    ),
                  );
                });
              },
            ),
            Row(
              crossAxisAlignment: CrossAxisAlignment.center,
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                ElevatedButton(
                  style: ButtonStyle(
                    backgroundColor: MaterialStateProperty.all<Color>(
                      Color.fromRGBO(214, 205, 189, 1),
                    ),
                  ),
                  onPressed: () {
                    _controller!.clearTags();
                  },
                  child: const Text(
                    'CLEAR TAGS',
                    style: TextStyle(color: Colors.white),
                  ),
                ),
                ElevatedButton(
                  onPressed: () => NetworkClient(
                          _controller!.getTags!.toString(),
                          _msgcontroller!.text)
                      .sendData(),
                  child: Text("SEND",
                      style:
                          TextStyle(color: Colors.white, letterSpacing: 1.5)),
                  style: ButtonStyle(
                    backgroundColor: MaterialStateProperty.all<Color>(
                      Color.fromRGBO(214, 205, 189, 1),
                    ),
                  ),
                )
              ],
            ),
          ],
        ),
      ),
    );
  }
}

class SimpleTextFieldWithBorder extends StatelessWidget {
  const SimpleTextFieldWithBorder(
      {Key? key,
      this.controller,
      required this.label,
      required this.bordercolor,
      this.validator,
      this.autofocus,
      this.onChanged})
      : super(key: key);

  final TextEditingController? controller;
  final String? label;
  final Color? bordercolor;
  final String? Function(String?)? validator;
  final bool? autofocus;
  final Function(String)? onChanged;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.fromLTRB(10, 2, 10, 2),
      decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(5),
          border:
              Border.all(width: 3.0, color: bordercolor ?? Colors.blueAccent)),
      child: TextFormField(
        onChanged: onChanged,
        autofocus: autofocus ?? false,
        decoration: InputDecoration(
          border: InputBorder.none,
          labelText: label ?? "label",
          labelStyle: const TextStyle(
            color: Color.fromRGBO(214, 205, 189, 1),
          ),
        ),
        controller: controller,
        validator: validator,
      ),
    );
  }
}

Last update : 13 novembre 2024
Created : 13 novembre 2024

Comments

Comments