728x90

플러터 버튼을 2개 이상 표시하는 방법이다.

 

floatingActionButton: Stack(
  children: [
    Align(
      alignment: Alignment(
          Alignment.bottomRight.x, Alignment.bottomRight.y - 0.4),
      child: FloatingActionButton(
        onPressed: () {
          Navigator.of(context).push(MaterialPageRoute(
            builder: (context) => const FileUploadScreen(),
          ));
        },
        tooltip: 'back',
        heroTag: UniqueKey(),
        child: const Icon(Icons.arrow_back),
      ),
    ),
    Align(
      alignment: Alignment(
          Alignment.bottomRight.x, Alignment.bottomRight.y - 0.2),
      child: FloatingActionButton(
        onPressed: () async {
          getImage(ImageSource.camera);
        },
        tooltip: 'image',
        heroTag: UniqueKey(),
        child: const Icon(Icons.camera_alt),
      ),
    ),
    Align(
      alignment: Alignment.bottomRight,
      child: FloatingActionButton(
        onPressed: () async {
          getImage(ImageSource.gallery);
        },
        tooltip: 'image',
        heroTag: UniqueKey(),
        child: const Icon(Icons.image),
      ),
    ),
  ],
),

 

 

블로그 이미지

Link2Me

,
728x90

위젯 트리가 빌드된 이후에 실행되는 콜백 메서드이다.

함수를 바로 실행하지 않고, 아래와 같이 WidgetsBinding.instance.addPostFrameCallback 함수 이용한다는 걸 알아두자.

WidgetsBinding.instance.addPostFrameCallback((_) {
  // 실행할 작업
});

 

class HomePage extends ConsumerStatefulWidget {
  const HomePage({
    Key? key,
  }) : super(key: key);
 
  @override
  ConsumerState createState() => _HomePageState();
}
 
