Flutter實戰 從頭擼一個「孤島」APP(No.1、專案初始化、螢幕適配)

洋小洋同學發表於2019-11-16

閱讀建議

章節目標

我們接下來會完成這部分

0102.gif

那由於我們是請求的網路圖片資源,會有一些請求時間,也是要優化的

寫在前面

在開始這段Flutter之旅前,需要儲備一些常用的點

  • 科學上網:不要問為什麼,因為作為開發來講這一步尤為的重要
  • 《Flutter 實戰》作者杜文(網名wendux) :這本書很適合新手初步瞭解Flutter的各個部件。這將不同於我們的HTML
  • Flutter中文社群:中文社群:其中會有一些視訊資源、外掛推薦
  • Flutter 鹹魚團隊技術部落格阿里巴巴鹹魚團隊:眾所周知,閒魚等APP就是國內應用Flutter技術開發的,他們對Flutter這個大家庭的貢獻也是尤為重要的。

本篇是這段旅程的第一段,因為筆者也不知會開發的什麼進度,但爭取每週更新一篇,讓我們共同學習,lets_do_it

專案初始化

那既然我們要開始一個新的專案,我們選擇初始化一個新的專案。在磁碟的方便找到的哪個位置都可以,那我就選擇這個

0101.png

專案的目錄

0102.png

專案建立好之後,依舊老套路,刪除無用的程式碼,其中主要的程式碼是main.dart

TIM截圖20191116190242.png

在這裡我們可以設定虛擬機器的層級,方便我們除錯

TIM截圖20191116190432.png

把這個總是在上邊開啟

目錄結構

開始建立一些見名知意的資料夾

TIM截圖20191116190802.png

  • models 主要是放置專案的Model類,這裡至於為什麼,在專案中我們直接操作後臺返回的JSON是不太好的
  • pages 主要是放置一些頁面檔案,其中包括首頁、書單、喜歡
  • provider 主要放置全域性狀態管理
  • utils 專案中公用的方法類
  • widgets 公用的部件

依賴安裝

我們可以嘗試收藏這兩個網址

  • pub一些第三方的外掛和包,在我們的專案中也會用到
  • hub包括像Flutter-go 這樣優秀的專案都在,聽說appid使用者可以官方渠道申請APP 端的使用
外掛名稱 地址
flutter_screenutil flutter_screenutil 螢幕適配
curved_navigation_bar curved_navigation_bar 底部導航欄
provider provider 狀態管理
shared_preferences shared_preferences 本地持久化
dio dio 網路請求
fluro fluro 路由框架
。。。

主檔案解析

那上邊我們已經初始化了專案,顯然一片黑色是有點醜陋的,不符合我們的審美,看一下MaterialApp

對外暴露的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 這個應該就是主頁面了
  • initialRoute 這個是不是初始化的路由,也許後邊我們寫到路由的時候可以用到
  • title 這個應該就是標題了
  • color 顏色
  • theme 莫非是主題

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

那我們就開始寫一個我的首頁

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

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

// 這裡我們用StatelessWidget,我是一個沒有狀態的"孩子"
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'),
      ),
    );
  }
}

複製程式碼

顯然我們如果都把這些部件放在同一個資料夾是不太符合開發規範的,也不利於後期的優化與維護,

那就寫在pages 資料夾下

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

複製程式碼

TIM截圖20191116195314.png

每個頁面的初始程式碼就是這個樣子的

  • 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('我是書單'),
      ),
    );
  }
}

複製程式碼
  • 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('我是首頁'),
      ),
    );
  }
}

複製程式碼
  • 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('我是喜歡'),
      ),
    );
  }
}

複製程式碼

底部導航bottomNavigationBar

在這裡我們使用 **curved_navigation_bar **這個輪子

首先,還是加入依賴

dependencies:
  curved_navigation_bar: ^0.3.1 #latest version
複製程式碼

TIM截圖20191116200144.png

在前面的時候,我們說過一些公用的部件我們放在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';
複製程式碼

其中的全部程式碼 是


