Build an NFT Art Collection with Flutter (Celo Composer)

Build an NFT Art Collection with Flutter (Celo Composer) https://celo.academy/uploads/default/optimized/2X/9/99efc592a5a5de8f0c35d2000cc77e2985496786_2_1024x576.jpeg
none 0.0 0

Introduction

In this tutorial, I will be explaining on how to build a CELO NFT art collection where users can mint an nft and create a collection for their art with Flutter using Celo Composer for bootstrap.

Prerequisites

First, This tutorial assumes that you are already familiar with solidity and understand how smart contracts work and also assumes that you already know the basics of using Flutter or you can please clone the flutter celo nft. You’ll also want to make sure you’ve installed the MetaMask Mobile App and have a MetaMask account. If you’d like, see our prior tutorial Setting Up Your Wallet for more details on this.

For this project, we’ll be using a few interesting dependencies & dev dependencies:

We’ll walk you through each one as we go and show you how to use them in combination to write some awesome dapps.

Getting Started

I assume that anyone going through this tutorial already understands and uses Flutter, so I will skip the setup involved in getting Flutter to work on your development computer. That means I assume you already have VS Code/Android Studio together with Android SDK and Flutter setup on your PC.

  • If you are entirely new to Flutter, here in Flutter Docs is a good tutorial you can learn from.

To get started, you can clone this repository flutter-celo-nft or make use of Celo Composer.

Celo Composer is a set of tools and starter templates that makes it easy for you to start your Celo-based web3 projects.

You can check out Celo Composer.

Using Celo Composer

To start our Flutter App Project, we will use the Celo Composer CLI; the CLI makes it easy for us to select the options that we want when bootstrapping our dApp. In other to do that you need to have Node set up on your PC and you can visit Node to get started. Run this on your terminal after setup Node.


npx @celo/celo-composer create

Choose Flutter when asked for the framework

Your Project will now be created; you can check to make sure it has the following folders

packages/flutter-app - Your Flutter project
packages/hardhat - Your Hardhat Folder - Where you can keep your Contracts

Setup the Smart Contract

The next step is to compile our smart contract using the solidity compiler of your choice, such as hardhat, truffle, or any other solidity compiler.

Create a file name NFTCollection.sol in hardhat/contracts folder at the root of your application then paste the below code snippet in it

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract NFTCollection is ERC721, Ownable {
    uint256 public tokenCounter;
    mapping(uint256 => string) private _tokenURIs;

    constructor() ERC721("NFT ART COLLECTION", "NAC") {
        tokenCounter = 0;
    }

    function getTokenCounter() public view returns (uint256) {
        return (tokenCounter);
    }

    function mint(string memory _tokenURI) public {
        _safeMint(msg.sender, tokenCounter);
        _setTokenURI(tokenCounter, _tokenURI);
        tokenCounter++;
    }

    function _setTokenURI(
        uint256 _tokenId,
        string memory _tokenURI
    ) internal virtual {
        require(
            _exists(_tokenId),
            "ERC721Metadata: URI set of nonexistent token"
        ); // Checks if the tokenId exists
        _tokenURIs[_tokenId] = _tokenURI;
    }

    function tokenURI(
        uint256 _tokenId
    ) public view virtual override returns (string memory) {
        require(
            _exists(_tokenId),
            "ERC721Metadata: URI set of nonexistent token"
        );
        return _tokenURIs[_tokenId];
    }
}

For more explanation you can check on this Figment article on how to create nft smart contract with hardhat

Deploy Smart contract (Remix)

Now that your contract is compiled, you can deploy your smart contract to the network. You can deploy to any Ethereum compatible network, and in this case we’ll be deploying the Celo testnet or mainnnet depending on your preference. If you’re brand new to this stick with testnet!

  • Click the Deploy and Run Transactions Icon on the left side menu.

  • Choose Injected Web3 as your environment.

  • Connect MetaMask to Celo testnet and verify the network.

Manage Environmental variable

Create a .env file at the root of your project inside flutter-app folder in packages and add the follow variable

ETHEREUM_RPC=https://alfajores-forno.celo-testnet.org
NFTCOLLECTION_CONTRACT_ADDRESS=0xE6347C2B5A6014C4C8b1ad28930847653e9d4e33