class _HomePageState extends ConsumerState<HomePage> {
  @override
  void initState() {
    // initiate viewModel
    WidgetsBinding.instance.addPostFrameCallback((_) {
      // call view model fetch data
      ref.read(homeViewModelProvider.notifier).fetchData();
    });
    super.initState();
  }
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(

 

블로그 이미지

Link2Me

,
728x90

Flutter 프로젝트 신규 생성 시 기본적으로 가장 먼저 추가할 라이브러리를 정리해봤다.

기본적으로 서버의 JSON 포멧 자료를 파싱처리를 자동으로 하기 위해서 Retrofit 라이브러리를 같이 추가해준다.

# 플러터에서 기본적으로 추가할 라이브러리
flutter pub add dio json_annotation freezed_annotation 
 
flutter pub add -d json_serializable build_runner freezed
 
# 서버 데이터를 가져오기 위한 Retrofit 추가
flutter pub add retrofit logger
flutter pub add -d retrofit_generator
 
# 상태관리툴 라이브러리
# riverpod 추가
flutter pub add flutter_riverpod riverpod_annotation
flutter pub add -d riverpod_generator

# provider 추가
flutter pub add provider 
 

 

상태관리 라이브러리는

riverpod 로 된 강좌도 있고, provider 로 된 강좌도 있다.

주로 provider 상태관리 라이브리를 활용한 강좌가 많다.

 

http 로 서버 통신 강좌 설명을 하기도 하지만 Dio 라이브러리를 활용하는 것이 더 좋다.

header 에 cookie 정보를 실어서 Session 처리를 하기도 좋고, JWT 토큰 인증시에도 활용하기 좋다.

블로그 이미지

Link2Me

,
728x90

플러터를 구현하다가 만나는 팁 정보를 하나씩 모아서 적어두려고 한다.

 

List<String>.from(item['name']),

    - 'List<dynamic>' is not a subtype of type 'List<String>' 에러 메시지를 만났을 때의 해결 방법

 

AppBar 배경 그림자 없애기

elevation : 0 으로 설정

 

검색창 코드

Padding(
  padding: const EdgeInsets.all(16.0),
  child: TextField(
    controller: _controller,
    decoration: InputDecoration(
      border: const OutlineInputBorder(
        borderRadius: BorderRadius.all(Radius.circular(10.0)),
      ),
      suffixIcon: IconButton(
        onPressed: () async {
          viewModel.fetch(_controller.text);
        },
        icon: const Icon(Icons.search),
      ),
    ),
  ),
),

 

 

 

 

 

 

블로그 이미지

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 결과 데이터를 반환하도록 처리하기 위해서 추가한 것이다.

블로그 이미지

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

 

블로그 이미지

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

로 변경되었다.

블로그 이미지

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

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

 

먼저 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

앱 내에 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

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

상대적으로 적은 양의 키-값 데이터를 저장하려고 한다면, shared_preferences 플러그인을 사용한다.

 

https://pub.dev/packages/shared_preferences/install 에서 최신 버전을 찾아서 pubspec.yaml 내에 추가한다.

dependencies:
  shared_preferences: ^2.0.15

 

Dart 코드에 아래 한줄을 import 한다.

import 'package:shared_preferences/shared_preferences.dart';

 

Save Data

// shared preferences 얻기
final prefs = await SharedPreferences.getInstance();
 
// 값 저장하기
prefs.setInt('counter', counter);

 

Read Data

// Save Data
final prefs = await SharedPreferences.getInstance();
prefs.setInt('counter'0);
prefs.setDouble('width'20.5);
prefs.setBool('isAdmin'true);
prefs.setString('userName''dev-yakuza');
prefs.setStringList('alphabet', ['a''b''c''d']);
 
// Read Data
final prefs = await SharedPreferences.getInstance();
final counter = prefs.getInt('counter') ?? 0;
final width = prefs.getDouble('width') ?? 10.5;
final isAdmin = prefs.getBool('isAdmin') ?? false;
final userName = prefs.getString('userName') ?? '';
final alphabet = prefs.getStringList('alphabet') ?? [];
final data = prefs.get('userInfo') : {};

 

Remove Data

final prefs = await SharedPreferences.getInstance();
prefs.remove('counter'); // 데이터 삭제

모든 데이터 삭제는 prefs.clear();

 

 

예제

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
 
class SharedPreferencesDemo extends StatefulWidget {
  const SharedPreferencesDemo({Key? key}) : super(key: key);
 
  @override
  State<SharedPreferencesDemo> createState() => _SharedPreferencesDemoState();
}
 
class _SharedPreferencesDemoState extends State<SharedPreferencesDemo> {
  // Future 은 미래의 값을 의미한다.
  // Future는 비동기 작업의 결과를 나타내며
  // 미완료(value를 생성하기 전)또는 완료(value 생성)의 두 가지 상태를 가질 수 있다.
  // Dart에서는 비동기 작업을 수행하기 위해 Future클래스와 async 및 await 키워드를 사용할 수 있다.
  final Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
  late Future<int> _counter;
 
  // async : 함수 본문 앞에 키워드로 사용하여 비동기로 표시
  // Future는 Future<타입명> 이런식으로 타입을 명시해야 한다.
  Future<void> _incrementCounter() async {
    final SharedPreferences prefs = await _prefs;
    final int counter = (prefs.getInt('counter') ?? 0+ 1;
 
    setState(() {
      _counter = prefs.setInt('counter', counter).then((bool success) {
        return counter;
      });
    });
  }
 
  @override
  void initState() {
    super.initState();
    _counter = _prefs.then((SharedPreferences prefs) {
      return prefs.getInt('counter') ?? 0;
    });
  }
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('SharedPreferences Demo'),
      ),
      body: Center(
          child: FutureBuilder<int>(
              future: _counter,
              builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
                switch (snapshot.connectionState) {
                  case ConnectionState.waiting:
                    return const CircularProgressIndicator();
                  default:
                    if (snapshot.hasError) {
                      return Text('Error: ${snapshot.error}');
                    } else {
                      return Text(
                        '${snapshot.data} time${snapshot.data == 1 ? '' : 's'}.\n'
                       'This should persist across restarts.',
                      );
                    }
                }
              })),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

 

블로그 이미지

Link2Me

,
728x90

안드로이드 앱에서는 화면에 표시되는 것을 View 클래스로 정의한다.

화면에 텍스트를 보여주고 이미지를 표시하고 버튼을 클릭하는 등의 모든 표현과 사용자 상호작용은 View를 통해 일어난다.

 

플러터에서는 모든 화면 표시와 사용자 상호작용은 위젯을 사용한다.

화면에 위젯을 배치하고 위젯이 정보를 표시하고 사용자의 입력을 받거나 네트워크에서 받아온 결과를 출력한다.

 

플러터의 위젯은 크게 Stateless Widget 과 Stateful Widget 으로 구별한다.

Stateless Widget은 표면 표시용 위젯이다. 위젯이 로딩되어 화면에 표시된 이후에는 사용자 이벤트나 동작이 있어도 내용을 변경할 수 없다.

Stateful Widget은 변경 가능한 상태(State)를 가진다.

Stateful Widget 은 UI가 동적으로 변경될 수 있는 경우에 유용하다.

 

StatelessWidget은 상태가 변경될 때 플러터 코어 프레임워크에 의해 자동으로 다시 랜더링도지 않는다.

StatefulWidget의 상태가 변하면 변경되는 원인에 관계없이 특정 생명주기 이벤트가 발생한다. 생명주기 이벤트 함수 호출은 트리거한 결과 위젯이 차지하는 화면의 일부를 다시 랜더링한다.

import 'package:flutter/material.dart';
 
class LView extends StatefulWidget {
  const LView({Key? key, required this.title}) : super(key: key);
 
  final String title; // 부모 위젯으로부터 전달받은 변수
  // final 지시어는 어떤 변수를 참조하는 값이 한번 설정되면
  // 다른 값으로 변경될 수 없다는 의미이다.
 
  @override
  State<LView> createState() => _LViewState();
}
 
class _LViewState extends State<LView> {
  // 상태를 변경하므로 변경가능한 것들은 선언할 수 있다.
 
  int _counter = 0// _ 를 붙이면 private 변수
  // var : 변수 할당시 타입이 지정된다. dart 컴퍼일러에서 타입을 추론한다.
  // dynamic : 타입을 특정하지 않는다.
  List<String> name = ['주영훈''홍길동''이순신'];
  var like = [000];
 
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
 
  // widget으로 StatefulWidget에 선언한 프로퍼티들에 접근이 가능
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: ListView.builder(
        itemCount: name.length,
        itemBuilder: (c, i){
          return ListTile(
            leading: Text(like[i].toString()),
            title: Text(name[i]),
            trailing: ElevatedButton(
              child: Text('좋아요'),
              onPressed: (){
                setState(() {
                  like[i]++;
                });
              },
            ),
          );
        }),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Text(_counter.toString()),
        // child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

 

플러터 UI는 코드에서 만들어진다.

 

 

 

 

 

 

블로그 이미지

Link2Me

,
728x90

플러터 팝업창에서 입력한 값을 전달받아 Array 를 업데이트하는 예제이다.

부모 위젯에서 만든 함수를 자식 위젯으로 인자로 넘겨서, 자식 위젯에서 입력한 변수를 받아서 매개변수로 넘기면 ListView가 업데이트되는 코드이다.

import 'package:flutter/material.dart';
 
class LView extends StatefulWidget {
  const LView({Key? key, required this.title}) : super(key: key);
 
  final String title; // 부모 위젯으로부터 전달받은 변수
 
  @override
  State<LView> createState() => _LViewState();
}
 
class _LViewState extends State<LView> {
  var person = ['강감찬''홍길동''이순신''유관순'];
  var clickme = [0000];
 
  addNameArr(name) { // 자식 위젯에서 입력한 값을 전달받아 Array Update
    setState(() {
      person.add(name);
      clickme.add(0);
    });
  }
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: ListView.builder(
          itemCount: person.length,
          itemBuilder: (c, i) {
            return ListTile(
              leading: Text(clickme[i].toString()),
              title: TextButton(
                child: Text(person[i]),
                onPressed: () => showDialog<String>(
                    context: context,
                    barrierDismissible: false// Dialog를 제외한 다른 화면 터치 x
                    builder: (context) {
                      return DialogUI(
                          Cnt: clickme[i], Name: person[i]); // 변수 넘겨주기
                    }),
              ),
              trailing: ElevatedButton(
                child: Text('좋아요'),
                onPressed: () {
                  setState(() {
                    clickme[i]++;
                  });
                },
              ),
            );
          }),
      floatingActionButton: FloatingActionButton(
        onPressed: () => showDialog(
            context: context,
            builder: (context) {
              return InsertDialogUI(addNameArr: addNameArr);
            }),
        child: const Icon(Icons.add),
      ),
    );
  }
}
 
class DialogUI extends StatelessWidget {
  const DialogUI({Key? key, this.Cnt, this.Name}) : super(key: key);
  final Cnt; // 부모 위젯으로부터 전달받은 변수 등록
  final Name; // 부모 위젯으로부터 전달받은 변수 등록
 
  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      // RoundedRectangleBorder - Dialog 화면 모서리 둥글게 조절
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)),
      backgroundColor: Colors.white,
      //Dialog Main Title
      title: Column(
        children: <Widget>[
          Text("팝업 메시지"),
        ],
      ),
      //
      content: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text(
            "카운트 횟수 ${Cnt}, 이름 : ${Name}",
          ),
        ],
      ),
      actions: <Widget>[
        ElevatedButton(
          onPressed: () => Navigator.pop(context, 'Cancel'),
          child: const Text('Cancel'),
        ),
        ElevatedButton(
          onPressed: () => Navigator.pop(context, 'OK'),
          child: const Text('OK'),
        ),
      ],
    );
  }
}
 
