Flutter實(shí)戰(zhàn) 從頭擼一個(gè)「孤島」APP(No.1、項(xiàng)目初始化、屏幕適配)

閱讀建議

  • 字?jǐn)?shù):2739
  • 時(shí)間:看你個(gè)人而定
  • 主要內(nèi)容:圖片、代碼都有
  • 場(chǎng)景:上下班的路上、床上

目標(biāo)

我們接下來(lái)會(huì)完成這部分

那由于我們是請(qǐng)求的網(wǎng)絡(luò)圖片資源,會(huì)有一些請(qǐng)求時(shí)間,也是要優(yōu)化的

寫(xiě)在前面

在開(kāi)始這段Flutter之旅前,需要儲(chǔ)備一些常用的點(diǎn)

  • 科學(xué)上網(wǎng):不要問(wèn)為什么,因?yàn)樽鳛殚_(kāi)發(fā)來(lái)講這一步尤為的重要
  • 《Flutter 實(shí)戰(zhàn)》作者杜文(網(wǎng)名wendux) :這本書(shū)很適合新手初步了解Flutter的各個(gè)部件。這將不同于我們的HTML
  • Flutter中文社區(qū):中文社區(qū):其中會(huì)有一些視頻資源、插件推薦
  • Flutter 咸魚(yú)團(tuán)隊(duì)技術(shù)博客阿里巴巴咸魚(yú)團(tuán)隊(duì):眾所周知,閑魚(yú)等APP就是國(guó)內(nèi)應(yīng)用Flutter技術(shù)開(kāi)發(fā)的,他們對(duì)Flutter這個(gè)大家庭的貢獻(xiàn)也是尤為重要的。

本篇是這段旅程的第一段,因?yàn)楣P者也不知會(huì)開(kāi)發(fā)的什么進(jìn)度,但爭(zhēng)取每周更新一篇,讓我們共同學(xué)習(xí),lets_do_it

目標(biāo)

初始化項(xiàng)目 init

那既然我們要開(kāi)始一個(gè)新的項(xiàng)目,我們選擇初始化一個(gè)新的項(xiàng)目。在磁盤(pán)的方便找到的哪個(gè)位置都可以,那我就選擇這個(gè)

項(xiàng)目的目錄

項(xiàng)目創(chuàng)建好之后,依舊老套路,刪除無(wú)用的代碼,其中主要的代碼是main.dart

在這里我們可以設(shè)置虛擬機(jī)的層級(jí),方便我們調(diào)試

把這個(gè)總是在上邊打開(kāi)

目錄結(jié)構(gòu)

開(kāi)始創(chuàng)建一些見(jiàn)名知意的文件夾

  • models 主要是放置項(xiàng)目的Model類(lèi),這里至于為什么,在項(xiàng)目中我們直接操作后臺(tái)返回的JSON是不太好的
  • pages 主要是放置一些頁(yè)面文件,其中包括首頁(yè)、書(shū)單、喜歡
  • provider 主要放置全局狀態(tài)管理
  • utils 項(xiàng)目中公用的方法類(lèi)
  • widgets 公用的部件

添加第三方包

我們可以嘗試收藏這兩個(gè)網(wǎng)址

  • pub一些第三方的插件和包,在我們的項(xiàng)目中也會(huì)用到
  • hub包括像Flutter-go 這樣優(yōu)秀的項(xiàng)目都在,聽(tīng)說(shuō)appid用戶(hù)可以官方渠道申請(qǐng)APP 端的使用
插件名稱(chēng) 地址
flutter_screenutil flutter_screenutil 屏幕適配
curved_navigation_bar curved_navigation_bar 底部導(dǎo)航欄
provider provider 狀態(tài)管理
shared_preferences shared_preferences 本地持久化
dio dio 網(wǎng)絡(luò)請(qǐng)求
fluro fluro 路由框架
。。。

main.dart

那上邊我們已經(jīng)初始化了項(xiàng)目,顯然一片黑色是有點(diǎn)丑陋的,不符合我們的審美,看一下MaterialApp

