利用Flutter做成应用11:应用市场发布

将应用发布到Google应用市场

进入Google Play控制台,创建一个应用。

在网站填写相关内容

政策->应用内容

  • 隐私权政策
    添加隐私权政策网站。 可通过下面的网站制作隐私文件。
    https://app-privacy-policy-generator.firebaseapp.com/

  • 广告
    确认应用是否包含了广告。在 Google Play 中,包含广告的应用旁边会显示“包含广告”标签。

  • 应用访问权限
    判断某些部分需要用户满足特定条件(例如,提供登录凭据、取得会员资格、在特定位置或进行其他形式的身份验证)才能访问。

  • 内容分级
    从官方分级机构获得内容分级。您的内容分级会显示在 Google Play 中。

利用Flutter做成应用10:打包应用

生成秘钥文件

要想把app发布到Play store,需要给app一个数字签名。

在windows,[project]/android/app下执行下面的代码:

keytool -genkey -v -keystore .\Beauty.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias Beauty

输入密码等信息,即可生成秘钥文件Beauty.jks,需要保持这个文件的私有性。 该命令会有一个Warning,建议迁移到PKCS12格式。

利用Flutter做成应用9:图标与启动画面

APP启动图标

使用插件flutter_launcher_icons来做成App的启动图标。

可以根据给定的图片自动生成不同分辨率的应用图标。

dev_dependencies:
  flutter_launcher_icons: ^0.10.0

flutter_icons:
  android: "ic_launcher"
  ios: true
  remove_alpha_ios: true
  image_path: "assets/imgs/launcher1.png"
  min_sdk_android: 21 # android min sdk min:16, default 21
  windows:
    generate: true
    image_path: "assets/imgs/launcher1.png"
    icon_size: 256 # min:48, max:256, default: 48

android:ic_launcher表示生成android平台的应用图标的名称 ios:false表示不生成ios的图标 image_path为源图片的路径

利用Flutter做成应用8:Google广告

Google广告

创建应用后可以获取应用ID,并在AndroidManifest.xml中添加以下字段。

<meta-data
    android:name="com.google.android.gms.ads.APPLICATION_ID"
    android:value="***app id***" />

后续步骤:

  1. 创建广告单元并测试 SDK 集成
  2. 完成测试后,请将您的应用发布到受支持的应用商店
  3. 将商店添加到您的 AdMob 应用中。我们会对您的应用进行一些检查,确保它可以展示广告。评估您的应用通常需要几天时间,但在某些情况下可能需要更长时间。
  • 创建广告单元 可创建横幅广告,激励广告和原生高级广告等。创建后可以获取广告id,用于app中来展示广告。

展示广告

使用插件google_mobile_ads来在应用中展示广告。

dependencies:
  google_mobile_ads: ^2.0.1

Android广告测试ID

利用Flutter做成应用7:音乐播放

音乐播放

为了在app中播放音乐,可使用插件audioplayers

示例代码如下:

import 'package:audioplayers/audioplayers.dart';
import 'package:sp_util/sp_util.dart';

import '../data/global.dart';
import 'log_util.dart';

class SEUtil {
  static AudioPlayer player = AudioPlayer();

  static play(String fileName) async {
    try {
      await player.setSourceAsset(fileName);
      await player.setReleaseMode(ReleaseMode.loop);
      await player.resume();

      player.onPlayerStateChanged.listen((event) {
        if (player.state == PlayerState.playing) {
          LogUtil.print('play start');
          Global.bMusic = true;
        } else {
          LogUtil.print('play stop');
          Global.bMusic = false;
        }

        SpUtil.putBool(Global.keyMusic, Global.bMusic!);
      });
    } catch (e) {
      LogUtil.print(e);
    }
  }

  static stop() async {
    await player.stop();
  }

  static release() async {
    LogUtil.print('play release');
    await player.release();
  }
}

在yaml文件中配置需要播放的音乐资源,资源应放在assets文件夹下。

利用Flutter做成应用6:主题,字体与资源

主题

设置主题的不同颜色,可通过插件provider来实现对主题颜色的状态变化的管理。

先实现一个类继承ChangeNotifier。

class ThemeProvider with ChangeNotifier {
  String _themeColor = '';

  String get themeColor => _themeColor;

  setTheme(String themeColor) {
    _themeColor = themeColor;

    notifyListeners();
  }
}

在主程序中,通过在MultiProvider中引入ThemeProvider,再通过Consumer来监听主题颜色变量。 颜色变量变化后主题颜色即可实时反映到AppBar上。

利用Flutter做成应用5:抽屉菜单

抽屉菜单

为了在app的顶部左边添加滑出菜单,需要在Scaffold中添加drawer字段,此时在appbar的左侧会出现一个三道杠的图标,点击后即可弹出自定义的菜单。

Scaffold(
      drawer: const DrawerWidget(),
);

示例代码如下:

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

import '../data/global.dart';
import '../util/log_util.dart';
import '../util/ui_util.dart';
import 'setting.dart';

