使用无头 Oracle Content Management 在 Flutter 中构建博客

简介

Flutter 是 Google 提供的开源框架,用于从单个代码库构建美丽、原生编译的多平台应用。Flutter 代码编译为 ARM 或 Intel 机器码以及 JavaScript,可在任何设备上实现快速性能。Flutter 可以是构建使用 Oracle Content Management 内容的应用程序的强大工具。借助合适的内容模型,您可以快速构建 Flutter UI 来组成典型博客。

在本教程中,我们将利用 Oracle Content Management 作为一个无头 CMS,在 Flutter 中构建一个简单的博客。GitHub 上提供了此 Flutter 示例。

本教程包含三个步骤:

  1. 准备 Oracle Content Management
  2. 在 Flutter 中构建博客
  3. 为部署准备应用

先决条件

继续本教程之前,我们建议您先阅读以下信息:

要遵循本教程,您需要:

我们正在建设

我们的博客将由三个屏幕组成,让访客可以探索组织为主题的博客文章。第一个屏幕,主屏幕,将包含一个博客主题列表。

要了解我们正在构建的内容,请查看教程的结束状态,这是一个使用 Oracle Content Management 内容的基本 Flutter 博客。

本教程末尾的主屏幕如下所示:

此图显示了 Cafe Supremo 演示站点的主屏幕,其中包含可用主题列表。

第二页(即主题屏幕)显示属于某个主题的每个博客文章的预览。下面是单个主题屏幕的外观:

此图像显示一个名为“如何制作咖啡”的主题屏幕,其中包含该主题的可用文章列表。

最后,文章屏幕呈现了最终的博客文章,包括有关博客作者的信息。以下是个人文章页面的外观:

此图像显示一个包含内容和作者参考的单独文章页面。

要继续,您需要对 Oracle Content Management 进行有效订阅,并使用内容管理员角色登录。

任务 1:准备 Oracle Content Management

如果您还没有 Oracle Content Management 实例,请参阅快速启动以了解如何注册 Oracle Cloud、预配 Oracle Content Management 实例以及将 Oracle Content Management 配置为无头 CMS。

对于本教程,您将需要通过两种方式之一创建内容模型。有一个可下载的资产包可用于为空存储库填写内容类型和关联内容,您也可以创建自己的内容模型和内容。

要准备 Oracle Content Management,请执行以下操作:

  1. 创建渠道和资产存储库
  2. 使用以下两种方法之一创建内容模型

创建渠道和资产存储库

首先需要在 Oracle Content Management 中创建渠道和资产存储库,以便发布内容。

要在 Oracle Content Management 中创建渠道和资产存储库,请执行以下操作:

  1. 以管理员身份登录 Oracle Content Management Web 界面。

  2. 在左侧导航菜单中选择内容,然后从页眉的选择列表中选择发布渠道

    此图像显示“内容”页面标题的下拉菜单中选择的“发布渠道”选项。

  3. 在右上角,单击创建以创建新通道。为本教程的目的命名渠道“OCEGettingStartedChannel”,并将访问权限保持为公开。单击保存以创建通道。

    此图像显示发布通道定义面板,通道名称字段中显示 "OCEGettingStartedChannel"。

  4. 在左侧导航菜单中选择内容,然后从页眉的选择列表中选择资料档案库

    此图像显示“内容”页标题的下拉菜单中选择的“资料档案库”选项。

  5. 在右上角,单击创建以创建新的资产存储库。为此教程的目的,将资产存储库命名为“OCEGettingStartedRepository”。

    此图像显示系统信息库定义面板,其中“存储库名称”字段中包含“OCEGettingStartedRepository”。

  6. 发布通道字段中,选择 OCEGettingStartedChannel 通道以向 Oracle Content Management 指明 OCEGettingStartedRepository 资料档案库中的内容可以发布到 OCEGettingStartedChannel 通道。完成后单击保存

    此图显示了系统信息库定义面板,其中“发布通道”字段中包含“OCEGettingStartedChannel”。

