Android Flutter hybrid development of high imitation large factory App

From the first part Summary of App and tips accumulation of flutter's 10 day high imitation large factory This time, we are full of dry goods.

This article will outline the building of Android component architecture and how to mix the development of Flutter and Android (only the first page of the whole App is completed with native Android, and other pages are introduced with the previous Flutter pages). The main host program is built by Android, and the component architecture is used to build the whole App. Different businesses correspond to different modules Projects and businesses adopt the form of interface communication (ARouter), which is mixed in the form of module into the Flutter, and data communication is carried out through the method channel and the Flutter side, and these functions realize the source code open source, and interested partners can move to GitHub.

The following blog will be divided into four parts:

  • Function preview of project completion
  • Analysis of project component structure
  • Detailed overview of project functions (knowledge points used)
  • Android Flutter hybrid development

Function preview of project completion

First of all, we will use a video to quickly preview the functions and running effects of the project, as follows

You can Click to watch the video (click gear - > more playback settings to hide the black edge)

After watching the video, in fact, most of the functions are the same as those before Pure shuttle project The functions are the same, but the home page has added 4 tab s, and Ctrip's second floor and layout have changed.

You can also scan and install the experience:

Analysis of project component structure

Preview of project structure chart

Secondly, analyze and sort out the project structure. The structure of the project is roughly as shown in the figure, and some details are not shown in the figure:

Project structure analysis

Business Engineering

Separate specific and independent businesses into separate module s to reduce the maintenance pressure of the project

  • ft_home: the homepage module, which can be divided into four tab s (featured, nearby, scenic spot and delicious food) can be divided into modules. I haven't split it here for the time being, and it will be completed later
  • ft_destination: the destination module has not been set up, because it directly introduces the previously completed flitter page
  • ft_travel: travel photography module also uses the shuttle page
  • Boutter: the boutter module. This module is from boutter_ Automatically generated in module

Foundation base project

Encapsulate specific functions into independent libraries for business modules to reduce maintenance costs and code coupling

  • lib_network: network library, using okhttp Second encapsulation of plug-in, simple call of business layer
  • lib_webview: open the webview Library of the web page, and use the agentweb Second encapsulation of plug-ins, the business layer only needs one code to complete the jump of web pages
  • lib_image_loader: image loading library, used glide The second encapsulation of plug-ins enables the business layer to load pictures with different parameters in one sentence of code
  • lib_asr: Baidu AI voice library, integrated with Android for the use of the Fletter
  • lib_common_ui: public UI library, centralized management of reused pages
  • lib_base: basic library, through ARouter The service function exposes the interface to provide services to the business layer, which can also expose the interface for external use

Some of the plug-ins used here are not reflected in the project structure diagram (structure diagram space is limited).

plug-in unit

Here, the plug-ins used in the project are listed for your reference:

  • magicindicator Powerful, customizable and easy to expand ViewPager indicator framework, the four tab s (select, nearby, scenic spot, food) of the home page are realized by this.
  • immersionbar One line of code easily realizes immersive management of status bar and navigation bar
  • pagerBottomTabStrip The navigation bar at the bottom and side of the page, home page, destination, travel photo and my page switching are realized by this.
  • rxjava/rxandroid Asynchronous and chained programming
  • butterknife View injection plug-in, used with Android plug-in, can quickly and automatically generate init view code without writing a findViewById code.
  • gson json parsing, used with Android plug-ins, can quickly generate entity classes
  • smartRefreshLayout Intelligent pull-down refresh framework, Ctrip second floor and pull-down refresh load are more realized with this
  • eventbus Publish / subscribe event bus, elegant communication between components
  • arouter Rely on injection, route jump and register service to complete communication between modules gracefully
  • okhttp Network request plug in
  • agentweb webview framework, simple secondary encapsulation, elegant web page Jump
  • glide High performance and extensible image loading plug-in
  • banner Picture carousel control

