Ah, Flutter smells good

I wanted to learn about Flutter about a year ago, but I was a little intimidated by the inferno of its layout. I thought, why is there no XML layout for flutter because nesting is so complicated? JSX is fine, why dart instead of javascript? However, with the release of a new version of Flutter at this year’s Google IO conference, the hype is building. I started to learn flutter from scratch again:

  • Browse the dart. Dev /
  • Browse book.flutterChina.club/I wanted to watch the actual video, but I found it too inefficient (a little wordy), so I gave it up. I decided to learn by reading the source code of the Flutter project, which was actually the most efficient way.

The company just had a new app development, so this time we decided to develop Flutter. Learning while developing flutter has completed both work and learning (PS: now the company has also learned 😂 on ios and the front-end).

The feeling after using flutter is that once you accept the nested layout, the layout is not that difficult, hot Reload, async, dart, etc.

The following is a record of the relevant points of the app development (rookie stage, welcome corrections)

Third-party libraries

  • Dio: network
  • Sqflite: database
  • Pull_to_refresh: pull-down refresh and pull-up loading
  • Json_serializable: JSON serialization, automatically generating model factory methods
  • Shared_preferences: Local storage
  • -Penny: Fluttertoast

Image resources

1,2, and 3 times as many images are usually needed to fit the image resources at various resolutions. Create assets/images under the root of the Flutter project and add the image configuration to the pubspec.yaml file

flutter:
  #...
  assets:
    - assets/images/
Copy the code

Then cut out 1/2/3 images using Sketch. Add 2.0x/ and 3.0x/ at the beginning of the word by editing the preset. The exported format meets the needs of the flutter image resources.

Dart utility class to generate the Image

class ImageHelper {
  static String png(String name) {
    return "assets/images/$name.png";
  }

  static Widget icon(String name, {double width, double height, BoxFit boxFit}) {
    returnImage.asset( png(name), width: width, height: height, fit: boxFit, ); }}Copy the code

Tab navigation on the main interface

On the main interface of app, TAB bottom navigation is the most commonly used. The Scaffold is usually used based on the bottomNavigationBar and PageView. Control PageView switching through PageController, and control TAB selection status using currentIndex of BottomNavigationBar. To enable listening for the return key, use the WillPopScope implementation to exit the app by hitting the return key twice.

List pages = <Widget>[HomePage(), MinePage()];

class _TabNavigatorState extends State<TabNavigator> {
  DateTime _lastPressed;
  int _tabIndex = 0;
  var _controller = PageController(initialPage: 0);

  BottomNavigationBarItem buildTab(
      String name, String normalIcon, String selectedIcon) {
    return BottomNavigationBarItem(
        icon: ImageHelper.icon(normalIcon, width: 20),
        activeIcon: ImageHelper.icon(selectedIcon, width: 20),
        title: Text(name));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      bottomNavigationBar: BottomNavigationBar(
          currentIndex: _tabIndex,
          backgroundColor: Colors.white,
          onTap: (index) {
            setState(() {
              _controller.jumpToPage(index);
              _tabIndex = index;
            });
          },
          selectedItemColor: Color(0xff333333),
          unselectedItemColor: Color(0xff999999),
          selectedFontSize: 11,
          unselectedFontSize: 11,
          type: BottomNavigationBarType.fixed,
          items: [
            buildTab("Home"."ic_home"."ic_home_s"),
            buildTab("Mine"."ic_mine"."ic_mine_s")
          ]),
      body: WillPopScope(
          child: PageView.builder(
            itemBuilder: (ctx, index) => pages[index],
            controller: _controller,
            physics: NeverScrollableScrollPhysics(),// Disallow PageView from sliding left and right
          ),
          onWillPop: () async {
            if (_lastPressed == null ||
                DateTime.now().difference(_lastPressed) >
                    Duration(seconds: 1)) {
              _lastPressed = DateTime.now();
              Fluttertoast.showToast(msg: "Press again to exit");
              return false;
            } else {
              return true; }})); }}Copy the code

Network layer encapsulation

The network framework uses DIO, and regardless of platform, network requests are ultimately turned into physical Models for UI presentation. Here will be dio to do a package, easy to use.

Universal interceptor

Custom interceptors are usually added to preprocess network requests. Login information (such as user_id) is usually added to public parameters.

import 'package:dio/dio.dart';
import 'dart:async';
import 'package:shared_preferences/shared_preferences.dart';

class CommonInterceptor extends Interceptor {
  @override
  Future onRequest(RequestOptions options) async {
    options.queryParameters = options.queryParameters ?? {};
    options.queryParameters["app_id"] = "1001";
    var pref = await SharedPreferences.getInstance();
    options.queryParameters["user_id"] = pref.get(Constants.keyLoginUserId);
    options.queryParameters["device_id"] = pref.get(Constants.keyDeviceId);
    return super.onRequest(options); }}Copy the code

