728x90

Flutter에서 서버 데이터 가져오기를 Retrofit 라이브러리를 활용하는 법을 구현해보고 적어둔다.

JSON Serializable 에서 설명한 사항은 건너뛰고 나머지를 설명한다.

 

pubspec.yaml 파일에서 필요한 라이브러리

dependencies:
  dio: ^5.4.0  
  json_annotation: ^4.8.1
  retrofit: ^4.0.3
 
dev_dependencies:  
  json_serializable: ^6.7.1  
  build_runner: ^2.4.7
  retrofit_generator: ^8.0.6

 

Model 구현

import 'package:json_annotation/json_annotation.dart';
 
part 'contact_item.g.dart';
 
@JsonSerializable()
class ContactItem {
  final int idx;
  final String userNM;
  final String mobileNO;
  final String? telNO;
  final String? photo;
  final bool checkBoxState;
 
  const ContactItem({
    required this.idx,
    required this.userNM,
    required this.mobileNO,
    required this.telNO,
    required this.photo,
    required this.checkBoxState,
  });
 
  factory ContactItem.fromJson(Map<String, dynamic> json) =>
      _$ContactItemFromJson(json);
 
  Map<String, dynamic> toJson() => _$ContactItemToJson(this);
}
 

 

서버에서 제공하는 JSON 데이터를 추출하기 위한 모델 클래스

import 'package:json_annotation/json_annotation.dart';
import 'package:retrofit_ex2/model/contact_item.dart';
 
part 'contact_result.g.dart';
 
@JsonSerializable()
class ContactResult {
  final String status;
  final String message;
  final List<ContactItem>? addrinfo;
 
  const ContactResult({
    required this.status,
    required this.message,
    this.addrinfo,
  });
 
  factory ContactResult.fromJson(Map<String, dynamic> json) =>
      _$ContactResultFromJson(json);
 
  Map<String, dynamic> toJson() => _$ContactResultToJson(this);
}

 

2개의 클래스 구현 후에 dart run build_runner build 를 해주면 자동으로 g.dart 파일이 생성된다.

 

Retrofit 라이브러리 구현

클래스명은 RestClient 라고 했지만 ContactRepository 라로 해도 될 듯하다.

import 'package:dio/dio.dart';
import 'package:retrofit/retrofit.dart';
import 'package:retrofit_ex2/common/repository/retrofit_url.dart';
import 'package:retrofit_ex2/model/contact_item.dart';
import 'package:retrofit_ex2/model/contact_result.dart';
 
part 'rest_client.g.dart';
 
@RestApi(baseUrl: RetrofitURL.baseUrl)
abstract class RestClient {
  factory RestClient(Dio dio, {String baseUrl}) = _RestClient;
 
  @GET(RetrofitURL.contactData)
  Future<ContactResult> getContactList();
 
  @GET(RetrofitURL.allcontactData)
  Future<List<ContactItem>> getAllContactData();
 
  @POST(RetrofitURL.postContactData)
  @FormUrlEncoded()
  Future<ContactResult> postContactList(
    @Field() String keyword,
    @Field() String search,
  );
}
 
/***
 * 서버에는 파라미터가 다르게 되어 있지만
 * 위에 3개의 함수는 모두 같이 List<ContactItem> 을 반환할 수 있다.
 * https://pub.dev/packages/retrofit/example 를 참조하면서 함수를 테스트하면 좋을 듯 하다.
 *
 * 함수를 추가하거나 수정하면 반드시 dart run build_runner build 를 해줘야 한다.
 */
 

 

 

JSON 메시지를 출력하는 서버 코드 이름을 모아서 RetofitURL 파일로 구현했다.

class RetrofitURL {
  static const baseUrl = "https://www.abc.com";
  static const mLogin = "/androidSample/loginChk.php";
  static const contactData = "/androidSample/flutter/getContactList.php";
  static const allcontactData = "/androidSample/flutter/getAllContactList.php";
  static const photoPath = "/androidSample/photos/";
 
  static const postContactData = "/androidSample/flutter/postContactList.php";
}

 

파일이 PHP인 것은 중요하지 않다.

서버에서 출력하는 JSON 으로부터 모델 클래스를 구현할 줄 아는 것이 중요하다.

PHP를 사용하는 이유는 회사 프로젝트 개발시 반드시 구현해야 하는 Secure Coding까지 할 줄 아는 유일한 서버 언어다. 다른 언어인 Python, Node.js 는 약간 알기는 하지만, 회사 프로젝트 개발에는 아직 적용할 수 없다.

APP 구현 필수사항

- 서버와 앱간에 로그인시 패스워드는 반드시 RSA 암호화해서 데이터를 전송해야 한다.

- 해커가 알아서는 안되는 중요 정보는 AES256 암호화/복호화 처리를 한다.

- 서버 데이터를 가져오기 위해 토큰이나 세션처리를 해야 한다.

- 앱 루팅탐지 기능을 포함해야 한다.

- 앱 최종 배포시에는 난독화 적용을 해야 한다.

- 중요 데이터는 SQLite 같은 Local DB에 저장하지 못하도록 모의해킹 검증자가 지적 하더라.

서버에서는 SQL Injection 방지, XSS필터 구현, 중복 로그인 방지, RSA 복호화 처리, 해킹 시도 탐지 및 방어코드, 모든 로그인 시도내역 기록 등 구현할 사항이 많다.

GET방식의 코드부터 시작해서 POST로 Parameter 전달하여 key가 맞지 않으면 데이터를 반환하지 못하도록 처리하는 것 등을 포함하기 위해서 샘플을 3가지 구현해봤다.

 

 

여기까지 기본 정보를 구현하고 나서 실제 출력코드는 아래 예시를 살펴보자.

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:retrofit_ex2/common/component/contact_card.dart';
import 'package:retrofit_ex2/common/repository/crypto_api.dart';
import 'package:retrofit_ex2/model/contact_item.dart';
import 'package:retrofit_ex2/model/contact_result.dart';
import 'package:retrofit_ex2/repository/rest_client.dart';
 
class ContactPostResultPage extends StatefulWidget {
  const ContactPostResultPage({Key? key}) : super(key: key);
 
  @override
  State<ContactPostResultPage> createState() => _ContactListPageState();
}
 
class _ContactListPageState extends State<ContactPostResultPage> {
  late final RestClient restClient;
 
  @override
  void initState() {
    Dio dio = Dio();
    restClient = RestClient(dio);
    super.initState();
  }
 
  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
        body: FutureBuilder<ContactResult>(
          future: restClient.postContactList(
              Crypto.AES_encrypt(Crypto.URLkey()), ''),
          builder:
              (BuildContext context, AsyncSnapshot<ContactResult> snapshot) {
            if (!snapshot.hasData) {
              return const Center(
                child: CircularProgressIndicator(),
              );
            }
 
            final ids = snapshot.data as ContactResult;
 
            if (ids.status.contains("success")) {
              final addrinfo = ids.addrinfo as List<ContactItem>;
              return _ListViewSubPage(posts: addrinfo);
            } else {
              return Center(
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Text(
                      ids.status,
                      style: TextStyle(
                        color: Colors.red,
                        fontSize: 22.0,
                        fontWeight: FontWeight.w700,
                      ),
                    ),
                    Text(ids.message),
                  ],
                ),
              );
            }
          },
        ),
      ),
    );
  }
}
 
class _ListViewSubPage extends StatelessWidget {
  final List<ContactItem> posts;
 
  const _ListViewSubPage({
    super.key,
    required this.posts,
  });
 
  @override
  Widget build(BuildContext context) {
    return ListView.separated(
      itemCount: posts.length,
      itemBuilder: (context, index) {
        final item = posts[index];
        // 누르면 다른 페이지로 이동하도록 하려면 GestureDetector 위젯으로 감싸고,
        // onTap 에 이동할 사항을 구현한다.
        return ContactCard.fromJson(item);
      },
      separatorBuilder: (context, index) => SizedBox(height: 2.0),
    );
  }
}
 

RestClient 호출하는 부분과 POST 변수 2개를 넣어서 전달하는 코드가 포함되어 있다.

keyword 변수는 AES256 암호화하여 전달하고, search 값은 ''으로 넣어서 모든 데이터를 가져오도록 처리했다.

