一個Android菜鳥入門Flutter 筆記(二)

1. 網絡編程與JSON解析

  • 默認的HttpClient請求網絡

get() async {
  //創建網絡調用示例,設置通用請求行爲(超時時間)
  var httpClient = HttpClient();
  httpClient.idleTimeout = Duration(seconds: 5);
  
  //構造URI,設置user-agent爲"Custom-UA"
  var uri = Uri.parse("https://flutter.dev");
  var request = await httpClient.getUrl(uri);
  request.headers.add("user-agent", "Custom-UA");
  
  //發起請求,等待響應
  var response = await request.close();
  
  //收到響應,打印結果
  if (response.statusCode == HttpStatus.ok) {
    print(await response.transform(utf8.decoder).join());
  } else {
    print('Error: \nHttp status ${response.statusCode}');
  }
}
  • 在 Flutter 中,所有網絡編程框架都是以 Future 作爲異步請求的包裝
  • http是Dart官方的另一個網絡請求類,需要添加依賴http: '>=0.11.3+12'

httpGet() async {
  //創建網絡調用示例
  var client = http.Client();

  //構造URI
  var uri = Uri.parse("https://flutter.dev");
  
  //設置user-agent爲"Custom-UA",隨後立即發出請求
  http.Response response = await client.get(uri, headers : {"user-agent" : "Custom-UA"});

  //打印請求結果
  if(response.statusCode == HttpStatus.ok) {
    print(response.body);
  } else {
    print("Error: ${response.statusCode}");
  }
}
  • dio,一般使用這個,dio是一個強大的Dart Http請求庫,支持Restful API、FormData、攔截器、請求取消、Cookie管理、文件上傳/下載、超時、自定義適配器等…添加依賴dio: '>2.1.3'

void getRequest() async {
  //創建網絡調用示例
  Dio dio = new Dio();
  
  //設置URI及請求user-agent後發起請求
  var response = await dio.get("https://flutter.dev", options:Options(headers: {"user-agent" : "Custom-UA"}));
  
 //打印請求結果
  if(response.statusCode == HttpStatus.ok) {
    print(response.data.toString());
  } else {
    print("Error: ${response.statusCode}");
  }
}

//下載-------------

//使用FormData表單構建待上傳文件
FormData formData = FormData.from({
  "file1": UploadFileInfo(File("./file1.txt"), "file1.txt"),
  "file2": UploadFileInfo(File("./file2.txt"), "file1.txt"),

});
//通過post方法發送至服務端
var responseY = await dio.post("https://xxx.com/upload", data: formData);
print(responseY.toString());

//使用download方法下載文件
dio.download("https://xxx.com/file1", "xx1.zip");

//增加下載進度回調函數
dio.download("https://xxx.com/file1", "xx2.zip", onReceiveProgress: (count, total) {
  //do something      
});


//並行請求--------------

//同時發起兩個並行請求
List<Response> responseX= await Future.wait([dio.get("https://flutter.dev"),dio.get("https://pub.dev/packages/dio")]);

//打印請求1響應結果
print("Response1: ${responseX[0].toString()}");
//打印請求2響應結果
print("Response2: ${responseX[1].toString()}");

//攔截器-----------------

//增加攔截器
dio.interceptors.add(InterceptorsWrapper(
    onRequest: (RequestOptions options){
      //爲每個請求頭都增加user-agent
      options.headers["user-agent"] = "Custom-UA";
      //檢查是否有token,沒有則直接報錯
      if(options.headers['token'] == null) {
        return dio.reject("Error:請先登錄");
      } 
      //檢查緩存是否有數據
      if(options.uri == Uri.parse('http://xxx.com/file1')) {
        return dio.resolve("返回緩存數據");
      }
      //放行請求
      return options;
    }
));

//增加try catch,防止請求報錯
try {
  var response = await dio.get("https://xxx.com/xxx.zip");
  print(response.data.toString());
}catch(e) {
  print(e);
}

2.JSON解析

  • 只能手動解析.
import 'dart:convert';

String jsonString = '''
{
  "id":"123",
  "name":"張三",
  "score" : 95,
  "teacher": { "name": "李四", "age" : 40 }
}
''';

//json解析

//所謂手動解析,是指使用 dart:convert 庫中內置的 JSON 解碼器,將 JSON 字符串解析成自定義對象的過程。

