Mobile Development with Flutter

Introduction

  • 구글에서 개발한 모바일, 웹, 데스크탑 등을 위한 UI 개발 프레임워크
    • 싱글 코드 기반으로 모든 플랫폼의 UI 개발을 지향
  • Dart 프로그래밍 언어를 사용하여 개발함

Setup an Environment

Flutter CLI 설치

  • flutter 컴파일을 위한 프로그램 설치
yay -S flutter
brew install flutter

Android 환경 설치

brew install adopt-openjdk
yay -S jdk-openjdk
brew install android-sdk
yay -S android-sdk android-sdk-platform-tools
  • Installing Android SDK into the PC
unzip commandlinetools-linux-6200805_latest.zip
cd tools
export ANDROID_HOME="path-to-install-sdk"
bin/sdkmanager --sdk_root=$ANDROID_HOME "tools"
export PATH="$ANDROID_HOME/tools/bin:$ANDROID_HOME/tools:$ANDROID_HOME/emulator:$ANDROID_HOME/platform-tools:$PATH"
mkdir ~/.android
touch ~/.android/repositories.cfg
sdkmanager "build-tools;28.0.3" "platforms;android-28"

Android Emulator 설정

  • Installing AVD targets and running an emulator
sdkmanager "system-images;android-28;google_apis;x86_64" "system-images;android-28;google_apis;x86"
avdmanager create avd --package "system-images;android-28;google_apis;x86_64" --name android-28
avdmanager list avd
emulator @android-28

Android Emulator 설정 확인

flutter emulators
2 available emulators:

luffy • luffy • Google • android
zoro  • zoro  • Google • android

To run an emulator, run 'flutter emulators --launch <emulator id>'.
To create a new emulator, run 'flutter emulators --create [--name xyz]'.

You can find more information on managing emulators at the links below:
  https://developer.android.com/studio/run/managing-avds
  https://developer.android.com/studio/command-line/avdmanager

Android Emulator 실행 및 확인

flutter emulators --launch luffy
flutter devices
3 connected devices:

Android SDK built for x86 (mobile) • emulator-5554 • android-x86    • Android 8.1.0 (API 27) (emulator)
Linux (desktop)                    • linux         • linux-x64      • Linux
Chrome (web)                       • chrome        • web-javascript • Google Chrome 92.0.4515.159

Flutter 개발

기본 프로젝트 생성

  • 프로젝트 이름은 dart 의 패키지 이름 컨벤션에 따라야함.
    • 알파벳과 _ 를 사용할수 있음.
    • - 는 사용할 수 없음
flutter create simpleapp

기본 프로젝트 구조

  • 디렉토리 구조가 복잡해보이지만 main.dart 만 집중하면 됨.
tree simpleapp -L 3
/home/hackartist/data/devel/github.com/hackartists/test/simpleapp
├── android
│   ├── app
│   │   ├── build.gradle
│   │   └── src
│   ├── build.gradle
│   ├── gradle
│   │   └── wrapper
│   ├── gradle.properties
│   ├── gradlew
│   ├── gradlew.bat
│   ├── local.properties
│   ├── settings.gradle
│   └── simpleapp_android.iml
├── ios
│   ├── Flutter
│   │   ├── AppFrameworkInfo.plist
│   │   ├── Debug.xcconfig
│   │   ├── flutter_export_environment.sh
│   │   ├── Generated.xcconfig
│   │   └── Release.xcconfig
│   ├── Runner
│   │   ├── AppDelegate.swift
│   │   ├── Assets.xcassets
│   │   ├── Base.lproj
│   │   ├── GeneratedPluginRegistrant.h
│   │   ├── GeneratedPluginRegistrant.m
│   │   ├── Info.plist
│   │   └── Runner-Bridging-Header.h
│   ├── Runner.xcodeproj
│   │   ├── project.pbxproj
│   │   ├── project.xcworkspace
│   │   └── xcshareddata
│   └── Runner.xcworkspace
│       ├── contents.xcworkspacedata
│       └── xcshareddata
├── lib
│   └── main.dart
├── linux
│   ├── CMakeLists.txt
│   ├── flutter
│   │   ├── CMakeLists.txt
│   │   ├── generated_plugin_registrant.cc
│   │   ├── generated_plugin_registrant.h
│   │   └── generated_plugins.cmake
│   ├── main.cc
│   ├── my_application.cc
│   └── my_application.h
├── pubspec.lock
├── pubspec.yaml
├── README.md
├── simpleapp.iml
├── test
│   └── widget_test.dart
└── web
    ├── favicon.png
    ├── icons
    │   ├── Icon-192.png
    │   └── Icon-512.png
    ├── index.html
    └── manifest.json

