最近,在使用 Flutter 做一個圖片分享的應(yīng)用,自己創(chuàng)建出一套 Flutter 版的 MVVM 開發(fā)模式跨算,覺得還挺好用,所以在此分享出來椭懊。
應(yīng)用功能展示
首先,我們來看看我們這套MVVM開發(fā)模式步势,開發(fā)出來的應(yīng)用是個什么樣子氧猬,大概的一部分功能如下:(也可以點擊觀看 演示視頻)
下拉刷新,如圖:
上拉加載更多坏瘩,如圖:
點贊盅抚,如圖:
缺省頁(空數(shù)據(jù)),如圖:
loading頁倔矾,如圖:
漸變的Appbar妄均,如圖:
評論,如圖:
我的頁面哪自,如圖:
以上只是 App 的一部分功能丰包,大家也可以也可以點擊觀看 演示視頻,或者掃描二維碼下載 App(android) 體驗:
Tip:App 可以使用登錄賬戶信息:
任意一個作者名字壤巷,密碼都是:666666邑彪,如:
用戶名 | 密碼 |
---|---|
persilee | 密碼:666666 |
攝影師蟈蟈小姐 | 密碼:666666 |
翠花小拍 | 密碼:666666 |
在介紹這套 MVVM 開發(fā)模式之前,我們首先需要了解 riverpod
和 retrofit
是什么胧华。
下面我們來分別了解他們是什么寄症。
riverpod
riverpod 是 Flutter 狀態(tài)管理庫,flutter 的狀態(tài)管理庫有很多矩动,例如: Redux
有巧、 Bloc
、 Provider
等悲没,flutter 官方推薦我們使用 provider
篮迎,一般我們使用 provider
的時候,會結(jié)合 ChangeNotifier
檀训、 StateNotifier
柑潦、 freezed
去使用,而 riverpod
是 provider
的一個升級加強(qiáng)版峻凫,解決了 provider
一些疑難雜癥渗鬼,在這里就不過多介紹梢薪,如想了解更多 riverpod
信息何荚,可以訪問 riverpod官網(wǎng) ,也可以參考我之前寫的以下Demo 。
retrofit
retrofit 是一個網(wǎng)絡(luò)請求庫晒骇,做過 android 的同學(xué)應(yīng)該比較熟悉,可以用注解的方式生成請求 Rest Api 的各種方法仓犬,如侠驯,以下的簡單的用法:
import 'package:retrofit/retrofit.dart';
part 'api_client.g.dart';
@RestApi(baseUrl: 'https://api.lishaoy.net')
abstract class ApiClient {
factory ApiClient({Dio dio, String baseUrl}) {
dio ??= BaseDio.getInstance().getDio();
return _ApiClient(dio, baseUrl: baseUrl);
}
/**
* 獲取首頁推薦文章
*/
@GET('/posts')
Future<PostModel> getPosts(
@Query('pageIndex') String pageIndex, @Query('pageSize') String pageSize,
{@Query('sort') String sort = 'recommend'});
/**
* 獲取文章詳情
*/
@GET('/posts/{postId}')
Future<SinglePostModel> getPostsById(@Path('postId') int postId,
{@Query('notView') bool notView});
/**
* 登錄
*/
@POST('/login')
Future<LoginModel> login(@Body() Login login);
}
更多詳情可以訪問 pub.dev retrofit 。
目錄結(jié)構(gòu)
接下來我們來看看項目的目錄結(jié)構(gòu)镐侯,如下:
.
├── android ## 原生android目錄
│ ├── app
│ └── gradle
├── assets ## 資源文件目錄
│ ├── fonts
│ ├── images
│ └── json
├── ios ## 原生iOS目錄
│ ├── Flutter
│ ├── Frameworks
│ ├── Pods
│ ├── Runner
│ ├── Runner.xcodeproj
│ └── Runner.xcworkspace
└── lib ## 項目文件目錄
├── http ##對網(wǎng)格請求相關(guān)的封裝
│ ├── api_client.dart ## rest api 請求類
│ ├── api_client.g.dart ## retrofit 自動生成的類
│ ├── base_dio.dart ## 對dio封裝類
│ ├── base_error.dart ## 服務(wù)端基本錯誤類型封裝類
│ └── header_interceptor.dart ##網(wǎng)絡(luò)請求攔截器
├── models ## json序列化的model類侦讨,相對于MVVM的 M 層
├── pages ## 主要的UI頁面目錄,相對于MVVM的 V 層
├── utils ## 一些工具類
│ ├── date_util.dart
│ ├── screen_util.dart
│ ├── status_bar_util.dart
│ ├── timeline_util.dart
│ └── widget_util.dart
├── view_model ## 處理數(shù)據(jù)狀態(tài)苟翻,業(yè)務(wù)邏輯韵卤,相對于 MVVM的 VM 層
│ ├── details_view_model.dart
│ ├── login_view_model.dart
│ ├── posts_view_model.dart
│ └── profile_view_model.dart
└── widgets ##公用或自定義組件
├── cache_image.dart
├── custom_circular_rect_angle.dart
├── custom_indicator.dart
├── custom_tabs.dart
├── error_page.dart
├── gradient_button.dart
├── icon_animation_widget.dart
├── iconfont.dart
├── image_paper.dart
├── over_scroll_behavior.dart
├── page_state.dart
├── per_flexible_space_bar.dart
├── pic_swiper.dart
└── refresh.dart
從目錄結(jié)構(gòu)可知, models
崇猫、 pages
沈条、view_model
分別是 MVVM 開發(fā)模式的 M(數(shù)據(jù)層)、 V(視圖層)诅炉、 VM(通過riverpod的StateNotifier將數(shù)據(jù)層和視圖層綁定蜡歹,state變化時數(shù)據(jù)層也跟著變化,當(dāng)然這里也可以處理一些頁面邏輯)涕烧,做過 android 的同學(xué)應(yīng)該知道 android 的MVVM是使用 jetpack 組件庫里的 DataBinding 和 LiveData 完成的月而,我這套開發(fā)模式靈感就是來源于此。
網(wǎng)絡(luò)請求模塊
首先议纯,我們來對網(wǎng)絡(luò)請求模塊封裝一把景鼠,讓它能夠通用易用。
retrofit 是依賴網(wǎng)絡(luò)請求庫的痹扇,我們可以選擇不同的庫铛漓,例如:http
、Dio
等鲫构。
在這里我們選擇 Dio
浓恶,如下,是官方提供的案例代碼:
@RestApi(baseUrl: "https://5d42a6e2bc64f90014a56ca0.mockapi.io/api/v1/")
abstract class RestClient {
factory RestClient(Dio dio, {String baseUrl}) = _RestClient;
@GET("/tasks")
Future<List<Task>> getTasks();
}
Dio的封裝
它需要傳一個 Dio 的實例和一個可選的 baseUrl结笨,我們需要對這里重新封裝一下包晰,使用者不用傳遞任何參數(shù)就可以使用,也可以選擇使用不同的網(wǎng)絡(luò)庫和 baseUrl炕吸;所以伐憾,我們要封裝一個 baseDio
單例類,如果用戶沒有傳赫模,我們就傳遞一個默認(rèn)的 baseDio
類树肃,代碼大概如下所示:
@RestApi(baseUrl: 'https://api.lishaoy.net')
abstract class ApiClient {
factory ApiClient({Dio dio, String baseUrl}) {
dio ??= BaseDio.getInstance().getDio();
return _ApiClient(dio, baseUrl: baseUrl);
@POST('/login')
Future<LoginModel> login(@Body() Login login);
}
所以我要對 Dio
進(jìn)行一次封裝,代碼如下:
import 'package:dio/dio.dart';
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
import 'package:pro_flutter/http/base_error.dart';
import 'package:pro_flutter/http/header_interceptor.dart';
class BaseDio {
BaseDio._(); // 把構(gòu)造方法私有化
static BaseDio _instance;
static BaseDio getInstance() { // 通過 getInstance 獲取實例
_instance ??= BaseDio._();
return _instance;
}
Dio getDio() {
final Dio dio = Dio();
dio.options = BaseOptions(receiveTimeout: 66000, connectTimeout: 66000); // 設(shè)置超時時間等 ...
dio.interceptors.add(HeaderInterceptor()); // 添加攔截器瀑罗,如 token之類胸嘴,需要全局使用的參數(shù)
dio.interceptors.add(PrettyDioLogger( // 添加日志格式化工具類
requestHeader: true,
requestBody: true,
responseBody: true,
responseHeader: false,
compact: false,
));
return dio;
}
BaseError getDioError(Object obj) { // 這里封裝了一個 BaseError 類雏掠,會根據(jù)后端返回的code返回不同的錯誤類
switch (obj.runtimeType) {
case DioError:
if ((obj as DioError).type == DioErrorType.RESPONSE) {
final response = (obj as DioError).response;
if (response.statusCode == 401) {
return NeedLogin();
} else if (response.statusCode == 403) {
return NeedAuth();
} else if (response.statusCode == 408) {
return UserNotExist();
} else if (response.statusCode == 409) {
return PwdNotMatch();
} else if (response.statusCode == 405) {
return UserNameEmpty();
} else if (response.statusCode == 406) {
return PwdEmpty();
} else {
return OtherError(
statusCode: response.statusCode,
statusMessage: response.statusMessage,
);
}
}
}
return OtherError();
}
}
BaseError的封裝
以上代碼中的 BaseError
類是一個抽象類,我們可以實現(xiàn)這個抽象類劣像,告訴UI不同的錯誤類型乡话,UI只需要用實現(xiàn)類就可以訪問錯誤碼和錯誤消息,代碼如下:
abstract class BaseError {
final int code;
final String message;
BaseError({this.code, this.message});
}
class NeedLogin implements BaseError {
@override
int get code => 401;
@override
String get message => "請先登錄";
}
class NeedAuth implements BaseError {
@override
int get code => 403;
@override
String get message => "非法訪問耳奕,請使用正確的token";
}
class UserNotExist implements BaseError {
@override
int get code => 408;
@override
String get message => "用戶不存在";
}
class UserNameEmpty implements BaseError {
@override
int get code => 405;
@override
String get message => "用戶名不能為空";
}
class PwdNotMatch implements BaseError {
@override
int get code => 409;
@override
String get message => "用戶密碼不正確";
}
class PwdEmpty implements BaseError {
@override
int get code => 406;
@override
String get message => "用戶密碼不能為空";
}
class OtherError implements BaseError {
final int statusCode;
final String statusMessage;
OtherError({this.statusCode, this.statusMessage});
@override
int get code => statusCode;
@override
String get message => statusMessage;
}
網(wǎng)絡(luò)模塊的使用
這樣我們的一個網(wǎng)絡(luò)請求模塊基本就封裝好了绑青,使用起來非常簡單,首先我們需要定義接口屋群,代碼如下:
@RestApi(baseUrl: 'https://api.lishaoy.net')
abstract class ApiClient {
factory ApiClient({Dio dio, String baseUrl}) {
dio ??= BaseDio.getInstance().getDio();
return _ApiClient(dio, baseUrl: baseUrl);
}
/**
* 獲取首頁推薦文章
*/
@GET('/posts')
Future<PostModel> getPosts(
@Query('pageIndex') String pageIndex, @Query('pageSize') String pageSize,
{@Query('sort') String sort = 'recommend'});
/**
* 獲取文章詳情
*/
@GET('/posts/{postId}')
Future<SinglePostModel> getPostsById(@Path('postId') int postId,
{@Query('notView') bool notView});
/**
* 登錄
*/
@POST('/login')
Future<LoginModel> login(@Body() Login login);
/**
* 點贊
*/
@POST('/posts/{postId}/like')
Future<BaseModel> like(@Path('postId') int postId);
...
然后时迫,我們會在 view model 使用它,如下:
/**
* 點贊
*/
Future<void> clickLike(int postId, int index) async {
try {
BaseModel data = await ApiClient().like(postId); // 使用非常簡單一句代碼即可
if (data.message == 'success') {
updatePostById(postId, index);
}
} catch (e) {
state = state.copyWith(
pageState: PageState.errorState,
error: BaseDio.getInstance().getDioError(e));
}
}
View Model 模塊
View Model 模塊主要處理數(shù)據(jù)和狀態(tài)的綁定谓晌、業(yè)務(wù)邏輯等。
創(chuàng)建狀態(tài)類
我們首先需要創(chuàng)建一個狀態(tài)類癞揉,來存放數(shù)據(jù)狀態(tài)和頁面狀態(tài)等纸肉,如下:
/// 存儲頁面狀態(tài)和數(shù)據(jù)狀態(tài)(如,缺省頁喊熟、錯誤頁柏肪、加載中...)
class PostState {
final List<Post> posts;
final List<Category> categories;
final int pageIndex;
final PageState pageState; // 頁面狀態(tài)類
final BaseError error; // 根據(jù)后端返回的錯誤的錯誤類
PostState(
{this.posts,
this.categories,
this.pageIndex,
this.pageState,
this.error});
PostState.initial()
: posts = [],
categories = [],
pageIndex = 1,
pageState = PageState.initializedState,
error = null;
PostState copyWith({
List<Post> posts,
List<Category> categories,
int pageIndex,
PageState pageState,
BaseError error,
}) {
return PostState(
posts: posts ?? this.posts,
categories: categories ?? this.categories,
pageIndex: pageIndex ?? this.pageIndex,
pageState: pageState ?? this.pageState,
error: error ?? this.error,
);
}
}
當(dāng)然這個狀態(tài)類也可以用 freezed
自動生成。
請求網(wǎng)絡(luò)數(shù)據(jù)和處理頁面狀態(tài)
我們會返回這個狀態(tài)類給UI芥牌,riverpod 的 StateNotifier 會監(jiān)聽這個狀態(tài)類里的所有成員變量烦味,當(dāng)我們更改這些數(shù)據(jù)之后,UI會自動刷新壁拉,代碼如下:
/**
* 獲取文章列表
*/
Future<void> getPosts(int categoryId, {bool isRefresh = false}) async {
if (state.pageState == PageState.initializedState) {
state = state.copyWith(pageState: PageState.busyState); // UI收到這個狀態(tài)可以呈現(xiàn)loading頁面
}
try {
if (isRefresh) { // 下拉刷新
PostModel postModel;
if(categoryId == -2) {
state = state.copyWith(pageState: PageState.emptyDataState); // UI收到這個狀態(tài)谬俄,可以顯示缺省頁空數(shù)據(jù)
return;
} else if (categoryId == -1) {
postModel = await ApiClient().getPosts('1', '10'); // 請求網(wǎng)絡(luò)接口
} else {
postModel =
await ApiClient().getPostsByCategoryId('1', '10', categoryId);
}
if (postModel.data.posts.isEmpty && state.pageIndex == 1) {
state = state.copyWith(pageState: PageState.emptyDataState);
} else {
initPostState();
state = state.copyWith(
posts: [...postModel.data.posts], // 把數(shù)據(jù)發(fā)給UI
pageState: PageState.refreshState, // 更改頁面狀態(tài)為刷新
pageIndex: 2,
);
}
} else { // 下拉加載更多
PostModel postModel;
if(categoryId == -2) {
state = state.copyWith(pageState: PageState.emptyDataState); // UI收到這個狀態(tài)可以呈現(xiàn)loading頁面
return;
} else if (categoryId == -1) {
postModel =
await ApiClient().getPosts(state.pageIndex.toString(), '10'); // 請求網(wǎng)絡(luò)接口
} else {
postModel = await ApiClient().getPostsByCategoryId(
state.pageIndex.toString(), '10', categoryId);
}
if (postModel.data.posts.isEmpty && state.pageIndex == 1) {
state = state.copyWith(pageState: PageState.emptyDataState);
} else {
state = state.copyWith(
posts: [...state.posts, ...postModel.data.posts], // 把數(shù)據(jù)發(fā)給UI
pageIndex: state.pageIndex + 1,
pageState: PageState.dataFetchState); // 更改頁面狀態(tài)
if (postModel.data.posts.isEmpty ||
postModel.data.posts.length < 10) {
state = state.copyWith(pageState: PageState.noMoreDataState);
}
}
}
} catch (e) {
state = state.copyWith(
pageState: PageState.errorState, // 如果發(fā)生錯誤,更改頁面狀態(tài)
error: BaseDio.getInstance().getDioError(e));
}
}
以上一個方面就完成了應(yīng)用首頁的所有列表數(shù)據(jù)請求和頁面狀態(tài)處理弃理,在UI層溃论,不需要寫 setState() 和 請求數(shù)據(jù)的任何代碼,UI層只是呈現(xiàn)UI痘昌。
View 模塊
那么在UI層怎么處理這些狀態(tài)呢钥勋?
這也非常簡單,代碼如下:
// 創(chuàng)建provider辆苔,返回viewModel
final postsProvider = StateNotifierProvider.family<PostsViewModel, int>(
(ref, categoryId) => PostsViewModel(categoryId));
class PostsPageCategory extends ConsumerWidget { // 繼承 ConsumerWidget
final int categoryId;
final ScrollController scrollController;
final RefreshController refreshController;
PostsPageCategory(
{this.categoryId, this.scrollController, this.refreshController});
@override
Widget build(BuildContext context, ScopedReader watch) {
final postsViewModel = watch(postsProvider(categoryId)); // 使用 watch 來監(jiān)聽Provider
final postState = watch(postsProvider(categoryId).state); // 使用 watch 來監(jiān)聽Provider的狀態(tài)
return Refresh(
controller: refreshController,
onLoading: () async { // 加載更多處理
await postsViewModel.getPosts(categoryId);
if (postState.pageState == PageState.noMoreDataState) {
refreshController.loadNoData();
} else {
refreshController.loadComplete();
}
},
onRefresh: () async { // 刷新處理
await context
.read(postsProvider(categoryId))
.getPosts(categoryId, isRefresh: true);
refreshController.refreshCompleted();
refreshController.footerMode.value = LoadStatus.canLoading;
},
content: _createContent(postState, context),
);
}
Widget _createContent(PostState postState, BuildContext context) {
if (postState.pageState == PageState.busyState ||
postState.pageState == PageState.initializedState) { // loading 狀態(tài)處理
return Center(
child: Lottie.asset(
'assets/json/loading2.json',
width: 126,
fit: BoxFit.cover,
alignment: Alignment.center,
),
);
}
if (postState.pageState == PageState.emptyDataState) {
return ErrorPage( // 錯誤處理
isEmptyPage: true,
icon: Lottie.asset(
'assets/json/empty3.json',
width: ScreenUtil.instance.width / 1.8,
height: 220,
fit: BoxFit.contain,
alignment: Alignment.center,
),
desc: '暫 無 數(shù) 據(jù)',
buttonAction: () => context.refresh(postsProvider(categoryId)),
);
}
if (postState.pageState == PageState.errorState) {
return ErrorPage(
title: postState.error is NeedLogin
? '?? 你竟然忘記登錄 ??'
: postState.error.code?.toString(),
desc: postState.error.message,
buttonAction: () async {
if (postState.error is NeedLogin) {
LoginState loginState = await Navigator.of(context).push(
MaterialPageRoute(builder: (context) => FlareSignInDemo()));
if (loginState.isLogin) {
context.refresh(postsProvider(categoryId));
}
} else {
context.refresh(postsProvider(categoryId));
}
},
buttonText: postState.error is NeedLogin ? '登錄' : null,
);
}
return ListView.separated( // 加載數(shù)據(jù)算灸,現(xiàn)在頁面
shrinkWrap: true,
separatorBuilder: (context, index) {
return Padding(padding: EdgeInsets.only(top: 12));
},
padding: EdgeInsets.fromLTRB(12, 18, 12, 18),
reverse: false,
itemCount: postState.posts.length,
controller: scrollController,
itemBuilder: (BuildContext context, int index) {
return PostsPageItem(
post: postState.posts[index],
index: index,
categoryId: categoryId,
);
},
);
}
}
是不是非常簡單,不需要寫 setState() 和 請求數(shù)據(jù)的任何代碼驻啤,代碼結(jié)構(gòu)也非常清晰菲驴。在上述APP應(yīng)用里的首頁以及分類頁面列表數(shù)據(jù)及頁面的loading和缺省頁等都是這一個簡單 PostsPageCategory
完成的。
其他相關(guān)
以上這套開發(fā)模式我給出了大概的思路和部分代碼骑冗,大家也可以順著這個思路試試谢翎;這套開發(fā)模式后續(xù)還會繼續(xù)優(yōu)化它捍靠。
應(yīng)用功能相關(guān)
用過 Flutter TabBar 同學(xué)應(yīng)該知道,它在字體放大時會卡頓森逮,以及如何自定義指示器等榨婆, 如圖:
以及,漸變的高斯模糊背景和圖片標(biāo)題動畫的實現(xiàn)等褒侧,如圖:
及更多這個應(yīng)用的功能實現(xiàn)和細(xì)節(jié)并沒有在這里講述良风,這篇文章主要介紹 MVVM,關(guān)于這個圖片分享APP闷供,只是我在業(yè)余時間對Flutter的研究探索和學(xué)習(xí)烟央,這個應(yīng)用大概只完成了一半,后續(xù)應(yīng)該還好寫關(guān)于這個APP的文章歪脏。
REST API接口相關(guān)
還有疑俭,這個APP的后端API也是我自己開發(fā)的,使用的是 nodejs 的 express + ts 開發(fā)的婿失,如首頁推薦接口及分類頁接口數(shù)據(jù)都是通過這個API查詢到的: 首頁API接口
具體的實現(xiàn)是使用一條SQL語句查詢得到钞艇,代碼如下:
SELECT
post.id,
post.content,
post.title,
category.name as category,
post.views,
JSON_OBJECT(
'id', user.id,
'name', user.name,
'avatar', CAST(
IF(COUNT(avatar.id),
GROUP_CONCAT(
DISTINCT JSON_OBJECT(
'largeAvatarUrl', concat('http://localhost:3001/avatar/', user.id, '|@u003f|size=large'),
'mediumAvatarUrl', concat('http://localhost:3001/avatar/', user.id, '|@u003f|size=medium'),
'smallAvatarUrl', concat('http://localhost:3001/avatar/', user.id, '|@u003f|size=small')
)
),
NULL)
AS JSON)
) as user,
(
SELECT COUNT(comment.id) FROM comment
WHERE comment.postId = post.id
GROUP BY comment.postId
) as totalComments,
CAST(
IF(
COUNT(cover.id),
GROUP_CONCAT(
DISTINCT JSON_OBJECT(
'id', cover.id,
'width', cover.width,
'height', cover.height,
'largeImageUrl', concat('http://localhost:3001/files/', cover.id, '/serve|@u003f|size=large'),
'mediumImageUrl', concat('http://localhost:3001/files/', cover.id, '/serve|@u003f|size=medium'),
'small', concat('http://localhost:3001/files/', cover.id, '/serve|@u003f|size=thumbnail')
) ORDER BY cover.id DESC
),
NULL
) AS JSON
) AS coverImage,
CAST(
IF(
COUNT(file.id),
CONCAT(
'[',
GROUP_CONCAT(
DISTINCT JSON_OBJECT(
'id', file.id,
'width', file.width,
'height', file.height,
'largeImageUrl', concat('http://localhost:3001/files/', file.id, '/serve|@u003f|size=large'),
'mediumImageUrl', concat('http://localhost:3001/files/', file.id, '/serve|@u003f|size=medium'),
'small', concat('http://localhost:3001/files/', file.id, '/serve|@u003f|size=thumbnail')
) ORDER BY file.id DESC
),
']'
),
NULL
) AS JSON
) AS files,
CAST(
IF(
COUNT(tag.id),
CONCAT(
'[',
GROUP_CONCAT(
DISTINCT JSON_OBJECT(
'id', tag.id,
'name', tag.name
)
),
']'
),
NULL
) AS JSON
) AS tags,
(
SELECT COUNT(user_like_post.postId)
FROM user_like_post
WHERE user_like_post.postId = post.id
) AS totalLikes
FROM post
LEFT JOIN user
ON user.id = post.userId
LEFT JOIN avatar
ON avatar.userId = user.id
LEFT JOIN LATERAL (
SELECT * FROM file
WHERE file.postId = post.id
ORDER BY file.id DESC
LIMIT 9
) AS file ON file.postId = post.id
LEFT JOIN LATERAL(
SELECT * FROM file
WHERE file.isCover = 1 AND file.postId = post.id
GROUP BY file.id
LIMIT 1
) AS cover ON cover.postId = post.id and cover.isCover = 1
LEFT JOIN post_tag
ON post_tag.postId = post.id
LEFT JOIN tag
ON tag.id = post_tag.tagId
LEFT JOIN category
ON post.categoryId = category.id
WHERE post.id IS NOT NULL
GROUP BY post.id
ORDER BY post.id DESC
LIMIT 10
OFFSET 0
這個是打印出來的log,具體的代碼如下(可根據(jù)不同的參數(shù)查詢不同的數(shù)據(jù))豪硅,如下:
export const getPosts = async (options: GetPostOptions) => {
const {
sort,
filter,
pagination: { limit, offset },
userId,
} = options;
let params: Array<any> = [limit, offset];
if (filter.param) {
params = [filter.param, ...params];
}
if (userId) {
params = [userId, ...params];
}
console.log(`params: ${params}`);
const sql = `
SELECT
post.id,
post.content,
post.title,
category.name as category,
post.views,
post.createdAt,
post.updatedAt,
${sqlFragment.user},
${sqlFragment.totalComments},
${sqlFragment.coverImage},
${sqlFragment.file},
${sqlFragment.tags}
${userId ? `, ${sqlFragment.liked} ` : ''},
${sqlFragment.totalLikes}
FROM post
${sqlFragment.leftJoinUser}
${sqlFragment.leftJoinOneFile}
${sqlFragment.leftJoinCover}
${sqlFragment.leftJoinTag}
${sqlFragment.leftJoinCategory}
${filter.name == 'userLiked' ? sqlFragment.innerJoinUserLikePost : ''}
WHERE ${filter.sql}
GROUP BY post.id
ORDER BY ${sort}
LIMIT ?
OFFSET ?
`;
console.log(sql);
const [data] = await connection.promise().query(sql, params);
return data;
};
如果這個后端 REST API 接口應(yīng)用感興趣的同學(xué)可以參考 寧皓網(wǎng) 的視頻哩照,我就是根據(jù)這套視頻做的,不過自己加了很多東西懒浮。
最后飘弧,很多同學(xué)都希望我開源,所以砚著,給出項目的地址次伶,不過項目還沒有完成,架構(gòu)也在優(yōu)化中...后續(xù)可能項目地址會變化稽穆,目前可以先參考以下地址:
項目地址:https://github.com/persilee/flutter_pro
博客地址:https://lishaoy.net
文章地址:https://h.lishaoy.net/fluttermvvm