Manage Dependecies

You can add the dependencies mentioned above in the prerequisites manually in the pubspec.yaml file or use the command line below

flutter pub add qr_flutter qr_code_scanner jazzicon dev:build_runner dev:web3dart_builders

ABI

Let’s copy our Contract ABIs into our project inside flutter-app/lib folder.

Then create a folder in the project folder directory lib and create a file named nftcollection.abi.json. Note your file name can be anything but make sure the file extension start and ends with .abi.json. With the help of build_runner & web3dart_builders we will be able to generate a dart contract object file which we will be using to interact with our smart contract.

Run this in your terminal

flutter pub run build_runner build

or

flutter pub run build_runner watch

This will generate a dart file to represent our smart contract object named nftcollection.g.dart.

OpenAI Api

Here we will be using OpenAI image generation to generate different art, head over to OpenAI Platform dashboard to get your api key and update your .env file

ETHEREUM_RPC=https://alfajores-forno.celo-testnet.org
NFTCOLLECTION_CONTRACT_ADDRESS=***
OPENAI_API_KEY=***

UI

To manage our nft collections on wallet authenticated, we need to create a cubit file to update web3cubit state file but to do that we need a model for our nft. In lib folder, open module/home folder then create new folder name model and create the nft model file in it you can name it anything then update the file with the code snippet below

// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';

class NftModel {
  int? tokenId;
  String? tokenUri;
  NftModel({
    this.tokenId,
    this.tokenUri,
  });

  NftModel copyWith({
    int? tokenId,
    String? tokenUri,
  }) {
    return NftModel(
      tokenId: tokenId ?? this.tokenId,
      tokenUri: tokenUri ?? this.tokenUri,
    );
  }

  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'tokenId': tokenId,
      'tokenUri': tokenUri,
    };
  }

  factory NftModel.fromMap(Map<String, dynamic> map) {
    return NftModel(
      tokenId: map['tokenId'] != null ? map['tokenId'] as int : null,
      tokenUri: map['tokenUri'] != null ? map['tokenUri'] as String : null,
    );
  }

  String toJson() => json.encode(toMap());

  factory NftModel.fromJson(String source) =>
      NftModel.fromMap(json.decode(source) as Map<String, dynamic>);

  @override
  String toString() => 'NftModel(tokenId: $tokenId, tokenUri: $tokenUri)';

  @override
  bool operator ==(covariant NftModel other) {
    if (identical(this, other)) return true;

    return other.tokenId == tokenId && other.tokenUri == tokenUri;
  }

  @override
  int get hashCode => tokenId.hashCode ^ tokenUri.hashCode;
}

To render the nft, we need to create another folder inside the module/home folder name widgets (which we will be using to manage all home widgets). Create a new data file name nft_card.dart (You can use any name you like though) and update it with below code

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_celo_composer/module/home/models/nft_model.dart';

class NftCard extends StatelessWidget {
  const NftCard({
    required this.nft,
    super.key,
  });
  final NftModel nft;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(5),
      decoration: BoxDecoration(
          color: const Color(0xFFFCFF52).withOpacity(0.3),
          borderRadius: BorderRadius.circular(10),
          boxShadow: <BoxShadow>[
            BoxShadow(
                offset: const Offset(0, 4),
                blurRadius: 12,
                spreadRadius: 0,
                color: Colors.black.withOpacity(0.04))
          ]),
      child: Column(
        children: <Widget>[
          Expanded(
            child: Container(
              decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(20),
                  image: DecorationImage(
                      image: CachedNetworkImageProvider(nft.tokenUri ?? ''))),
            ),
          ),
          Text(
            '#${nft.tokenId}',
            overflow: TextOverflow.ellipsis,
            style: Theme.of(context)
                .textTheme
                .headlineSmall!
                .copyWith(color: Colors.black),
          )
        ],
      ),
    );
  }
}

In order to generate art from OpenAI, we will be creating a file to manage generate the art and update our UI using cubit. Inside the module/home, create new folder name services/cubits then create two file name them art_cubit.dart and art_state.dart and update them with code below.

import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart' as http;
import 'package:flutter_bloc/flutter_bloc.dart';

part 'art_state.dart';

class ArtCubit extends Cubit<ArtState> {
  ArtCubit() : super(const ArtState());

