移动端Flutter简单应用
本文最后更新于 458 天前,其中的信息可能已经有所发展或是发生改变。

Flutter介绍

Flutter是谷歌公司开发的一款开源、免费的UI框架,可以让我们快速的在Android和iOS上构建高质量

App。它最大的特点就是跨平台、以及高性能。 目前 Flutter 已经支持 iOS、Android、Web、

Windows、macOS、Linux等。

Flutter基于谷歌的dart语言,如果没有任何Dart语言的基础,不建议直接学习Flutter。建议先学习Dart

语言的基本语法。然后再进入Flutter的学习。

Flutter 官网:https://flflutter.dev/

Flutter Packages官网:https://pub.dev/

需要有一定基础才能看懂此文章

项目准备

下载安装Flutter

官网

https://flutter.cn/docs/get-started/install/windows

下载完成后配置环境变量

加入 flutter\bin 目录的完整路径。

运行

where flutter dart

androad studio环境配置

安装插件

flutter和dart

创建flutter工程

Dart和Flutter对包名有一些规定,包名必须全部是小写,并且只能包含字母、数字和下划线。此外,包名不能以数字开头。

flutter create my_app (这里我叫only_you)

https://developer.android.google.cn/studio/run/managing-avds?hl=zh-cn#emulator

创建login组件

flutter create –template=package login_sdk

导入

导入测试

配置下载组组件

hi_download

flutter create --template=package hi_download

依赖

flutter pub add http

flutter pub add path_provider
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
typedef DownLoadListener = void Function(int total,int received,bool done);
class HiDownload{
  int _total = 0, _received = 0;
  Future<String> download({required String downLoadUrl,required String fileName,required DownLoadListener listener})
  async {
          debugPrint('start download');
          var uri= Uri.parse(downLoadUrl);
          var request=http.Request('GET',uri);
          var response= await http.Client().send(request);
          _total=response.contentLength??0;
          var path=(await getApplicationDocumentsDirectory()).path;
          final file=File('$path/$fileName');
          final writeFile=file.openSync(mode: FileMode.write);
  //         采用stream避免内存占用,提示性能
          response.stream.listen((value) {
            writeFile.writeFromSync(value);
            _received+=value.length;
            listener(_total,_received,false);
            debugPrint('progress:${_received/_total}');

          }).onDone(() async{
            await  writeFile.close();
            listener(_total,_received,true);
            debugPrint("-----done-----");

          });
          return file.absolute.path;
  }
}

主项目

yaml

name: only_you
description: A new Flutter project.
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev


version: 1.0.0+1

environment:
  sdk: '>=3.1.0 <4.0.0'


dependencies:
  flutter:
    sdk: flutter


  cupertino_icons: ^1.0.2
#  组件化配置
  login_sdk:
    path: "../login_sdk"
  hi_download:
    path: '../hi_download'
dev_dependencies:
  flutter_test:
    sdk: flutter


  flutter_lints: ^2.0.0


flutter:
  uses-material-design: true

新建文件目录

my_app/
├── android/
├── ios/
├── lib/
│   ├── main.dart
│   ├── app.dart
│   ├── config/
│   ├── models/
│   ├── views/
│   │   ├── pages/
│   │   ├── widgets/
│   ├── services/
│   ├── utils/
│   ├── routes/
│   └── theme/
├── test/
├── pubspec.yaml
└── README.md

- android/ 和 ios/ 目录包含了特定平台的代码。
- lib/ 目录是我们编写Dart代码的地方。
- main.dart 是应用程序的入口点。
- app.dart 通常包含了应用程序的根Widget,如 MaterialApp 或 CupertinoApp。
- config/ 目录用于存放应用的配置文件,如网络配置、环境配置等。
- models/ 目录包含了应用程序的数据模型。
- views/ 目录包含了应用程序的所有页面和自定义的Widget。
- services/ 目录包含了与后端API交互的服务类。
- utils/ 目录包含了一些实用工具,如网络请求、本地存储等。
- routes/ 目录包含了应用程序的所有路由(页面)。
- theme/ 目录用于存放应用的主题样式。
- test/ 目录用于存放测试代码。
- pubspec.yaml 文件用于管理Flutter应用的依赖项。
- README.md 文件用于描述项目的信息。

进入主项目only_you创建