21 directories, 39 files

Deep Dive into Flutter

  • Android 실행

    flutter run -d android
    
  • lib/main.dart

    import 'package:flutter/material.dart';
    
    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      // This widget is the root of your application.
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            // This is the theme of your application.
            //
            // Try running your application with "flutter run". You'll see the
            // application has a blue toolbar. Then, without quitting the app, try
            // changing the primarySwatch below to Colors.green and then invoke
            // "hot reload" (press "r" in the console where you ran "flutter run",
            // or simply save your changes to "hot reload" in a Flutter IDE).
            // Notice that the counter didn't reset back to zero; the application
            // is not restarted.
            primarySwatch: Colors.blue,
          ),
          home: MyHomePage(title: 'Flutter Demo Home Page'),
        );
      }
    }
    
    class MyHomePage extends StatefulWidget {
      MyHomePage({Key? key, required this.title}) : super(key: key);
    
      // This widget is the home page of your application. It is stateful, meaning
      // that it has a State object (defined below) that contains fields that affect
      // how it looks.
    
      // This class is the configuration for the state. It holds the values (in this
      // case the title) provided by the parent (in this case the App widget) and
      // used by the build method of the State. Fields in a Widget subclass are
      // always marked "final".
    
      final String title;
    
      @override
      _MyHomePageState createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      int _counter = 0;
    
      void _incrementCounter() {
        setState(() {
          // This call to setState tells the Flutter framework that something has
          // changed in this State, which causes it to rerun the build method below
          // so that the display can reflect the updated values. If we changed
          // _counter without calling setState(), then the build method would not be
          // called again, and so nothing would appear to happen.
          _counter++;
        });
      }
    
      @override
      Widget build(BuildContext context) {
        // This method is rerun every time setState is called, for instance as done
        // by the _incrementCounter method above.
        //
        // The Flutter framework has been optimized to make rerunning build methods
        // fast, so that you can just rebuild anything that needs updating rather
        // than having to individually change instances of widgets.
        return Scaffold(
          appBar: AppBar(
            // Here we take the value from the MyHomePage object that was created by
            // the App.build method, and use it to set our appbar title.
            title: Text(widget.title),
          ),
          body: Center(
            // Center is a layout widget. It takes a single child and positions it
            // in the middle of the parent.
            child: Column(
              // Column is also a layout widget. It takes a list of children and
              // arranges them vertically. By default, it sizes itself to fit its
              // children horizontally, and tries to be as tall as its parent.
              //
              // Invoke "debug painting" (press "p" in the console, choose the
              // "Toggle Debug Paint" action from the Flutter Inspector in Android
              // Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
              // to see the wireframe for each widget.
              //
              // Column has various properties to control how it sizes itself and
              // how it positions its children. Here we use mainAxisAlignment to
              // center the children vertically; the main axis here is the vertical
              // axis because Columns are vertical (the cross axis would be
              // horizontal).
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text(
                  'You have pushed the button this many times:',
                ),
                Text(
                  '$_counter',
                  style: Theme.of(context).textTheme.headline4,
                ),
              ],
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: _incrementCounter,
            tooltip: 'Increment',
            child: Icon(Icons.add),
          ), // This trailing comma makes auto-formatting nicer for build methods.
        );
      }
    }
    

KAS Tutorial

KAS Tutorial 메인 페이지

코드 구조

