In this section, the target

  • Strapi + GraphQL plugin + Docker installation
  • Strapi manages data structures, content
  • The plugin for Flutter + GraphQL implements the query

video

www.bilibili.com/video/BV1Zz…

code

Github.com/ducafecat/f…

The body of the

Background development steps

Strapi + NodeJS + gateway scheme is adopted

1. Strapi installation

1.1 Docker-compose installation mode

  • .env
PASSWORD=123456
Copy the code
  • docker-compose.yml
version: "3"
services:
  mongo:
    image: mongo
    container_name: mongo
    restart: always
    ports:
      - 27017: 27017
    environment:
      - TZ=Asia/Shanghai
      - MONGO_INITDB_ROOT_USERNAME=root
      - MONGO_INITDB_ROOT_PASSWORD=${PASSWORD}
    volumes:
      - ./docker-data/mongo:/data/db
    networks:
      docker_net:
        ipv4_address: 172.22. 011.

  # starpi
  # admin / 123456 / [email protected]
  strapi-app:
    image: strapi/strapi
    container_name: strapi-app
    restart: always
    ports:
      - 1337: 1337
    # command: strapi build
    # command: strapi start
    environment:
      - TZ=Asia/Shanghai
      - DATABASE_CLIENT=mongo
      - DATABASE_HOST=mongo
      - DATABASE_PORT=27017
      - DATABASE_NAME=strapi
      - DATABASE_USERNAME=root
      - DATABASE_PASSWORD=${PASSWORD}
      - DATABASE_AUTHENTICATION_DATABASE=strapi
      # - NODE_ENV=production
    depends_on:
      - mongo
    volumes:
      - ./docker-data/strapi-app:/srv/app
    networks:
      docker_net:
        ipv4_address: 172.22. 012.

networks:
  docker_net:
    driver: bridge
    ipam:
      config:
        - subnet: 172.22. 0. 0/ 16
Copy the code

http://localhost:1337/admin

1.2 Installing the GraphQL plug-in

2. Build news data structure

2.1 Creating data Types

  • Add a type

  • Add fields

  • Field list

2.2 Adjusting the data editing interface

2.3 Adjusting the Data List interface

2.4 Maintenance Data

  • The list of

  • add

3. Debug graphQL requests

3.3 graphql grammar

  • type
    • Query query
    • Mutate operation

3.4 Debugging the news list

http://localhost:1337/graphql

4. Write flutter code

4.1 Add graphQL plug-in

Pub. Flutter – IO. Cn/packages/gr…

  • pubspec.yaml
dependencies:
  # graphql
  graphql: ^ 3.0.2
Copy the code

4.2 Encapsulating the GraphQL Client utility class

  • lib/common/utils/graphql_client.dart
import 'package:flutter/material.dart';
import 'package:flutter_ducafecat_news/common/values/values.dart';
import 'package:flutter_ducafecat_news/common/widgets/widgets.dart';
import 'package:graphql/client.dart';

class GraphqlClientUtil {
  static OptimisticCache cache = OptimisticCache(
    dataIdFromObject: typenameDataIdFromObject,
  );

  static client() {
    HttpLink _httpLink = HttpLink(
      uri: '$SERVER_STRAPI_GRAPHQL_URL/graphql',);// final AuthLink _authLink = AuthLink(
    // getToken: () =>
    // 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlZmMzNDdhYzgzOTVjMDAwY2ViYzE5NyIsImlhdCI6MTU5MzY1NDcwNiwiZXhwIjoxNTk2MjQ 2NzA2fQ.RYDmNSDJxcZLLPHAf4u59IER7Bs5VoWfBo1_t-TR5yY',
    // );

    // final Link _link = _authLink.concat(_httpLink);

    return GraphQLClient(
      cache: cache,
      link: _httpLink,
    );
  }

  / / query
  static Future query({
    @required BuildContext context,
    @required String schema,
    Map<String.dynamic> variables,
  }) async {
    QueryOptions options = QueryOptions(
      documentNode: gql(schema),
      variables: variables,
    );

    QueryResult result = await client().query(options);

    if (result.hasException) {
      toastInfo(msg: result.exception.toString());
      throw result.exception;
    }

    return result;
  }

