💡
【Flutter】SJISでファイルを出力する
はじめに
裏で昔のシステムが動いていたり、Windowsが絡んできたりすると往々にして「アプリから送信するファイルは SJIS で」ということがありますよね。
dart:convert
にEncoding
というクラスがあったのでサクッと対応できると思いきや Dart の Encoding
では SJIS が未対応でした。
いくつかプラグインがあるようでしたが、このためにプラグイン入れるのもなあと思ったので MethodChannel
を使ってAndroid/iOSネイティブ側にやってもらうことにしました。
実装する
- 同一プロジェクト内にプラグインのプロジェクトを作成
flutter create --template=plugin --platforms=ios,android flutter_sjis
- 生成されたMethodChannelを使うDartのクラスを変更
import 'dart:async';
import 'package:flutter/services.dart';
class FlutterSjis {
static const MethodChannel _channel = MethodChannel('flutter_sjis');
/// 文字列を受け取りSJISに変換
static Future<List<int>> encode(String value) async {
return await _channel.invokeMethod('encode', value);
}
/// SJISのバイト配列を受け取り文字列に変換
static Future<String> decode(List<int> source) async {
return await _channel.invokeMethod('decode', source);
}
}
3-1. Android側の実装
package com.github.kiyosuke.flutter_sjis
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import java.nio.charset.Charset
class FlutterSjisPlugin : FlutterPlugin, MethodCallHandler {
private lateinit var channel: MethodChannel
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "flutter_sjis")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"encode" -> encode(call, result)
"decode" -> decode(call, result)
else -> result.notImplemented()
}
}
private fun encode(call: MethodCall, result: Result) {
val value = call.arguments as String
val encoded = value.toByteArray(charset = Charset.forName("SJIS"))
result.success(encoded)
}
private fun decode(call: MethodCall, result: Result) {
val source = call.arguments as ByteArray
val decoded = String(source, charset = Charset.forName("SJIS"))
result.success(decoded)
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}
3-2. iOS側の実装
import Flutter
import UIKit
public class SwiftFlutterSjisPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "flutter_sjis", binaryMessenger: registrar.messenger())
let instance = SwiftFlutterSjisPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "encode":
encode(call, result: result)
break
case "decode":
decode(call, result: result)
break
default:
result(FlutterMethodNotImplemented)
break
}
}
private func encode(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
let value = call.arguments as! String
let encoded = value.data(using: String.Encoding.shiftJIS, allowLossyConversion: true)!
let data = FlutterStandardTypedData.init(bytes: encoded)
result(data)
}
private func decode(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
let source = call.arguments as! FlutterStandardTypedData
let decoded = String.init(bytes: source.data, encoding: String.Encoding.shiftJIS)!
result(decoded)
}
}
- メインプロジェクトの pubspec.yaml -> dependencies に作成したプラグインを追加
dependencies:
flutter:
sdk: flutter
# パス指定でローカルのプラグインを利用できる
flutter_sjis:
path: ./flutter_sjis
利用例
ボタンをタップしたら文字列をSJISにエンコードしファイル保存する例です。
保存先のディレクトリ取得には path_provider を使用します。
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_sjis/flutter_sjis.dart';
import 'package:path_provider/path_provider.dart';
class MyHomePage extends StatefulWidget {
const MyHomePage({
Key? key,
}) : super(key: key);
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool isLoading = false;
File? savedFile;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('FlutterSjis'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton.icon(
onPressed: () async {
setState(() => isLoading = true);
try {
savedFile = await saveToFile(testData);
setState(() {});
} finally {
setState(() => isLoading = false);
}
},
icon: const Icon(Icons.save),
label: const Text('保存'),
),
],
),
),
);
}
Future<File> saveToFile(String data) async {
final filesdir = await getApplicationSupportDirectory();
final timestamp = DateTime.now().millisecondsSinceEpoch;
final filename = '$timestamp.csv';
final file = File('${filesdir.path}/$filename');
final encodedData = await FlutterSjis.encode(data);
return await file.writeAsBytes(encodedData);
}
}
const testData = """
id,name,birthday,age,gender
1,佐藤,2000-01-01,22,男
2,鈴木,2002-01-01,20,男
3,亀田,2004-01-01,18,女
""";
解説
MethodChannel
MethodChannelはFlutterとAndroid/iOSといったプラットフォームの間を橋渡ししてくれる仕組みです。
Flutter側からはMethodChannel#invokeMethod
を実行することでネイティブで実装した処理を呼び出すことができます。
第一引数にメソッド名、第二引数に渡したいデータをとります。
static Future<List<int>> encode(String value) async {
return await _channel.invokeMethod('encode', value);
}
MethodChannelを介してやりとりできるデータの型には以下のものです。
Flutter(Dart) | Android(Kotlin) | iOS(Swift) |
---|---|---|
null | null | nil |
bool | Boolean | NSNumber(value: Bool) |
int | Int | NSNumber(value: Int32) |
int, if 32 bits not enough | Long | NSNumber(value: Int) |
double | Double | NSNumber(value: Double) |
String | String | String |
Uint8List | ByteArray | FlutterStandardTypedData(bytes: Data) |
Int32List | IntArray | FlutterStandardTypedData(int32: Data) |
Int64List | LongArray | FlutterStandardTypedData(int64: Data) |
Float32List | FloatArray | FlutterStandardTypedData(float32: Data) |
Float64List | DoubleArray | FlutterStandardTypedData(float64: Data) |
List | List | Array |
Map | HashMap | Dictionary |
プリミティブな型は一通りサポートされているので、大体のケースで利用できると思います。
備考
- iOSネイティブの実装に詳しくないので、これが正しい実装なのか不明
- この実装だと UTF-8 -> SJIS の変換に失敗する文字列もありますがそれは本記事の主題ではないので記載していません。
- 本記事のように別途プラグイン用のプロジェクトを作るべきか、プラグインを作成せずプロジェクト内に直接実装するべきなのか。。
個人的にプラグインで分けた方がわかりやすくて好きですが、何か問題が出てきたらまた考えようと思います。
Discussion