返回首頁Dialogflow

Dialogflow 行動開發整合:Android 與 Flutter 完整教學

15 min 分鐘閱讀
#Dialogflow#Android#Flutter#行動開發#App 整合

Dialogflow 行動開發整合:Android 與 Flutter 完整教學

Dialogflow 行動開發整合:Android 與 Flutter 完整教學

想在你的 App 中加入 AI 對話功能嗎?

不論是客服助理、語音助手還是互動式導覽,Dialogflow 都可以幫你實現。這篇文章教你如何在 Android 和 Flutter 中整合 Dialogflow,打造具備自然語言理解能力的行動應用。

如果你還不熟悉 Dialogflow,建議先閱讀 Dialogflow 完整指南


行動 App 整合方式比較

在 App 中整合 Dialogflow 有兩種主要方式,各有優缺點。

直接 API 呼叫

App 直接呼叫 Dialogflow API:

App → Dialogflow API → 回應

優點

  • 架構簡單
  • 延遲較低
  • 不需要自建後端

缺點

  • API 金鑰暴露在 App 中(資安風險)
  • 無法加入額外商業邏輯
  • 難以記錄對話歷史

透過後端轉發

App 透過你的後端呼叫 Dialogflow:

App → 你的後端 → Dialogflow API → 你的後端 → App

優點

  • API 金鑰安全保存在後端
  • 可以加入商業邏輯(驗證、記錄、過濾)
  • 容易整合其他系統

缺點

  • 需要建置和維護後端
  • 延遲稍高
  • 架構較複雜

優缺點分析與選擇建議

方式適用場景
直接 APIPOC 驗證、學習用途、內部工具
後端轉發正式產品、需要資安、企業應用

建議:正式上線的 App 一律使用後端轉發方式。

插圖:兩種整合架構比較圖

場景描述: 一張並排比較圖,左側顯示「直接 API 呼叫」架構(App → Dialogflow),右側顯示「後端轉發」架構(App → Backend → Dialogflow)。每個架構下方列出優缺點。

視覺重點:

  • 主要內容清晰呈現

必須出現的元素:

  • 依據描述中的關鍵元素

需要顯示的中文字:

顏色調性: 專業、清晰

避免元素: 抽象圖形、齒輪、發光特效

Slug: dialogflow-mobile-integration-architecture


Android Studio 整合

專案設定

Step 1:新增依賴套件

app/build.gradle 中加入:

dependencies {
    implementation 'com.google.cloud:google-cloud-dialogflow:4.0.0'
    implementation 'io.grpc:grpc-okhttp:1.56.1'
    implementation 'com.google.auth:google-auth-library-oauth2-http:1.19.0'
}

Step 2:設定網路權限

AndroidManifest.xml 中加入:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />

Gradle 依賴套件

完整的 build.gradle 設定:

android {
    compileSdk 34

    defaultConfig {
        minSdk 24
        targetSdk 34
    }

    packagingOptions {
        exclude 'META-INF/INDEX.LIST'
        exclude 'META-INF/DEPENDENCIES'
    }
}

dependencies {
    implementation 'com.google.cloud:google-cloud-dialogflow:4.0.0'
    implementation 'io.grpc:grpc-okhttp:1.56.1'
    implementation 'io.grpc:grpc-stub:1.56.1'
}

Service Account 設定

Step 1:取得金鑰檔案

  1. 前往 Google Cloud Console > IAM > Service Accounts
  2. 建立或選擇服務帳戶
  3. 下載 JSON 金鑰檔案

Step 2:放置金鑰(開發用)

將金鑰檔案放到 app/src/main/res/raw/credentials.json

注意:這只適合開發測試。正式環境請使用後端轉發。

DetectIntent API 呼叫

建立 Dialogflow 客戶端類別:

class DialogflowClient(context: Context) {
    private val sessionsClient: SessionsClient
    private val session: SessionName
    private val projectId = "your-project-id"
    private val sessionId = UUID.randomUUID().toString()

    init {
        // 載入憑證
        val stream = context.resources.openRawResource(R.raw.credentials)
        val credentials = GoogleCredentials.fromStream(stream)
            .createScoped(listOf("https://www.googleapis.com/auth/cloud-platform"))

        val settings = SessionsSettings.newBuilder()
            .setCredentialsProvider { credentials }
            .build()

        sessionsClient = SessionsClient.create(settings)
        session = SessionName.of(projectId, sessionId)
    }

