Flutter and native hybrid development

Posted by Adarmo on Sun, 16 Jan 2022 13:25:40 +0100

Flutter is a cross platform development of the mobile terminal. A set of code can be run on Android and iOS systems respectively, which can save development time and efficiency. At this stage, flutter is still in the stage of continuous development and updating, and can not fully adapt to multiple systems, especially calling some native functions, such as photo album, camera, address book, etc. At this time, it is necessary to call fluent and iOS or Android for hybrid development. The mixed development of flutter can be roughly divided into two scenarios. One is to take flutter as the main project to remove the function of using original sound, and the other is to embed flutter as a small module into the project based on native sound.

I Fluent calls native functions

Take the camera and battery as an example to introduce the native function called by the fluent

1.1. Camera

Some applications may need to use mobile devices to take photos or select photos in photo albums. The official plug-in provided by FLUENT is image_picker

1.1.1. Add dependency

Add to image_ Picker dependency: https://pub.dev/packages/image_picker

dependencies:
  image_picker: ^0.6.5

1.1.2. Platform configuration

For iOS platform, if you want to access photo albums or cameras, you need to obtain the user's permission:

  • Still modify info Plist file: / IOS / Runner / info plist
  • Add access to album: Privacy - Photo Library Usage Description
  • Add access to the camera: Privacy - Camera Usage Description

 

 

1.1.3. code implementation

image_ The core code of picker is the pickImage method:

  • You can input data source, picture size, quality, front and rear cameras, etc
  • Data source is a required parameter: ImageSource enumeration type
    • Camera: camera
    • gallery: photo album
      import 'package:flutter/material.dart';
      import 'dart:io';
      import 'package:image_picker/image_picker.dart';
      
      class HYCameraScreen extends StatefulWidget {
        static const String routeName = "/camera";
      
        @override
        _HYCameraScreenState createState() => _HYCameraScreenState();
      }
      
      class _HYCameraScreenState extends State<HYCameraScreen> {
        File _image;
      
        @override
        Widget build(BuildContext context) {
          return Scaffold(
            appBar: AppBar(
              title: Text("Camera"),
            ),
            body: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  _image == null ? Text("No pictures selected"): Image.file(_image),
                  RaisedButton(
                    child: Text("Select photos"),
                    onPressed: _pickImage,
                  )
                ],
              ),
            ),
          );
        }
      
        void _pickImage() async {
          File image = await ImagePicker.pickImage(source: ImageSource.gallery);
          setState(() {
            _image = image;
          });
        }
      }

       

1.2. Battery information

For some native information, if there is no good plug-in, we can obtain information through platform channels.

1.2.1. Introduction to platform channel

How does the platform channel work?

  • Messages are transmitted between the client (UI) and the host (platform) using platform channels;
  • Messages and responses are delivered asynchronously to ensure that the user interface can maintain response;

 

 

The calling process is as follows:

  • 1. The client (fluent side) sends the message corresponding to the method call
  • 2. The platform (iOS, Android) receives the method and returns the result;
    • The iOS end responds through the FlutterMethodChannel;
    • The Android end responds through the MethodChannel;

Correspondence of data types on fluent, iOS and Android:

 

 

1.2.2. Create test project

Here we create a project to obtain battery power information, and obtain the corresponding information through iOS and Android native codes respectively:

Creation method 1: default creation method

Currently, the Flutter project is created by default. The corresponding iOS programming language is Swift and the corresponding Android programming language is kotlin

flutter create batterylevel

Creation method 2: specify the programming language

 

If we want to specify the programming language, for example, the iOS programming language is Objective-C and the Android programming language is Java

flutter create -i objc -a java batterylevel2

1.2.3. Write Dart code

