日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

flutter刷新页面_用Flutter实现58App的首页

發布時間:2023/12/15 编程问答 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 flutter刷新页面_用Flutter实现58App的首页 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

背景

Flutter作為全新跨平臺應用框架,在頁面渲染和MD開發上優勢明顯,可謂是業界一枝獨秀。正好最近有這樣的一個機會學習Flutter開發,我們便嘗試用它開發一個MD風格的較復雜頁面,來比較跟原生應用開發的優勢。也是想通過對新框架的學習探索,找到適合自身應用的框架。

頁面展示

首頁是整個應用里邊交互最為復雜的一個頁面了,它集合了各種滑動方式,包括:縱向滑動、橫向滑動、嵌套滑動;同時,也集合了各種動效,包括:下拉刷新、上拉加載、頭圖視差、二級吸頂、回到頂部、橫向Banner和縱向News輪播等。

開發歷程

  • 搭建了開發環境,新建flutter module并學習dart語法

  • 調研用Flutter實現CoordinatorLayout的方案

  • 實現了首頁主框架的demo搭建,目前同樣遇到了滑動沖突的問題,在調研解決方案

  • 解決了滑動沖突的問題,并集成了下拉刷新能力

  • 完成了各區塊和feed流的靜態UI內容,目前剩余feed流加載更多和負二樓動效

  • 實現首頁feed流的加載更多功能

技術難點

兩級吸頂

在Flutter中實現吸頂功能比較容易,使用SliverPersistentHeader控件或者間接使用該控件都可以滿足吸頂的功能;更重要的是,它支持滑動過程中任意組件的吸頂,即多級吸頂功能。

既然多級吸頂都支持,那么兩級吸頂就很輕松了,首頁頭部和feed流tab的兩級吸頂是這樣實現的:第一級,使用SliverAppBar(它內部就是一個SliverPersistentHeader控件),不僅可以吸頂,還帶有折疊屬性,折疊屬性能更好的滿足頭部滑動時的動效處理;第二級,使用SliverPersistentHeader并自定義它的delegate,通過pinned屬性靈活選擇當前模塊吸頂與否,這樣可以實現任意組件的吸頂功能。

SliverAppBar(
pinned: true,
...,
bottom: PreferredSize(
child: Header(...),
preferredSize: Size(screenWidth, 15),
),
),
SliverPersistentHeader(
pinned: false,
delegate: _SliverColumnDelegate(
Column(...),
)
),
SliverPersistentHeader(
pinned: true,
delegate: _SliverTabBarDelegate(
TabBar(...)
),
),

pinned的原理很簡單,將它設置為true內容到達頂部后不會再跟隨外層的ScrollView繼續滾動;反之,內容則會滾動出容器外。

而native端實現這個二級吸頂卻很費力,通常你可能需要事先隱藏一個跟吸頂內容一樣的駐頂view在那里,然后在頁面滾動時計算吸頂內容是否已經劃至頂部,維護駐頂view的可見屬性達到吸頂效果。

上面粗獷的兩級吸頂完成了,但想要充分滿足首頁的折疊效果和準確的二級吸頂需求,還得深挖一下AppBar內部的折疊計算方法。

SliverAppBar折疊計算

SliverAppBar通常作為頁面頭部使用,是會隨內容一起滑動的一個組件;它的構造方法中有四個Widget類型的參數。分析Widget類型的參數,是因為我們需要一個容器來滿足自定義首頁頭部——它既能實現吸頂,又可以接入自定義組件。

  • leading // 左側按鈕

  • title // 標題

  • flexibleSpace // 可以展開區域

  • bottom // 底部內容區域

回顧一下首頁的折疊展示效果,首先排除了leading,因為它的位置大小只是一個按鈕的位置,顯示比較局限;然后title受leading占位影響寬度有限制也無法滿足需要;之后,就剩下兩個參數可選了,從命名上看,感覺flexibleSpace更符合折疊效果的實現思路,然后一直在嘗試使用其實現頭部折疊的需求,但開發過程中發現折疊后的高度是無法達到預期的,最大高度也滿足不了設計圖給的高度。本來想直接排除法使用起bottom的,但是想到一遇到問題就繞過還是有點SUI。那么想知道為什么flexibleSpace會有高度限制,必然得看一下SliverAppBar的實現源碼了。