Retrofit 라이브러리를 이용하면 함수 세부내용을 구현하지 않고 간단하게 호출만 하면 결과를 반환해준다.

FutureBuilder 사용하면 서버 전송 결과를 snapshot.data 로 반환한다.

snapshot.data를 ContactResult 클래스로 캐스팅하고, 다시 List<ContactItem> 배열을 반환하도록 addrinfo 변수로 캐스팅했다.

ListView.seperated 로 반복 출력하도록 한다.

가독성을 높이기 위해 내부 클래스를 추가하여 데이터를 넘기고 처리하는 것을 확인할 수 있다.

 

 

위에 나오는 ContactCard.fromJson 은 Retofit 활용법과는 별개이므로 설명하지 않는다.

 

728x90
블로그 이미지

Link2Me

,
728x90

서버 샘플 소스는 Android 앱 샘플 개발시 사용했던 코드를 가지고 Android Retrofit 라이브러리를 사용할 때와 비교하면서 시도를 해보는 중이다.

factory 생성자를 수동으로 생성하고 내 마음대로 코드를 이렇게 하면 되겠다 싶어 처리했더니 죽어도 해결이 안되었다.

Flutter 에서 JSON 데이터 가져오는 것이 왜 이리 힘들어 ㅠㅠㅠ 하면서 맨붕에 빠졌다가 차분하게 유투브 동영상에 나온 예제를 그대로 따라하면서 살펴보고 문제가 뭔지 비교 분석을 했다.

 

직집 구현한 factory 생성자

class ContactResult {
  final String status;
  final String message;
  final List<Contact_Item> addrinfo;
 
  const ContactResult({
    required this.status,
    required this.message,
    required this.addrinfo,
  });
 
  factory ContactResult.fromJson(Map<String, dynamic> map) {
    return ContactResult(
      status: map['status'] as String,
      message: map['message'] as String,
      addrinfo: List<Contact_Item>.from(map['addrinfo']),
    );
  }
}

final List<Contact_Item>? addrinfo; 라고 해야 하는데 강제로 null 값을 제거하고 동작 결과를 확인하기 위해 시도했으나 해결되지 않았다.

에러메시지를 구글링 해보니 addrinfo: List<Contact_Item>.from(map['addrinfo']) 로 하면 해결될 것처럼 써있는 답변이 있어서 시도해봤다. 하지만 여전히 에러가 발생한다.

 

자동 생성한 factory 생성자

Json Serializable 을 이용하여 자동 생성한 factory 생성자 결과

ContactResult _$ContactResultFromJson(Map<String, dynamic> json) =>
    ContactResult(
      status: json['status'] as String,
      message: json['message'] as String,
      addrinfo: (json['addrinfo'] as List<dynamic>?)
          ?.map((e) => Contact_Item.fromJson(e as Map<String, dynamic>))
          .toList(),
    );
 

 

위 코드와 아래 자동 생성된 코드를 비교해보면 addrinfo 변수를 처리한 결과가 확연하게 다른 것을 확인할 수 있다.

 

정상적으로 처리되고 나서 Contact_Item 변수에 오류가 있다는 것도 알게 되었다.

정교하게 변수 처리를 해주지 않으면 에러를 뱉어내는 걸 확인했고, 화면 출력된 결과를 보면서 제대로 수정했다.

 

 

import 'package:json_annotation/json_annotation.dart';
 
part 'contact_item.g.dart';
 
@JsonSerializable()
class Contact_Item {
  final int idx; // String 으로 변수를 선언했는데, 출력 결과에서 int 라고 변경 요청
  final String userNM;
  final String mobileNO;
  final String? telNO;
  final String? photo; // 출력 결과에 null 값이 존재하는 걸 확인하고 변경
  final bool checkBoxState;
 
  const Contact_Item({
    required this.idx,
    required this.userNM,
    required this.mobileNO,
    this.telNO,
    this.photo,
    required this.checkBoxState,
  });
 
  factory Contact_Item.fromJson(Map<String, dynamic> json) =>
      _$Contact_ItemFromJson(json);
 
  Map<String, dynamic> toJson() => _$Contact_ItemToJson(this);
}

위와 같이 모델 class 를 선언하고 터미널 창에서 dart run build_runner build 를 하면 자동으로 contact_item.g.dart 파일이 생성된다. 코드를 수정시에는 항상 dart run build_runner build 를 해서 업데이트 해줘야 한다.

 

import 'package:json_annotation/json_annotation.dart';
import 'package:login_ex/contact/model/contact_item.dart';
 
part 'contact_result.g.dart';
 
@JsonSerializable()
class ContactResult {
  final String status;
  final String message;
  final List<Contact_Item>? addrinfo;
 
  const ContactResult({
    required this.status,
    required this.message,
    this.addrinfo,
  });
 
  factory ContactResult.fromJson(Map<String, dynamic> json) 
=> _$ContactResultFromJson(json);
 
  Map<String, dynamic> toJson() => _$ContactResultToJson(this);
}

 

 

 

json_serializable 는 https://pub.dev/packages/json_serializable/install 를 보면서 라이브러리를 설치해주면 된다.

https://pub.dev/packages/json_annotation/install 를 참조하여 설치한다.

https://pub.dev/packages/build_runner/install 를 참조하여 설치한다.

 

위 3개의 라이브러를 터미털 창에서 아래 코드를 한줄씩 실행하면 최신버전으로 설치된다.

기존에 설치된 라이브러리 버전이 낮다면 해당 줄을 삭제하고 다시 실행하면 최신버전으로 설치된다.

flutter pub add json_annotation
 
flutter pub add dev:json_serializable
 
flutter pub add dev:build_runner
 

 

 

그러면 실제 샘플로 구현한 코드를 살펴보자.

import 'package:dio/dio.dart';
import 'package:login_ex/common/repository/retrofit_url.dart';
import 'package:login_ex/contact/model/contact_request.dart';
import 'package:login_ex/contact/model/contact_result.dart';
import 'package:login_ex/common/repository/logging.dart';
 
abstract class ContactRepo {
  Future<ContactResult> getContactList(ContactRequest req);
}
 
class ContactService extends ContactRepo {
 
  Future<ContactResult> getContactList(ContactRequest req) async {
    BaseOptions options = BaseOptions(
      baseUrl: RetrofitURL.baseUrl,
    );
    Dio dio = Dio(options);
    dio.interceptors.add(LoggingInterceptor());
 
    FormData formData = FormData.fromMap({
      "keyword": req.keyword,
      "search": req.search,
    });
 
    final response = await dio.post(RetrofitURL.contactData, data: formData);
    print(response);
    // print(response.data.runtimeType);
    //print(response.headers);
    if (response.statusCode == 200) {
      // final Map<String, dynamic> body = jsonDecode(response.data);
      ContactResult result = ContactResult.fromJson(response.data);
      return result;
    } else {
      return ContactResult(status: "fail", message: "fail", addrinfo: []);
    }
  }
}
 

 

로그 화면에서 결과를 확인하기 위한 코드 예시이다.

class MainScreen extends StatefulWidget {
  const MainScreen({Key? key}) : super(key: key);
 
  @override
  State<MainScreen> createState() => _MainScreenState();
}
 
class _MainScreenState extends State<MainScreen> {
  final ContactRepo repo = ContactService();
 
  @override
  void initState() {
    super.initState();
    getContactData();
  }
 
  Future<void> getContactData() async {
 
    ContactRequest dataPost = ContactRequest(
      keyword: Crypto.AES_encrypt(Crypto.URLkey()),
      search: '',
    );
 
    ContactResult response = await repo.getContactList(dataPost);
 
    for(var item in response.addrinfo as List<Contact_Item>){
      print('${item.idx} | ${item.userNM} | ${item.mobileNO} | ${item.photo}');
 
    }
  }
 
  @override
  Widget build(BuildContext context) {
    return DefaultLayout(
      child: Center(
        child: Text('Main Page'),
      ),
    );
  }
}
 

 

keyword는 서버에서 key값을 비교하여 일치하는 경우에만 JSON 결과 데이터를 반환하도록 처리하기 위해서 추가한 것이다.

728x90
블로그 이미지