Basically, there should be no missing. For the detailed use of plug-ins, please go to the GitHub homepage of each plug-in.

Here, the plug-in code of my project and the gradle code of version management are pasted as follows:

Plug in import Code:

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

    implementation rootProject.depsLibs.appcompat
    implementation rootProject.depsLibs.legacy
    implementation rootProject.depsLibs.recyclerview
    implementation rootProject.depsLibs.constraintlayout
    implementation rootProject.depsLibs.cardview

    //tab indicator
    implementation rootProject.depsLibs.magicindicator
    //immersive 
    implementation rootProject.depsLibs.immersionbar
    //navigation bar
    implementation rootProject.depsLibs.pagerBottomTabStrip
    //rxjava
    implementation rootProject.depsLibs.rxjava
    //rxandroid
    implementation rootProject.depsLibs.rxandroid
    //view injection
    implementation rootProject.depsLibs.butterknife
    //view injection
    annotationProcessor rootProject.depsLibs.butterknifeCompiler
    //gson
    implementation rootProject.depsLibs.gson
    //banner
    implementation rootProject.depsLibs.banner
    //smartRefreshLayout up and down refresh
    implementation rootProject.depsLibs.smartRefreshLayout
    implementation rootProject.depsLibs.refreshHeader
    implementation rootProject.depsLibs.refreshHeaderTwoLevel
    implementation rootProject.depsLibs.refreshFooter
    //eventbus
    implementation rootProject.depsLibs.eventbus
    //arouter Library
    implementation(rootProject.depsLibs.arouterapi) {
        exclude group: 'com.android.support'
    }
    annotationProcessor rootProject.depsLibs.aroutercompiler

    //Introduce home module
    implementation project(':ft_home')
    //Import image loading Library
    implementation project(':lib_image_loader')
    //Introduce network library
    implementation project(':lib_network')
    //webview
    implementation project(':lib_webview')
    //Introduce basic ui Library
    implementation project(':lib_common_ui')
    //base library
    implementation project(':lib_base')
    //Introduction of fluent module
    implementation project(':flutter')
    //Introduction of Baidu AI voice library
    implementation project(':lib_asr')
}

Version management code (unified management version No.):

