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

1. 網(wǎng)絡(luò)編程與JSON解析

  • 默認的HttpClient請求網(wǎng)絡(luò)

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

httpGet() async {
  //創(chuàng)建網(wǎng)絡(luò)調(diào)用示例
  var client = http.Client();

  //構(gòu)造URI
  var uri = Uri.parse("https://flutter.dev");
  
  //設(shè)置user-agent為"Custom-UA",隨后立即發(fā)出請求
  http.Response response = await client.get(uri, headers : {"user-agent" : "Custom-UA"});

  //打印請求結(jié)果
  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 {
  //創(chuàng)建網(wǎng)絡(luò)調(diào)用示例
  Dio dio = new Dio();
  
  //設(shè)置URI及請求user-agent后發(fā)起請求
  var response = await dio.get("https://flutter.dev", options:Options(headers: {"user-agent" : "Custom-UA"}));
  
 //打印請求結(jié)果
  if(response.statusCode == HttpStatus.ok) {
    print(response.data.toString());
  } else {
    print("Error: ${response.statusCode}");
  }
}

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

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

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

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

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


//并行請求--------------

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

//打印請求1響應(yīng)結(jié)果
print("Response1: ${responseX[0].toString()}");
//打印請求2響應(yīng)結(jié)果
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:請先登錄");
      } 
      //檢查緩存是否有數(shù)據(jù)
      if(options.uri == Uri.parse('http://xxx.com/file1')) {
        return dio.resolve("返回緩存數(shù)據(jù)");
      }
      //放行請求
      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 庫中內(nèi)置的 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中去進行,不用擔(dān)心阻塞UI了. compute得有Widget才行.

3. 數(shù)據(jù)持久化

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

3.1 文件

需要引入: path_provider: ^1.6.4


//創(chuàng)建文件目錄
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 數(shù)據(jù)庫

需要引入: 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]));
    }

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

4. Flutter調(diào)原生

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


  handleButtonClick() async {
    bool result;
    //捕獲  萬一失敗了呢
    try {
      //異步等待,可能很耗時  等待結(jié)果
      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的數(shù)據(jù)會被序列化成一段二進制格式的數(shù)據(jù)在通道中傳輸,當(dāng)該數(shù)據(jù)傳遞到Flutter后,又會被反序列化成Dart語言中的類型.

5. Flutter中復(fù)用原生控件

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

6. Android項目中嵌入Flutter

官網(wǎng)地址: https://flutter.dev/docs/development/add-to-app

7. 混合開發(fā)導(dǎo)航棧

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

8. 狀態(tài)管理(跨組件傳遞數(shù)據(jù),Provider)

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

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

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

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

    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())),
      ),
    );
  }
}

//示例: 讀和寫數(shù)據(jù)
//使用Consumer 可以精準刷新發(fā)生變化的Widget
class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //取出數(shù)據(jù)
    //final _counter = Provider.of<CounterModel>(context);
    return Scaffold(
      //使用Consumer來封裝counter的讀取
      body: Consumer(
        //builder函數(shù)可以直接獲取到counter參數(shù)
        //Consumer 中的 builder 實際上就是真正刷新 UI 的函數(shù),它接收 3 個參數(shù),即 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獲得屏幕寬度來加載什么布局
  • 豎屏?xí)r用什么布局,橫屏?xí)r用什么布局.可以根據(jù)屏幕寬度才判斷.
  • 如需適配空間等的大小,則需要以切圖為基準,算出當(dāng)前設(shè)備的縮放系數(shù),在布局的時候乘一下.

10. 編譯模式

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

配置一些app的通用配置

///配置抽象
class AppConfig extends InheritedWidget {
  //主頁標(biāo)題
  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更新.由于是應(yīng)用入口,無需更新
  @override
  bool updateShouldNotify(InheritedWidget oldWidget) {
    return false;
  }
}

///為不同的環(huán)境創(chuàng)建不同的應(yīng)用入口

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

//main.dart   這個是測試環(huán)境的入口
/*void main(){
  var configuredApp = AppConfig(){
    appName: 'example',//主頁標(biāo)題
   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),
      ),
    );
  }
}



//運行開發(fā)環(huán)境應(yīng)用程序
//flutter run -t lib/main_dev.dart

//運行生產(chǎn)環(huán)境應(yīng)用程序
//flutter run -t lib/main.dart

/*
*
//打包開發(fā)環(huán)境應(yīng)用程序
flutter build apk -t lib/main_dev.dart
flutter build ios -t lib/main_dev.dart

//打包生產(chǎn)環(huán)境應(yīng)用程序
flutter build apk -t lib/main.dart
flutter build ios -t lib/main.dart
* */

11. Hot Reload

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

12. 關(guān)于調(diào)試

  • debugPrint函數(shù)同樣會將消息打印至控制臺,但與print不同的是,它提供了定制打印的能力.正式環(huán)境的時候?qū)ebugPrint函數(shù)定義為一個空函數(shù)體,就可以一鍵實現(xiàn)取消打印的功能了.
  // 正式環(huán)境 將debugPrint指定為空的執(zhí)行體, 所以它什么也不做
  debugPrint = (String message, {int wrapWidth}) {};
  debugPrint('test');

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

13. 常用命令行

階段 子任務(wù) 命令
工程初始化 App工程 flutter create --template=app hello
工程初始化 Dart包工程 flutter create --template=package hello
工程初始化 插件工程 flutter create --template=plugin hello
構(gòu)建 Debug構(gòu)建 flutter build apk --debug </p> flutter build ios --debug
構(gòu)建 Release構(gòu)建 flutter build apk --release </p> flutter build ios --release
構(gòu)建 Profile構(gòu)建 flutter build apk --profile </p> flutter build ios --profile
集成原生工程 獨立App打包 flutter build apk --release </p> flutter build ios --release
集成原生工程 Pod/AAR打包 flutter build apk --release </p> flutter build ios --release
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容