如何编写地道的 Flutter 代码

背景

初入Flutter,都会带有之前开发经验的一种思维定式,如果改变这一现状,就必须搞清楚Flutter的声明式编程的编程思想,,这篇文章我将从编程范式、如何正确使用widget,以及Flutter的最佳实践与技巧三个方便解读,帮助大家写出地道的Flutter的代码。

两种编程范式

  • 命令式编程:命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现。是函数式编程的意思,更强调流程及步骤。强调的操作细节,即Step by Step,而不是对象之间的关系

  • 声明式编程:告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。更强调语义的层面,而不是细节,不强调具体流程的细节,即更强调What,描述对象之间的关系,而不是具体的操作细节

这样说可能不太容易理解,我们就拿 Andorid 和 IOS 来举个栗子:

  1. iOS编程范式

很纯粹的命令式,new viewaddsubview

  1. 安卓编程范式:

算是半命令式,xml 声明了 UI,这是声明式的部分;但程序运行时如果要修改某个 view,仍是取到这个view,再去命令式地修改。

  1. Flutter编程范式:

Flutter 的 UI 框架吸取了 react 的理念,即 UI 是关于状态的函数。

如何正确的使用 widget

  1. use smallwidgets

  • 构建更加健壮/安全
  • 构建更高效,避免不必要的 bulid,提高性能
  • widget tree 更具有清晰干净
// like this
const MyPage extends StatelessWidget {
  @override
  Widget build(context) {
    return Column(
      children: const [    // <- I am now able to give a const list to the Column
        TopBar(),
        MyWidget1(),
        MyWidget2(),
      ],
    );
  }
}

  1. build 中减少复杂业务

    1. 避免在 build 使用过度变量的变量
 // Don’t
bool arg1, arg2, arg3

const MyPage extends StatelessWidget {
  @override
  Widget build(context) {
   if (arg1) {
     // do something
    } else if (arg2) {
     // do something
    } else if (arg3) {
     // do something
    } else {
     // do something
    }
  }
}

// Do
enum MyPageState { arg1 , arg2, arg3,  default,}

const MyPage extends StatelessWidget {
  @override
  Widget build(context) {
    switch (type) {
      case MyPageState.arg1:
       // do something
      case MyPageState.arg2:
       // do something
      case MyPageState.arg3:
       // do something
      case MyPageState.default:
       // do something
    }
  }
}

  1. 避免在 build 中做复杂/耗时的业务逻辑
  // Don’t

Future isReady() async{
   final cache = await DefaultCacheManager().getFileFromCache(assetUrl);
   return cache?.file.path.isNotEmpty == true;
}

const MyPage extends StatelessWidget {
  @override
  Widget build(context) {
    if (isReady()) {
    // do something
    }
  }
}

// Do
bool get isReady {
  return cachePath?.isNotEmpty == true && isValid;
}

Future preloadAsset() async {
  final cache = await DefaultCacheManager().getFileFromCache(assetUrl);
  cachePath = cache?.file.path;
}

const MyPage extends StatelessWidget {

  @override
  void initState() {
   super.initState();
   preloadAsset();
  }

  @override
  Widget build(context) {
    if (isReady) {
    // do something
    }
  }
}

  1. 合理使用StatefulWidget

通过区分场景适当选择StatefulWidget/StatelessWidget,避免滥用StatefulWidget

  1. 使用状态管理框架

  • Provider
  • Riverpod
  1. 函数判断优先级

    1. 子widget逻辑在子widget里判断
  // Don’t
const MyPage extends StatelessWidget {
  bool isShowTopBar;
  @override
  Widget build(context) {
    return Column(
      children: const [   
        if(isShowTopBar) TopBar(),
        MyWidget1(),
        MyWidget2(),
      ],
    );
  }
}

  // Do
const MyPage extends StatelessWidget {

  @override
  Widget build(context) {
    return Column(
      children: const [    
        TopBar(),
        MyWidget1(),
        MyWidget2(),
      ],
    );
  }
}

const TopBar extends StatelessWidget {

  @override
  Widget build(context) {
    return if(model != null) ? Column(
      children: const [
        MyWidget(),
      ],
    ):const SizedBox.shrink();;
  }
}

  1. 组合判断,把简单的放在前面

举一个例子:

image.png

为什么红色的要改为绿色的?

对简单的逻辑,我们要放前面,不要混在一起,看似兼容性更强实际降低了部分场景的性能。

Flutter 最佳实践和技巧

  1. 文件使用相对导入lib

当同时使用相对和绝对导入时,当同一个类从两种不同的方式导入时,可能会造成混淆。为了避免这种情况,我们应该在文件夹中使用相对路径lib/

// Don't
import 'package:demo/src/utils/dialog_utils.dart';

// Do
import '../../../utils/dialog_utils.dart';

  1. 指定类成员的类型

当成员的值类型已知时,尽可能避免使用var

//Don't
var item = 10;
final car = Car();
const timeOut = 2000;

//Do
int item = 10;
final Car bar = Car();
String name = 'john';
const int timeOut = 20;

为什么是已知类别,避免使用var?

对于全局变量、局部变量、静态属性或成员属性,通常可以通过初始值推断出来它们的类型,但是如果没有初始化值的话会导致类型推断失败。

// Don't
var parameters; 
if (node is Constructor) { 
  parameters = node.signature; 
} else if (node is Method) {
  parameters = node.parameters;
}

//Do
List parameters; 
if (node is Constructor) {
  parameters = node.signature; 
} else if (node is Method) {
  parameters = node.parameters; 
} 

  1. 避免使用as,使用is运算符

在 dart 中 as 更像是一个断言,如果值类型不匹配 as 会导致运行时异常。通常,as如果无法进行强制类型转换,则类型转换运算符会引发异常。为避免引发异常,可以使用is

//Don't
(item as Animal).name = 'Lion';

//Do
if (item is Animal)
  item.name = 'Lion';

  1. 使用???.

//Don't
v = a == null ? b : a;

//Do
v = a ?? b;

//Don't
v = a == null ? null : a.b;

//Do
v = a?.b;

  1. 使用级联运算符

如果我们想对同一个对象执行一系列操作,那么我们应该使用 Cascades(..) 运算符。

// Don't
var path = Path();
path.lineTo(0, size.height);
path.lineTo(size.width, size.height);
path.lineTo(size.width, 0);
path.close();  

// Do
var path = Path()
..lineTo(0, size.height)
..lineTo(size.width, size.height)
..lineTo(size.width, 0)
..close(); 

  1. 使用 async/await 过度使用futures回调

异步代码很难阅读和调试。同步代码async/await提高了可读性。

// Don’t
Future countActiveUser() {
  return getActiveUser().then((users) {
    return users?.length ?? 0;
  }).catchError((e) {
    log.error(e);
    return 0;
  });
}

// Do
Future countActiveUser() async {
  try {
    var users = await getActiveUser();

    return users?.length ?? 0;

  } catch (e) {
    log.error(e);
    return 0;
  }
}

  1. 在Widget中使用 Const

Widget在 setState 调用时不会改变时,我们应该将其定义为常量。这样避免不必要的build,从而提高性能。

  1. 使用 ListView.builder 获取长列表

在使用无限列表或非常大的列表时,通常建议使用ListView构建器以提高性能。

默认ListView构造函数一次构建整个列表。ListView.builder创建一个惰性列表,当用户向下滚动列表时,Flutter 会按需构建小部件。

参考

Flutter — Why you should use small widgets

Flutter: Best Practices and Tips

你可能感兴趣的:(如何编写地道的 Flutter 代码)