Link2Me

,
728x90

Flutter 에서 AES 256 으로 암호화하고 PHP에서 복호화하는 코드 클래스를 구현했다.

Java for Android 에서 활용하던 코드를 기반으로 Flutter에 맞게 변환 및 구현했다.

Flutter 와 PHP 간에 암호화 및 복호화 처리하는데 정상 동작됨을 확인했다.

 

AES_key는 32 byte 이고 IV는 16 byte 이다.

서로 다른 언어간에도 IV 값이 일치해야 한다.

import 'package:intl/intl.dart';
import 'package:encrypt/encrypt.dart' as enc;
 
// flutter pub add encrypt
// flutter pub add intl
// flutter pub get 하면 라이브러리 추가된다.
 
class CIpheR {
  static final String URLKEY = "jgsysyksr897213579";
  static final String AES_Key = "abcdefghijtuvwxyz1234klmnopqrs56";
 
  static String URLkey() {
    DateTime now = DateTime.now();
    DateFormat formatter = DateFormat('yyyyMMdd');
    String date = formatter.format(now);
    String keyword = '${URLKEY}${date}';
    return keyword;
  }
 
  static String AES_encrypt(String value) {
    final key = enc.Key.fromUtf8(AES_Key);
    final iv = enc.IV.allZerosOfLength(16);
 
    final encrypter = enc.Encrypter(enc.AES(key, mode: enc.AESMode.cbc));
    final encrypted = encrypter.encrypt(value, iv: iv);
    return encrypted.base64;
  }
 
  static String AES_decrypt(String encrypted){
    final key = enc.Key.fromUtf8(AES_Key);
    final iv = enc.IV.allZerosOfLength(16);
 
    final encrypter = enc.Encrypter(enc.AES(key, mode: enc.AESMode.cbc));
    enc.Encrypted enBase64 = enc.Encrypted.from64(encrypted);
    final decrypted = encrypter.decrypt(enBase64, iv: iv);
    return decrypted;
  }
}
 

 

PHP에서 str_repeat(chr(0), 16) 에 해당하는 Flutter IV 값을 찾아내느라고 애를 먹었다.

final iv = enc.IV.allZerosOfLength(16); // PHP ::: str_repeat(chr(0), 16)

 

PHP 암호화/복호화 클래스

 

<?php
class LoginClass extends DBController {
    // class 자식클래스 extends 부모클래스
    // override : 부모 클래스와 자식 클래스가 같은 메소드를 정의했을 경우 자식 클래스가 우선시된다.
 
    // ########################################################################
    // RSA 암호화 키
    function get_publickey() {
        // 경로 : 절대경로로 설정 필요
        $rsakeyfile = '/home/rsa/key/rsa_pub.pem';
        
        $fp = fopen($rsakeyfile,"r");
        $key = "";
        while(!feof($fp)) {
            $key .= fgets($fp,4096);
        } 
        fclose($fp);
 
        $key = preg_replace('/\r\n|\r|\n/','',$key);
        return $key;
    } 
 
    function get_privatekey() {
        // 경로 : 절대경로로 설정 필요
        $rsakeyfile = '/home/rsa/key/rsa_pri.pem';
        
        $fp = fopen($rsakeyfile,"r");
        $key = "";
        while(!feof($fp)) {
            $key .= fgets($fp,4096);
        } 
        fclose($fp);
 
        $key = preg_replace('/\r\n|\r|\n/','',$key);
        return $key;
    } 
 
    // RSA 복호화
    function rsa_decrypt($str){
        $private_key = "file:////home/rsa/key/rsa_pri.pem";
        $openssl_pk = openssl_get_privatekey($private_key);
        $_plaintext = base64_decode(explode(':::'$str)[0]);
        openssl_private_decrypt($_plaintext$return$openssl_pk);
        return $return;
    }
 
    // AES 암호화
    function AES_encrypt($plain_text){
        global $key;
        $encryptedMessage = openssl_encrypt($plain_text"aes-256-cbc"$keytrue,str_repeat(chr(0), 16));
        return base64_encode($encryptedMessage);
    }
 
    // AES 복호화
    function AES_decrypt($base64_text){
        global $key;
        $decryptedMessage = openssl_decrypt(base64_decode($base64_text), "aes-256-cbc"$keytrue, str_repeat(chr(0), 16));
        return $decryptedMessage;
    }
 
}
?>
 
<?php
class DBController {
    protected $db// 변수를 선언한 클래스와 상속받은 클래스에서 참조할 수 있다.
 
    // 생성자
    function __construct() {
        $this->db = $this->connectDB();
        // construct 메소드는 객체가 생성(인스턴스화)될 때 자동으로 실행되는 특수한 메소드다.
    }
 
    // 소멸자(destructor)
    function __destruct() {
        mysqli_close($this->connectDB());
    }
 
    private function connectDB() {
        require_once 'dbinfo.php';
        // MySQLi 객체지향 DB 연결
        $conn = new mysqli(DB_HOST, DB_USER, DB_PASSWORD, DB_DATABASE);
        $conn->set_charset("utf8");
        return $conn// return database handler
    }
 
    public function putDbArray($sql) {
        $stmt = $this->db->prepare($sql);
        $stmt->execute();
        $result = $stmt->get_result();
        return $result;
    }
 
    public function getDbArray($sql) {
        $stmt = $this->db->prepare($sql);
        $stmt->execute();
        $result = $stmt->get_result();
        return $result;
    }
 
}
?>
 
<?php
// DB 정보 입력
define("DB_HOST""localhost");
define("DB_USER""andsample");
define("DB_PASSWORD""sample!#%");
define("DB_DATABASE""android");
 
/* 사용자 권한 등록
use mysql;
create user andsample@localhost identified by 'sample!#%';
grant all privileges on android.* to andsample@localhost;
flush privileges;
*/
?>

 

 

728x90

'Flutter 앱 > Network' 카테고리의 다른 글

Flutter Login with PHP Session #2 (Retrofit)  (0) 2023.12.24
Flutter Login with PHP Session #1  (0) 2023.12.24
ListView.separated 예제  (0) 2023.11.20
Flutter Login Example  (0) 2022.07.25
Session vs JWT  (0) 2022.07.22
블로그 이미지

Link2Me

,
728x90

배열.fold()는 배열의 값들을 하나씩 커내 하나의 값으로 변환하는 함수이다.

- curr 는 elememt 이다.

- prev 는 이전값. 최초에는 prev에 0이 들어가고, 다음부터는 return 의 결과값을 prev 에 넣는다.

void main() {
  int totalPrice = [12345].fold(0, (prev, curr) {
    print("$prev / $curr");
    return prev + curr;
  });
  print(totalPrice);
}

 

728x90

'Flutter 앱 > Dart 언어' 카테고리의 다른 글

Dart List.where  (0) 2024.01.16
[Dart 고급] Dart asMap, entries  (0) 2024.01.16
Dart mixin  (0) 2023.12.11
getter & setter  (0) 2023.12.09
Dart 직렬화(Serialization) 및 역직렬화(Deserialization)  (0) 2023.12.08
블로그 이미지

Link2Me

,
728x90

가장 기본적인 상태관리를 하는 방법이다.

부모위젯

- StatefulWidget으로 생성한다.

- 모든 공유 상태를 가지고 있다.

- 상태 변경시 setState()로 자식 위젯들을 갱신한다.

 

자식 위젯

-  부모 위젯의 공유 상태를 전달받는다.

- 이벤트 발생시, 부모 위젯에게 전달하여 공유 상태를 변경한다.

 

import 'package:flutter/material.dart';
 
void main() {
  runApp(
    MaterialApp(
      debugShowCheckedModeBanner: false,
      home: MyApp(),
    ),
  );
}
 
class MyApp extends StatefulWidget {
  @override
  State<MyApp> createState() => _MyAppState();
}
 
class _MyAppState extends State<MyApp> {
  // 공유 상태
  int state = 0;
 
  // 공유 상태 업데이트
  void increaseState() {
    setState(() {
      state += 1;
    });
  }
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text("state : $state"),
            Child(
              state: state, // 부모로부터 공유 상태를 전달 받는다.
              onPressed: increaseState, // 이벤트를 부모에게 전달한다.
            ),
          ],
        ),
      ),
    );
  }
}
 