對(duì)外暴露的API

  const MaterialApp({
    Key key,
    this.navigatorKey,
    this.home, 
    this.routes = const <String, WidgetBuilder>{},
    this.initialRoute,
    this.onGenerateRoute,
    this.onUnknownRoute,
    this.navigatorObservers = const <NavigatorObserver>[],
    this.builder,
    this.title = '',
    this.onGenerateTitle,
    this.color,
    this.theme,
    this.darkTheme,
    this.themeMode = ThemeMode.system,
    this.locale,
    this.localizationsDelegates,
    this.localeListResolutionCallback,
    this.localeResolutionCallback,
    this.supportedLocales = const <Locale>[Locale('en', 'US')],
    this.debugShowMaterialGrid = false,
    this.showPerformanceOverlay = false,
    this.checkerboardRasterCacheImages = false,
    this.checkerboardOffscreenLayers = false,
    this.showSemanticsDebugger = false,
    this.debugShowCheckedModeBanner = true,

  • home 這個(gè)應(yīng)該就是主頁(yè)面了
  • initialRoute 這個(gè)是不是初始化的路由,也許后邊我們寫(xiě)到路由的時(shí)候可以用到
  • title 這個(gè)應(yīng)該就是標(biāo)題了
  • color 顏色
  • theme 莫非是主題

一個(gè)APP,在我們的印象中,都是 分為上中下三部分,就像是我們的人一樣頭部身體,腳部

那我們就開(kāi)始寫(xiě)一個(gè)我的首頁(yè)

import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';

void main() => runApp(MyApp());

// 這里我們用StatelessWidget,我是一個(gè)沒(méi)有狀態(tài)的"孩子"
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '孤島',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHome(),
    );
  }
}

class MyHome extends StatefulWidget {
  MyHome({Key key}) : super(key: key);

  @override
  _MyHomeState createState() => _MyHomeState();
}

class _MyHomeState extends State<MyHome> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('孤島APP'),
      ),
    );
  }
}

顯然我們?nèi)绻及堰@些部件放在同一個(gè)文件夾是不太符合開(kāi)發(fā)規(guī)范的,也不利于后期的優(yōu)化與維護(hù),

那就寫(xiě)在pages 文件夾下

lib
├── pages
├────book_list_page.dart
├────home_page.dart
├────love_page.dart

每個(gè)頁(yè)面的初始代碼就是這個(gè)樣子的

  • book_list_page.dart
import 'package:flutter/material.dart';

class BookListPage extends StatefulWidget {
  BookListPage({Key key}) : super(key: key);

  @override
  _BookListPageState createState() => _BookListPageState();
}

class _BookListPageState extends State<BookListPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('我是書(shū)單'),
      ),
    );
  }
}

  • home_page.dart
import 'package:flutter/material.dart';

class HomePage extends StatefulWidget {
  HomePage({Key key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('我是首頁(yè)'),
      ),
    );
  }
}

  • love_page.dart
import 'package:flutter/material.dart';

class LovePage extends StatefulWidget {
  LovePage({Key key}) : super(key: key);

  @override
  _LovePageState createState() => _LovePageState();
}

class _LovePageState extends State<LovePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('我是喜歡'),
      ),
    );
  }
}

底部導(dǎo)航 bottomNavigationBar

在這里我們使用 **curved_navigation_bar **這個(gè)輪子

首先,還是加入依賴(lài)

dependencies:
  curved_navigation_bar: ^0.3.1 #latest version

在前面的時(shí)候,我們說(shuō)過(guò)一些公用的部件我們放在widgets文件下,那我們打算放在公用的部件文件夾下,并命名為widget_bottom_navigation_bar.dart

在文件的頭部引入

import 'package:flutter/material.dart';
import 'package:curved_navigation_bar/curved_navigation_bar.dart';

import '../pages/home_page.dart';
import '../pages/book_list_page.dart';
import '../pages/love_page.dart';

其中的全部代碼 是

/// 在這里我們生命一個(gè)有狀態(tài)的部件,因?yàn)槠渲袝?huì)牽扯到index的改變
class BottomNavBarWidget extends StatefulWidget {
  BottomNavBarWidget({Key key}) : super(key: key);

  @override
  _BottomNavBarWidgetState createState() => _BottomNavBarWidgetState();
}

