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

Class Fruits의 인스턴스를 생성하고 a.list 의 값을 확인해보자.

참조변수 b 에 a.list 를 할당하는 것은 shallow copy(얕은 복사)로 같은 메모리 주소를 바라보고 있어 동일한 값을 가진다.

참조변수 b에 새로운 값을 할당하고 나서 a.list 를 출력해보면 새로 추가한 수박이 반영되어 있는 걸 확인할 수 있다.

 

깊은 복사 방법은

- toList()를 추가하여 새로운 배열을 만드는 방법,

- 전개 연산자(...)를 사용하는 방법,

- map()함수를 활용하는 방법

이 있다.

class Fruits {
  List<String> _list = ['사과','복숭아','배','감','호두','자두'];
  List<String> get list => _list;
}
 
void main(){
  final a = Fruits();
  print(a.list); // [사과, 복숭아, 배, 감, 호두, 자두]
  List<String> b = a.list;
  print(b);      // [사과, 복숭아, 배, 감, 호두, 자두]
  print(a.list == b); // true 얕은 복사(동일한 메모리 주소를 가짐)
 
  b.add('수박');
  print(a.list); // [사과, 복숭아, 배, 감, 호두, 자두, 수박]
 
  List<String> c = a.list.toList(); // 새로운 배열
  print(c == a.list); // fasle 깊은 복사(deep copy)
  print(c); // [사과, 복숭아, 배, 감, 호두, 자두, 수박]
  /***
   * 깊은 복사란, 값이 동일한 객체를 새롭게 생성하는 것을 의미한다.
   */
 
  List<String> d = [...a.list]; // 전개 연산자를 활용하여 새로운 배열
  print(d == a.list); // false 깊은 복사
  print(d); // [사과, 복숭아, 배, 감, 호두, 자두, 수박]
 
  List<String> e = a.list.map((e) => e).toList();
  // map은 배열을 순환하며 값을 변경할 수 있는 함수이다.
  // map()은 Iterable을 반환한다.
  print('-------------------------------');
  print(e); // [사과, 복숭아, 배, 감, 호두, 자두, 수박]
  print(e == a.list); // false. 깊은 복사
 
  c.add('살구');
  print(c);      // [사과, 복숭아, 배, 감, 호두, 자두, 수박, 살구]
  print(a.list); // [사과, 복숭아, 배, 감, 호두, 자두, 수박]
 
  d.add('포도');
  a.list.remove('복숭아');
  print(d);      // [사과, 복숭아, 배, 감, 호두, 자두, 수박, 포도]
  print(a.list); // [사과, 배, 감, 호두, 자두, 수박]
 
}

 

 

그리고 copyWith를 활용하는 깊은 복사 방법을 알아보자.

class Person {
  final String name;
  final int age;
 
  Person({
    required this.name,
    required this.age,
  });
 
  Person copyWith({
    String? name,
    int? age,
  }) {
    return Person(
      name: name ?? this.name,
      age: age ?? this.age,
    );
  }
 
  @override
  String toString() {
    return 'Person{name: $name, age: $age}';
  }
}
 
void main(){
  final a = Person(name: '홍길동', age: 33);
  final b = Person(name: '홍길동', age: 33);
  print(a == b); // fasle. 객체 생성할 때마다 메모리 주소가 새로 할당된다.
 
  final d = a;
  print(d == a); // true 얕은 복사(동일한 메모리 주소를 가짐)
 
  final e = a.copyWith(); // 깊은 복사
  print(a.toString()); // Person{name: 홍길동, age: 33}
  print(e.toString()); // Person{name: 홍길동, age: 33}
  print(e == a); // false. 서로 다른 메모리 주소
 
}
 

 

 

 

 

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

Dart 값 비교 - Equatable 패키지  (0) 2024.01.17
Dart cascade notation  (0) 2024.01.16
Dart List.where  (0) 2024.01.16
[Dart 고급] Dart asMap, entries  (0) 2024.01.16
Dart fold 함수  (2) 2023.12.15
블로그 이미지

Link2Me

,
728x90

Operator ==

oop라는 명칭이 생겨난 이유는 모든 클래스가 기본적으로 object를 extend 하기 때문이다.
Object라는 최상위 클래스가 제공해주는 모든 파라미터들을 모든 클래스들이 사용할 수 있다는 의미다.
대부분의 언어에서는 이 Object라는 클래스에 A 인스턴스와 B 인스턴스를 비교하는 알고리즘이 정의되어 있다.
Dart 에서는 operator라는 함수에 정의 되어있고, 이를 override 함으로써 값 비교 알고리즘을 자유롭게 변경할 수 있다.


Hash Code

hashcode 함수는 Map 또는 Set에서 키의 역할을 하게 된다.
Map이나 Set은 키가 중복으로 저장될 수 없기 떄문에 Set, Map의 키로 Object가 저장되었을 때 어떻게 키값을 정의할지가 중요하다.

프로퍼티(변수)가 늘어나면 늘어날수록 operator와 hashcode를 작성하는것이 귀찮아진다.
dart에서는 equatable클래스를 상속받아 이 문제를 해결하고 있다.

Equatable 클래스를 상속받고 props 라는 메소드를 override 해주면 된다.

 

import 'package:equatable/equatable.dart';
 
class A extends Equatable {
  // Equatable 패키지는 불변 객체를 전제로 설계되었다.
  final int a;
  final int b;
 
  const A({
    required this.a,
    required this.b,
  });
 
  @override
  List<Object?> get props => [this.a, this.b];
  // Equatable 클래스를 상속을 받고
  // props 라는 메소드를 override 해주면 된다.
  // props는 비교시 사용하고 싶은 속성을 배열로 반환해주면 된다.
}
 
void main() {
  final a1 = A(a:88, b:53);
  final a2 = A(a:88, b:53);
 
  print(a1 == a2); // true
  print(a1); // A(88,53)
}
 
/***
 * Dart 기본적으로 참조 비교(Reference Equality)를 사용하기 때문에,
 * 가변 객체인 A 인스턴스는 생성할 때 마다 다른 메모리에 할당되어 false를 반환
 * Equatable 패키지를 이용하면 보다 손쉽게 값 비교를 구현할 수 있다.
 */

 