class Child extends StatelessWidget {
  const Child({
    required this.state,
    required this.onPressed,
  });
 
  final int state;
  final void Function() onPressed;
 
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: onPressed,
      child: Text("Child : $state"),
    );
  }
}
 

 

 

 

728x90
블로그 이미지

Link2Me

,
728x90

Flutter 에서는 named parameter 기반 생성자를 사용한다.

Android Studio 가 기본으로 제공하는 것에서는 name parameter 기반으로 생성되지 않기 때문에 불편하다.

이를 해결할 방법이다.

 

 

Plugins 탭을 누르고 MarketPlace를 선택한 다음에 dart data class 를 입력하면 검색 결과가 나온다.

 

여기까지 하면 준비는 된 것이다.

 

실제 data class 를 생성하고 나서 시도해 보자.

필요로 하는 변수를 선언하고 나서

  1. Windows/Linux: Alt + Enter
  2. MacOS: ⌘ + Enter

단축키를 누른다.

 

 

OK버튼을 누르면 아래와 같이 코드가 자동으로 추가된다.

 

 

 

Copy는 Copywith를 자동으로 생성해주고,

toMap() and fromMap()은 자동 생성한 후에 toJson() 과 fromJson 으로 용어 변경을 해주고 세부적인 사항은 수정해주면 된다.

 

 

 

 

728x90
블로그 이미지

Link2Me

,
728x90

family를 이용하면 Provider에 매개변수를 추가할 수 있다.

.family를 붙이고, 매개변수 타입을 명시한다.

 

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
 
final counterProvider = NotifierProvider.family<Counter, intint>(() {
  return Counter();
});
 
class Counter extends FamilyNotifier<intint> {
  @override
  int build(int initValue) => initValue;
 
  void increment() => state++;
}
 
 
void main() {
  runApp(
    const ProviderScope(
      child: MaterialApp(
        debugShowCheckedModeBanner: false,
        home: MyApp(),
      ),
    ),
  );
}
 
class MyApp extends ConsumerWidget {
  const MyApp({super.key});
 
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    int counter = ref.watch(counterProvider(10));
    return Scaffold(
      body: Center(
        child: Text(
          "$counter",
          style: const TextStyle(
            fontSize: 24,
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: ref.read(counterProvider(10).notifier).increment,
        child: const Icon(Icons.add),
      ),
    );
  }
}
 

 

counterProvider(10)과 같이 원하는 초기값을 전달할 수 있다.
ref.read(counterProvider(10))에서도 처음 전달한 매개 변수와 동일한 값을 전달해야 한다.

 

728x90

'Flutter 앱 > 상태관리' 카테고리의 다른 글

Flutter Riverpod 예제 : Bucket List  (0) 2023.12.27
Flutter StatefulWidget  (0) 2023.12.13
Flutter Riverpod - NotifierProvider  (0) 2023.12.11
Flutter riverpod - StateProvider  (0) 2023.11.23
Flutter GetX (반응형 상태관리)  (0) 2022.06.24
블로그 이미지

Link2Me

,
728x90

Notifier is a replacement of StateNotifier + ChangeNotifier.

 

아래 예제는 하나의 파일로 작성되었지만 별도의 파일로 Provider 를 추가하는 것을 권장한다.

- 클래스를 구현하고, Notifier 를 상속받고 제네릭 타입을 명시한다.

- 아래 예시는 숫자를 증가시키는 Provider 이므로 counterProvider 를 명명한다.

 

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
 
final counterProvider = NotifierProvider<Counter, int>(() {
  return Counter();
});
 
// Notifier를 상속 받은 클래스만 NotifierProvider에 등록 가능
 
class Counter extends Notifier<int> {
  @override
  int build() => 0// state 초기값
 
  void increment() => state++;
}
 
 
void main() {
  runApp(
    const ProviderScope(
      child: MaterialApp(
        debugShowCheckedModeBanner: false,
        home: MyApp(),
      ),
    ),
  );
}
 
class MyApp extends ConsumerWidget {
  const MyApp({super.key});
 
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    int counter = ref.watch(counterProvider);
    return Scaffold(
      body: Center(
        child: Text(
          "$counter",
          style: const TextStyle(
            fontSize: 24,
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: ref.read(counterProvider.notifier).increment,
        child: const Icon(Icons.add),
      ),
    );
  }
}
 

 

ref.watch는 provider를 입력값으로 받고, state를 리턴하는 함수다.

 

https://riverpod.dev/docs/providers/notifier_provider 를 참조하면 도움된다.

 

 

728x90

'Flutter 앱 > 상태관리' 카테고리의 다른 글

Flutter StatefulWidget  (0) 2023.12.13
Flutter Riverpod - NotifierProvider.family  (0) 2023.12.11
Flutter riverpod - StateProvider  (0) 2023.11.23
Flutter GetX (반응형 상태관리)  (0) 2022.06.24
Flutter GetX (단순 상태관리)  (0) 2022.06.23
블로그 이미지

Link2Me

,

Dart mixin

Flutter 앱/Dart 언어 2023. 12. 11. 09:23
728x90

Mixin은 " Mixins are a way of defining code that can be reused in multiple class hierarchies. (여러 클래스 계층에서 코드 정의한 부분을 재사용하기 위한 방법 중 하나이다.)" 공식문서에 표현되어 있다. 

https://dart.dev/language/mixins

Dart 언어에는 extends, implements이 주로 사용되며 extends는 클래스를 상속할 때, implements는 추상화 클래스를 만들 때 사용한다.

상속(extends)할 때에는 클래스의 메소드를 override(재정의)하지 않으나, implements를 한 경우에는 반드시 override(재정의)가 필요하다.

Dart에서는 extends를 통한 다중 상속이 지원되지 않는다. mixin은 다중 상속해야 하는 경우에 주로 사용된다.

 

mixin class Scanner {
  // mixin class 키워드는 Dart 3.0.0 버전 이상부터 사용할 수 있다.
  void scanning() => print("scanning...");
}
 
mixin class Printer {
  void printing() => print("printing...");
}
 
class Machine with Printer, Scanner {}
 
void main() {
  final machine = Machine();
  machine.printing();
  machine.scanning();
}
 
/***
 * class Scanner {}       // mixin 불가능 & 인스턴스 생성 가능
 * mixin Scanner {}       // mixin 가능 & 인스턴스 생성 불가능
 * mixin class Scanner {} // mixin 가능 & 인스턴스 생성 가능
 */

 

 

 

 

 

 

 

728x90

'Flutter 앱 > Dart 언어' 카테고리의 다른 글

[Dart 고급] Dart asMap, entries  (0) 2024.01.16
Dart fold 함수  (2) 2023.12.15
getter & setter  (0) 2023.12.09
Dart 직렬화(Serialization) 및 역직렬화(Deserialization)  (0) 2023.12.08
Dart 3.0 records  (0) 2023.10.19
블로그 이미지

Link2Me

,
728x90

get 키워드를 사용한 함수를 Getter 라고 부른다.

// 일반 형태
반환타입 get 이름 {
    return 반환값;
}
 
// 화살표 구문 사용
반환타입 get 이름 => 반환값;

 

 

set 키워드를 사용하는 함수를 Setter 라고 부른다.

set 이름(값) {
    // 할당시 수행하고 싶은 로직
}

 

 

 

728x90

'Flutter 앱 > Dart 언어' 카테고리의 다른 글

Dart fold 함수  (2) 2023.12.15
Dart mixin  (0) 2023.12.11
Dart 직렬화(Serialization) 및 역직렬화(Deserialization)  (0) 2023.12.08
Dart 3.0 records  (0) 2023.10.19
Dart Collection  (0) 2022.06.28
블로그 이미지

Link2Me

,
728x90

https://link2me.tistory.com/2346 게시글에서 Live Templete를 추가한 것을 이용하여 코드를 작성한다.

Android Studio 에서 파일명을 생성하고 나서 frf 를 입력하면 자동 완성 포멧이 만들어진다.

여기에서 Class Name을 입력하고, part 파일명을 소문자로 입력하면 된다.

import 'package:json_annotation/json_annotation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
 
part 'person.freezed.dart';
part 'person.g.dart';
 
@freezed
class Person with _$Person {
  factory Person({
    required String name,
    required int age,
  }) = _Person;
 
  factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);
}

 

위 코드를 추가한 후에

터미널창에서 dart run build_runner build 를 하면 자동으로 2개의 파일이 추가된다.

변경될 때마다 자동으로 파일이 추가/수정되도록 하고 싶다면 dart run build_runner watch 명령어를 입력한다.

 

상속이 포함된 Class 작성에는 어려움이 있다고 하니 참고

 

 

import 'dart:convert';
import 'package:dart_ex/model/person.dart';
 
void main() {
  Person a = Person(name: '철수', age: 10);
  Person b = a; // 얕은 복사
 
  // a.name = '영희'; // 불변 객체이므로 name 수정 불가
 
  // copyWith()
  a = a.copyWith(name: '영희'); // 깊은 복사
 
  // toString()
  print("toString() : $a");
  print("toString() : $b");
 
  // 값 비교(Value Equality)
  bool valueEquality = Person(name: "철수", age: 1== Person(name: "철수", age: 1);
  print("값 비교 : $valueEquality");
 
  // JSON 직렬화(Serialization)
  Map<String, dynamic> map = a.toJson();
  print("toJson() : $map");
  String jsonString = jsonEncode(map);
 
  // JSON 역직렬화(Deserialization)
  Map<String, dynamic> jsonMap = jsonDecode(jsonString);
  Person person = Person.fromJson(jsonMap);
  print("fromJson() : $person");
}

 

728x90
블로그 이미지

Link2Me

,
728x90

1. 터미널을 열고 다음 명령어를 실행하여 필요한 패키지들을 설치한다. dev_dependency 설치

    dart pub add -d freezed build_runner json_serializable

 

2. 터미널에서 다음 명령어를 실행한다. dependency 설치

    dart pub add freezed_annotation json_annotation

 

그러면 아래와 같이 pubspec.yaml 파일에 자동으로 추가될 것이다.

name: dart_ex
description: "A new Flutter project."
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
 
version: 1.0.0+1
 
environment:
  sdk: '>=3.2.2 <4.0.0'
 
dependencies:
  flutter:
    sdk: flutter
 
  cupertino_icons: ^1.0.2
  equatable: ^2.0.5
  freezed_annotation: ^2.4.1
  json_annotation: ^4.8.1
 
dev_dependencies:
  flutter_test:
    sdk: flutter
 
  flutter_lints: ^2.0.0
  freezed: ^2.4.5
  build_runner: ^2.4.7
  json_serializable: ^6.7.1
 
flutter:
 
  uses-material-design: true

 

 

이제 LiveTemplete 를 설정해보자.

Android Studio 에서 아래와 같은 번호 순서대로 하면 된다.

 

 

 

5번 이름이 나타나도록 지정해준다.

 

8번에 붙여넣을 코드이다.

import 'package:json_annotation/json_annotation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part '$NAME$.freezed.dart';
part '$NAME$.g.dart';

@freezed
class $CAP_NAME$ with _$$$CAP_NAME$ {
  factory $CAP_NAME$({
    $END$
  }) = _$CAP_NAME$;
  
  factory $CAP_NAME$.fromJson(Map<String, dynamic> json) => _$$$CAP_NAME$FromJson(json); 
}

 

 

아래와 같이 Change 로 변경해줘야 한다.

 

여기까지 하면 자동완성하는 코드가 완성된 것이다.

 

실제 사용하는 예제를 유투브에서 가져왔다.

https://www.youtube.com/watch?v=i5p6wXLAX7I

728x90

'Flutter 앱 > 환경설정' 카테고리의 다른 글

flutter db test 에러  (0) 2024.01.08
Flutter Dart Data Class 자동 생성  (0) 2023.12.13
Flutter Null Safety 확인 및 Migration  (0) 2022.06.25
IntelliJ IDEA Community 에 Dart 설치  (0) 2022.06.24
Flutter Upgrade  (0) 2022.06.17
블로그 이미지

Link2Me

,
728x90

 

JSON(JavaScript Object Notation)은 데이터를 표현하는 규칙이다.

  • 문자열에서 단따옴표('문자열')가 아닌 쌍따옴표("문자열")을 사용해야 한다.
  • 네트워크를 통해 다른 컴퓨터로 데이터 전송할 때 일련의 바이트(문자열)로 전달한다.

역직렬화(Deserialization) : 일련의 문자열 → Dart 클래스
직렬화(Serialization) : Dart 클래스 → 일련의 문자열
역직렬화(Deserialization) 진행 순서

  • JSON 포맷 String → Map<String, dynamic>
  • Map<String, dynamic> → Person 클래스

JSON 포맷 문자열이기 때문에 jsonDecode() 함수를 이용하면 Map<String, dynamic>으로 손쉽게 변경할 수 있다.

 

import 'dart:convert';
 
class Person {
  final String name;
  final int age;
 
  const Person({
    required this.name,
    required this.age,
  });
 
  // factory 키워드를 붙여 생성자 메소드로 만들 수 있으며, 다음 규칙을 지켜야 한다.
  // 클래스 인스턴스를 반환해야 한다.
  // 메소드명을 클래스명.메소드명() 형태로 작성해야 한다.
  factory Person.fromJson(Map<String, dynamic> json) {
    return Person(
      name: json['name'],
      age: json['age'],
    );
  }
 
  // 클래스의 인스턴스를 Map<String, dynamic>으로 변경하는 함수는 
// 일반적으로 클래스에 toJson() 메소드를 만들어 진행한다.
  Map<String, dynamic> toJson() {
    return {
      "name": name,
      "age": age,
    };
  }
}
 
void main() {
  // 네트워크 응답 문자열
  String jsonString = '{"name": "철수", "age": 10}';
 
  // JSON 포맷 String -> Map<String, dynamic>
  Map<String, dynamic> jsonMap = jsonDecode(jsonString);
  print(jsonMap);
 
  // Map<String, dynamic> -> Person
  Person person = Person.fromJson(jsonMap);
  print(person);
 
  // Person -> Map<String, dynamic>
  Map<String, dynamic> personMap = person.toJson();
  print(personMap);
 
  // jsonEncode() 함수를 이용하면 Map<String, dynamic>을 
// JSON 포맷 String으로 변경할 수 있다.
  // Map<String, dynamic> -> JSON 포맷 String
  String personString = jsonEncode(personMap);
  print(personString);
}
 

 

 

728x90

'Flutter 앱 > Dart 언어' 카테고리의 다른 글

Dart mixin  (0) 2023.12.11
getter & setter  (0) 2023.12.09
Dart 3.0 records  (0) 2023.10.19
Dart Collection  (0) 2022.06.28
Dart Class(클래스)  (0) 2022.06.27
블로그 이미지

Link2Me

,
728x90

9.1.1 버전에서는 정상 동작한다.

GoRouterState.of(context).location

 

10.0.0 버전부터는 아래와 같이 변경해줘야 한다.

GoRouterState.of(context).uri.toString()

 

 

GoRouterState.of(context).queryParameters

GoRouterState.of(context).uri.queryParameters

로 변경되었다.

728x90
블로그 이미지

Link2Me

,
728x90

Riverpod 라이브러리를 이용한 상태관리 방법에 대해 알아보고 적어둔다.

 

Provider는 상태를 저장하고, 자손 위젯에서 상태에 접근할 수 있도록 제공해주는 InheritedWidget를 래핑한 라이브러리이다. Provider 개발자가 단점을 보완하여 재작성된 라이브러리가 Riverpod 상태 관리 라이브러리이다.

 

1. 먼저 https://pub.dev/packages/riverpod 사이트에서 flutter_riverpod 의 최신버전을 확인하여 pubspec.yaml 에 추가한다.

 

name: riverpod_ex
description: A new Flutter project.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
 
version: 1.0.0+1
 
environment:
  sdk: '>=3.1.5 <4.0.0'
 
dependencies:
  flutter:
    sdk: flutter
 
  cupertino_icons: ^1.0.2
  flutter_riverpod: ^2.4.8
 
dev_dependencies:
  flutter_test:
    sdk: flutter
 
  flutter_lints: ^2.0.0
 
 
flutter:
 
  uses-material-design: true
 

 

 

2. main.dart 파일에서 아래와 같이

_MyApp을 ProviderScope 로 감싸준다.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_ex/screen/home_screen.dart';
 
void main() {
  runApp(ProviderScope(
    child: _MyApp(),
  ));
}
 
class _MyApp extends StatelessWidget {
  const _MyApp({Key? key}) : super(key: key);
 
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: HomeScreen(),
    );
  }
}

 

 