ext {
    android = [
            compileSdkVersion: 29,
            buildToolsVersion: "29.0.0",
            minSdkVersion    : 19,
            targetSdkVersion : 29,
            applicationId    : 'net.lishaoy.android_ctrip',
            versionCode      : 1,
            versionName      : '1.0',
            multiDexEnabled  : true,
    ]

    depsVersion = [
            appcompat            : '1.1.0',
            legacy               : '1.0.0',
            recyclerview         : '1.0.0',
            constraintlayout     : '1.1.3',
            cardview             : '1.0.0',
            magicindicator       : '1.5.0',
            immersionbar         : '3.0.0',
            pagerBottomTabStrip  : '2.3.0X',
            glide                : '4.11.0',
            glidecompiler        : '4.11.0',
            butterknife          : '10.2.1',
            butterknifeCompiler  : '10.2.1',
            rxjava               : '3.0.0',
            rxandroid            : '3.0.0',
            okhttp               : '4.7.2',
            okhttpLogging        : '4.7.2',
            gson                 : '2.8.6',
            banner               : '2.0.10',
            smartRefreshLayout   : '2.0.1',
            refreshHeader        : '2.0.1',
            refreshFooter        : '2.0.1',
            refreshHeaderTwoLevel: '2.0.1',
            eventbus             : '3.2.0',
            agentweb             : '4.1.3',
            arouterapi           : '1.5.0',
            aroutercompiler      : '1.2.2',

    ]

    depsLibs = [
            appcompat            : "androidx.appcompat:appcompat:${depsVersion.appcompat}",
            legacy               : "androidx.legacy:legacy-support-v4:${depsVersion.legacy}",
            recyclerview         : "androidx.recyclerview:recyclerview:${depsVersion.recyclerview}",
            constraintlayout     : "androidx.constraintlayout:constraintlayout:${depsVersion.constraintlayout}",
            cardview             : "androidx.cardview:cardview:${depsVersion.cardview}",
            magicindicator       : "com.github.hackware1993:MagicIndicator:${depsVersion.magicindicator}",
            immersionbar         : "com.gyf.immersionbar:immersionbar:${depsVersion.immersionbar}",
            pagerBottomTabStrip  : "me.majiajie:pager-bottom-tab-strip:${depsVersion.pagerBottomTabStrip}",
            glide                : "com.github.bumptech.glide:glide:${depsVersion.glide}",
            glidecompiler        : "com.github.bumptech.glide:compiler:${depsVersion.glidecompiler}",
            butterknife          : "com.jakewharton:butterknife:${depsVersion.butterknife}",
            butterknifeCompiler  : "com.jakewharton:butterknife-compiler:${depsVersion.butterknifeCompiler}",
            rxjava               : "io.reactivex.rxjava3:rxjava:${depsVersion.rxjava}",
            rxandroid            : "io.reactivex.rxjava3:rxandroid:${depsVersion.rxandroid}",
            okhttp               : "com.squareup.okhttp3:okhttp:${depsVersion.okhttp}",
            okhttpLogging        : "com.squareup.okhttp3:logging-interceptor:${depsVersion.okhttpLogging}",
            gson                 : "com.google.code.gson:gson:${depsVersion.gson}",
            banner               : "com.youth.banner:banner:${depsVersion.banner}",
            smartRefreshLayout   : "com.scwang.smart:refresh-layout-kernel:${depsVersion.smartRefreshLayout}",
            refreshHeader        : "com.scwang.smart:refresh-header-classics:${depsVersion.refreshHeader}",
            refreshHeaderTwoLevel: "com.scwang.smart:refresh-header-two-level:${depsVersion.refreshHeader}",
            refreshFooter        : "com.scwang.smart:refresh-footer-classics:${depsVersion.refreshFooter}",
            eventbus             : "org.greenrobot:eventbus:${depsVersion.eventbus}",
            agentweb             : "com.just.agentweb:agentweb:${depsVersion.agentweb}",
            arouterapi           : "com.alibaba:arouter-api:${depsVersion.arouterapi}",
            aroutercompiler      : "com.alibaba:arouter-compiler:${depsVersion.aroutercompiler}",
    ]
}

Detailed overview of project functions (knowledge points used)

Here is an overview of the functions and knowledge points of the home page. As other pages introduce the previous Flutter page, the specific functions are as follows: Summary of App and tips accumulation of flutter's 10 day high imitation large factory It has been introduced, and will not be elaborated here.

The home page focuses on the realization of the following functions:

  • Pull down to refresh the second floor of Ctrip
  • Search appBar
  • Gradient grid navigation
  • banner components
  • Multi state tab indicator (scrolling fixed top)

Pull down to refresh the second floor of Ctrip

First, look at the specific renderings, as shown in the figure:

Pull down refresh and Ctrip second floor are used smartRefreshLayout The implementation code of the plug-in is as follows:

