Flutter实战 从头撸一个「孤岛」APP(No.1、项目初始化、屏幕适配)

9,906 阅读8分钟

阅读建议

章节目标

我们接下来会完成这部分

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 留言感谢开源,感谢大家