equatable 패키지 설치는 https://pub.dev/packages/equatable 를 참조하면 된다.

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

Dart 얕은 복사(shallow copy), 깊은 복사(deep copy)  (0) 2024.01.17
Dart cascade notation  (0) 2024.01.16
Dart List.where  (0) 2024.01.16
[Dart 고급] Dart asMap, entries  (0) 2024.01.16
Dart fold 함수  (2) 2023.12.15
블로그 이미지

Link2Me

,
728x90

Dart 에 cascade notation(.., ?..) 이 있다.

처음 접하다보니 이게 뭔가 싶어서 찾아보고 적어둔다.

 

아래 예제의 출처는 https://www.educative.io/answers/what-is-dart-cascade-notation  인데, 약간 주석 내용을 포함해서 조금 더 보완했다.

 

class Example{
  var a;
  var b;
 
  void bSetter(b) {
    this.b = b;
  }
 
  void printValues(){
    print(this.a);
    print(this.b);
  }
}
 
void main() {
  //Instantiating two Example objects
  Example eg1 = new Example(); // 변수는 참조(메모리주소)를 저장한다.
  Example eg2 = new Example();
 
  //Using the .. operator for operations on Example object
  print("Example 1 results:");
  eg1
    ..a = 88
    ..bSetter(53)
    ..printValues();
 
  //The same operations as above but without the .. operator
  print("Example 2 results:");
  eg2.a = 88;
  eg2.bSetter(53);
  eg2.printValues();
 
  // 위 2개는 출력된 결과는 동일하다.
 
  print(eg1 == eg2); // false (메모리 주소 비교했는데 메모리 주소가 다르다.)
  /***
   * 배열은 가변 객체(Mutable Object), Custom Class 도 가변 객체
   * 객체 생성시, 가변 객체는 항상 새로운 메모리를 할당하고,
   * 불변 객체(Immutable Object)는 값이 동일하다면 기존에 생성한 객체를 재활용한다.
   * 불변 객체로 String, int, double, bool, const로 선언된 객체 등이 있다.
   * const는 컴파일 타임에 고정 값인 객체 앞에만 선언할 수 있다.
   * 컴파일 타임 : 앱 실행 전 소스코드를 기계어로 변환하는 시점
   */
}

 

cascade notation(.., ?..) 을 사용하면 같은 객체에 대해 일련의 작업을 수행할 수 있다. 

인스턴스 멤버에 접근하는 것 외에도 같은 객체에서 인스턴스 메서드를 호출할 수도 있다. 

이렇게 하면 임시 변수를 만드는 단계가 줄어들고 더 유동적인 코드를 작성할 수 있다.

 

 
  final fruits = Fruits();
 
  fruits.insert('사과');
  fruits.insert('수박');
  fruits.insert('오이');
  fruits.insert('참외');
  fruits.insert('딸기');
  fruits.insert('배');
  fruits.insert('감');
 
  // 아래와 같이 캐스케이드 연산자를 사용해서 값을 추가할 수도 있다.
  Fruits()
    ..insert('사과')
    ..insert('수박')
    ..insert('오이')
    ..insert('참외')
    ..insert('딸기')
    ..insert('배')
    ..insert('감');

 

하지만 위의 예시는 잘못된 결과가 반환된다.

왜? Custom Class 객체를 생성할 때마다 메모리 주소가 다르기 때문이다.

 

  fruits
    ..insert('사과')
    ..insert('수박')
    ..insert('오이')
    ..insert('참외')
    ..insert('딸기')
    ..insert('배')
    ..insert('감');

 

위와 같이 해야 정상적인 결과를 반환한다.

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

Dart 얕은 복사(shallow copy), 깊은 복사(deep copy)  (0) 2024.01.17
Dart 값 비교 - Equatable 패키지  (0) 2024.01.17
Dart List.where  (0) 2024.01.16
[Dart 고급] Dart asMap, entries  (0) 2024.01.16
Dart fold 함수  (2) 2023.12.15
블로그 이미지

Link2Me

,
728x90

Flutter 코드 구현시 자주 사용하는 기능이라 적어둔다.

void main1(){
  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)
}

 

 

 

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

Dart 값 비교 - Equatable 패키지  (0) 2024.01.17
Dart cascade notation  (0) 2024.01.16
[Dart 고급] Dart asMap, entries  (0) 2024.01.16
Dart fold 함수  (2) 2023.12.15
Dart mixin  (0) 2023.12.11
블로그 이미지

Link2Me

,
728x90

Class Model 생성할 때 key 가 되는 id 가 없이 구현될 경우를 살펴보고자 한다.

값을 수정하거나, 삭제할 때 index 로 삭제해야 한다.

Model 에는 index 가 없기 때문에 asMap 으로 변환하여 index 기준으로 수정, 삭제할 수 있다.

 

Custom Model 대신에 간단한 테스트를 위해 String 으로 대체했다.

Riverpod 용 Provider 로 구현 시에는 _list 대신에 state 로 변경하면 된다.

 

asMap()을 사용하면 Map으로 형변환이 가능하다.

Map 에서 .keys를 사용하면 key 값을, .values를 사용하면 value 값을 가져온다.

map.keys 는 Iterable 객체로 반환되기 때문에 .toList()를 사용해서 변형해줘야 List 객체가 된다.

asMap().entries.map 을 사용하면 새로운 리스트가 만들어진다. entry 조건을 비교하여 값을 update할 수 있다.

class Fruits {
  List<String> _list = const [];
 
  List<String> get list => _list;
 
  void insert(String newItem) {
    _list = [..._list, newItem];
  }
 
  void update(int selectedIndex, String newItem) {
    _list = _list
        .asMap()
        .entries
        .map((entry) => selectedIndex == entry.key ? newItem : entry.value)
        .toList();
  }
 
  void delete(List<String> deleteList) {
    _list = _list.where((item) => !deleteList.contains(item)).toList();
  }
 
  void deleteIndex(int index) {
    _list = List.from(_list)..removeAt(index);
  }
}
 