class InsertDialogUI extends StatelessWidget {
  InsertDialogUI({Key? key, this.addNameArr}) : super(key: key);
  final addNameArr; // 부모 위젯에서 전달받은 변수(함수)
 
  final _textFieldController = TextEditingController();
 
  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: Text('Item 추가'),
      content: TextField(
        controller: _textFieldController,
        decoration: const InputDecoration(hintText: "성명 입력하세요"),
      ),
      actions: [
        ElevatedButton(
          onPressed: () => Navigator.pop(context, 'Cancel'),
          child: const Text('Cancel'),
          style: ElevatedButton.styleFrom(
            primary: Colors.green,
            onPrimary: Colors.white,
          ),
        ),
        ElevatedButton(
          onPressed: () {
            addNameArr(_textFieldController.text);
            Navigator.pop(context);
          },
          child: const Text('OK'),
          style: ElevatedButton.styleFrom(
            primary: Colors.green,
            onPrimary: Colors.white,
          ),
        ),
      ],
    );
  }
}

 

 

블로그 이미지

Link2Me

,
728x90

특정 버튼(이미지, List Item 등)을 눌렀을 때 팝업되는 메시지 구현 예제이다.

import 'package:flutter/material.dart';
 
class LView extends StatefulWidget {
  const LView({Key? key, required this.title}) : super(key: key);
 