class Teacher {
  String name;
  int age;

  Teacher({this.name, this.age});

  factory Teacher.fromJson(Map<String, dynamic> parsedJson) {
    return Teacher(name: parsedJson['name'], age: parsedJson['age']);
  }

  @override
  String toString() {
    return 'Teacher{name: $name, age: $age}';
  }
}

class Student {
  String id;
  String name;
  int score;
  Teacher teacher;

  Student({this.id, this.name, this.score, this.teacher});

  //從Map中取
  factory Student.fromJson(Map<String, dynamic> parsedJson) {
    return Student(
        id: parsedJson['id'],
        name: parsedJson['name'],
        score: parsedJson['score'],
        teacher: Teacher.fromJson(parsedJson['teacher']));
  }

  @override
  String toString() {
    return 'Student{id: $id, name: $name, score: $score, teacher: $teacher}';
  }
}


void main() {

  final jsonResponse = json.decode(jsonString);//將字符串解碼成Map對象
  Student student = Student.fromJson(jsonResponse);//手動解析
  print(student.teacher.name);
}

  • json解析比較耗時,放compute中去進行,不用擔心阻塞UI了. compute得有Widget才行.

3. 數據持久化

  • 由於 Flutter 僅接管了渲染層,真正涉及到存儲等操作系統底層行爲時,還需要依託於原生 Android、iOS.
  • 三種數據持久化方法,即文件、SharedPreferences 與數據庫
  • Flutter 提供了兩種文件存儲的目錄,即臨時(Temporary)目錄與文檔(Documents)目錄:

3.1 文件

需要引入: path_provider: ^1.6.4


//創建文件目錄
Future<File> get _localFile async {
  final directory = await getApplicationDocumentsDirectory();
  final path = directory.path;
  return File('$path/content.txt');
}
//將字符串寫入文件
Future<File> writeContent(String content) async {
  final file = await _localFile;
  return file.writeAsString(content);
}
//從文件讀出字符串
Future<String> readContent() async {
  try {
    final file = await _localFile;
    String contents = await file.readAsString();
    return contents;
  } catch (e) {
    return "";
  }
}

3.2 SharedPreferences

需要引入: shared_preferences: ^0.5.6+2


//讀取SharedPreferences中key爲counter的值
Future<int>_loadCounter() async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  int  counter = (prefs.getInt('counter') ?? 0);
  return counter;
}

//遞增寫入SharedPreferences中key爲counter的值
Future<void>_incrementCounter() async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
    int counter = (prefs.getInt('counter') ?? 0) + 1;
    prefs.setInt('counter', counter);
}

3.3 數據庫

需要引入: sqflite: ^1.2.1

dbDemo() async {
    final Future<Database> database = openDatabase(
      //join是拼接路徑分隔符
      join(await getDatabasesPath(), 'student_database.db'),
      onCreate: (db, version) => db.execute(
          "CREATE TABLE students(id TEXT PRIMARY KEY,name TEXT,score INTEGER)"),
      onUpgrade: (db, oldVersion, newVersion) {
        //dosth for 升級
      },
      version: 1,
    );

    Future<void> insertStudent(Student std) async {
      final Database db = await database;
      await db.insert(
        'students',
        std.toJson(),
        //插入衝突策略,新的替換舊的
        conflictAlgorithm: ConflictAlgorithm.replace,
      );
    }

    //插入3個
    await insertStudent(student1);
    await insertStudent(student2);
    await insertStudent(student3);

    Future<List<Student>> students() async {
      final Database db = await database;
      final List<Map<String, dynamic>> maps = await db.query('students');
      return List.generate(maps.length, (i) => Student.fromJson(maps[i]));
    }

    ////讀取出數據庫中插入的Student對象集合
    students().then((list) => list.forEach((s) => print(s.name)));
    //釋放數據庫資源
    final Database db = await database;
    db.close();
  }

