一个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

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 </p> flutter build ios --debug
构建 Release构建 flutter build apk --release </p> flutter build ios --release
构建 Profile构建 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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容