class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMixin {
...
@override
Widget build(BuildContext context) {
assert(!widget.primary || debugCheckHasMediaQuery(context));
final double topPadding = widget.primary ? MediaQuery.of(context).padding.top : 0.0;
final double collapsedHeight = (widget.pinned && widget.floating && widget.bottom != null)
? widget.bottom.preferredSize.height + topPadding : null;

return MediaQuery.removePadding(
context: context,
removeBottom: true,
child: SliverPersistentHeader(
floating: widget.floating,
pinned: widget.pinned,
delegate: _SliverAppBarDelegate(
...
collapsedHeight: collapsedHeight,
topPadding: topPadding,
),
),
);
}
}

final double collapsedHeight = (widget.pinned && widget.floating && widget.bottom != null)
? widget.bottom.preferredSize.height + topPadding : null;

變量collapsedHeight代表了折疊后頭部的高度,從它的計算表達式可看出:當widget.bottom == null的時候,collapsedHeight的值為null。換言之,如果不使用bottom,那么折疊高度是沒有的。如果沒有折疊后的高度會發生什么?這個需要進一步驗證。

const double kToolbarHeight = 56.0;

class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
...
final double _bottomHeight = bottom?.preferredSize?.height ?? 0.0;
@override
double get minExtent => collapsedHeight ?? (topPadding + kToolbarHeight + _bottomHeight);
}

從上面的源碼看,如果collapsedHeight == null,那么折疊后的頭部高度就是topPadding + kToolbarHeight了。topPadding是系統狀態欄的高度,kToolbarHeight是個自定義常量。不難看出,bottom為空時折疊頭部的高度就會是一個固定高度。那么反過來,想要自定義高度,就必須得使用bottom,折疊后的頭部高度完全取決于bottom的高度(一般,系統狀態欄的高度是確定的)。

你看,不是我們用排除法用了最后一個參數bottom,而是我們分析后知道不用它真得不行。

實現兩級吸頂并明確了頭部參數設置后,其實整個頁面框架就基本擬定了。接下來,我們細化一下,看看頭部控件具體怎么實現。

自定義頭部