4. Flutter調原生

  • 用AS單獨打開Flutter項目中的Android工程,寫代碼,每次寫完代碼rebuild一下.然後想讓Flutter代碼能調到Android這邊的代碼,得重新運行.
  • 如果AS run窗口不展示任何消息,可以使用 命令flutter run lib/native/invoke_method.dart執行dart,然後看錯誤消息.
  • Flutter發起方法調用請求開始,請求經由唯一標識符指定的方法通道到達原生代碼宿主,而原生代碼宿主則通過註冊對應方法實現,響應並處理調用請求.最後將執行結果通過消息通道,回傳至Flutter.
  • 方法通道是非線程安全的,需要在UI線程(Android或iOS的主線程)回調.
  • 數據持久化,推送,攝像頭,藍牙等,都需要平臺支持
  • 輕量級解決方案: 方法通道機制 Method Channel
  • 調用示例:
class _MyHomePageState extends State<MyHomePage> {
  //聲明MethodChannel
  static const platform = MethodChannel('com.xfhy.basic_ui/util');


  handleButtonClick() async {
    bool result;
    //捕獲  萬一失敗了呢
    try {
      //異步等待,可能很耗時  等待結果
      result = await platform.invokeMethod('isEmpty', "have data");
    } catch (e) {
      result = false;
    }
    print('result : $result');
  }

}

//Android代碼
import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugins.GeneratedPluginRegistrant

class MainActivity : FlutterActivity() {

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        GeneratedPluginRegistrant.registerWith(flutterEngine)

        //參考: https://flutter.dev/docs/development/platform-integration/platform-channels

        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.xfhy.basic_ui/util").setMethodCallHandler { call, result ->
            //判斷方法名是否支持
            if (call.method == "isEmpty") {
                val arguments = call.arguments
                result.success(StringUtil.isEmpty(arguments as? String))
                print("success")
            } else {
                //方法名暫不支持
                result.notImplemented()
                print("fail")
            }
        }
    }
}


  • Android或者iOS的數據會被序列化成一段二進制格式的數據在通道中傳輸,當該數據傳遞到Flutter後,又會被反序列化成Dart語言中的類型.

5. Flutter中複用原生控件

  • 除去地圖、WebView、相機等涉及底層方案的特殊情況外,大部分原生代碼能夠實現的 UI 效果,完全可以用 Flutter 實現.
  • 使用這種方式對性能造成非常大的影響且不方便維護.
  • 方法通道: 原生邏輯複用
  • 平臺視圖: 原生視圖複用

6. Android項目中嵌入Flutter

官網地址: https://flutter.dev/docs/development/add-to-app

  • FlutterEngine 文檔: https://github.com/flutter/flutter/wiki/Experimental:-Reuse-FlutterEngine-across-screens
  • FlutterView 文檔: https://github.com/flutter/flutter/wiki/Experimental:-Add-Flutter-View
  • API一會兒就過時了,得去官網看最新的才行.
  • 可以在Android App中開啓Flutter的Activity,Flutter的Activity是在另外一個進程,第一次進入特別慢.也可以加入Flutter的View和Fragment
  • 在Android工程下新建一個Flutter的module比較簡單直接

7. 混合開發導航棧

  • Android跳轉Flutter,依賴FlutterView.Flutter在FlutterView中建立了自己的導航棧.
  • 通常會將Flutter容器封裝成一個獨立的Activity或者ViewController. 這樣打開一個普通的Activity既是打開Flutter界面了
  • Flutter頁面跳轉原生界面,需要利用方法通道,然後用原生去打開響應的界面.
  • Flutter實例化成本非常高,每啓動一個Flutter實例,就會創建一套新的渲染機制,即Flutter Engine,以及底層的Isolate.而這些實例之間的內存是不相互共享的,會帶來較大的系統資源消耗.
  • 實際開發中,儘量用Flutter去開發閉環的業務模塊.原生跳轉過去就行,剩下的全部由Flutter內部完成. 儘量避免Flutter頁面回到原生頁面,原生頁面又啓動新的Flutter實例的情況.

8. 狀態管理(跨組件傳遞數據,Provider)

  • Dart的一個庫,可以實現在StatelessWidget中刷新數據.跨組件傳遞數據.全局共享數據.依賴注入
  • 使用Provider後,我們就再也不需要StalefullWidget了.
  • Provider以InheritedWidget語法糖的方法,通過數據資源封裝,數據注入,和數據讀寫這3個步驟,爲我們實現了跨組件(跨頁面)之間的數據共享
  • 我們既可以用Provider來實現靜態的數據讀傳遞,也可以使用ChangeNotifierProvider來實現動態的數據讀寫傳遞,還用通過MultiProvider來實現多個數據資源的共享
  • Provider.of和Consumer都可以實現數據的讀取,並且Consumer還可以控制UI刷新的粒度,避免與數據無關的組件的無謂刷新
  • 封裝數據