class _BottomNavBarWidgetState extends State<BottomNavBarWidget>
    with SingleTickerProviderStateMixin {
  /// 這里聲明一個(gè)控制器,在flutter中好多用到控制器的地方,包括像最常見(jiàn)的表單
  TabController tabController;

  /// 這里把我們引入的三個(gè)頁(yè)面放進(jìn)List集合里,等候發(fā)落
  List _pages = [HomePage(), BookListPage(), LovePage()];

  /// 這個(gè)就是比較核心的索引了,默認(rèn)值就是我們的首頁(yè)
  int currentIndex = 0;

  @override
  void initState() {
    super.initState();
    tabController = TabController(vsync: this, length: 3)
      ..addListener(() {
        /// setState 這里有點(diǎn)像咱們 的React,更改數(shù)據(jù)的時(shí)候是要在setState()里
        setState(() {
          currentIndex = tabController.index;
        });
      });
  }

  // 這里是一個(gè)部件,返回的值類(lèi)型是個(gè)Widget是用Scaffold包著的,里邊也是界面的核心
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        bottomNavigationBar: CurvedNavigationBar(
          // backgroundColor: _pages[currentIndex],
          index: currentIndex,
          // 底部按鈕
          items: <Widget>[
            Image.asset(
              'images/bottom_nav/home@light.png',
              width: 50,
              height: 50,
            ),
            Image.asset(
              'images/bottom_nav/book_list@light.png',
              width: 50,
              height: 50,
            ),
            Image.asset(
              'images/bottom_nav/love@light.png',
              width: 50,
              height: 50,
            ),
          ],

          /// 點(diǎn)擊不同的底部導(dǎo)航
          onTap: (index) {
            //Handle button tap
            setState(() {
              currentIndex = index;
            });
            tabController.animateTo(index,
                duration: Duration(milliseconds: 300), curve: Curves.ease);
          },
        ),
        // 主體部分,就是文中我們所說(shuō)的人的身體一樣
        body: TabBarView(
          controller: tabController,
          children: <Widget>[
            Container(
              child: _pages[0],
            ),
            Container(
              child: _pages[1],
            ),
            Container(
              child: _pages[2],
            )
          ],
        ));
  }
}


至于這個(gè)輪子怎么用是傳字符串,還是部件呢,那沒(méi)有比看源碼更好不過(guò)了

  • 項(xiàng)目:小部件列表
  • 索引:NavigationBar的索引,可用于更改當(dāng)前索引或設(shè)置初始索引
  • 顏色:NavigationBar的顏色,默認(rèn)為Colors.white
  • buttonBackgroundColor:浮動(dòng)按鈕的背景色,默認(rèn)與顏色屬性
  • backgroundColor: NavigationBar的背景,默認(rèn)Colors.blueAccent
  • onTap:函數(shù)處理對(duì)項(xiàng)目的點(diǎn)擊
  • animationCurve:曲線插值按鈕更改動(dòng)畫(huà),默認(rèn)Curves.easeOutCubic
  • animationDuration:按鈕更改動(dòng)畫(huà)的持續(xù)時(shí)間,默認(rèn)Duration(毫秒:600)
  • height:NavigationBar的高度,最小值0.0,最高75.0

Flutter 本地圖片的引入 assets

那關(guān)于上文我們引入的圖片有必要一起學(xué)習(xí)下

 Image.asset(
              'images/bottom_nav/book_list@light.png',
              width: 50,
              height: 50,
            ),

也就是images/bottom_nav/book_list@light.png,

  • 在工程根目錄下創(chuàng)建一個(gè)images目錄,并將所需的圖片拷貝到該目錄

  • pubspec.yaml中的flutter部分添加如下內(nèi)容:

      assets:
        - images/bottom_nav/home@light.png
        - images/bottom_nav/book_list@light.png
        - images/bottom_nav/love@light.png
    
    
  • 加載該圖片

    • Image(
        image: AssetImage("images/avatar.png"),
        width: 100.0
      );
      
      
    • Image.asset("images/avatar.png",
        width: 100.0,
      )
      
      

那截止目前呢我們已經(jīng)開(kāi)發(fā)了一部分了,也沒(méi)有遇到什么磕磕絆絆,那《孤島APP》現(xiàn)在她便是這個(gè)樣子

屏幕適配