mkdir lib/config lib/models lib/views lib/views/pages lib/views/widgets lib/services lib/utils lib/routes lib/theme

New-Item -ItemType Directory -Path .\lib\config
New-Item -ItemType Directory -Path .\lib\models
New-Item -ItemType Directory -Path .\lib\views
New-Item -ItemType Directory -Path .\lib\views\pages
New-Item -ItemType Directory -Path .\lib\views\widgets
New-Item -ItemType Directory -Path .\lib\services
New-Item -ItemType Directory -Path .\lib\utils
New-Item -ItemType Directory -Path .\lib\routes
New-Item -ItemType Directory -Path .\lib\theme

创建app.dart文件,这个文件通常用于定义应用的主题和路由。在lib/目录下创建app.dart文件:

touch lib/app.dart

新建routes.dart

import 'package:flutter/material.dart';
import 'package:your_project/views/pages/home_page.dart';
import 'package:your_project/views/pages/login_page.dart';

class Routes {
  static const home = '/';
  static const login = '/login';

  static Route<dynamic> generateRoute(RouteSettings settings) {
    switch (settings.name) {
      case home:
        return MaterialPageRoute(builder: (_) => HomePage());
      case login:
        return MaterialPageRoute(builder: (_) => LoginPage());
      default:
        throw FormatException("Route not found");
    }
  }
}

在home_page.dart文件中创建一个HomePage类:

import 'package:flutter/material.dart';

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home'),
      ),
      body: Center(
        child: Text('Welcome to Home Page'),
      ),
    );
  }
}

在login_page.dart文件中创建一个LoginPage类:

import 'package:flutter/material.dart';

class LoginPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('login'),
      ),
      body: Center(
        child: Text('Welcome to'),
      ),
    );
  }
}

在app.dart文件中

import 'package:flutter/material.dart';
import 'views/pages/home_page.dart';
import 'views/pages/login_page.dart';
import 'routes/routes.dart';

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      initialRoute: Routes.home,
      routes: {
        Routes.home: (context) => HomePage(),
        Routes.login: (context) => LoginPage(),
      },
    );
  }
}

或者也可以直接在app配置路由

import 'package:flutter/material.dart';
import 'views/pages/home_page.dart';
import 'views/pages/login_page.dart';

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      initialRoute: '/',
      routes: {
        '/': (context) => HomePage(),
        '/login': (context) => LoginPage(),
      },
    );
  }
}

更新main.dart文件

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

void main() {
  runApp(MyApp());
}

登錄頁面

在widgets中新建input_widget

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

class InputWidget extends StatelessWidget {
  final String hint;
  final ValueChanged<String>? onChanged;
  final bool obscureText;
  final TextInputType? keyboardType;
  final Widget? suffixIcon;
  final List<TextInputFormatter>? inputFormatters;
  const InputWidget(
      {super.key,
      required this.hint,
      this.onChanged,
      this.obscureText = false,
      this.keyboardType,
      this.suffixIcon,
      this.inputFormatters});

  get _input {
    return TextField(
      // 当输入框内容改变时,会调用这个函数
      onChanged: onChanged,
      // 是否隐藏输入的文本
      obscureText: obscureText,
      // 键盘类型
      keyboardType: keyboardType,
      // 是否启用自动纠错,当obscureText为true(隐藏输入的文本)时,自动纠错被禁用。
      autocorrect: !obscureText,
      // 光标颜色
      cursorColor: Colors.white,
      /*输入的文本的样式*/
      style: const TextStyle(
          fontSize: 17, color: Colors.white, fontWeight: FontWeight.w500),
      /*输入框的装饰,包括边框、提示文本和提示文本的样式 */
      decoration: InputDecoration(
        border: InputBorder.none,
        hintText: hint,
        hintStyle: const TextStyle(fontSize: 17, color: Colors.grey),
        //
        suffixIcon: suffixIcon,
      ),
      inputFormatters: inputFormatters,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        _input,
        const Divider(
          color: Colors.white,
          height: 1,
          thickness: 0.5,
        )
      ],
    );
  }
}

樣式

扩展

