学习Flutter中的缓存、日志和字体设计

264 阅读13分钟

Flutter中的缓存、日志和排版

作为一名程序员,知道如何创建漂亮的应用程序并将其部署到生产中是一件好事。资源是稀缺的,不管你的背景如何,充分的利用将使所有的游戏方受益。

在缓存的情况下,当我们的应用程序对同一资源进行频繁的请求/访问而不发生变化时,会被归类为浪费,因为没有获得新的信息(与最初的请求/访问不同)。

同样,了解我们的应用程序在运行时发生了什么,对于调试至关重要。我们想知道我们的应用程序是以它应该的方式还是以它不应该的方式行事。当有东西损坏时,我们要检测是什么原因造成的,什么时候发生的,在哪里发生的,以修复导致故障的问题。为了达到这个目的,我们利用了日志记录。

前提条件

读者应具备以下条件才能跟上。

  • 熟练掌握创建应用程序的Dart和Flutter编程语言。
  • 在应用程序中整合服务和功能的经验。
  • 了解dart编程语言中的各种数据结构和数据类型,可以更好地指导你开发高效的应用程序。

目标

在本教程中,我们将。

  • 在我们的应用程序代码库中实施缓存。
  • 在我们的flutter代码中实现日志记录。
  • 知道什么时候需要缓存我们的数据。
  • 看看记录我们的应用程序的好处。
  • 为我们的应用程序的字体实现排版。

缓存

缓存是一种由系统或系统创建者实现的机制,它将数据暂时储存在可用的内存中。它使访问和检索这些存储的信息更加容易。缓存内存在计算机系统中供系统使用,用户在操作系统时目前正在使用的数据有时会被缓存起来。

缓存是由系统的创建者,即程序员来实现的,因为他编写了计算机执行的指令。缓存将经常访问的数据、图像和对象存储在需要的地方,使访问这些信息的速度更快。

在这种情况下,当用户在你的应用程序中的屏幕之间切换时,会有一个HTTP请求发送到服务器,以获取一个陈旧的资源(不改变的数据),缓存在这种情况下是至关重要的,它可以改善用户体验。

用户不必每次在屏幕之间切换时都要等待数据被取走。对于可能不稳定的数据,你可以让用户选择刷新获取的数据,并在有新数据时更新缓存的记录。

缓存的好处

  • 缓存可以节省资源。
  • 缓存给用户带来更好的体验。
  • 缓存使你的应用程序工作得更快、更好。
  • 它可以帮助你创建高效的应用程序。

缓存的实现

为了展示一个例子,我们将在我们的flutter应用程序中执行一个HTTP请求,当用户浏览到一个新的屏幕时,获取数据,其内容被获取。

第一步:设置我们的应用程序

在您的main.dart 文件中设置您的flutter应用程序,以显示一个按钮,在点击时导航到一个新的屏幕。

import 'package:flutter/material.dart';
void main() {
 runApp(const MyApp());
}
class MyApp extends StatelessWidget {
 const MyApp({Key? key}) : super(key: key);
  @override
 Widget build(BuildContext context) {
   return MaterialApp(
     title: 'Caching',
     theme: ThemeData(

       primarySwatch: Colors.blue,
     ),
     Home: Scaffold(
 appBar: AppBar(

   title: Text('Caching'),
 ),
 body: CountriesSelect(),
       );
   );
 }
}

我们有一个基本的flutter应用程序,在上面的片段中设置了一个带有标题缓存的app bar 。它有一个body 和一个container ,里面什么都没有。

class _CountriesSelectState extends State<CountriesSelect> {
 final country = 'USA';

 @override
 Widget build(BuildContext context) {
   return ListTile(
     leading: const Icon(Icons.gps_fixed),
     title: Text(country),
     onTap: () {
       Navigator.push(
           context,
           MaterialPageRoute(
               builder: (context) =>
                   FetchDataScreen(country: country)));
     },
   );
 }
}

接下来,我们将制作一个小部件,包含一个标题为美国的文本,即我们想要获得更多信息的国家。

    onTap: (){
    Navigator.push(context,
        MaterialPageRoute(builder: (context) => FetchDataScreen(country: country)));
    },

我们通过将选定的国家作为参数传递给FetchDataScreen 类的命名构造器来指定我们希望导航到的屏幕,并在onTap 功能属性的帮助下,每当点击列表时就会导航到那里。

根据从构造函数中收到的信息,我们将其存储在一个数据字段中,使其能够被类所访问。

