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

로 변경되었다.

블로그 이미지

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'),
            ),
          ],
        ),
      ),
    );
  }
}

 

블로그 이미지

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',
};
 

 

 

블로그 이미지

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);
                },
              );
            },
          ),
        ),
      ),
    );
  }
}
 

 

 

'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 에 나열된 시도 명칭은 행정순서와는 무관하게 나와 있으니 순서를 맞추기보다는 그냥 활용하는 편이 나을 듯 싶다.

블로그 이미지

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;
}
 

 

 

블로그 이미지

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)
  ];
}

 

 

'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

,
728x90

Flutter 에서 WebView를 사용하기 위한 설정이다.

 

Android 설정

android/app/src/main/AndroidManifest.xml 파일에 추가할 사항

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.link2me.flutter.webview">
 
    <uses-permission android:name="android.permission.INTERNET"/>
    
   <application
        android:label="webview"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher"
        android:usesCleartextTraffic="true">

 

IOS 설정

ios/Runner/info.plist 파일 하단에 추가할 사항

    <key>CADisableMinimumFrameDurationOnPhone</key>
    <true/>
    <key>NSAppTransportSecurity</key>
    <dict>
        <key>NSAllowsLocalNetworking</key>
        <true/>
        <key>NSAllowsArbitraryLoadsInWebContent</key>
        <true/>
    </dict>
</dict>
</plist>

 

pubspec.yaml

name: webview
description: A new Flutter project.
 
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
 
version: 1.0.0+1
 
environment:
  sdk: ">=2.17.6 <3.0.0"
 
dependencies:
  flutter:
    sdk: flutter
 
  cupertino_icons: ^1.0.2
  webview_flutter: ^3.0.0
 
dev_dependencies:
  flutter_test:
    sdk: flutter
 
  flutter_lints: ^2.0.0
 
flutter:
 
  uses-material-design: true
 
  # For details regarding fonts from package dependencies,
  # see https://flutter.dev/custom-fonts/#from-packages

 

webview 소스코드

import 'package:flutter/material.dart';
import 'package:webview/screen/home_screen.dart';
 
void main() {
  runApp(
    MaterialApp(
      debugShowCheckedModeBanner: false,
      home: HomeScreen(),
    )
  );
}

 

lib/screen/home_screen.dart

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
 
class HomeScreen extends StatelessWidget {
  WebViewController? controller;
  final homeUrl = 'https://m.naver.com/';
 
  HomeScreen({Key? key}) : super(key: key);
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('모바일 네이버'),
        centerTitle: true,
        actions: [
          IconButton(
              onPressed: () {
                if(controller != null){
                  controller!.loadUrl(homeUrl);
                }
              },
              icon: Icon(
                Icons.home,
              ))
        ],
      ),
      body: WebView(
        onWebViewCreated: (WebViewController controller) {
          this.controller = controller;
        },
        initialUrl: homeUrl,
        javascriptMode: JavascriptMode.unrestricted,
      ),
    );
  }
}

 

flutter webView 4.0 버전

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  webview_flutter: ^4.4.4

 

main.dart

import 'package:flutter/material.dart';
import 'package:webview/screen/home_screen.dart';
 
void main() {
  // Flutter 프레임워크가 앱을 실행할 준비가 될 때까지 기다린다.
  WidgetsFlutterBinding.ensureInitialized();
 
  runApp(
    MaterialApp(
      debugShowCheckedModeBanner: false,
      home: HomeScreen(),
    )
  );
}

 

lib/screen/home_detail_view.dart

 
import 'package:flutter/material.dart';
import 'dart:core';
import 'package:webview_flutter/webview_flutter.dart';
 
class HomeDetailView extends StatelessWidget {
  final String Url_path;
 
  HomeDetailView({
    Key? key,
    required this.Url_path,
  }) : super(key: key);
 
  WebViewController controller = WebViewController()
    ..setJavaScriptMode(JavaScriptMode.unrestricted)
    ..setBackgroundColor(Colors.blueGrey.shade100)
    ..setNavigationDelegate(
      NavigationDelegate(
        onProgress: (progress) {},
        onPageStarted: (url) {},
        onPageFinished: (url) {},
        onWebResourceError: (error) {},
        onNavigationRequest: (request) {
          if (request.url.startsWith('url')) {
            return NavigationDecision.prevent;
          } else {
            return NavigationDecision.navigate;
          }
        },
      ),
    );
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: WebViewWidget(
        controller: controller..loadRequest(Uri.parse(Url_path)),
      ),
    );
  }
}
 

 

- last Update : 2024.1.24일

 

 

GitHub 에 webview 4.0 코드 필요한 것만 발췌하여 올렸다.

https://github.com/jsk005/Flutter/tree/main/webview

 

GitHub - jsk005/Flutter: Flutter Study

Flutter Study. Contribute to jsk005/Flutter development by creating an account on GitHub.

github.com

 

 

 

 

블로그 이미지

Link2Me

,
728x90

앱 내에 key-value 형태로 데이터를 저장하려면 shared preferences 라이브러리를 이용한다.

자동 로그인 처리 쉽게 하려고 패스워드 정보 저장하면 안된다.

 

1. 의존성 추가

pubspec.yaml 파일에 shared_preferences 라이브러리를 추가한다.

터미널 창에서 flutter pub add shared_preferences 한 다음에 flutter pub get 을 하면 자동으로 최신 버전이 추가된다.

https://pub.dev/packages/shared_preferences/install 

 

2. 데이터 저장하기

void _saveCookie(String newCookie) async {
  if (_cookie != newCookie) {
    _cookie = newCookie;
    SharedPreferences prefs = await SharedPreferences.getInstance();
    prefs.setString('cookie', _cookie!);
  }
}

Shared preferences에는 int, double, bool, string, 그리고 List<String> 데이터를 저장할 수 있다.

 

3. 데이터 읽기

Future<void> initCookie() async {
  // shared preferences 얻기
  SharedPreferences prefs = await SharedPreferences.getInstance();
  _cookie = prefs.getString('cookie');
}
 

 

 

 

4. 데이터 삭제하기

void _clearCookie() async {
  _cookie = null;
  SharedPreferences prefs = await SharedPreferences.getInstance();
  prefs.remove('cookie');
}

모든 데이터 삭제는 prefs.clear(); 를 하면 된다.

 

 

블로그 이미지

Link2Me

,
728x90

구글 검색하면 kakao Login 또는 Firebase 기반 로그인 기능 구현에 대한 예제들이 검색된다.

이 예제는 기업/클라우드 서버와의 통신을 전제로 한 로그인 구현 기본 사항이다.

아직 더 해결해야 할 사항들로는 RSA 암호화 통신, 세션기반 처리 기능 구현이다.

더 고민하고 테스트해서 해결되면 다른 게시글에 추가 작성할 예정이다.

 

Flutter 에서 로그인 처리를 이해하는데 한걸음 더 나아간 예제라고 보면 된다.

로그인에 대한 이해를 하기 위해서는 서버에서 응답하는 메시지의 형태가 어떤지 알아야 한다.

 

1. 서버의 PHP 코드 예제

- 로그인 핵심 코드는 loginClass 에 정의되어 있다.

- 여기서는 JSON encode 처리 메시지에 대한 이해 관점으로 전체적인 흐름을 이해하면 된다.

더보기

 