點(diǎn)擊的底部導(dǎo)航的時(shí)候,能夠在三個(gè)頁(yè)面中進(jìn)行切換,那現(xiàn)在有個(gè)很重要的問(wèn)題需要考慮,讓我們把目光聚焦在頭部的字體,當(dāng)下在這種模擬器下是這個(gè)大小,那手機(jī)的型號(hào)是千千萬(wàn)萬(wàn)的。所以就需要適配不通的屏幕

這里我們使用flutter_ScreenUtil

flutter 屏幕適配方案,讓你的UI在不同尺寸的屏幕上都能顯示合理的布局!

先說(shuō)下怎么使用

  • 寬度 width ScreenUtil.getInstance().setWidth(540)
  • 高度 height ScreenUtil.getInstance().setHeight(200)
  • 字體大小 fontSize
//長(zhǎng)方形:
Container(
           width: ScreenUtil.getInstance().setWidth(375),
           height: ScreenUtil.getInstance().setHeight(200),
            ),
            
//如果你想顯示一個(gè)正方形:
Container(
           width: ScreenUtil.getInstance().setWidth(300),
           height: ScreenUtil.getInstance().setWidth(300),
            ),

//傳入字體大小,默認(rèn)不根據(jù)系統(tǒng)的“字體大小”輔助選項(xiàng)來(lái)進(jìn)行縮放(可在初始化ScreenUtil時(shí)設(shè)置allowFontScaling)
ScreenUtil.getInstance().setSp(28)         
 
//傳入字體大小,根據(jù)系統(tǒng)的“字體大小”輔助選項(xiàng)來(lái)進(jìn)行縮放(如果某個(gè)地方不遵循全局的allowFontScaling設(shè)置)     
ScreenUtil(allowFontScaling: true).setSp(28)   

在需要適配的文件引入

import 'package:flutter_screenutil/flutter_screenutil.dart';

在這里需要注意一下,我們把適配尺寸的初始化寫(xiě)在了底部導(dǎo)航

接著我們對(duì)底部的三個(gè)圖片屏幕適配

    items: <Widget>[
            Image.asset(
              'images/bottom_nav/home@light.png',
              width: ScreenUtil.getInstance().setWidth(100),
              height: ScreenUtil.getInstance().setHeight(100),
            ),
            Image.asset('images/bottom_nav/book_list@light.png',
                width: ScreenUtil.getInstance().setWidth(100),
                height: ScreenUtil.getInstance().setHeight(100)),
            Image.asset('images/bottom_nav/love@light.png',
                width: ScreenUtil.getInstance().setWidth(100),
                height: ScreenUtil.getInstance().setHeight(100)),
          ],

那現(xiàn)在就需要我們處理一下頭部的字體了不是嗎?

  • 引入 import 'package:flutter_screenutil/flutter_screenutil.dart';
  • 具體適配
 title: Text(
        '我是首頁(yè)',
        style: TextStyle(fontSize: ScreenUtil.getInstance().setSp(36)),
      ),

有內(nèi)味了是吧

右上角的DEBUG

在 MaterialApp 中,將 debugShowCheckdModeBanner 設(shè)成 false 就可以了

這里放上一個(gè)參考的鏈接 如何移掉 flutter app 中的 debug label

在這段旅途的最后,我們來(lái)完善一下,這款《孤島》

    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        title: Text(
          '首頁(yè)',
          style: TextStyle(fontSize: ScreenUtil.getInstance().setSp(36)),
        ),
      ),
      body: Container(
        height: ScreenUtil.getInstance().setHeight(1334),
        width: ScreenUtil.getInstance().setWidth(750),
        child: Image.network(
          'https://i.demo-1s.com/2019/11/16/yjhPSQWjuqPmosIL.jpg',
          fit: BoxFit.cover,
        ),
      ),
    );

寫(xiě)在最后

這一段路,我們就一塊走到這兒,筆者會(huì)持續(xù)更新,請(qǐng)多多關(guān)注,相關(guān)代碼也會(huì)同步更新到 筆者的倉(cāng)庫(kù), https://github.com/yayxs/flutter_lsolated_island_app

如果喜歡的話,不妨給個(gè)鼓勵(lì),好了就這young 加油~~

END

tips:一些思路有借鑒一些優(yōu)秀的博文,如有不當(dāng),也可到筆者site 留言感謝開(kāi)源,感謝大家

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容