I. 概要
Flutter には独自の Dart パッケージ リポジトリもあります。プラグインの開発と再利用により、開発効率の向上とプロジェクトの結合度の削減が可能であり、ネットワークリクエスト(http)、ユーザー認可(permission_handler)などの共通機能モジュールやその他のクライアントサイド開発では、対応するプラグインを導入するだけで済みます。 -ins を使用して、プロジェクトの関連機能を迅速に統合し、特定のビジネス機能の実現に集中します。
開発者は、ウェアハウス内の一般的なコンポーネントを使用するだけでなく、Flutter プロジェクトの開発中に一般的なビジネス ロジックの分割やネイティブ機能のカプセル化の必要性に直面した場合でも、新しいコンポーネントを開発する必要があります。この記事では、特定のnative_image_viewプラグインを例として、Flutterコンポーネントの作成、開発、テスト、リリースを多面的に紹介し、Flutterコンポーネント全体の開発とリリースのプロセスを完全に説明します。
2. FlutterとNative間の通信
Flutter プラグインの開発では、ほとんどの場合、Flutter と Native の間でデータのやり取りが必要になるため、プラグインを開発する前に、プラットフォーム チャネルの仕組みを簡単に理解しておきましょう。
Flutter と Native 間の通信は、C/S モデルであるプラットフォーム チャネルを通じて実現され、Flutter がクライアントとして機能し、iOS および Android プラットフォームがホストとして機能します。Flutter はこのメカニズムを通じてネイティブにメッセージを送信し、ネイティブはメッセージを受信したプラットフォームに独自の API を実装し、処理結果を Flutter ページに返します。
Flutter のプラットフォーム チャネル メカニズムは、次の 3 つの対話方法を提供します。
-
BasicMessageChannel: 文字列と半構造化情報を渡すために使用されます。
-
MethodChannel: メソッド呼び出しを渡し、コールバックを処理するために使用されます。
-
EventChannel: データ ストリームの監視と送信に使用されます。
これら 3 つのチャネルには異なる目的がありますが、すべてに 3 つの重要なメンバー変数が含まれています。
(1)文字列名
チャンネルの名前を示します。プロジェクトには多数のチャンネルが存在する可能性があるため、各チャンネルは一意の名前を使用する必要があります。そうしないと上書きされる可能性があります。推奨される命名方法は、組織の名前とプラグインの名前を組み合わせたものです (例: com.tencent.game/native_image_view)。プラグインに複数のチャネルが含まれている場合は、機能モジュールに従ってさらに区別できます。
(2)BinaryMessager メッセージャ
NativeとFlutter間の通信キャリアとして、コーデックで変換されたバイナリデータをNativeとFlutter間で転送することができます。各チャネルは、初期化時に対応するメッセージャを生成または提供する必要があります。チャネルが対応するハンドラを登録すると、メッセージャは名前とハンドラの間のマッピング関係を維持します。
Nativeプラットフォームが相手からメッセージを受信すると、meesagerはメッセージの内容を対応するハンドラーに振り分けて処理し、処理完了後はコールバックメソッドのresultを通じてFlutterに処理結果を返すことができます。
(3)MessageCodec/MethodCodecコーデック
NativeとFlutter間の通信時のエンコードとデコードに使用され、送信側はFlutter(またはNative)の基本型をバイナリにエンコードしてデータ送信することができ、受信側はNative(またはFlutter)がバイナリを基本型に変換することができます。ハンドラーが認識できること。
注: この記事で実装されているネイティブイメージ共有プラグインは、最も一般的に使用される MethodChannel 通信のみを使用します。Flutter は、リモート画像アドレスまたはローカル画像ファイル名を MethodChannel を通じてネイティブ側に渡します。iOS および Android プラットフォームが画像を取得した後、変換します。それをバイナリに変換し、結果を通じて返します。MessageChannel と EventChannel のその他の例は、この記事の最後に参照用として記載されているので、詳細を参照してください。
3. プラグインの作成
Flutter コンポーネントは、ネイティブ コードが含まれているかどうかに応じて 2 つのタイプに分類できます。
-
Flutter Package (パッケージ) : dart コードのみが含まれます。一般的には、ネットワーク リクエストの http パッケージなど、フラッター固有の機能のカプセル化実装です。
-
Flutter Plugin (プラグイン) : Dart コードに加えて、Android および iOS プラットフォームのコード実装も含まれており、クライアントのネイティブ機能をカプセル化して Flutter プロジェクトに提供するためによく使用されます。たとえば、キーボードの表示状態を決定するために使用される flutter_keyboard_visibility プラグインは、iOS 側と Android 側でそれぞれキーボードの開閉イベントを監視し、対応するイベントをプラットフォーム チャネルを通じて Flutter プロジェクトに渡します。
-
Flutter プラグインは、Android Studio を通じて作成することも (Dart および Flutter プラグインを Android Studio にインストールする必要があります)、コマンド ラインを使用して作成することもできます。
-
Flutterプラグインを作成する
flutter create --org com.qidian.image --template=plugin --platforms=android,ios -i objc -a javanative_image_view
--template=plugin ステートメントを使用すると、iOS と Android の両方のコードを含むプラグインが作成されます。
--org オプションを使用して組織を指定します。通常は逆ドメイン名の表記を使用します。
-i オプションを使用して、iOS プラットフォーム開発言語、objc または swift を指定します。
-a オプションを使用して、Android プラットフォーム開発言語 (java または kotlin) を指定します。
lib ディレクトリはパッケージのコード実装を保存するために使用され、Flutter スキャフォールディングはパッケージと同じ名前の dart ファイルを自動的に生成します。
pubspec.yaml ファイルは、Flutter 開発を行った学生にはよく知られているはずで、パッケージの開発に使用するパッケージまたはプラグインは、このファイルで宣言する必要があります。
4. プラグインの開発
Plugin と Package の開発とリリースのプロセスは基本的に同じですが、Plugin は iOS や Android の開発も伴うため、実装はより複雑です。
Flutter がネイティブ プロジェクトに埋め込まれているシナリオでよくある問題は、同じ画像が Flutter とネイティブ プロジェクトの両方で使用される場合、両側が別々に保存されること、つまり画像が 2 回保存されることです。ネイティブの画像取得と表示に依存する Weex、Hippy、およびその他の JS ベースのクロスプラットフォーム フレームワークとは異なり、Flutter は画像を独自に管理し、Skia エンジンを通じて直接描画します。
この問題に対応して、この記事では Flutter 画像のダウンロードとキャッシュを Native に引き継ぎ、Flutter 側は画像の描画のみを行う Flutter プラグイン (native_image_view) を開発します。さらに、ローカル イメージの呼び出しを処理する特別なプロトコルを定義すると同時に、Flutter がネイティブ プロジェクトのローカル イメージを再利用できない問題を解決することもできます。
注: この記事で開発されたプラグインは、プラグインの開発およびリリース プロセスを紹介するためにのみ使用されます。実稼働環境で直接使用することはお勧めできません。イメージの二次キャッシュの問題については、次のこともできます。テクスチャ(外部テクスチャ)については、詳しくは「多読」の記事を参照してください。
1.フラッターサイド開発
まず、Flutter 側でプラグインの MethodChannel を宣言し、initState メソッドの invokeMethod (メソッド名、パラメーター) を介してネイティブ側へのメソッド呼び出しを開始します。setState を呼び出し、Image.memory メソッドを使用して描画します。バイナリデータを画像表示に変換します。
ネイティブイメージビュー.dart:
class _NativeImageViewState extends State<NativeImageView> {
Uint8List _data;
static const MethodChannel _channel =
const MethodChannel('com.tencent.game/native_image_view');
@override
void initState() {
super.initState();
loadImageData();
}
loadImageData() async {
_data = await _channel.invokeMethod("getImage", {"url": widget.url});
setState(() {});
}
@override
Widget build(BuildContext context) {
return _data == null
? Container(
color: Colors.grey,
width: widget.width,
height: widget.height,
)
: Image.memory(
_data,
width: widget.width,
height: widget.height,
fit: BoxFit.cover,
);
2. ネイティブ開発
(1) iOS開発
プラグインの iOS プラットフォームは SDWebImage コンポーネントを使用してネットワーク イメージをダウンロードしてキャッシュするため、native_image_view.podspec ファイルで依存関係を宣言します。
s.dependency 'Flutter'
s.dependency 'SDWebImage'
s.platform = :ios, '8.0'
Flutter スキャフォールディングは、NativeImageViewPlugin.m ファイルと registerWithRegistrar メソッドを自動的に生成します。このメソッドはコンポーネント実行のエントリ ポイントであり、Flutter のプラグイン マネージャーによって自動的に呼び出されます。
このメソッドでは、Flutter 側と同じ名前の MethodChannel を作成し、Flutter 側でメソッド呼び出しを処理するプラグイン オブジェクトのインスタンスを作成します。MethodChannel が Flutter 側からメソッド呼び出しを受け取った後に、handleMethodCall メソッドがトリガーされます。開発者は、FlutterMethodCall を通じてメソッド名とパラメータを取得し、FlutterResult を通じて画像コンテンツを返すことができます。
NativeImageViewPlugin.m:
#import "NativeImageViewPlugin.h"
#import <SDWebImage/SDWebImage.h>
@implementation NativeImageViewPlugin
//组件注册接口,Flutter自动调用
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
FlutterMethodChannel* channel = [FlutterMethodChannel
methodChannelWithName:@"com.tencent.game/native_image_view"
binaryMessenger:[registrar messenger]];
NativeImageViewPlugin* instance = [[NativeImageViewPlugin alloc] init];
[registrar addMethodCallDelegate:instance channel:channel];
}
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
if ([@"getImage" isEqualToString:call.method]) {
[self getImageHandler:call result:result];
} else {
result(FlutterMethodNotImplemented);
}
}
- (void)getImageHandler:(FlutterMethodCall*)call result:(FlutterResult)result{
if(call.arguments != nil && call.arguments[@"url"] != nil){
NSString *url = call.arguments[@"url"];
if([url hasPrefix:@"localImage://"]){
//获取本地图片
NSString *imageName = [url stringByReplacingOccurrencesOfString:@"localImage://" withString:@""];
UIImage *image = [UIImage imageNamed:imageName];
if(image != nil){
NSData *imgData = UIImageJPEGRepresentation(image,1.0);
result(imgData);
}else{
result(nil);
}
}else {
//获取网络图片
UIImage *image = [[SDImageCache sharedImageCache] imageFromCacheForKey:url];
if(!image){
//本地无缓存,下载后返回图片
[[SDWebImageDownloader sharedDownloader]
downloadImageWithURL:[[NSURL alloc] initWithString:url]
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
if(finished){
result(data);
[[SDImageCache sharedImageCache] storeImage:image forKey:url completion:nil];
}
}];
}else{
//本地有缓存,直接返回图片
NSData *imgData = UIImageJPEGRepresentation(image,1.0);
result(imgData);
}
}
}
}
@end
Flutter によって開始されたイメージ呼び出しを処理するときは、まず Flutter がローカル イメージを要求するかネットワーク イメージを要求するかを決定します。ローカル イメージの場合は、イメージのバイナリ データを UIImage オブジェクトから直接読み取って返します。ネットワーク イメージの場合は、まずローカル キャッシュがあるかどうかを確認します。キャッシュがある場合は直接返します。キャッシュがない場合は、最初に画像をダウンロードしてからデータを返す必要があります。
(2) Android開発
プラグインの Android プラットフォームは、Glide コンポーネントを使用してネットワーク イメージをダウンロードしてキャッシュし、依存関係を build.gradle ファイルで宣言する必要があります。
dependencies {
implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
}
履歴バージョンと互換性を保つために、Android 側プラグインは、onAttachedToEngine メソッドと registerWith メソッドに同じ MethodChannel 登録および監視ロジックを実装する必要があります。onMethodCall は Flutter でメソッド呼び出しを処理するために使用され、MethodCall と Result も提供しますiOS プラットフォームに似たオブジェクト。
android/src/main/xxxx/NativeImageViewPlugin.java:
//新的插件注册接口
@Override
public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) {
channel = new MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "com.tencent.game/native_image_view");
channel.setMethodCallHandler(this);
setContext(flutterPluginBinding.getApplicationContext());
}
@Override
public void onDetachedFromEngine(FlutterPluginBinding binding) {
channel.setMethodCallHandler(null);
}
// Flutter-1.12之前的插件注册接口,功能与onAttachedToEngine一样
public static void registerWith(Registrar registrar) {
NativeImageViewPlugin plugin = new NativeImageViewPlugin();
plugin.setContext(registrar.context());
final MethodChannel channel = new MethodChannel(registrar.messenger(), "com.tencent.game/native_image_view");
channel.setMethodCallHandler(plugin);
}
@Override
public void onMethodCall(final MethodCall call,final Result result) {
if (call.method.equals("getImage")) {
getImageHandler(call,result);
} else {
result.notImplemented();
}
}
Android側のコード実装ロジックはiOSと同様で、まずFlutterがローカル画像を呼び出しているのかネットワーク画像を呼び出しているのかを判断し、ローカル画像の場合はファイル名から画像のBitmapを取得し、その後、バイト配列に変換されて返されます; ネットワーク イメージのキャッシュとダウンロードは Glide に基づいています コンポーネントは、ファイル キャッシュまたはダウンロード パスを取得した後、ファイルをバイト配列として読み取って返すことを認識します。
public void getImageHandler(final MethodCall call,final Result result){
HashMap map = (HashMap) call.arguments;
String urlStr = map.get("url").toString();
Uri uri = Uri.parse(urlStr);
if("localImage".equals(uri.getScheme())){
String imageName = uri.getHost();
int lastIndex = imageName.lastIndexOf(".");
if(lastIndex > 0){
imageName = imageName.substring(0,lastIndex);
}
String imageUri = "@drawable/"+imageName;
int imageResource = context.getResources().getIdentifier(imageUri, null, context.getPackageName());
if(imageResource > 0){
Bitmap bmp = BitmapFactory.decodeResource(context.getResources(),imageResource);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bmp.compress(Bitmap.CompressFormat.PNG, 100, stream);
byte[] byteArray = stream.toByteArray();
result.success(byteArray);
}else{
result.error("NOT_FOUND","file not found",call.arguments);
}
}else {
Glide.with(context).download(urlStr).into(new CustomTarget<File>() {
@Override
public void onResourceReady(@NonNull File resource, @Nullable Transition<? super File> transition) {
byte[] bytesArray = new byte[(int) resource.length()];
try {
FileInputStream fis = new FileInputStream(resource);
fis.read(bytesArray);
fis.close();
result.success(bytesArray);
} catch (IOException e) {
e.printStackTrace();
result.error("READ_FAIL",e.toString(),call.arguments);
}
}
@Override
public void onLoadFailed(@Nullable Drawable errorDrawable) {
super.onLoadFailed(errorDrawable);
result.error("LOAD_FAIL","image download fail",call.arguments);
}
@Override
public void onLoadCleared(@Nullable Drawable placeholder) {
result.error("LOAD_CLEARED","image load clear",call.arguments);
}
});
}
}
5. プラグインテスト
Flutter スキャフォールディングは、プラグインの作成時にサンプル プロジェクトを自動的に生成します。このプロジェクトは、プラグインのパスを指定することで開発中のコンポーネントを参照するため、リリース前にプラグインを完全にテストできます。
native_image_view:
path: ../
開発とデバッグに加えて、サンプル プロジェクトはプラグインの使用例としても役立ちます。多くの開発者は、ドキュメントと比較して、プラグイン サンプルのコード実装を直接見ることを好みます。main.dart でネットワーク イメージの使用を示します。ローカル イメージには、ネイティブ プロジェクトに対応するファイルが必要です。
メイン.ダーツ:
String url = "";
//String url = "localImage://xxx.jpeg";
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('example'),
),
body: Center(
child: NativeImageView(
url: url,
width: 300,
height: 200,
),
),
));
}
6. プラグインのリリース
プラグインの開発が完了すると、リリース リンクに入ります。その後のメンテナンスとユーザー フィードバックを容易にするために、プラグインを github でメンテナンスし、プラグインの pubspec.yaml ファイルにウェアハウス アドレスを記入します。 -で
name: native_image_view
description: 该组件提供了一种方式,可以让flutter通过methodChannel调用原生的本地和网络图片的加载
version: 0.0.1
repository:
ウェアハウスにコミットする前に、ドライラン コマンドを実行して、コンポーネントが現在リリース要件を満たしているかどうかを確認する必要があります。
flutter pub publish --dry-run
Flutter スキャフォールディングによって作成された LICENSE ファイルは空であり、開発者はプラグインのオープンソース契約を自分で記入する必要があります。入力しない場合、ドライランではプロンプトは表示されませんが、ウェアハウスで公開するステップでエラーが報告されます。
LICENSE ファイルの作成方法は前回の記事で説明したので、詳細は説明しません。