import 'package:flutter/cupertino.dart';
// 扩展int以方便使用
extension Intfix on int {
  SizedBox get paddingHeight{
    return SizedBox(height: toDouble(),);
  }
  SizedBox get paddingWidth{
    return SizedBox(width: toDouble());
  }
}
//扩展double以方便使用
extension Dobulefix on double{
  SizedBox get paddingHeight{
    return SizedBox(height: this,);
  }
  SizedBox get paddingWidth{
    return SizedBox(width: this);
  }
}

页面使用

                 //原本
                  const SizedBox(height: 16.0),
                  //扩展效果
                  16.paddingHeight

背景改成本地的方便加载

新建assets

yaml

flutter:
  uses-material-design: true
  assets:
    - assets/background.jpg

注册

注册是一个h5页面

所以需要跳转到h5

flutter pub add url_launcher

页面

  _jumpRegistration() async {
    Uri uri = Uri.parse('http://together.muxuetianyin.cn/register');
    if(!await launchUrl(uri,mode: LaunchMode.externalApplication)){
      debugPrint('Could not launch $uri');
    }
  }

请求

import 'package:http/http.dart' as http;
import 'dart:convert';

class ApiService {
  static const String _baseUrl = 'https://muxuetianyin.cn/api';
  // 登录接口
  static Future<Map<String, dynamic>> login(String userAccount, String userPassword) async {
    var url = '$_baseUrl/api/user/login';
    var body = jsonEncode({
      'userAccount': userAccount,
      'userPassword': userPassword,
    });
    var response = await http.post(
      Uri.parse(url),
      headers: {"Content-Type": "application/json"},
      body: body,
    );
    if (response.statusCode == 200) {
      return jsonDecode(response.body);
    } else {
      throw Exception('Failed to login');
    }
  }
//

}

yaml

shared_preferences: ^2.0.8

或者用另一个库

flutter pub add flutter_hi_cache

页面

  void _handleLogin() async {
    try {
      var result = await ApiService.login(
        _usernameController.text,
        _passwordController.text,
      );
      if (result['code'] == 0) {
        print('登录成功');
        // 这里可以处理登录成功后的逻辑,例如跳转到主页
        // 存储请求头
        SharedPreferences prefs = await SharedPreferences.getInstance();
        await prefs.setString('Authorization', result['data']['token']);
      } else {
        print('登录失败:${result['description']}');
      }
    } catch (e) {
      print('请求失败:$e');
    }
  }

LoginService

import 'package:shared_preferences/shared_preferences.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

class LoginService {
  static const String _baseUrl = 'https://muxuetianyin.cn';

  // 登录接口
  static Future<Map<String, dynamic>> login(String userAccount, String userPassword) async {
    var url = '$_baseUrl/api/user/login';
    var body = jsonEncode({
      'userAccount': userAccount,
      'userPassword': userPassword,
    });
    var response = await http.post(
      Uri.parse(url),
      headers: {"Content-Type": "application/json"},
      body: body,
    );
    if (response.statusCode == 200) {
      var result = jsonDecode(response.body);
      // 保存token
      SharedPreferences prefs = await SharedPreferences.getInstance();
      prefs.setString("token", result['data']['token']);
      prefs.setString("refreshToken", result['data']['refreshToken']);
      return result;
    } else {
      throw Exception('Failed to login');
    }
  }
  //刷新Token
  static Future<Map<String, dynamic>> refreshToken() async {
    var url = '$_baseUrl/api/user/refresh';
    SharedPreferences prefs = await SharedPreferences.getInstance();
    var refreshToken = prefs.getString("refreshToken");
    if(refreshToken == null){
      throw Exception('Failed to refreshToken');
    }
    var body = jsonEncode({
      'refreshToken': refreshToken
    });
    var response = await http.post(
      Uri.parse(url),
      headers: {"Content-Type": "application/json"},
      body: body,
    );
    if (response.statusCode == 200) {
      var result = jsonDecode(response.body);
      // 保存新的 token
      prefs.setString("token", result['data']);
      return result;
    } else {
      throw Exception('Failed to refreshToken');
    }
  }

  // 获取token
  static Future<String?> getToken() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    var token = prefs.getString("token");
    return token;
  }

  // 登出
  static Future<void> logout() async {
    // 移除token
    SharedPreferences prefs = await SharedPreferences.getInstance();
    prefs.remove("token");
    // 跳转到登录页
    // ...
  }
}

上一篇
下一篇