Dio encapsulation

Then use DIO to encapsulate get and POST requests and preprocess the code that responds to the response. Suppose our response format looks like this:

{code:0, MSG :" get data successfully ", result:[] // or {}}Copy the code
import 'package:dio/dio.dart';
import 'common_interceptor.dart';

/* * Network management */
class HttpManager {
  static HttpManager _instance;

  static HttpManager getInstance() {
    if (_instance == null) {
      _instance = HttpManager();
    }
    return _instance;
  }

  Dio dio = Dio();

  HttpManager() {
    dio.options.baseUrl = "https://api.xxx.com/";
    dio.options.connectTimeout = 10000;
    dio.options.receiveTimeout = 5000;
    dio.interceptors.add(CommonInterceptor());
    dio.interceptors.add(LogInterceptor(responseBody: true));
  }

  static Future<Map<String.dynamic>> get(String path, Map<String.dynamic> map) async {
    var response = await getInstance().dio.get(path, queryParameters: map);
    return processResponse(response);
  }

  /* Form */
  static Future<Map<String.dynamic>> post(String path, Map<String.dynamic> map) async {
    var response = await getInstance().dio.post(path,
        data: map,
        options: Options(
            contentType: "application/x-www-form-urlencoded",
            headers: {"Content-Type": "application/x-www-form-urlencoded"}));
    return processResponse(response);
  }

  static Future<Map<String.dynamic>> processResponse(Response response) async {
    if (response.statusCode == 200) {
      var data = response.data;
      int code = data["code"];
      String msg = data["msg"];
      if (code == 0) {// The request response succeeded
        return data;
      }
      throw Exception(msg);
    }
    throw Exception("server error"); }}Copy the code

Turn the map model

With DIO, we can convert the final request response to a Map

object, and we need to convert the Map to the corresponding Model. Suppose we have an interface that gets the list of articles as follows:
,>

[{article_id:1, article_title:" title ", article_link:"https://xxx.xxx"}]}Copy the code

You need an Article model. Since reflection is disabled under Flutter, we have to manually initialize each member variable. However, we can hand over the manual initialization to jSON_serializable. First introduce it in pubspec.yaml:

Dependencies: json_annotation: ^2.0.0 dev_dependencies: json_serialIZABLE: ^2.0.0Copy the code

Dart we create an article. Dart Model class:

import 'package:json_annotation/json_annotation.dart';

part 'article.g.dart';
//FieldRename. Snake specifies the json field underscore type such as article_id
@JsonSerializable(fieldRename: FieldRename.snake, checked: true)
class Article {
  final int articleId;
  final String articleTitle;
  final String articleLikn;
}
Copy the code

Note that there is a reference to an article. G. art file that is not generated, which is generated by the pub run build_runner build command

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'article.dart';

// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

Article _$ArticleFromJson(Map<String, dynamic> json) {
  return $checkedNew('Article', json, () {
    final val = Article();
    $checkedConvert(json, 'article_id', (v) => val.articleId = v as int);
    $checkedConvert(
        json, 'article_title', (v) => val.articleTitle = v as String);
    $checkedConvert(json, 'article_link', (v) => val.articleLink = v as String);
    return val;
  }, fieldKeyMap: const {
    'articleId': 'article_id'.'articleTitle': 'article_title'.'articleLink': 'article_link'
  });
}

Map<String, dynamic> _$ArticleToJson(Article instance) => <String, dynamic>{
      'article_id': instance.articleId,
      'article_title': instance.articleTitle,
      'article_link': instance.articleLink
    };
Copy the code

Then add the factory method in article. Dart

class Article{
  ...
  factory Article.fromJson(Map<String, dynamic> json) => _$ArticleFromJson(json);
}
Copy the code

Specific request encapsulation

After creating the Model class, we can create a concrete API request class, ApiRepository. Through the Async library, we can wrap the network request into a Future object. When we call it, we can convert the asynchronous callback request into a synchronous one, which is somewhat similar to Kotlin’s coroutine:

import 'dart:async';
import '.. /http/http_manager.dart';
import '.. /model/article.dart';

class ApiRepository {
  static Future<List<Article>> articleList() async {
    var data = await HttpManager.get("articleList", {"page": 1});
    return data["result"].map((Map<String, dynamic> json) {
      returnArticle.fromJson(json); }); }}Copy the code

The actual call

Once the network request is wrapped, it can be used in a specific component. Suppose there is a _ArticlePageState:

import 'package:flutter/material.dart';
import '.. /model/article.dart';
import '.. /repository/api_repository.dart';

class ArticlePage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _ArticlePageState();
  }
}

class _ArticlePageState extends State<ArticlePage> {
  List<Article> _list = [];