  /// Get greeting from
  Future<void> fetchNewArt() async {
    try {
      emit(FetchArtLoading());

      final http.Response res = await http.Client().post(
          Uri.parse('https://api.openai.com/v1/images/generations'),
          headers: <String, String>{
            'Authorization': 'Bearer ${dotenv.get('OPENAI_API_KEY')}',
            'Content-Type': 'application/json',
          },
          body: jsonEncode(
              '{"prompt": "A colorful abstract painting with geometric shapes", "n": 2, "size": "1024x1024"}'));

      if (!<int>[200, 201, 202].contains(res.statusCode)) {
        throw 'Failed to fetch data';
      }

      // final Uint8List imageData = res.bodyBytes;
      // final String base64Image = base64Encode(imageData);
      // final String imageUrl = 'data:image/jpeg;base64,$base64Image';
      final Map<String, dynamic> data = jsonDecode(res.body);
      final List<dynamic> rawData = data['data'] as List<dynamic>;

      emit(FetchArtSuccess(url: rawData[0]['url']));
    } catch (e) {
      emit(FetchArtFailed(errorCode: '', message: e.toString()));
    }
  }
}
part of 'art_cubit.dart';

class ArtState {
  const ArtState();
}

class FetchArtLoading extends ArtState {
  FetchArtLoading();
}

class FetchArtSuccess extends ArtState {
  const FetchArtSuccess({required this.url});
  final String url;
}

class FetchArtFailed extends ArtState {
  const FetchArtFailed({
    required this.errorCode,
    required this.message,
  });

  final String errorCode;
  final String message;
}

Now open lib/internal/web3contract.dart file to update our core smart contract function to

/// Greeter contract
/// Function names should be similar to the function name specified in the contract
const String minFunction = 'mint';
const String tokenURIFunction = 'tokenURI';
const String getTokenCounterFunction = 'getTokenCounter';

/// TODO: <another> contract
/// You can add and specify another contract here

Now, let’s update our infracstructure/services/cubit/web3_cubit.dart & infracstructure/services/cubit/web3_state.dart file.

import 'dart:async';

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_celo_composer/internal/ethereum_credentials.dart';
import 'package:flutter_celo_composer/internal/web3_contract.dart';
import 'package:flutter_celo_composer/internal/web3_utils.dart';
import 'package:flutter_celo_composer/module/home/models/nft_model.dart';
import 'package:walletconnect_dart/walletconnect_dart.dart';
import 'package:web3dart/web3dart.dart';

part 'web3_state.dart';

class Web3Cubit extends Cubit<Web3State> {
  Web3Cubit({
    required this.web3Client,
    required this.nftCollectionContract,
  }) : super(const Web3State());

  // core declarations
  final Web3Client web3Client;
  final DeployedContract nftCollectionContract;
  late String sender;
  late SessionStatus sessionStatus;
  late EthereumWalletConnectProvider provider;
  late WalletConnect walletConnector;
  late WalletConnectEthereumCredentials wcCredentials;

  // contract-specific declarations
  late Timer fetchTokenCountTimer;

  /// Terminates metamask, provider, contract connections
  void closeConnection() {
    fetchTokenCountTimer.cancel();
    walletConnector.killSession();
    walletConnector.close();

    emit(SessionTerminated());
  }

  /// Initialize provider provided by [session] and [connector]
  void initializeProvider({
    required WalletConnect connector,
    required SessionStatus session,
  }) {
    walletConnector = connector;
    sessionStatus = session;
    sender = connector.session.accounts[0];
    provider = EthereumWalletConnectProvider(connector);
    wcCredentials = WalletConnectEthereumCredentials(provider: provider);

    /// periodically fetch greeting from chain
    fetchTokenCountTimer =
        Timer.periodic(const Duration(seconds: 5), (_) => fetchTokenCount());
    emit(InitializeProviderSuccess(
        accountAddress: sender, networkName: getNetworkName(session.chainId)));
  }

  /// Greeter contract

  /// Get greeting from
  Future<void> fetchTokenCount() async {
    try {
      List<dynamic> response = await web3Client.call(
        contract: nftCollectionContract,
        function: nftCollectionContract.function(getTokenCounterFunction),
        params: <dynamic>[],
      );
      emit(FetchTokenCountSuccess(counter: (response[0] as BigInt).toInt()));
    } catch (e) {
      emit(FetchTokenCountFailed(errorCode: '', message: e.toString()));
    }
  }