    suspend fun detectIntent(text: String): String {
        return withContext(Dispatchers.IO) {
            val textInput = TextInput.newBuilder()
                .setText(text)
                .setLanguageCode("zh-TW")
                .build()

            val queryInput = QueryInput.newBuilder()
                .setText(textInput)
                .build()

            val request = DetectIntentRequest.newBuilder()
                .setSession(session.toString())
                .setQueryInput(queryInput)
                .build()

            val response = sessionsClient.detectIntent(request)
            response.queryResult.fulfillmentText
        }
    }

    fun close() {
        sessionsClient.close()
    }
}

範例程式碼

完整的 Activity 範例:

class ChatActivity : AppCompatActivity() {
    private lateinit var dialogflowClient: DialogflowClient
    private lateinit var messageAdapter: MessageAdapter
    private val messages = mutableListOf<Message>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_chat)

        dialogflowClient = DialogflowClient(this)
        messageAdapter = MessageAdapter(messages)
        recyclerView.adapter = messageAdapter

        sendButton.setOnClickListener {
            val text = inputEditText.text.toString()
            if (text.isNotEmpty()) {
                sendMessage(text)
                inputEditText.text.clear()
            }
        }
    }

    private fun sendMessage(text: String) {
        // 顯示使用者訊息
        messages.add(Message(text, isUser = true))
        messageAdapter.notifyItemInserted(messages.size - 1)

        // 呼叫 Dialogflow
        lifecycleScope.launch {
            try {
                val response = dialogflowClient.detectIntent(text)
                messages.add(Message(response, isUser = false))
                messageAdapter.notifyItemInserted(messages.size - 1)
                recyclerView.scrollToPosition(messages.size - 1)
            } catch (e: Exception) {
                messages.add(Message("抱歉,發生錯誤", isUser = false))
                messageAdapter.notifyItemInserted(messages.size - 1)
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        dialogflowClient.close()
    }
}

Flutter 整合

套件選擇

Flutter 有幾個 Dialogflow 相關套件:

套件說明維護狀態
dialogflow_grpc官方 gRPC 協定活躍
flutter_dialogflow社群套件較少更新
dialogflow_flutter簡化版本較少更新

建議:使用 dialogflow_grpc 或直接用 HTTP API。

跨平台實作

Step 1:新增依賴

pubspec.yaml 中:

dependencies:
  flutter:
    sdk: flutter
  http: ^1.1.0
  uuid: ^4.0.0

Step 2:建立 API 服務

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:uuid/uuid.dart';

class DialogflowService {
  final String projectId;
  final String accessToken; // 從後端取得
  final String sessionId = Uuid().v4();

  DialogflowService({required this.projectId, required this.accessToken});

  Future<String> detectIntent(String text) async {
    final url = Uri.parse(
      'https://dialogflow.googleapis.com/v2/projects/$projectId/agent/sessions/$sessionId:detectIntent'
    );

    final response = await http.post(
      url,
      headers: {
        'Authorization': 'Bearer $accessToken',
        'Content-Type': 'application/json',
      },
      body: jsonEncode({
        'queryInput': {
          'text': {
            'text': text,
            'languageCode': 'zh-TW',
          },
        },
      }),
    );

    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);
      return data['queryResult']['fulfillmentText'];
    } else {
      throw Exception('Dialogflow API 錯誤: ${response.statusCode}');
    }
  }
}

範例 Widget

class ChatScreen extends StatefulWidget {
  @override
  _ChatScreenState createState() => _ChatScreenState();
}

class _ChatScreenState extends State<ChatScreen> {
  final TextEditingController _controller = TextEditingController();
  final List<ChatMessage> _messages = [];
  late DialogflowService _dialogflow;
  bool _isLoading = false;

  @override
  void initState() {
    super.initState();
    _dialogflow = DialogflowService(
      projectId: 'your-project-id',
      accessToken: 'your-access-token', // 實際應從後端取得
    );
  }

  void _sendMessage() async {
    final text = _controller.text.trim();
    if (text.isEmpty) return;

    setState(() {
      _messages.add(ChatMessage(text: text, isUser: true));
      _isLoading = true;
    });
    _controller.clear();

    try {
      final response = await _dialogflow.detectIntent(text);
      setState(() {
        _messages.add(ChatMessage(text: response, isUser: false));
      });
    } catch (e) {
      setState(() {
        _messages.add(ChatMessage(text: '抱歉,發生錯誤', isUser: false));
      });
    } finally {
      setState(() => _isLoading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('AI 助理')),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
              itemCount: _messages.length,
              itemBuilder: (context, index) {
                final message = _messages[index];
                return ChatBubble(
                  text: message.text,
                  isUser: message.isUser,
                );
              },
            ),
          ),
          if (_isLoading) LinearProgressIndicator(),
          _buildInputArea(),
        ],
      ),
    );
  }

  Widget _buildInputArea() {
    return Container(
      padding: EdgeInsets.all(8),
      child: Row(
        children: [
          Expanded(
            child: TextField(
              controller: _controller,
              decoration: InputDecoration(hintText: '輸入訊息...'),
              onSubmitted: (_) => _sendMessage(),
            ),
          ),
          IconButton(
            icon: Icon(Icons.send),
            onPressed: _sendMessage,
          ),
        ],
      ),
    );
  }
}