  final String title; // 부모 위젯으로부터 전달받은 변수
 
  @override
  State<LView> createState() => _LViewState();
}
 
class _LViewState extends State<LView> {
  var name = ['강감찬''홍길동''이순신','유관순'];
  var clickme = [0000];
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: ListView.builder(
         itemCount: name.length,
          itemBuilder: (c, i) {
            return ListTile(
              leading: Text(clickme[i].toString()),
              title: Text(name[i]),
              trailing: ElevatedButton(
                child: Text('좋아요'),
                onPressed: () {
                  setState(() {
                    clickme[i]++;
                  });
                },
              ),
            );
          }),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ShowDialog(),
        // child: Text(_counter.toString()),
        child: const Icon(Icons.add),
      ),
    );
  }
 
  ShowDialog() {
    showDialog<String>(
        context: context,
        barrierDismissible: false// Dialog를 제외한 다른 화면 터치 x
        builder: (BuildContext context) {
          return AlertDialog(
            // RoundedRectangleBorder - Dialog 화면 모서리 둥글게 조절
            shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(10.0)),
            backgroundColor: Colors.white,
            //Dialog Main Title
            title: Column(
              children: <Widget>[
                Text("팝업 메시지"),
              ],
            ),
            //
            content: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Text(
                  "AlertDialog Content",
                ),
              ],
            ),
            actions: <Widget>[
              ElevatedButton(
                onPressed: () => Navigator.pop(context, 'Cancel'),
                child: const Text('Cancel'),
              ),
              ElevatedButton(
                onPressed: () => Navigator.pop(context, 'OK'),
                child: const Text('OK'),
              ),
            ],
          );
        });
  }
}
 

 

값을 넘겨주는 걸 처리하는 방법에 대해 알아보자.

부모 위젯 → 자식 위젯 으로 값을 넘기는 방법

Class 간에 값 전달 방식으로 값, 함수를 넘기므로 아래 코드에서 해당 주석 부분을 잘 살펴보면 된다.

import 'package:flutter/material.dart';
 
class LView extends StatefulWidget {
  const LView({Key? key, required this.title}) : super(key: key);
 
  final String title; // 부모 위젯으로부터 전달받은 변수
 
  @override
  State<LView> createState() => _LViewState();
}
 
