Recently, I have been working on Flutter of the company’s new project, mainly responsible for writing part of Flutter pages and bridging with native Android. Due to the shortage of staff, the main integration work was entrusted to the students of the platform group. The company platform Group provides a complete set of integrated toolchains, development tools, MVVM structures and a series of wheels, right out of the box. For a long time, only stay in the use of the level, few in-depth study, or need to see more.

This time integrate Flutter for old projects and rewrite the post details page using Flutter. To experience the construction and development of hybrid mode provided by the official.

The old native page that needs to be rewritten this time is:

The rewritten page for Flutter is:

Project address: github.com/stevenwsg/X…

Well, without further ado, let’s get started.

1. Old projects integrate WITH Flutter

1.1 Flutter mixed development mode

Flutter hybrid development mode generally has two ways:

1. The native project is a subproject of the Flutter project. The default user of Flutter creates the Android and iOS project directories, which can be used for native client development.

Create a Flutter Module as a dependency and add it to an existing native project.

The second approach is more decoupled than the first approach, especially for existing projects with lower renovation costs.

1.2 Creating a Flutter Module

Create a Flutter Module using As

Select File->New->New Flutter Project in As and select Flutter Module to create a Flutter Module subproject As follows:

1.3 Two ways to add Flutter

There are two ways to add Flutter to a native project

  1. Aar integration into existing Android projects
  2. Integrate into existing Android projects as Flutet Modules

In the second way, the Flutter Module is integrated into an existing Android project and mixed compiled so that the Flutter thermal update can be used.

In Jenkins automatic packaging, the first method is adopted. The Flutter project is converted into an AAR product, and the Android APK file is compiled with the aar product generated.

Integrate into existing Android projects as Flutet Modules:

Configure the flutter module in the setting.gradle file as follows:

Include ':app', ':easeui' : setBinding(new Binding([gradle: this])) evaluate(new File(settingsDir, '.. /flutter_bbs/.android/include_flutter.groovy' ))Copy the code

Then add the dependencies of the flutter Module to the build.gradle file as follows:

dependencies {
  implementation project(':flutter')
}
Copy the code

After the build has been completed, the project has become a mixed build of the native project and the Flutter project.

1.4 Adding a Page

This allows the jump from the native interface to the Flutter interface

Modify the Flutter entry file

import 'package:flutter/material.dart'; import 'package:flutter_bbs/post_deatil/view/post_deatil_page.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: PostDetailPage(), ); }}Copy the code

Display the Community details page for Flutter at this time

import 'package:flutter/material.dart'; class PostDetailPage extends StatefulWidget { @override State<StatefulWidget> createState() { return PostDetailState(); } } class PostDetailState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(" 信 息 "),), body: Center(child: Text(" 信 息 "), style: TextStyle(fontSize: 20, color: Colors.blueAccent), ), ), ); }}Copy the code

Create an Activity in the native project that inherits FlutterActivity and declares it in the androidmanifest.xml file:

class MomentDetailActivity : FlutterActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }
}
Copy the code
<activity android:name=".flutter.MomentDetailActivity" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|dens ity|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize"> </activity>Copy the code

How do I start this Activity?

startActivity(new Intent(getActivity(), MomentDetailActivity.class));
Copy the code

The effect is as follows:

2, rewrite the post details page

The page rewritten using Flutter is the post details page

As you can see, the entire page can be done with a single ListView, which contains multiple types. Post details, split lines, comments, comments, etc

2.1 Integrate the Bmob Flutter warehouse

Since the original project used Bmob cloud to provide data services, the Flutter project also needed to integrate Bmob warehouse to achieve data access and access addresses

Added dependencies to the Pubspec. yaml file of the Flutter project

Dependencies: data_plugin: ^ 0.0.16Copy the code

Enter the following command on the terminal to install:

 flutter packages get
Copy the code

Perform the following initialization operations in runApp:

/ * * * the encryption initialization. * / Bmob init (" https://api2.bmob.cn ", "appId", "apiKey");Copy the code

2.2 The native project page passes the post Id to the Flutter page

In native projects, the way to jump to a Flutter page was changed to:

val intent = Intent(context, MomentDetailActivity::class.java) intent.action = Intent.ACTION_RUN intent.putExtra( "route", "moment? noteId = ${note.objectId}" ) context? .startActivity(intent)Copy the code

In the Flutter project, receive the parameters passed:

    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        routes: {
          Routes.MOMENT: (BuildContext context) => PostDetailPage(null),
        },
        onGenerateRoute: (settings) {
          Uri uri = Uri.parse(settings.name);
          Map<String, String> params = uri.queryParameters;
          return MaterialPageRoute(
              builder: (context) => PostDetailPage(params));
        });
Copy the code

Now the Flutter post details page gets the post Id

2.3 Flutter obtains post information based on the post Id

2.3.1 Data pull

Create a network information class

import 'package:data_plugin/bmob/bmob_query.dart'; import 'package:data_plugin/utils/dialog_util.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_bbs/post_deatil/model/bean/note.dart'; class NetWorkRepo { static Note getNoteInfo(BuildContext context, String noteId) { BmobQuery<Note> query = BmobQuery(); query .queryObject(noteId) .then((value) => {showSuccess(context, value.toString())}); }}Copy the code

Pull when the PostDetailPage is initialized

class PostDetailState extends State<PostDetailPage> { String _noteId; @override void initState() { super.initState(); _noteId = widget._map["noteId"] as String; _initData(); } void _initData() {NetWorkRepo. GetNoteInfo (context, _noteId); }... }Copy the code

The result of the pull is zero

2.3.2 Json parsing

Deserialize Json data into bean entities.

The Json2Dart plugin is used here (I think the jSON_serializable library is difficult to use and has many holes).

The generated code is:

import 'package:data_plugin/bmob/table/bmob_object.dart'; class Note extends BmobObject { String content; String createdAt; String objectId; int replaycount; String title; int top; String typeid; String updatedAt; String userid; int zancount; Note.fromJsonMap(Map<String, dynamic> map) : content = map["content"], createdAt = map["createdAt"], objectId = map["objectId"], replaycount = map["replaycount"], title = map["title"], top = map["top"], typeid = map["typeid"], updatedAt = map["updatedAt"], userid = map["userid"], zancount = map["zancount"]; Map<String, dynamic> toJson() { final Map<String, dynamic> data = new Map<String, dynamic>(); data['content'] = content; data['createdAt'] = createdAt; data['objectId'] = objectId; data['replaycount'] = replaycount; data['title'] = title; data['top'] = top; data['typeid'] = typeid; data['updatedAt'] = updatedAt; data['userid'] = userid; data['zancount'] = zancount; return data; } @override Map getParams() { toJson(); } @override String toString() { return 'Note{content: $content, createdAt: $createdAt, objectId: $objectId, replaycount: $replaycount, title: $title, top: $top, typeid: $typeid, updatedAt: $updatedAt, userid: $userid, zancount: $zancount}'; }}Copy the code

At this point, the Json data has been converted to Bean entities:

2.3.3 the UI display

Next, the post is physically displayed on the UI

Dart: Post_deatil_page. Dart: post_deatil_page. Dart: Post_deatil_page