狀態管理整合

使用 Provider 管理對話狀態:

class ChatProvider extends ChangeNotifier {
  final List<ChatMessage> _messages = [];
  bool _isLoading = false;

  List<ChatMessage> get messages => _messages;
  bool get isLoading => _isLoading;

  Future<void> sendMessage(String text) async {
    _messages.add(ChatMessage(text: text, isUser: true));
    _isLoading = true;
    notifyListeners();

    try {
      final response = await _dialogflow.detectIntent(text);
      _messages.add(ChatMessage(text: response, isUser: false));
    } catch (e) {
      _messages.add(ChatMessage(text: '發生錯誤', isUser: false));
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }
}

插圖:Flutter 聊天介面截圖

場景描述: 一張手機模擬器截圖,顯示 Flutter 開發的聊天介面。畫面有對話氣泡(使用者在右、AI 在左)、底部輸入框和發送按鈕。介面設計簡潔現代。

視覺重點:

  • 主要內容清晰呈現

必須出現的元素:

  • 依據描述中的關鍵元素

需要顯示的中文字:

顏色調性: 專業、清晰

避免元素: 抽象圖形、齒輪、發光特效

Slug: flutter-dialogflow-chat-interface


語音助理功能

讓 App 支援語音輸入和輸出。

Speech-to-Text 整合

Android(使用 SpeechRecognizer)

class VoiceInputManager(private val context: Context) {
    private var speechRecognizer: SpeechRecognizer? = null
    private var onResult: ((String) -> Unit)? = null

    fun startListening(onResult: (String) -> Unit) {
        this.onResult = onResult

        speechRecognizer = SpeechRecognizer.createSpeechRecognizer(context)
        speechRecognizer?.setRecognitionListener(object : RecognitionListener {
            override fun onResults(results: Bundle?) {
                val matches = results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)
                matches?.firstOrNull()?.let { onResult(it) }
            }

            override fun onError(error: Int) {
                // 處理錯誤
            }

            // 其他必要的 override 方法...
        })

        val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
            putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
            putExtra(RecognizerIntent.EXTRA_LANGUAGE, "zh-TW")
        }

        speechRecognizer?.startListening(intent)
    }

    fun stopListening() {
        speechRecognizer?.stopListening()
        speechRecognizer?.destroy()
    }
}

Flutter(使用 speech_to_text)

import 'package:speech_to_text/speech_to_text.dart' as stt;

class VoiceInput {
  final stt.SpeechToText _speech = stt.SpeechToText();
  bool _isListening = false;

  Future<void> initialize() async {
    await _speech.initialize();
  }

  void startListening(Function(String) onResult) {
    _speech.listen(
      onResult: (result) {
        if (result.finalResult) {
          onResult(result.recognizedWords);
        }
      },
      localeId: 'zh_TW',
    );
    _isListening = true;
  }

  void stopListening() {
    _speech.stop();
    _isListening = false;
  }
}

Text-to-Speech 回放

import 'package:flutter_tts/flutter_tts.dart';

class VoiceOutput {
  final FlutterTts _tts = FlutterTts();

  Future<void> initialize() async {
    await _tts.setLanguage('zh-TW');
    await _tts.setSpeechRate(0.5);
  }

  Future<void> speak(String text) async {
    await _tts.speak(text);
  }

  Future<void> stop() async {
    await _tts.stop();
  }
}

持續對話模式

實現類似 Siri 的持續對話體驗:

class ContinuousDialogue {
  final VoiceInput _voiceInput;
  final VoiceOutput _voiceOutput;
  final DialogflowService _dialogflow;

  bool _isActive = false;

  void startConversation() async {
    _isActive = true;

    while (_isActive) {
      // 語音輸入
      final userText = await _listenForInput();
      if (userText == '結束對話') {
        _isActive = false;
        break;
      }

      // 呼叫 Dialogflow
      final response = await _dialogflow.detectIntent(userText);

      // 語音輸出
      await _voiceOutput.speak(response);

      // 短暫等待後繼續監聽
      await Future.delayed(Duration(milliseconds: 500));
    }
  }
}

安全性考量

行動 App 的安全性尤其重要,API 金鑰一旦外洩就無法收回。

API 金鑰保護策略

絕對不要做的事