  / / operation
  static Future mutate({
    @required BuildContext context,
    @required String schema,
    Map<String.dynamic> variables,
  }) async {
    QueryOptions options = QueryOptions(
      documentNode: gql(schema),
      variables: variables,
    );

    QueryResult result = await client().mutate(options);

    if (result.hasException) {
      toastInfo(msg: result.exception.toString());
      throw result.exception;
    }

    returnresult; }}Copy the code

4.3 Write graphQL query request

  • lib/common/graphql/news_content.dart
const String GQL_NEWS_LIST = r''' query News { newsContents { title category author url addtime thumbnail { url } } } ''';
Copy the code

4.4 Writing data Entities

lib/common/entitys/gql_news.dart

class GqlNewsResponseEntity {
  GqlNewsResponseEntity({
    this.id,
    this.title,
    this.category,
    this.author,
    this.url,
    this.addtime,
    this.thumbnail,
  });

  String id;
  String title;
  String category;
  String author;
  String url;
  DateTime addtime;
  Thumbnail thumbnail;

  factory GqlNewsResponseEntity.fromJson(Map<String.dynamic> json) =>
      GqlNewsResponseEntity(
        id: json["id"],
        title: json["title"],
        category: json["category"],
        author: json["author"],
        url: json["url"],
        addtime: DateTime.parse(json["addtime"]),
        thumbnail: Thumbnail.fromJson(json["thumbnail"]));Map<String.dynamic> toJson() => {
        "id": id,
        "title": title,
        "category": category,
        "author": author,
        "url": url,
        "addtime":
            "${addtime.year.toString().padLeft(4.'0')}-${addtime.month.toString().padLeft(2.'0')}-${addtime.day.toString().padLeft(2.'0')}"."thumbnail": thumbnail.toJson(),
      };
}

class Thumbnail {
  Thumbnail({
    this.url,
  });

  String url;

  factory Thumbnail.fromJson(Map<String.dynamic> json) => Thumbnail(
        url: json["url"]);Map<String.dynamic> toJson() => {
        "url": url,
      };
}

Copy the code

4.5 Write API access

  • lib/common/apis/gql_news.dart
import 'package:flutter/material.dart';
import 'package:flutter_ducafecat_news/common/entitys/entitys.dart';
import 'package:flutter_ducafecat_news/common/graphql/graphql.dart';
import 'package:flutter_ducafecat_news/common/utils/utils.dart';
import 'package:graphql/client.dart';

/ / / news
class GqlNewsAPI {
  / / / page
  static Future<List<GqlNewsResponseEntity>> newsPageList({
    @required BuildContext context,
    Map<String.dynamic> params,
  }) async {
    QueryResult response =
        await GraphqlClientUtil.query(context: context, schema: GQL_NEWS_LIST);

    return response.data['newsContents'] .map<GqlNewsResponseEntity>( (item) => GqlNewsResponseEntity.fromJson(item)) .toList(); }}Copy the code

4.6 Modifying the News List page

  • lib/pages/main/main.dart
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_ducafecat_news/common/apis/apis.dart';
import 'package:flutter_ducafecat_news/common/entitys/entitys.dart';
import 'package:flutter_ducafecat_news/common/utils/utils.dart';
import 'package:flutter_ducafecat_news/common/values/values.dart';
import 'package:flutter_ducafecat_news/common/widgets/widgets.dart';
import 'package:flutter_ducafecat_news/pages/main/ad_widget.dart';
import 'package:flutter_ducafecat_news/pages/main/categories_widget.dart';
import 'package:flutter_ducafecat_news/pages/main/channels_widget.dart';
import 'package:flutter_ducafecat_news/pages/main/news_item_widget.dart';
import 'package:flutter_ducafecat_news/pages/main/newsletter_widget.dart';
import 'package:flutter_ducafecat_news/pages/main/recommend_widget.dart';
import 'package:flutter_easyrefresh/easy_refresh.dart';

class MainPage extends StatefulWidget {
  MainPage({Key key}) : super(key: key);

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

class _MainPageState extends State<MainPage> {
  EasyRefreshController _controller; // EasyRefresh controller