@override Widget build(BuildContext context) {return Scaffold(appBar: appBar (title: Text(" post details "),), body: ListView.builder( itemCount: items.length, itemBuilder: (context, index) { return _buildListViewCell( items[index]); // Build different widgets to populate the ListView based on the data},); } Widget _buildListViewCell(Object Object) {if (Object is Note) {return MomentDetailWidget(Object); // Return post details Widget}}Copy the code

Post details MomentDetailWidget

import 'package:flutter/material.dart'; import 'package:flutter_bbs/post_deatil/model/bean/note.dart'; class MomentDetailWidget extends StatelessWidget { final Note note; MomentDetailWidget(this.note); @override Widget build(BuildContext context) { return Container( margin: EdgeInsets.only(left: 20, top: 10, bottom: 10, right: 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ _buildHeaderWidget(), _buildContentWidget(), _buildIconWidget(), _buildReplayWidget(), ], ), ); } Widget _buildHeaderWidget() { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( margin: EdgeInsets.only(right: 10), child: ClipOval( child: Image.asset( "images/logo.webp", width: 80, height: 80, ), ), ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( note.title ?? "", style: TextStyle(color: Colors.black54, fontSize: 20), maxLines: 1, overflow: TextOverflow.ellipsis, ), Container( margin: EdgeInsets.only(top: 20), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( note.typeid ?? "", style: TextStyle(color: Colors.black45, fontSize: 16), ), Expanded(child: Container()), Text( note.updatedAt?.substring(0, 10) ?? "", style: TextStyle(color: Colors.black45, fontSize: 16), ) ], ), ), ], )), ], ); } Widget _buildContentWidget() { return Container( margin: EdgeInsets.only(top: 10), child: Expanded( child: Text( note.content ?? "", style: TextStyle(color: Colors.black54, fontSize: 16), ), ), ); } Widget _buildIconWidget() { return Container( margin: EdgeInsets.only(top: 20), child: Flex( direction: Axis.horizontal, mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Row( children: [ Image.asset( "images/zan.webp", width: 20, height: 20, ), Container( margin: EdgeInsets.only(left: 5), child: Text( note.zancount?.toString() ?? "", style: TextStyle(fontSize: 14), ), ) ], ), Row( children: [ Image.asset( "images/replay.webp", width: 20, height: 20, ), Container( margin: EdgeInsets.only(left: 5), child: Text( note.replaycount?.toString() ?? "", style: TextStyle(fontSize: 14), ), ) ], ) ], ), ); } Widget _buildReplayWidget() { return Container( margin: EdgeInsets.only( top: 20, ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: TextField( decoration: InputDecoration( hintText: HintStyle: TextStyle(fontFamily: 'MaterialIcons', fontSize: 16), contentPadding: EdgeInsets. Only (top: 8, bottom: 8), border: OutlineInputBorder( borderSide: BorderSide.none, borderRadius: BorderRadius.all(Radius.circular(5)), ), filled: true, ), )), Container( margin: EdgeInsets.only(left: 20), child: OutlinedButton (onPressed: () {}, child, Text (" comment "),),),),); }}Copy the code

The results are as follows:

2.4 Flutter obtains comment information based on the post Id

2.4.1 Data Acquisition

Static List<Comment> getCommentInfo(BuildContext context, String noteId) { BmobQuery<Comment> query = BmobQuery(); query.addWhereEqualTo("noteid", noteId); query.queryObjects().then((value) { List<Comment> list = List(); value.forEach((element) { list.add(Comment.fromJsonMap(element)); }); print(list.toString()); }).catchError((e) { showError(context, BmobError.convert(e).error); }); }Copy the code

The request result is:

2.4.2 Json parsing

import 'package:data_plugin/bmob/table/bmob_object.dart'; class Comment extends BmobObject { String content; String createdAt; String noteid; String objectId; String updatedAt; String userid; String username; Comment.fromJsonMap(Map<String, dynamic> map) : content = map["content"], createdAt = map["createdAt"], noteid = map["noteid"], objectId = map["objectId"], updatedAt = map["updatedAt"], userid = map["userid"], username = map["username"]; Map<String, dynamic> toJson() { final Map<String, dynamic> data = new Map<String, dynamic>(); data['content'] = content; data['createdAt'] = createdAt; data['noteid'] = noteid; data['objectId'] = objectId; data['updatedAt'] = updatedAt; data['userid'] = userid; data['username'] = username; return data; } @override Map getParams() { toJson(); } @override String toString() { return 'Comment{content: $content, createdAt: $createdAt, noteid: $noteid, objectId: $objectId, updatedAt: $updatedAt, userid: $userid, username: $username}'; }}Copy the code

The analytical results are:

2.4.3 UI display

The details page of the ListView is transformed into a multi-type ListView, can display posts, lines, comments and other content