  Future<void> mint(String nftUrl) async {
    emit(MintLoading());
    try {
      String txnHash = await web3Client.sendTransaction(
        wcCredentials,
        Transaction.callContract(
          contract: nftCollectionContract,
          function: nftCollectionContract.function(minFunction),
          from: EthereumAddress.fromHex(sender),
          parameters: <dynamic>[nftUrl],
        ),
        chainId: sessionStatus.chainId,
      );

      late Timer txnTimer;
      txnTimer = Timer.periodic(
          Duration(milliseconds: getBlockTime(sessionStatus.chainId)),
          (_) async {
        TransactionReceipt? t = await web3Client.getTransactionReceipt(txnHash);
        if (t != null) {
          emit(const MintSuccess());
          fetchTokenCount();
          txnTimer.cancel();
        }
      });
    } catch (e) {
      emit(MintFailed(errorCode: '', message: e.toString()));
    }
  }

  Future<List<NftModel>> fetchAllNft(int totalTokenCounter) async {
    try {
      final List<NftModel> nfts = <NftModel>[];
      for (int i = 0; i < totalTokenCounter; i++) {
        List<dynamic> response = await web3Client.call(
          contract: nftCollectionContract,
          function: nftCollectionContract.function(tokenURIFunction),
          params: <dynamic>[BigInt.from(i)],
        );

        nfts.add(NftModel(tokenId: i, tokenUri: response[0]));
      }

      return nfts;
    } catch (e) {
      //

      throw e.toString();
    }
  }
}
part of 'web3_cubit.dart';

class Web3State {
  const Web3State();
}

/// Event classes

class InitializeProviderLoading extends Web3State {
  InitializeProviderLoading();
}

class InitializeProviderSuccess extends Web3State {
  const InitializeProviderSuccess({
    required this.accountAddress,
    required this.networkName,
  });

  final String accountAddress;
  final String networkName;
}

class InitializeProviderFailed extends Web3State {
  const InitializeProviderFailed({
    required this.errorCode,
    required this.message,
  });

  final String errorCode;
  final String message;
}

class SessionTerminated extends Web3State {
  SessionTerminated();
}

/// Greeter contract
/// Contains Greeter contract related events

class FetchTokenCountLoading extends Web3State {
  FetchTokenCountLoading();
}

class FetchTokenCountSuccess extends Web3State {
  const FetchTokenCountSuccess({required this.counter});
  final int counter;
}

class FetchTokenCountFailed extends Web3State {
  const FetchTokenCountFailed({
    required this.errorCode,
    required this.message,
  });

  final String errorCode;
  final String message;
}

class MintLoading extends Web3State {
  MintLoading();
}

class MintSuccess extends Web3State {
  const MintSuccess();
}

class MintFailed extends Web3State {
  const MintFailed({
    required this.errorCode,
    required this.message,
  });

  final String errorCode;
  final String message;
}

/// TODO: <another> contract
/// You can add and specify more contracts here

Lastly, let’s update our home screen file and our app entry point file lib/module/home/interfaces/screen/home_screen.dart & lib/main.dart

import 'dart:async';

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_celo_composer/infrastructures/service/cubit/web3_cubit.dart';
import 'package:flutter_celo_composer/module/auth/interfaces/screens/authentication_screen.dart';
import 'package:flutter_celo_composer/module/home/models/nft_model.dart';
import 'package:flutter_celo_composer/module/home/services/cubit/art_cubit.dart';
import 'package:flutter_celo_composer/module/home/widgets/nft_card.dart';
import 'package:jazzicon/jazzicon.dart';
import 'package:jazzicon/jazziconshape.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:walletconnect_dart/walletconnect_dart.dart';

enum TabSection { art, nft }

class HomeScreen extends StatefulWidget {
  const HomeScreen({
    required this.session,
    required this.connector,
    required this.uri,
    Key? key,
  }) : super(key: key);

