使用Flutter App Template Generater快速开发应用

如何快速、优雅的创建应用,减少敲代码的时间,减少后期维护的时间,一直都是码农们追求的目标之一。我们做我们所想的,创造性的工作,但是我们不想重复敲同样的代码。
前段时间介绍了Flutter App Template Generater的基本功能,现在用它来创建一个像样的例子。基于 Unsplash Api来浏览图片的例子。先来几张图:

discover dark

collection list dark

settings dark

初始化工程

  1. 安装插件到Android Studio。
  2. 用Android Studio生成一个项目photo。
  3. 右击项目目录的lib,选择New --> Generate App Template。
  4. 点击“init project”按钮,这样就初始化完成。

创建主页面HomeView

  1. 同样右击lib打开插件。
  2. 在PageName输入Home,选择BottomTabBar来生成底部导航栏。在Model Entry Name输入数据类名User,从 Unsplash User Api复制一个User对象的json数据到json编辑框,点击OK进行下一步。
  3. 在类的编辑对话框中,把User类的id设为unique,必须有一个唯一的属性,因为是根据这个唯一的属性来做查找的。为时间类型的字段选择Datetime类型,点击生成。

创建第一个页面DiscoverView

  1. 同样右击lib打开插件。
  2. 在Page Name输入"Discover" ,选择: Query、AppBar、TopTabBar、ActionButton、Search。在Model Entry Name输入Photo,从 Unsplash Photo Api复制Photo的json对象到编辑区。点击OK进行下一步。
  3. 设置id为unique,生成,前面生成的类,再次生成会询问要不要覆盖的。.

创建第二个页面CollectListView

打开插件,输入页面名称CollectList,选择基本界面元素Query、AppBar、ListView、ActionButton、Search。输入数据类名Collection,从Unsplash Collection Api复制Collection数据对象。点击OK进行下一步生成页面。

创建第三个页面MeView

打开插件,输入Me和选择想要的界面元素。选择UI only,因为前面已经生成了User类,无需再生成。但是还是要再Model Entry Name中输入User,生成页面。

修改页面导航

main.dart

Map<String, WidgetBuilder> _routes() {
   return <String, WidgetBuilder>{
     "/settings": (_) => SettingsOptionsPage(
           options: _options,
           onOptionsChanged: _handleOptionsChanged,
         ),
     "/": (_) => new HomeView(),
   };
 }