void main() {
  final fruits = Fruits();
 
  fruits.insert('사과');
  fruits.insert('수박');
  fruits.insert('오이');
  fruits.insert('참외');
  fruits.insert('딸기');
  fruits.insert('배');
  fruits.insert('감');
 
  print(fruits.list); // [사과, 수박, 오이, 참외, 딸기, 배, 감]
  final map = fruits.list.asMap();
  print(map); // {0: 사과, 1: 수박, 2: 오이, 3: 참외, 4: 딸기, 5: 배, 6: 감}
 
  final map_keys = map.keys.toList(); // List 객체
  print(map_keys); // [0, 1, 2, 3, 4, 5, 6]
  print(map.keys); // Iterable 객체 - (0, 1, 2, 3, 4, 5, 6)
  print(map.values); // Iterable 객체 - (사과, 수박, 오이, 참외, 딸기, 배, 감)
 
  print(map.keys is Iterable); // true
  print(map.keys.toList() is List); // true
 
  int selectedIndex = 2;
  fruits.update(selectedIndex, '포도');
  print(fruits.list); // [사과, 수박, 포도, 참외, 딸기, 배, 감]
 
  List<String> deleteList = ['참외''배'];
  fruits.delete(deleteList);
  print(fruits.list); // [사과, 수박, 포도, 딸기, 감]
 
  fruits.deleteIndex(2);
  print(fruits.list); // [사과, 수박, 딸기, 감]
 
  fruits.insert('자두');
  fruits.insert('살구');
  fruits.insert('복숭아');
  print(fruits.list); // [사과, 수박, 딸기, 감, 자두, 살구, 복숭아]
}
 

 

 

 

 

 

 

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

Dart cascade notation  (0) 2024.01.16
Dart List.where  (0) 2024.01.16
Dart fold 함수  (2) 2023.12.15
Dart mixin  (0) 2023.12.11
getter & setter  (0) 2023.12.09
블로그 이미지

Link2Me

,
728x90

Riverpod 가 좋은 상태관리 라이브러리 이지만 초보자에겐 어렵다는 것에 공감한다.

하나씩 이해하기 위해서 기록하고 수정 보완하려고 한다.

 

Flutter riverpod 자동 생성하는 방법이다.

# 상태관리툴 라이브러리 riverpod 추가
flutter pub add flutter_riverpod riverpod_annotation
flutter pub add -d riverpod_generator

 

 

@riverpod 를 어노테이션을 사용하여 riverpod 자동 생성을 할 수 있다.

함수로 정의하는 경우와 클래스로 정의하는 경우가 있는데, 클래스로 정의하는 경우를 더 많이 사용할 거 같다.

import 'package:riverpod_annotation/riverpod_annotation.dart';
 
part 'sample_provider.g.dart';
 
/***
 * 1) 어떤 Provider를 사용할지 결정할 고민 필요없도록
 * 2) Parameter > Family 파라미터를 일반 함수처럼 사용할 수 있도록
 *
 * 함수명 gState 에서 첫글자를 대문자로 변경하고, Ref 를 붙인다. GStateRef ref
 * 함수 정의 이후  dart run build_runner build 명령어를 터미널 창에서 실행한다.
 */
 
@riverpod
String gState(GStateRef ref) {
  return 'Code Generation Riverpod';
}
 
// final _gStateProvider = Provider<String>((ref) => 'Code Generation Riverpod');
 
@riverpod
int gStateMultiply(GStateMultiplyRef ref, {
  required int number1,
  required int number2,
}){
  return number1 * number2;
}
 
@riverpod
class GNotifier extends _$GNotifier {
  @override
  int build() {
    return 0;
  }
 
  increment(){
    state++;
  }
 
  decrement() {
    state--;
  }
}
 
@riverpod
class MyNotifier extends _$MyNotifier {
  @override
  List<String> build() {
    return [];
  }
 
  void addString(String stringToAdd) {
    state = [...state, stringToAdd];
  }
}

 

함수처럼 정의하여 사용하는 경우와 Class 로 정의하여 사용하는 경우를 예시하고 있다.

 

 

자동으로 생성하지 않고 직접 구현하는 경우의 코드.

import 'package:riverpod_annotation/riverpod_annotation.dart';
 
final myNotifierProvider = NotifierProvider<MyNotifier, List<String>>(MyNotifier.new);
 
class MyNotifier extends Notifier<List<String>> {
  // List<String> 이 state 를 의미하고, 반환타입이다.
  @override
  List<String> build() {
    return []; // 상태(state) 초기값 정의
  }
 
  void addString(String stringToAdd){
    state = [...state, stringToAdd];
  }
}

 

자동으로 생성한 코드

아래 코드와 위의 코드는 서로 같은 결과를 반환한다.

import 'package:riverpod_annotation/riverpod_annotation.dart';
 
part 'my_provider.g.dart';
 
@riverpod
class MyNotifier extends _$MyNotifier {
  @override
  List<String> build() {
    return [];
  }
 
  void addString(String stringToAdd) {
    state = [...state, stringToAdd];
  }
}

 

 

UI에 출력하여 결과를 확인해보자.

최상위에 ProviderScope를 지정하여 project 전반에 프로바이더 선언/접근을 가능하게 한다.

void main() {
  runApp(
    ProviderScope(
      child: const MyApp(),
    ),
  );
}
 
class MyApp extends StatelessWidget {
  const MyApp({super.key});
 
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Riverpod Autho Generation Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: HomeScreen(),
    );
  }
}
 

 

 

Riverpod를 읽기 위해서는 ref 객체가 필요하다.
ref 객체는 일반적으로 사용하는 StatelessWidget 에서는 얻을 수 없고, ConsumerWidget을 사용하거나 사용하고 싶은 위젯 부분에서 Consumer 위젯으로 감싸면 ref 객체를 얻을 수 있다.
build() 메서드 내부에 선언한 listOfString 변수는 ref 객체의 watch 메서드를 사용하여 myNotifierProvider의 List<String> 타입의 상태값을 받아온다.
myNotifierProvider의 상태가 변할 때마다 감지할 수 있고 위젯을 리빌드 할 수 있다.

import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_v2/provider/sample_provider.dart';
 
class HomeScreen extends ConsumerWidget {
  HomeScreen({Key? key}) : super(key: key);
 
  Random random = new Random();
 
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final listOfString = ref.watch(myNotifierProvider) as List;
 