第二步:分析一个低效的场景

接下来,我们将使用HTTP flutter包和休息国提供的国家信息的公共休息API库来请求所选国家的信息。

为了实现这一目标,前往HTTP包的repo,并获得最新的安装版本。在这种情况下,我们的是HTTP: ^0.13.4 ,并将其添加到我们应用程序根目录下的pubspec.yaml 文件的依赖项中。

dependencies:
 flutter:
   sdk: flutter
 http: ^0.13.4

上面是我们的flutter应用程序中的依赖关系和dev_dependencies 之间的部分。一旦我们的HTTP包被添加,运行下面的代码。

Flutter pub get

这将获得该包并将其添加到我们的应用程序的依赖关系中。

class FetchDataScreen extends StatefulWidget {
 final String country;

 const FetchDataScreen({Key? key, required this.country}) : super(key: key);

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

class _FetchDataScreenState extends State<FetchDataScreen> {
 late String countryName = '';
 late String capital = '';
 late String region = '';
 late int population = 0;
 late String alpha3Code = '';

 Future<void> getCountryInfo(String country) async {
   var url = Uri.https('restcountries.com', '/v2/name/$country');

   // Await the http get response, then decode the json-formatted response.
   var response = await http.get(url);
   if (response.statusCode == 200) {
     var jsonResponse = convert.jsonDecode(response.body);

     setState(() {
       countryName = jsonResponse[0]['name'];
       capital = jsonResponse[0]['capital'];
       region = jsonResponse[0]['region'];
       population = jsonResponse[0]['population'];
       alpha3Code = jsonResponse[0]['alpha3Code'];
     });
     print(jsonResponse);
   } else {
     print('Request failed with status: ${response.statusCode}.');
   }
 }

 @override
 void initState() {
   super.initState();
   getCountryInfo(widget.country);
 }

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     backgroundColor: Colors.white,
     appBar: AppBar(
       title: Text(countryName),
     ),
     body: Container(
       padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20),
       child: Column(
         children: [
           Row(
             children: [
               const Text("Country Name"),
               const SizedBox(
                 width: 20,
               ),
               Wrap(
                 children: [Text(countryName)],
               )
             ],
           ),
           const SizedBox(
             height: 20,
           ),
           Row(
             children: [
               const Text("Country Capital"),
               const SizedBox(
                 width: 20,
               ),
               Wrap(
                 children: [Text(capital)],
               )
             ],
           ),
           const SizedBox(
             height: 20,
           ),
           Row(
             children: [
               const Text("Country Region"),
               const SizedBox(
                 width: 20,
               ),
               Wrap(
                 children: [Text(region)],
               )
             ],
           ),
           const SizedBox(
             height: 20,
           ),
           Row(
             children: [
               const Text("Country Population"),
               const SizedBox(
                 width: 20,
               ),
               Wrap(
                 children: [Text('$population')],
               )
             ],
           ),
           const SizedBox(
             height: 20,
           ),
           Row(
             children: [
               const Text("Country Abbr"),
               const SizedBox(
                 width: 20,
               ),
               Wrap(
                 children: [Text(alpha3Code)],
               )
             ],
           ),
           const SizedBox(
             height: 20,
           ),
           RaisedButton(
               onPressed: () {
                 Navigator.push(
                     context,
                     MaterialPageRoute(
                         builder: (context) =>
                             FetchMoreScreen(country: countryName)));
               },
               child: const Text('More'))
         ],
       ),
     ),
   );
 }
}

上面的代码向端点发送了一个请求,以获得所选国家的信息,并在当前屏幕上显示部分数据。下面的按钮将我们导航到一个新的屏幕,以查看同一个国家的更多细节。

RaisedButton(
onPressed: () {
    Navigator.push(
        context,
        MaterialPageRoute(
            builder: (context) =>
                FetchMoreScreen(country: countryName)));
},
child: const Text('More'))

新的屏幕,FetchMoreScreen() ,接受一个在HTTP请求中使用的选定国家的参数。fetchMoreScreen 的代码规定如下。

class FetchMoreScreen extends StatefulWidget {
 final String country;

 const FetchMoreScreen({Key? key, required this.country}) : super(key: key);

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

class _FetchMoreScreenState extends State<FetchMoreScreen> {
 late String subregion = '';
 late List currencies = [];
 late String flag = '';
 late double area = 0;