In Dart code, we need to create a MethodChannel object:

  • When creating this object, you need to pass in a name, which is the name that distinguishes multiple communications
  • You can send messages to the corresponding platform for communication by calling the invokeMethod of the object
    • This call is an asynchronous operation. You need to get the result through await then callback
      import 'package:flutter/material.dart';
      import 'package:flutter/services.dart';
      
      void main() => runApp(MyApp());
      
      
      class MyApp extends StatelessWidget {
        @override
        Widget build(BuildContext context) {
          return MaterialApp(
            title: 'Flutter Demo',
            theme: ThemeData(
                primarySwatch: Colors.blue, splashColor: Colors.transparent),
            home: HYBatteryScreen(),
          );
        }
      }
      
      class HYBatteryScreen extends StatefulWidget {
        static const String routeName = "/battery";
      
        @override
        _HYBatteryScreenState createState() => _HYBatteryScreenState();
      }
      
      class _HYBatteryScreenState extends State<HYBatteryScreen> {
        // Core code 1:
        static const platform = const MethodChannel("dzq.com/battery");
        int _result = 0;
      
        @override
        Widget build(BuildContext context) {
          return Scaffold(
            appBar: AppBar(
              title: Text("Battery"),
            ),
            body: Center(
              child: Column(
                children: <Widget>[
                  Text("Current battery information: $_result"),
                  RaisedButton(
                    child: Text("Get battery information"),
                    onPressed: getBatteryInfo,
                  )
                ],
              ),
            ),
          );
        }
      
        void getBatteryInfo() async {
          // Core code 2
          final int result = await platform.invokeMethod("getBatteryInfo");
          setState(() {
            _result = result;
          });
        }
      }

       

When we pass the platform Invokemethod: when calling the corresponding platform method, its operation needs to be implemented on the corresponding platform:

  • iOS can be implemented through Objective-C or Swift
  • Android can be implemented in Java or Kotlin

1.2.4. Writing iOS code

1.2.4.1. Swift code implementation

The operation steps related to the code are as follows:

  • 1. Get the fluterviewcontroller (which is the default controller of the application)
  • 2. Get methodchannel
    • Note: Here we need to get the information according to the name when we created it
  • 3. Listen for method calls (the incoming callback function will be called)
    • How to get information in iOS
    • If it is not obtained, an exception is returned to the fluent end
    • Callback the result to the fluent end through result
    • 3.1. Judge whether it is a call to getBatteryInfo, and tell the Flutter that the corresponding method is not implemented
    • 3.2. If the method of getBatteryInfo is called, the callback is implemented through another encapsulated method
    • import UIKit
      import Flutter
      
      @UIApplicationMain
      @objc class AppDelegate: FlutterAppDelegate {
        override func application(
          _ application: UIApplication,
          didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
        ) -> Bool {
          
          // 1.obtain FlutterViewController(Is the default for the application Controller)
          let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
          
          // 2.obtain MethodChannel(Method channel)
          let batteryChannel = FlutterMethodChannel(name: "coderwhy.com/battery",
                                                    binaryMessenger: controller.binaryMessenger)
          
          // 3.Listen for method calls(The incoming callback function is called)
          batteryChannel.setMethodCallHandler({
            [weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
            // 3.1.Judge whether it is getBatteryInfo Call of,inform Flutter The client does not implement the corresponding method
            guard call.method == "getBatteryInfo" else {
              result(FlutterMethodNotImplemented)
              return
            }
            // 3.2.If you call getBatteryInfo Method of, Then implement the callback through another encapsulated method
            self?.receiveBatteryLevel(result: result)
          })
          
          GeneratedPluginRegistrant.register(with: self)
          return super.application(application, didFinishLaunchingWithOptions: launchOptions)
        }
          
        private func receiveBatteryLevel(result: FlutterResult) {
          // 1.iOS How to get information in
          let device = UIDevice.current
          device.isBatteryMonitoringEnabled = true
          
          // 2.If not obtained,Then return to Flutter There is an exception at the end
          if device.batteryState == UIDevice.BatteryState.unknown {
            result(FlutterError(code: "UNAVAILABLE",
                                message: "Battery info unavailable",
                                details: nil))
          } else {
            // 3.adopt result Callback results to Flutter end
            result(Int(device.batteryLevel * 100))
          }
        }
      }

       

  • Not found 'shutter'
1,Clean up first
flutter clean

2,Loading plug-ins
flutter pub get

3,compile ios
flutter build ios

If the third step can't be solved, go straight to the following
4,hold flutter project->build->ios Drag the following two directories to ios->Flutter Under the directory
 Specific steps
as Right click to open build->ios->App.framework Directory, in the folder App.framework and Flutter.framework Drag to xcode of Flutter Directory and import

1.2.5. Writing Android code

1.2.5.1. Kotlin code implementation

The implementation idea is consistent with the above, but Kotlin is used to implement:

  • You can refer to the notes
    import androidx.annotation.NonNull
    import io.flutter.embedding.android.FlutterActivity
    import io.flutter.embedding.engine.FlutterEngine
    import io.flutter.plugin.common.MethodChannel
    
    import android.content.Context
    import android.content.ContextWrapper
    import android.content.Intent
    import android.content.IntentFilter
    import android.os.BatteryManager
    import android.os.Build.VERSION
    import android.os.Build.VERSION_CODES
    
    class MainActivity: FlutterActivity() {
        private val CHANNEL = "coderwhy.com/battery"
    
        override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
            // 1.establish MethodChannel object
            val methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) 
            
            // 2.Add callback to call method
            methodChannel.setMethodCallHandler {
                // Note: this method is invoked on the main thread.
                call, result ->
                // 2.1.If the method called is getBatteryInfo,Then perform normally
                if (call.method == "getBatteryInfo") {
                    // 2.1.1.Call another custom method to retrieve the information
                    val batteryLevel = getBatteryLevel()
                    
                    // 2.1.2. Judge whether it is obtained normally
                    if (batteryLevel != -1) {
                        // Get returned results
                        result.success(batteryLevel)
                    } else {
                        // Get throw exception not found
                        result.error("UNAVAILABLE", "Battery level not available.", null)
                    }
                } else {
                    // 2.2.If the method called is getBatteryInfo,Then perform normally
                    result.notImplemented()
                }
            }
        }
    
        private fun getBatteryLevel(): Int {
            val batteryLevel: Int
            if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
                val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
                batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
            } else {
                val intent = ContextWrapper(applicationContext).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
                batteryLevel = intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
            }
    
            return batteryLevel
        }
    }

     