/home/hackartist/data/devel/github.com/hackartists/kas-tutorial-ui
├── android
│   ├── app
│   │   ├── build.gradle
│   │   └── src
│   ├── build.gradle
│   ├── gradle
│   │   └── wrapper
│   ├── gradle.properties
│   ├── gradlew
│   ├── gradlew.bat
│   ├── kastutorial_android.iml
│   ├── local.properties
│   ├── settings_aar.gradle
│   └── settings.gradle
├── assets
│   └── images
│       ├── demo_img.png
│       ├── demo_widget.png
│       ├── klaytn-logo-green.png
│       ├── profile_img.jpeg
│       └── select-image.png
├── ios
│   ├── Flutter
│   │   ├── AppFrameworkInfo.plist
│   │   ├── Debug.xcconfig
│   │   ├── flutter_export_environment.sh
│   │   ├── Generated.xcconfig
│   │   └── Release.xcconfig
│   ├── Podfile
│   ├── Runner
│   │   ├── AppDelegate.swift
│   │   ├── Assets.xcassets
│   │   ├── Base.lproj
│   │   ├── GeneratedPluginRegistrant.h
│   │   ├── GeneratedPluginRegistrant.m
│   │   ├── Info.plist
│   │   └── Runner-Bridging-Header.h
│   ├── Runner.xcodeproj
│   │   ├── project.pbxproj
│   │   ├── project.xcworkspace
│   │   └── xcshareddata
│   └── Runner.xcworkspace
│       ├── contents.xcworkspacedata
│       └── xcshareddata
├── kastutorial.iml
├── lib
│   ├── components
│   │   ├── app_button.dart
│   │   ├── list_cards.dart
│   │   ├── nft_card.dart
│   │   ├── safe_money_tile.dart
│   │   ├── select_warrant_card.dart
│   │   └── toast.dart
│   ├── constants.dart
│   ├── generated_plugin_registrant.dart
│   ├── interfaces
│   │   └── sqlite_model.dart
│   ├── main.dart
│   ├── models
│   │   ├── klay_transfer.dart
│   │   ├── nft_token.dart
│   │   ├── safe.dart
│   │   └── user.dart
│   ├── screens
│   │   ├── create_safe_money_screen.dart
│   │   ├── home_screen.dart
│   │   ├── issue_nft_screen.dart
│   │   ├── klay_history_screen.dart
│   │   ├── login_screen.dart
│   │   ├── nft_home_screen.dart
│   │   ├── safe_money_cards_screen.dart
│   │   ├── safe_money_screen.dart
│   │   ├── send_klay_screen.dart
│   │   ├── send_nft_screen.dart
│   │   └── sign_card_screen.dart
│   ├── services
│   │   └── client.dart
│   ├── store
│   │   ├── preference.dart
│   │   └── sqlite.dart
│   └── themes
├── linux
│   ├── CMakeLists.txt
│   ├── flutter
│   │   ├── CMakeLists.txt
│   │   ├── ephemeral
│   │   ├── generated_plugin_registrant.cc
│   │   ├── generated_plugin_registrant.h
│   │   └── generated_plugins.cmake
│   ├── main.cc
│   ├── my_application.cc
│   └── my_application.h
├── pubspec.lock
├── pubspec.yaml
├── README.html
├── README.md
├── run.sh
├── test
│   └── widget_test.dart
└── web
    ├── favicon.png
    ├── icons
    │   ├── Icon-192.png
    │   └── Icon-512.png
    ├── index.html
    └── manifest.json

31 directories, 75 files