/// 在這裡我們生命一個有狀態的部件,因為其中會牽扯到index的改變
class BottomNavBarWidget extends StatefulWidget {
  BottomNavBarWidget({Key key}) : super(key: key);

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

class _BottomNavBarWidgetState extends State<BottomNavBarWidget>
    with SingleTickerProviderStateMixin {
  /// 這裡宣告一個控制器,在flutter中好多用到控制器的地方,包括像最常見的表單
  TabController tabController;

  /// 這裡把我們引入的三個頁面放進List集合裡,等候發落
  List _pages = [HomePage(), BookListPage(), LovePage()];

  /// 這個就是比較核心的索引了,預設值就是我們的首頁
  int currentIndex = 0;

  @override
  void initState() {
    super.initState();
    tabController = TabController(vsync: this, length: 3)
      ..addListener(() {
        /// setState 這裡有點像我們們 的React,更改資料的時候是要在setState()裡
        setState(() {
          currentIndex = tabController.index;
        });
      });
  }

  // 這裡是一個部件,返回的值型別是個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,
            ),
          ],

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

複製程式碼

至於這個輪子怎麼用是傳字串,還是部件呢,那沒有比看原始碼更好不過了

TIM截圖20191116201616.png

  • 專案:小部件列表
  • 索引:NavigationBar的索引,可用於更改當前索引或設定初始索引
  • 顏色:NavigationBar的顏色,預設為Colors.white
  • buttonBackgroundColor:浮動按鈕的背景色,預設與顏色屬性
  • backgroundColor: NavigationBar的背景,預設Colors.blueAccent
  • onTap:函式處理對專案的點選
  • animationCurve:曲線插值按鈕更改動畫,預設Curves.easeOutCubic
  • animationDuration:按鈕更改動畫的持續時間,預設Duration(毫秒:600)
  • height:NavigationBar的高度,最小值0.0,最高75.0

本地圖片引入

那關於上文我們引入的圖片有必要一起學習下

 Image.asset(
              'images/bottom_nav/book_list@light.png',
              width: 50,
              height: 50,
            ),
複製程式碼

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

  • 在工程根目錄下建立一個images目錄,並將所需的圖片拷貝到該目錄

  • pubspec.yaml中的flutter部分新增如下內容:

      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,
      )
      複製程式碼

那截止目前呢我們已經開發了一部分了,也沒有遇到什麼磕磕絆絆,那《孤島APP》現在她便是這個樣子

0101.gif

螢幕適配

點選的底部導航的時候,能夠在三個頁面中進行切換,那現在有個很重要的問題需要考慮,讓我們把目光聚焦在頭部的字型,當下在這種模擬器下是這個大小,那手機的型號是千千萬萬的。所以就需要適配不通的螢幕

這裡我們使用flutter_ScreenUtil

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

先說下怎麼使用

  • 寬度 width ScreenUtil.getInstance().setWidth(540)
  • 高度 height ScreenUtil.getInstance().setHeight(200)
  • 字型大小 fontSize
//長方形:
Container(
           width: ScreenUtil.getInstance().setWidth(375),
           height: ScreenUtil.getInstance().setHeight(200),
            ),
            
//如果你想顯示一個正方形:
Container(
           width: ScreenUtil.getInstance().setWidth(300),
           height: ScreenUtil.getInstance().setWidth(300),
            ),
複製程式碼
//傳入字型大小,預設不根據系統的“字型大小”輔助選項來進行縮放(可在初始化ScreenUtil時設定allowFontScaling)
ScreenUtil.getInstance().setSp(28)         
 
//傳入字型大小,根據系統的“字型大小”輔助選項來進行縮放(如果某個地方不遵循全域性的allowFontScaling設定)     
ScreenUtil(allowFontScaling: true).setSp(28)   
複製程式碼

在需要適配的檔案引入

import 'package:flutter_screenutil/flutter_screenutil.dart';
複製程式碼

在這裡需要注意一下,我們把適配尺寸的初始化寫在了底部導航

TIM截圖20191116222116.png

接著我們對底部的三個圖片螢幕適配

    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)),
          ],
複製程式碼

那現在就需要我們處理一下頭部的字型了不是嗎?

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

TIM截圖20191116223249.png

有內味了是吧

右上角的DEBUG

在 MaterialApp 中,將 debugShowCheckdModeBanner 設成 false 就可以了

這裡放上一個參考的連結 如何移掉 flutter app 中的 debug label

在這段旅途的最後,我們來完善一下,這款《孤島》

    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        title: Text(
          '首頁',
          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,
        ),
      ),
    );
複製程式碼

寫在最後

這一段路,我們就一塊走到這兒,筆者會持續更新,請多多關注,相關程式碼也會同步更新到 筆者的倉庫github.com/yayxs/flutt…

如果喜歡的話,不妨給個鼓勵,好了就這young 加油~~

END

tips:一些思路有借鑑一些優秀的博文,如有不當,也可到筆者site 留言感謝開源,感謝大家

相關文章