private void initRefreshMore() {
    homeHeader.setRefreshHeader(new ClassicsHeader(getContext()), -1, (int) Utils.dp2px(76)); //Set the height of pull-down refresh and second floor header
    homeHeader.setFloorRate(1.6f); //Set the second floor trigger ratio
    homeRefreshContainer.setPrimaryColorsId(R.color.colorPrimary, R.color.white); //Set dropdown refresh and second floor prompt text color
    homeRefreshContainer.setOnMultiListener(new SimpleMultiListener() {
        @Override
        public void onLoadMore(@NonNull RefreshLayout refreshLayout) {
            loadMore(refreshLayout); //Load more
        }

        @Override
        public void onRefresh(@NonNull RefreshLayout refreshLayout) {
            refreshLayout.finishRefresh(1600); //Set dropdown refresh delay
        }

        @Override
        public void onHeaderMoving(RefreshHeader header, boolean isDragging, float percent, int offset, int headerHeight, int maxDragHeight) {
            homeSecondFloorImg.setVisibility(View.VISIBLE);  //Hide second floor background
            homeSearchBarContainer.setAlpha(1 - Math.min(percent, 1)); //Change searchBar transparency
        }

        @Override
        public void onStateChanged(@NonNull RefreshLayout refreshLayout, @NonNull RefreshState oldState, @NonNull RefreshState newState) {
            if (oldState == RefreshState.ReleaseToTwoLevel) {  //Going to the second floor
                homeSecondFloorImg.setVisibility(View.GONE);
                homeHeaderContent.animate().alpha(1).setDuration(666);
            } else if (newState == RefreshState.PullDownCanceled) { //Drop down cancel status processing
                homeHeaderContent.animate().alpha(0).setDuration(666);
            } else if (newState == RefreshState.Refreshing) { //Refreshing state processing
                homeHeaderContent.animate().alpha(0).setDuration(666);
            } else if (oldState == RefreshState.TwoLevelReleased) { // Ready to go to the second floor to complete the status processing, here open webview
                WebViewImpl.getInstance().gotoWebView("https://m.ctrip.com/webapp/you/tsnap/secondFloorIndex.html?isHideNavBar=YES&s_guid=feb780be-c55a-4f92-a6cd-2d81e04d3241", true);
                homeHeader.finishTwoLevel();
            } else if (oldState == RefreshState.TwoLevel) { //Arrive at the second floor for status processing
                homeCustomScrollView.setVisibility(View.GONE);
                homeHeaderContent.animate().alpha(0).setDuration(666);
            } else if (oldState == RefreshState.TwoLevelFinish) { //Second floor completion status processing
                homeCustomScrollView.setVisibility(View.VISIBLE);
                homeCustomScrollView.animate().alpha(1).setDuration(666);
            }
        }

    });

}

The XML page layout file code is as follows:

<com.scwang.smart.refresh.layout.SmartRefreshLayout
    android:id="@+id/home_refresh_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipChildren="false"
    app:srlAccentColor="@color/colorPrimary"
    app:srlPrimaryColor="@color/colorPrimary">
    <com.scwang.smart.refresh.header.TwoLevelHeader
        android:id="@+id/home_header"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="top">

        <ImageView
            android:id="@+id/home_second_floor_img"
            android:layout_width="match_parent"
            android:layout_height="460dp"
            android:layout_alignTop="@+id/home_header"
            android:scaleType="fitXY"
            android:src="@drawable/second_floor"
            android:visibility="gone"/>
        <FrameLayout
            android:id="@+id/home_header_content"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:alpha="0">
            <ImageView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="fitXY"
                android:src="@drawable/second_floor" />
        </FrameLayout>

    </com.scwang.smart.refresh.header.TwoLevelHeader>
    
    ...

    <com.scwang.smart.refresh.footer.ClassicsFooter
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</com.scwang.smart.refresh.layout.SmartRefreshLayout>

Detailed implementation details can be moved GitHub Check the source code.

Search appBar

The scrolling placeholder text for the search bar uses the banner For plug-in implementation, click the search box to jump to the search page (the search page written by flitter), and then you can bring the placeholder text to the flitter search page.

The effect is as follows:

The implementation code of the scrolling placeholder text is as follows (the implementation of the search box is no longer shown here, but some XML layout codes):

        homeSearchBarPlaceholder
                .setAdapter(new HomeSearchBarPlaceHolderAdapter(homeData.getSearchPlaceHolderList())) // Set adapter
                .setOrientation(Banner.VERTICAL) // Set scrolling direction
                .setDelayTime(3600) // Set interval
                .setOnBannerListener(new OnBannerListener() {
                    @Override
                    public void OnBannerClick(Object data, int position) {  //Click to open the shuttle search page
                        ARouter.getInstance()
                                .build("/home/search")
                                .withString("placeHolder", ((Home.SearchPlaceHolderListBean) data).getText())
                                .navigation();
                    }
                });
    }