 Future<void> getCountryInfo(String country) async {
   var url = Uri.https('restcountries.com', '/v2/name/$country');

   // Await the http get response, then decode the json-formatted response.
   var response = await http.get(url);
   if (response.statusCode == 200) {
     var jsonResponse = convert.jsonDecode(response.body);
     setState(() {
       subregion = jsonResponse[0]['subregion'];
       currencies = jsonResponse[0]['currencies'];
       flag = jsonResponse[0]['flags']['png'];
       area = jsonResponse[0]['area'];
     });
     print(flag);
   } else {
     print('Request failed with status: ${response.statusCode}.');
   }
 }

 @override
 void initState() {
   super.initState();
   getCountryInfo(widget.country);
 }

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     backgroundColor: Colors.white,
     appBar: AppBar(
       title: Text(widget.country),
     ),
     body: Container(
       padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20),
       child: Column(
         children: [
           Row(
             children: [
               const Text("Country Subregion"),
               const SizedBox(
                 width: 20,
               ),
               Wrap(
                 children: [Text(subregion)],
               )
             ],
           ),
           const SizedBox(
             height: 20,
           ),
           Row(
             children: [
               const Text("Country Currency"),
               const SizedBox(
                 width: 20,
               ),
               Expanded(
                   child: currencies.length > 0
                       ? Text(
                           "${currencies[0]['name'] ?? ''} - ${currencies[0]['symbol'] ?? ''}")
                       : Container())
             ],
           ),
           const SizedBox(
             height: 20,
           ),
           Row(
             children: [
               const Text("Country Flag"),
               const SizedBox(
                 width: 20,
               ),
               Container(
                 width: 100,
                 child: flag.length > 0
                     ? Image(
                         image: NetworkImage(flag, scale: 1),
                       )
                     : Container(),
               )
             ],
           ),
           const SizedBox(
             height: 20,
           ),
           Row(
             children: [
               const Text("Country Area"),
               const SizedBox(
                 width: 20,
               ),
               Wrap(
                 children: [Text('$area')],
               )
             ],
           ),
           const SizedBox(
             height: 20,
           ),

        RaisedButton(
               onPressed: () {
                 Navigator.push(
                     context,
                     MaterialPageRoute(
                         builder: (context) =>
                             FetchDataScreen(country: widget.country )));
               },
               child: const Text(Less))
         ],
       ),
     ),
   );
 }
}

FetchMoreScreen() 执行对同一资源的请求,并以不同于前一个屏幕FetchDataScreen() 的数据填充其内容。它还有一个标有 "Less "的按钮,可以将用户导航到FetchDataScreen()

尽管这可能不是最有效的例子,但它仍然足以代表我们正在讨论的缓存的概念。

第三步:用我们的实现来定义问题

如果你按原样运行代码,你会得到一个在第一个屏幕上列出的国家列表。选择一个国家。你将进入下一个屏幕,在那里从终端获取所选国家的详细信息。数据需要一段时间来显示,但一旦收到就会呈现在屏幕上。

点击 "更多 "按钮,你将进入下一个屏幕,它同样向数据库发送请求,以获取有关国家的额外细节。它还有一个标记为 "更少 "的按钮,带你回到前一个屏幕,在那里你需要获取关于这个国家的最初几个数据,这是一个不相关的请求。

每次你在屏幕之间切换时,被获取的数据并没有改变。为什么要为一个你之前已经访问过的资源发送请求呢?这个例子可以发生在任何情况下的场景,尽管方式不一样。

为了以编程的方式实现这一点,我们将对获取的数据进行设置,将第一次加载时获得的信息存储在一个临时的缓存文件中。检查数据是否存在于临时缓存文件中。如果数据存在,就使用数据。

注意:如果数据不存在,不要发送请求。

为了实现这一点,我们将从flutter pub仓库path_provider导入一个包(该包可以得到你的应用程序存储数据的目录的路径),并将该包添加到你的pubspec.yamlpath_provider中。

将该包添加到你的pubspec.yaml

dependencies:
 flutter:
   sdk: flutter
 http: ^0.13.4
 path_provider: ^2.0.8

第四步:实施我们的解决方案

运行flutter pub get 来安装添加的依赖关系。在你文件的顶部导入你的path_provider 包。编辑getCountryInfo() 函数来实现你的缓存。