  final dynamic session;
  final WalletConnect connector;
  final String uri;

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  String accountAddress = '';
  String networkName = '';
  TextEditingController greetingTextController = TextEditingController();
  JazziconData? jazz;
  int totalNftCounter = 0;
  bool loadingNewArt = false;
  TabSection currentTab = TabSection.art;
  bool minting = false;
  String artUrl =
      'https://oaidalleapiprodscus.blob.core.windows.net/private/org-NgdYB80bWCLnUzlRzOhYiCUw/user-lToV6MMtIH7Nt2nIXJIVHgDA/img-7Yc45JxPIFofnfuHfKNwkOp1.png?st=2023-05-17T10%3A50%3A34Z&se=2023-05-17T12%3A50%3A34Z&sp=r&sv=2021-08-06&sr=b&rscd=inline&rsct=image/png&skoid=6aaadede-4fb3-4698-a8f6-684d7786b067&sktid=a48cca56-e6da-484e-a814-9c849652bcb3&skt=2023-05-16T19%3A11%3A25Z&ske=2023-05-17T19%3A11%3A25Z&sks=b&skv=2021-08-06&sig=T8gIbfAxE/mGe6YIFfSrs/sfh6qwict3QrojXxE9q4o%3D';

  ButtonStyle buttonStyle = ButtonStyle(
    elevation: MaterialStateProperty.all(0),
    backgroundColor: MaterialStateProperty.all(
      Colors.white.withAlpha(60),
    ),
    shape: MaterialStateProperty.all(
      RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)),
    ),
  );

  void updateGreeting() {
    launchUrlString(widget.uri, mode: LaunchMode.externalApplication);

    //context.read<Web3Cubit>().updateGreeting(greetingTextController.text);
    greetingTextController.text = '';
  }

  @override
  void initState() {
    super.initState();

    /// Execute after frame is rendered to get the emit state of InitializeProviderSuccess
    WidgetsBinding.instance.addPostFrameCallback((_) {
      accountAddress = widget.connector.session.accounts[0];
      jazz = Jazzicon.getJazziconData(40,
          address: widget.connector.session.accounts[0]);

      setState(() {});

      context.read<Web3Cubit>().initializeProvider(
            connector: widget.connector,
            session: widget.session,
          );
    });
  }

  void showToast(BuildContext context, String message,
      [Color color = Colors.black, Color textColor = Colors.white]) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
          content: Text(
            message,
            style: Theme.of(context)
                .textTheme
                .headlineSmall!
                .copyWith(color: textColor, fontSize: 14),
          ),
          backgroundColor: color),
    );
  }

  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
    final double width = MediaQuery.of(context).size.width;
    final double height = MediaQuery.of(context).size.height;

    return BlocListener<Web3Cubit, Web3State>(
      listener: (BuildContext context, Web3State state) {
        if (state is SessionTerminated) {
          Future<void>.delayed(const Duration(seconds: 2), () {
            Navigator.of(context).pushReplacement(
              MaterialPageRoute<void>(
                builder: (BuildContext context) => const AuthenticationScreen(),
              ),
            );
          });
        } else if (state is MintFailed) {
          setState(() {
            minting = false;
          });
          showToast(context, state.message, Colors.red, Colors.black);
        } else if (state is MintLoading) {
          setState(() {
            minting = true;
          });
        } else if (state is MintSuccess) {
          setState(() {
            minting = false;
          });
          showToast(
            context,
            'Minted successfully',
          );
          // Generate new art to mint
          // context.read<ArtCubit>().fetchNewArt();
        } else if (state is FetchTokenCountFailed) {
          showToast(context, state.message, Colors.red, Colors.black);
        } else if (state is InitializeProviderSuccess) {
          setState(() {
            accountAddress = state.accountAddress;
            networkName = state.networkName;
          });
        } else if (state is FetchTokenCountSuccess) {
          setState(() {
            totalNftCounter = state.counter;
          });
        }
      },
      child: Scaffold(
        appBar: AppBar(
          elevation: 0,
          title: Image.asset(
            'assets/images/logo.png',
            width: 50,
            height: 50,
          ),
          centerTitle: true,
          // ignore: use_decorated_box
          backgroundColor: const Color(0xFFFCFF52),
        ),
        backgroundColor: Colors.white,
        body: SafeArea(
          child: Column(
            children: <Widget>[
              Column(
                children: <Widget>[
                  const SizedBox(height: 10),
                  if (accountAddress.isNotEmpty)
                    Text(
                      'Address: ${accountAddress.substring(0, 8)}...${accountAddress.substring(accountAddress.length - 8, accountAddress.length)}',
                      style: theme.textTheme.titleMedium!.copyWith(
                        color: Colors.black,
                      ),
                    ),
                ],
              ),
              const SizedBox(height: 10),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 20),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: <Widget>[
                    SizedBox(
                      width: width * 0.4,
                      child: ElevatedButton.icon(
                        onPressed: () {
                          setState(() {
                            currentTab = TabSection.art;
                          });
                        },
                        icon: const SizedBox.shrink(),
                        label: Text(
                          'New art',
                          style: theme.textTheme.titleMedium!
                              .copyWith(color: Colors.black),
                        ),
                        style: ButtonStyle(
                          elevation: MaterialStateProperty.all(0),
                          backgroundColor: MaterialStateProperty.all(
                            Colors.grey.shade400,
                          ),
                          shape: MaterialStateProperty.all(
                            RoundedRectangleBorder(
                              borderRadius: BorderRadius.circular(25),
                            ),
                          ),
                        ),
                      ),
                    ),
                    SizedBox(
                      width: width * 0.4,
                      child: ElevatedButton.icon(
                        onPressed: () {
                          setState(() {
                            currentTab = TabSection.nft;
                          });
                        },
                        icon: CircleAvatar(
                          radius: 15,
                          backgroundColor: Colors.grey.shade400,
                          child: Text(
                            '$totalNftCounter',
                            style: theme.textTheme.titleMedium!
                                .copyWith(color: Colors.black),
                          ),
                        ),
                        label: Text(
                          'My NFT',
                          style: theme.textTheme.titleMedium!
                              .copyWith(color: Colors.black),
                        ),
                        style: ButtonStyle(
                          elevation: MaterialStateProperty.all(0),
                          backgroundColor: MaterialStateProperty.all(
                            Colors.grey.shade400,
                          ),
                          shape: MaterialStateProperty.all(
                            RoundedRectangleBorder(
                              borderRadius: BorderRadius.circular(25),
                            ),
                          ),
                        ),
                      ),
                    ),
                  ],
                ),
              ),
              if (currentTab == TabSection.art)
                BlocListener<ArtCubit, ArtState>(
                  listener: (BuildContext context, ArtState state) {
                    if (state is FetchArtLoading) {
                      loadingNewArt = true;
                      setState(() {});
                    } else if (state is FetchArtFailed) {
                      loadingNewArt = false;
                      setState(() {});

                      ScaffoldMessenger.of(context).showSnackBar(
                        SnackBar(
                          content: Text(state.message),
                          backgroundColor: Colors.red,
                        ),
                      );
                    } else if (state is FetchArtSuccess) {
                      loadingNewArt = false;
                      artUrl = state.url;
                      setState(() {});
                    }
                  },
                  child: Expanded(
                      child: Column(
                    children: <Widget>[
                      if (loadingNewArt)
                        const CircularProgressIndicator(
                          value: 30,
                          color: Color(0xFFFCFF52),
                        ),
                      if (artUrl.isNotEmpty && !loadingNewArt)
                        Column(
                          crossAxisAlignment: CrossAxisAlignment.center,
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: <Widget>[
                            Container(
                              height: height * 0.5,
                              width: width * 0.8,
                              decoration: BoxDecoration(
                                  borderRadius: BorderRadius.circular(20),
                                  image: DecorationImage(
                                      image: CachedNetworkImageProvider(
                                    artUrl,
                                  ))),
                            ),
                            Center(
                              child: SizedBox(
                                width: width * 0.4,
                                child: ElevatedButton.icon(
                                  onPressed: () {
                                    if (minting) {
                                      return;
                                    }

                                    context.read<Web3Cubit>().mint(artUrl);

                                    showToast(
                                        context, 'Minting, please wait...');
                                  },
                                  icon: const SizedBox.shrink(),
                                  label: Text(
                                    'Mint',
                                    style: theme.textTheme.titleMedium!
                                        .copyWith(color: Colors.black),
                                  ),
                                  style: ButtonStyle(
                                    elevation: MaterialStateProperty.all(0),
                                    backgroundColor: MaterialStateProperty.all(
                                      Colors.grey.shade400,
                                    ),
                                    shape: MaterialStateProperty.all(
                                      RoundedRectangleBorder(
                                        borderRadius: BorderRadius.circular(25),
                                      ),
                                    ),
                                  ),
                                ),
                              ),
                            ),
                          ],
                        ),
                      Center(
                        child: SizedBox(
                          width: width * 0.4,
                          child: ElevatedButton.icon(
                            onPressed: () {
                              if (minting) {
                                return;
                              }
                              context.read<ArtCubit>().fetchNewArt();
                            },
                            icon: const SizedBox.shrink(),
                            label: Text(
                              'Generate new art',
                              style: theme.textTheme.titleMedium!
                                  .copyWith(color: Colors.black),
                            ),
                            style: ButtonStyle(
                              elevation: MaterialStateProperty.all(0),
                              backgroundColor: MaterialStateProperty.all(
                                Colors.grey.shade400,
                              ),
                              shape: MaterialStateProperty.all(
                                RoundedRectangleBorder(
                                  borderRadius: BorderRadius.circular(25),
                                ),
                              ),
                            ),
                          ),
                        ),
                      ),
                    ],
                  )),
                )
              else if (currentTab == TabSection.nft)
                Expanded(
                  child: totalNftCounter > 0
                      ? FutureBuilder<List<NftModel>>(
                          future: context
                              .read<Web3Cubit>()
                              .fetchAllNft(totalNftCounter),
                          builder: (BuildContext context,
                              AsyncSnapshot<List<NftModel>> snapshot) {
                            if (snapshot.hasData && snapshot.data != null) {
                              return GridView.builder(
                                padding: const EdgeInsets.symmetric(
                                    horizontal: 20, vertical: 10),
                                gridDelegate:
                                    const SliverGridDelegateWithFixedCrossAxisCount(
                                  crossAxisCount: 2,
                                  childAspectRatio: 1 / 1.08,
                                  crossAxisSpacing: 10,
                                  mainAxisSpacing: 10,
                                ),
                                shrinkWrap: true,
                                itemBuilder: (_, int index) {
                                  return NftCard(
                                    nft: snapshot.data![index],
                                    key: Key(index.toString()),
                                  );
                                },
                                itemCount: snapshot.data!.length,
                              );
                            } else if (snapshot.hasError) {
                              return Text(
                                snapshot.error.toString(),
                                style: Theme.of(context)
                                    .textTheme
                                    .headlineSmall!
                                    .copyWith(
                                        color: Colors.black, fontSize: 14),
                              );
                            }

                            return const Center(
                              child: SizedBox(
                                height: 30,
                                width: 30,
                                child: CircularProgressIndicator(
                                  value: 30,
                                  color: Color(0xFFFCFF52),
                                ),
                              ),
                            );
                          },
                        )
                      : Text(
                          'Nothing in your collection',
                          style: Theme.of(context)
                              .textTheme
                              .headlineSmall!
                              .copyWith(color: Colors.black, fontSize: 14),
                        ),
                ),
              Container(
                padding:
                    const EdgeInsets.symmetric(vertical: 10, horizontal: 10),
                child: SizedBox(
                  width: width,
                  child: ElevatedButton.icon(
                    onPressed: context.read<Web3Cubit>().closeConnection,
                    icon: const Icon(
                      Icons.power_settings_new,
                    ),
                    label: Text(
                      'Disconnect',
                      style: theme.textTheme.titleMedium!
                          .copyWith(color: Colors.black),
                    ),
                    style: ButtonStyle(
                      elevation: MaterialStateProperty.all(0),
                      backgroundColor: MaterialStateProperty.all(
                        Colors.grey.shade400,
                      ),
                      shape: MaterialStateProperty.all(
                        RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(25),
                        ),
                      ),
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_celo_composer/configs/themes.dart';
import 'package:flutter_celo_composer/configs/web3_config.dart';
import 'package:flutter_celo_composer/infrastructures/repository/secure_storage_repository.dart';
import 'package:flutter_celo_composer/infrastructures/service/cubit/secure_storage_cubit.dart';
import 'package:flutter_celo_composer/infrastructures/service/cubit/web3_cubit.dart';
import 'package:flutter_celo_composer/module/auth/interfaces/screens/authentication_screen.dart';
import 'package:flutter_celo_composer/module/auth/service/cubit/auth_cubit.dart';
import 'package:flutter_celo_composer/module/home/services/cubit/art_cubit.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:walletconnect_dart/walletconnect_dart.dart';
import 'package:web3dart/web3dart.dart';

Future<void> main() async {
  /// Load env file
  await dotenv.load();

  runApp(
    MyApp(
      walletConnect: await walletConnect,
      nftCollectionContract: await deployedNftCollectionContract,
      web3client: web3Client,
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({
    required this.walletConnect,
    required this.nftCollectionContract,
    required this.web3client,
    Key? key,
  }) : super(key: key);
  final WalletConnect walletConnect;
  final DeployedContract nftCollectionContract;
  final Web3Client web3client;

  @override
  Widget build(BuildContext context) {
    return MultiBlocProvider(
      providers: <BlocProvider<dynamic>>[
        BlocProvider<Web3Cubit>(
          create: (BuildContext context) => Web3Cubit(
            web3Client: web3client,
            nftCollectionContract: nftCollectionContract,
          ),
        ),
        BlocProvider<ArtCubit>(
          create: (BuildContext context) => ArtCubit(),
        ),
        BlocProvider<AuthCubit>(
          create: (BuildContext context) => AuthCubit(
            storage: SecureStorageRepository(),
            connector: walletConnect,
          ),
        ),
        BlocProvider<SecureStorageCubit>(
          create: (BuildContext context) => SecureStorageCubit(
            storage: SecureStorageRepository(),
          ),
        ),
      ],
      child: MaterialApp(
        title: 'Sophon',
        debugShowCheckedModeBanner: false,
        theme: buildDefaultTheme(context),
        home: const MyHomePage(),
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    /// Lock app to portrait mode
    SystemChrome.setPreferredOrientations(<DeviceOrientation>[
      DeviceOrientation.portraitUp,
      DeviceOrientation.portraitDown,
    ]);
    return const AuthenticationScreen();
  }
}

You might experience some issues with most of the testing files, I will advice to comment them out since we are not focusing on testing in this tutorial. Now it’s time we run the app for testing.

Note: It is advisable to test on physical android device.



Conclusion

Congratulations, you have now learned how to build an nft art collections using Celo Composer. You have seen how to connect with a blockchain wallet, interact with smart contract, mint and render nft collections.

About the Author

I am a Software Engineer, Tech Evangelist (Preaching the gospel of flutter & blockchain) also and Ex-GDSC Leads.

References

5 Likes

i have always been a flutter lover. would love to read this.

2 Likes

Approved for you to get started. You can manage the tutorial here by changing the category to Proposals > In Progress then Proposals > Review as you complete the tutorial. Thanks!

2 Likes

I’ll be reviewing your piece

Review Suggestions

  1. Hi @mujhtech can you make sure all your links are embedded and also you can loose the emojis at the start, since it’s not rendering
1 Like

Nice you embedded the link although, I just noticed you piece was intersected in between, there are some comments dividing the tutorial, you should be able to make changes to correct that

1 Like

Hi @joenyzio we need your help here…

Is there a way we can increase the word count limit for a piece?

2 Likes

Hey @Phenzic - I just 3x it from 32000 to 100000. Merged this post into 1 and I’ll look out for others we can merge too. Thanks for the callout :rocket:

1 Like

Hi @mujhtech can you attach your contact details to your tutorial “about the author” section,

LinkedIn :ballot_box_with_check:
and Github… if you would like to

Ouu and you’ll notice your word count limit spell has been lifted :ok_hand::clinking_glasses:

Not a fan of bloc. Would like to test this out with riverpod soon. Great work

1 Like

I really need someone to teach me Flutter. Most videos I found on Youtube are not comprehensive enough.

10 Likes

You can check Code with Andrea. Has very comprehensive courses.

The issue with flutter videos currently is that any video recorder earlier than 2020 or 2021 sef would give a lot of issues.

1 Like

Dart has updates quite frequently. I used NetNinja to get a grasp of flutter then. But now his course would be a bit redundant

2 Likes