class _LViewState extends State<LView> {
  var name = ['강감찬''홍길동''이순신','유관순'];
  var clickme = [0000];
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: ListView.builder(
         itemCount: name.length,
          itemBuilder: (c, i) {
            return ListTile(
              leading: Text(clickme[i].toString()),
              title: Text(name[i]),
              trailing: ElevatedButton(
                child: Text('좋아요'),
                onPressed: () {
                  setState(() {
                    clickme[i]++;
                  });
                },
              ),
            );
          }),
      floatingActionButton: FloatingActionButton(
        onPressed: () => showDialog<String>(
            context: context,
            barrierDismissible: false// Dialog를 제외한 다른 화면 터치 x
            builder: (context){
          return DialogUI(Cnt: clickme[1], Name: name[1]); // 변수 넘겨주기
        }),
        // child: Text(_counter.toString()),
        child: const Icon(Icons.add),
      ),
    );
  }
 
}
 
class DialogUI extends StatelessWidget {
  const DialogUI({Key? key, this.Cnt, this.Name}) : super(key: key);
  final Cnt; // 부모 위젯으로부터 전달받은 변수 등록
  final Name; // 부모 위젯으로부터 전달받은 변수 등록
 
  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      // RoundedRectangleBorder - Dialog 화면 모서리 둥글게 조절
      shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(10.0)),
      backgroundColor: Colors.white,
      //Dialog Main Title
      title: Column(
        children: <Widget>[
          Text("팝업 메시지"),
        ],
      ),
      //
      content: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text(
            "AlertDialog Content : 카운트 횟수 ${Cnt}, 이름 : ${Name}",
          ),
        ],
      ),
      actions: <Widget>[
        ElevatedButton(
          onPressed: () => Navigator.pop(context, 'Cancel'),
          child: const Text('Cancel'),
        ),
        ElevatedButton(
          onPressed: () => Navigator.pop(context, 'OK'),
          child: const Text('OK'),
        ),
      ],
    );
  }
}

 

이제 ListView Item 에서 클릭을 하면 팝업창이 뜨는 예제로 전환해보자.

title : Text 를 title : TextButton 으로 변경하고 onPressed 이벤트를 추가한다.

import 'package:flutter/material.dart';
 
class LView extends StatefulWidget {
  const LView({Key? key, required this.title}) : super(key: key);
 
  final String title; // 부모 위젯으로부터 전달받은 변수
 
  @override
  State<LView> createState() => _LViewState();
}
 
class _LViewState extends State<LView> {
  var name = ['강감찬''홍길동''이순신','유관순'];
  var clickme = [0000];
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: ListView.builder(
         itemCount: name.length,
          itemBuilder: (c, i) {
            return ListTile(
              leading: Text(clickme[i].toString()),
              title: TextButton(
                  child: Text(name[i]),
                onPressed: () => showDialog<String>(
                    context: context,
                    barrierDismissible: false// Dialog를 제외한 다른 화면 터치 x
                    builder: (context){
                      return DialogUI(Cnt: clickme[i], Name: name[i]); // 변수 넘겨주기
                    }),
              ),
              trailing: ElevatedButton(
                child: Text('좋아요'),
                onPressed: () {
                  setState(() {
                    clickme[i]++;
                  });
                },
              ),
            );
          }),
      floatingActionButton: FloatingActionButton(
        onPressed: (){},
        child: const Icon(Icons.add),
      ),
    );
  }
 
}
 
class DialogUI extends StatelessWidget {
  const DialogUI({Key? key, this.Cnt, this.Name}) : super(key: key);
  final Cnt; // 부모 위젯으로부터 전달받은 변수 등록
  final Name; // 부모 위젯으로부터 전달받은 변수 등록
 
  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      // RoundedRectangleBorder - Dialog 화면 모서리 둥글게 조절
      shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(10.0)),
      backgroundColor: Colors.white,
      //Dialog Main Title
      title: Column(
        children: <Widget>[
          Text("팝업 메시지"),
        ],
      ),
      //
      content: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text(
            "카운트 횟수 ${Cnt}, 이름 : ${Name}",
          ),
        ],
      ),
      actions: <Widget>[
        ElevatedButton(
          onPressed: () => Navigator.pop(context, 'Cancel'),
          child: const Text('Cancel'),
        ),
        ElevatedButton(
          onPressed: () => Navigator.pop(context, 'OK'),
          child: const Text('OK'),
        ),
      ],
    );
  }
}
 

 

 

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

Flutter Widget 개념(stateless, stateful)  (0) 2022.06.18
Flutter input dialog (자식 → 부모)  (0) 2022.06.15
Flutter ListView 예제1  (0) 2021.12.27
Flutter Toast 메시지  (0) 2021.12.24
Flutter 화면 이동 (Navigator)  (0) 2021.12.23
블로그 이미지

Link2Me

,
728x90