    ref.listen<List>(myNotifierProvider, (List? prevState, List newState) {
      print('This function have been called');
    });
 
    return Scaffold(
      appBar: AppBar(
        title: Text('Riverpod Sample'),
        centerTitle: true,
        actions: [
          IconButton(
            onPressed: () {
              ref
                  .read(myNotifierProvider.notifier)
                  .addString('string ${random.nextInt(100)}');
            },
            icon: const Icon(Icons.add),
          ),
        ],
      ),
      body: Center(
        child: Column(
          children: [
            ...listOfString.map(
              (string=> Text(string),
            ),
          ],
        ),
      ),
    );
  }
}

 

 

자동으로 생성하는 CRUD 예제를 더 보강할 예정이다.

Riverpod 를 생성하는 기본 사항을 알고 나면, Dart 언어를 잘 다루는 것이 중요하다는 걸 많이 느끼고 있다.

Dart asMap 에 대한 Dart 문법은 https://link2me.tistory.com/2372 를 참조하면 도움된다.

블로그 이미지

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

flutter test
00:08 +0 -1: C:/Workspace/Flutter/note_app/test/data/data_source/note_db_helper_test.dart: db test [E]
  SqfliteFfiException(error, Invalid argument(s): Failed to load dynamic library 'sqlite3.dll': error code 126}) DatabaseException(Invalid argument(s): Failed to load dynamic library 'sqlite3.dll': error code 126)
  package:sqflite_common_ffi/src/method_call.dart 125:9  responseToResultOrThrow
  package:sqflite_common_ffi/src/isolate.dart 33:12      SqfliteIsolate.handle

 

윈도우 환경에서 테스트 하니까 위와 같은 메시지가 출력되었다.

프로젝트 root 폴더에 sqlite3.dll 파일을 복사하여 넣으면 해결된다.

 

sqlite3.dll
2.76MB

 

블로그 이미지

Link2Me

,
728x90

Flutter Provider로 작성된 상태관리 ViewModel이다.

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
 
class CatViewModel extends ChangeNotifier {
 
  final List<String> _catImages = []; // 고양이 사진을 담을 변수
 
  List<String> _favoriteImages = []; // 좋아요 사진을 담을 변수
 
  List<String> get catImages => _catImages;
 
  List<String> get favoriteImages => _favoriteImages;
 
 Dio _dio = Dio();
 
  // 생성자
  CatViewModel() {
   _dio.interceptors.add(LogInterceptor());
   _dio.interceptors.add(CustLogInterceptor());
    getRandomCatImages();
  }
 
  void getRandomCatImages() async {
    Response resp = await _dio.get(
        'https://api.thecatapi.com/v1/images/search?limit=10&mime_types=jpg');
    print(resp.data);
    for (int i = 0; i < resp.data.length; i++) {
      final map = resp.data[i];
      _catImages.add(map['url']); // url만 추출하여 catImages 에 이미지 추가.
    }
    notifyListeners();
  }
 
  // 좋아요 토글
  void toggleFavoriteImage(String catImage) {
    if (_favoriteImages.contains(catImage)) {
      _favoriteImages.remove(catImage); // 이미 좋아요한 경우 제거
    } else {
      _favoriteImages.add(catImage); // 새로운 사진 추가
    }
 
    notifyListeners(); // 새로고침
  }
}

 

ViewModel 로 클래스명을 정의하기도 하고 Service로 정의하기도 한다.

View 에서 코드를 분리하여 ViewModel에 코드를 대부분 구현하고, View에서는 UI중심으로 코드가 최소화되도록 구현하는 것이 중요하다.

ViewModel 에서는 UI에서 직접적으로 수정할 수 없도록 변수를 선언하는 것이 매우 중요하다.

그래서 위 예제에서는 Class 내부에 있는 변수에 언더바(_)를 붙여서 Private로 정의했고, 외부에서 접근하는 것은 Getter를 추가했다.

기능을 추가할 때 ViewModel 안에 선언한 변수를 외부에서 직접적으로 접근할 수 없도록 신경써야 한다.

 

 

CustLogInterceptor는 Interceptor를 상속받았고 특별하게 코드를 추가한 것은 없다.

아래 코드를 잘 활용하면 유용하게 사용할 수 있다.

import 'package:dio/dio.dart';
 
class CustLogInterceptor extends Interceptor {
 
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
    print('REQUEST[${options.method}] => PATH: ${options.path}');
    return super.onRequest(options, handler);
  }
 
  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) async {
    print('Response_URI >>> ${response.realUri.toString()}');
    return super.onResponse(response, handler);
  }
 
  @override
  void onError(DioError err, ErrorInterceptorHandler handler) {
    print(
      'ERROR[${err.response?.statusCode}] => PATH: ${err.requestOptions.path}',
    );
    return super.onError(err, handler);
  }
}

 

 

HomePage UI 클래스

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class HomePage extends StatelessWidget {
  const HomePage({super.key});
  @override
  Widget build(BuildContext context) {
    return Consumer<CatViewModel>(
      builder: (context, viewModel, child) {
        return Scaffold(
          body: GridView.count(
            mainAxisSpacing: 8,
            crossAxisSpacing: 8,
            crossAxisCount: 2,
            padding: EdgeInsets.all(8),
            children: List.generate(
              viewModel.catImages.length,
              (index) {
                String catImage = viewModel.catImages[index];
                return GestureDetector(
                  onTap: () {
                    viewModel.toggleFavoriteImage(catImage);  // 사진 클릭시
                  },
                  child: Stack(
                    children: [
                      // 사진
                      Positioned.fill(
                        child: Image.network(
                          catImage,
                          fit: BoxFit.cover,
                        ),
                      ),
                      // 좋아요
                      Positioned(
                        bottom: 8,
                        right: 8,
                        child: Icon(
                          Icons.favorite,
                          color: viewModel.favoriteImages.contains(catImage)
                              ? Colors.amber
                              : Colors.transparent,
                        ),
                      ),
                    ],
                  ),
                );
              },
            ),
          ),
        );
      },
    );
  }
}

 

 

 

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