  // NewsPageListResponseEntity _newsPageList; // Turn the news page
  List<GqlNewsResponseEntity> _newsPageList; // Turn the news page

  NewsItem _newsRecommend; // News recommendation
  List<CategoryResponseEntity> _categories; / / classification
  List<ChannelResponseEntity> _channels; / / channel

  String _selCategoryCode; // The selected category Code

  @override
  void initState() {
    super.initState();
    _controller = EasyRefreshController();
    _loadAllData();
    _loadLatestWithDiskCache();
  }

  // If there is disk cache, delay 3 seconds to pull update file
  _loadLatestWithDiskCache() {
    if (CACHE_ENABLE == true) {
      var cacheData = StorageUtil().getJSON(STORAGE_INDEX_NEWS_CACHE_KEY);
      if(cacheData ! =null) {
        Timer(Duration(seconds: 3), () { _controller.callRefresh(); }); }}}// Read all data
  _loadAllData() async {
    _categories = await NewsAPI.categories(
      context: context,
      cacheDisk: true,); _channels =await NewsAPI.channels(
      context: context,
      cacheDisk: true,);// _newsRecommend = await NewsAPI.newsRecommend(
    // context: context,
    // cacheDisk: true,
    // );

    // _newsPageList = await NewsAPI.newsPageList(
    // context: context,
    // cacheDisk: true,
    // );
    _newsPageList = await GqlNewsAPI.newsPageList(
      context: context,
    );

    _selCategoryCode = _categories.first.code;

    if(mounted) { setState(() {}); }}// pull recommendations, news
  _loadNewsData(
    categoryCode, {
    bool refresh = false,})async {
    _selCategoryCode = categoryCode;
    _newsRecommend = await NewsAPI.newsRecommend(
      context: context,
      params: NewsRecommendRequestEntity(categoryCode: categoryCode),
      refresh: refresh,
      cacheDisk: true,);// _newsPageList = await NewsAPI.newsPageList(
    // context: context,
    // params: NewsPageListRequestEntity(categoryCode: categoryCode),
    // refresh: refresh,
    // cacheDisk: true,
    // );
    _newsPageList = await GqlNewsAPI.newsPageList(
      context: context,
    );

    if(mounted) { setState(() {}); }}// Categorize the menu
  Widget _buildCategories() {
    return _categories == null? Container() : newsCategoriesWidget( categories: _categories, selCategoryCode: _selCategoryCode, onTap: (CategoryResponseEntity item) { _loadNewsData(item.code); }); }// Recommended reading
  Widget _buildRecommend() {
    return _newsRecommend == null // The data is not in place, you can use skeleton diagram to show
        ? Container()
        : recommendWidget(_newsRecommend);
  }

  / / channel
  Widget _buildChannels() {
    return _channels == null
        ? Container()
        : newsChannelsWidget(
            channels: _channels,
            onTap: (ChannelResponseEntity item) {},
          );
  }

  // News list
  Widget _buildNewsList() {
    return _newsPageList == null
        ? Container(
            height: duSetHeight(161 * 5 + 100.0),
          )
        : Column(
            children: _newsPageList.map((item) {
              / / news
              List<Widget> widgets = <Widget>[
                newsItem(item),
                Divider(height: 1)];// Display ads every 5
              int index = _newsPageList.indexOf(item);
              if (((index + 1) % 5) = =0) {
                widgets.addAll(<Widget>[
                  adWidget(),
                  Divider(height: 1),]); }/ / return
              return Column(
                children: widgets,
              );
            }).toList(),
          );
  }

  // AD banners
  // Subscribe by email
  Widget _buildEmailSubscribe() {
    return newsletterWidget();
  }

  @override
  Widget build(BuildContext context) {
    return _newsPageList == null
        ? cardListSkeleton()
        : EasyRefresh(
            enableControlFinishRefresh: true,
            controller: _controller,
            header: ClassicalHeader(),
            onRefresh: () async {
              await _loadNewsData(
                _selCategoryCode,
                refresh: true,); _controller.finishRefresh(); }, child: SingleChildScrollView( child: Column( children: <Widget>[ _buildCategories(), Divider(height:1),
                  _buildRecommend(),
                  Divider(height: 1),
                  _buildChannels(),
                  Divider(height: 1),
                  _buildNewsList(),
                  Divider(height: 1), _buildEmailSubscribe(), ], ), ), ); }}Copy the code

4.7 Modifying the News Details page

