Flutter 新聞客戶端 - 07 Provider、認證授權、骨架屏、磁碟快取

會煮咖啡的貓發表於2020-06-24

Flutter 新聞客戶端 - 07 Provider、認證授權、骨架屏、磁碟快取

B站視訊

www.bilibili.com/video/BV1vV… www.bilibili.com/video/BV1SA… www.bilibili.com/video/BV1jt… www.bilibili.com/video/BV1wt… www.bilibili.com/video/BV1b5… www.bilibili.com/video/BV11z…

本節目標

  • 第一次登入顯示歡迎介面
  • 離線登入
  • Provider 響應資料管理
  • 實現 APP 色彩灰度處理
  • 登出登入
  • Http Status 401 認證授權
  • 首頁磁碟快取
  • 首頁快取策略,延遲 1~3 秒
  • 首頁骨架屏

視訊

資源

第一次顯示歡迎介面、離線登入

Flutter 新聞客戶端 - 07 Provider、認證授權、骨架屏、磁碟快取

  • lib/global.dart
  /// 是否第一次開啟
  static bool isFirstOpen = false;

  /// 是否離線登入
  static bool isOfflineLogin = false;

  /// init
  static Future init() async {
    ...

    // 讀取裝置第一次開啟
    isFirstOpen = !StorageUtil().getBool(STORAGE_DEVICE_ALREADY_OPEN_KEY);
    if (isFirstOpen) {
      StorageUtil().setBool(STORAGE_DEVICE_ALREADY_OPEN_KEY, true);
    }

    // 讀取離線使用者資訊
    var _profileJSON = StorageUtil().getJSON(STORAGE_USER_PROFILE_KEY);
    if (_profileJSON != null) {
      profile = UserLoginResponseEntity.fromJson(_profileJSON);
      isOfflineLogin = true;
    }
複製程式碼
  • lib/pages/index/index.dart
class IndexPage extends StatefulWidget {
  IndexPage({Key key}) : super(key: key);

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

class _IndexPageState extends State<IndexPage> {
  @override
  Widget build(BuildContext context) {
    ScreenUtil.init(
      context,
      width: 375,
      height: 812 - 44 - 34,
      allowFontScaling: true,
    );

    return Scaffold(
      body: Global.isFirstOpen == true
          ? WelcomePage()
          : Global.isOfflineLogin == true ? ApplicationPage() : SignInPage(),
    );
  }
}
複製程式碼

Provider 實現動態灰度處理

pub.flutter-io.cn/packages/pr…

步驟 1:安裝依賴

dependencies:
  provider: ^4.0.4
複製程式碼

步驟 2:建立響應資料類

  • lib/common/provider/app.dart
import 'package:flutter/material.dart';

/// 系統相應狀態
class AppState with ChangeNotifier {
  bool _isGrayFilter;

  get isGrayFilter => _isGrayFilter;

  AppState({bool isGrayFilter = false}) {
    this._isGrayFilter = isGrayFilter;
  }
}
複製程式碼

步驟 3:初始響應資料

方式一:先建立資料物件,再掛載

  • lib/global.dart
  /// 應用狀態
  static AppState appState = AppState();
複製程式碼
  • lib/main.dart
void main() => Global.init().then((e) => runApp(
      MultiProvider(
        providers: [
          ChangeNotifierProvider<AppState>.value(
            value: Global.appState,
          ),
        ],
        child: MyApp(),
      ),
    ));
複製程式碼

方式二:掛載時,建立物件

  • lib/main.dart
void main() => Global.init().then((e) => runApp(
      MultiProvider(
        providers: [
          ChangeNotifierProvider<AppState>(
            Create: (_) => new AppState(),
          ),
        ],
        child: MyApp(),
      ),
    ));
複製程式碼

步驟 4:通知資料發聲變化

  • lib/common/provider/app.dart
class AppState with ChangeNotifier {
  ...

  // 切換灰色濾鏡
  switchGrayFilter() {
    _isGrayFilter = !_isGrayFilter;
    notifyListeners();
  }
}
複製程式碼

步驟 5:收到資料發聲變化

方式一:Consumer

  • lib/main.dart
void main() => Global.init().then((e) => runApp(
      MultiProvider(
        providers: [
          ChangeNotifierProvider<AppState>.value(
            value: Global.appState,
          ),
        ],
        child: Consumer<AppState>(builder: (context, appState, _) {
          if (appState.isGrayFilter) {
            return ColorFiltered(
              colorFilter: ColorFilter.mode(Colors.white, BlendMode.color),
              child: MyApp(),
            );
          } else {
            return MyApp();
          }
        }),
      ),
    ));
複製程式碼

方式二:Provider.of

  • lib/pages/account/account.dart
    final appState = Provider.of<AppState>(context);

    return Column(
      children: <Widget>[
        MaterialButton(
          onPressed: () {
            appState.switchGrayFilter();
          },
          child: Text('灰色切換 ${appState.isGrayFilter}'),
        ),
      ],
    );
複製程式碼

多個響應資料處理

  • 掛載用 MultiProvider

  • 接收用 Consumer2 ~ Consumer6

登出登入

  • lib/common/utils/authentication.dart
/// 檢查是否有 token
Future<bool> isAuthenticated() async {
  var profileJSON = StorageUtil().getJSON(STORAGE_USER_PROFILE_KEY);
  return profileJSON != null ? true : false;
}

/// 刪除快取 token
Future deleteAuthentication() async {
  await StorageUtil().remove(STORAGE_USER_PROFILE_KEY);
  Global.profile = null;
}

/// 重新登入
Future goLoginPage(BuildContext context) async {
  await deleteAuthentication();
  Navigator.pushNamedAndRemoveUntil(
      context, "/sign-in", (Route<dynamic> route) => false);
}
複製程式碼
  • lib/pages/account/account.dart
class _AccountPageState extends State<AccountPage> {
  @override
  Widget build(BuildContext context) {
    final appState = Provider.of<AppState>(context);

    return Column(
      children: <Widget>[
        Text('使用者: ${Global.profile.displayName}'),
        Divider(),
        MaterialButton(
          onPressed: () {
            goLoginPage(context);
          },
          child: Text('退出'),
        ),
      ],
    );
  }
}
複製程式碼

Http Status 401 認證授權

dio 封裝介面的上下文物件 BuildContext context

  • lib/common/utils/http.dart
  Future post(
    String path, {
    @required BuildContext context,
    dynamic params,
    Options options,
  }) async {
    Options requestOptions = options ?? Options();
    requestOptions = requestOptions.merge(extra: {
      "context": context,
    });
    ...
  }
複製程式碼

錯誤處理 401 去登入介面

  • lib/common/utils/http.dart
    // 新增攔截器
    dio.interceptors
        .add(InterceptorsWrapper(onRequest: (RequestOptions options) {
      return options; //continue
    }, onResponse: (Response response) {
      return response; // continue
    }, onError: (DioError e) {
      ErrorEntity eInfo = createErrorEntity(e);
      // 錯誤提示
      toastInfo(msg: eInfo.message);
      // 錯誤互動處理
      var context = e.request.extra["context"];
      if (context != null) {
        switch (eInfo.code) {
          case 401: // 沒有許可權 重新登入
            goLoginPage(context);
            break;
          default:
        }
      }
      return eInfo;
    }));
複製程式碼

首頁磁碟快取

  • lib/common/utils/net_cache.dart
      // 策略 1 記憶體快取優先,2 然後才是磁碟快取

      // 1 記憶體快取
      var ob = cache[key];
      if (ob != null) {
        //若快取未過期,則返回快取內容
        if ((DateTime.now().millisecondsSinceEpoch - ob.timeStamp) / 1000 <
            CACHE_MAXAGE) {
          return cache[key].response;
        } else {
          //若已過期則刪除快取,繼續向伺服器請求
          cache.remove(key);
        }
      }

      // 2 磁碟快取
      if (cacheDisk) {
        var cacheData = StorageUtil().getJSON(key);
        if (cacheData != null) {
          return Response(
            statusCode: 200,
            data: cacheData,
          );
        }
      }
複製程式碼

首頁快取策略,延遲 1~3 秒

  • lib/pages/main/channels_widget.dart
  // 如果有磁碟快取,延遲3秒拉取更新檔案
  _loadLatestWithDiskCache() {
    if (CACHE_ENABLE == true) {
      var cacheData = StorageUtil().getJSON(STORAGE_INDEX_NEWS_CACHE_KEY);
      if (cacheData != null) {
        Timer(Duration(seconds: 3), () {
          _controller.callRefresh();
        });
      }
    }
  }
複製程式碼

首頁骨架屏

pub.flutter-io.cn/packages/pk…

  • lib/pages/main/main.dart
  @override
  Widget build(BuildContext context) {
    return _newsPageList == null
        ? cardListSkeleton()
        : EasyRefresh(
            enableControlFinishRefresh: true,
            controller: _controller,
            ...
複製程式碼

相關文章