Flutter riverpod 자동 생성  (0) 2024.01.15
Flutter Riverpod 예제 : Bucket List  (0) 2023.12.27
Flutter StatefulWidget  (0) 2023.12.13
Flutter Riverpod - NotifierProvider.family  (0) 2023.12.11
Flutter Riverpod - NotifierProvider  (0) 2023.12.11
블로그 이미지

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

,

Flutter Stack

Flutter 앱/Layout 2023. 12. 27. 15:04
728x90

이미지 위에 또다른 이미지를 겹쳐서 보이도록 하고 싶을 때는 Stack 위젯을 사용한다.

 

child: Stack(
  children: [
    // 사진
    Positioned.fill(
      child: Image.network(
        catImage,
        fit: BoxFit.cover,
      ),
    ),
    // 좋아요
    Positioned(
      bottom: 8,
      right: 8,
      child: Icon(
        Icons.favorite,
        color: catService.favoriteImages.contains(catImage)
            ? Colors.amber
            : Colors.transparent,
      ),
    ),
  ],
),
 

 

 

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

Flutter Custom Button  (0) 2022.07.25
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

,
728x90

온라인 강좌를 듣고 Provider 상태관리를 Riverpod 로 변경해보고 있다.

구글 검색으로 많이 나오는 Todo 에 예제와 비슷한 예제이다.

import 'package:bucket_list/view/home_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
 
void main() {
  runApp(
    ProviderScope(
      child: const MyApp(),
    ),
  );
}
 
class MyApp extends StatelessWidget {
  const MyApp({super.key});
 
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: HomePage(),
    );
  }
}

 

import 'package:bucket_list/provider/bucket_provider.dart';
import 'package:bucket_list/view/create_page.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
 
class HomePage extends ConsumerStatefulWidget {
  const HomePage({super.key});
 
  @override
  ConsumerState<HomePage> createState() => _HomePageState();
}
 
class _HomePageState extends ConsumerState<HomePage> {
 
  @override
  Widget build(BuildContext context) {
    final bucketList = ref.watch(bucketProvider);
 
    return Scaffold(
      appBar: AppBar(
        title: Text("버킷 리스트"),
        centerTitle: true,
      ),
      body: bucketList.isEmpty
          ? Center(child: Text("버킷 리스트를 작성해 주세요."))
          : ListView.builder(
        itemCount: bucketList.length// bucketList 개수 만큼 보여주기
        itemBuilder: (context, index) {
          final bucket = bucketList[index]; // index에 해당하는 bucket 가져오기
          return ListTile(
            // 버킷 리스트 할 일
            title: Text(
              bucket.job,
              style: TextStyle(
                fontSize: 24,
                color: bucket.isDone ? Colors.grey : Colors.black,
                decoration: bucket.isDone
                    ? TextDecoration.lineThrough
                    : TextDecoration.none,
              ),
            ),
            // 삭제 아이콘 버튼
            trailing: IconButton(
              icon: Icon(CupertinoIcons.delete),
              onPressed: () {
                // 삭제 버튼 클릭시
                showDeleteDialog(context, index);
              },
            ),
            onTap: () {
              bucket.isDone = !bucket.isDone;
              //bucketService.updateBucket(bucket, index);
              ref.read(bucketProvider.notifier).updateBucket(bucket);
            },
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () async {
          // + 버튼 클릭시 버킷 생성 페이지로 이동
          String? job = await Navigator.push(
            context,
            MaterialPageRoute(builder: (_) => CreatePage()),
          );
        },
      ),
    );
  }
 
  void showDeleteDialog(
      BuildContext context, int index) {
    showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: Text("정말로 삭제하시겠습니까?"),
          actions: [
            // 취소 버튼
            TextButton(
              onPressed: () {
                Navigator.pop(context);
              },
              child: Text("취소"),
            ),
            // 확인 버튼
            TextButton(
              onPressed: () {
                //bucketService.deleteBucket(index);
                ref.read(bucketProvider.notifier).deleteBucket(index);
                Navigator.pop(context);
              },
              child: Text(
                "확인",
                style: TextStyle(color: Colors.pink),
              ),
            ),
          ],
        );
      },
    );
  }
}
 

 

import 'package:bucket_list/provider/bucket_provider.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
 
class CreatePage extends ConsumerStatefulWidget {
  const CreatePage({super.key});
 
  @override
  ConsumerState<CreatePage> createState() => _CreatePageState();
}
 
class _CreatePageState extends ConsumerState<CreatePage> {
  // TextField의 값을 가져올 때 사용합니다.
  TextEditingController textController = TextEditingController();
 
