Integrasi Flutter dengan Library Reader Handheld

Belakangan ini, Flutter telah menjadi salah satu framework UI open-source populer pada berbagai platform, termasuk perangkat mobile seperti Android dan iOS, serta platform lainnya seperti web dan desktop. Flutter menawarkan kemudahan dalam pembuatan antarmuka pengguna (UI) yang menarik dan performa tinggi dengan satu basis kode.

Dalam tutorial kali ini, akan membahas bagaimana SDK Reader handheld Electron dapat diintegrasikan dan digunakan dalam Flutter.

Gambaran Umum

Hampir semua reader Electron menggunakan bahasa Java untuk SDK dan contoh program demo tersedia.

Komunikasi Flutter memanggil fungsi-fungsi Reader tetap membutuhkan kode Java. Memanggil kode Java dengan Dart (yang berjalan di Flutter) mekanisme ini dinamakan PlatformChannel. PlatformChannel memungkinkan aplikasi Flutter untuk memanfaatkan fungsi atau fitur yang tidak tersedia di Flutter, tetapi tersedia di platform native, seperti akses ke sensor, Bluetooth, atau API platform lainnya.

Dengan demikian, Flutter akan dijadikan sebagai UI dan pengolahan data, sedangkan untuk memanggil fungsi Reader akan dilakukan oleh kode Java dipanggil dengan kode Dart melalui PlatformChannel.

Terdapat 2 method yang dapat digunakan dalam proses pemanggilan kode Java:

MethodChannel

Jenis komunikasi Request-Response. Kode Dart akan request, kode Java akan berikan 1x response. Pada kasus reader, cocok digunakan untuk inventory answer mode, write tag, atau get/set reader settings.

pigeon | Package salah satu Flutter package yang dapat mengenerate kode MethodChannel, dapat mempercepat development dan type-safe.

EventChannel

Jenis komunikasi Stream (continuous). Data akan dikirim berkelanjutan dari kode Java. Kode Dart hanya akan listen tanpa mengirim data. Pada kasus reader, cocok digunakan untuk listen bluetooth terputus, atau ketika user menekan fire button (hard button pistol) agar dapat mengeksekusi kode Dart.

Lebih lanjut dapat dipelajari di dokumentasi resmi Flutter Platform-specific code.

Tutorial

Buat flutter project dengan spesifik bahasa Android ke bahasa Java:

flutter create --a java flutter_tutorial_el_uhf_rh02

Import Library

Import/paste folder & file library ke folder/android/app/ :

  • /android/app/libs: Berisi file *.jar
  • /android/app/src/main/jniLibs: Berisi file *.so

Library *.jar dan *.so, silahkan diambil pada contoh program demo reader yang Anda beli (beda reader beda Library SDK).

Pada file /android/app/build.gradle tentukan folder library *.jar berada

...
 
// Add /.jar file
dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
}

Tambahkan file /android/app/proguard-rules.pro, agar saat build release class-class Library yang dibutuhkan tetap dipertahankan:

-keep class com.handheld.** { *; }
-keep class com.uhf.api.** { *; }
-keep class cn.pda.serialport.** { *; }

Sesuaikan isi file proguard-rules.pro pada program demo yang tersedia (beda reader beda isi file).

Daftarkan file proguard-rules.pro pada file /android/app/build.gradle:

buildTypes {
	release {
		// TODO: Add your own signing config for the release build.
		// Signing with the debug keys for now, so `flutter run --release` works.
		signingConfig signingConfigs.debug
		proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' // Add proguard-rules.pro
	}
}

Setelah semua ditambahkan, tree folder /android/app/ akan terlihat seperti berikut:

.
├── build.gradle
├── libs
│   ├── ...*.jar
├── proguard-rules.pro
└── src
    ├── main
    │   ├── AndroidManifest.xml
    │   ├── java
    │   │   ├── com
    │   │   │   └── example
    │   │   │       └── flutter_tutorial_el_uhf_rh02
    │   │   │           └── MainActivity.java
    │   │   └── io
    │   │       └── flutter
    │   │           └── plugins
    │   │               ├── GeneratedPluginRegistrant.java
    │   ├── jniLibs
    │   │   ├── arm64-v8a
    │   │   │   ├── ...*.so
    │   │   ├── armeabi
    │   │   │   ├── ...*.so
    │   │   └── armeabi-v7a
    │   │       ├── ...*.so
    │   └── res
    │       ├── drawable
...

Code

Untuk kode lebih lengkap, dapat di download pada link berikut: https://cloud.electron.id/s/DJJLDn554opWeSX