<?php
// 파일을 직접 실행하는 비정상적 동작을 방지 하기 위한 목적
if(isset($_POST&& $_SERVER['REQUEST_METHOD'== "POST"){
    @extract($_POST); // $_POST['loginID'] 라고 쓰지 않고, $loginID 라고 써도 인식되게 함
    if(isset($userID&& !empty($userID&& isset($password&& !empty($password)) {
        require_once 'config/config.php';
        require_once 'phpclass/dbconnect.php';
        require_once 'phpclass/loginClass.php';
        $c = new LoginClass();
 
        header("Cache-Control: no-cache, must-revalidate");
        header("Content-type: application/json; charset=UTF-8");
 
        //키워드 확인
        if(!isset($_POST['keyword'])){
            $result = array(
                'status' => "key fail",
                'message' => "서버 KEY 정보가 없습니다.",
                'userinfo' => null
            );
            echo json_encode($result);
            exit;
        }
 
        $keyword=$c->AES_decrypt($_POST['keyword']);
        //키워드 일치 확인
        if(strcmp($keyword,$mykey)<>0){
            $result = array(
                'status' => "key fail",
                'message' => "서버와 단말의 KEY가 일치하지 않습니다.",
                'userinfo' => null
            );
            echo json_encode($result);
            exit;
        }
 
        $userID = $c->AES_decrypt($_POST['userID']);
        $password = $c->rsa_decrypt($_POST['password']);
 
        // 동일 휴대폰 번호로 로그인하면 phoneSE 업데이트 처리
        $c->LoginUserPhoneChk($userID,$password,$uID,$mfoneNO);
 
        $rs = $c->LoginUserChk($userID,$password,$uID);
        if($rs > 0){
            $user = $c->getUser($userID$password);
            if ($user != false) {
                $c->regUserPhone($userID,$mfoneNO);
 
                if(!isset($_SESSION)) {
                    session_start();
                }
 
                $_SESSION['userID'= $user['userID'];
                $_SESSION['userNM'= $user['userNM'];
                $_SESSION['admin'= $user['admin'];
 
                $row = array("userNM"=>$user['userNM'],"mobileNO"=>$user['mobileNO'],"profileImg"=>$user['idx']);
 
                $status = "success";
                $message = "";
                $userinfo = $row;
            } else {
                $status = "로그인 에러";
                $message = "다시 한번 시도하시기 바랍니다.";
                $userinfo = null;
            }
 
        } else if($rs === -1){            
            $status = "단말 불일치";
            $message = '등록 단말 정보가 일치하지 않습니다. 관리자에게 문의하시기 바랍니다.';
            $userinfo = null;
        } else {
            $status = "로그인 에러";
            $message = '로그인 정보가 일치하지 않습니다';
            $userinfo = null;
        }
        $result = array(
            'status' => $status,
            'message' => $message,
            'userinfo' => $userinfo
        );
        echo json_encode($result);
    }
else { // 비정상적인 접속인 경우
    echo 0// loginChk.php 파일을 직접 실행할 경우에는 화면에 0을 찍어준다.
    exit;
}
?>

 

 

2. JSON 메시지 형태

- PHP 서버 코드가 제공하는 JSON 메시지

{
  "status": "로그인 에러",
  "message": "로그인 정보가 일치하지 않습니다",
  "userinfo": null
}
 
{
  "status": "단말 불일치",
  "message": "등록 단말 정보가 일치하지 않습니다. 관리자에게 문의하시기 바랍니다.",
  "userinfo": null
}
 
{
  "status": "success",
  "message": "",
  "userinfo": {
    "userNM": "강감찬",
    "mobileNO": "01092010852",
    "profileImg": 1
  }
}

- 로그인이 성공했을 때 제공되는 JSON 메시지

- 로그인이 실패했을 때 제공되는 JSON 메시지

로 구분해서 UserResult 클래스를 만들도록 서버의 PHP 코드를 고려했다.

- 메시지를 보면 userinfo 가 null 일 수도 있고, 값을 반환할 수도 있다는 점을 확인하고

  UserResult 클래스 구현시 이 사항을 반드시 고려해야 한다.

 

3. UserResult 클래스 구현

- Java 에서 구현하는 클래스로 접근해서 엄청난 삽질과 어려움을 겪었다.

- Flutter 에서 JSON 메시지 파싱처리 개념을 제대로 이해해야 문제없이 구현할 수 있다.

  반드시 factory 생성자를 구현해야 한다.

- factory 생성자 : 새로운 인스턴스를 생성하지 않는 생성자를 구현할 때 사용한다.

  . 기존에 이미 생성된 인스턴스가 있다면 return 하여 재사용한다.

  . 하나의 클래스에서 하나의 인스턴스만 생성한다.(싱글톤 패턴)

  . 서브 클래스 인스턴스를 리턴할 때 사용할 수 있다.

  . factory constructor 에서는 this에 접근할 수 없다.

class UserResult {
  final String status;
  final String message;
  final UserInfo? userinfo;
 
  UserResult(
      {required this.status, required this.message, required this.userinfo});
 
  // 반드시 정의 해주어야 서버에서 전달받은 데이터를 처리하더라.
  factory UserResult.fromJson(Map<String, dynamic> parsedJson) {
    return UserResult(
      status: parsedJson['status'],
      message: parsedJson['message'],
      userinfo: parsedJson['userinfo'== null
          ? null
          : UserInfo.fromJson(parsedJson['userinfo']),
      // UserInfo 가 null 일 수도 있고, 값이 들어있을 수도 있는 걸 삼항연산자로 처리
    );
  }
}
 
class UserInfo {
  final String userNM;
  final String mobileNO;
  final int profileImg;
 
  UserInfo({
    required this.userNM,
    required this.mobileNO,
    required this.profileImg,
  });
 
  factory UserInfo.fromJson(Map<String, dynamic> parsedJson) {
    return UserInfo(
      userNM: parsedJson['userNM'],
      mobileNO: parsedJson['mobileNO'],
      profileImg: parsedJson['profileImg'],
    );
  }
 
  Map<String, dynamic> toJson() => {
        "userNM": userNM,
        "mobileNO": mobileNO,
        "profileImg": profileImg,
      };
}
 

 

// android/app/src/main/AndroidManifest.xml 에 퍼미션 추가 
 
<uses-permission android:name="android.permission.INTERNET"/>
 
// pubspec.yaml 파일 
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  http: ^0.13.4
  dio: ^4.0.6
  logging: ^1.0.2
  get: ^4.6.5
  awesome_dialog: ^2.2.1
  validators: ^3.0.0
  shared_preferences: ^2.0.15
  platform_device_id: ^1.0.1
  mobile_number: ^1.0.4
  cached_network_image: ^3.2.1
  fluttertoast: ^8.0.9
  intl: ^0.17.0
  encrypt: ^5.0.1
  json_annotation: ^4.6.0
 

 

4. DIO 라이브러리를 이용한 Login Class 정의

import 'dart:convert';
 
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:login_ex01/model/user_result.dart';
 
import '../api/api.dart';
import '../api/logging.dart';
 
abstract class ILogin {
  Future<UserResult> login(String keyword, String userID, String password,
      String uID, String mfoneNO);
}
 
class LoginService extends ILogin {
  @override
  Future<UserResult> login(String keyword, String userID, String password,
      String uID, String mfoneNO) async {
    BaseOptions options = BaseOptions(
      baseUrl: Api.baseUrl,
      connectTimeout: 5000,
      receiveTimeout: 3000,
    );
    Dio dio = Dio(options);
    dio.interceptors.add(Logging());
 
    FormData formData = FormData.fromMap({
      "keyword": keyword,
      "userID": userID,
      "password": password,
      "uID": uID,
      "mfoneNO": mfoneNO
    });
 
    final response = await dio.post(Api.mLogin, data: formData);
      final Map<String, dynamic> body = response.data;      
      print("${body}"); // 로그 분석 목적
 
      UserResult result = UserResult.fromJson(body);
      return result;
    } else {
      return UserResult(status: "fail", message: "fail", userinfo: null);
    }
  }
}

 

5. 로그인 구현 예제

- 로그인 UI 구성이 포함되어 있으며, 로그인시 전달될 파라미터로 폰의 고유한 ID (UUID), 폰의 휴대폰번호 수집 로직, 서버와 주고받을 keyword 일치 여부를 통한 통신의 기본 체크 사항이 포함되어 있다.

- AES 암호화/복호화 메소드는 다른 게시글을 참조하면 된다.

- 로그인 성공하면 다른 UI 로의 이동은 포함하지 않았다.

  GetX 를 이용한 로그인/로그아웃 처리 로직을 구현 시 처리할 예정이다.

import 'dart:async';
import 'dart:convert';
import 'dart:io';
 
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:login_ex01/model/user_result.dart';
import 'package:platform_device_id/platform_device_id.dart';
import 'package:mobile_number/mobile_number.dart';
 
import '../api/api_cipher.dart';
import '../service/login_service.dart';
import '../utils/utils.dart';
 
class LoginView extends StatefulWidget {
  const LoginView({Key? key}) : super(key: key);
 
  @override
  State<LoginView> createState() => _LoginViewState();
}
 
class _LoginViewState extends State<LoginView> {
  final _formKey = GlobalKey<FormState>(); // 이 코드는 학습해야 함.
 
  final ILogin _login = LoginService();
  final _userIDController = TextEditingController();
  final _passwordController = TextEditingController();
 
  late String _deviceId;
 
  String _mobileNumber = '';
  List<SimCard> _simCard = <SimCard>[];
 
  late String _userID;
  late String _password;
 
  var userIDFocusNode;
  var passwdFocusNode;
 
  @override
  void initState() {
    super.initState();
    getDeviceUniqueId();
    MobileNumber.listenPhonePermission((isPermissionGranted) {
      if (isPermissionGranted) {
        initMobileNumberState();
      } else {}
    });
 
    initMobileNumberState();
  }
 
  Future<void> getDeviceUniqueId() async {
    String? deviceId;
    try {
      deviceId = await PlatformDeviceId.getDeviceId;
    } on PlatformException {
      deviceId = 'Failed to get deviceId.';
    }
 
    if (!mounted) return;
 
    setState(() {
      _deviceId = deviceId!;
      print("deviceId -> $_deviceId");
    });
  }
 
  Future<void> initMobileNumberState() async {
    if (!await MobileNumber.hasPhonePermission) {
      await MobileNumber.requestPhonePermission;
      return;
    }
    String mobileNumber = '';
    try {
      mobileNumber = (await MobileNumber.mobileNumber)!;
      _simCard = (await MobileNumber.getSimCards)!;
    } on PlatformException catch (e) {
      debugPrint("Failed to get mobile number because of '${e.message}'");
    }
 
    if (!mounted) return;
 
    setState(() {
      _mobileNumber = mobileNumber;
      _mobileNumber = Utils.getPhoneNumber(_mobileNumber);
      print("mobileNumber -> $_mobileNumber");
    });
  }
 
  Widget fillCards() {
    List<Widget> widgets = _simCard
        .map((SimCard sim) => Text(
            'Sim Card Number: (${sim.countryPhonePrefix}) - ${sim.number}\nCarrier Name: ${sim.carrierName}\nCountry Iso: ${sim.countryIso}\nDisplay Name: ${sim.displayName}\nSim Slot Index: ${sim.slotIndex}\n\n'))
        .toList();
    return Column(children: widgets);
  }
 
  onSubmit() async {
    if (_userIDController.text.trim().isEmpty ||
        _passwordController.text.trim().isEmpty) {
      if (_userIDController.text.trim().isEmpty) {
        Utils.showSnackBar(context, '아이디를 입력하세요');
        userIDFocusNode.requestFocus();
        return;
      }
 
      if (_passwordController.text.trim().isEmpty) {
        Utils.showSnackBar(context, '비밀번호를 입력하세요');
        passwdFocusNode.requestFocus();
        return;
      }
    } else {
      String keyword = CIpheR.AES_encrypt(CIpheR.URLkey());
      String passwd = _passwordController.text.trim();
     // passwd = CIpheR.rsaEncrypt(passwd);
 
      UserResult result = await _login.login(
          keyword,
          CIpheR.AES_encrypt(_userIDController.text.trim()),
          passwd,
          _deviceId,
          _mobileNumber);
 
      if (result.status.contains("success")) {
        Utils.showSnackBar(context, '로그인 성공');
      } else {
        Utils.showAlert(context, result.status, result.message);
      }
    }
  }
 
  @override
  void dispose() {
    userIDFocusNode.dispose();
    passwdFocusNode.dispose();
    super.dispose();
  }
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Login Page'),
      ),
      body: Center(
        child: SingleChildScrollView(
          child: Container(
            padding: const EdgeInsets.all(16.0),
            child: Form(
              key: _formKey,
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  Container(
                    padding: const EdgeInsets.fromLTRB(1010100),
                    child: TextFormField(
                      controller: _userIDController,
                      decoration: const InputDecoration(
                        border: OutlineInputBorder(),
                        labelText: 'UserID',
                      ),
                      focusNode: userIDFocusNode,
                      textInputAction: TextInputAction.next,
                    ),
                  ),
                  SizedBox(
                    height: 30,
                  ),
                  Container(
                    padding: const EdgeInsets.fromLTRB(1010100),
                    child: TextFormField(
                      controller: _passwordController,
                      obscureText: true,
                      decoration: const InputDecoration(
                        border: OutlineInputBorder(),
                        labelText: '비밀번호',
                      ),
                      focusNode: passwdFocusNode,
                      onEditingComplete: () async {},
                    ),
                  ),
                  SizedBox(
                    height: 30,
                  ),
                  Container(
                    height: 50,
                    padding: const EdgeInsets.fromLTRB(500500),
                    child: ElevatedButton(
                        child: Text(
                          'Login',
                          style: TextStyle(fontSize: 20.0),
                        ),
                        onPressed: onSubmit),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}
 

 

6. API 정의

class Api {
  static const baseUrl = "https://www.abc.com";
  static const mLogin = "/androidSample/loginChk.php";
 
  static const api_key = ""// 서버와 통신하기 위한 AES 암호화 키
  static const String URLKEY = ''// 서버 인증 수단 체크 keyword
 
  // 완벽 통신 위해서는 RSA 암호화 통신 및 Session 통신 처리해야 한다.
}

- 보안을 고려한 통신 처리 로직 구현에 많은 시간을 할애할 수 밖에 없다.

아직 해결해야 할 사항이 많지만 Flutter 기본 로그인 처리에 대한 이해를 하고 기록해 둔다.

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

Flutter AES256 with PHP  (0) 2023.12.19
ListView.separated 예제  (0) 2023.11.20
Session vs JWT  (0) 2022.07.22
Flutter Login 로직 구현 예제 (오류 포함)  (0) 2022.07.22
Flutter DIO 라이브러리 예제2  (0) 2022.07.02
블로그 이미지

Link2Me

,
728x90

플러터에서 제공하는 버튼이 몇개 있다.

기존 버튼들인 FlatButton, OutlineButton, RaisedButton  -> TextButton, OutlinedButton, ElevatedButton 으로 변경 되었다.

이런 버튼에 개발자가 맞춤형 버튼을 별도로 구현할 수 있다.

아래 예제 이외에도 몇 개 더 있지만 이 버튼이 사용법에 좋은 예시인거 같아서 이걸 적어둔다.

import 'package:flutter/material.dart';
 
class CustomGradientButton extends StatelessWidget {  
  final Function()? onTap;
  final String? btnText;
  final Color? firstColor;
  final Color? secondColor;
 
  const CustomGradientButton({
    Key? key,
    required this.onTap,
    this.btnText = 'Gradient Button',
    this.firstColor = Colors.green,
    this.secondColor = Colors.greenAccent,
  }) : super(key: key);
 
  @override
  Widget build(BuildContext context) {
    double height = MediaQuery.of(context).size.height;
    double width = MediaQuery.of(context).size.width;
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 24.0),
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(10.0),
        splashColor: Colors.blue.withOpacity(0.4),
        child: Ink(
          height: 50.0,
          width: width,
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(10.0),
            gradient: LinearGradient(
              colors: [
                firstColor!,
                secondColor!,
              ],
              begin: Alignment.topRight,
              end: Alignment.bottomLeft,
            ),
          ),
          child: Center(
            child: Text(
              btnText!,
              style: const TextStyle(
                fontSize: 24.0,
                color: Colors.white,
              ),
            ),
          ),
        ),
      ),
    );
  }
}
 

 

출처 : https://skillypro.com/how-to-make-a-custom-gradient-button-in-flutter/

 

 

import 'package:flutter/material.dart';
 
class RoundedButton extends StatelessWidget {
  const RoundedButton({
    Key? key,
    required this.colour,
    required this.title,
    required this.onPressed,
  }) : super(key: key);
 
  final Color colour;
  final String title;
  final VoidCallback onPressed;
 
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 16.0),
      child: Material(
        elevation: 5.0,
        color: colour,
        borderRadius: BorderRadius.circular(30.0),
        child: MaterialButton(
          onPressed: onPressed,
          minWidth: 200.0,
          height: 42.0,
          child: Text(
            title,
            style: const TextStyle(
              color: Colors.white,
              fontSize: 30.0,
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
      ),
    );
  }
}

 

 

참고하면 좋은 자료

https://nilenpatelinc.com/post/flutter-ui-3-fun-with-buttons-in-flutter/

 

Flutter UI #3: Fun with Buttons in Flutter - Nilen Patel Inc.

Tutorial and code of Buttons in Flutter. Copy and paste the below code as per your requirements. import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({Key key}) : super(key: key); @

nilenpatelinc.com

 

 

 

 

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

Flutter Stack  (0) 2023.12.27
Flutter Row 위젯  (0) 2022.06.24
Flutter Column 위젯  (0) 2022.06.24
Flutter Container 위젯  (0) 2022.06.24
Flutter CustomScrollView  (0) 2022.06.21
블로그 이미지

Link2Me

,

Session vs JWT

Flutter 앱/Network 2022. 7. 22. 20:49
728x90

Session_Based Authentication

- 세션을 이용하면 서버 Side 에 로그인된 사용자의 모든 정보를 저장한다.

- 동일 아이디 다중 접속시 기존 접속 강제 로그아웃 기능을 직접 구현해야 한다.

 

Token-Based Authentication

- 토큰 기반 인증은 클라이언트 Side에 저장되기 때문에 별도의 DB없이 로그인 구현이 가능하다.

- 서버는 토큰의 유효성 여부만 체크하므로 서버의 부담이 적다.

- 로그인된 사용자 통제가 어렵고, 강제 로그아웃 기능은 사용할 수 없다.

- 해커에게 토큰을 탈취당하면, 해당 토큰을 무효화시킬 대응이 쉽지 않다.

- JWT(JSON Web Token)는 속성 정보 (Claim)를 JSON 데이터 구조로 표현한 토큰으로 RFC 7519 표준이다.

- JWT는 서버와 클라이언트 간 정보를 주고 받을 때 Http 리퀘스트 헤더에 JSON 토큰을 넣은 후 서버는 별도의 인증 과정없이 헤더에 포함되어 있는 JWT 정보를 통해 인증한다.

- JWT는 세 파트로 나누어지며, 각 파트는 점로 구분하여 xxxxx.yyyyy.zzzzz 이런식으로 표현된다. 순서대로 헤더 (Header), 페이로드 (Payload), 서명 (Sinature)로 구성한다.

 

일반적으로 JWT 를 사용하면 아래와 같은 순서로 진행된다.

1. 클라이언트 사용자가 아이디, 패스워드를 통해 웹 서비스 인증

2. 서버에서 서명된(signed) JWT 를 생성하여 클라이언트에 응답으로 돌려주기

3. 클라이언트가 서버에 데이터를 추가적으로 요구할 때 JWT를 HTTP Header 에 첨부한다.

4. 서버에서 클라이언트로부터 전달받은 JWT를 검증한다.

 

 

 

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

ListView.separated 예제  (0) 2023.11.20
Flutter Login Example  (0) 2022.07.25
Flutter Login 로직 구현 예제 (오류 포함)  (0) 2022.07.22
Flutter DIO 라이브러리 예제2  (0) 2022.07.02
Flutter DIO 라이브러리 예제 1  (0) 2022.06.29
블로그 이미지

Link2Me

,
728x90

UI 구현 코드는 다른 게시글에 올릴 예정이므로 여기서는 언급하지 않는다.

Flutter Login 처리하는 로직 구현을 했는데 완벽하지는 않다.

아직 배우는 중이라 기록해두고 나중에 제대로 수정하련다.

import 'dart:convert';
 
import 'package:dio/dio.dart';
import 'package:login_ex01/model/user_result.dart';
 
import '../api/api.dart';
import '../model/user_model.dart';
import '../utils/utils.dart';
 
class LoginService extends ILogin {
  @override
  Future<UserResult> login(String keyword, String userID, String password,
      String uID, String mfoneNO) async {
    BaseOptions options = BaseOptions(
      baseUrl: Api.baseUrl,
      connectTimeout: 3000,
      receiveTimeout: 3000,
    );
    Dio dio = Dio(options);
 
    var formData = FormData.fromMap({
      "keyword": keyword,
      "userID": userID,
      "password": password,
      "uID": uID,
      "mfoneNO": mfoneNO
    });
 
    Response response;
 
    response = await dio.post(Api.mLogin, data: formData);
    if (response.statusCode == 200) {
      final body = json.decode(response.data);
      print("${body}");
      return body;
    } else {
      return UserResult(status: "fail", message: "fail", userinfo: null);
    }
  }
}
 
abstract class ILogin {
  Future<UserResult> login(String keyword, String userID, String password,
      String uID, String mfoneNO);
}
 

 

 

class UserResult {
  final String status;
  final String message;
  final UserInfo? userinfo;
 
  UserResult({
    required this.status,
    required this.message,
    required this.userinfo
  });
}
 
class UserInfo {
  final String userNM;
  final String mobileNO;
  final String profileImg;
 
  UserInfo({
    required this.userNM,
    required this.mobileNO,
    required this.profileImg,
  });
 
  factory UserInfo.fromJson(Map<String, dynamic> parsedJson) {
    return UserInfo(
      userNM: parsedJson['userNM'],
      mobileNO: parsedJson['mobileNO'],
      profileImg: parsedJson['profileImg'],
    );
  }
 
  Map<String, dynamic> toJson() =>
      {
        "userNM": userNM,
        "mobileNO": mobileNO,
        "profileImg": profileImg,
      };
 
}
 

 

 

import 'package:flutter/material.dart';
 
class Utils {
  static Future<void> showAlert(BuildContext context, String title, String message) async {
    return showDialog<void>(
      context: context,
      barrierDismissible: false// user must tap button!
      builder: (BuildContext context) {
        return AlertDialog(
          title: Text(title),
          content: SingleChildScrollView(
            child: ListBody(
              children: <Widget>[
                Text(message),
              ],
            ),
          ),
          actions: <Widget>[
            TextButton(
              child: const Text('OK'),
              onPressed: () {
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }
 
  static String getPhoneNumber(String phoneNumber) {
    if (phoneNumber.startsWith("82+82")) {
      phoneNumber = phoneNumber.replaceAll("82+82""0");
    }
    return phoneNumber.replaceAll("-""");
  }
}
 

 

 

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

ListView.separated 예제  (0) 2023.11.20
Flutter Login Example  (0) 2022.07.25
Session vs JWT  (0) 2022.07.22
Flutter DIO 라이브러리 예제2  (0) 2022.07.02
Flutter DIO 라이브러리 예제 1  (0) 2022.06.29
블로그 이미지

Link2Me

,
728x90

DIO 라이브러리 interceptors 사용 시 Null Safety 전후 변동사항

initializeInterceptors(){
  _dio.interceptors.add(InterceptorsWrapper(
    onError: (error){
      print(error.message);
    },
    onRequest: (request){
      print("${request.method} ${request.path}");
    },
    onResponse: (response){
      print(response.data);
    }
  ));
}
 
// ------ Null Safety 이후 -----------------------------
 
initializeInterceptors() {
  _dio.interceptors.add(InterceptorsWrapper(
    onRequest: (request, handler) async {
      print("${request.method} ${request.path}");
    },
    onResponse: (response, handler) async {
      print(response.data);
    },
    onError: (error, handler) async {
      print(error.message);
    },
  ));
}

 

pubspec.yaml 수정사항 : Null Safety 기준 : 2.12.0

- flutter pub add http 및 flutter pub add dio , flutter pub add json_serializable 을 각각 터미널 창에서 실행

- flutter pub get 를 하면 아래 dependencies 에 최신 버전이 추가된다.

environment:
  sdk: ">=2.17.5 <3.0.0"
 
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  http: ^0.13.4
  dio: ^4.0.6
  json_serializable: ^6.2.0
 
 
dev_dependencies:
  flutter_test:
    sdk: flutter
 
  flutter_lints: ^2.0.0
  build_runner: ^2.1.11
 
flutter:
 
  uses-material-design: true
  assets:
    - images/photo_base.png
 

 

User 클래스 생성하는 방법이 아래와 같은 방식으로 구글에서 많이 검색된다.

이전 게시글에서 직접 User Class 를 정의한 것과 동일하므로 비교해보는 것도 ....

user.dart 파일

import 'package:json_annotation/json_annotation.dart';
 
part 'user.g.dart';
// 아래와 같이 Class 를 정의하고 터미널 창에서
// flutter pub run build_runner build
// flutter pub get
 
@JsonSerializable()
class User {
  int idx;
  @JsonKey(name : "userNM")
  String userNM;
  @JsonKey(name : "mobileNO")
  String mobileNO;
  @JsonKey(name : "telNO")
  String telNO;
 
  String photo;
 
  User(
      {required this.idx,
        required this.userNM,
        required this.mobileNO,
        required this.telNO,
        required this.photo});
 
  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
 
}

 

자동 생성된 user.g.dart 파일

// GENERATED CODE - DO NOT MODIFY BY HAND
 
part of 'user.dart';
 
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
 
User _$UserFromJson(Map<String, dynamic> json) => User(
      idx: json['idx'] as int,
      userNM: json['userNM'] as String,
      mobileNO: json['mobileNO'] as String,
      telNO: json['telNO'] as String,
      photo: json['photo'] as String,
    );
 
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
      'idx': instance.idx,
      'userNM': instance.userNM,
      'mobileNO': instance.mobileNO,
      'telNO': instance.telNO,
      'photo': instance.photo,
    };
 

 

http_service.dart 파일

import 'package:dio/dio.dart';
 
class DioService {
  late final Dio _dio;
  final baseUrl = 'https://www.abc.com';
 
  DioService(){
    BaseOptions options = BaseOptions(
      baseUrl: baseUrl,
      connectTimeout: 3000,
      receiveTimeout: 3000,
    );
 
    _dio = Dio(options);
    initializeInterceptors();
  }
 
  Future<Response> getRequest(String endPoint) async {
    Response response;
 
    try {
      response = await _dio.get(endPoint);
    } on DioError catch (e) {
      print(e.message);
      throw Exception(e.message);
    }
 
    return response;
  }
 
  initializeInterceptors() {
    _dio.interceptors.add(InterceptorsWrapper(
      onRequest: (request, handler) async {
        print("${request.method} ${request.path}");
        return handler.next(request);
      },
      onResponse: (response, handler) async {
        print(response.headers);
        print(response.data);
        return handler.next(response);
      },
      onError: (error, handler) async {
        print(error.message);
        return handler.next(error);
      },
    ));
  }
}

 

main.dart 파일

import 'package:dio_sampe2/network/http_service.dart';
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'model/user.dart';
 
void main() {
  runApp(const MyApp());
}
 
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
 
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'JSON Data Parsing Demo'),
    );
  }
}
 
class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
 
  final String title;
 
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}
 
class _MyHomePageState extends State<MyHomePage> {
  bool isLoading = false;
  late List<User> users;
  late DioService dio;
 
  Future getListUser() async {
    Response response;
    try {
      isLoading = true;
      response = await dio.getRequest("/androidSample/getData_Flutter2.php");
      isLoading = false;
 
      if (response.statusCode == 200) {
        setState(() {
          users = response.data.map<User>((json) {
            return User.fromJson(json);
          }).toList();
        });
      } else {
        print("There is some problem status code not 200");
      }
    } on Exception catch (e) {
      isLoading = false;
      print(e);
    }
  }
 
  @override
  void initState() {
    dio = DioService();
    getListUser();
    super.initState();
  }
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: isLoading
          ? Center(child: CircularProgressIndicator())
          : users != null
              ? ListView.builder(
                  itemCount: users.length,
                  itemBuilder: (context, index) {
                    final user = users[index];
 
                    return ListTile(
                      title: Text(user.userNM),
                      leading: user.photo.length == 0
                          ? Image.asset('images/photo_base.png')
                          : Image.network(
                              'https://www.abc.com/androidSample/photos/${user.photo}'),
                      subtitle: Text(user.mobileNO),
                    );
                  },
                )
              : Center(
                  child: Text("No User Object"),
                ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: () {
              setState(() {
                //getServerDataWithHttp();
              });
            },
            tooltip: 'Increment',
            child: const Icon(Icons.add),
          ),
          SizedBox(
            width: 10.0,
            height: 10.0,
          ),
          FloatingActionButton(
            onPressed: () {
              setState(() {
                dio = DioService();
                getListUser();
              });
            },
            tooltip: 'Increment',
            child: const Icon(Icons.remove),
          ),
        ],
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
 
 

 

서버에 있는 List 자료를 가져와서 ListView 에 보여주는 코드 구현의 기본 샘플이 작성되었다.

 

https://github.com/themaaz32/dio_and_json 는 Null Safety 이전 코드인데, 참고하면 도움된다.

 

 

 

 

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

ListView.separated 예제  (0) 2023.11.20
Flutter Login Example  (0) 2022.07.25
Session vs JWT  (0) 2022.07.22
Flutter Login 로직 구현 예제 (오류 포함)  (0) 2022.07.22
Flutter DIO 라이브러리 예제 1  (0) 2022.06.29
블로그 이미지

Link2Me

,
728x90

구글링이나 GitHub 샘플코드를 받아서 보면 Null Safety 이전과 이후의 버전에 따라 동작이 되기도 하고 안되기도 하더라.

라이브러리 버전을 최신버전으로 변경해보면 에러가 발생하는 것이 http, dio 부분에서 걸리는 거 같아서

dio 라이브러리에 대한 이해 부족으로 소스코드 변경에 시간 낭비를 하는 거 같아서 학습해 두고자 한다.

 

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  http: ^0.13.4  # 0.12.2 null safety 이전, 최신 버전 0.13.4
  dio: ^4.0.6   # 3.0.10 null safety 이전, 최신 버전 4.0.6

 

main.dart 파일 import 사항

import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:dio/dio.dart';

 

android/app/src/main/AndroidManifest.xml 파일 추가 사항

- 추가 안해도 개발 모드에서 잘 동작하는 걸 확인했다.

<uses-permission android:name="android.permission.INTERNET" />

 

서버에서 데이터 가져오고 파싱처리하는 코드

class _MyHomePageState extends State<MyHomePage> {
  Future getServerDataWithHttp() async {
    try {
      var jsonString = await http.get(
          Uri.parse('https://www.abc.com/androidSample/getData_Flutter.php'));
      if (jsonString.statusCode == 200) {
        print(jsonString);
        var resp = jsonDecode(jsonString.body);
        print(resp);
        UserResult userResult = UserResult.fromJson(resp);
        for (User user in userResult.result) {
          print(user.userNM);
        }
      }
    } catch (e) {
      print('error : ${e}');
    }
  }
 
  Future getServerDataWithDio() async {
    String url = 'https://www.abc.com/androidSample/getData_Flutter.php';
    BaseOptions options = BaseOptions(
      baseUrl: 'https://www.abc.com',
      connectTimeout: 3000,
      receiveTimeout: 3000,
    );
    Dio dio = Dio(options);
    try {
      Response resp = await dio.get(
        "/androidSample/getData_Flutter.php",
        //queryParameters: {"search": "dio"},
      );
      print("Response:");
      print("Status: ${resp.statusCode}");
      print("Header:\n${resp.headers}");
      print("Data:\n${resp.data}");
 
      UserResult userResult = UserResult.fromJson(resp.data);
      for (User user in userResult.result) {
        print(user.userNM);
      }
 
    } catch (e) {
      print("Exception: $e");
    }
 
 
  }
 
  @override
  void initState() {
    super.initState();
    getServerDataWithHttp();
    getServerDataWithDio();
  }
 
  @override
  Widget build(BuildContext context) {  }

 

 

서버에서 가져온 JSON 데이터 구조

{result: [
    {idx: 1, userNM: 개발자, mobileNO: 01000010001, telNO: 0234560001, photo: 1.jpg}, 
    {idx: 2, userNM: 이정은, mobileNO: 01001230001, telNO: , photo: 2.jpg}, 
    {idx: 3, userNM: 김홍길, mobileNO: 01001230002, telNO: , photo: }
  ]
}

 

 

 

class UserResult {
  final List<User> result;
 
  UserResult({required this.result});
 
  factory UserResult.fromJson(Map<String, dynamic> parsedJson) {
 
    var list = parsedJson['result'] as List;
    List<User> usersList = list.map((i) => User.fromJson(i)).toList();
 
    return UserResult(
      result: usersList,
    );
  }
}
 
class User {
  final int idx;
  final String userNM;
  final String mobileNO;
  final String telNO;
  final String photo;
 
  User(
      {required this.idx,
      required this.userNM,
      required this.mobileNO,
      required this.telNO,
      required this.photo});
 
  factory User.fromJson(Map<String, dynamic> parsedJson) {
    return User(
      idx: parsedJson['idx'],
      userNM: parsedJson['userNM'],
      mobileNO: parsedJson['mobileNO'],
      telNO: parsedJson['telNO'],
      photo: parsedJson['photo'],
    );
  }
 
  Map<String, dynamic> toJson() => {
    "idx": idx,
    "userNM": userNM,
    "moboileNO": mobileNO,
    "telNO": telNO,
    "photo": photo,
  };
}
 

factory는 싱글톤 패턴을 사용할 때 쓰는 예약어이다.

dart 공식문서에 새로운 인스턴스를 생성하지 않는 생성자를 구현할 때 factory 키워드를 사용하라고 명시되어 있다.

factory 의 특징

- 이전에 이미 생성된 인스턴스가 있다면 원래 값을 return하여 재사용한다.

- 하나의 클래스에서 하나의 인스턴스만 사용한다.

- 서브 클래스를 리턴할 때 사용할 수 있다.

- factory 생성자에서는 this에 접근할 수 없다.

 

 

 

서버에서 넘겨주는 데이터 형식을 아래와 같이 JSON 배열 데이터로 변경했다.

[
 {idx: 1, userNM: 개발자, mobileNO: 01000010001, telNO: 0234560001, photo: 1.jpg}, 
 {idx: 2, userNM: 이정은, mobileNO: 01001230001, telNO: , photo: 2.jpg}, 
 {idx: 3, userNM: 김홍길, mobileNO: 01001230002, telNO: , photo: }
]

 

DIO를 이용해 데이터를 변환하는 로직

Future getServerDataWithDio() async {
  BaseOptions options = BaseOptions(
    baseUrl: 'https://www.abc.com',
    connectTimeout: 3000,
    receiveTimeout: 3000,
  );
  Dio dio = Dio(options);
  try {
    Response resp = await dio.get(
      "/androidSample/getData_Flutter.php",
      //queryParameters: {"search": "dio"},
    );
    print("Response:");
    print("Status: ${resp.statusCode}");
    print("Header:\n${resp.headers}");
    print("Data:\n${resp.data}");
 
    List<User> users = resp.data.map<User>((parsedJson) {
      return User.fromJson(parsedJson);
    }).toList();
 
    for (User user in users) {
      print(user.userNM);
    }
 
  } catch (e) {
    print("Exception: $e");
  }
}

 

 

POST 방식으로 데이터 통신을 할 경우 코드 예제이다.

formData 변수 사용법으로 사용하니까 제대로 동작되더라.

Future getServerDataWithDio() async {
  BaseOptions options = BaseOptions(
    baseUrl: 'https://www.abc.com',
    connectTimeout: 3000,
    receiveTimeout: 3000,
  );
  Dio dio = Dio(options);
 
  var formData = FormData.fromMap({
    "search""이정은",
  });
 
  try {
    Response resp = await dio.post(
      "/androidSample/putData_Flutter.php",
      data: formData,
    );
    // print("Response:");
    // print("Status: ${resp.statusCode}");
    // print("Header:\n${resp.headers}");
    print("Data:\n${resp.data}");
 
    // UserResult userResult = UserResult.fromJson(resp.data);
    List<User> users = UserResult.fromJson(resp.data).result;
    for (User user in users) {
      print(user.userNM);
    }
  } catch (e) {
    print("Exception: $e");
  }
}
 

 

이해를 돕기 위해 서버 코드에 사용한 PHP 코드를 첨부한다.

<?php
if(!isset($_SESSION)) {
    session_start();
}
 
//ini_set("display_startup_errors", 1);
//ini_set("display_errors", 1);
//error_reporting(E_ALL);
 
// 파일을 직접 실행하면 동작되지 않도록 하기 위해서
if(isset($_POST) && $_SERVER['REQUEST_METHOD'== "POST"){
    @extract($_POST); // POST 전송으로 전달받은 값 처리
 
    require_once 'phpclass/dbconnect.php';
    require_once 'phpclass/loginClass.php';
    $c = new LoginClass();
    $column ="idx,userNM,mobileNO,telNO,photo";
    $sql = "select idx,userNM,mobileNO,telNO,photo from Person";
    if(!empty($search)) {
        $where = "userNM LIKE '%".$search."%' or mobileNO LIKE '%".$search."%'";
    } else {
        $where = "";
    }
 
    if(strlen($where) > 0){
        $sql .= " where ".$where;
    }
 
    $R = array(); // 결과 담을 변수 생성
    $result = $c->putDbArray($sql);
    while($row = $result->fetch_assoc()) {
        if($row['photo'== NULL) {
            $row['photo'= "";
        } else {
            $path = "./photos/".$row['photo'];
            if(!file_exists($path)) {
                $row['photo'= "";
            }
        }
        array_push($R, $row);
    }
    header("Cache-Control: no-cache, must-revalidate");
    header("Content-type: application/json; charset=UTF-8");
 
    echo json_encode(array('result'=>$R)); //배열-문자열등을 json형식의 '문자열'로 변환
}
?>

 

추가적으로 계속 유사 예제로 테스트하고 적어둘 예정이다.

 

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

ListView.separated 예제  (0) 2023.11.20
Flutter Login Example  (0) 2022.07.25
Session vs JWT  (0) 2022.07.22
Flutter Login 로직 구현 예제 (오류 포함)  (0) 2022.07.22
Flutter DIO 라이브러리 예제2  (0) 2022.07.02
블로그 이미지

Link2Me

,
728x90

다트는 List, Map, Set 컬렉션을 제공한다.

 

List

- 순서가 있는 자료를 담는 컬렉션이다.

- 같은 타입의 자료를 여러개 담을 수 있고 특정 인덱스로 접근 가능하다.

 

Map : { key : value }

- key 와 value의 쌍으로 저장할 수 있고, 키를 통해 값을 얻을 수 있다.

- key는 고유한 값이여야 한다. 이미 있는 key에 값을 넣으면 덮어 씌어진다.

void main(){
  var fruits = { //맵 만들기
    'apple''사과',
    'banana''바나나',
    'carrot''당근',
  };
 
  fruits.addAll({
    'pear' : '배',
    'pineapple' : '파인애플',
  });
 
  print(fruits);
 
  fruits.remove('banana'); // 제거
 
  print(fruits.keys.toList()); // key 가져오기
  print(fruits.values.toList()); // values 가져오기
}

 

 

Set

- 중복이 허용되지 않고, 찾는 값이 있는지 없는지 판단하고자 할 때 사용한다.

 

 

 

 

void main(){
  final List<String> Items = ['짜장''짬봉''볶음밥''사천짜장''송이덧밥'];
 
  for(String item in Items){
    print(item);
  }
 
  // map() 메소드는, 사용자가 반환 된 객체에서 항목을 요청할 때까지 함수가 호출되지 않는다.
  final newItems = Items.map((x) => x);
  final newItems1 = Items.map((x) => x);
  print(newItems);
  print(newItems1);
  print(newItems == newItems1); // false
  // 눈에 보이는 결과는 동일하게 보이지만, 각각 객체로 생성되어 서로 다르다.
 
  // // 즉시 반영되도록 하고자 하는 경우에는 toList(), toMap() 메소드를 사용한다.
  final newItems2 = Items.map((x) => x).toList();
  print(Items);
  print(newItems2);
  print(Items == newItems2); // false : 원 Items 와 newItems2 는 다르다.
}

 

void main(){
  // 문자열을 분리하고 map 메소드를 사용하여 새로운 List 만들기
  var numbers = '13579';
  final parsed = numbers.split('').map((e) => '$e.png').toList();
  print(parsed);
}

 

void main(){
  var myList = [0842697];
  var result = myList.where((item) => item > 5).toList();
  print(result); // [6, 8, 7]
  var fst = myList.firstWhere((item) => item > 5);
  print(fst); // 8
  var last = myList.lastWhere((item) => item > 5);
  print(last); // 7
}

 

void main(){
  List<String> names = ['Max''John''Sara''Peter''Charlie'];
  Iterable<String> v_name = names.where((element) => element.contains('a'));
  print(v_name); // (Max, Sara, Charlie)
}

 

void main() {
  final myList = [13549110-4-10];
  final result = myList.reduce((sum, element){
    print('sum : ${sum}, element : ${element}');
    return sum + element;
  });
  print(result);
}

첫번째만 sum, element 가 1과 3이 대입되고, 그 다음부터는 return 결과값이 sum 으로 대입된다.

void main() {
  final myList = [13549110-4-10];
  final result = myList.reduce((max, element){
    //print('max : ${max}, element : ${element}');
    if(max > element){
      return max;
    } else {
      return element;
    }
  });
  print(result);
}

 

fold 는 시작값인 0을 지정한다.

void main(){
  List<int> myList = [13549110-4-10];
  final sum = myList.fold<int>(0, (sum, element){
    //print('sum : ${sum}, element : ${element}');
    // 최초 sum 값에 0 이 대입된다. 이후의 sum 은 return 결과값
    return sum + element;
  });
  print(sum); // result : 19
}

 

class Person {
  final String name;
  final int age;
 
  Person(this.name, this.age);
}
 
List<Person> person = [
  Person('A'10),
  Person('B'20),
  Person('C'30),
  Person('D'40),
  Person('E'50),
  Person('F'60),
];
 
void main() {
  final ages = person.fold<int>(0, (sum, value) => sum + value.age);
  print('$ages'); // result : 210
 
  final names = person.fold<String>('', (sum, value) => sum + value.name);
  print('${names}'); // result : ABCDEF
}

 

 

 

 

스프레드 연산자

... 연산자는 컬렉션을 펼쳐주는 연산자로 스프레드 연산자라고 한다.

다른 컬렉션 안에 컬렉션을 삽입할 때 사용한다.

컬렉션도 타입 추론이 가능하여 일반적으로 var 로 선언한다.

void main() {
  var items = ['짜장''짬봉''볶음밥'];
  print(items);
 
  var data = [...items, '라면''떡볶이''김밥'];
  print(data); // [짜장, 짬봉, 볶음밥, 라면, 떡볶이, 김밥]
}

 

 

 

 

 

 

블로그 이미지

Link2Me

,
728x90

Last Updated : 2023.10.19

 

Dart Class

- this.name 에서 this는 클래스를 의미한다.

- 인스턴스를 생성할 때 new 키워드는 붙여도 되고 붙이지 않아도 된다.

- final 키워드를 사용하여 생성자 생성시 name, leg를 변경하지 못하게 할 수 있다.

- String? _name 처럼 변수명 앞에 _가 붙으면 private를 의미하며, 같은 파일내에서만 private로 동작한다.

  Java는 Class 당 1개의 파일을 생성하는 것을 원칙으로 하고 있어 class 내에서만 private가 동작한다.

- 가시성 : 밑줄로 시작하지 않으면 모든 것이 공개(public)된다.

  자바처럼 public, protected, private 키워드를 별도로 사용하지 않는다.

void main(){
  Animal lion = Animal(name: '사자', leg: 4);
  lion.move();
 
  Animal spider = Animal(name: '거미', leg: 8);
  spider.move();
}
 
class Animal {
  final String name;
  final int leg;
 
  //Animal(this.name, this.leg);
  Animal({required this.name, required this.leg});
 
  void move(){
    print('${this.name}가 ${this.leg}개의 다리로 움직입니다.');
  }
}

 

getter / setter 예제

void main(){
  Animal lion = Animal('사자'4);
  lion.name = '호랑이'// setter
  lion.move();
 
  Animal spider = Animal('거미'8);
  spider.move();
  print(spider.name); // getter
}
 
class Animal {
  String? _name;
  final int leg;
 
  Animal(String name, int leg): this._name= name, this.leg=leg;
 
  // String get name {
  //   return this._name ?? '';
  // }
  String get name => this._name ?? ''// getter
 
  void set name(String name) { // setter
    this._name = name;
  }
 
  void move(){
    print('${this._name}가 ${this.leg}개의 다리로 움직입니다.');
  }
}

 

static 은 인스턴스에 귀속되지 않고 class 에 귀속된다.

class Employee {
  static String? building;
  final String name;
 
  const Employee({
    required this.name,
  });
 
  void workInfo(){
    print('제 이름은 ${name}입니다. ${building} 건물에서 근무중입니다.');
  }
}
 
void main() {
  Employee.building = '르네상스호텔'// static은 class 에 귀속된다.
  Employee seungki = Employee(name: '승기');
 
  seungki.workInfo();
 
  Employee gildong = Employee(name: '길동');
  gildong.workInfo();
}

 

Class Generic

- 제네릭(Generic) : 클래스를 정의할 때, 구체적인 타입(type)을 적지 않고 변수 형태로 적어 놓는 것이다. 
- 클래스를 선언하여 객체를 생성할 때, 구체적인 타입을 기재한다. 즉, 타입을 어떤 클래스 종류의 매개변수로 보는 것이다.

- 제네릭 프로그래밍(Generic Programming) : 작성한 코드를 다양한 타입의 객체에 대해 재사용하는 객체 지향 기법이다.

class Employee<T, V> {
  // Generic : 타입을 외부에서 받을 때
  final T name;
  final V age;
 
  const Employee({
    required this.name,
    required this.age,
  });
 
  void employeeInfo(){
    print('제 이름은 ${name}이고, 나이는 ${age}살 입니다.');
  }
 
  void varType(){
    print('name : ${name.runtimeType}, age : ${age.runtimeType}');
  }
}
 
void main() {
  Employee<Stringint> seungki = Employee(name: '승기', age: 30);
  seungki.employeeInfo();
  print(seungki.runtimeType); // Employee<String, int>
  seungki.varType(); // name : String, age : int
 
  Employee<Stringint> gildong = Employee(name: '길동', age: 35);
  gildong.employeeInfo();
}

 

 

 

If We have a few classes such as a Dog class, Cat Class, Wolf Class, Cow class and a Lion Class.

We can see that all these classes share common behaviors and features. 

All of these classes should have a eat method, sleep method, move method

and a makenoise method and common characteristics such as food.

클래스 상속

- 클래스는 다른 클래스를 상속(Inheritance) 받을 수 있다.

- 상속 받을 때에는 extends라는 키워드를 사용한다.

- Dart에서는 하나의 클래스만 상속이 가능하다.

- 부모 클래스에 접근하기 위해서는 super 키워드를 사용한다.

- 자식 클래스에서 @override 키워드를 사용하여 부모 클래스 메소드를 오버라이드(재정의)할 수 있다.

- 자식 클래스에서 새로운 메소드를 추가할 수 있다.

void main(){
  Animal spider = Animal(name: '거미', leg: 8);
  spider.move();
 
  Cat cat = Cat(name: '고양이', leg: 4);
  cat.move();
  cat.eat();
 
  Dog dog = Dog(name: '개', leg: 4);
  dog.move();
 
  print('------ Type Comparison ---- ');
  print(spider is Animal); // true
  print(spider is Cat); // false
  print(dog is Animal); // true
  print(dog is Dog); // true
}
 
class Animal {
  final String name;
  final int leg;
 
  //Animal(this.name, this.leg);
  Animal({required this.name, required this.leg});
 
  void move(){
    print('${this.name}가 ${this.leg}개의 다리로 움직입니다.');
  }
}
 
class Cat extends Animal {
  Cat({required super.name, required super.leg});
 
  void eat(){
    print('${super.name}가 참치를 맛있게 먹고 있습니다.');
  }
 
}
 
class Dog extends Animal {
  Dog({required super.name, required super.leg});
 
  @override
  void move() {
    //super.move();
    print('${super.name}가 ${super.leg}개의 다리로 힘차게 달립니다.');
  }
}

 

인터페이스

- Dart에서는 Interface라는 키워드 대신 class를 사용하여 인터페이스를 정의한다.

- Dart 3.0 부터는 interface class로 선언할 수 있으며 implement만 가능하다.

- 인스턴스 생성을 못하게 abstract 키워드를 붙이고, 메소드 정의만 하도록 한다.

- 인터페이스 구현은 implements 를 사용한다.

abstract class Food {
  String? name;
  void printName(); // 메소드 정의
}
 
class Fruit implements Food {
  String? name;
 
  Fruit(String name) : this.name = name;
 
  void printName() { // 메소드 구현
    print('Fruit name is ${this.name}!');
  }
 
}
 
void main() {
  Fruit fruit = Fruit('Apple');
  fruit.printName();
}

 

 

클래스 확장, 인터페이스 구현, 추상 클래스 외에도 다트는 믹스인(mixin) 개념을 제공하고 여기서 with 키워드를 사용한다.

 

Mixins

- 믹스인은 여러 클래스 계층에서 클래스의 코드를 재사용하는 방법이다.
- with 키워드를 사용하면 상속하지 않고 다른 클래스의 기능을 가져오거나 override 할 수 있다.
- 믹스인을 구현하려면 생성자를 선언하지 않는 클래스를 만든다.
- 믹스인을 일반 클래스로 사용하려면 class 대신 mixin 키워드를 사용한다.

abstract class Super {
  void method() {
    print("Super");
  }
}
 
class MySuper implements Super {
  void method() {
    print("MySuper");
  }
}
 
mixin Mixin on Super {
  void method() {
    super.method();
    print("Sub");
  }
}
 
class Client extends MySuper with Mixin {}
 
void main() {
  Client().method();
}
/* 실행 결과
MySuper
Sub
*/

참고 : https://medium.com/flutter-community/dart-what-are-mixins-3a72344011f3

 

 

Dart 3.0 변경 사항

- final로 클래스를 선언하면 extends, implement, mixin 으로 사용이 불가능하다.

- base로 선언하면 extends는 가능하지만 implement는 불가능하다.

- base, sealed, final로 선언된 클래스만 extend가 가능하다.

- interface로 선언하면 implement만 가능하다.

- sealed 클래스는 adstract이면서 final이다.

  또한 sealed 클래스는 패턴 매칭을 사용할 수 있도록 해준다.

- mixin 클래스로 선언 하여 사용할 수 있다.(mixin과 동일한 특성)

  • extends나 with를 사용할 수 없다.
  • on 키워드를 사용 할 수 없다.

 

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

Dart 3.0 records  (0) 2023.10.19
Dart Collection  (0) 2022.06.28
Dart Asynchronous programming(비동기 프로그래밍)  (0) 2022.06.23
Dart function(함수)  (0) 2022.06.22
Dart Type 검사(is, is!)  (0) 2022.06.22
블로그 이미지

Link2Me

,
728x90

https://docs.flutter.dev/development/accessibility-and-localization/internationalization 에서 localizations 관련 사항을 추가한다.

 

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations: # Add this line
    sdk: flutter         # Add this line
  get: ^4.6.5
  intl: ^0.17.0

 

 

import 'package:flutter/material.dart';
import 'dialog/datepicker_page.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
 
void main() {
  runApp(
   const MyApp(),
  );
}
 
class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);
 
  @override
  State<MyApp> createState() => _MyAppState();
}
 
class _MyAppState extends State<MyApp> {
 
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      localizationsDelegates: [
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: [
        const Locale('en''US'),
        const Locale('ko''KO'),
      ],
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: DatePickerPage(),
    );
  }
}

 

 

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
 
class DatePickerPage extends StatefulWidget {
  const DatePickerPage({Key? key}) : super(key: key);
 
  @override
  State<DatePickerPage> createState() => _DatePickerPageState();
}
 
class _DatePickerPageState extends State<DatePickerPage> {
  DateTime? _selectedDate;
 
  String getText() {
    if (_selectedDate == null) {
      return 'No date chosen!';
    } else {
      return DateFormat('yyyy-MM-dd').format(_selectedDate!);
    }
  }
 
  Future _pickDateDialog(BuildContext context) async {
    final initialDate = DateTime.now();
    final pickedDate = await showDatePicker(
      context: context,
      initialDate: initialDate,
      firstDate: DateTime(DateTime.now().year - 3),
      lastDate: DateTime(DateTime.now().year + 3),
    );
 
    if (pickedDate == nullreturn;
 
    setState(() => _selectedDate = pickedDate);
  }
 
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('DatePicker'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              child: Text('Date Picker'),
              onPressed: () => _pickDateDialog(context),
            ),
            SizedBox(width: 10.0, height: 10.0,),
            Text(getText()),
          ],
        ),
      ),
    );
  }
 
}

 

참고 :

https://github.com/JohannesMilke/date_picker_example/blob/master/lib/widget/date_picker_widget.dart

https://stackoverflow.com/questions/58066675/getting-value-from-form-with-date-picker

 

showDatePicker with GetX

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'dialog/datepicker_page.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
 
void main() {
  runApp(
    const MyApp(),
  );
}
 
class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);
 
  @override
  State<MyApp> createState() => _MyAppState();
}
 
class _MyAppState extends State<MyApp> {
 
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: 'Flutter Demo',
      localizationsDelegates: [
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: [
        const Locale('en''US'),
        const Locale('ko''KO'),
      ],
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: DatePickerPage(),
    );
  }
}

 

import 'package:flutter/material.dart';
import 'package:get/get.dart';
 
class DatePickerController extends GetxController {
  var selectedDate = DateTime.now().obs;
 
  @override
  void onInit() {
    super.onInit();
  }
 
  @override
  void onReady() {
    super.onReady();
  }
 
  @override
  void onClose() {
    super.onClose();
  }
 
  chooseDate() async {
    DateTime? pickedDate = await showDatePicker(
      context: Get.context!,
      initialDate: selectedDate.value,
      firstDate: DateTime(DateTime.now().year - 3),
      lastDate: DateTime(DateTime.now().year + 3),
      //initialEntryMode: DatePickerEntryMode.input,
      cancelText: 'Close',
    );
 
    if(pickedDate != null && pickedDate != selectedDate.value){
      selectedDate.value = pickedDate;
    }
  }
 
}

 

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import '../controllers/datepicker_controller.dart';
 
class DatePickerPage extends StatefulWidget {
  const DatePickerPage({Key? key}) : super(key: key);
 
  @override
  State<DatePickerPage> createState() => _DatePickerPageState();
}
 
class _DatePickerPageState extends State<DatePickerPage> {
  @override
  Widget build(BuildContext context) {
    Get.put(DatePickerController());
    return Scaffold(
      appBar: AppBar(
        title: Text('DatePicker'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              child: Text('Date Picker'),
              onPressed: () => Get.find<DatePickerController>().chooseDate(),
            ),
            SizedBox(
              width: 10.0,
              height: 10.0,
            ),
            Obx(
              () => Text(
                DateFormat('yyyy-MM-dd')
                    .format(Get.find<DatePickerController>().selectedDate.value)
                    .toString(),
                style: TextStyle(fontSize: 20),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

 

참고 : https://github.com/RipplesCode/DatePickerFlutterGetX?ref=morioh.com&utm_source=morioh.com

 

GitHub - RipplesCode/DatePickerFlutterGetX

Contribute to RipplesCode/DatePickerFlutterGetX development by creating an account on GitHub.

github.com

 

 

블로그 이미지

Link2Me

,
728x90

https://github.com/PuzzleLeaf/flutter_mvvm_tutorial 에서 받은 소스코드가 null-safety 이전 버전이라서 제대로 동작되지 않아서 수정하는 과정을 적어둔 것이다.

 

Null Safety 를 이용하기 위해서는 pubspec.yaml 파일의 environment 의 버전을 2.12.0 버전 이상이어야 한다.

 

environment:
  sdk: ">=2.12.0 <3.0.0"

 

Null Safety 지원 확인 및 dependencies: 자동 업데이트
- Teminal 창에서 아래와 같이 입력한다.

dart pub outdated --mode=null-safety
 
dart pub upgrade --null-safety

 

 

Flutter Upgrade 방법

// 터미널 창에서 아래 두줄을 입력한다.
flutter channel stable
 
flutter upgrade

 

 

AndroidManifest.xml 파일 수정

 

빨간색 박스 부분을 아래와 같이 수정한다.

android:name="${applicationName}"

그리고 Android Compile 버전을 31로 할 경우에는 android:exported="true" 한줄을 추가해야 한다.

 

Setting 에서 Plugins 에 업데이트 할 사항이 있다면 Update 해준다.

 

Android app build.gradle 수정사항

 

 

 

android\gradle\wrapper\gradle-wrapper.properties to gradle-6.5-all.zip

 

최종적으로 아래와 같이 수정했다.

 

android gradle 수정

 

기능 동작 테스트를 거쳐서 최종적으로 아래와 같이 수정했다.

 

 

Null safety 적용 전 버전의 모든 변수는 Nullable 이였지만 Flutter 2.0 이상부터는 Nullable 변수를 만들려면 꼭 "?" 를 선언해야 된다.
"Class 내의 변수"는 반드시 선언과 동시에 초기화를 시켜야 한다.
변수 초기화를 위해서 생성자를 만들어주거나 아니면 late keyword를 붙여 줘야 한다. 

class Person {
  String name;
  int age;
 
  Person({required this.name, required this.age});
}
 
main(){
  Person p = Person(name: "홍길동", age: 25);
  print(p);
  print(p.name);
  print(p.age);
}

 

news_article.dart 파일 내용을 아래와 같이 수정했다.

전체 소스코드를 일일이 확인하면서 수정한 것이 아니라 에러가 발생하는 부분을 찾아서 수정하다보니 null 처리를 위해 ? 붙이는 것으로 대체했다.

class NewsArticle {
  final String? title;
  final String? author;
  final String? description;
  final String? urlToImage;
  final String? url;
  late final String publishedAt;
  final String? content;
 
  NewsArticle(
      {required this.title,
      required this.author,
      required this.description,
      required this.urlToImage,
      required this.url,
      required this.publishedAt,
      required this.content}
      );
 
  factory NewsArticle.fromJson(Map<String, dynamic> json) {
    return NewsArticle(
      title: json['title'],
      author: json['author'],
      description: json['description'],
      urlToImage: json['urlToImage'],
      url: json['url'],
      publishedAt: json['publishedAt'],
      content: json['content'],
    );
  }
}

 

 

 

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

Flutter Dart Data Class 자동 생성  (0) 2023.12.13
Flutter freezed data class 자동완성 (Android Studio)  (0) 2023.12.08
IntelliJ IDEA Community 에 Dart 설치  (0) 2022.06.24
Flutter Upgrade  (0) 2022.06.17
dart-sdk 설치  (0) 2022.01.11
블로그 이미지

Link2Me

,
728x90

IntelliJ IDEA Community 툴에서 Dart 언어를 설치하는 방법이다.

 

dart-sdk 경로를 찾아서 적어준다. flutter 설치한 서브폴더를 찾는다.

 

 

 

 

이제 사용할 준비가 된 것이다.

블로그 이미지

Link2Me

,