//定義需要共享的數據模型,通過混入ChangeNotifier管理聽衆
class CounterModel with ChangeNotifier {
  int _count = 0;
  //讀方法
  int get counter => _count; 
  //寫方法
  void increment() {
    _count++;
    notifyListeners();//通知聽衆刷新
  }
}
  • 放數據
儘量把數據放到更高的層級

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //通過Provider組件封裝數據資源
    //因Provider是InheritedWidget的語法糖,所以它是一個Widget
    //ChangeNotifierProvider只能搞一個
    //MultiProvider可以搞多個
    return MultiProvider(
      providers: [
        //注入字體大小  下個界面讀出來
        Provider.value(value: 30.0),
        //注入計數器實例
        ChangeNotifierProvider.value(value: CounterModel())
      ],
      child: MaterialApp(
        home: FirstPage(),
      ),
    );
  }
}
  • 讀數據
//示例: 讀數據
class FirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //取出資源  類型是CounterModel
    //獲取計時器實例
    final _counter = Provider.of<CounterModel>(context);
    //獲取字體大小
    final textSize = Provider.of<double>(context);

    /*
    *
//使用Consumer2獲取兩個數據資源
Consumer2<CounterModel,double>(
  //builder函數以參數的形式提供了數據資源
  builder: (context, CounterModel counter, double textSize, _) => Text(
      'Value: ${counter.counter}',
      style: TextStyle(fontSize: textSize))
)
* 我們最多可以使用到 Consumer6,即共享 6 個數據資源。
    * */

    return Scaffold(
      body: Center(
        child: Text(
          'Counter: ${_counter.counter}',
          style: TextStyle(fontSize: textSize),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Text('Go'),
        onPressed: () => Navigator.of(context)
            .push(MaterialPageRoute(builder: (context) => SecondPage())),
      ),
    );
  }
}

//示例: 讀和寫數據
//使用Consumer 可以精準刷新發生變化的Widget
class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //取出數據
    //final _counter = Provider.of<CounterModel>(context);
    return Scaffold(
      //使用Consumer來封裝counter的讀取
      body: Consumer(
        //builder函數可以直接獲取到counter參數
        //Consumer 中的 builder 實際上就是真正刷新 UI 的函數,它接收 3 個參數,即 context、model 和 child
        builder: (context, CounterModel counter, _) => Center(
          child: Text('Value: ${counter.counter}'),
        ),
      ),
      floatingActionButton: Consumer<CounterModel>(
        builder: (context, CounterModel counter, child) => FloatingActionButton(
          onPressed: counter.increment,
          child: child,
        ),
        child: Icon(Icons.add),
      ),
    );
  }
}

9. 適配不同分辨率的手機屏幕

  • Flutter中平時寫控件的尺寸,其實有點類似於Android中的dp
  • 只能是通過MediaQuery.of(context).size.width獲得屏幕寬度來加載什麼佈局
  • 豎屏時用什麼佈局,橫屏時用什麼佈局.可以根據屏幕寬度才判斷.
  • 如需適配空間等的大小,則需要以切圖爲基準,算出當前設備的縮放係數,在佈局的時候乘一下.

10. 編譯模式

  • 根據kReleaseMode這個編譯常數可以判斷出當前是release環境還是debug環境.
  • 還可以用個斷言判斷,release編譯的時候會將斷言全部移除.
  • 通過使用InheritedWidget爲應用中可配置部分進行抽象封裝(比如接口域名,app名稱等),通過配置多入口方式爲應用的啓動注入配置環境
  • 使用kReleaseMode能判斷,但是另一個環境的代碼雖然不能執行到,但是會被打入二進制包中.會增大包體積,儘量使用斷言.或者打release包的時候把kReleaseMode的另一個邏輯註釋掉.
if (kReleaseMode) {
  //正式環境
  text = "release";
} else {
  //測試環境  debug
  text = "debug";
}

配置一些app的通用配置

///配置抽象
class AppConfig extends InheritedWidget {
  //主頁標題
  final String appName;

  //接口域名
  final String apiBaseUrl;