home_view.dart

   widget = PageView(
       children: <Widget>[DiscoverView(), CollectListView(), MeView()],

配置服务器信息

根据Unsplash Public Action Api 修改服务器地址和Clent-ID,network_common.dart

    // address
    dio.options.baseUrl = 'https://api.unsplash.com/';
    
    // authentication info
    options.headers["Authorization"] =
          "Client-ID xxxxxxxxx";

创建最新Photo页面

打开插件,输入页面名称Photo,选择UI only,Query,ListView。输入medel名称Photo,生成页面。

编辑photo网络接口

根据Unsplash list-photo Api 的指示修改photo_repository.dart

   Future<Page> getPhotosList(String sorting, int page, int limit) {
    return new NetworkCommon().dio.get("photos", queryParameters: {
      "order_by": sorting,
      "page": page,
      "per_page": limit
    }).then((d) {
      var results = new NetworkCommon().decodeResp(d);
      Page page = new NetworkCommon().decodePage(d);
      page.data =
      results.map<Photo>((item) => new Photo.fromJson(item)).toList();
      return page;
    });
  }

修改中间件

编辑photo_middleware.dart

Middleware<AppState> _createGetPhotos(PhotoRepository repository) {
  return (Store<AppState> store, dynamic action, NextDispatcher next) {
    if (checkActionRunning(store, action)) return;
    running(next, action);
    int num = store.state.photoState.page.next;
    if (action.isRefresh) {
      num = 1;
    } else {
      if (store.state.photoState.page.next <= 0) {
        noMoreItem(next, action);
        return;
      }
    }
    repository.getPhotosList(action.orderBy, num, 10).then((page) {
      next(SyncPhotosAction(page: page, photos: page.data));
      completed(next, action);
    }).catchError((error) {
      catchError(next, action, error);
    });
  };
}

编辑photo_actions.dart

// add orderBy property
class GetPhotosAction {
  final String actionName = "GetPhotosAction";
  final bool isRefresh;
  final String orderBy;

  GetPhotosAction({this.orderBy, this.isRefresh});
}

编辑 photo_view.dart

我们使用下面两个插件来显示photo列表,在pubspec.yaml引入它们。

  cached_network_image: ^0.7.0
  flutter_staggered_grid_view: ^0.2.7

修改 photo_view.dart

// define orderBy property in PhotoView
class PhotoView extends StatelessWidget {
  final String orderBy;

//build()
    widget = NotificationListener(
        onNotification: _onNotification,
        child: RefreshIndicator(
            key: _refreshIndicatorKey,
            onRefresh: _handleRefresh,
            child: new StaggeredGridView.countBuilder(
              controller: _scrollController,
              crossAxisCount: 2,
              itemCount: this.widget.viewModel.photos.length,
              itemBuilder: (_, int index) => _createItem(context, index),
              staggeredTileBuilder: (int index) => new StaggeredTile.fit(1),
              mainAxisSpacing: 0.0,
              crossAxisSpacing: 0.0,
            )));
// _createItem()
  _createItem(BuildContext context, int index) {
    if (index < this.widget.viewModel.photos?.length) {
      return Container(
          padding: EdgeInsets.all(2.0),
          child: Stack(
            children: <Widget>[
              Hero(
                tag: this.widget.viewModel.photos[index].id,
                child: InkWell(
                  onTap: () => Navigator.push(
                        context,
                        MaterialPageRoute(
                          builder: (context) =>
                              ViewPhotoView(id: 0, pageIndex: index),
                        ),
                      ),
                  child: new CachedNetworkImage(
                    imageUrl: this.widget.viewModel.photos[index].urls.small,
                    placeholder: (context, url) =>
                        new CircularProgressIndicator(),
                    errorWidget: (context, url, error) => new Icon(Icons.error),
                  ),
                ),
              ),
            ],
          ),
          decoration: BoxDecoration(
              border: Border(
                  bottom: BorderSide(color: Theme.of(context).dividerColor))));
    }

添加属性orderBy到getPhotos中。
photo_view_model.dart

  final Function(bool, String) getPhotos;
 
      getPhotos: (isRefresh, orderBy) {
        store.dispatch(GetPhotosAction(isRefresh: isRefresh,orderBy: orderBy));
      },

添加PhotoView 到 DiscoverView

  TabController _controller;
  List<String> _tabs = [
    "Latest",
 //will add some photos of some collection here
  ];
  List<int> _views = [
    0,
  ];
 
 // init TabController
  TabController _makeNewTabController() => TabController(
        vsync: this,
        length: _tabs.length,
      );

  Widget build(BuildContext context) {
    var widget;

    widget = TabBarView(
      key: Key(Random().nextDouble().toString()),
      controller: _controller,
      children: _views.map((id) {
        if (id == 0) {
          return PhotoView(orderBy: "latest");
        } else {
          return CollectionView(collection: id);
        }
      }).toList(),
    );
    return Scaffold(
      appBar: AppBar(
        bottom: TabBar(
          controller: _controller,
          isScrollable: true,
          tabs: _tabs.map((title) => Tab(text: title)).toList(),
        ),
        title: Text("Discover"),
        actions: _buildActionButton(),
      ),
      body: widget,
    );
  }

now you can the app.

下一步创建 CollectionView 及其 中间件

添加新的Action到中间件

在 photo_state.dart 添加一个变量来存每个集合的照片。

  final Map<int, PhotoOfCollection> collectionPhotos;

在 photo_actions.dart添加新Action类

class SyncCollectionPhotosAction {
  final String actionName = "SyncCollectionPhotosAction";
  final Page page;
  final int collectionId;

  SyncCollectionPhotosAction({this.collectionId, this.page});
}

编辑 photo_reducer.dart,更新middleware传来的数据到state,以此Redux来通知UI状态改变,从而更新UI。

  TypedReducer<PhotoState, SyncCollectionPhotosAction>(_syncCollectionPhotos),
  
PhotoState _syncCollectionPhotos(
    PhotoState state, SyncCollectionPhotosAction action) {
  state.collectionPhotos.update(action.collectionId, (v) {
    v.id = action.collectionId;
    v.page?.last = action.page?.last;
    v.page?.prev = action.page?.prev;
    v.page?.first = action.page?.first;
    v.page?.next = action.page?.next;
    for (var photo in action.page?.data) {
      v.photos
          ?.update(photo.id.toString(), (vl) => photo, ifAbsent: () => photo);
    }
    return v;
  }, ifAbsent: () {
    PhotoOfCollection pc = new PhotoOfCollection();
    pc.id = action.collectionId;
    Page page = Page();
    page.last = action.page?.last;
    page.prev = action.page?.prev;
    page.first = action.page?.first;
    page.next = action.page?.next;
    pc.page = page;
    pc.photos = Map();
    for (var photo in action.page?.data) {
      pc.photos
          ?.update(photo.id.toString(), (v) => photo, ifAbsent: () => photo);
    }
    return pc;
  });
  return state.copyWith(collectionPhotos: state.collectionPhotos);
}

编辑photo_middleware.dart,捕捉UI的action,并从网络获取数据或者从本地数据库。

//add new action to list
  final getCollectionPhotos = _createGetCollectionPhotos(_repository);
  
    TypedMiddleware<AppState, GetCollectionPhotosAction>(getCollectionPhotos),

// new handle function
Middleware<AppState> _createGetCollectionPhotos(PhotoRepository repository) {
  return (Store<AppState> store, dynamic action, NextDispatcher next) {
    if (checkActionRunning(store, action)) return;
    running(next, action);
    int num =
        store.state.photoState.collectionPhotos[action.id]?.page?.next ?? 1;
    if (action.isRefresh) {
      num = 1;
    } else {
      if (store.state.photoState.collectionPhotos[action.id] != null &&
          store.state.photoState.collectionPhotos[action.id].page.next <= 0) {
        noMoreItem(next, action);
        return;
      }
    }
    repository.getCollectionPhotos(action.id, num, 10).then((page) {
      next(SyncCollectionPhotosAction(collectionId: action.id, page: page));
      completed(next, action);
    }).catchError((error) {
      catchError(next, action, error);
    });
  };
}

添加新的Photo api,Unsplash collection api

  Future<Page> getCollectionPhotos(int id, int page, int limit) {
    return new NetworkCommon().dio.get("collections/${id}/photos", queryParameters: {
      "page": page,
      "per_page": limit
    }).then((d) {
      var results = new NetworkCommon().decodeResp(d);
      Page page = new NetworkCommon().decodePage(d);
      page.data =
          results.map<Photo>((item) => new Photo.fromJson(item)).toList();
      return page;
    });
  }

创建CollectionView

打开插件,输入页面名Collection,选择UI only,Query,Listview。输入Photo到Model Entry Name。生成页面。
编辑collection_View.dart

// add a int property in CollectionView to specify collection id
  final int collection;
  
  edit widget
    widget = NotificationListener(
        onNotification: _onNotification,
        child: RefreshIndicator(
            key: _refreshIndicatorKey,
            onRefresh: _handleRefresh,
            child: new StaggeredGridView.countBuilder(
              controller: _scrollController,
              crossAxisCount: 2,
              itemCount: this.widget.viewModel.photos.length + 1,
              itemBuilder: (_, int index) => _createItem(context, index),
              staggeredTileBuilder: (int index) => new StaggeredTile.fit(1),
              mainAxisSpacing: 0.0,
              crossAxisSpacing: 0.0,
            )));

// modify list item
  _createItem(BuildContext context, int index) {
    if (index < this.widget.viewModel.photos?.length) {
      return Container(
          padding: EdgeInsets.all(2.0),
          child: Stack(
            children: <Widget>[
              Hero(
                tag: this.widget.viewModel.photos[index].id,
                child: InkWell(
                  onTap: () => Navigator.push(
                        context,
                        MaterialPageRoute(
                          builder: (context) => ViewPhotoView(
                              id: this.widget.collection, pageIndex: index),
                        ),
                      ),
                  child: new CachedNetworkImage(
                    imageUrl: this.widget.viewModel.photos[index].urls.small,
                    placeholder: (context, url) =>
                        new CircularProgressIndicator(),
                    errorWidget: (context, url, error) => new Icon(Icons.error),
                  ),
                ),
              ),
            ],
          ),
          decoration: BoxDecoration(
              border: Border(
                  bottom: BorderSide(color: Theme.of(context).dividerColor))));
    }

    return Container(
      height: 44.0,
      child: Center(
        child: _getLoadMoreWidget(),
      ),
    );
  }

修改collection_view_model.dart

  final Function(bool) getPhotoOfCollection;
  
  static CollectionViewModel fromStore(Store<AppState> store, int id) {
    return CollectionViewModel(
      photos: store.state.photoState.collectionPhotos[id]?.photos?.values
          ?.toList() ?? [],
      getPhotoOfCollection: (isRefresh) {
        store.dispatch(GetCollectionPhotosAction(id: id, isRefresh: isRefresh));
      },

添加一些collection 到 Discover page

  List<String> _tabs = [
    "Latest",
    "Wallpapers",
    "Textures",
    "Rainy",
    "Summer",
    "Flowers",
    "Women",
    "Home",
    "Oh Baby","Work","Winter","Animals"
  ];
  List<int> _views = [
    0,
    151521,
    175083,
    1052192,
    583479,
    1988224,
    4386752,
    145698,
    1099399,385548,3178572,181581
  ];

或者,通过接口从服务器获取

  List<int> getTabPPage() {
    List<int> list = [];
    list.add(0);
    for (var c in this.widget.viewModel.collections) {
      list.add(c.id);
    }

    return list;
  }

  List<String> getTab() {
    List<String> list = [];
    list.add("latest");
    for (var c in this.widget.viewModel.collections) {
      list.add(c.title ?? "");
    }

    return list;
  }

创建photo Viewer page

打开插件,输入ViewPhoto,选择UI only,输入Photo作为Model Entry Name,生成。
Add two property to ViewPhotoView

  final int id; //collection id
  final int pageIndex; //the index of photo in the list
  
  // build()
      widget = Container(
        child: PhotoViewGallery.builder(
      scrollPhysics: const BouncingScrollPhysics(),
      builder: (BuildContext context, int index) {
        return PhotoViewGalleryPageOptions(
          imageProvider: CachedNetworkImageProvider(
              this.widget.viewModel.photos[index].urls.small),
          initialScale: PhotoViewComputedScale.contained * 0.8,
          heroTag: this.widget.viewModel.photos[this.widget.pageIndex ?? 0].id,
        );
      },
      itemCount: this.widget.viewModel.photos.length,
      loadingChild: new CircularProgressIndicator(),
      pageController: _pc,
    ));

编辑view_photo_view_model.dart,添加数据源

    return ViewPhotoViewModel(
      photos: id == 0
          ? store.state.photoState.photos.values.toList() ?? []
          : store.state.photoState.collectionPhotos[id]?.photos?.values
                  ?.toList() ??
              [],
    );

添加点击时间到photo列表项:collection_view.dart and photo_view.dart

                child: InkWell(
                  onTap: () => Navigator.push(
                        context,
                        MaterialPageRoute(
                          builder: (context) => ViewPhotoView(
                              id: this.widget.collection, pageIndex: index),
                        ),
                      ),

添加 Setting page到Me page
编辑me_view.dart

    widget = RaisedButton(
      child: Text("Settings"),
      onPressed: () => Navigator.of(context).pushNamed("/settings"),
    );

到此,一个应用的雏形已经呈现出来了,如此简单。
Check the source code for the detail.

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

推荐阅读更多精彩内容

  • 使用网站提供的API爬取网站中的数据是一种操作较为简单直接的方式,例如豆瓣网提供了对于电影、书籍等资源的各种数据的...
    Carina_55阅读 2,181评论 0 0
  • 大四了~要去实习,要忙论文,要相亲… 大学几年的生活,让我成功的从精致的小仙女蜕变为粗糙女汉子。作为大四老学姐,在...
    笙笳爱吃糖阅读 649评论 0 15
  • 001 不要轻易改变列表 一旦你做好计划,不要轻易去改变,否则开了一个头以后就一发不可收拾,可能到最后这个计划被你...
    淡茈阅读 214评论 0 0
  • 不能心安理得 那就或悲鸣或己律吧
    RWn阅读 142评论 0 1