创建内容模型

下一步是创建内容模型。您可以使用以下两种方法之一:

导入 Oracle Content Management 示例资产包

您可以下载预配置的 Oracle Content Management 示例资产包,其中包含此教程所需的所有内容类型和资产。如果您愿意,还可以创建自己的内容模型,而不是下载示例资产包。

您可以从 Oracle Content Management 示例资产包上载本教程中使用的内容的副本。这样,您可以试验内容类型并修改内容。如果要导入 Oracle Content Management 示例资产包,可以下载资产包档案 OCESamplesAssetPack.zip,然后将其提取到您选择的目录中:

  1. 从 Oracle Content Management 下载页下载 Oracle Content Management 示例资产包 (OCESamplesAssetPack.zip)。将下载的 zip 文件解压缩到计算机上的某个位置。提取后,此位置将包括一个名为 OCEGettingStarted_data.zip 的文件。

  2. 以管理员身份登录 Oracle Content Management Web 界面。

  3. 在左侧导航菜单中选择内容,然后从页眉的选择列表中选择资料档案库。现在选择 OCEGettingStartedRepository,然后单击顶部操作栏中的导入内容按钮。

    此图像显示“资料档案库”页,其中选择了 OCEGettingStartedRepository 项。

  4. 将 OCEGettingStarted_data.zip 从本地计算机上载到文档文件夹。

    此图显示了 OCEGettingStarted_data.zip 文件的上载确认屏幕。

  5. 上载后,选择 OCEGettingStarted_data.zip 并单击确定将内容导入到资产存储库中。

    此图像显示已启用“确定”按钮的选定 OCEGettingStarted_data.zip 文件。

  6. 成功导入内容后,导航到资产页并打开 OCEGettingStartedRepository 系统信息库。您将看到所有相关图像和内容项现在已添加到资产存储库中。

    此图像显示 OCEGettingStartedRepository 存储库,其中包含刚刚导入的所有资产。

  7. 单击左上方的全选,然后单击发布将所有导入的资产添加到您之前创建的发布渠道 OCEGettingStartedChannel。

    此图像显示 OCEGettingStartedRepository 存储库,其中选择所有资产,操作栏中的“发布”选项可见。

  8. 在发布之前,您需要验证所有资产。首先将 OCEGettingStartedChannel 添加为所选通道,然后单击验证按钮。

    此图像显示“验证结果”页,其中添加了 OCEGettingStartedChannel 渠道、要验证的所有资产以及“验证”按钮。

  9. 验证资产后,可以通过单击右上角的发布按钮将所有资产发布到选定渠道。

    此图像显示“验证结果”页,其中添加了 OCEGettingStartedChannel 渠道、验证的所有资产以及“发布”按钮。

完成此操作后,您可以在资产页上看到所有资产都已发布。(您可以通过资产名称上方的图标来判断。)

此图像显示“资产”页,其中所有资产都已发布。

导入 Oracle Content Management 示例资产包后,您可以开始在 Flutter 中构建博客

创建您自己的内容模型

您还可以创建自己的内容模型,而不是导入 Oracle Content Management 示例资产包

对于本教程,我们使用一种名为“OCEGettingStartedHomePage”的内容类型来构建博客的主屏幕。此主页包含应包括在屏幕上的博客主题的列表。

此图显示了 Cafe Supremo 演示站点的主屏幕。

要为内容模型创建内容类型,请执行以下操作:

  1. 以管理员身份登录 Oracle Content Management Web 界面。
  2. 在左侧导航菜单中选择内容,然后从页面标题的选择列表中选择资产类型
  3. 单击右上角的创建
  4. 选择创建内容类型(不是数字资产类型)。对所有必需的内容类型重复此操作。

此图显示了 Oracle Content Management Web 界面中的“创建资产类型”对话框。

我们将创建四个内容类型,每个类型都有自己的字段集:

第一个内容类型 OCEGettingStartedHomePage 应该具有以下字段:

显示名称 字段类型 必需 计算机名
公司名称 单值文本字段 X company_name
公司标识 单值文本字段 X company_logo
主题 多值参考字段 X 主题
联系 URL 单值文本字段 X contact_url
关于 URL 单值文本字段 X about_url

这是 OCEGettingStartedHomePage 内容类型定义应如下所示:

此图像显示内容类型 'OCEGettingStartedHomePage' 的定义。它包括以下数据字段:“公司名称”、“公司标识”、“主题”、“联系人 URL”和“关于 URL”。

第二种内容类型 OCEGettingStartedTopic 应具有以下字段:

显示名称 字段类型 必需 计算机名
缩略图 单值图像字段 X 缩略图

这是 OCEGettingStartedTopic 内容类型应如下所示:

此图像显示内容类型 'OCEGettingStartedTopic' 的定义。它包括以下数据字段:缩略图。

第三个内容类型 OCEGettingStartedAuthor 应该具有以下字段:

显示名称 字段类型 必需 计算机名
头像 单值图像字段 X 头像

这是 OCEGettingStartedAuthor 内容类型应如下所示:

此图像显示内容类型 'OCEGettingStartedAuthor' 的定义。它包括以下数据字段:头像。

第四个和最后一个内容类型 OCEGettingStartedArticle 应该具有以下字段:

显示名称 字段类型 必需 计算机名
发布日期 单值日期字段 X published_name
作者 单值引用字段 X 作者
图像 单值图像字段 X 图像
图像标题 单值文本字段 X image_caption
文章内容 单值大文本字段 X article_content
主题 单值引用字段 X 主题

这是 OCEGettingStartedArticle 内容类型应如下所示:

此图像显示内容类型 'OCEGettingStartedArticlePage' 的定义。它包括以下数据字段:“发布日期”、“作者”、“图像”、“图像标题”、“文章内容”和“主题”。

创建内容类型后,可以将这些内容类型添加到之前创建的存储库 OCEGettingStartedRepository:

  1. 以管理员身份登录 Oracle Content Management Web 界面。
  2. 导航到 OCEGettingStartedRepository
  3. 编辑系统信息库,然后在资产类型下指定所有四个新创建的内容类型。单击保存按钮以保存更改。

此图像显示 Oracle Content Management 中的“编辑资料档案库”页,其中包含与 OCEGettingStartedRepository 资料档案库关联的四个新创建的内容类型。

将内容类型添加到资料档案库后,可以在资产页上打开 OCEGettingStartedRepository 资料档案库,并开始为所有内容类型创建内容项。

此图像显示 Oracle Content Management Web 界面中“资产”页上的内容项,左侧包含用于集合、渠道、语言、类型、内容项选择和状态的选项。

任务 2:在 Flutter 中构建博客

要在 Flutter 应用程序中使用 Oracle Content Management 内容,可以使用 Flutter 博客示例,该示例可作为 GitHub 上的开源存储库使用。

注意:请记住,使用 Flutter 示例是可选的,我们将在本教程中使用它来帮助您快速入门。您还可以构建自己的 Flutter 应用程序。

在 Flutter 中构建博客包括以下步骤:

  1. 安装 Flutter
  2. 克隆样例系统信息库并安装相关项
  3. 配置 Flutter 应用程序
  4. 使用 Oracle Content Management REST API 提取内容

安装 Flutter

要开始使用 Flutter,您需要做的第一件事是设置您的环境。您可以在Flutter 网站找到设置它的说明。使用与操作系统相对应的选项。下面是安装步骤的摘要:

  1. 获取 Flutter SDK。
  2. 更新路径环境变量。
  3. 运行 flutter doctor
  4. 按照Flutter 网站中列出的步骤安装您选择的编辑器。
  5. 为 Android Studio、IntelliJ、VS 代码或 Emacs 添加编辑器插件。说明指定如何在编辑器中安装 Flutter 和 Dart 插件。
  6. 修改 android.properties 文件中的值以引用相应的位置, sdk 和 kotlin 版本。您可能需要修改相应版本的 android.gradle 文件中的值。