// ❌ 把金鑰寫死在程式碼中
val apiKey = "AIzaSyXXXXXXXXXXXXXXX"

建議做法

1. 使用後端 Proxy(推薦)

App → 你的後端(驗證 + 呼叫 Dialogflow)→ App

後端保存 API 金鑰,App 只需要跟後端通訊。

2. 使用 Firebase Remote Config

val remoteConfig = Firebase.remoteConfig
remoteConfig.fetchAndActivate().addOnCompleteListener { task ->
    if (task.isSuccessful) {
        val apiKey = remoteConfig.getString("dialogflow_api_key")
    }
}

3. 使用 Android Keystore

val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
// 安全儲存和讀取金鑰

使用者認證整合

確保只有登入的使用者才能使用 AI 功能:

class SecureDialogflowClient(private val authService: AuthService) {

    suspend fun detectIntent(text: String): String {
        // 確認使用者已登入
        val user = authService.currentUser
            ?: throw UnauthorizedException("請先登入")

        // 取得存取權杖
        val token = authService.getIdToken()

        // 呼叫你的後端(不是直接呼叫 Dialogflow)
        return apiService.chat(
            token = token,
            text = text
        )
    }
}

敏感資料處理

不要記錄敏感對話

// ❌ 記錄完整對話
Log.d("Chat", "User said: $userInput")

// ✓ 只記錄必要資訊
Log.d("Chat", "User sent message, length: ${userInput.length}")

清除本地對話歷史

fun clearChatHistory() {
    // 清除記憶體
    messages.clear()

    // 清除本地儲存
    sharedPreferences.edit().remove("chat_history").apply()
}

插圖:安全架構圖

場景描述: 一張安全架構圖,展示推薦的後端 Proxy 模式。App(帶鎖圖示)→ HTTPS → 後端(帶防火牆圖示)→ Dialogflow。標示金鑰只存在後端,App 不接觸敏感資訊。

視覺重點:

  • 主要內容清晰呈現

必須出現的元素:

  • 依據描述中的關鍵元素

需要顯示的中文字:

顏色調性: 專業、清晰

避免元素: 抽象圖形、齒輪、發光特效

Slug: dialogflow-mobile-security-architecture


擔心 API 金鑰安全?行動 App 的資安問題很容易被忽略,一旦出事就很麻煩。預約架構諮詢,讓我們幫你設計安全的整合架構。


發布注意事項

App Store / Play Store 審核

隱私政策要求

如果 App 會收集對話內容,需要在隱私政策中說明:

  • 收集什麼資料
  • 資料如何使用
  • 資料保存多久
  • 使用者如何刪除資料

權限說明

如果使用麥克風權限,需要說明為什麼需要:

  • 「用於語音輸入,讓您可以用說的與 AI 助理對話」

效能優化

減少啟動時間

// 懶載入 Dialogflow 客戶端
private val dialogflowClient by lazy {
    DialogflowClient(applicationContext)
}

背景處理

// 在背景執行緒處理
viewModelScope.launch(Dispatchers.IO) {
    val response = dialogflowClient.detectIntent(text)
    withContext(Dispatchers.Main) {
        updateUI(response)
    }
}

離線處理

當沒有網路時的處理:

suspend fun detectIntent(text: String): String {
    if (!isNetworkAvailable()) {
        return "目前沒有網路連線,請稍後再試。"
    }

    return try {
        dialogflowClient.detectIntent(text)
    } catch (e: IOException) {
        "網路連線不穩定,請稍後再試。"
    }
}

更多 API 整合細節,請參考 Dialogflow Fulfillment 與 API 整合教學。Intent 設計請參考 Dialogflow Intent 與 Context 教學。費用估算請參考 Dialogflow 費用完整解析


下一步

完成行動整合後,你可以:

  1. 優化對話設計Dialogflow Intent 與 Context 完整教學
  2. 開發後端整合Dialogflow Fulfillment 與 API 整合教學
  3. 控制成本Dialogflow 費用完整解析

想在 App 中加入 AI 對話功能?

在 App 中整合 AI 對話涉及很多技術細節:API 整合、安全性、效能優化、審核合規...

如果你需要:

  • 在現有 App 中加入 AI 客服功能
  • 開發具備語音助理的新 App
  • 設計安全可靠的整合架構
  • 跨平台(iOS + Android)解決方案

預約 AI 導入諮詢,讓有經驗的團隊幫你規劃和實作。

我們已協助多個 App 成功整合 AI 對話功能,諮詢完全免費。


需要專業的雲端建議?

無論您正在評估雲平台、優化現有架構,或尋找節費方案,我們都能提供協助

預約免費諮詢

相關文章