플러터 ListView 에 표시할 데이터를 가져올 사이트는 https://jsonplaceholder.typicode.com/posts 이다.

 

 

http 통신을 위한 설정

http 패키지를 사용하면 인터넷으로부터 데이터를 손쉽게 가져올 수 있다.

http패키지를 설치하기 위해서, pubspec.yaml의 의존성(dependencies) 부분에 추가해줘야 한다.

 

 

dependencies 에 http, http_parser 를 위와 같이 추가하고

 

 

 

최신버전에 대한 정보는 https://pub.dev/packages/http/versions 에서 확인한다.

 

최신버전으로 하지 않아서 에러가 발생해서 환경 설정 부분을 수정했다.

 

 

https://www.youtube.com/watch?v=EwHMSxSWIvQ 동영상 자료를 참고해서 데이터 가져오기를 그대로 해보려고 했는데 url 사이트가 연결이 되지 않아서 URL 이 실제 동작하는 것으로 변경했다.

 

http.get() 메소드는 Response를 포함하고 있는 Future를 반환한다.

Future는 비동기 연산에 사용되는 Dart의 핵심 클래스이다.

 

예제 코드

import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
 
class ListView_remoteJSON extends StatefulWidget {
  const ListView_remoteJSON({Key? key}) : super(key: key);
 
  @override
  _ListView_remoteJSONState createState() => _ListView_remoteJSONState();
}
 
class _ListView_remoteJSONState extends State<ListView_remoteJSON> {
  Future<List<PostData>> _getPost() async {
    // http 0.12.2 로 하면 에러가 발생하지 않고 http 0.13.4 로 하면 에러 발생
    final response =
        await http.get("https://jsonplaceholder.typicode.com/posts");
    if (response.statusCode == 200) {
      var jsonData = json.decode(response.body);
      List<PostData> postDatas = [];
 
      for (var item in jsonData) {
        PostData postData =
            PostData(item['userId'], item['id'], item['title'], item['body']);
        postDatas.add(postData);
      }
      //print("데이터 개수 : ${postDatas.length}");
      return postDatas;
    } else {
      throw Exception('Failed to load postData');
    }
  }
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('ListView_Demo with JSON')),
      body: Container(
        child: FutureBuilder(
          future: _getPost(),
          builder: (context, AsyncSnapshot snapshot) {
            
            if (snapshot.hasData) {
              return ListView.builder(
                  itemCount: snapshot.data.length,
                  itemBuilder: (context, index) {
                    return Card(
                      child: ListTile(
                        title: Text(snapshot.data[index].title),
                        subtitle: Text(snapshot.data[index].body),
                        onTap: () {
                          Navigator.push(
                              context,
                              MaterialPageRoute(
                                  builder: (context) =>
                                      DetailPage(snapshot.data[index])));
                        },
                      ),
                    );
                  });
            } else {
              return Container(
                child: Center(
                  child: Text("Loading..."),
                ),
              );
            }
          },
        ),
      ),
    );
  }
}
 
class DetailPage extends StatelessWidget {
  final PostData postData;
 
  DetailPage(this.postData); // 생성자를 통해서 입력변수 받기
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Detail Page'),
      ),
      body: Container(
        child: Center(
          child: Text(postData.title),
        ),
      ),
    );
  }
}
 
class PostData {
  final int userId;
  final int id;
  final String title;
  final String body;
 
  PostData(this.userId, this.id, this.title, this.body);
}
 

 

블로그 이미지

Link2Me

,
728x90

Flutter 에서 Toast 메시지를 띄우는 걸 하는 방법이다.

 

https://pub.dev/packages/fluttertoast/install 를 보고 순서대로 진행한다.

현재 실행중인 Project 에서 터미널 창을 열어서 flutter pub add fluttertoast 를 입력한다.

아래 파일에 자동으로 추가된 것을 확인할 수 있다.

Pub get 을 눌러준다. Android Native 앱 Sync Now 와 같은 기능으로 보인다.

여기까지 해주고 예제 소스코드로 테스트하는데 동작이 제대로 안된다. 흐미ㅜㅜ

Hot Reload 기능이 제대로 동작되지 않는 거 같더라.

그래서 완전 종료한 후 버튼을 눌렀더니 제대로 동작되는 걸 확인할 수 있었다.

 

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'listview_demo1.dart';
 
class CallsfloatBtn extends StatefulWidget {
  const CallsfloatBtn({Key? key}) : super(key: key);
 
  @override
  _CallsfloatBtnState createState() => _CallsfloatBtnState();
}
 