  // 경고 메세지
  String? error;
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("버킷리스트 작성"),
        // 뒤로가기 버튼
        leading: IconButton(
          icon: Icon(CupertinoIcons.chevron_back),
          onPressed: () {
            Navigator.pop(context);
          },
        ),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            // 텍스트 입력창
            TextField(
              controller: textController, // 연결해 줍니다.
              autofocus: true,
              decoration: InputDecoration(
                hintText: "하고 싶은 일을 입력하세요",
                errorText: error,
              ),
            ),
            SizedBox(height: 32),
            // 추가하기 버튼
            SizedBox(
              width: double.infinity,
              height: 48,
              child: ElevatedButton(
                child: Text("추가하기", style: TextStyle(fontSize: 18)),
                onPressed: () {
                  // 추가하기 버튼 클릭시
                  String job = textController.text; // 값 가져오기
                  if (job.isEmpty) {
                    setState(() {
                      error = "내용을 입력해주세요."// 내용이 없는 경우 에러 메세지
                    });
                  } else {
                    setState(() {
                      error = null// 내용이 있는 경우 에러 메세지 숨기기
                    });
 
                    // BucketService 가져오기
                    //BucketService bucketService = context.read<BucketService>();
                    //bucketService.createBucket(job);
                    ref.read(bucketProvider.notifier).createBucket(job);
 
                    Navigator.pop(context, job); // job 변수를 반환하며 화면을 종료합니다.
                  }
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}
 
 

 

/// 버킷 클래스
class Bucket {
  String id;
  String job; // 할 일
  bool isDone; // 완료 여부
 
  Bucket(this.id, this.job, this.isDone); // 생성자
}
 

 

import 'package:bucket_list/model/bucket.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
 
final bucketProvider = NotifierProvider<BucketNotifier, List<Bucket>>(BucketNotifier.new);
 
class BucketNotifier extends Notifier<List<Bucket>> {
  @override
  List<Bucket> build() => [];
 
  /// bucket 추가
  void createBucket(String job){
    state = [...state, Bucket((state.length + 1).toString(),job, false)];
  }
 
  /// bucket 수정
  void updateBucket(Bucket bucket){
    state = [
      for(final item in state)
        if(item.id == bucket.id)
          Bucket(item.id, bucket.job, bucket.isDone)
        else
          item
    ];
  }
 
  /// bucket 삭제
  void deleteBucket(int index){
    //state = state.where((e) => e.id != bucket.id).toList(); // Bucket bucket 인자로 받을 때
    state = List.from(state)..removeAt(index);
  }
}

 

Riverpod Notifier로 구현한 코드와

Provider 로 구현한 BucketService 코드를

비교해서 보면 도움이 될 거 같다.

 

import 'package:burket_list/model/bucket.dart';
import 'package:flutter/material.dart';
 
class BucketService extends ChangeNotifier {
  List<Bucket> bucketList = [];
 
  /// bucket 추가
  void createBucket(String job){
    bucketList.add(Bucket(job, false));
    notifyListeners();
  }
 
  /// bucket 수정
  void updateBucket(Bucket bucket, int index){
    bucketList[index] = bucket;
    notifyListeners();
  }
 
  /// bucket 삭제
  void deleteBucket(int index){
    bucketList.removeAt(index);
    notifyListeners();
  }
}
 
/***
 * 전역적으로 사용되는 데이터를 담당할 서비스를 만들고,
 * 해당 데이터에 대한 CRUD를 모두 해당 서비스에서 구현한다.
 */

 

 

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

Flutter riverpod 자동 생성  (0) 2024.01.15
Flutter Provider 상태관리 예제  (0) 2024.01.06
Flutter StatefulWidget  (0) 2023.12.13
Flutter Riverpod - NotifierProvider.family  (0) 2023.12.11
Flutter Riverpod - NotifierProvider  (0) 2023.12.11
블로그 이미지

Link2Me

,

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

728x90

로그인 구현 함수를 별도로 구현하지 않고, Retrofit 라이브러를 활용하여 간단하게 로그인 하는 코드로 변경했다.

 

import 'package:dio/dio.dart' hide Headers;
import 'package:login_ex/contact/repository/retrofit_url.dart';
import 'package:login_ex/contact/model/contact_result.dart';
import 'package:login_ex/user/model/login_response.dart';
import 'package:retrofit/retrofit.dart';
 
part 'rest_client.g.dart';
 
@RestApi(baseUrl: RetrofitURL.baseUrl)
abstract class RestClient {
  factory RestClient(Dio dio, {String baseUrl}) = _RestClient;
 
  @POST(RetrofitURL.mLogin)
  @FormUrlEncoded()
  Future<LoginResponse> userLogin(
      @Field() String keyword,
      @Field() String userID,
      @Field() String password,
      @Field() String uID,
      @Field() String mfoneNO,
      );
 
  @POST(RetrofitURL.contactData)
  @FormUrlEncoded()
  Future<ContactResult> postContactList(
      @Field() String keyword,
      @Field() String search,
      );
}

로그인 함수를 추가하고 터미널창에서 dart run build_runner build 를 다시 해서 rest_client.g.dart 파일을 업데이트 한다.

 

login_screen.dart 파일 코드

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:platform_device_id_v3/platform_device_id.dart';
import 'package:login_ex/common/repository/logging.dart';
import 'package:login_ex/common/res/data.dart';
import 'package:login_ex/common/utils/utils.dart';
import 'package:login_ex/contact/repository/rest_client.dart';
import 'package:login_ex/contact/view/contact_result_page.dart';
import 'package:login_ex/user/model/login_response.dart';
import 'package:login_ex/common/component/custom_text_form_field.dart';
import 'package:login_ex/common/res/colors.dart';
import 'package:login_ex/common/view/default_layout.dart';
import 'package:login_ex/user/repository/crypto_api.dart';
 
class LoginScreen extends StatefulWidget {
  const LoginScreen({super.key});
 
  @override
  State<LoginScreen> createState() => _LoginScreenState();
}
 
class _LoginScreenState extends State<LoginScreen> {
  String userid = '';
  String password = '';
  late String _deviceId;
  late final RestClient restClient;
 
  @override
  void initState() {
    super.initState();
    getDeviceUniqueId();
   loginRestInit();
  }
 
  Future<void> loginRestInit() async {
    Dio dio = Dio();
    dio.interceptors.add(LogInterceptor());
    dio.interceptors.add(const CustLogInterceptor(storage: storage));
    restClient = RestClient(dio);
  }
 
  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");
    });
  }
 