엔트리 코드

  • lib/main.dart

    import 'package:flutter/material.dart';
    import 'package:kastutorial/store/preference.dart';
    import 'package:flutter_dotenv/flutter_dotenv.dart' as DotEnv;
    import 'package:kastutorial/store/sqlite.dart';
    
    import 'screens/home_screen.dart';
    import 'screens/login_screen.dart';
    
    Future main() async {
      await DotEnv.load(fileName: '.env');
      await Sqlite.initDatabase();
      runApp(MyApp());
    }
    
    class MyApp extends StatefulWidget {
      // This widget is the root of your application.
      @override
      _MyAppState createState() => _MyAppState();
    }
    
    class _MyAppState extends State<MyApp> {
      String address;
      String username;
      String password;
    
      @override
      void initState() {
        getAccountAddress();
        super.initState();
      }
    
      getAccountAddress() async {
        Preference.getAddress().then((value) => setState(() {
              address = value;
            }));
        Preference.getUsername().then((value) => setState(() {
              username = value;
            }));
        Preference.getPassword().then((value) => setState(() {
              password = value;
            }));
      }
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'KAS Tutorial',
          theme: ThemeData(
            // brightness: Brightness.dark,
            primarySwatch: Colors.deepPurple,
            accentColor: Colors.orange,
            textSelectionTheme: TextSelectionThemeData(cursorColor: Colors.orange),
            // fontFamily: 'SourceSansPro',
            textTheme: TextTheme(
              headline3: TextStyle(
                fontFamily: 'OpenSans',
                fontSize: 45.0,
                // fontWeight: FontWeight.w400,
                color: Colors.orange,
              ),
              button: TextStyle(
                // OpenSans is similar to NotoSans but the uppercases look a bit better IMO
                fontFamily: 'OpenSans',
              ),
              caption: TextStyle(
                fontFamily: 'NotoSans',
                fontSize: 12.0,
                fontWeight: FontWeight.normal,
                color: Colors.deepPurple[300],
              ),
              headline1: TextStyle(fontFamily: 'Quicksand'),
              headline2: TextStyle(fontFamily: 'Quicksand'),
              headline4: TextStyle(fontFamily: 'Quicksand'),
              headline5: TextStyle(fontFamily: 'NotoSans'),
              headline6: TextStyle(fontFamily: 'NotoSans'),
              subtitle1: TextStyle(fontFamily: 'NotoSans'),
              bodyText1: TextStyle(fontFamily: 'NotoSans'),
              bodyText2: TextStyle(fontFamily: 'NotoSans'),
              subtitle2: TextStyle(fontFamily: 'NotoSans'),
              overline: TextStyle(fontFamily: 'NotoSans'),
            ),
          ),
          home: username != null &&
                  username != "" &&
                  password != null &&
                  password != ""
              ? HomeScreen(username: username, password: password)
              : LoginScreen(),
          // navigatorObservers: [TransitionRouteObserver()],
          // initialRoute: LoginScreen.routeName,
          // routes: {
          //   LoginScreen.routeName: (context) => LoginScreen(),
          //   DashboardScreen.routeName: (context) => DashboardScreen(),
          // },
        );
      }
    }
    
    

홈 화면 구성

  • lib/screens/home_screen.dart
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:kastutorial/components/app_button.dart';
import 'package:kastutorial/screens/klay_history_screen.dart';
import 'package:kastutorial/screens/nft_home_screen.dart';
import 'package:kastutorial/screens/safe_money_screen.dart';
import 'package:kastutorial/screens/send_klay_screen.dart';
import 'package:kastutorial/services/client.dart';
import 'package:kastutorial/store/preference.dart';

class HomeScreen extends StatefulWidget {
  final String username;
  final String password;

  HomeScreen({Key key, this.username, this.password}) : super(key: key);

  @override
  HomeScreenState createState() =>
      HomeScreenState(username: this.username, password: this.password);
}

class HomeScreenState extends State<HomeScreen> {
  String username;
  String password;
  String address;
  String balance;
  static int interval;

  HomeScreenState({this.username, this.password});

  @override
  void initState() {
    setState(() {
      this.balance = '-';
    });
    super.initState();
    if (username != null) {
      Client.getBalance(username).then((value) => setState(() {
            this.balance = value;
          }));
    }

    interval = int.parse(env['INTERVAL']);
    Timer.periodic(new Duration(seconds: interval), (timer) async {
      print(username);
      String balance = await Client.getBalance(username);
      setState(() {
        this.balance = balance;
      });
    });

    Preference.getUsername().then((value) => setState(() {
          this.username = value;
        }));

    Preference.getPassword().then((value) => setState(() {
          this.password = value;
        }));

    Preference.getAddress().then((value) => setState(() {
          this.address = value;
        }));
  }

  sendKlay() async {}

  @override
  Widget build(BuildContext context) {
    final _width = MediaQuery.of(context).size.width;
    final _height = MediaQuery.of(context).size.height;

    return new Container(
      child: new Stack(
        children: <Widget>[
          new Scaffold(
            backgroundColor: Colors.transparent,
            body: buildContainer(_height, _width),
            floatingActionButton: FloatingActionButton(
              tooltip: 'KLAY 전송',
              onPressed: () {
                Navigator.of(context).push(MaterialPageRoute(
                  builder: (context) {
                    return SendKlayScreen(user: this.username);
                  },
                ));
              },
              child: Icon(Icons.send),
            ), // This trailing comma makes auto-formatting nicer for build methods.
          ),
        ],
      ),
    );
  }