class _CallsfloatBtnState extends State<CallsfloatBtn> {
  int _counter = 0;
  late FToast fToast;
 
  @override
  void initState() {
    super.initState();
    fToast = FToast();
    fToast.init(context);
  }
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center, // 중앙정렬
          children: <Widget>[
            Text(
              '당신은 버튼을 눌렀어요 :',
              softWrap: false// 텍스트가 영역을 넘어갈 경우 줄바꿈 여부
              style: Theme.of(context).textTheme.headline5,
            ),
            Text(
              '$_counter 번',
              style: TextStyle(fontSize: 25),
            ),
            ElevatedButton(
              child: Text('화면 이동'),
              onPressed: () {
                Navigator.push(context,
                    MaterialPageRoute(builder: (context) => ListView_Demo1()));
              },
              style: ButtonStyle(
                  backgroundColor: MaterialStateProperty.all(Colors.teal),
                  padding: MaterialStateProperty.all(EdgeInsets.all(15)),
                  textStyle:
                      MaterialStateProperty.all(TextStyle(fontSize: 14))),
            ),
            ElevatedButton(
              child: Text("Show Toast"),
              onPressed: () {
                showToast();
              },
            ),
            ElevatedButton(
              child: Text("Show Custom Toast"),
              onPressed: () {
                showCustomToast();
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
 
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
 
  void showToast() {
    Fluttertoast.showToast(
        msg: "기본 토스트 메시지입니다!",
        toastLength: Toast.LENGTH_LONG,
        fontSize: 14,
        backgroundColor: Colors.green
    );
  }
 
  showCustomToast() {
    Widget toast = Container(
      padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(25.0),
        color: Colors.greenAccent,
      ),
      child: Text("This is a Custom Toast"),
    );
 
    fToast.showToast(
      child: toast,
      toastDuration: Duration(seconds: 3),
    );
  }
  
}
 

 

참고자료

https://codesinsider.com/flutter-toast-message-example-tutorial/

 

Flutter Toast Message Example Tutorial – CODES INSIDER

Toast is nothing but a flash message in flutter. Learn how to create & show toast message and custom toast using FlutterToast dependency.

codesinsider.com

OKToast 는 기회되면 나중에 한번 테스트 해보고자 한다.

https://github.com/OpenFlutter/flutter_oktoast

 

GitHub - OpenFlutter/flutter_oktoast: a pure flutter toast library

a pure flutter toast library. Contribute to OpenFlutter/flutter_oktoast development by creating an account on GitHub.

github.com

 

블로그 이미지

Link2Me

,
728x90

Flutter에서 screen 과 page 는 route 로 불린다.
Route는 Android의 Activity, iOS의 ViewController와 동일하다. 
Flutter에서는 Route 역시 위젯이다.

새로운 route로 이동은 Navigator를 사용한다.
Navigator.push()를 사용하여 두 번째 route로 전환한다.
Navigator.pop()을 사용하여 첫 번째 route로 되돌아 온다.

 

import 'package:link2me/route.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.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> {
 
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: FirstRoute(),
    );
  }
}

 

 

import 'package:flutter/material.dart';
 
class FirstRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Route'),
      ),
      body: Center(
        child: ElevatedButton(
          child: Text('Open route'),
          onPressed: () {
            // 눌렀을 때 두 번째 route로 이동
            // push() 메서드는 Route를 Navigator에 의해 관리되는 route 스택에 추가
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => SecondRoute()),
            );
          },
        ),
      ),
    );
  }
}
 
class SecondRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Second Route"),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            // pop() 메서드는 route 스택에서 현재 Route를 제거
            Navigator.pop(context);
          },
          child: Text('Go back!'),
        ),
      ),
    );
  }
}
 

자료 출처 : https://docs.flutter.dev/cookbook/navigation/navigation-basics

 

Navigator.pushNamed()를 통한 화면전환

출처 : https://docs.flutter.dev/cookbook/navigation/named-routes

Route 정의하기
다음으로, MaterialApp 생성자에 initialRoute와 routes 이름의 추가 프로퍼티를 제공하여 route를 정의한다.
initialRoute 프로퍼티는 앱의 시작점을 나타내는 route를 정의하고, 
routes 프로퍼티는 이용가능한 named route와 해당 route로 이동했을 때 빌드될 위젯을 정의한다.

 
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      initialRoute: '/',
      routes: {
        // "/" Route로 이동하면, FirstRoute 위젯을 생성한다.
        '/': (context) => FirstRoute(),
        // "/second" route로 이동하면, SecondRoute 위젯을 생성한다.
        '/second': (context) => SecondRoute(),
      },
      //home: FirstRoute(),
    );
  }

 