克隆样例资料档案库并安装相关性

Flutter 博客样例在 GitHub 上作为开源系统信息库提供。

首先需要将示例从 GitHub 克隆到本地计算机,并将目录更改为系统信息库根:

git clone https://github.com/oracle-samples/oce-flutter-blog-sample.git
cd oce-flutter-blog-sample

从 Android Studio 或您选择的任何其他编辑器打开项目。要获取项目中使用的所有软件包,请单击获取软件包按钮。

配置 Flutter 应用程序

在此 Flutter 博客示例中,您需要配置一些信息,以便 REST API 请求可以使用正确的渠道令牌来定位正确的实例 URL 和 API 版本。lib/config/oce.dart 中定义的这些值由 lib/networking/content.dart 中定义的函数使用,用于为 REST API 建立端点。

您可以在 lib/config/oce.dart 中看到以下信息:

const Map<String, String> config = <String, String>{
  'serverUrl': 'https://samples.mycontentdemo.com',
  'apiVersion': 'v1.1',
  'channelToken': '47c9fb78774d4485bc7090bf7b955632'
};

更改每个键 - 值对以反映您的实例 URL、要定位的 API 版本以及与发布渠道关联的渠道标记。本教程的渠道为 OCEGettingStartedChannel。

使用 Oracle Content Management REST API 提取内容

通过 REST API for Content Delivery,可访问 Oracle Content Management 中发布的资产。已发布的资产包括内容项和数字资产,以及其重现。我们现在可以利用 Oracle Content Management REST API 提取内容,以便将其显示在博客 Flutter 应用程序中。

lib/networking/content.dart 文件使用 REST API 连接到 oce.dart 文件中指定的服务器并返回其响应的方法。

  //Utility method to build up the URL for published content.
  String _getPublishedContentServerURL() {
    final String? serverUrl = data['serverUrl'] as String?;
    final String? apiVersion = data['apiVersion'] as String?;
    return '$serverUrl/content/published/api/$apiVersion/';
  }

  // Adds the channel token to the URL
  String _addChannelToURL(String currUrl) {
    final String? channelToken = data['channelToken'] as String?;
    return '$currUrl?channelToken=$channelToken';
  }

  //Make an http get call and return the response if successful
  Future<dynamic> _get(String url) async {
    dynamic responseJson;
    try {
      final Response response = await get(Uri.parse(url));
      responseJson = _returnResponse(response);
    } on SocketException {
      throw FetchDataException(kConnectionError);
    }
    return responseJson;
  }

  //Return the json decoded response body if response status is successful
  dynamic _returnResponse(Response response) {
    switch (response.statusCode) {
      case 200:
        final Map<String, dynamic>? responseJson =
            json.decode(response.body.toString()) as Map<String, dynamic>?;
        return responseJson;
      case 400:
        throw BadRequestException(response.body.toString());
      case 401:
      case 403:
        throw UnauthorizedException(response.body.toString());
      case 500:
      default:
        throw FetchDataException('StatusCode : ${response.statusCode}');
    }
  }

为了呈现图像,content.dart 还提供了帮助程序方法来检索资产的各种重现。

  String getMediumRenditionUrl(Map<String, String> args) {
    final String itemId = args['id'];
    if (itemId == null) return null;
    String url = _getPublishedContentServerURL();
    url = '${url}assets/$itemId/Medium';
    // add the channel token to the URL
    url = _addChannelToURL(url);
    url = '$url&format=jpg&&type=responsiveimage';
    return url;
  }

  String getRenditionURL(Map<String, String> args) {
    final String itemId = args['id'];
    if (itemId == null) return null;
    String url = _getPublishedContentServerURL();
    url = '${url}assets/$itemId/native';
    // add the channel token to the URL
    url = _addChannelToURL(url);
    return url;
  }