II Embed original project

First of all, let's make it clear that the original intention of FLUENT is not to develop with other platforms, but to build a complete cross platform application.

However, in the actual development, it is unrealistic for the original project to completely use fluent for reconstruction. For the original project, we are more likely to adopt the method of mixed development.

2.1. Creating a Flutter module

For the original projects requiring mixed development, fluent can be inherited into the existing projects as a library or module.

  • The module is introduced into your Android or iOS application to render part of the UI or shared Dart code using fluent.
  • In fluent v1 12, the basic scenarios added to existing applications have been supported, and each application can integrate a full screen Flutter instance at the same time.

However, some scenarios are still limited:

  • Running multiple instances of Flutter or running Flutter locally on the screen may lead to unpredictable behavior;
  • The ability to use fluent in the background mode is still under development (not supported at present);
  • It is not supported to package the Flutter library into another sharable library or package multiple Flutter libraries into the same application;
  • The implementation of the application on the Android platform is based on the FlutterPlugin API. Some plug-ins that do not support FlutterPlugin may have unpredictable behavior.

Creating a shutter module

flutter create --template module my_flutter

After the module is created, it can be opened, developed and run through Android Studio or VSCode;

The directory structure is as follows:

  • Different from previous projects, iOS and Android projects are hidden files, and we usually don't open them separately to run;
  • Their function is to compile the fluent module and then inherit it into the existing project;
    my_flutter/
    ├── .ios/
    ├── .android/
    ├── lib/
    │   └── main.dart
    ├── test/
    └── pubspec.yaml

     

2.2. Embedded iOS project

There are many ways to embed into existing iOS projects:

  • You can use CocoaPods dependency management and the installed fluent SDK;
  • You can also manually compile the fluent engine, your dart code and all the fluent plugins into a framework, manually integrate them into your application with Xcode, and update the compilation settings;

At present, almost all iOS projects have been managed by cocoapods, so the first cocoapods method is recommended;

We build an iOS project that needs to be inherited in the following way:

1. For testing, we create a default iOS project here: just use Xcode to create it

2. Add the project to CocoaPods for management

  • CocoaPods needs to be installed on your computer

Initialize CocoaPods:

pod init

Dependencies for installing CocoaPods:

pod install

Compile Podfile file:

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

# Add the path where the module is located
flutter_application_path = '../my_flutter'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

target 'ios_my_test' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!
  
  # Installing the Flutter module
  install_all_flutter_pods(flutter_application_path)
  
  # Pods for ios_my_test

end

Re execute the dependency of installing CocoaPods:

pod install

2.2.1. Swift code

In order to display the Flutter page in existing iOS applications, you need to start the} Flutter Engine and} fluterviewcontroller.