import 'package:flutter/material.dart';
 
class FirstRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Route'),
      ),
      body: Center(
        child: ElevatedButton(
          child: Text('Open route'),
          onPressed: () {
            // 눌렀을 때 두 번째 route로 이동
            // push() 메서드는 Route를 Navigator에 의해 관리되는 route 스택에 추가
            // Navigator.push(
            //   context,
            //   MaterialPageRoute(builder: (context) => SecondRoute()),
            // );
 
            // Named route를 사용하여 두 번째 화면으로 전환
            Navigator.pushNamed(context, '/second');
          },
        ),
      ),
    );
  }
}
 
class SecondRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Second Route"),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            // pop() 메서드는 route 스택에서 현재 Route를 제거
            Navigator.pop(context);
          },
          child: Text('Go back!'),
        ),
      ),
    );
  }
}

 

 

 

화면 전환 코드가 포함된 예제이다.

import 'package:flutter/material.dart';
import 'package:myapp/listview_demo1.dart';
 
class CallsfloatBtn extends StatefulWidget {
  const CallsfloatBtn({Key? key}) : super(key: key);
 
  @override
  _CallsfloatBtnState createState() => _CallsfloatBtnState();
}
 
class _CallsfloatBtnState extends State<CallsfloatBtn> {
  int _counter = 0;
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center, // 중앙정렬
          children: <Widget>[
            Text(
              '당신은 버튼을 눌렀어요 :',
              softWrap: false// 텍스트가 영역을 넘어갈 경우 줄바꿈 여부
              style: Theme.of(context).textTheme.headline5,
            ),
            Text(
              '$_counter 번',
              style: TextStyle(fontSize: 25),
            ),
            ElevatedButton(
              child: Text('화면 이동'),
              onPressed: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(builder: (context) => ListView_Demo1())
                );
              },
              style: ButtonStyle(
                  backgroundColor: MaterialStateProperty.all(Colors.teal),
                  padding: MaterialStateProperty.all(EdgeInsets.all(15)),
                  textStyle: MaterialStateProperty.all(TextStyle(fontSize: 14))),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
 
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
}
 

 

 

import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
 
// Android Studio 에서 stless 를 입력후 엔터키를 치면
// 자동완성 코드가 작성되고 클래스명을 지정하면 된다.
class ListView_Demo1 extends StatelessWidget {
  const ListView_Demo1({Key? key}) : super(key: key);
 
  // ListView 위젯 정적 데모
  static const List<String> _data = [
    'Mercury(수성)',
    'Venus(금성)',
    'Earth(지구)',
    'Mars(화성)',
    'Jupiter(목성)',
    'Saturn(토성)',
    'Uranus(천왕성)',
    'Neptune(해왕성)',
    'Pluto(명왕성)',
  ];
 
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: CupertinoNavigationBar(
          leading: CupertinoNavigationBarBackButton(
            onPressed: () => Navigator.of(context).pop(),
          ),
          middle: Text('쿠퍼티노 UI'),
          trailing: GestureDetector(
            child: Icon(CupertinoIcons.search),
            onTap: (){},
          ),
          backgroundColor: CupertinoColors.activeGreen,
          border: Border(
              bottom:BorderSide(
                  width: 1,
                  color: CupertinoColors.activeGreen,
                  style: BorderStyle.solid
              )
          ),
        ),
        //body: _buildStaticListView(),
        body: _buildStaticListViewSeperated(),
      ),
    );
  }
 
  Widget _buildStaticListView() {
    return ListView.builder(
      itemCount: _data.length,
      itemBuilder: (BuildContext _context, int i) {
        return ListTile(
          title: Text(_data[i],
              style: TextStyle(
                fontSize: 23,
              )),
          trailing: Icon(
            Icons.favorite_border,
          ),
        );
      },
    );
  }
 
  Widget _buildStaticListViewSeperated() {
    return ListView.separated(
      itemCount: _data.length,
      itemBuilder: (BuildContext _context, int i) {
        return ListTile(
          title: Text(_data[i],
              style: TextStyle(
                fontSize: 23,
              )),
          trailing: Icon(
            Icons.favorite_border,
          ),
        );
      },
      separatorBuilder: (BuildContext _context, int i) {
        return const Divider();
      },
    );
  }
}
 
블로그 이미지

Link2Me

,