Home app responsive,一个更简单的响应式框架
Post
Cancel

app responsive,一个更简单的响应式框架

app_responsive 是基于google provider的响应式框架,本框架主要目的是更简化更方便的处理UI与controller(逻辑)之间的关系。

为什么要造轮子

我们先来看看provider有哪些问题或者有哪些不方便的地方:

  1. 代码量偏多。在使用prodiver时,布局中需要包含Prodiver widget、Consumer,以及颗粒度更细的Selector。它们不仅使页面布局显得很臃肿,而且基本上每个页面都要重复的写这些代码。这在日常的开发中,很不方便。
  2. prodiver 仅仅负责UI的控制,对逻辑层并未涉及。比如,我们的页面,加载数据、刷新、分页时的UI呈现与逻辑层强关联时,都得手动实现逻辑,且也是基本每个页面都需要的。能不能封装一下。
  3. 页面间的数据共享。使用provider做数据共享,一般需要提前在MaterialApp外包裹指定数据类型的provider,这种方式比较死板、不够灵活。可不可以做到动态的共享数据。

现在来看看本框架是如何解决这些痛点的,主要分三个方面来表述:页面UI刷新控制、页面数据加载逻辑的封装、页面间的数据共享。

本框架不仅是响应式UI框架,针对逻辑层也做了相应的封装。一个完整的页面通常由Istate、IController两个角色组合完成。

阐述

1. 页面UI刷新控制

看下面的动图,使用我们的框架是怎样实现的

首先,每个页面必不可少的IController:

1
2
3
4
5
6
7
8
9
10
11
class ExampleController extends IController {
  String text = "android";

  @override
  Future<int> load([int page]) async {
    Future.delayed(Duration(seconds: 1), () {
        text = "flutter";
        get<PPage>().notify();
    });
  }
}

load()方法会在State.initState之后自动触发

再看看布局是怎么写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ExampleState extends IState<ExamplePage, ExampleController> {
  final ExampleController _controller = ExampleController();

  @override
  Widget buildChild(BuildContext context) {
    return Scaffold(
	appBar: AppBar(
	  title: Text("app responsive demo"),
	),
	body: buildBody.watch<PPage>()(context),
    );
  }

  Widget buildBody(BuildContext context) {
    return Center(
	  child: Text(controller.text),
    );
  }

  @override
  ExampleController get controller => _controller;
}

通过buildBody.watch<PPage>()(context)buildBody代表的widget将被纳入PPage的控制范围。当我们触发get<PPage>.notify()时,PPage范围内的widget就会自动刷新了。

PPage是框架自带的一个UI控制点。它的基类Level是所有UI控制点的基类。一个控制点,可以管理它范围内的Widget。通过.watch<PPage>()的方式将Widget纳入自己的管理范围内。

框架自带的UI控制点有:PPage、Load、Scope、Child。本质上都是一样的继承自Level, 但我们约定它们控制范围大小关系:PPage > Load > Scope > Child。

若使用了除PPage、Load之外的Level, 需要先IController.useLevel激活。

2. 页面的数据加载逻辑封装

数据的加载、刷新、分页、加载空数据

上面的动图,展示了页面的首次加载、空数据情况下的UI、刷新、分页。这些个功能,从头开始写也是很烦人的。看看框架是怎么写的。

首先,看看IController:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class ExampleController extends IController {
  List<String> data = [];
  int httpRows = 50;

  @override
  Future<int> load([int page]) async {
     final newData = await _load(page);
     if (newData == null) {
        return computeErrorState(page);
     }
     return computeLoadingState(data, newData, page, pageRows: httpRows);
  }

  /// 模拟网络请求
  Future<List<String>> _load(int page) async {
    await Future.delayed(Duration(seconds: 2));

    if (page > 2) return [];

    int size = httpRows;

    return List.generate(size, (index) {
      return ((page - 1) * httpRows + index + 1).toString();
    });
  }
}

load方法中的page就是当前的页数,它是自动管理的,无需手动修改。最关键的是load方法的返回值,根据返回的状态,框架会自动切换到相应的UI呈现上。

computeLoadingState、computeErrorState是框架提供的帮助计算当前状态的方法。我们也可以直接返回LoadState中的某个状态值,如:LoadState.loaded

如图一,最终返回的状态是LoadState.loaded | LoadState.moreLoad,此时loading...UI就会切换到数据显示上,当我们触发刷新、或者分页时,框架会自动触发相应的loading UI,同时自动调用Icontroller.load(currentPage)。而这些过程,都无需再写代码实现了。

如图二,如果接口未加载到数据,load方法直接返回LoadState.empty, 此时页面UI就会如图二所示了。

看看布局的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ExampleState extends IState {

  ...

  @override
  Widget buildChild(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("app responsive demo"),
      ),
      body:buildBody.load(refresh: true, loadMore: true).watch<PPage>()(context),
    );
  }

  Widget buildBody(BuildContext context) {
    return ListView.separated(
        itemBuilder: (_, index) => ListTile(title: Text(controller.data[index]),),
        separatorBuilder: (_, __) => Divider(height: 1),
        itemCount: controller.data.length);
  }

}

通过buildBody.load(refresh: true, loadMore: true)方法,就能支持上面描述的所有功能了,就这么简单。

load方法的入参refresh、loadMore, 可以控制页面是否支持刷新、分页功能。

3. 页面间数据共享

要实现页面间的数据共享,需要在MaterialApp外包裹AppProvider,无其他要求。如这样:

1
2
3
4
5
6
7
8
@override
Widget build(BuidContext context) {
    return AppProvider(
      child: MaterialApp(
      	...
      ),
    );
}

3.1 共享当前数据

共享页面数据,一般共享的是 icontroller实例,在IController中:

1
2
3
4
5
6
7
8
9
10
11
12
13
class ExampleController extends IController {

  @override
  mount(IState state) {
    super.mount(state);
    get<PPage>().exposeToApp(buildContext);
    /// 或者
    AppProvider.expose(context, this);
  }

  ...

}

PPage.exposeToApp本质上也是调用的AppProvider.expose.

这样,我们就把IController实例分享了出来。下面看看其他的页面是如何获取的?

3.2. 获取其他页面数据

上面动图展示了page2 获取前一个页面的数据。在3.1中,展示了如何将数据共享出来,现在看看如何获取其他页面的数据:

1
2
3
4
5
Widget buildBody(BuildContext context) {
  return Center(
    child: "来自第一页的数据: ${Text(AppProvider.get<ExampleController>(context).text)}",
  );
}

可以看到,通过AppProvider.get<ExampleController>(context)即可获取到指定类型的IController了。

3.3. 监听其他页面的数据变化

上面展示了如何获取其他页面的数据,当然,也可以监听它数据的变化,通过:

1
2
3
4
5
6
7
8
9
10
class ... extends IController {

  @override
  mount(IState state) {
    super.mount(state);
    AppProvider.watch<ExampleController, PPage>(this);
  }

  ...
}

通过AppProvider.watch<ExampleController, PPage>, 监听其他页面的ExampleControllerPPage的变化。一旦被监听页面中的PPage有通知,当前页也会相应的刷新。

仓库

项目的仓库地址:app_responsive

This post is licensed under CC BY 4.0 by the author.