3. state_provider.dart

상태관리를 할 StateProvider를 구현한다.

import 'package:flutter_riverpod/flutter_riverpod.dart';
 
final numberProvider = StateProvider<int>((ref) => 0);

 

 

4.state_provider_screen.dart

ConsumerWidget 을 상속하고, Widget build(BuildContext context) 를 Widget build(BuildContext context, WidgetRef ref) 로 변경해준다.

Riverpod에서 정의된 추상클래스 WidgetRef를 통해서 Provider에 접근 가능하고,
ref.watch를 통해서 변경을 감지하면 해당 위젯을 다시 build한다.

 

final provider = ref.watch(numberProvider); 를 추가하여 상태 변화를 감지한다.

Provider의 값이 변경되면 자체적으로 다시 build된다.

 

WidgetRef의 read 메소드를 호출(Provider의 값을 읽어오기만 함)하며, 상태를 변경할 StateProvider의 notifier를 전달한다.

ref.read(numberProvider.notifier).update((state) => state + 1); 로 상태를 증가시키거나,

ref.read(numberProvider.notifier).state = ref.read(numberProvider.notifier).state - 1; 와 같은 방법으로 상태를 감소/증가 시킬 수 있다.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
 
import '../layout/default_layout.dart';
import '../riverpod/state_provider.dart';
 
class StateProviderScreen extends ConsumerWidget {
  const StateProviderScreen({Key? key}) : super(key: key);
 
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final provider = ref.watch(numberProvider);
 
    return DefaultLayout(
      title: 'Basic Provider',
      body: SizedBox(
        width: MediaQuery.of(context).size.width,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Text(
              provider.toString(),
              textAlign: TextAlign.center,
            ),
            ElevatedButton(
              onPressed: () {
                ref.read(numberProvider.notifier).update((state) => state + 1);
              },
              child: Text('UP'),
            ),
            ElevatedButton(
              onPressed: () {
                ref.read(numberProvider.notifier).state =
                    ref.read(numberProvider.notifier).state - 1;
              },
              child: Text(
                'DOWN',
              ),
            ),
            ElevatedButton(
              onPressed: () {
                Navigator.of(context).push(
                  MaterialPageRoute(builder: (_) => _NextScreen()),
                );
              },
              child: Text(
                'Next Screen',
              ),
            ),
          ],
        ),
      ),
    );
  }
}
 
class _NextScreen extends ConsumerWidget {
  const _NextScreen({Key? key}) : super(key: key);
 
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final provider = ref.watch(numberProvider);
 
    return DefaultLayout(
      title: 'Basic Provider',
      body: SizedBox(
        width: MediaQuery.of(context).size.width,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Text(
              provider.toString(),
              textAlign: TextAlign.center,
            ),
            ElevatedButton(
              onPressed: () {
                ref.read(numberProvider.notifier).update((state) => state + 1);
              },
              child: Text('UP'),
            ),
          ],
        ),
      ),
    );
  }
}

 

728x90
블로그 이미지

Link2Me

,
728x90

아래 코드는 factory 생성자를 일일이 타이핑을 해야 하는 번거로움이 있다.

import 'package:rest/restaurant/component/restaurant_card.dart';
 
import '../../common/const/data.dart';
 
enum RestaurantPriceRange {
  expensive,
  medium,
  cheap,
}
 
class RestaurantModel {
  final String id;
  final String name;
  final String thumbUrl;
  final List<String> tags;
  final RestaurantPriceRange priceRange;
  final double ratings;
  final int ratingsCount;
  final int deliveryTime;
  final int deliveryFee;
 
  RestaurantModel({
    required this.id,
    required this.name,
    required this.thumbUrl,
    required this.tags,
    required this.priceRange,
    required this.ratings,
    required this.ratingsCount,
    required this.deliveryTime,
    required this.deliveryFee,
  });
 
  factory RestaurantModel.fromJson({
    required Map<String, dynamic> json,
  }) {
    return RestaurantModel(
      id: json['id'],
      name: json['name'],
      thumbUrl: 'http://$realIp${json['thumbUrl']}',
      tags: List<String>.from(json['tags']),
      priceRange: RestaurantPriceRange.values.firstWhere(
        (e) => e.name == json['priceRange'],
      ),
      ratings: json['ratings'],
      ratingsCount: json['ratingsCount'],
      deliveryTime: json['deliveryTime'],
      deliveryFee: json['deliveryFee'],
    );
  }
 
}

 

 

위 코드에서 factory 생성자 코드 부분을 자동 생성하는 방법으로 JSON Serialize 를 이용한다.

1. https://pub.dev/packages/json_serializable 에서 최신버전을 확인한다.

Readme 탭에서 Setup 부분의 example 을 누르면 pubspec.yaml 에 추가할 버전이 나온다.

아래의 라이브러리를 추가하고 flutter pub get 명령을 실행한다.

dependencies:
  json_annotation: ^4.8.0
 
dev_dependencies:
  build_runner: ^2.3.3
  json_serializable: ^6.7.1

 

 

Class 위에 @JsonSerializable() 을 추가하고, part 'restaurant_model.g.dart'; 코드를 추가한다.

그리고 나서 터미널 창을 열면 프로젝트 root 폴더가 된다.

root에서 dart run build_runner build 를 입력하고 실행한다.

매번 입력하기 귀찮다면 dart run build_runner watch 명령어를 실행하면 된다.

import 'package:json_annotation/json_annotation.dart';
import 'package:rest/common/utils/data_utils.dart';
 
part 'restaurant_model.g.dart';
 
/**
* 수정 사항이 생기면 터미널에서 dart run build_runner build 를 다시 실행한다.
 * 그러면 자동으로 g.dart 파일을 업데이트한다.
 */
 
enum RestaurantPriceRange {
  expensive,
  medium,
  cheap,
}
 
@JsonSerializable()
class RestaurantModel {
  final String id;
  final String name;
  @JsonKey(
    fromJson: DataUtils.pathToUrl,
  )
  final String thumbUrl;
  final List<String> tags;
  final RestaurantPriceRange priceRange;
  final double ratings;
  final int ratingsCount;
  final int deliveryTime;
  final int deliveryFee;
 
  RestaurantModel({
    required this.id,
    required this.name,
    required this.thumbUrl,
    required this.tags,
    required this.priceRange,
    required this.ratings,
    required this.ratingsCount,
    required this.deliveryTime,
    required this.deliveryFee,
  });
 
  factory RestaurantModel.fromJson(Map<String, dynamic> json)
  => _$RestaurantModelFromJson(json);
 
  Map<String, dynamic> toJson() => _$RestaurantModelToJson(this);
 
 
  // factory RestaurantModel.fromJson({
  //   required Map<String, dynamic> json,
  // }) {
  //   return RestaurantModel(
  //     id: json['id'],
  //     name: json['name'],
  //     thumbUrl: 'http://$realIp${json['thumbUrl']}',
  //     tags: List<String>.from(json['tags']),
  //     priceRange: RestaurantPriceRange.values.firstWhere(
  //       (e) => e.name == json['priceRange'],
  //     ),
  //     ratings: json['ratings'],
  //     ratingsCount: json['ratingsCount'],
  //     deliveryTime: json['deliveryTime'],
  //     deliveryFee: json['deliveryFee'],
  //   );
  // }
 
}

 

모델 클래스 내부에 fromJson과 toJson을 정의해 두면 해당 메서드를 호출할 때마다 오타를 걱정할 필요가 없어진다.

 

 

칼럼에서 별도 변경이 필요하면

  @JsonKey(
    fromJson: DataUtils.pathToUrl,
  )

와 같이 추가하고 나서 dart run build_runner build 를 다시 실행한다.