It is generally recommended to preheat a "long-lived" FlutterEngine for our application:

  • We will create a FlutterEngine in the app delegate launched by the application and expose it to the outside world as an attribute
    import UIKit
    import FlutterPluginRegistrant
    
    @UIApplicationMain
    class AppDelegate: UIResponder, UIApplicationDelegate {
       // 1.Create a FlutterEngine object
        lazy var flutterEngine = FlutterEngine(name: "my flutter engine")
        
        func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
           // 2.start-up flutterEngine
            flutterEngine.run()
            return true
        }
    }

    In the started ViewController, create a UIButton, and click this Button to pop up the FlutterViewController

    import UIKit
    import Flutter
    
    class ViewController: UIViewController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
            
           // 1.Create a button
            let button = UIButton(type: UIButton.ButtonType.custom)
            button.addTarget(self, action: #selector(showFlutter), for: .touchUpInside)
            button.setTitle("Show Flutter", for: .normal)
            button.frame = CGRect(x: 80, y: 210, width: 160, height: 40)
            button.backgroundColor = UIColor.blue
            self.view.addSubview(button)
        }
        
        @objc func showFlutter() {
            // 2.establish FlutterViewController Object (need to get first) flutterEngine)
            let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEngine;
            let flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil);
            navigationController?.pushViewController(flutterViewController, animated: true);
        }
    }

    We can also omit the pre created FlutterEngine: This is not recommended because there may be a significant delay before the first pin image is rendered.

2.3. Embedded Android project

There are many ways to embed into existing Android projects:

  • Compile as AAR file (Android Archive)
    • Compile it into aar through fluent and add relevant dependencies
  • It depends on the source code of the module and is configured in gradle

Here we use the second method

1. Create an Android test project using Android Studio

2. Add related dependencies

Modify settings in Android project Gradle file:

include ':app'                                     // assumed existing content
setBinding(new Binding([gradle: this]))                                 // new
evaluate(new File(                                                      // new
  settingsDir.parentFile,                                               // new
  'my_flutter/.android/include_flutter.groovy'                          // new
))  

In addition, we need to build. 0 in the Android project Add dependencies to gradle:

dependencies {
  implementation project(':flutter')
}

When compiling the code, the following errors may occur:

  • This is because interface methods have only been supported since Java 8
  • The Flutter Android engine uses the new features of Java 8

 

 

Solution: by setting the build.exe of Android project The gradle configuration is compiled using java 8:

 compileOptions {
    sourceCompatibility 1.8
    targetCompatibility 1.8
  }

Next, let's try to add a screen of fluent to the Android application

Flutter provides a fluteractivity to show the flutter interface. In the Android application, we need to register the fluteractivity first:

  • At androidmanifest Register in XML
    <activity
      android:name="io.flutter.embedding.android.FlutterActivity"
      android:theme="@style/AppTheme"
      android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
      android:hardwareAccelerated="true"
      android:windowSoftInputMode="adjustResize"
      />

     

2.3.1. Java code

package com.coderwhy.testandroid;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import io.flutter.embedding.android.FlutterActivity;

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
//        setContentView(R.layout.activity_main);
        startActivity(
            FlutterActivity.createDefaultIntent(this)
        );
    }
}
You can also pass in the default route when creating
package com.coderwhy.testandroid;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import io.flutter.embedding.android.FlutterActivity;

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
//        setContentView(R.layout.activity_main);
        startActivity(
          FlutterActivity
          .withNewEngine()
          .initialRoute("/my_route")
          .build(currentActivity)
        );
    }
}

2.3.2. Kotlin code

package com.coderwhy.test_demo_a_k

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import io.flutter.embedding.android.FlutterActivity

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
//        setContentView(R.layout.activity_main)
        startActivity(
            FlutterActivity.createDefaultIntent(this)
        )
    }
}

You can also specify a route at creation time

package com.coderwhy.test_demo_a_k

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import io.flutter.embedding.android.FlutterActivity

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
//        setContentView(R.layout.activity_main)
        startActivity(
            FlutterActivity
                .withNewEngine()
                .initialRoute("/my_route")
                .build(this)
        );
    }
}

III Flutter module commissioning

Once you inherit the fluent module into your project and run the fluent engine or UI using the API of the fluent platform, you can build your own Android or iOS project like ordinary Android or iOS

However, Flutter has a great advantage in its rapid development, that is, hot reload.

How can we use hot reload to speed up our debugging speed corresponding to the fluent module?

  • You can use the shuttle attach
    # --app-id Which application is specified
    # -d Yes, specify which device to connect
    flutter attach --app-id com.coderwhy.ios-my-test -d 3D7A877C-B0DD-4871-8D6E-0C5263B986CD

     

 

Topics: Flutter