NCMBのFlutter SDKを使って翻訳アプリを作る（その2：翻訳画面の構築とDeepLを使った翻訳処理）

NCMBでは公式SDKとしてSwift/Objective-C/Kotlin/Java/Unity/JavaScript SDKを用意しています。また、それ以外にもコミュニティSDKとして、非公式ながらFlutter/React Native/Google Apps Script/C#/Ruby/Python/PHPなど幅広い言語向けにSDKが開発されています。

今回はコミュニティSDKの一つ、Flutter SDKとDeepL APIを使って翻訳アプリを作ってみます。[前回は画面の仕様とSDKの初期化について解説しました](https://qiita.com/goofmint/items/06b4b2e40e57d4d35d5c)ので、今回は翻訳画面と翻訳処理について解説します。

[ncmb | Dart Package](https://pub.dev/packages/ncmb)

## 完成版のコード

作成したデモアプリのコードは[NCMBMania/flutter_translation_app: Flutter SDKとDeepL APIを組み合わせた翻訳アプリです](https://github.com/NCMBMania/flutter_translation_app)にアップロードしてあります。

## メイン画面について

メイン画面（MyApp）ではFlutterアプリの初期化を行っているだけです。

```dart
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Translation App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      debugShowCheckedModeBanner: false,
      home: MainPage(),
    );
  }
}
```

## MainPageウィジェットについて

MainPageはタブを用意して、翻訳画面と履歴画面をそれぞれ読み込んでいます。

```dart
// タブ表示用
class MainPage extends StatelessWidget {
  MainPage({Key? key}) : super(key: key);
  final _tabs = [
    const Tab(icon: Icon(Icons.translate)), // 翻訳画面
    const Tab(icon: Icon(Icons.list)), // 履歴画面
  ];
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: DefaultTabController(
        length: _tabs.length,
        child: Scaffold(
          appBar: AppBar(
            title: const Text('翻訳アプリ'),
            bottom: TabBar(
              tabs: _tabs.toList(),
            ),
          ),
          body: const TabBarView(
            children: [
              TranslationPage(),
              HistoryPage(),
            ],
          ),
        ),
      ),
    );
  }
}
```

![Simulator Screen Shot - iPhone SE (3rd generation) - 2022-06-20 at 16.59.01のコピー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/a80dd371-adeb-f7ec-5b67-560728271114.png)


## TranslationPageについて

TranslationPageは_TranslationPageStateを読み込んでいるだけです。

```dart
class TranslationPage extends StatefulWidget {
  const TranslationPage({Key? key}) : super(key: key);
  @override
  State<TranslationPage> createState() => _TranslationPageState();
}
```

## _TranslationPageStateについて

_TranslationPageStateの役割は以下の3つです。

- 翻訳画面の構築
- 入力テキストの翻訳とNCMBへの保存
- 入力テキストの削除
- 翻訳結果のクリップボードへのコピー

```dart
// 翻訳画面
class _TranslationPageState extends State<TranslationPage> {
}
```

### インスタンス変数

用意しているインスタンス変数は次の通りです。

```dart
// テキスト編集用コントローラー
final _controller = TextEditingController();
// 翻訳結果が入る変数
var _translateText = "";
// DeepL用ライブラリ
final _translator = Translator(authKey: dotenv.env['DEEPL_AUTH_KEY']!);
```

### 画面構築

ColumnとRowを組み合わせているだけなので難しいものではありません。翻訳するボタンをタップした際のイベント `_translate` と 翻訳結果をクリップボードにコピーする `_copyText` 、入力テキストを削除する `_clearText` を呼び出しています。

```dart
@override
Widget build(BuildContext context) {
	return Center(
		child: Column(
			mainAxisAlignment: MainAxisAlignment.center,
			children: [
				const Text(
					'翻訳する言葉を入力してください',
				),
				Row(
					mainAxisAlignment: MainAxisAlignment.center,
					children: [
						SizedBox(
							width: 260,
							child: TextFormField(
								onFieldSubmitted: (str) {
									_translate();
								},
								controller: _controller,
								decoration: const InputDecoration(
									hintText: "日本語…",
								),
								keyboardType: TextInputType.multiline,
								maxLines: null,
							),
						),
						TextButton(onPressed: _translate, child: const Text("翻訳する"))
					],
				),
				Padding(
					padding: const EdgeInsets.all(10),
					child: Container(color: Colors.white),
				),
				_translateText != ""
						? Column(
								children: [
									Row(mainAxisAlignment: MainAxisAlignment.center, children: [
										const Text("翻訳結果"),
										TextButton(
												onPressed: _copyText, child: const Icon(Icons.copy)),
										TextButton(
												onPressed: _clearText, child: const Icon(Icons.clear))
									]),
									Text(_translateText),
								],
							)
						: const Text("翻訳結果がこの下に表示されます")
			],
		),
	);
}
```

![Simulator Screen Shot - iPhone SE (3rd generation) - 2022-06-20 at 16.59.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/6fbb51ea-035f-b558-d67d-c1b82b21d6fa.png)


### 翻訳処理を行う _translate 関数について

翻訳処理は `translator.translateTextSingular` だけで完結します。2つ目の引数は翻訳後の言語です。翻訳が完了したら、元の日本語とともにニフクラ mobile backendのデータストア（クラウドデータベース）に保存します。その際にはNCMBObjectを利用します。saveメソッドはasync/awaitですが、処理を特に待つ必要はないのでawaitを使っていません。

```dart
// 翻訳処理
void _translate() async {
	// 入力されたテキスト（日本語）
	var originalText = _controller.text;
	// DeepLの翻訳処理
	final result =
			await _translator.translateTextSingular(originalText, 'en-US');
	// 結果を入れるデータストアのオブジェクト
	final translate = NCMBObject("Translate");
	// オブジェクトに必要なデータをセット
	translate
		..set("original", originalText)
		..set("translate", result.text);
	// 保存
	translate.save();
	// 画面に翻訳結果を反映
	setState(() {
		_translateText = result.text;
	});
}
```

![Simulator Screen Shot - iPhone SE (3rd generation) - 2022-06-20 at 17.00.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/593b146e-3de3-0f25-bea7-4ac4860deb47.png)


### クリップボードへコピーする _copyText 関数について

クリップボードを操作する際には `flutter/services` が必要です。

```dart
import 'package:flutter/services.dart';
```

そして、以下のようにClipboardクラスにデータを送信します。

```dart
// 翻訳結果をクリップボードにコピー
void _copyText() {
	Clipboard.setData(ClipboardData(text: _translateText));
}
```

### 入力値をクリアする_clearText関数について

こちらはテキストを削除するだけです。

```dart
// 入力、翻訳結果を消す処理
void _clearText() {
	_controller.text = "";
	setState(() {
		_translateText = "";
	});
}
```

## まとめ

今回は翻訳画面の作成と翻訳処理、そしてクリップボードへのコピー処理までを解説しました。次回は[翻訳結果の履歴取得と表示までを解説します](https://qiita.com/goofmint/items/f5d7922b87ad4120c067)。