그러면 restaurant_model.g.dart 파일이 업데이트되어 자동 생성된다.

 

// GENERATED CODE - DO NOT MODIFY BY HAND
 
part of 'restaurant_model.dart';
 
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
 
RestaurantModel _$RestaurantModelFromJson(Map<String, dynamic> json) =>
    RestaurantModel(
      id: json['id'as String,
      name: json['name'as String,
      thumbUrl: DataUtils.pathToUrl(json['thumbUrl'as String),
      tags: (json['tags'as List<dynamic>).map((e) => e as String).toList(),
      priceRange:
          $enumDecode(_$RestaurantPriceRangeEnumMap, json['priceRange']),
      ratings: (json['ratings'as num).toDouble(),
      ratingsCount: json['ratingsCount'as int,
      deliveryTime: json['deliveryTime'as int,
      deliveryFee: json['deliveryFee'as int,
    );
 
Map<String, dynamic> _$RestaurantModelToJson(RestaurantModel instance) =>
    <String, dynamic>{
      'id': instance.id,
      'name': instance.name,
      'thumbUrl': instance.thumbUrl,
      'tags': instance.tags,
      'priceRange': _$RestaurantPriceRangeEnumMap[instance.priceRange]!,
      'ratings': instance.ratings,
      'ratingsCount': instance.ratingsCount,
      'deliveryTime': instance.deliveryTime,
      'deliveryFee': instance.deliveryFee,
    };
 
const _$RestaurantPriceRangeEnumMap = {
  RestaurantPriceRange.expensive: 'expensive',
  RestaurantPriceRange.medium: 'medium',
  RestaurantPriceRange.cheap: 'cheap',
};
 

 

 

728x90
블로그 이미지

Link2Me

,
728x90

FutureBuilder 와 Dio 라이브러리를 이용하여 서버에 있는 데이터를 가져와서 화면에 보여주는 ListView 예제이다.

 

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:rest/restaurant/component/restaurant_card.dart';
 
import '../../common/const/data.dart';
 
class RestaurantScreen extends StatelessWidget {
  const RestaurantScreen({Key? key}) : super(key: key);
 
  Future<List> pagenateRestaurant() async {
    final dio = Dio();
    final accessToken = await storage.read(key: ACCESS_TOKEN_KEY);
    //print('accessToken read ::: ${accessToken}');
 
    final resp = await dio.get(
      'http://$realIp/restaurant',
      options: Options(headers: {
        'authorization''Bearer $accessToken',
      }),
    );
 
    return resp.data['data'];
  }
 
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16.0),
          child: FutureBuilder<List>(
            future: pagenateRestaurant(),
            builder: (context, AsyncSnapshot<List> snapshot) {
              //print(snapshot.error);
              //print(snapshot.data);
              if (!snapshot.hasData) {
                return Container();
              }
 
              return ListView.separated(
                itemCount: snapshot.data!.length,
                itemBuilder: (_, index) {
                  final item = snapshot.data![index];
                  return RestaurantCard(
                    image: Image.network(
                      'http://$realIp${item['thumbUrl']}',
                      fit: BoxFit.cover,
                    ),
                    name: item['name'],
                    tags: List<String>.from(item['tags']),
                    ratingsCount: item['ratingsCount'],
                    deliveryTime: item['deliveryTime'],
                    deliveryFee: item['deliveryFee'],
                    ratings: item['ratings'],
                  );
                },
                separatorBuilder: (_, index) {
                  return SizedBox(height: 16.0);
                },
              );
            },
          ),
        ),
      ),
    );
  }
}
 

 

 

위 코드를 아래와 같이 Model 화하여 수정한 코드이다.

 
enum RestaurantPriceRange {
  expensive,
  medium,
  cheap,
}
 
class RestaurantModel {
  final String id;
  final String name;
  final String thumbUrl;
  final List<String> tags;
  final RestaurantPriceRange priceRange;
  final double ratings;
  final int ratingsCount;
  final int deliveryTime;
  final int deliveryFee;
 
  RestaurantModel({
    required this.id,
    required this.name,
    required this.thumbUrl,
    required this.tags,
    required this.priceRange,
    required this.ratings,
    required this.ratingsCount,
    required this.deliveryTime,
    required this.deliveryFee,
  });
}
 

 

 

 
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:rest/restaurant/component/restaurant_card.dart';
import 'package:rest/restaurant/model/restaurant_model.dart';
 
import '../../common/const/data.dart';
 
class RestaurantScreen extends StatelessWidget {
  const RestaurantScreen({Key? key}) : super(key: key);
 
  Future<List> pagenateRestaurant() async {
    final dio = Dio();
    final accessToken = await storage.read(key: ACCESS_TOKEN_KEY);
    //print('accessToken read ::: ${accessToken}');
 
    final resp = await dio.get(
      'http://$realIp/restaurant',
      options: Options(headers: {
        'authorization''Bearer $accessToken',
      }),
    );
 
    return resp.data['data'];
  }
 
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16.0),
          child: FutureBuilder<List>(
            future: pagenateRestaurant(),
            builder: (context, AsyncSnapshot<List> snapshot) {
              //print(snapshot.error);
              //print(snapshot.data);
              if (!snapshot.hasData) {
                return Container();
              }
 
              return ListView.separated(
                itemCount: snapshot.data!.length,
                itemBuilder: (_, index) {
                  final item = snapshot.data![index];
                  final pItem = RestaurantModel(
                    id: item['id'],
                    name: item['name'],
                    thumbUrl: 'http://$realIp${item['thumbUrl']}',
                    tags: List<String>.from(item['tags']),
                    priceRange: RestaurantPriceRange.values.firstWhere(
                      (e) => e.name == item['priceRange'],
                    ),
                    ratings: item['ratings'],
                    ratingsCount: item['ratingsCount'],
                    deliveryTime: item['deliveryTime'],
                    deliveryFee: item['deliveryFee'],
                  );
 
                  return RestaurantCard(
                    image: Image.network(
                      pItem.thumbUrl,
                      fit: BoxFit.cover,
                    ),
                    name: pItem.name,
                    tags: pItem.tags,
                    ratingsCount: pItem.ratingsCount,
                    deliveryTime: pItem.deliveryTime,
                    deliveryFee: pItem.deliveryFee,
                    ratings: pItem.ratings,
                  );
                },
                separatorBuilder: (_, index) {
                  return SizedBox(height: 16.0);
                },
              );
            },
          ),
        ),
      ),
    );
  }
}
 

 

 

728x90

'Flutter 앱 > Network' 카테고리의 다른 글

Flutter Login with PHP Session #1  (0) 2023.12.24
Flutter AES256 with PHP  (0) 2023.12.19
Flutter Login Example  (0) 2022.07.25
Session vs JWT  (0) 2022.07.22
Flutter Login 로직 구현 예제 (오류 포함)  (0) 2022.07.22
블로그 이미지

Link2Me

,
728x90

미세먼지 공공데이터 신청 방법이다.

 

먼저 https://www.data.go.kr/index.do 에 접속한다.

회원가입이 안되어 있다면 회원가입부터 해야 한다. 회원가입은 네이버 로그인 정보 연동으로 가입했다.

 

미세먼지를 검색어에 입력하고 나서 오픈 API 탭을 누른다.

 

 

 

4번 한국환경공단_에어코리아_대기오염정보통계 현황을 누르면

인증키 정보가 나온다. postman 에서 사용할 인증키는 Encoding 인증키를 사용하면 된다.

인증키를 복사하고 나서 5번 상세정보를 클릭한다.

 

 

 

 

다음으로는 postman 을 구글 검색으로 다운로드 받아서 회원가입(구글로그인 연동)하고 나서 팝업되는 창에 입력을 한다.

운영체제에 맞는 버전을 다운로드 하여 설치한다.

 

위 그림에 나온 요청주소와 요청변수를 입력하고 Send 키를 누른다.

 

결과는 아래와 같이 나온다.

 

위 그림에서 나온 정보를 복사하여 플러터(Flutter) 코드에서 가공하여 수정하여 활용하면 된다.

items 에 나열된 시도 명칭은 행정순서와는 무관하게 나와 있으니 순서를 맞추기보다는 그냥 활용하는 편이 나을 듯 싶다.