  Container buildContainer(double _height, double _width) {
    return new Container(
      decoration: new BoxDecoration(),
      child: new Stack(
        children: <Widget>[
          new Container(
            decoration: new BoxDecoration(
              borderRadius: BorderRadius.only(
                bottomLeft: Radius.circular(40.0),
                bottomRight: Radius.circular(40.0),
              ),
              gradient: new LinearGradient(
                colors: [
                  const Color(0xFFFFCBE6),
                  const Color(0xFF26CBFF),
                ],
                begin: Alignment.topCenter,
                end: Alignment.center,
              ),
            ),
            child: new Align(
              alignment: Alignment.center,
              child: new Padding(
                padding: new EdgeInsets.only(top: _height / 15),
                child: buildProfile(_height),
              ),
            ),
          ),
          new Padding(
            padding: new EdgeInsets.only(top: _height / 2.2),
            child: new Container(
              color: Colors.white,
            ),
          ),
          new Padding(
            padding: new EdgeInsets.only(
              top: _height / 2.6,
              left: _width / 20,
              right: _width / 20,
            ),
            child: buildProfileInfo(_width),
          ),
          new Padding(
            padding: new EdgeInsets.only(top: _height / 1.9),
            child: Center(
              widthFactor: _width,
              child: Column(
                children: [
                  new Row(
                    children: [
                      Spacer(),
                      AppButton(
                        label: "거래내역",
                        icon: Icons.history,
                        onPressed: () {
                          Navigator.of(context).push(
                            MaterialPageRoute(
                              builder: (context) {
                                return KlayHistoryScreen(userId: this.username);
                              },
                            ),
                          );
                        },
                      ),
                      Spacer(),
                      AppButton(
                        label: "자산",
                        icon: Icons.card_giftcard,
                        color: Colors.blueAccent,
                        onPressed: () {
                          Navigator.of(context).push(
                            MaterialPageRoute(
                              builder: (context) {
                                return NftHomeScreen(
                                  username: username,
                                );
                              },
                            ),
                          );
                        },
                      ),
                      Spacer(),
                      AppButton(
                        label: "공동금고",
                        icon: Icons.monetization_on,
                        color: Colors.amberAccent,
                        onPressed: () {
                          Navigator.of(context).push(
                            MaterialPageRoute(
                              builder: (context) {
                                return SafeMoneyScreen(
                                  userId: username,
                                );
                              },
                            ),
                          );
                        },
                      ),
                      Spacer(),
                      // AppButton(
                      //   label: "포인트",
                      //   icon: Icons.credit_card,
                      //   color: Colors.blueAccent,
                      //   onPressed: () {},
                      // ),
                    ],
                  ),
                  new Row(
                    children: [],
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

  Column buildProfileInfo(double _width) {
    return new Column(
      children: <Widget>[
        new Container(
          decoration: new BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.all(Radius.circular(40.0)),
            boxShadow: [
              new BoxShadow(
                color: Colors.black45,
                blurRadius: 2.0,
                offset: new Offset(0.0, 2.0),
              )
            ],
          ),
          child: new Padding(
            padding: new EdgeInsets.all(_width / 20),
            child: new Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  headerChild('KLAY', this.balance),
                  // headerChild('Followers', 1205),
                  // headerChild('Following', 360),
                ]),
          ),
        ),
        // buildPadding(_height, _width)
      ],
    );
  }

  Column buildProfile(double _height) {
    return new Column(
      crossAxisAlignment: CrossAxisAlignment.center,
      children: <Widget>[
        new CircleAvatar(
          backgroundColor: Colors.transparent,
          backgroundImage:
              new AssetImage('assets/images/klaytn-logo-green.png'),
          radius: _height / 10,
        ),
        new SizedBox(
          height: _height / 30,
        ),
        new Text(
          this.username,
          style: new TextStyle(
              fontSize: 18.0, color: Colors.white, fontWeight: FontWeight.bold),
        )
      ],
    );
  }

  Widget headerChild(String header, String value) => new Expanded(
          child: new Column(
        children: <Widget>[
          new Text(header),
          new SizedBox(
            height: 8.0,
          ),
          new Text(
            '$value',
            style: new TextStyle(
                fontSize: 14.0,
                color: const Color(0xFF26CBE6),
                fontWeight: FontWeight.bold),
          )
        ],
      ));

  Widget infoChild(double width, IconData icon, data) => new Padding(
        padding: new EdgeInsets.only(bottom: 8.0),
        child: new InkWell(
          child: new Row(
            children: <Widget>[
              new SizedBox(
                width: width / 10,
              ),
              new Icon(
                icon,
                color: const Color(0xFF26CBE6),
                size: 36.0,
              ),
              new SizedBox(
                width: width / 20,
              ),
              new Text(data)
            ],
          ),
          onTap: () {
            print('Info Object selected');
          },
        ),
      );
}

API 호출 방법

  • lib/services/client.dart
import 'dart:convert';

import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart' as http;
import 'package:kastutorial/models/klay_transfer.dart';
import 'package:kastutorial/models/nft_token.dart';
import 'package:kastutorial/models/safe.dart';
import 'package:kastutorial/models/user.dart';

class Client {
  static final endpoint = env['CLIENT_URL'];

  static Future<User> loginUser(String username, String password) async {
    final response = await http.post(
      Uri.http(endpoint, '/v1/user'),
      headers: <String, String>{
        'Content-Type': 'application/json; charset=UTF-8',
      },
      body: jsonEncode(<String, String>{
        "username": username,
        "password": password,
      }),
    );

    if (response.statusCode == 200) {
      print(response.body);
      return User.fromJson(jsonDecode(response.body));
    }

    throw Exception('failed to sign in');
  }

  static Future<String> getBalance(String userid) async {
    final response =
        await http.get(Uri.http(endpoint, '/v1/user/$userid/klay'));

    if (response.statusCode == 200) {
      return jsonDecode(response.body)['balance'];
    }

    throw Exception('failed to get balance');
  }

  static Future<String> sendKlay(
      String userid, String toUserid, String amount) async {
    final response = await http.post(
      Uri.http(endpoint, '/v1/user/$userid/klay'),
      headers: <String, String>{
        'Content-Type': 'application/json; charset=UTF-8',
      },
      body: jsonEncode(<String, dynamic>{
        "to": toUserid,
        "amount": amount,
      }),
    );

    if (response.statusCode == 200) {
      print(response.body);
      return jsonDecode(response.body)['txHash'];
    }

    throw Exception('failed to send balance');
  }

  static Future<List> getSuggestions(String pattern) async {
    final response = await http
        .get(Uri.http(endpoint, "/v1/search", {"user-pattern": pattern}));

    print(response.statusCode);
    if (response.statusCode == 200) {
      print(response.body);
      return jsonDecode(response.body)['users'];
    }

    return [];
  }

  static Future<List<KlayTransfer>> getKlayHistory(
      String userId, int start, int end) async {
    final response = await http
        .get(Uri.http(endpoint, "/v1/user/$userId/klay/transfer-history", {
      'start-timestamp': start.toString(),
      'end-timestamp': end.toString(),
    }));

    print(response.statusCode);
    List<KlayTransfer> ret = [];
    if (response.statusCode == 200) {
      List<dynamic> history = jsonDecode(response.body);

      history.forEach((element) {
        ret.add(KlayTransfer.fromJson(element));
      });
    }

    return ret;
  }

  static Future<String> issueToken(user, path, name, kind) async {
    var request = http.MultipartRequest(
      'POST',
      Uri.http(endpoint, '/v1/asset/$user/issue'),
    )
      ..fields['name'] = name
      ..fields['kind'] = kind
      ..files.add(await http.MultipartFile.fromPath(
        'file',
        path,
      ));
    var response = await request.send();
    String body = await response.stream.bytesToString();
    print(body);
    String uri = jsonDecode(body)['uri'];

    return 'http://$endpoint$uri';
  }

  static Future<List> listTokens(user) async {
    final response =
        await http.get(Uri.http(endpoint, "/v1/asset/$user/token"));

    List<NftToken> ret = [];
    if (response.statusCode == 200) {
      List<dynamic> history = jsonDecode(response.body);
      print(history);

      for (final el in history) {
        final tokenUri = el['tokenUri'];
        print(tokenUri);
        final resp = await http.get(Uri.parse(tokenUri));

        ret.add(NftToken.fromResponseBody(jsonDecode(resp.body)));
      }
      history.forEach((element) {});
    }

    return ret;
  }

  static Future<String> sendNftToken(user, tokenId, to) async {
    final response = await http.post(
      Uri.http(endpoint, '/v1/asset/$user/token/$tokenId'),
      headers: <String, String>{
        'Content-Type': 'application/json; charset=UTF-8',
      },
      body: jsonEncode(<String, dynamic>{
        "to": to,
      }),
    );

    print(response.statusCode);
    if (response.statusCode == 200) {
      Map<String, dynamic> result = jsonDecode(response.body);

      return result['transactionHash'];
    }

    throw Exception('failed to send NFT token');
  }

  static Future<SafeMoney> createSafeMoney(
      userId, safeName, tokenId, invitedUsers, image) async {
    final response = await http.post(
      Uri.http(endpoint, '/v1/safe'),
      headers: <String, String>{
        'Content-Type': 'application/json; charset=UTF-8',
      },
      body: jsonEncode(<String, dynamic>{
        'creator': userId,
        'name': safeName,
        'warrant': tokenId,
        'invitees': invitedUsers,
        'image': image,
      }),
    );

    if (response.statusCode == 200) {
      return SafeMoney.fromJson(jsonDecode(response.body));
    }

    throw Exception('failed to create safe money');
  }

  static Future<List> listSafeMoney(userId) async {
    final response = await http.get(Uri.http(endpoint, '/v1/safe/$userId'));
    print(response.body);
    List<SafeMoney> ret = [];
    if (response.statusCode == 200) {
      for (final map in jsonDecode(response.body)) {
        ret.add(SafeMoney.fromJson(map));
      }
    }
    return ret;
  }

  static Future approveTransaction(
      userId, transactionId, safeAddress, tokenId) async {
    final response = await http.post(
      Uri.http(endpoint, '/v1/safe/$safeAddress/$tokenId/sign'),
      headers: <String, String>{
        'Content-Type': 'application/json; charset=UTF-8',
      },
      body: jsonEncode(<String, dynamic>{
        'transactionId': transactionId,
        'userId': userId,
      }),
    );

    print(response.body);
  }

  static Future<String> sendSafeNftToken(safe, user, tokenId, to) async {
    final response = await http.post(
      Uri.http(endpoint, '/v1/safe/$safe/token/$tokenId'),
      headers: <String, String>{
        'Content-Type': 'application/json; charset=UTF-8',
      },
      body: jsonEncode(<String, dynamic>{
        "to": to,
        "from": user,
      }),
    );

    print(response.statusCode);
    if (response.statusCode == 200) {
      Map<String, dynamic> result = jsonDecode(response.body);

      return result['transactionId'];
    }

    throw Exception('failed to send NFT token');
  }
}

모델 구현

  • lib/models/klay_transfer.dart
import 'package:kastutorial/interfaces/sqlite_model.dart';
import 'package:kastutorial/store/sqlite.dart';

class KlayTransfer extends Sqlite<KlayTransfer> implements SqliteModel {
  @override
  int id;

  final int timestamp;
  final String eventType;
  final String target;
  final String value;

  KlayTransfer({this.eventType, this.target, this.value, this.timestamp})
      : super('klay_transfer') {
    super.doc = this;
  }

  static KlayTransfer fromJson(Map<String, dynamic> json) {
    return KlayTransfer(
      eventType: json['eventType'],
      target: json['target'],
      value: json['value'],
      timestamp: json['timestamp'],
    );
  }

  @override
  Map<String, dynamic> toMap() {
    Map<String, dynamic> ret = {
      'timestamp': timestamp,
      'eventType': eventType,
      'target': target,
      'value': value,
    };
    if (id != null) {
      ret['id'] = id;
    }

    return ret;
  }

  @override
  dynamic fromMap(Map<String, dynamic> map) {
    // TODO: implement fromMap
    KlayTransfer ret = fromJson(map);
    ret.id = map['id'];

    return ret;
  }
}

Licensed under CC BY-NC-SA 4.0
Sep 02, 2021 08:07 KST