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

Setup an Environment
Flutter CLI 설치
- flutter 컴파일을 위한 프로그램 설치
yay -S flutter
brew install flutter
Android 환경 설치
- Downloading AdoptOpenJDK version 8
brew install adopt-openjdk
yay -S jdk-openjdk
- Downloading Android SDK
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;
}
}