Pada tutorial kali ini kami menggunakan pigeon | Package, pertama buat folder pigeons/ sejajar dengan folder lib/ Flutter.

Buat file pigeons/reader.dart:

import 'package:pigeon/pigeon.dart';
 
class ReaderPower {
  int? readPower;
  int? writePower;
}
 
class InventoryResult {
  String? epc;
  int? rssi;
}
 
@HostApi()
abstract class ReaderApi {
  @async
  bool init();
 
  @async
  ReaderPower? getPower();
 
  @async
  bool setPower(int readPower, int writePower);
 
  @async
  bool writeEpc(String currentEpc, String toWriteEpc, String accessPassowrd);
 
  @async
  List<InventoryResult?> inventory();
 
  @async
  bool dispose();
}

Buat fungsi’ yang ingin direquest oleh Flutter, pada contoh hanya ada 6, silahakan tambahkan sesuai keperluan:

  • init: inisialisasi
  • inventory: inventory tag
  • getPower: ambil data reader power
  • setPower: atur reader power
  • writeEpc: write EPC memory
  • dispose: close library

Jalankan command berikut untuk mengenerate kode Flutter reader.dart & Java Pigeon.java:

flutter pub run pigeon --input pigeons/reader.dart --dart_out lib/services/reader.dart --java_out ./android/app/src/main/java/io/flutter/plugins/Pigeon.java --java_package "io.flutter.plugins"

MainActivity.java

Lakukan pengkodean Java dengan import Pigeon.Java hasil generate.

// MainActivity.java
 
package com.example.flutter_tutorial_el_uhf_rh02;
 
import android.view.KeyEvent;
import android.util.Log;
import android.os.Handler;
import android.os.Looper;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
 
import androidx.annotation.NonNull;
 
import cn.pda.serialport.Tools;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.EventChannel.EventSink;
import io.flutter.plugin.common.EventChannel.StreamHandler;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
 
import com.handheld.uhfr.UHFRManager;
import com.uhf.api.cls.Reader;
 
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
 
import io.flutter.plugins.Pigeon.Result;
import io.flutter.plugins.Pigeon.NullableResult;
import io.flutter.plugins.Pigeon.ReaderApi;
import io.flutter.plugins.Pigeon.InventoryResult;
import io.flutter.plugins.Pigeon.ReaderPower;
 
public class MainActivity extends FlutterActivity {
    private static final String TAG = "MainActivity";
    private static UHFRManager uhfrManager;
 
    private BinaryMessenger messenger;
    private EventChannel fireButtonEventChannel;
    private EventSink fireButtonEventSink;
 
    @Override
    public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
        super.configureFlutterEngine(flutterEngine);
        Log.d(TAG, "configureFlutterEngine");
 
        String packageName = getApplicationContext().getPackageName();
        messenger = flutterEngine.getDartExecutor().getBinaryMessenger();
 
        // Set up Pigeon-generated ReaderApi
        ReaderApi.setUp(messenger, new ReaderApiHandler());
 
