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…git

本節目標

  • 第一次登陸顯示歡迎界面
  • 離線登陸
  • Provider 響應數據管理
  • 實現 APP 色彩灰度處理
  • 註銷登陸
  • Http Status 401 認證受權
  • 首頁磁盤緩存
  • 首頁緩存策略,延遲 1~3 秒
  • 首頁骨架屏

視頻

資源

第一次顯示歡迎界面、離線登陸

  • 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}'),
        ),
      ],
    );
複製代碼

多個響應數據處理

  • 掛載用 MultiProvidermarkdown

  • 接收用 Consumer2 ~ Consumer6app

註銷登陸

  • 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…async

  • lib/pages/main/main.dart
@override
  Widget build(BuildContext context) {
    return _newsPageList == null
        ? cardListSkeleton()
        : EasyRefresh(
            enableControlFinishRefresh: true,
            controller: _controller,
            ...
複製代碼
相關文章
相關標籤/搜索