  @override
  void initState() { super.initState(); _loadData(); } void _loadData() async {// If the progress bar needs to be displayed, a try/catch is required to catch the request exception. showLoading(); try { var list = await ApiRepository.articleList();setState(() {
        _list = list;
      });
    } catch (e) {}
    hideLoading();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
          child: ListView.builder(
              itemCount: _list.length,
              itemBuilder: (ctx, index) {
                returnText(_list[index].articleTitle); }))); }}Copy the code

The database

Database operation through SQflite, simple encapsulation processing example of Article insert operation.

import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import 'dart:async';
import '.. /model/article.dart';

class DBManager {
  static const int _VSERION = 1;
  static const String _DB_NAME = "database.db";
  static Database _db;
  static const String TABLE_NAME = "t_article";
  static const String createTableSql = ''' create table $TABLE_NAME( article_id int, article_title text, article_link text, user_id int, primary key(article_id,user_id) ); ' ' ';

  static init() async {
    String dbPath = await getDatabasesPath();
    String path = join(dbPath, _DB_NAME);
    _db = await openDatabase(path, version: _VSERION, onCreate: _onCreate);
  }

  static _onCreate(Database db, int newVersion) async {
    await db.execute(createTableSql);
  }

  static Future<int> insertArticle(Article item, int userId) async {
    var map = item.toMap();
    map["user_id"] = userId;
    return _db.insert("$TABLE_NAME", map); }}Copy the code

Android layer compatible communication processing

To be compatible with the underlying layer, Flutter and Native(Android/iOS) communication is required via the MethodChannel

Flutter calls the Android layer methods

Here is an example of the flutter end opening system album intent and getting the final album path callback to the flutter end. We handle the communication logic in the onCreate method of MainActivity in Android

eventChannel = MethodChannel(flutterView, "event") eventChannel? .setMethodCallHandler { methodCall, result ->when (methodCall.method) {\
                "openPicture" -> PictureUtil.openPicture(this) {
                    result.success(it)
                }
            }
        }
Copy the code

Because the result is called back to the Flutter side via result.success, the tool class to open the album is encapsulated.

object PictureUtil {
    fun openPicture(activity: Activity, callback: (String?) -> Unit) {
        val f = getFragment(activity)
        f.callback = callback
        val intentToPickPic = Intent(Intent.ACTION_PICK, null)
        intentToPickPic.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*")
        f.startActivityForResult(intentToPickPic, 200)
    }

    private fun getFragment(activity: Activity): PictureFragment {
        var fragment = activity.fragmentManager.findFragmentByTag("picture")
        if (fragment is PictureFragment) {

        } else {
            fragment = PictureFragment()
            activity.fragmentManager.apply {
                beginTransaction().add(fragment, "picture").commitAllowingStateLoss()
                executePendingTransactions()
            }
        }
        return fragment
    }
}
Copy the code

Then add callback to PictureFragment and process the onActivityResult logic

class PictureFragment : Fragment() {
    var callback: ((String?) -> Unit)? = null
    override fun onActivityResult(requestCode: Int, resultCode: Int.data: Intent?). {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == 200) {
            if (data! =null) { callback? .invoke(FileUtil.getFilePathByUri(activity,data!!!!! .data)}}}}Copy the code

Here FileUtil getFilePathByUri is through the data access album don’t post code path logic, many can search online. Then apply to the flutter end

void _openPicture() async {
    var result = await MethodChannel("event").invokeMethod("openPicture");
    images.add(result as String);
    setState(() {});
  }
Copy the code

Android calls the Flutter code

Declare the eventChannel in the MainActivity as a class variable, and you can use it elsewhere. For example, if a push notification is required, call the buried interface method on the Flutter side.

class MainActivity : FlutterActivity() {
    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        GeneratedPluginRegistrant.registerWith(this)
        eventChannel = MethodChannel(flutterView, "event") eventChannel? .setMethodCallHandler { methodCall, result -> ... } } checkNotify(intent) initPush() }companion object {
        var eventChannel: MethodChannel? = null}}Copy the code

Call the Flutter method in the Firebase message notification

class FirebaseMsgService : FirebaseMessagingService() {
    override fun onMessageReceived(msg: RemoteMessage?) {
        super.onMessageReceived(msg)
        "onMessageReceived:$msg".logE()
        if(msg ! = null){ showNotify(msg) MainActivity.eventChannel? .invokeMethod("saveEvent", 1)}}}Copy the code

Then we add callbacks to the Flutter layer

class NativeEvent {
  static const platform = const MethodChannel("event");

  static void init() {
    platform.setMethodCallHandler(platformCallHandler);
  }

  static Future<dynamic> platformCallHandler(MethodCall call) async {
    switch (call.method) {
      case "saveEvent":
        print("saveEvent.....");
        await ApiRepository.saveEvent(call.arguments);
        return "";
        break; }}}Copy the code