728x90
블로그 이미지

Link2Me

,
728x90

Flutter 에서 로컬 데이터베이스 패키지 중 하나인 Drift 에 대해 알아보자.
Drift 는 Sqlflite 와 다르게 ORM 방식의 데이터베이스이다.
ORM 은 Object Relational Mapping (객체-관계 매핑), 쉽게 말해서 객체와 관계형 데이터베이스의 데이터를 연결해주는 것이다.

Drift로 SQLite DB 생성부터 관리
- 테이블 정의하기
- DB 생성하기
- DB 관리파일 생성(g.dart)
- CRUD 코드 추가하기

 

먼저 pubspec.yaml 파일에 추가할 내용은 https://drift.simonbinder.eu/docs/getting-started/ 에 나와 있다.

 

name: drift_ex
description: A new Flutter project.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
 
version: 1.0.0+1
 
environment:
  sdk: '>=3.1.5 <4.0.0'
 
dependencies:
  flutter:
    sdk: flutter
 
  cupertino_icons: ^1.0.2
  drift: ^2.13.0
  sqlite3_flutter_libs: ^0.5.0
  path_provider: ^2.0.0
  path: ^1.8.3
 
dev_dependencies:
  flutter_test:
    sdk: flutter
 
  flutter_lints: ^2.0.0
  drift_dev: ^2.13.0
  build_runner: ^2.4.6
 
flutter:
 
  uses-material-design: true
 
  #     fonts:
  #       - asset: fonts/TrajanPro.ttf
  #       - asset: fonts/TrajanPro_Bold.ttf
  #         weight: 700
  #

 

 

테이블 정의하기

import 'package:drift/drift.dart';
 
/***
 * CONTENT, DATE, STARTTIME, ENDTIME, COLORID, CREATEDAT
 */
class Schedules extends Table {
  IntColumn get id => integer().autoIncrement()(); // PRIMARY KEY
 
  TextColumn get content => text()(); // 내용
 
  DateTimeColumn get date => dateTime()(); // 일정날짜
 
  IntColumn get startTime => integer()(); // 시작 시간
  IntColumn get endTime => integer()();  // 종료 시간
 
  IntColumn get colorId => integer()(); // Category Color Table ID
 
  DateTimeColumn get createAt => dateTime().clientDefault(() => DateTime.now(),)();
}
 

 

import 'package:drift/drift.dart';
 
class CategoryColors extends Table {
  // PRIMARY KEY
  IntColumn get id => integer().autoIncrement()();
 
  // 색상 코드
  TextColumn get hexCode => text()();
}

 

 

DB 생성하기

schemaVersion은 일반적으로 1부터 시작하고 Table의 변화가 있을때 1씩 올려준다.

import 'dart:io';
 
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
 
import '../model/category_color.dart';
import '../model/schedule.dart';
 
part 'schedule_database.g.dart';
 
@DriftDatabase(
  tables: [
    Schedules,
    CategoryColors,
  ],
)
 
class LocalDatabase extends _$LocalDatabase {
  LocalDatabase() : super(_openConnection());
 
  @override
  int get schemaVersion => 1;
}
 
LazyDatabase _openConnection() {
  // LazyDatabase에서는 db파일이 저장될 폴더 위치를 지정해준다.
  return LazyDatabase(() async {
    final dbFolder = await getApplicationDocumentsDirectory();
    final file = File(p.join(dbFolder.path, 'db.sqlite'));
    return NativeDatabase(file);
  });
}
 

 

 

g.dart 코드 자동 생성하기

아래와 같이 터미널에서 flutter pub run build_runner build  명령어를 입력한다.

 

 

 

CRUD 코드 추가하기

class LocalDatabase extends _$LocalDatabase {
  LocalDatabase() : super(_openConnection());
 
  Future<int> createSchedule(SchedulesCompanion data) =>
      into(schedules).insert(data);
 
  Future<int> createCategoryColor(CategoryColorsCompanion data) =>
      into(categoryColors).insert(data);
 
  Future<List<CategoryColor>> getCategoryColors() =>
      select(categoryColors).get();
 
  // getSingle : 하나의 데이터만 가져와라.
  Future<Schedule> getScheduleById(int id) =>
      (select(schedules)..where((tbl) => tbl.id.equals(id))).getSingle();
 
  Future<int> updateScheduleById(int id, SchedulesCompanion data) =>
      (update(schedules)..where((tbl) => tbl.id.equals(id))).write(data);
 
  // 삭제한 ID의 int 값을 리턴 받는다.
  Future<int> removeSchedule(int id) =>
      (delete(schedules)..where((tbl) => tbl.id.equals(id))).go();
 
  Stream<List<ScheduleWithColor>> watchSchedules(DateTime date) {
    final query = select(schedules).join([
      innerJoin(categoryColors, categoryColors.id.equalsExp(schedules.colorId))
    ]);
 
    query.where(schedules.date.equals(date));
    query.orderBy(
      [
        // asc -> ascending 오름차순
        // desc -> descending 내림차순
        OrderingTerm.asc(schedules.startTime),
      ],
    );
 
    return query.watch().map(
          (rows) => rows
              .map(
                (row) => ScheduleWithColor(
                  schedule: row.readTable(schedules),
                  categoryColor: row.readTable(categoryColors),
                ),
              )
              .toList(),
        );
  }
 
  @override
  int get schemaVersion => 1;
}
 

 

 

728x90
블로그 이미지

Link2Me

,
728x90

https://dart.dev/language/records 에 공식적인 설명이 되어 있다.

 

Records는 익명성, 불변성, 집계성을 가진 타입이며
다른 컬렉션 유형(List, Set, Map 등등)과 마찬가지로 여러 개체를 하나의 개체로 묶을 수 있다.
하지만 다른 컬렉션 유형과 달리 Record는 크기와 유형이 고정되어 있다.

void main() {
  final result = person({
    'name''홍길동'
    'age'25
  });
  
  print(result); // (홍길동, 25)
  
  print(result.$1); // 홍길동
  
  print(result.$2); // 25
  
  
 
  var record = ('first', a: 2, b: true'last');
 
  print(record.$1); // Prints 'first'
  print(record.a); // Prints 2
  print(record.b); // Prints true
  print(record.$2); // Prints 'last'
 
 
}
 
// 튜플로 반환하여 Type과 순서를 보장받을 수 있다.
// 튜플은 소괄호를 사용한다.
(String, int) person(Map<String, dynamic> json) {
  return (json['name'as String, json['age'as int);
}
 

 

 

 
// Returns multiple values in a record:
(String, int) userInfo(Map<String, dynamic> json) {
  return (json['name'as String, json['age'as int);
}
 
final json = <String, dynamic>{
  'name''Dash',
  'age'10,
  'color''blue',
};
 
// Destructures using a record pattern:
var (name, age) = userInfo(json);
 
/* Equivalent to:
  var info = userInfo(json);
  var name = info.$1;
  var age  = info.$2;
*/

 

 

void main() {
  final result = getPersonWithType();  
 
  for(final item in result){
    print(item);
  }
  
  print('-----------------------');
  
  for(final item in result){
    print(item.$1);
    print(item.$2);
  }
 
}
 
List<(String name, int age)> getPersonWithType() {
  return [
    ('홍길동'25),
    ('강감찬'37),
    ('이순신'33)
  ];
}

 

 

void main() {
  final result = getPersonWithNameType(); 
 
  for(final item in result){
    print(item);
  }
  
  print('-----------------------');
  
  for(final item in result){
    print(item.name);
    print(item.age);
  }
 
}
 
List<({String name, int age})> getPersonWithNameType() {
  return [
    ( name: '홍길동', age: 25),
    ( name: '강감찬', age: 37),
    ( name: '이순신', age: 33)
  ];
}

 

 

728x90

'Flutter 앱 > Dart 언어' 카테고리의 다른 글

getter & setter  (0) 2023.12.09
Dart 직렬화(Serialization) 및 역직렬화(Deserialization)  (0) 2023.12.08
Dart Collection  (0) 2022.06.28
Dart Class(클래스)  (0) 2022.06.27
Dart Asynchronous programming(비동기 프로그래밍)  (0) 2022.06.23
블로그 이미지

Link2Me

,