The specific functions of searchBar are not elaborated, which is consistent with the previous projects.

Gradient grid navigation

Gradient grid navigation is basically some XML page layout code, but I encapsulated it as a separate component, as shown in the figure

After encapsulation, the introduction is very simple. The code is as follows:

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:background="@color/white">

    <!-- Grid navigation -->
    <net.lishaoy.ft_home.GridNavView
        android:id="@+id/home_grid_nav_container"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    ...

</LinearLayout>

Detailed implementation details can be moved GitHub Check the source code.

banner components

banner components are also used banner Implemented by plug-in, as shown in the figure

The implementation code is as follows:

private void initBanner() {
    homeBanner.addBannerLifecycleObserver(this)
            .setAdapter(new HomeBannerAdapter(homeData.getBannerList())) //Set adapter
            .setIndicator(new EllipseIndicator(getContext()))           //Set the indicator, as shown in the figure, which is not provided in my customized plug-in
            .setIndicatorSelectedColorRes(R.color.white)                //Set indicator color
            .setIndicatorSpace((int) BannerUtils.dp2px(10))             //Set spacing
            .setBannerRound(BannerUtils.dp2px(6));                      //Set fillet

}

Multi state tab indicator

The implementation of multi state tab indicator needs to pay attention to many details, because it embeds viewPaper in the ScrollView of the fragment on the home page. First, you will find the problem that viewPaper does not display, and second, the problem that scrolling is not smooth. My solutions to these two problems are:

  • Problems with viewPaper not displaying: overriding the onMeasure method with a custom ViewPager and recalculating the height
  • The problem of rolling is not smooth: using the custom ScrollView, rewrite computeScroll and onScrollChanged to get the rolling distance again

The realization effect is as shown in the figure:

Too many codes for this function are inconvenient to display here. For details, please move GitHub Check the source code.

Android Flutter hybrid development

Only the home page of this project is implemented by Android, and other pages are implemented by Flutter Pure shuttle project.

The following steps are required for Android to introduce Flutter for hybrid development

  • Create a fluent module
  • Write fluent code (create fluent route)
  • Communication between flitter and android

The following is an overview of how these parts are implemented.

Create a fluent module

This should not be described too much. For basic operations, you will see File > New > new module as shown in the figure below:

After the creation, android studio will automatically generate the configuration code into the gradle configuration file, and generate a library module of the fluent.

Tips:
When creating a new one, it is better to put the flitter module and android project in the same directory;
The new version of android studio will automatically generate the gradle configuration code. It seems that the old version needs to be manually configured

For example, there is no gradle configuration code generated, you need to settings.gradle Manually add the following configuration to the file:

setBinding(new Binding([gradle: this]))
evaluate(new File(
  settingsDir, //Set the root path, and configure it according to the path of the specific fluent module
  'flutter_module/.android/include_flutter.groovy'
))

include ':flutter_module'

It also needs to be in the host project (app if it's not renamed) build.gradle Introduce the fluent as follows:

dependencies {
    ...
    //Introduction of fluent module
    implementation project(':flutter')
    ...
}

Write fluent code

Write the fluent code. In the fluent module, write the fluent code according to the normal fluent development process. (the code of the fluent in my project is written in the previous project. Copy it, change the introduction of the package, and then it can be run. )

It should be noted here that there is only one entry for flutter, which is the main() function. We need to deal with the jump problem of flutter page here.

On the android side, create the flitter page with the following code:

    Flutter.createView(getActivity(),getLifecycle(),"destination");

Flutter.createView Three parameters, activity, lifecycle and route, are required. This route is to be passed to the flitter end. Of course, it is of String type. We can freely pass ordinary strings or json strings.

We can also create fluent pages in other ways, such as: Flutter.createFragment() , FlutterActivity.withNewEngine(), FlutterFragment.createDefault() etc.

For specific use, you can go to Official documentation of Flutter Check.

Then, how to receive the route parameter from the router is through window.defaultRouteName In this project, the routing code of the managed router is as follows:

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter model',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        fontFamily: 'PingFang',
      ),
      home: _widgetRoute(window.defaultRouteName), // Through window.defaultRouteName  Receive parameters from android
    );
  }
}