  • lib/pages/main/news_item_widget.dart
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_ducafecat_news/common/entitys/entitys.dart';
import 'package:flutter_ducafecat_news/common/utils/utils.dart';
import 'package:flutter_ducafecat_news/common/values/values.dart';
import 'package:flutter_ducafecat_news/common/widgets/widgets.dart';
import 'package:flutter_ducafecat_news/common/router/router.gr.dart';

/// Item of the news line
Widget newsItem(GqlNewsResponseEntity item) {
  return Container(
    height: duSetHeight(161),
    padding: EdgeInsets.all(duSetWidth(20)),
    child: Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        / / figure
        InkWell(
          onTap: () {
            ExtendedNavigator.rootNavigator.pushNamed(
              Routes.detailsPageRoute,
              arguments: DetailsPageArguments(item: item),
            );
          },
          child: imageCached(
            '$SERVER_STRAPI_GRAPHQL_URL${item.thumbnail.url}',
            width: duSetWidth(121),
            height: duSetWidth(121),),),/ / on the right side
        SizedBox(
          width: duSetWidth(194),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              / / the author
              Container(
                margin: EdgeInsets.all(0),
                child: Text(
                  item.author,
                  style: TextStyle(
                    fontFamily: 'Avenir',
                    fontWeight: FontWeight.normal,
                    color: AppColors.thirdElementText,
                    fontSize: duSetFontSize(14),
                    height: 1,),),),/ / title
              InkWell(
                onTap: () {
                  ExtendedNavigator.rootNavigator.pushNamed(
                    Routes.detailsPageRoute,
                    arguments: DetailsPageArguments(item: item),
                  );
                },
                child: Container(
                  margin: EdgeInsets.only(top: duSetHeight(10)),
                  child: Text(
                    item.title,
                    style: TextStyle(
                      fontFamily: 'Montserrat',
                      fontWeight: FontWeight.w500,
                      color: AppColors.primaryText,
                      fontSize: duSetFontSize(16),
                      height: 1,
                    ),
                    overflow: TextOverflow.clip,
                    maxLines: 3,),),),// Spacer
              Spacer(),
              // One row, three columns
              Container(
                child: Row(
                  crossAxisAlignment: CrossAxisAlignment.center,
                  children: <Widget>[
                    / / classification
                    ConstrainedBox(
                      constraints: BoxConstraints(
                        maxWidth: duSetWidth(60),
                      ),
                      child: Text(
                        item.category,
                        style: TextStyle(
                          fontFamily: 'Avenir',
                          fontWeight: FontWeight.normal,
                          color: AppColors.secondaryElementText,
                          fontSize: duSetFontSize(14),
                          height: 1,
                        ),
                        overflow: TextOverflow.clip,
                        maxLines: 1,),),// Add time
                    Container(
                      width: duSetWidth(15),
                    ),
                    ConstrainedBox(
                      constraints: BoxConstraints(
                        maxWidth: duSetWidth(100),
                      ),
                      child: Text(
                        ',${duTimeLineFormat(item.addtime)}',
                        style: TextStyle(
                          fontFamily: 'Avenir',
                          fontWeight: FontWeight.normal,
                          color: AppColors.thirdElementText,
                          fontSize: duSetFontSize(14),
                          height: 1,
                        ),
                        overflow: TextOverflow.clip,
                        maxLines: 1,),),/ / more
                    Spacer(),
                    InkWell(
                      child: Icon(
                        Icons.more_horiz,
                        color: AppColors.primaryText,
                        size: 24,
                      ),
                      onTap: () {},
                    ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ],
    ),
  );
}

Copy the code

resources

Design draft Blue Lake preview

lanhuapp.com/url/lYuz1 Password: gSKl

Blue Lake now charges, so please upload xD design draft by yourself to check the mark. Commercial design draft file is not easy to share directly, you can add wechat to contact Ducafecat

reference

  • Pub. Flutter – IO. Cn/packages/gr…
  • Strapi. IO/documentati…

The elder brother of the © cat

ducafecat.tech