        // Set up EventChannel for fire button
        fireButtonEventChannel = new EventChannel(messenger, packageName + "/fire_button");
        fireButtonEventChannel.setStreamHandler(fireButtonStreamHandler);
    }
 
    // Handler class for ReaderApi methods
    public class ReaderApiHandler implements ReaderApi {
 
        // Init - Init library variable (UHFRManager) and check if connect to reader
        @Override
        public void init(Result<Boolean> callback) {
            Log.d(TAG, "ReaderApiHandler.init");
 
            uhfrManager = UHFRManager.getInstance();
            if (uhfrManager == null || uhfrManager.getHardware() == null) {
                callback.error(new Exception("Failed to initialize reader (can't get hardware version)"));
                return;
            }
 
            uhfrManager.setGen2session(false);
            callback.success(true);
        }
 
        // Inventory
        @Override
        public void inventory(Result<List<InventoryResult>> callback) {
            new Thread(() -> {
                uhfrManager.setCancleInventoryFilter();
                uhfrManager.setGen2session(false);
 
                List<InventoryResult> inventoryResults = new ArrayList<>();
                List<Reader.TAGINFO> tags = uhfrManager.tagInventoryByTimer((short) 100);
 
                if (tags != null) {
                    for (Reader.TAGINFO tag : tags) {
                        InventoryResult inventoryResult = new InventoryResult();
                        inventoryResult.setEpc(Tools.Bytes2HexString(tag.EpcId, tag.EpcId.length));
                        inventoryResult.setRssi((long) tag.RSSI);
                        inventoryResults.add(inventoryResult);
                    }
                }
 
                callback.success(inventoryResults);
            }).start();
        }
 
        // Get power
        @Override
        public void getPower(NullableResult<ReaderPower> callback) {
            int[] power = uhfrManager.getPower();
            if (power != null) {
                ReaderPower readerPower = new ReaderPower();
                readerPower.setReadPower((long) power[0]);
                readerPower.setWritePower((long) power[1]);
                callback.success(readerPower);
                return;
            }
 
            callback.error(new Exception("Failed to get reader power"));
        }
 
        // Set power
        @Override
        public void setPower(Long readPower, Long writePower, Result<Boolean> callback) {
            Reader.READER_ERR response = uhfrManager.setPower(readPower.intValue(), writePower.intValue());
            if (response == Reader.READER_ERR.MT_OK_ERR) {
                callback.success(true);
                return;
            }
 
            callback.error(new Exception("Failed to set reader power"));
        }
 
        // Write EPC
        @Override
        public void writeEpc(String currentEpc, String toWriteEpc, String accessPassword,
                Result<Boolean> callback) {
            new Thread(() -> {
                byte[] currentEpcBytes = Tools.HexString2Bytes(currentEpc);
                byte[] toWriteEpcBytes = Tools.HexString2Bytes(toWriteEpc);
                byte[] accessPasswordBytes = Tools.HexString2Bytes(accessPassword);
 
                Reader.READER_ERR response = uhfrManager.writeTagDataByFilter((char) 1, 2, toWriteEpcBytes,
                        toWriteEpcBytes.length, accessPasswordBytes, (short) 3000,
                        currentEpcBytes, 1, 2, true);
 
                if (response == Reader.READER_ERR.MT_OK_ERR) {
                    callback.success(true);
                    return;
                }
 
                callback.success(false);
            }).start();
        }
 
        // Dispose, close the library variable
        @Override
        public void dispose(Result<Boolean> callback) {
            uhfrManager.close();
            callback.success(true);
        }
    }
 
    // StreamHandler for the fire button
    private final StreamHandler fireButtonStreamHandler = new StreamHandler() {
        private final BroadcastReceiver fireButtonReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                int keyCode = intent.getIntExtra("keyCode", intent.getIntExtra("keycode", 0));
                boolean keyDown = intent.getBooleanExtra("keydown", false);
 
                if (keyCode == KeyEvent.KEYCODE_F4) {
                    Map<String, Object> keyShootMap = new HashMap<>();
                    keyShootMap.put("key_code", keyCode);
                    keyShootMap.put("key_down", keyDown);
                    fireButtonEventSink.success(keyShootMap);
                }
            }
        };
 
        @Override
        public void onListen(Object args, EventSink eventSink) {
            fireButtonEventSink = new EventSinkWrapper(eventSink);
 
            IntentFilter filter = new IntentFilter("android.rfid.FUN_KEY");
            registerReceiver(fireButtonReceiver, filter);
 
            Log.d(TAG, "fireButtonStreamHandler.onListen");
        }
 
        @Override
        public void onCancel(Object args) {
            fireButtonEventSink = null;
            Log.d(TAG, "fireButtonStreamHandler.onCancel");
        }
    };
 
    // Wrapper for EventSink to ensure execution on the main thread
    private static class EventSinkWrapper implements EventSink {
        private final EventSink eventSink;
        private final Handler handler = new Handler(Looper.getMainLooper());
 
        EventSinkWrapper(EventSink eventSink) {
            this.eventSink = eventSink;
        }
 
        @Override
        public void success(Object result) {
            handler.post(() -> eventSink.success(result));
        }
 
        @Override
        public void error(@NonNull String errorCode, String errorMessage, Object errorDetails) {
            handler.post(() -> eventSink.error(errorCode, errorMessage, errorDetails));
        }
 
        @Override
        public void endOfStream() {
            handler.post(eventSink::endOfStream);
        }
    }
}

configureFlutterEngine: method bawaan FlutterActivity setiap aplikasi mulai dijakankan, bisa untuk inisialiasi varible Java.

ReaderApiHandler implements ReaderApi: panggil fungsi’ library di dalam class ini, override tiap method yang sudah ada di Pigeon.Java.

StreamHandler fireButtonStreamHandler untuk Flutter listen EventChannel user menekan fire button (hard button pistol).

Flutter

Untuk Flutter tinggal memanggil class ReaderApi() hasil generate dari library pigeon | Package, pada contoh letaknya di lib/services/reader.dart.

Panggil class tersebut seperti proses memanggil service logic request Database atau request API Endpoint.

Pada contoh kami menggunakan state management bloc (cubit), sehingga proses memanggil class ReaderApi() dan EventChannel terjadi di class state management tersebut.

Video Demo