Widget _widgetRoute(String defaultRouteName) {
    Map<String, dynamic> params = convert.jsonDecode(defaultRouteName); //Analytic parameters
    defaultRouteName = params['routeName'];
    placeHolder = params['placeHolder'];

    switch (defaultRouteName) { // Return the corresponding page according to the parameters
        ...
        case 'destination/search':
            return DestinationSearchPage(
                hideLeft: false,
        );
        ...
        default:
            return Center(
                child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                    Text('not found $defaultRouteName',
                        textDirection: TextDirection.ltr),
                ],
                ),
            );
    }
}

In fact, there is another way to receive this route parameter on the fluent side, that is, through onGenerateRoute, which is a method in MaterialApp.

The code is as follows:

onGenerateRoute: (settings){ //Through settings.name  Get parameters from android
    return _widgetRoute(settings.name);
},

Communication between flitter and android

flutter can call android's methods and how to transfer data to each other. flutter officially provides three methods to implement, which are:

  • EventChannel: one way continuous communication, such as network changes, sensors, etc.
  • Method channel: one time communication, generally applicable to the call of methods.
  • BasicMessageChannel: continuous two-way communication.

In this project, MethodChannel method is used for communication, such as: the flutter calls the AI intelligent voice method of android and the flutter opens the android page with MethodChannel.

The AI intelligent voice method code of the android terminal called by the fluent terminal is as follows:

class AsrManager {
  static const MethodChannel _channel = const MethodChannel('lib_asr');
  //Start recording
  static Future<String> start({Map params}) async {
    return await _channel.invokeMethod('start', params ?? {});
  }
  //Stop recording
    ...
  //cancel recording
    ...
  //Destruction
    ...
}

The code of opening android page by FLUENT is as follows:

class MethodChannelPlugin {

  static const MethodChannel methodChannel = MethodChannel('MethodChannelPlugin');

  static Future<void> gotoDestinationSearchPage() async {
    try {
      await methodChannel.invokeMethod('gotoDestinationSearchPage'); //gotoDestinationSearchPage parameter will be passed to android side
    } on PlatformException {
      print('Failed go to gotoDestinationSearchPage');
    }
  }
    ...
}

android is also received through MethodChannel. The specific implementation code is as follows:

public class MethodChannelPlugin implements MethodChannel.MethodCallHandler {

    private static MethodChannel methodChannel;
    private Activity activity;

    private MethodChannelPlugin(Activity activity) {
        this.activity = activity;
    }

    //The caller registers the fluent page through registerWith
    public static void registerWith(FlutterView flutterView) {
        methodChannel = new MethodChannel(flutterView, "MethodChannelPlugin");
        MethodChannelPlugin instance = new MethodChannelPlugin((Activity) flutterView.getContext());
        methodChannel.setMethodCallHandler(instance);
    }

    @Override
    public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
        if (methodCall.method.equals("gotoDestinationSearchPage")) { // Receive the message for specific operation
            EventBus.getDefault().post(new GotoDestinationSearchPageEvent());
            result.success(200);
        } 
        ...
        else {
            result.notImplemented();
        }
    }
}

These three steps are the basic steps of Android flitter hybrid development. For other details and specific processes, please refer to GitHub Project source code.

Finally, attach the project address and blog address:

Project address: https://github.com/persilee/android_ctrip
Blog address: https://h.lishaoy.net/androidctrip

Keywords: Android github ButterKnife Gradle

Added by prashanth on Fri, 19 Jun 2020 06:12:56 +0300