首頁頭部組件包括以下內容:

  • 搜索欄和城市名吸頂

  • 頭圖視差

  • 基于之前首頁native的開發經驗,這些效果的實現其實可以由一個變量驅動完成,即首頁頭部的縱向滑動偏移值。這個偏移值參照它的初始位置,分為上偏移和下偏移。上偏移驅動處理搜索欄和城市名的動效,下偏移則驅動處理頭圖視差的動效。

    通過自定義Header組件來處理搜索欄和城市名吸頂的動畫,其中主要是借助外部傳入的上偏移值驅動整個動畫的完成。

    import 'package:flutter/material.dart';
    import 'package:wuba_flutter_lib/home/search_bar.dart';

    const double SEARCH_MARGIN_LEFT = 15.0; // 搜索欄left居左位置

    typedef OnOffsetChangeListener = Function(double percent);

    class Header extends StatefulWidget {

    Header({
    Key key,
    this.offset: 0.0,// 外部驅動的偏移屬性
    this.cityName,
    this.onOffsetChangeListener,
    }) : super(key: key);

    final double offset;
    final String cityName;
    final OnOffsetChangeListener onOffsetChangeListener;

    double searchLeft = SEARCH_MARGIN_LEFT;
    double searchLeftEnd = SEARCH_MARGIN_LEFT;

    @override
    State<StatefulWidget> createState() => HeaderState();
    }

    class HeaderState extends State<Header> with TickerProviderStateMixin {

    AnimationController searchBgColorAnimController;
    Animation<Color> searchBgColor;

    // 偏移值驅動動畫屬性
    drive(offset) {
    // 過渡比例
    double percent = offset / 80.0 > 1.0 ? 1.0 : offset / 80.0;
    // 偏移比例回調
    if (widget.onOffsetChangeListener != null) {
    widget.onOffsetChangeListener(percent);
    }
    // 搜索欄居左吸頂后的位置
    widget.searchLeftEnd = SEARCH_MARGIN_LEFT + (widget.cityName ?? '').length * 22 + CITY_MARGIN_RIGHT;
    // 搜索欄居左位置
    widget.searchLeft = (SEARCH_MARGIN_LEFT + (widget.searchLeftEnd - SEARCH_MARGIN_LEFT) * percent);
    // 背景顏色控制
    searchBgColorAnimController.value = percent;
    }

    @override
    void didUpdateWidget(Header oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.offset != oldWidget.offset) {
    drive(widget.offset);
    }
    }

    @override
    void initState() {
    super.initState();
    searchBgColorAnimController = new AnimationController(vsync: this);
    searchBgColor = ColorTween(
    begin: Color(0xffffffff),
    end: Color(0xffDADDE1),
    ).animate(
    CurvedAnimation(
    parent: searchBgColorAnimController,
    curve: Interval(0.0, 1.0, curve: Curves.linear),
    ),
    );
    }

    @override
    Widget build(BuildContext context) {
    return Stack(
    overflow: Overflow.visible,
    children: <Widget>[
    // 搜索欄
    SearchBar(
    left: widget.searchLeft,
    bgColor: searchBgColor.value,
    ...
    ),
    ...
    ],
    );
    }

    }

    頭圖視差 則使用了Container的矩陣變換屬性,主要是對y軸進行位移,這個位移加以視差系數便能產生跟Header組件的視差效果。

    // 矩陣
    Matrix4 matrix = Matrix4.translationValues(0.0, _offset/*驅動y軸偏移*/, 0.0);

    // 容器
    Container(
    transform: matrix,// 矩陣變換
    width: screenWidth,
    height: screenWidth,
    child: Image.asset("assets/images/home_bg.jpg", fit: BoxFit.fill)
    ),

    組件化思考

    Flutter中分無狀態組件StatelessWidget和有狀態組件StatefulWidget,React中分無狀態組件Stateless Component和高級組件Stateful Component,它們在組件化方面的設計思路是一樣的。

    組件化?越來越趨向于按狀態劃分設計,因為這樣更貼合實際場景并滿足需要。比如首頁的區塊列表場景中,有一些區塊一旦設置后不會再發生狀態改變,可理解為無狀態的;而另有一些區塊初始化后還需要做狀態變更,它有了狀態,可視為有狀態的。無狀態的區塊和有狀態的區塊進行組件封裝,便成了無狀態組件和有狀態組件。

    首頁區塊中,無狀態的組件主要包括:

    • BigGroup,大類頁

    • SmallGroup,小類頁

    • LocalNews,同城頭條

    • LocalTribe,同城部落

    • BannerAd,廣告Banner

    有狀態的組件目前只有一個:

    • Notification,通知提醒;沒有下發通知鏈接或者請求后臺后發現沒有通知內容時需要隱藏

    如此,按照首頁區塊的場景,我們便基于無狀態組件設計封裝了首頁無狀態組件類HomeStatelessWidget,而基于有狀態組件實現了首頁有狀態組件HomeStatefulWidget。

    HomeStatelessWidget類封裝,內部設有一個容器,然后需要指定它的大小,僅此而已。

    abstract class HomeStatelessWidget<T> extends StatelessWidget implements PreferredSizeWidget {

    HomeStatelessWidget(this.context, this.key, this.value);

    final BuildContext context;
    final String key;
    final T value;

    Widget get child;
    double get height;

    @override
    Size get preferredSize {// 指定容器大小
    return Size(MediaQuery.of(context).size.width, height);
    }

    @override
    Widget build(BuildContext context) {
    return Container(// 容器
    width: preferredSize.width,
    height: preferredSize.height,
    color: Colors.white,
    alignment: Alignment.centerLeft,
    child: child,
    );
    }
    }

    HomeStatefulWidget類封裝,和HomeStatelessWidget類近似,只是多了一個狀態類HomeStatefulWidgetState,它用于管理組件的各種狀態。

    abstract class HomeStatefulWidget<T> extends StatefulWidget implements PreferredSizeWidget {

    HomeSizeStatefulWidget(this.context, this.key, this.value);

    final BuildContext context;
    final String key;
    final T value;

    Widget get child;
    double get height;

    void initState(State state) {}

    @override
    Size get preferredSize {
    return Size(MediaQuery.of(context).size.width, height);
    }

    @override
    State<StatefulWidget> createState() => HomeStatefulWidgetState();
    }

    // 狀態類
    class HomeStatefulWidgetState extends State<HomeStatefulWidget> {
    @override
    void initState() {
    super.initState();
    widget.initState(this);
    }
    @override
    Widget build(BuildContext context) {
    return Container(
    width: widget.preferredSize.width,
    height: widget.preferredSize.height,
    color: Colors.white,
    alignment: Alignment.centerLeft,
    child: widget.child,
    );
    }
    }

    無狀態組件實現起來很容易,只需要給它一次性賦值就可以了,這里不做過多解釋。接下來,看看有狀態的組件是如何實現的!

    通知提醒組件因為需要改變可見性狀態,所以要實現首頁有狀態的組件類HomeStatefulWidget才能滿足狀態的管理,如下是通知提醒組件的代碼實現。

    這一點跟native相比,優勢還是很明顯的。因為native端在view的設計上沒有“狀態”這個概念,它對狀態的概念完全是模糊的。

    class Notification extends HomeStatefulWidget<String> {

    // 狀態字段,當通知內容為空時控制當前組件是否可見
    bool isContentEmpty = true;

    Notification(BuildContext context, String key, String value) : super(context, key, value);

    @override
    void initState(State<StatefulWidget> state) {
    super.initState(state);
    // 如果url不為空,則請求通知接口數據
    if (!isUrlEmpty()) {
    HomeDataManager.getNotification(value).then((object) {
    // 獲取到通知數據,改變組件的可見性狀態
    state.setState(() {
    isContentEmpty = object == null;
    });
    });
    }
    }

    @override
    Widget get child => isUrlEmpty() || isContentEmpty ? Container() : Center(child: Text(value));

    /// 如果url為空,或是通知接口返回的內容為空,則隱藏自己;
    /// 否則,顯示自己。
    @override
    double get height => isUrlEmpty() || isContentEmpty ? 0 : 40;

    // 判斷傳入的url是否為空
    bool isUrlEmpty() => (value == null || '' == value);

    }

    滑動沖突

    Android中,只要兩個“輪子”有嵌套關系,那么勢必存在滑動沖突的問題。要解決嵌套滑動沖突,就只能允許一個輪子驅動,而另一個輪子被帶動;而不是兩個輪子同時驅動

    首頁中存在兩級沖突問題,也就是說有兩層嵌套關系。一,下拉刷新和首頁主體;二,首頁主體和feed流內容。這相當于有三個輪子存在相互嵌套的關聯,如何解決三個輪子的滑動沖突問題,這里有三種思路:

  • 由一個輪子驅動,另外兩個輪子同步被帶動;

  • 由一個輪子驅動,另一個輪子被帶動,還有一個輪子卸載;

  • 由一個輪子先驅動,到達某個位置后轉換為另一個輪子驅動,然后剩下的兩個輪子跟1和2情況。

  • 三種思路其實都是將三個輪子的嵌套關系進行了降維處理,本質上都在解決兩個輪子的沖突問題;總之,核心思想是不能出現兩個輪子同時驅動。

    NestedScrollViewRefreshIndicator(// 下拉刷新
    child: ExtendedNestedScrollView(// 首頁主體
    keepOnlyOneInnerNestedScrollPositionActive: true,
    headerSliverBuilder: (c, f) {
    return <Widget>[
    SliverAppBar(), // 頭部搜索
    SliverPersistentHeader(),// 區塊列表
    SliverPersistentHeader(),// feed流TabBar
    ];
    },
    body: TabBarView(// feed流內容
    children: [
    Container(
    child: ListView(),// 推薦
    ),
    Container(
    child: ListView(),// 家鄉
    ),
    Container(
    child: ListView(),// 部落?
    ),
    ],
    ),
    ),
    )

    首頁主體控件使用了NestedScrollView的擴展類ExtendedNestedScrollView,前者允許嵌套滾動,但是對子視圖的高度有要求——確定的高度。做過feed流的開發都知道,它的高度并不好計算,因為模板類型不同對應各自的高度不等,加以本身又可以無限加載擴展,高度一直在變化計算起來難度很大。基于NestedScrollView的擴展類ExtendedNestedScrollView解決了這個痛點,在不依賴子視圖高度的情況下同樣能夠滿足嵌套滾動。

    解決滑動沖突問題,離不開它的這個重要屬性keepOnlyOneInnerNestedScrollPositionActive,直譯是僅保活一個內部嵌套的滾動位置,意譯便是僅允許一個內部嵌套的視圖滾動,即僅允許一個輪子驅動。

    if (widget.keepOnlyOneInnerNestedScrollPositionActive) {
    ///get notifications and compute active one in _innerController.nestedPositions
    body = NotificationListener<ScrollNotification>(
    onNotification: (ScrollNotification notification) {
    if (((notification is ScrollEndNotification) ||
    (notification is UserScrollNotification &&
    notification.direction == ScrollDirection.idle)) &&
    notification.metrics is PageMetrics &&
    notification.metrics.axis == Axis.horizontal) {
    _coordinator._innerController
    ._computeActivatedNestedPosition(notification);
    }
    return false;
    },
    child: body);
    }

    這里的條件判斷計算,其實已經能看出來了,它是實現了上面思路3的做法。此時,首頁的兩級嵌套滾動沖突解決方案其實已經浮出水面了,只剩下最后一個輪子的處理了,具體是使用情況1還是情況2呢?

    ScrollController _scrollController = ScrollController();

    _scrollListener() {
    setState(() {
    _offset = _scrollController.offset;
    });
    }

    @override
    void initState() {
    super.initState();
    _scrollController.addListener(_scrollListener);
    }

    NestedScrollViewRefreshIndicator(// 下拉刷新
    // _offset > 0.0 表示頭部上移動,這時候禁止notification事件處理
    notificationPredicate: (notification) => _offset == 0.0,
    child: ExtendedNestedScrollView(// 首頁主體
    controller: _scrollController,
    keepOnlyOneInnerNestedScrollPositionActive: true,
    ...
    ),
    )

    這個問題其實不是一個單選,具體在應用場景中,最終兩者都有用到。下滑到達頂部,此時需要觸發下拉刷新操作,隨即下拉刷新模塊被帶動,那么就實現了思路1的做法;而其他位置的滑動,則不會觸發這個操作,所以可以理解為將其暫時卸載,那么就有了思路2的做法。整體首頁的實現,其實是綜合應用了這三種思路。

    下拉刷新

    • 下拉高度限制

    • 負二樓 // TODO

    默認的下拉刷新組件在下拉時可以一直往下,沒有對滑動距離做限制,而首頁要求下拉至頭圖完整出現后不再滾動。這個特定的需求RefreshIndicator并不能滿足,需要改動一下這個組件才可以。

    class NestedScrollViewRefreshIndicator extends StatefulWidget {
    final OnOffsetCallback onOffset;// 下拉偏移量回調
    final double offsetLimit;// 下拉偏移的限制值
    const NestedScrollViewRefreshIndicator({
    this.onOffset,
    this.offsetLimit = 0.0,
    ...
    });
    }

    class NestedScrollViewRefreshIndicatorState
    extends State<NestedScrollViewRefreshIndicator>
    with TickerProviderStateMixin<NestedScrollViewRefreshIndicator> {

    AnimationController _positionController;
    AnimationController _scaleController;

    Animation<RelativeRect> _positionRect;
    Animation<RelativeRect> _positionRectDown;
    Animation<RelativeRect> _positionRectUp;

    Animatable _headerPositionTweenDown;
    Animatable _headerPositionTweenUp;

    double _headerOffset = 0.0;// 頭部偏移值

    @override
    void initState() {
    super.initState();
    _headerPositionTweenDown = RelativeRectTween(
    begin: RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0),
    end: RelativeRect.fromLTRB(0.0, widget.offsetLimit, 0.0, 0.0)
    );

    _positionController = AnimationController(vsync: this);
    _scaleController = AnimationController(vsync: this);

    _positionRectDown = _positionController.drive(_headerPositionTweenDown);
    _positionRect = _mode != _RefreshIndicatorMode.done ? _positionRectDown : _positionRectUp;

    if (widget.onOffset != null) {
    _positionController.addListener(() {
    _headerOffset = _positionController.value;
    widget.onOffset(_headerOffset);
    double value = widget.offsetLimit * _headerOffset;
    _headerPositionTweenUp = RelativeRectTween(
    begin: RelativeRect.fromLTRB(0.0, value, 0.0, 0.0),
    end: RelativeRect.fromLTRB(0.0, 0, 0.0, 0.0)
    );
    _positionRectUp = _scaleController.drive(_headerPositionTweenUp);
    });
    _scaleController.addListener(() {
    double value = (1.0 - _scaleController.value) * _headerOffset;
    widget.onOffset(value);
    });
    }
    }

    setPositionRect(newMode) {
    setState(() {
    _positionRect = newMode != _RefreshIndicatorMode.done ? _positionRectDown : _positionRectUp;
    });
    }

    // _RefreshIndicatorMode.drag
    bool _handleScrollNotification(ScrollNotification notification) {
    ...
    setPositionRect(_RefreshIndicatorMode.drag);
    return false;
    }

    // _RefreshIndicatorMode.canceled || _RefreshIndicatorMode.done
    Future<void> _dismiss(_RefreshIndicatorMode newMode) async {
    ...
    setPositionRect(newMode);
    switch (newMode) {
    case _RefreshIndicatorMode.done:
    await _scaleController.animateTo(1.0, duration: _kIndicatorScaleDuration);
    break;
    case _RefreshIndicatorMode.canceled:
    await _positionController.animateTo(0.0, duration: _kIndicatorScaleDuration);
    break;
    default:
    assert(false);
    }
    }

    // _RefreshIndicatorMode.refresh
    void _show() {
    ...
    _positionController.animateTo(1.0 / _kDragSizeFactorLimit, duration: _kIndicatorSnapDuration).then<void>((void value) {
    setPositionRect(_RefreshIndicatorMode.refresh);
    });
    }

    @override
    Widget build(BuildContext context) {
    return Stack(
    children: <Widget>[
    // child, // 改動前
    PositionedTransition(// 改動后
    rect: _positionRect,
    child: child,
    ),
    ...
    ],
    );
    }
    }

    以上便是摘出的改動了下拉刷新控件的代碼邏輯,主要是通過位置動畫限定了首頁主體向下滾動的最大位移。同時,通過動畫偏移的計算,向外輸出了頭部偏移的值,以便于外部通過監聽這個偏移值做更多的動效處理;比如:搜索框、天氣、城市、頭部、掃碼、背景圖等頭部元素的動畫處理。

    負二樓的效果實現其實并不復雜,能理解如何通過動畫原理改動下拉刷新控件從而實現個性化的動效,那么實現負二樓的效果就是個舉一反三的事情。

    加載更多

    加載更多的原理其實跟native的思路是一樣的——判斷列表滾動到最末位置觸發特定事件。之前native的做法就是判斷RecyclerView滑動到最后一項時向feed流最末位置插入一個特定的動畫模板,等加載結束后再把這個模板去掉,然后把請求到的內容添加到視圖列表中去,這樣列表組件就擁有了一個加載更多的能力。

    ExtendedNestedScrollView的改動:

    double nestOffset(double value, _NestedScrollPosition target) {
    // 滑動到小于50的時候觸發加載更多事件
    if (target.extentAfter < 50) {
    _onLoadMore();
    }
    }

    總結

    這樣,一個由Flutter開發的首頁就已經基本落地了。整個開發過程總結下來,有這樣幾點可以分享:

  • 用Flutter和Android開發首頁,都依賴了MD組件,它們對此支持得都比較完善;由于Dart語言的特性,Flutter在使用這些組件時更容易擴展、靈活性更強。

  • Flutter狀態化的組件管理機制,顯得比Android更切合場景,在區塊列表的設計上得心應手,這點也是眾多前端框架的亮點。

  • Flutter的動畫設計api很豐富,能充分滿足各種UI動效,讓頁面開發更輕松且不復雜。

  • Flutter表達性更強,又加以跨平臺的解決方案,減少了代碼量并大大提升了開發效率,為應用開發起到了開源節流的作用。

  • Flutter作為新秀,在Java老大哥已經爛熟于MVP等模式設計后,Flutter在此方面還需要積累;也可能Flutter本身并不需要這樣的積累,它等待的是比Java中更好的開發模式。

  • 參考文檔

    Flutter擴展NestedScrollView

    總結

    以上是生活随笔為你收集整理的flutter刷新页面_用Flutter实现58App的首页的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。