Flutter is in its second major release, Flutter2, and one of the biggest changes is new support for Web production quality, which has moved smoothly from Beta to positive.
As the saying goes, “If you are a mule or a horse, pull it out for a walk”, it is necessary to write a project to verify it.
Since at the time of writing this article, the project has been completed, the project results can be experienced first: webdemo.loveli.site
Project source code: Swiftdo/Web-demo
This project will refer to the function of OldBirds, my wechat small program, to realize the page of article list, article details, classified article list and other pages, and the data will be dynamically obtained through API.
OldBirds mini program in addition to update their own blog, will also recommend some quality articles for you to read, welcome to experience
So let’s go through the implementation of this project from scratch. Because it’s not easy to go from zero to one, there are N articles. The general contents are as follows:
- Project structures,
- Encapsulation of network requests
- Encapsulation of the project environment
- Implement home page, request cross-domain issues
- State management encapsulation
- The page adapter
- Encapsulation of routing 2.0
- Url strategy
- The project is packaged and deployed online
Set up the environment and create the initial project
Because I am used to each project with its own version of the Flutter, I use FVM for the version management of the Flutter. If you’re not familiar with how to use FVM, don’t hesitate to read my previous post:
- Smooth withdrawal of Flutter 2.0, web first experience
- FVM enjoyable toggle version of the Flutter, highly recommended!
The general command for creating a project is as follows:
$ mkdir web-demo # Create directory
$ cd web-demo # enter directory
$ fvm install stable Install the version of flutter Stable Channel
$ fvm use stable --force Web-demo uses stable version
$ fvm flutter create . # Generate project with web-demo as project name
$ fvm flutter run -d Chrome # Run to Chrome
Copy the code
When the project runs successfully and the browser automatically opens to display the page, it means that we have successfully created the Web-Demo project. The next is to add to the project, to add blood.
Project structure planning
So let’s build the skeleton of the project together:
- Assets: Images, files, fonts and other resource files
- Components: Stores common components and focuses on business
- Config: environment configuration of the project, such as debug, Product, preview environment configuration
- Core: A lightweight utility class, or common component, that can be easily portable to other projects
- Models: Model class, JSON data parsing
- Pages: pages
- The router: routing
- Services: encapsulation of some third-party libraries, network requests, etc
- Style: common style, color, font, size, etc
The above directory planning is based on your own experience, you can also structure your own project. But components, pages, routing, resources, environment, and services are basically industry consensus, and many projects are divided this way.
Now that we’ve done the basic division, where do we start?
Usually in development, we will first have the UI design draft and requirements document, and then we start to write static UI, when the back-end student interface is completed, continue to connect the interface, and then test, change bugs, release version.
This project is special, with existing APIS and data, so we can prioritize the encapsulation of network requests.
Network encapsulation
The Dio plugin is usually used for Flutter web requests.
Create the api.dart file in the Servers directory and define an interface API:
abstract class Api {
/// Get a list of articles
/// [categoryId] is the categoryId of the article
Future<Map> fetchArticleList({int pageNo, int pageSize = 20.String categoryId});
/// Get article details
Future<Map> fetchArticleDetail({String articleId});
}
Copy the code
Then we define an implementation class:
class ApiImpl implements Api {
Dio _dio;
ApiImpl() {
_dio = Dio(
BaseOptions(baseUrl: 'baseurl.com', connectTimeout: 20000, receiveTimeout: 20000)); }/// Interface request
Future<Map> fetchArticleList({int pageNo, int pageSize = 20.String categoryId}) async {
final response = await _dio.get('list', queryParameters: {
'pageNo': pageNo,
'pageSize': pageSize,
"category_id": categoryId,
});
Map data = response.data;
return data;
}
Future<Map> fetchArticleDetail({String articleId}) async {
final response = await _dio.get('detail', queryParameters: {
'article_id': articleId,
});
Map data = response.data;
return ValueUtil.toMap(data['data']); }}Copy the code
This is how we completed the secondary encapsulation of DIO. Extract the Api base class and implement it in ApiImpl. The nice thing about encapsulation is that you don’t need dio details where the Api is called, and then if you’re using another request library instead of DIO, you just need to change the implementation of the ApiImpl.
Are there any glaring problems with the above code? Let’s analyze it together
Problem analysis
Baseurl problem
There is a problem with baseurl.com in ApiImpl’s code. Because in the development environment, we might use localhost, but the online environment is baseurl.com.
Soon someone will say that you can distinguish between a formal environment and a development environment by using kDebugMode.
BaseOptions(baseUrl: kDebugMode ? 'localhost': 'baseurl.com', connectTimeout: 20000, receiveTimeout: 20000),
Copy the code
If you only have two circumstances, you can do that. However, if one day, a new pre-release environment is added, then kDebugMode will not be useful at this time, and there will be no bool type to distinguish the three cases.
In another case, in addition to baseUrl, other configurations such as connectTimeout may require different values, so there will be a lot of kDebugMode. :.
How to solve it?
The analysis of Baseurl leads to two questions:
- The value of baseurl depends on the environment
- If there are multiple values that are context-dependent, a lot of judgment needs to be made
Suppose our current baseurl has three:
- When debugging the environment, the value is a.com
- In the Preview environment, it’s b.com
- In the Product environment, it’s c.com
We started with baseurl, but this time we’re thinking about the environment. If the environment is determined, so is baseurl. We can think in this direction.
So how do we identify the environment?
Usually, a lot of people do this:
- Env == 1, debug environment
- Env == 2, preivew environment
- Env == 3, product environment
You then determine the environment by setting the value of env (some people also use enumerations).
if (env == 1) {
baseurl = "a.com";
} else if (env == 2) {
baseurl = "b.com"
} else if (env == 3) {
baseurl = "c.com"
}
Copy the code
And that does what we’re trying to do, but when it comes to the environment, it’s full of if else judgments, which are not very elegant. We don’t like it.
If you want a baseurl, let’s give it to you:
abstract class Config {
String get baseurl; /// That's what we want
}
Copy the code
Since there is only one environment for the entire application, we can treat it as a global variable:
Config config = Config();
Copy the code
But Config is an abstract class, so we can’t assign a value directly. We need an implementation class for Config, because we have three environments, so we implement three Config subclasses:
class ConfigDebug extends Config {
@override
String get baseurl => "a.com";
}
class ConfigPreview extends Config {
@override
String get baseurl => "b.com";
}
class ConfigProduct extends Config {
@override
String get baseurl => "c.com";
}
Copy the code
If this is the debug environment, then:
Config config = ConfigDebug();
Copy the code
We then call config.baseurl directly where we need to use baseurl, and we don’t need any criteria anymore. If we also need a client environment, we can simply create a Config implementation class.
ConnectTimeout = connectTimeout = connectTimeout = connectTimeout
abstract class Config {
String get baseurl; /// That's what we want
int get connectTimeout = 2000;
}
class ConfigProduct extends Config {
@override
String get baseurl => "c.com";
@override
int get connectTimeout = 6000;
}
Copy the code
The above code implements a connectTimeout of 2000 for the Debug and Preview environments, and 6000 for the Product environment.
Isn’t this much more elegant than global env control?
Call the problem
How to use the ApiImpl that we encapsulate? I’m sure you’ve seen or written code like this:
// home.dart
getList() async{
var res = await ApiImpl().fetchArticleList(pageNo: pageNo);
//....
}
Copy the code
In this way, the request of the interface can indeed be completed, and the project runs perfectly. But have you thought of a more elegant solution? Doesn’t the Api function as an interface? A lot of people will say, well, what’s the use of an Api abstract class? I don’t have this class, so I’m just going to class ApiImpl {}.
Is it really worthless? Let’s look at the following code:
// home.dart
class Home {
Home({this.api})
final Api api;
getList() async{
var res = await api.fetchArticleList(pageNo: pageNo);
//....
}
}
Home(api: ApiImpl())
Copy the code
Home only relies on the Api and does not need to be associated with the ApiImpl. If A needs to complete A function during development, but the function depends on B’s code, but B hasn’t had time to implement it. At this point, we need to abstract the functionality we want into an interface and then rely on the abstract base class so that the code can compile without providing an implementation. Of course, we can also write a temporary implementation to get the code running. When someone else has time, or someone else’s module has been written, docking with the corresponding interface can be implemented.
class ApiMockImpl implements Api {}
// Home(api: ApiMockImpl())
Home(api: ApiImpl())
Copy the code
This way, you can write code without being delayed by others, and the flexibility of your code increases.
In the above code, it would be easy to change if you had only one class, Home, but if you had a network request, it would be scattered in N places, so you would have to replace ApiMockImpl with ApiImpl, which is a pain. It would be nice if we could just change one place, so we gave an implementation for this problem.
Dependency injection
Config config = ConfigDebug();
Copy the code
Global variables are the quickest and most convenient way for us to “synchronize” program data.
- The memory address is fixed, and the read and write efficiency is high.
- Globally visible, any function or thread can read or write global variables
Very simple and flexible, and then too free and the riskier the modifications. Global variables break the encapsulation performance of functions. Because global variables can be used by multiple functions and their values can change at any time during function execution, the same input does not necessarily have the same output. For the program error checking and debugging are very bad, the reliability is greatly reduced.
It is best not to use global variables unless you absolutely have toCopy the code
So what do we do? Singletons can be used. But our Config is not suitable as a singleton. So we need a singleton object with Config as one of its properties.
class SomeSharedInstance {
// The singleton exposes the access point
factory SomeSharedInstance() =>_sharedInstance()
// Static private member, not initialized
static SomeSharedInstance _instance;
// Private constructor
SomeSharedInstance._() {
// The concrete initialization code
}
// Static, synchronous, private access point
static SomeSharedInstance _sharedInstance() {
if (_instance == null) {
_instance = SomeSharedInstance._();
}
return _instance;
}
Config config;
}
Copy the code
Then, when using config, we need to do something similar:
SomeSharedInstance() .. config = ConfigDebug();// SomeSharedInstance().config.baseurl;
Copy the code
I personally think a common application on a singleton is basically enough.
At this point, I strongly recommend the get_IT plug-in, which is very suitable for our current situation. Decouple the created code.
A Service Locator Pattern is a design Pattern in software development that encapsulates the process involved in trying to acquire a Service by applying a powerful abstraction layer. This pattern uses a central registry called a “Service Locator” to process requests and return the necessary information to process a particular task. From: Service Locator mode
Create locator. Dart in the lib directory:
GetIt locator = GetIt.instance;
setupLocator() {
// Configure the project environment
if (kDebugMode) {
locator.registerSingleton<Config>(ConfigDebug());
} else {
locator.registerSingleton<Config>(ConfigProduct());
}
/// So that's what I'm going to do. I'm going to do a global substitution
locator.registerLazySingleton<Api>(() => ApiImpl());
}
Copy the code
This implements the registration of the service. Then call setupLocator() in main.dart:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await setupLocator();
runApp(MyApp());
}
Copy the code
If you need to use services and need to obtain the Config configuration, directly call locator
() :
class ApiImpl implements Api {
Dio _dio;
ApiImpl() {
_dio = Dio(
BaseOptions(baseUrl: locator<Config>().baseUrl, connectTimeout: 20000, receiveTimeout: 20000)); }}Copy the code
It also solves the problem that ApiImpl calls can be modified multiple times.
getList() async{
var res = await locator<Api>().fetchArticleList(pageNo: pageNo);
//....
}
Copy the code
For more information about get_it, see its documentation.
Chapter summary
In this article, we take you to realize:
- Encapsulation of network requests
- Encapsulation of the project environment
In the process of encapsulation, we are constantly making the code more elegant and flexible. Design is a process of continuous iteration, continuous optimization, thinking can be closer to the goal.
In short, remember: an architecture based on abstractions is much more stable than one based on details, so when you get the requirements, code in the face of the interface, first top-level design and then detailed design code structure.
Finally, the source code of this project has been uploaded to Github: Swiftdo /web-demo
If you want to join the wechat group, please follow the wechat official account: OldBirds
Of course, the article may have improper understanding of the place, welcome to point out. Stay tuned for the next chapter on state management!