  @override
  Widget build(BuildContext context) {
    return DefaultLayout(
      child: SafeArea(
        top: true,
        bottom: false,
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              const SizedBox(height: 50.0),
              Image.asset(
                'asset/img/logo/logo.png',
                width: MediaQuery.of(context).size.width ,
              ),
              const SizedBox(height: 16.0),
              CustomTextFormField(
                hintText: '아이디를 입력하세요',
                obscureText: false,
                onChanged: (String value) {
                  userid = value.trim();
                },
              ),
              const SizedBox(height: 16.0),
              CustomTextFormField(
                hintText: '비밀번호를 입력하세요',
                obscureText: true,
                onChanged: (String value) {
                  password = value.trim();
                },
              ),
              const SizedBox(height: 16.0),
              ElevatedButton(
                onPressed: () async {
                  if(userid.isNotEmpty && password.isNotEmpty){
                    final mobileNO = await storage.read(key: MNumber);
 
                    LoginResponse result = await restClient.userLogin(
                        Crypto.AES_encrypt(Crypto.URLkey()),
                        Crypto.AES_encrypt(userid),
                        Crypto.RSA_encrypt(password),
                        _deviceId,
                        mobileNO!
                    );
                    if(result.status.contains('success')){
                      Utils.showSnackBar(context, '로그인 성공');
 
                      Navigator.of(context).push(
                        MaterialPageRoute(
                          builder: (_) => const ContactResultPage(),
                        ),
                      );
 
                    } else {
                      Utils.showAlert(context, result.status, result.message);
                    }
                  } else {
                    if(userid.isEmpty) {
                      Utils.showSnackBar(context, '아이디를 입력하세요');
                      return;
                    }
                    if(password.isEmpty){
                      Utils.showSnackBar(context, '비밀번호를 입력하세요');
                      return;
                    }
                  }
                },
                style: ElevatedButton.styleFrom(
                  backgroundColor: PRIMARY_COLOR,
                ),
                child: const Text(
                  '로그인',
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
 
}

 

 

PHP Session 을 처리하는 핵심코드를 구현하느라고 하루종일 삽질을 엄청했다.

구글링을 해도 제대로 된 것을 찾기가 어려웠고, CookieJar 커스텀으로 해결하려고 했으나 실패했다.

인프런 유료강좌 "[코드팩토리][중급] Flutter 진짜 실전"  강좌에서 언급된 코드가 생각이 나서 Access_Token 처리 대신에 Cookie 를 실어보내는 코드로 구현해보면 되겠다 싶어서 테스트해봤더니 성공했다.

블로그 이미지

Link2Me

,
728x90

플러터에서 로그인은 시도하는 코드를 살펴보자.

 

Android APP 개발시에는 서버 세션을 처리하기 위해서 CookieJar를 Custom 으로 implements 하여 해결을 했다.

Flutter 에서 이 방법으로 해결 시도해보다가 실패하여 포기했다.

아래 주석처리한 CustLogInterceptor 파일을 활용하여 PHP Session 을 FlutterSecureStorage 에 저장하고 Load하여 보내는 방법으로 해결했다.

dio.interceptors.add(CookieManager(cookieJar)); 코드는 사용되지 않으므로 삭제하면 된다.

 

최근에는 2차 인증은 기본으로 적용하고 있다. PHP Session 생성을 하는 위치가 매우 중요하다.

1차 인증 로그인 이후 2차 인증(Google OTP, SMS OTP 등)을 성공하고 난 이후에 PHP Session 을 생성해야 한다.

 

아래 코드는 1차 인증 후 PHP Session 을 생성하는 예제로 구현했다.

import 'package:cookie_jar/cookie_jar.dart';
import 'package:dio/dio.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:login_ex/contact/repository/retrofit_url.dart';
import 'package:login_ex/user/model/login_response.dart';
import 'package:login_ex/user/model/user_request.dart';
 
abstract class UserRepository {
  Future<LoginResponse> login(UserRequest req);
}
 
class LoginService extends UserRepository {
  @override
  Future<LoginResponse> login(UserRequest req) async {
    BaseOptions options = BaseOptions(
      baseUrl: RetrofitURL.baseUrl,
    );
    Dio dio = Dio(options);
    final cookieJar = CookieJar();
    dio.interceptors.add(CookieManager(cookieJar));
    dio.interceptors.add(LogInterceptor());
    //dio.interceptors.add(CustLogInterceptor(storage: storage));
 
    FormData formData = FormData.fromMap({
      "keyword": req.keyword,
      "userID": req.userID,
      "password": req.password,
      "uID": req.uID,
      "mfoneNO": req.mfoneNO
    });
 
    final response = await dio.post(RetrofitURL.mLogin, data: formData);
    if (response.statusCode == 200) {
      LoginResponse result = LoginResponse.fromJson(response.data);
      return result;
    } else {
      return const LoginResponse(status: "fail", message: "fail", userinfo: null);
    }
  }
}
 

 

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:login_ex/common/res/data.dart';
import 'package:login_ex/common/utils/utils.dart';
import 'package:login_ex/contact/view/contact_result_page.dart';
import 'package:login_ex/user/model/login_response.dart';
import 'package:login_ex/user/model/user_request.dart';
import 'package:platform_device_id_v3/platform_device_id.dart';
import 'package:login_ex/common/component/custom_text_form_field.dart';
import 'package:login_ex/common/res/colors.dart';
import 'package:login_ex/common/view/default_layout.dart';
import 'package:login_ex/user/repository/login_service.dart';
import 'package:login_ex/user/repository/crypto_api.dart';
 
class LoginScreen extends StatefulWidget {
  const LoginScreen({super.key});
 
  @override
  State<LoginScreen> createState() => _LoginScreenState();
}
 
class _LoginScreenState extends State<LoginScreen> {
  String userid = '';
  String password = '';
  final UserRepository loginRepository = LoginService();
  late String _deviceId;
 
  @override
  void initState() {
    super.initState();
    getDeviceUniqueId();
  }
 
  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");
    });
  }
 
  @override
  Widget build(BuildContext context) {
    return DefaultLayout(
      child: SafeArea(
        top: true,
        bottom: false,
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              const SizedBox(height: 50.0),
              Image.asset(
                'asset/img/logo/logo.png',
                width: MediaQuery.of(context).size.width ,
              ),
              const SizedBox(height: 16.0),
              CustomTextFormField(
                hintText: '아이디를 입력하세요',
                obscureText: false,
                onChanged: (String value) {
                  userid = value.trim();
                },
              ),
              const SizedBox(height: 16.0),
              CustomTextFormField(
                hintText: '비밀번호를 입력하세요',
                obscureText: true,
                onChanged: (String value) {
                  password = value.trim();
                },
              ),
              const SizedBox(height: 16.0),
              ElevatedButton(
                onPressed: () async {
                  if(userid.isNotEmpty && password.isNotEmpty){
                    final mobileNO = await storage.read(key: MNumber);
 
                    UserRequest req = UserRequest(
                      keyword: Crypto.AES_encrypt(Crypto.URLkey()),
                      userID: Crypto.AES_encrypt(userid),
                      password: Crypto.RSA_encrypt(password),
                      uID: _deviceId,
                      mfoneNO: mobileNO!,
                    );
 
                    LoginResponse result = await loginRepository.login(req);
                    if(result.status.contains('success')){
                      Utils.showSnackBar(context, '로그인 성공');
 
                      Navigator.of(context).push(
                        MaterialPageRoute(
                          builder: (_) => const ContactResultPage(),
                        ),
                      );
 
                    } else {
                      Utils.showAlert(context, result.status, result.message);
                    }
                  } else {
                    if(userid.isEmpty) {
                      Utils.showSnackBar(context, '아이디를 입력하세요');
                      return;
                    }
                    if(password.isEmpty){
                      Utils.showSnackBar(context, '비밀번호를 입력하세요');
                      return;
                    }
                  }
                },
                style: ElevatedButton.styleFrom(
                  backgroundColor: PRIMARY_COLOR,
                ),
                child: const Text(
                  '로그인',
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
 
}

 

로그인을 위한 코드와 로그인 화면은 위와 같다.

로그인을 시도하고 로그인은 성공하고 난 이후의 로그를 살펴보자.

 

위의 코드에서 일부러 loginChk.php 에서 로그인 성공이 되더라도 APP에서 서버의 Session 값을 저장하지 않았다.

 

 

로그를 보면 loginChk.php 파일에서 set-cookie 값이 넘어온 값과 ContactList.php 에서 넘어온 set-cookie 값이 서로 다르다는 걸 확인할 수 있다.

APP에서는 서버에서 전달받은 set-cookie 값을 저장해서 이 값을 header 정보에 다시 실어보내야 서버에서 생성한 Session 일치 여부를 확인할 수 있다.

 

 

loginChk.php 파일은 어떻게 구성되어 있는지 살펴보자.

- 서버와 APP간에 키 정보가 있는지 확인하고, 키가 일치해야만 다음 단계로 진행된다.

- 어떤 userID로 로그인을 시도하는지 모르도록 AES256 암호화/복호화 처리를 했다.

- 비밀번호는 반드시 RSA 암호화/복호화를 해야 보안검증에 통과된다. (기업 사용 용도)

- 아래 예시코드는 완벽한 예제는 아니다.

  개발 테스트용으로 구현한 코드이기 때문에 필요한 사항만 넣었다.

- APP에서 폰 번호를 획득하여, 서버 DB에 있는 인사정보 휴대폰번호와 불일치하는 경우에는

  사용하지 못하도록 처리하는 서버 함수를 구현하면 된다.

<?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']); // userID AES256 복호화
        $password = $c->rsa_decrypt($_POST['password']); // 비밀번호 RSA 복호화 
 
        // 동일 번호로 로그인하면 phoneSE 업데이트 처리(2022.5.30)
        $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;
}
?>
 

 

 

ContactList.php 파일 코드

- 세션 정보가 일치하는지 여부를 체크하는 로직이 추가되어 있다.

- 세부 설명은 아래 코드에 포함된 주석 정보를 보면 이해될 것이다.

<?php
if(!isset($_SESSION)) {
    session_start();
}
 
// 파일을 직접 실행하면 동작되지 않도록 하기 위해서
if(isset($_POST&& $_SERVER['REQUEST_METHOD'== "POST"){
    @extract($_POST); // POST 전송으로 전달받은 값 처리
    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");
 
    // 로그인 이후 생성된 SESSION 정보가 불일치하면 아래코드에서 걸려 이 정보를 전송한다.
    if(!(isset($_SESSION['userID']) && !empty($_SESSION['userID']))){
        $result = array(
            'status' => "Session 불일치",
            'message' => "세션 정보가 일치하지 않습니다.",
            'addrinfo' => null
        );
        echo json_encode($result);
        exit;
    }
 
    //키워드 확인
    if(!isset($_POST['keyword'])){
        $result = array(
            'status' => "key fail",
            'message' => "서버 키 정보가 없습니다.",
            'addrinfo' => null
        );
        echo json_encode($result);
        exit;
    }
 
    $keyword=$c->AES_decrypt($_POST['keyword']);
    //키워드 일치 확인
    if(strcmp($keyword,$mykey)<>0){
        $result = array(
            'status' => "key fail",
            'message' => "서버와 단말의 KEY가 일치하지 않습니다.",
            'addrinfo' => null
        );
        echo json_encode($result);
        exit;
    }
 
    $sql = "select idx,userNM,mobileNO,telNO,photo from Person "// 화면에 출력할 칼럼 발췌
    if(!empty($search)) {
        $sql .= "where userNM LIKE '%".$search."%' or mobileNO LIKE '%".$search."%'";
    }
 
    $R = array(); // 결과 담을 변수 생성
    $result = $c->putDbArray($sql);
    while($row = $result->fetch_assoc()) {
        $photo_path = './photos/'.$row['photo'];
        if(file_exists($photo_path)) { // 실제 사진 이미지 파일이 존재하는 여부 확인
            $row['photo'];
        } else {
            $row['photo']=null;
        }
        $row['checkBoxState'= false;
 
        // 중요한 정보는 AES256 암호화해서 전송해야 하지만 개발용이기 때문에 평문 형태로 전송 처리
        array_push($Rarray("idx"=>$row['idx'],"userNM"=>$row['userNM'],"mobileNO"=>$row['mobileNO'],
                               "telNO"=>$row['telNO'],"photo"=>$row['photo'],"checkBoxState"=>$row['checkBoxState']));
    }
 
    $status = "success";
    $message = "";
    $addrinfo = $R// 전체 ArrayList 데이터
 
    $result = array(
        'status' => $status,
        'message' => $message,
        'addrinfo' => $addrinfo
    );
    echo json_encode($result);
 
}
?>
 

 

여기까지 1단계 과정을 적어둔다.

 

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

Flutter Login with PHP #3 (플러터 세션 처리 코드)  (0) 2023.12.27
Flutter Login with PHP Session #2 (Retrofit)  (0) 2023.12.24
Flutter AES256 with PHP  (0) 2023.12.19
ListView.separated 예제  (0) 2023.11.20
Flutter Login Example  (0) 2022.07.25
블로그 이미지

Link2Me

,

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

728x90

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

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

 

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

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

 

Model 구현

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

 

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

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

 

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

 

Retrofit 라이브러리 구현

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

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

 

 

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

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

 

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

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

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

APP 구현 필수사항

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

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

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

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

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

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

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

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

 

 

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

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

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

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

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

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

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

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

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

 

 

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

 

블로그 이미지

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

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

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

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

 

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

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

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

 

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

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

 

PHP 암호화/복호화 클래스

 

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

 

 

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

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

Link2Me

,