lib/networking/services.dart 文件包含用于获取应用程序数据的所有代码。

主页数据

主页需要多个数据调用才能获取其所有数据:

  1. 首先,我们在 oce.dart 中指定的通道中查询项目。
  2. 获取每个主题项的详细信息。

打开 lib/networking/services.dart 并找到下面的函数,这有助于获取主页的所有数据。

  // Fetch the top level values to be displayed on the home page.
  Future<TopicListModel> fetchHomePage() async {
    final Content content = Content();
    try {
      final dynamic topicListData = await content.queryItems(<String, String>{
        'q': '(type eq "OCEGettingStartedHomePage" AND name eq "HomePage")',
        'fields': 'all',
      });
      return TopicListModel.fromJson(topicListData);
    } catch (exception) {
      rethrow;
    }
  }

  // Fetch details about the specific topic
  Future<TopicListItemModel> fetchTopic(topicId) async {
    final Content content = Content();
    try {
      final dynamic data = await content.getItem(<String, String?>{
        'id': topicId,
        'fields': 'all',
        'expand': 'all',
      });
      TopicListItemModel topicListItemModel = TopicListItemModel.fromJson(data);
      topicListItemModel.thumbnailUrl = getMediumRenditionUrl(topicListItemModel.thumbnailId);
      return topicListItemModel;
    } catch (exception) {
      rethrow;
    }
  }

主题页

主题页接收主题 ID,需要多次数据调用才能获取其所有数据:

  1. 获取指定主题的所有文章。
  2. 获取每篇文章的重现网址。

打开 lib/networking/services.dart 并找到下面的函数,该函数由 article_list.dart 中的 fetchData 函数使用。

  // Get all the articles for the specified topic.
  //
  // @param {String} topicId - the id of the topic
  // @returns ArticleListModel which contains the list of articles for the topic
  Future<ArticleListModel>fetchArticles(topicId) async{
    final Content content = Content();
    try {
      final dynamic data = await content.queryItems(<String, String>{
        'q': '(type eq "OCEGettingStartedArticle" AND fields.topic eq "$topicId")',
        'fields': 'all',
        'orderBy': 'fields.published_date:desc',
      });
      ArticleListModel articleListModel = ArticleListModel.fromJson(data);
      for (ArticleListItemModel articleListItemModel in articleListModel.articlesList) {
        articleListItemModel.thumbnailUrl = getMediumRenditionUrl(articleListItemModel.thumbnailId);
      }
      return articleListModel;
    } catch (exception) {
      rethrow;
    }
  }

文章页面

文章页面收到一个文章 ID,需要多次数据调用才能获取其所有数据:

  1. 获取指定文章的文章详细信息。
  2. 获取文章作者的头像重现 URL。

打开 lib/networking/services.dart 并查找下面的函数,该函数由 article.dart 中的 fetchData 函数使用。

  // Get details of the specified article.
  //
  // @param {String} articleId - The id of the article
  // @returns ArticleModel - the article
  Future<ArticleModel>fetchArticle(articleId) async{
    final Content content = Content();
    try {
      final dynamic data = await content.getItem(<String, String?>{
        'id': articleId,
        'expand': 'all',
      });
      ArticleModel articleModel =  ArticleModel.fromJson(data);
      articleModel.authorImageUrl = getMediumRenditionUrl(articleModel.authorImageId);
      articleModel.imageUrl = getRenditionUrl(articleModel.imageId);
      return articleModel;
    } catch (exception) {
      rethrow;
    }
  }

现在我们有数据查询,我们可以在Flutter 组件中呈现响应。

Flutter 组件

博客应用程序将每个屏幕分解为若干较小的组件。

接下来的几节概述了 Flutter 如何在我们的每个屏幕中呈现我们的应用程序:

TopicsList 屏幕