Future<void> getCountryInfo(String country) async {
 String fileName = 'countryData.json';
 var dir = await getTemporaryDirectory();

 File file = File(dir.path + '/' + fileName);
 if (file.existsSync()) {
   print("Fetching from cache");
   var jsonData = file.readAsStringSync();
   var jsonResponse = convert.jsonDecode(jsonData);

   setState(() {
     countryName = jsonResponse[0]['name'];
     capital = jsonResponse[0]['capital'];
     region = jsonResponse[0]['region'];
     population = jsonResponse[0]['population'];
     alpha3Code = jsonResponse[0]['alpha3Code'];
   });
 } else {
   print("Fetching from API");
   var url = Uri.https('restcountries.com', '/v2/name/$country');
   // Await the http get response, then decode the json-formatted response.
   var response = await http.get(url);
   if (response.statusCode == 200) {

     var jsonResponse = convert.jsonDecode(response.body);
     // saving to cache
     file.writeAsStringSync(response.body, flush: true, mode:FileMode.write );

     setState(() {
       countryName = jsonResponse[0]['name'];
       capital = jsonResponse[0]['capital'];
       region = jsonResponse[0]['region'];
       population = jsonResponse[0]['population'];
       alpha3Code = jsonResponse[0]['alpha3Code'];
     });
   } else {
     print('Request failed with status: ${response.statusCode}.');
   }
 }
}

我们在上面的代码中创建了一个名为 "fileName "的临时文件和dart.io 库。

 var dir = await getTemporaryDirectory();

Gets the directory in the device’s storage. 

 File file = File(dir.path + '/' + fileName);

Then creates the file in the device storage.

 if (file.existsSync()) {

}else{

} 

使用file.existsSync() ,我们检查该文件是否存在。如果它存在,我们就从它那里读取。由于这是第一次调用API,我们请求获取该文件。

 // saving to cache
    file.writeAsStringSync(response.body, flush: true, mode:FileMode.write );

第五步:结束缓存过程

当第一次调用文件时,我们使用上面的代码片段将我们的响应写到文件中。由于发送请求的HTTP包的响应已经是JSON格式,所以不需要对其进行解码并存储在数据库中。

如果我们运行我们的代码,我们看到在第一次点击国家名称的时候,我们有一个记录的信息说从API中获取的在屏幕上来回导航的记录到终端,随后的读取是从缓存文件中获取的。我们的数据访问是快速的,迅速的,并且节省了资源,在用户每次在屏幕之间导航时从终端获取相同的数据集。

同样的方法也适用于FetchMoreScreeen()

记录

日志是对系统操作的记录,从数据输入、处理、输出到最后的结果,都用表格记录下来。本教程中的日志与普通编程语言对终端的标准日志输出不同(在dart的情况下,print() )。

普通编程语言对终端或控制台的这种日志记录是基本的、没有描述性的,在实际应用生产过程中有时是没有帮助的。他们没有给出其他相关的细节,如日志发生的时间,什么启动了日志。这些信息对于描述应用程序的操作以及应用程序的崩溃都是相关的。

  • 我们将使用flutter包来模拟flutter中的日志记录程序。
  • 前往flutter包,按照安装步骤,登录到你的应用程序。
  • 将该包作为一个依赖项添加到您的pubspec.yaml 文件中。
dependencies:
 flutter:
   sdk: flutter
 logging: ^1.0.2

将该包导入你打算使用记录器的dart文件中。在我们的例子中,我们将把它放在main.dart文件中,并在我们的FetchDataScreen()类中使用它。当实现记录器时,最好是在你的widget树中尽可能地创建记录器的实例化。使用我们之前的代码库,让我们实现一些日志记录。

注意:用一个独特的名字创建日志器,以识别日志信息的来源。

final log = Logger(‘MyApp’);

Main.dart

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

class FetchDataScreen extends StatefulWidget {
 final String country;

 FetchDataScreen({
   Key? key,
   required this.country,
 }) : super(key: key);

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

class _FetchDataScreenState extends State<FetchDataScreen> {
 bool level = false;
 late String countryName = '';
 late String capital = '';
 late String region = '';
 late int population = 0;
 late String alpha3Code = '';
 final log = Logger('MyApp');

