Магазины электронной коммерции — это интернет-магазины, которые помогают вести торговлю без участия третьего лица. Этот учебник научит вас, как создать наш магазин электронной коммерции на бэкенде Medusa.
Flutter — это кроссплатформенный набор инструментов пользовательского интерфейса, который разработан для повторного использования кода в операционных системах, таких как iOS и Android, а также позволяет приложениям напрямую взаимодействовать с базовыми сервисами платформы.
Medusa — это open source headless commence, позволяющий пользователям создавать масштабируемые и уникальные магазины электронной коммерции и беспрепятственно настраивать продукты. Она помогает разработчикам быстро и эффективно создавать, управлять и настраивать API.
Medusa обладает множеством возможностей, среди которых простая в использовании панель администратора, бесплатное использование, множество плагинов для различных операций и большая поддержка сообщества.
В этом руководстве будет показано, как взаимодействовать с бэкенд-сервисами Medusa из приложения Flutter при создании магазина электронной коммерции. Medusa будет заниматься созданием наших продуктов. В конце этого урока вы узнаете, как использовать Medusa для работы с внутренними сервисами.
Ниже приведена ссылка на исходный код полного приложения Flutter
https://github.com/iamVictorSam/my-store.git
- Предварительные условия
- Настройте рабочее пространство для Medusa
- Установите Medusa CLI
- Создать сервер Medusa
- Запуск сервера Medusa
- Установка Medusa Admin
- Запуск панели администратора
- Создание нашего пользовательского продукта
- Настройка рабочего пространства для приложения Flutter
- Создайте приложение Flutter
- Установка пакетов
- Структура папок
- Создание всплывающего экрана
- Экран всплеска
- Добавление файла логотипа
- Настройка модели
- Создание нашей модели
- Выполнение запроса API из sever
- Настройка урла базы
- Получение продуктов из API
- Управление состоянием с помощью Getx
- Построение главного экрана
- Создание компонента карточки товара
- Создание компонента AllProducts
- Создание компонентов для дома
- Построение экрана подробностей
- Создание цены продукта
- Создание компонента Size
- Построение экрана подробной информации о продукте
- Что дальше
- Исходный код
- Заключение
Предварительные условия
Чтобы следовать этому руководству, на вашей машине должны быть установлены эти двоичные файлы:
- Node и NPM: Node.js — это внутренняя среда выполнения JavaScript с открытым исходным кодом, которая запускается вне веб-браузера с помощью движка V8 и выполняет код JavaScript. Node можно загрузить с помощью страницы загрузки Node.js.
- Flutter SDK: Это инструмент командной строки для создания и запуска проектов Flutter Flutter. Посетите сайт https://flutter.dev/docs/get-started/install, чтобы установить Flutter SDK. В этом учебнике будет использоваться последняя версия Flutter 3.0.
- Git: Это инструмент DevOps, используемый для контроля версий и управления кодом. Посетите сайт https://git-scm.com/download, чтобы загрузить git на ваше устройство.
Настройте рабочее пространство для Medusa
Установите Medusa CLI
Прежде чем запускать сервер Medusa, сначала установите Medusa cli. Это даст доступ к серверу Medusa. Выполните приведенную ниже команду в терминале.
npm install -g @medusajs/medusa-cli
Создать сервер Medusa
После этого выполните приведенную ниже команду.
medusa new flutter-store --seed
Приведенная выше команда создает проект сервера, а также устанавливает необходимые зависимости. Флаг --seed
в конце команды создает магазин по умолчанию с демонстрационными товарами.
Для загрузки изображений товаров на админку необходимо установить файловый сервис типа MinIO, S3 или DigitalOcean Spaces.
Запуск сервера Medusa
После выполнения вышеуказанной команды, устанавливающей все необходимые пакеты Medusa, ваш сервер готов к тестированию! Сначала измените директорию на папку проекта, выполнив команду ниже.
cd flutter-store
Наконец, запустите сервер с помощью команды develop
.
medusa develop
Приведенная выше команда по умолчанию запустит ваш сервер на localhost:9000/.
Установка Medusa Admin
Пришло время приступить к созданию некоторых продуктов, но прежде чем продолжить, давайте установим панель администратора.
Medusa имеет встроенную панель администратора, которая помогает создавать и изменять продукты, клиентов, заказы, купоны, подарочные карты и многое другое.
Чтобы установить Medusa Admin, клонируйте репозиторий Admin с github.
git clone https://github.com/medusajs/admin my-admin
После клонирования измените каталог на каталог my-admin
с помощью команды ниже.
cd my-admin
Находясь в директории, выполните приведенную ниже команду для установки пакетов, необходимых для запуска панели администратора.
npm install
Запуск панели администратора
После того как все зависимости и пакеты будут загружены и установлены должным образом, выполните приведенную ниже команду, чтобы запустить панель администратора в браузере.
N.B.: Чтобы иметь возможность использовать панель администратора, необходимо, чтобы сервер Medusa был запущен.
npm start
Приведенная выше команда создаст и запустит панель администратора в вашем браузере на localhost:7000
по умолчанию.
Войдите в систему, используя email admin@medusa-test.com
и пароль supersecret
.
Создание нашего пользовательского продукта
Перейдите на вкладку Продукты и нажмите на Новый продукт в правом верхнем углу приборной панели, чтобы создать продукт.
Создайте столько продуктов, сколько захотите, а затем нажмите кнопку Publish, чтобы запустить их в работу.
Настройка рабочего пространства для приложения Flutter
Мы успешно опубликовали множество продуктов в нашей панели администратора. Давайте создадим наше приложение для тестирования нашего API.
Создайте приложение Flutter
Выполните приведенную ниже команду в выбранной вами директории, чтобы создать приложение flutter
flutter create my_store
Установка пакетов
В этом уроке мы будем использовать некоторые пакеты flutter
- http: Это сетевой пакет, который помогает в потреблении HTTP и сетевых вызовах
- get: Get — это пакет управления состоянием Flutter.
- animated_splash_screen: Этот пакет будет обрабатывать нашу заставку с анимацией.
Давайте обновим наш файл pubspec.yaml
, импортировав вышеуказанные пакеты.
name: my_store
description: A new Flutter project.
publish_to: "none" # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
sdk: ">=2.17.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
get: ^4.6.3
http: ^0.13.4
animated_splash_screen: ^1.2.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter:
uses-material-design: true
Выполните команду flutter pub get
, чтобы установить пакеты локально.
Структура папок
Давайте посмотрим, какой будет структура папок и файлов в этом проекте.
📦lib
┣ 📂api
┃ ┣ 📜base.dart
┃ ┗ 📜products_api.dart
┣ 📂controller
┃ ┗ 📜productController.dart
┣ 📂model
┃ ┗ 📜product_model.dart
┣ 📂screens
┃ ┣ 📂details
┃ ┃ ┣ 📂components
┃ ┃ ┃ ┣ 📜product_price.dart
┃ ┃ ┃ ┗ 📜product_size.dart
┃ ┃ ┗ 📜product_details.dart
┃ ┣ 📂home
┃ ┃ ┣ 📂components
┃ ┃ ┃ ┣ 📜products.dart
┃ ┃ ┃ ┗ 📜product_card.dart
┃ ┃ ┗ 📜home.dart
┃ ┗ 📜splash_screen.dart
┗ 📜main.dart
- api: Эта папка содержит файл
api.dart
, который мы будем использовать для выполнения вызовов API, иbase.dart
, содержащий наш базовый URL. - controller: Содержит файл dart для управления состоянием.
- модель: Папка model содержит файл модели, который мы будем использовать для отображения нашего json-ответа.
- screens: Папка screen содержит все наши пользовательские интерфейсы. Она содержит две папки, а именно:
- splash_screen.dart: Здесь находится логотип приложения и анимация
- home: Эта папка содержит домашний пользовательский интерфейс нашего приложения и его компоненты.
- details: В этой папке находится экран подробностей о нашем продукте и его компоненты.
main.dart: Это точка входа в наше приложение. Он содержит функцию runApp()
, которая запускает виджет MyApp
.
Компоненты — это простая функциональная часть основного виджета или дизайна. Они помогают обеспечить чистый, сухой и читаемый код.
Создание всплывающего экрана
Прежде чем приступить к созданию Splash-экрана, давайте очистим наш файл lib/main.dart, удалив виджеты MyHomePage
и _myHomePageState
и заменив виджет MyApp
приведенным ниже кодом.
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:my_store/screens/splash_screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'Medusa Store',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: const SplashScreen(),
);
}
}
Исходя из приведенного выше кода, мы используем GetMaterialApp из пакета get для нашего материального дизайна. Это дает нам доступ к навигации и управлению состоянием get. Мы также включаем новый дизайн Flutter useMaterial3, а затем, наконец, маршрутизируемся на наш первый экран, который является Splashscreen
. Это нормально, если вы увидите ошибку на SplashScreen
, поскольку он еще не создан.
Экран всплеска
Splash screen — это причудливый экран длительностью 2,5 секунды, который придает нашему приложению приятное эстетическое вступление.
Добавление файла логотипа
Создайте папку assets в корневой папке проекта и добавьте в нее изображение logo.png. Перейдите к файлу pubspec.yaml
и обновите свойство assets, указав путь к изображению, как показано ниже.
...
flutter:
uses-material-design: true
assets:
- assets/logo.png
Создайте файл splash_screen.dart
в папке lib/screens и вставьте в него код, приведенный ниже
import 'package:animated_splash_screen/animated_splash_screen.dart';
import 'package:flutter/material.dart';
import 'package:my_store/screens/home/home.dart';
class SplashScreen extends StatelessWidget {
const SplashScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return AnimatedSplashScreen(
duration: 2500,
splash: 'assets/logo.png',
splashIconSize: 300,
nextScreen: HomeScreen(),
splashTransition: SplashTransition.fadeTransition,
backgroundColor: Colors.black,
);
}
}
Приведенный выше код отвечает за анимацию заставки. Она длится 2,5 секунды с красивой затухающей анимацией, которая отображает наш логотип на черном фоне, а затем переходит в HomeScreen. После вставки приведенного выше фрагмента кода.
StatelessWidgets — это статические виджеты во Flutter, которые не хранят в себе никакого состояния. Они не могут быть перерендерированы или манипулированы при взаимодействии с пользователем, в то время как StatelessWidget может быть перерендерирован во время выполнения при взаимодействии и активности пользователя.
Настройка модели
Главный экран отвечает за отображение наших продуктов для пользователей. Но прежде чем мы начнем отображать продукты, давайте создадим модель, которая будет отображать ответы от нашего http-вызова.
Создание нашей модели
Создайте файл product_model.dart
в папке lib/model и вставьте в него код, приведенный ниже
import 'dart:convert';
class Products {
Products({
required this.id,
required this.title,
required this.description,
required this.thumbnail,
required this.variants,
required this.options,
required this.images,
});
String id;
String title;
String description;
String thumbnail;
List<Variant> variants;
List<Option> options;
List<Image> images;
factory Products.fromJson(Map<String, dynamic> json) => Products(
id: json["id"],
title: json["title"],
description: json["description"],
thumbnail: json["thumbnail"],
variants: List<Variant>.from(
json["variants"].map((x) => Variant.fromJson(x))),
options:
List<Option>.from(json["options"].map((x) => Option.fromJson(x))),
images: List<Image>.from(json["images"].map((x) => Image.fromJson(x))),
);
}
Свойства variants
, options
и images
содержат данные на втором уровне, в отличие от свойств thumbnail
, title
, id
и description
, данные которых находятся на первом уровне. Чтобы получить наши значения со второго уровня, нам нужно будет создать отдельный класс для каждого still в этом файле model.dart
.
По-прежнему в файле product_model.dart
вставьте следующие блоки кода ниже метода factory, описанного выше.
class Image {
Image({
required this.url,
});
String url;
factory Image.fromJson(Map<String, dynamic> json) => Image(
url: json["url"],
);
}
Исходя из приведенного выше кода, мы получаем наше изображение url
из второго класса, таким образом, сопоставляя его с классом image все еще в файле модели.
class Option {
Option({
required this.title,
required this.values,
});
String title;
List<Value> values;
factory Option.fromJson(Map<String, dynamic> json) => Option(
title: json["title"],
values: List<Value>.from(json["values"].map((x) => Value.fromJson(x))),
);
}
class Value {
Value({
required this.id,
required this.value,
});
String id;
String value;
factory Value.fromJson(Map<String, dynamic> json) => Value(
id: json["id"],
value: json["value"]!,
);
}
Исходя из приведенного выше кода, класс Value
моделируется в класс Option
, чтобы дать ему доступ к данным третьего уровня. Здесь мы пытаемся получить свойство Size для наших продуктов.
class Variant {
Variant({
required this.id,
required this.title,
required this.prices,
required this.options,
});
String id;
String title;
List<Price> prices;
List<Value> options;
factory Variant.fromJson(Map<String, dynamic> json) => Variant(
id: json["id"],
title: json["title"]!,
prices: List<Price>.from(json["prices"].map((x) => Price.fromJson(x))),
options:
List<Value>.from(json["options"].map((x) => Value.fromJson(x))),
);
}
class Price {
Price({
required this.id,
required this.currencyCode,
required this.amount,
});
String id;
String currencyCode;
int amount;
factory Price.fromJson(Map<String, dynamic> json) => Price(
id: json["id"],
currencyCode: json["currency_code"]!,
amount: json["amount"],
);
}
Класс Variant
хранит цены для продукта, которые также находятся на третьем уровне. Поэтому нам потребуется создать отдельный класс Price
, чтобы получить сумму и код валюты для нас.
Приведенная выше модель продукта была создана на основе информации, которую мы хотим получить от нашего API Medusa, включая изображение, название, описание, размер и цену нашего продукта, затем мы сопоставляем ее с нашим входящим json-ответом на API.
Выполнение запроса API из sever
Теперь у нас есть готовая модель, давайте сделаем HTTP-вызов к нашему API.
Настройка урла базы
Создайте файл base.dart
в папке lib/api и вставьте в него фрагмент кода, приведенный ниже. Приведенный ниже фрагмент будет динамически обрабатывать наш URL.
class Base {
static const String baseUrl = '<http://192.168.0.104:9000>';
}
Примечание: URL следует заменить на IP-адрес вашей системы. Это связано с тем, что flutter не распознает localhost.
N.B. Flutter также не поддерживает запрос http://
, так как считает его небезопасным. Чтобы исправить это, перейдите в файл android/app/src/main/AndroidManifest.xml и вставьте приведенный ниже код в тег activity для android
<activity
android:usesCleartextTraffic="true" //paste this
....
</activity>
и вставьте это в файл info.plist, расположенный в папке ios/Runner внутри тега <dist>
.
<dist>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
...
</dict>
Получение продуктов из API
Создайте файл products_api.dart
в папке lib/api и вставьте фрагмент кода ниже
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:my_store/api/base.dart';
import 'package:my_store/model/product_model.dart';
class ProductApi {
Future<List<Products>> getAllProducts() async {
final url = Uri.parse('${Base.baseUrl}/store/products');
try {
final response = await http.get(url);
if (response.statusCode == 200) {
var result = jsonDecode(response.body);
var data = result['products'];
return List<Products>.from(data.map((x) => Products.fromJson(x)));
} else {
return [];
}
} catch (e) {
print(e.toString());
}
throw 'error';
}
}
В приведенном выше коде мы делаем HTTP вызов на наш сервер, используя пакет http и наш base_url, и если запрос успешен, мы декодируем ответ, который сохраняется в переменной result
. Объект product
отфильтровывается в переменную data
, которая затем проходит через модель.
Управление состоянием с помощью Getx
Нам нужно управлять состоянием для входящих данных. Создайте файл productController.dart
в папке lib/controller.
import 'package:get/get.dart';
import 'package:my_store/api/products_api.dart';
import 'package:my_store/model/product_model.dart';
class AllProductsController extends GetxController {
var getAllProducts = <Products>[].obs;
var isLoading = true.obs;
@override
void onInit() {
super.onInit();
fetchAllProduct();
}
void fetchAllProduct() async {
isLoading(true);
try {
var getAllProductResult = await ProductApi().getAllProducts();
getAllProducts.value = getAllProductResult;
} finally {
isLoading(false);
}
}
}
В блоке кода выше мы расширяем наш класс AllProductsController
с помощью GetxController
, чтобы предоставить нам доступ к системе управления get, затем назначаем входящий ответ от вызова API в наблюдаемый список getAllProduct
, который автоматически обновляется при изменении данных в нем, и, наконец, мы устанавливаем значение isLoading
в false. Свойство isLoading
поможет отобразить индикатор загрузки, когда продукт будет получен на сервере.
Построение главного экрана
Главный экран содержит два набора компонентов: карточку продукта и компонент популярных продуктов. Карточка продукта — это пользовательский интерфейс отдельного продукта, а популярный продукт динамически заполняет экран на основе данных из нашего API.
Создание компонента карточки товара
Карточка продукта управляет внешним видом отдельного продукта. Создайте файл product_card.dart
в папке lib/screens/home/components и вставьте приведенный ниже код.
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:my_store/model/product_model.dart' as model;
class ProductCard extends StatelessWidget {
const ProductCard({Key? key, required this.product}) : super(key: key);
final model.Products product;
@override
Widget build(BuildContext context) {
return SizedBox(
width: 140,
child: GestureDetector(
onTap: (){
//Todo: Route to Product Details Screen
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Hero(
tag: product.id.toString(),
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
image: DecorationImage(
image: NetworkImage(product.thumbnail),
fit: BoxFit.cover))),
),
),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Text(
product.title,
style: const TextStyle(
color: Colors.black, fontWeight: FontWeight.w600),
maxLines: 2,
)),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Text("\$${product.variants[0].prices[1].amount / 100}",
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.teal,
))),
const SizedBox(height: 10),
],
)));
}
}
В приведенном выше коде PopularCard
принимает параметр продукта типа Products. Это дает нам доступ к модели отображения, созданной ранее, чтобы заполнить наши виджеты изображениями, ценами и названиями продуктов, которые мы хотим отобразить. Вся карточка обернута GestureDetector
, который дает нам возможность клика, таким образом, при клике на каждой карточке, пользователи будут перенаправлены на экран ProductDetails
, который отображает подробности.
N.B. Модель *Products*
и свойство image в *DecoratedImage*
имеют конфликтующие классы изображений. Чтобы исправить это, нам пришлось привести модель к *model*
.
Создание компонента AllProducts
Компонент All Products содержит наш виджет, который отображает товары для пользователей. Создайте файл products.dart
в папке lib/screens/home/components и вставьте приведенный ниже код.
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:my_store/controller/productController.dart';
import 'package:my_store/screens/home/components/product_card.dart';
class AllProducts extends StatelessWidget {
AllProducts({Key? key}) : super(key: key);
final productController = Get.put(AllProductsController());
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(children: [
Text(
"All Products",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: (20)),
Obx(
() => SizedBox(
width: double.infinity,
child: productController.isLoading.value
? const Center(
child: CircularProgressIndicator(),
)
: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: GridView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: productController.getAllProducts.length,
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 20,
mainAxisSpacing: 20,
childAspectRatio: 0.6,
),
itemBuilder: (BuildContext context, int index) =>
ProductCard(
product: productController.getAllProducts[index],
)),
)),
),
]));
}
}
Компонент PopularProducts
содержит Gridview.builder
, который автоматически генерирует продукт из длины списка getAllProduct
, созданного в классе контроллера AllProductsController
, который мы создали ранее. Но чтобы это произошло, мы должны были инициализировать контроллер при загрузке класса PopularProduct
.
Gridview автоматически перебирает продукты в цикле getAllProduct
и предоставляет нам индекс, который заполняет экран каждым продуктом в ProductCard
.
Создание компонентов для дома
Когда все компоненты готовы, давайте создадим наш главный экран. Создайте файл home.dart
в папке lib/screens/home и вставьте в него приведенный ниже код
import 'package:flutter/material.dart';
import 'package:my_store/screens/home/components/products.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home Page'),
),
body: AllProducts(),
);
}
}
Главный экран состоит из каркаса, который служит элементом верхнего уровня нашего приложения. Он содержит панель App, которая используется для отображения заголовка «Главная страница» в верхней части экрана, и тело, которое принимает в качестве значения виджет PopularProducts()
.
Чтобы проверить наш прогресс, давайте повторно запустим приложение с помощью команды ниже.
flutter run
Учитывая достигнутый прогресс, мы должны быть в состоянии получить все продукты на главный экран.
Построение экрана подробностей
Давайте построим наш экран подробностей, начав с его компонентов.
Создание цены продукта
Наш API предоставляет нам две цены, одну в евро и другую в долларах США. Чтобы получить их, создайте файл product_price.dart
в папке lib/screens/details/components и вставьте приведенный ниже код.
import 'package:flutter/material.dart';
import 'package:my_store/model/product_model.dart';
class ProductPrice extends StatelessWidget {
const ProductPrice({Key? key, required this.product}) : super(key: key);
final Products product;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 50.0,
child: ListView.separated(
itemCount: product.variants[0].prices.length,
scrollDirection: Axis.horizontal,
shrinkWrap: true,
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.symmetric(horizontal:20.0),
child: Row(
children: [
Text(product.variants[0].prices[index].currencyCode.toUpperCase(),
style:
const TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
const SizedBox(width: 10.0),
Text('${product.variants[0].prices[index].amount / 100}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20,
color: Colors.teal)),
],
),
),
separatorBuilder: (context, index) {
return const SizedBox(width: 10.0);
},
),
);
}
}
На основе приведенного выше кода мы перебираем прайс-лист, чтобы получить код валюты и сумму, которую мы делим на 100, а затем отображаем их в строке.
Создание компонента Size
Наконец, давайте создадим компонент Size для отображения различных размеров, доступных для данного товара. Создайте файл product_size.dart
в папке lib/screens/details/components и вставьте приведенный ниже код.
import 'package:flutter/material.dart';
import 'package:my_store/model/product_model.dart';
class ProductSize extends StatelessWidget {
ProductSize({Key? key, required this.product}) : super(key: key);
final Products product;
Color secondary = const Color(0xFFE7B944);
Color textLightColor = const Color(0xFF605A65);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
const Text('Sizes Available',
style: TextStyle(fontSize: 18, color: Colors.grey)),
const SizedBox(width: 20),
...List.generate(
product.variants.length,
(index) => Container(
margin: const EdgeInsets.only(right: 10.0),
padding: const EdgeInsets.symmetric(horizontal: 15.0),
decoration: BoxDecoration(
color: secondary,
borderRadius: BorderRadius.circular(5.0)),
child: Center(
child: Text(
product.variants[index].title,
style: TextStyle(color: textLightColor),
)))),
],
),
),
);
}
}
Код выше возвращает Row
с заголовком «Sizes Available» и виджет List.generate
, который работает точно так же, как Listview.builder
. Виджет List.generate
просматривает весь массив цен и отображает каждую доступную цену товара.
Построение экрана подробной информации о продукте
Наконец, мы построим экран подробностей о продукте, чтобы дать более подробное описание каждого продукта. Создайте файл product_details.dart
в папке lib/screens/details и вставьте приведенный ниже код.
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:my_store/model/product_model.dart' as model;
import 'package:my_store/screens/details/components/product_price.dart';
import 'package:my_store/screens/details/components/product_size.dart';
class ProductDetails extends StatefulWidget {
ProductDetails({Key? key, required this.product}) : super(key: key);
final model.Products product;
@override
State<ProductDetails> createState() => _ProductDetailsState();
}
class _ProductDetailsState extends State<ProductDetails> {
Color white = const Color(0xFFFFFFFF);
int selectedImage = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: Get.width,
height: Get.height * 0.35,
child: Hero(
tag: widget.product.id,
child: Image.network(widget.product.images[selectedImage].url,
fit: BoxFit.cover))),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
widget.product.images.length, (index) => productReview(index)),
),
Padding(
padding: const EdgeInsets.all(20),
child: Text(widget.product.title,
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 24))),
ProductPrice(product: widget.product),
const SizedBox(height: 10.0),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 20.0),
child: Text('Product Details',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18))),
const SizedBox(height: 10.0),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Text(widget.product.description,
style: const TextStyle(fontSize: 18, color: Colors.grey))),
const SizedBox(height: 10.0),
ProductSize(product: widget.product),
],
),
));
}
GestureDetector productReview(int index) {
return GestureDetector(
onTap: () {
setState(() => selectedImage = index);
},
child: Container(
margin: const EdgeInsets.only(right: 15, top: 15),
padding: const EdgeInsets.all(3),
width: 70,
height: 70,
decoration: BoxDecoration(
color: white,
borderRadius: BorderRadius.circular(10),
border: Border.all(
width: 2,
color: selectedImage == index
? const Color(0xFFE7B944)
: Colors.transparent,
),
),
child:
Image.network(widget.product.images[index].url, fit: BoxFit.cover),
),
);
}
}
На основе приведенного выше кода экран ProductDetails
возвращает Scaffold со свойством body. В теле находится виджет Column, который выравнивает дочерние элементы по вертикали на экране, и виджет SingleChildScrollView
, который делает колонку прокручиваемой. Column
содержит виджеты изображений, productImages, title, product details и product size.
Мы также создаем компонент productReview
, который возвращает список вариантов изображений нашего продукта, а при нажатии на каждое из изображений происходит переключение между каждым изображением в строке.
N.B. Мы также приводим модель продукта к имени *model*
, чтобы избежать и устранить проблему конфликта имен.
Давайте решим нашу Todo
в функции onpressed
путем маршрутизации на экран ProductDetails. Вернитесь к файлу product_card.dart
в папке lib/screens/home/components и добавьте приведенный ниже импорт.
import 'package:my_store/screens/details/product_details.dart';
Затем добавьте маршрут к виджету GestureDetector
на экране продукта.
onTap: () {
//Todo: Route to Product Details Screen
Get.to(() => ProductDetails(product: product));
},
В приведенном выше блоке кода мы маршрутизируем на экран ProductDetails
, передавая продукт в качестве реквизита.
Мы закончили! Запустите приложение заново, но убедитесь, что сервер запущен, иначе запустите его с помощью medusa develop
.
Что дальше
Поздравляем вас с тем, что вы дошли до этого момента. В этом руководстве мы рассмотрели, что такое Medusa, некоторые ее возможности и панель администратора. Мы также создали наш магазин, используя Flutter, новый материальный дизайн 3 и API Medusa, из которого мы получали созданные нами товары и отображали их для наших пользователей. Вы можете разместить свою панель администратора на любой хостинговой платформе, например, Netlify.
Исходный код
Вот ссылка на исходный код
https://github.com/iamVictorSam/my-store.git
Заключение
Medusa имеет массу возможностей для реализации. Начиная от системы отслеживания заказов и заканчивая доставкой и оплатой, все упрощено для вас. Также есть боты уведомлений для получения заказов из внешних источников, таких как Slack. Чтобы начать работу с Medusa и внедрить ее плагины, вам не понадобятся специальные навыки, документация уже подготовлена.