  AppConfig(
      {@required this.appName,
      @required this.apiBaseUrl,
      @required Widget child})
      : super(child: child);

  //方便其子Widget在Widget樹中找到它
  static AppConfig of(BuildContext context) {
    return context.inheritFromWidgetOfExactType(AppConfig);
  }

  //判斷是否需要子Widget更新.由於是應用入口,無需更新
  @override
  bool updateShouldNotify(InheritedWidget oldWidget) {
    return false;
  }
}

///爲不同的環境創建不同的應用入口

//main_dev.dart    這個是正式環境的入口
void main() {
  var configuredApp = AppConfig(
    appName: 'dev', //主頁標題
    apiBaseUrl: 'http://dev.example.com/', //接口域名
    child: MyApp(),
  );
  runApp(configuredApp);
}

//main.dart   這個是測試環境的入口
/*void main(){
  var configuredApp = AppConfig(){
    appName: 'example',//主頁標題
   apiBaseUrl: 'http://api.example.com/',//接口域名
   child: MyApp(),
  }
  runApp(configuredApp);
}*/

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var config = AppConfig.of(context);
    return MaterialApp(
      title: config.appName,
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var config = AppConfig.of(context);
    return Scaffold(
      appBar: AppBar(
        title: Text(config.appName),
      ),
      body: Center(
        child: Text(config.apiBaseUrl),
      ),
    );
  }
}



//運行開發環境應用程序
//flutter run -t lib/main_dev.dart

//運行生產環境應用程序
//flutter run -t lib/main.dart

/*
*
//打包開發環境應用程序
flutter build apk -t lib/main_dev.dart
flutter build ios -t lib/main_dev.dart

//打包生產環境應用程序
flutter build apk -t lib/main.dart
flutter build ios -t lib/main.dart
* */

11. Hot Reload

  • Flutter的熱重載是基於JIT編譯模式的代碼增量同步.由於JIT屬於動態編譯,能夠將Dart代碼編譯成生成中間代碼,讓Dart VM在運行時解釋執行,因此可以通過動態更新中間代碼實現增量同步.
  • 熱重載流程分爲5步:
    1. 掃描工程改動
    2. 增量編譯
    3. 推送更新
    4. 代碼合併
    5. Widget樹重建
  • Flutter接收到代碼變更,不會重新啓動App,只會觸發Widget樹的重新繪製…因此可以保持之前的狀態
  • 由於涉及到狀態保存與恢復,因此涉及狀態兼容和狀態初始化的場景,熱重載是無法支持的.(比如改動前後Widget狀態無法兼容,全局變量與靜態屬性的更改,main方法裏面的更改,initState方法裏面更改,枚舉和泛型的更改等)
  • 如果遇到了熱重載無法支持的場景,可以點擊工程面板左下角的熱重啓(Hot Restart)按鈕,也很快

12. 關於調試

  • debugPrint函數同樣會將消息打印至控制檯,但與print不同的是,它提供了定製打印的能力.正式環境的時候將debugPrint函數定義爲一個空函數體,就可以一鍵實現取消打印的功能了.
  // 正式環境 將debugPrint指定爲空的執行體, 所以它什麼也不做
  debugPrint = (String message, {int wrapWidth}) {};
  debugPrint('test');

  //開發環境就需要打印出日誌
  debugPrint = (String message, {int wrapWidth}) =>
      debugPrintSynchronously(message, wrapWidth: wrapWidth);
  • 開啓Debug Painting,有點像原生的繪製佈局邊界.
void main() {
  //Debug Painting 界面調試工具
  //有點像原生的顯示佈局邊界
  debugPaintSizeEnabled = true;
  runApp(MyApp());
}
  • 還可以使用Flutter Inspector去查看更詳細的可視化信息.

13. 常用命令行

階段 子任務 命令
工程初始化 App工程 flutter create --template=app hello
工程初始化 Dart包工程 flutter create --template=package hello
工程初始化 插件工程 flutter create --template=plugin hello
構建 Debug構建 flutter build apk --debug flutter build ios --debug
構建 Release構建 flutter build apk --release flutter build ios --release
構建 Profile構建 flutter build apk --profile flutter build ios --profile
集成原生工程 獨立App打包 flutter build apk --release flutter build ios --release
集成原生工程 Pod/AAR打包 flutter build apk --release flutter build ios --release
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章