 Future<void> getCountryInfo(String country) async {
   String fileName = 'countryData.json';
   var dir = await getTemporaryDirectory();

   File file = File(dir.path + '/' + fileName);
   if (file.existsSync()) {
     log.info("Fetching from cache");
     var jsonData = file.readAsStringSync();
     var jsonResponse = convert.jsonDecode(jsonData);

     setState(() {
       countryName = jsonResponse[0]['name'];
       capital = jsonResponse[0]['capital'];
       region = jsonResponse[0]['region'];
       population = jsonResponse[0]['population'];
       alpha3Code = jsonResponse[0]['alpha3Code'];
     });
   } else {
     log.fine("Fetching from API");
     var url = Uri.https('restcountries.com', '/v2/name/$country');
     // Await the http get response, then decode the json-formatted response.
     var response = await http.get(url);
     if (response.statusCode == 200) {

       var jsonResponse = convert.jsonDecode(response.body);
       // saving to cache
       file.writeAsStringSync(response.body, flush: true, mode:FileMode.write );

       setState(() {
         countryName = jsonResponse[0]['name'];
         capital = jsonResponse[0]['capital'];
         region = jsonResponse[0]['region'];
         population = jsonResponse[0]['population'];
         alpha3Code = jsonResponse[0]['alpha3Code'];
       });
     } else {
       print('Request failed with status: ${response.statusCode}.');
     }
   }
 }

 @override
 void initState() {
   super.initState();
   getCountryInfo(widget.country);
 }

@override
Widget build(BuildContext context) {
	
  // The code to go here is similar to code in the same method in the previous example

}
}

然后使用logging包提供的方法输出基本信息和精细信息。

下面是一个选项列表,每个选项象征着日志消息的不同等级或级别。

  • Level.OFF
  • Level.SHOUT
  • Level.SEVERE
  • Level.WARNING
  • 等级.INFO
  • 等级.CONFIG
  • 等级.FINE
  • 等级.FINER
  • 级别.FINEST

如果我们运行该应用程序,我们将不会收到响应,因为我们没有监听日志。所以在main方法中,我们实现了root Logger。

Logger.root.level = Level.ALL; 
Logger.root.onRecord.listen((record) {
  print('${record.loggerName} -
${record.level.name}: ${record.time}: ${record.message}');
});

如果我们运行我们的应用程序,我们会得到正如我们指定的在控制台中打印出来的响应。

MyApp - FIINE: 2021-12-22 19:37:00.608065: Fetching from API

这很好,因为现在我们有了更多关于我们的日志的信息,但是仍然,但是我们仍然在使用打印来记录到终端。对于大多数实时应用程序,我们可以把这些日志写到一个文件中,并把它们存储在内存中。

听众中的记录值返回还提供了其他属性。你可以到日志文档中去查看,或者更好的是,用你能获得的信息来玩一玩。

  • Loggername - 在记录器实例化中指定的名称。
  • Message - 要显示的日志信息。
  • Level - 日志级别,可以是良好、严重、警告,以及更多。
  • Error - 如果有任何错误,则为错误。
  • Time - 记录器的时间。
  • stackTrace - 传播出去的堆栈跟踪。
  • Zoneobject - 日志的区域。
  • sequenceNumber - 序列号。

另一个实现可以如下。

Queue<LogRecord> logs = Queue();

Logger.root.level = Level.All
Logger.root.onRecord.listen((record) {
  print('${record.loggerName} -
${record.level.name}: ${record.time}: ${record.message}');
logs.addLast(record);
while(logs.length > 100) {
	logs.removeFirst();
}
});

字体设计

创建任何应用程序时,字体都是必不可少的,因为文本存在于我们的应用程序中。大多数应用程序80%都是文本内容,所以外观和感觉对于好的应用程序设计非常重要。

处理排版造型的一种方法是下载你希望在应用程序中使用的字体,映射你的应用程序以访问pubspec.yaml 文件中的字体,然后将字体家族添加到TextStyle小部件中。

在这一节中,我们将了解如何使用Google字体包来实现字体,它使我们能够访问Google提供的字体库。

我们通过把它添加到我们的pubspec.yaml 文件中来安装这个包,然后把它导入我们要使用它的文件中。

dependencies:
 flutter:
   sdk: flutter
google_fonts: ^2.1.1

我们通过所提供的谷歌字体来使用它。

Text(
  'This is Google Fonts',
  style: GoogleFonts.Montserrat(),
),

另外,如果我们想动态地加载它,我们可以使用下面的代码。

Text(
  'This is Google Fonts',
  style: GoogleFonts.getFont(‘Montserrat’),
),

总结

在本教程中,我们已经了解了什么是缓存,什么时候缓存数据最好,如何实现一个简单的缓存机制,缓存的好处,以及为什么我们的应用程序需要缓存。

在第二部分,我们学习了在实际应用中如何理想地进行日志记录,以及如何实现包含更多细节和指定日志级别的日志记录,这些都是每种编程语言的基本日志记录器所不能提供的。