class DrawerWidget extends StatelessWidget {
  const DrawerWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return Drawer(
      child: MediaQuery.removePadding(
          context: context,
          removeTop: false,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Padding(
                padding: const EdgeInsets.only(top: 40.0),
                child: Center(
                  child: ClipOval(
                    child: Image.asset(
                      "assets/imgs/avatar.png",
                      width: 120,
                    ),
                  ),
                ),
              ),
              Expanded(
                  child: ListView(
                children: <Widget>[
                  const Divider(),
                  ListTile(
                    leading: const Icon(Icons.settings),
                    title: const Text(
                      'Setting',
                      textScaleFactor: 1.4,
                    ),
                    trailing: const Icon(Icons.arrow_forward),
                    onTap: () {
                      LogUtil.print('setting');
                      Navigator.push(context,
                          MaterialPageRoute(builder: (BuildContext context) {
                        return const SettingWidget();
                      }));
                    },
                  ),
                  const Divider(),
                  ListTile(
                    leading: const Icon(Icons.info),
                    title: const Text(
                      'About',
                      textScaleFactor: 1.4,
                    ),
                    trailing: const Icon(Icons.arrow_forward),
                    onTap: () {
                      LogUtil.print('about');
                      Navigator.push(context,
                          MaterialPageRoute(builder: (BuildContext context) {
                        return openAbout();
                      }));
                    },
                  ),
                  const Divider(),
                  ListTile(
                      leading: const Icon(Icons.star),
                      title: const Text(
                        'Rate on Google Play',
                        textScaleFactor: 1.3,
                      ),
                      trailing: const Icon(Icons.arrow_forward),
                      onTap: () {
                        LogUtil.print('Rate');
                        _launchUrl(Global.googlePlayUrl);
                      }),
                  const Divider(),
                ],
              ))
            ],
          )),
    );
  }

}

对于菜单项,点击后跳转到其他页面,可使用如下代码:

利用Flutter做成应用4:存储

键值数据存储

对于简单的数据存储,可以使用插件sp_util方便的读写数据,该插件是对shared_preferences的实用包装。

dependencies:
  sp_util: ^2.0.3

使用之前,需要对sp_util进行异步初始化。

await SpUtil.getInstance();

复杂数据存储

  1. 对于复杂的数据,可以将数据存入到数据库中。可使用插件sqflite进行数据读写。
dependencies:
  sqflite: ^2.1.0

创建一个工厂类,实现对db的操作。

import 'package:path/path.dart' as p;
import 'package:sqflite/sqflite.dart';
import 'package:synchronized/synchronized.dart';

const String tblName = "Img";
class SaveUtil {
  // 私有构造函数.
  SaveUtil._();
  // 单例.
  static final SaveUtil _singleton = SaveUtil._();
  // 工厂构造函数.
  factory SaveUtil() => _singleton;

  static final Lock _lock = Lock();

  static Database? _database;
  Future<Database> get database async {
    if (_database == null) {
      await _lock.synchronized(() async {
        _database ??= await _initDb();
      });
    }
    return _database!;
  }

  _initDb() async {
    try {
      String databasesPath = await getDatabasesPath();
      String path = p.join(databasesPath, 'beautyBook.db');

//    await deleteDatabase(path);

      return await openDatabase(path, version: 2, onCreate: _onCreate);
    } catch (e) {
    }
  }

  void _onCreate(Database db, int newVersion) async {
    await db
        .execute('CREATE TABLE $tblName (id INTEGER PRIMARY KEY, url TEXT)');
  }

  Future<int?> save(String url) async {
    final db = await database;
    var result =
        await db.rawInsert("INSERT INTO $tblName(url) VALUES(\"$url\")");
    return result;
  }

  Future<List<String>> get() async {
    List<String> list = [];
    final db = await database;

    List listTmp = await db.rawQuery('SELECT url FROM $tblName');
    for (var map in listTmp) {
      String url = map['url'];
      if (!list.contains(url)) {
        list.add(url);
      }
    }

    return list;
  }

  Future<int?> count() async {
    final db = await database;
    int? count = Sqflite.firstIntValue(
        await db.rawQuery('SELECT COUNT(*) FROM $tblName'));

    return count;
  }

  Future delete(String url) async {
    final db = await database;
    await db.rawDelete('DELETE FROM $tblName WHERE url = ?', [url]);
  }

  Future closeDB() async {
    return _database?.close();
  }
}

sqflite不支持windows,在windows平台下,会报下面的错误。

利用Flutter做成应用3:图片

获取网络图片

需要预先将app需要展示的图片存储到网络空间。本应用将图片存储到cloudinary

在pubspec.yaml的dependencies添加http插件。

dependencies:
  http: ^0.13.5

注意:
flutter默认debug版可以进行网络连接,对于release版,需要在android/app/src/main/AndroidManifest.xml中添加网络配置。

<uses-permission android:name="android.permission.INTERNET" />

展示图片

通过插件extended_image可以方便的展示网络上的图片。

dependencies:
  extended_image: ^6.3.1

代码如下所示,显示url指向的的图片,图片显示前显示图片的加载进度。

  ExtendedImage.network(
      url,
      handleLoadingProgress: true,
      clearMemoryCacheIfFailed: true,
      clearMemoryCacheWhenDispose: true,
      cache: true,
      loadStateChanged: (ExtendedImageState state) {
        if (state.extendedImageLoadState == LoadState.loading) {
          final loadingProgress = state.loadingProgress;
          int? nTotal = loadingProgress?.expectedTotalBytes;
          double? progress;
          if (nTotal != null) {
            progress = loadingProgress!.cumulativeBytesLoaded / nTotal;
          }

          return Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: <Widget>[
                SizedBox(
                  width: 150.0,
                  child: LinearProgressIndicator(
                    value: progress,
                  ),
                ),
                const SizedBox(
                  height: 10.0,
                ),
                Text('${((progress ?? 0.0) * 100).toInt()}%'),
              ],
            ),
          );
        }
        return null;
      },
    );

可以对图片有更多的操作,如放大,缩小等。

利用Flutter做成应用2:APP做成

通过android studio创建应用

  1. 选择 File>New Flutter Project
  2. 选择 Flutter application 作为 project 类型, 然后点击 Next
  3. 输入项目名称 (如 myapp), 然后点击 Next
  4. 点击 Finish
  5. 等待Android Studio安装SDK并创建项目.

使用如下代码,做成一个空的app,该app有一个AppBar,底部有四个按钮。