@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( leading: BackButton(onPressed: () {}), title: Text(" itemCount: items.length "), centerTitle: true,), Body: ListView.Builder (itemCount: items.length, itemBuilder: (context, index) { return _buildListViewCell( items[index]); // Build different widgets to populate the ListView based on the data},); } Widget _buildListViewCell(Object object) { if (object is Note) { return MomentDetailWidget(object); } else if (object is Comment) {return CommentDetailWidget(object); } else if (object is DividerBean) {return DividerWidget(); } else if (object is CommentEmptyBean) {return CommentEmptyWidget(); Else if (object is CommentTitleBean) {return CommentTitleWidget(object.mentnum);} else if (object is CommentTitleBean) {return CommentTitleWidget(object.mentnum); } else {return Container(); Container}}Copy the code

Comment on the Widget

import 'package:flutter/material.dart'; import 'package:flutter_bbs/post_deatil/model/bean/comment.dart'; class CommentDetailWidget extends StatelessWidget { final Comment comment; CommentDetailWidget(this.comment); @override Widget build(BuildContext context) { return Container( margin: EdgeInsets.only(left: 20, top: 8, bottom: 8, right: 20), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ Text( comment.username + " : ", style: TextStyle(fontSize: 16, color: Colors.blue), ), Expanded( child: Text( comment.content, style: TextStyle(fontSize: 16), maxLines: 1, overflow: TextOverflow.ellipsis, )), ], )); }}Copy the code

The result is as follows:

2.5 Making comments

Because the commenting data structure needs to be populated with its own userId and userName, Flutter is implemented to get the user’s own UID and userName information natively.

2.5.1 Using MethodChannel to get a user’s UID natively

Flutter part

import 'package:flutter/services.dart'; class MomentBridge { static const String BRIDGE_NAME = "flutter.bbs/moment"; static const String METHOD_GET_USER_INFO = "getUserInfo"; static const String KEY_USER_ID = "key_user_id"; static const String KEY_USER_NAME = "key_user_name"; static const _methodChannel = const MethodChannel(BRIDGE_NAME); static Future<Map> getUserInfo() async { try { Map res = await _methodChannel.invokeMethod(METHOD_GET_USER_INFO); print("getUserInfo suc" + res.toString()); return res; } catch (e) { print("getUserInfo error" + e.toString()); } return Map(); }}Copy the code

Native Android:

package com.wsg.xsybbs.flutter import android.os.Bundle import cn.bmob.v3.BmobUser import com.wsg.xsybbs.bean.User import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel import io.flutter.plugins.GeneratedPluginRegistrant /** * Create by wangshengguo on 2021/3/25. */ class MomentDetailActivity : FlutterActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) // flutterEngine.let { // GeneratedPluginRegistrant. RegisterWith (it) / /} / / registered MethodChannel MethodChannel (flutterEngine dartExecutor, MomentBridge.BRIDGE_NAME ).setMethodCallHandler { call, result -> when (call.method) { MomentBridge.METHOD_GET_USER_INFO -> { val user = BmobUser.getCurrentUser(User::class.java) val map: HashMap<String, String> = hashMapOf() map[MomentBridge.KEY_USER_ID] = user.objectId map[MomentBridge.KEY_USER_NAME] = user.username result.success(map) } else -> { } } } } }Copy the code

After the call is initiated, the result is displayed

2.5.2 Comment

Static void addComment(BuildContext Context, String noteId, String content, Function(Comment comment) update) async { Comment comment = Comment(); comment.noteid = noteId; comment.content = content; Map map = await MomentBridge.getUserInfo(); comment.userid = map[MomentBridge.KEY_USER_ID]; comment.username = map[MomentBridge.KEY_USER_NAME]; Comment.save ().then((value) {toast.show (" comment on success ", context); update(comment); }).catchError((e) { showError(context, BmobError.convert(e).error); }); }Copy the code

2.5.3 refresh the UI

When a comment is successfully posted, insert it to the last item in the comment list

return MomentDetailWidget(object, (String content) { NetWorkRepo.addComment(context, _noteId, content, (comment) {setState(() {if (items[items.length-1] is CommentEmptyBean) {// If the comment list is empty, remove the UI when the comment is empty. Insert comments into the data set to show kitems.removeat (Kitems.leng-1); items.add(comment); } else {// Insert items. Add (comment); }}); }); });Copy the code

In the future, I will continue to improve the function of liking and rewrite the page using MVVM if I have time.

3. Project address

The project address is: github.com/stevenwsg/X…

4, reference

The Android hybrid Development chapter of the Flutter series

Native jump Flutter transfer parameters

bmob-flutter-sdk

Dart (*) JSON serialization

toast