主页由渠道中的主题列表组成。它由位于 lib/screens/topic_list.dart 的 TopicsList 组件呈现。

lib/screens/topic_list.dart 文件包含用于获取屏幕数据的所有代码,并调用 services.dart 中定义的函数。

  Future<void> fetchData() async {
    final Services services = Services();
    try {
      topicListModel = await services.fetchHomePage();
      setState(() {
        topicListModel = topicListModel;
      });
      //for each topicid , fetch the topic
      for (var topicId in topicListModel!.topicIdList) {
        TopicListItemModel topic = await services.fetchTopic(topicId);
        setState(() {
          topics.add(topic);
          if (topics.length == topicListModel!.topicIdList.length) {
            dataFetched = true;
          }
        });
      }
    } catch (e) {
      setState(() {
        exception = e.toString();
      });
      print(exception.toString());
    }
  }

组件使用 fetchData 方法通过在 services.dart 文件中定义的方法获取数据。此文件还使用位于 lib/components/topic_list_item.dart 的组件呈现主题屏幕中的每个主题项。

ArticlesList 屏幕

“主题”页显示主题中的文章。它由位于 lib/screens/articles_list.dart 的 ArticlesList 组件呈现。

打开 lib/screens/articles_list.dart 并找到 fetchData 函数,该函数用于获取此屏幕的所有数据。

  Future<void> fetchData() async {
    final Services services = Services();
    try {
      ArticleListModel articleListModel =
          await services.fetchArticles(widget.topicModel.id);
      setState(() {
        articles = articleListModel.articlesList;
      });
    } catch (exception) {
      setState(() {
        this.exception = exception.toString();
      });
      print(exception.toString());
    }
  }

组件使用 fetchData 方法从 services.dart 文件获取数据。此文件还使用位于 lib/components/article_list_item 的组件呈现主题屏幕中的每个主题项。

文章屏幕

“文章”页显示文章的详细信息。它由位于 lib/screens/article.dart 的文章组件提供。

打开 lib/screens/article.dart 并找到 fetchData 函数,该函数用于获取文章屏幕的所有数据。

  Future<void> fetchData() async {
    final Services services = Services();
    try {
      ArticleModel articleModel =
          await services.fetchArticle(widget.articleListItemModel.id);
      setState(() {
        this.articleModel = articleModel;
      });
    } catch (exception) {
      setState(() {
        this.exception = exception.toString();
      });
      print(exception.toString());
    }
  }

文章组件使用前面所述的 fetchData 方法从 services.dart 文件获取数据。此文件还使用位于 lib/components/item_image 的 ItemImage 组件呈现文章图像。

任务 3:准备应用程序以进行部署

现在我们已经构建了我们的 Flutter 博客网站,我们需要将其部署在仿真器或设备上,以便我们可以在应用程序投入使用之前调试任何问题并预览应用程序。

按照Flutter 网站上的说明,让您的编辑器运行应用程序。

  1. 如果您使用 Android Studio 作为编辑器,请找到 Android Studio 主工具栏。
  2. 在目标选择器中,选择用于运行应用程序的 Android 设备。如果未列出任何可用项,请选择“Tools(工具)”>“Android”>“AVD Manager(AVD 管理器)”,并在其中创建一个。有关详细信息,请参阅管理 AVD
  3. 单击工具栏中的“运行”图标,或者调用菜单项“运行”>“运行”。

结论

在本教程中,我们在 Flutter 中创建了一个博客网站,该网站位于 GitHub 上。此站点使用 Oracle Content Management 作为无头 CMS。在使用博客网站教程的已发布内容渠道设置和配置 Oracle Content Management 之后,我们安装并运行 Flutter 站点以提取所需内容并运行应用程序。

有关 Flutter 的更多信息,请访问 Flutter 网站

文档中了解重要的 Oracle Content Management 概念。

您可以在 Oracle 帮助中心Oracle Content Management 抽样页上找到更多类似示例。