728x90

인터넷(Network)을 통해서 전송된 JSON 포맷 문자열을 Custom Class 로 역직렬화(Deserialization)해서 사용자 클래스로 변환하는 과정을 거쳐야 한다.
사용자 클래스를 네트웍을 통해 전송하기 위해서는 문자열로 변환을 해야 한다.
과거에는 XML 이라는 포멧으로 전송 했으나, 현재는 JSON 포멧 문자열로 전송한다.

Dart 언어에서 역직렬화(Deserialization)하는 과정은 
ㅇ JSON 포맷 String → Map<String, dynamic>
ㅇ Map<String, dynamic> → Custom Class
2단계 과정을 거친다.

Custom Class를 JSON 포멧 문자열로 직렬화(Serialization)하는 과정
ㅇ Custom Class → Map<String, dynamic>
ㅇ Map<String, dynamic> → JSON 포맷 String
2단계 과정을 거친다.

먼저 서버에 있는 JSON 포멧 데이터를 역직렬화하기 위한 Model Class 를 구현해야 한다.

구현 방법은 Model 클래스를 직접 구현하는 방법과 Code generation 라이브러리를 이용하는 방법이 있다.

1. 직접 구현 방법

- 모델 클래스 내부에 fromJson과 toJson을 정의한다.

class LoginResponse {
  final String status;
  final String message;
  final UserInfo? userinfo;
 
  const LoginResponse({
    required this.status,
    required this.message,
    this.userinfo,
  });
 
  factory LoginResponse.fromJson(Map<String, dynamic> json) {
    return LoginResponse(
      status: json['status'] as String,
      message: json['message'] as String,
      userinfo: json['userinfo'== null
          ? null
          : UserInfo.fromJson(json['userinfo'] as Map<String, dynamic>),
    );
  }
 
  Map<String, dynamic> toJson() {
    return {
      "status"this.status,
      "message"this.message,
      "userinfo"this.userinfo,
    };
  }
}
 
class UserInfo {
  final String userNM;
  final String mobileNO;
  final int profileImg;
 
  const UserInfo({
    required this.userNM,
    required this.mobileNO,
    required this.profileImg,
  });
 
  factory UserInfo.fromJson(Map<String, dynamic> json) {
    return UserInfo(
      userNM: json['userNM'] as String,
      mobileNO: json['mobileNO'] as String,
      profileImg: json['profileImg'] as int,
    );
  }
 
  Map<String, dynamic> toMap() {
    return {
      'userNM'this.userNM,
      'mobileNO'this.mobileNO,
      'profileImg'this.profileImg,
    };
  }
 
}

 

2. Code generation 라이브러리 이용방법

- 라이브러리 설치

flutter pub add json_annotation
flutter pub add -d build_runner json_serializable

 

- 모델 클래스 내부에 fromJson과 toJson을 아래와 같은 규칙으로 정의한다.

import 'package:json_annotation/json_annotation.dart';
 
part 'userinfo.g.dart';
 
@JsonSerializable()
class UserInfo {
  final String userNM;
  final String mobileNO;
  final int profileImg;
 
  const UserInfo({
    required this.userNM,
    required this.mobileNO,
    required this.profileImg,
  });
 
  factory UserInfo.fromJson(Map<String, dynamic> json) => _$UserInfoFromJson(json);
 
  Map<String, dynamic> toJson() => _$UserInfoToJson(this);
}
 

 

import 'package:json_annotation/json_annotation.dart';
import 'package:login_ex/user/model/userinfo.dart';
 
part 'login_response.g.dart';
 
@JsonSerializable()
class LoginResponse {
  final String status;
  final String message;
  final UserInfo? userinfo;
 
  const LoginResponse({
    required this.status,
    required this.message,
    this.userinfo,
  });
 
  factory LoginResponse.fromJson(Map<String, dynamic> json) => _$LoginResponseFromJson(json);
 
  Map<String, dynamic> toJson() => _$LoginResponseToJson(this);
}
 

 

와 같이 한 다음에 터미널 창에서 dart run build_runner build 를 하면 자동으로 추가된다.

 

 

  /// 네트워크 응답 문자열
  String jsonString = '{"name": "길동", "age": 33}';
 
  /// JSON 포맷 String -> Map<String, dynamic>
  Map<String, dynamic> jsonMap = jsonDecode(jsonString);
  print(jsonMap);
 
  /// Map<String, dynamic> -> Person
  Person person = Person.fromJson(jsonMap);
  print(person);

 

일반적인 JSON decoding에는 dart:convert의 jsonDecode() 메서드를 사용한다. 

이 방법은 간단하고 빠르게 decoding할 수 있다는 장점이 있지만 decoding된 Map<String, dynamic> 타입을 다시 적절한 Data Type으로 변환하는 부분을 수작업으로 진행해야 한다.

 

하지만 위와 같은 과정을 처리하지 않고,

서버로 데이터를 보내고 결과를 응답받는 과정을 손쉽게 처리하는 방법은 Retrofit 라이브러리를 이용하면 된다.

import 'package:dio/dio.dart' hide Headers;
import 'package:retrofit/retrofit.dart';
import 'package:login_ex/common/repository/retrofit_url.dart';
import 'package:login_ex/contact/model/contact_result.dart';
import 'package:login_ex/user/model/login_response.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,
      );
}

 

Retrofit 라이브러리 동작과정에 대한 설명은 Android 에서 설명한 게시글이 있으니 참고하면 도움된다.

로그인에 필요한 데이터를 POST 방식으로 전송하고, 결과를 LoginResponse 로 받는다.

 

riverpod 상태관리 패키지 활용 예제

- Dio 라이브러리와 Retrofit 을 처리하는 RestClient 를 보고 활용하면 된다.

- CustLogInterceptor는 필요하면 추가하지만 필요없는 경우에는 추가할 필요가 없다.

- 서버에서 로그인 정보가 일치하면 Session 정보를 생성하고 생성된 Session 정보를 쿠키로 저장하고 다른 URL 전송시 Cookie 정보를 실어서 보내도록 처리하는 로직을 CustLogInterceptor에 구현했다.

import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:login_ex/common/repository/custlog_interceptor.dart';
import 'package:login_ex/common/repository/rest_client.dart';
 
final secureStorageProvider =
    Provider<FlutterSecureStorage>((ref) => const FlutterSecureStorage());
 
final dioProvider = Provider<Dio>((ref) {
    final dio = Dio();
 
    final storage = ref.read(secureStorageProvider);
 
    dio.interceptors.add(LogInterceptor());
    dio.interceptors.add(
        CustLogInterceptor(storage: storage,),
    );
    return dio;
});
 
final restClientProvider = Provider<RestClient>((ref) {
    final dio = ref.read(dioProvider);
    final repository = RestClient(dio);
    return repository;
});

 

 

 

LoginView 처리는 View 와 ViewModel를 분리하여 구현한다.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:login_ex/common/view/base_view.dart';
import 'package:login_ex/user/view/login_view_model.dart';
 
class LoginView extends ConsumerWidget {
  String userid = '';
  String password = '';
 
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return BaseView(
      viewModelProvider: loginViewModelProvider,
      builder: (ref, viewModel, state) => DefaultLayout(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              const SizedBox(height: 50.0),
              Image.asset(
                'assets/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: () => viewModel.login(context, userid, password),
                style: ElevatedButton.styleFrom(
                  backgroundColor: PRIMARY_COLOR,
                ),
                child: const Text(
                  '로그인',
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
 

 

final loginViewModelProvider =
    NotifierProvider.autoDispose<LoginViewModel, LoginViewState>(
        LoginViewModel.new);
 
class LoginViewModel extends BaseViewModel<LoginViewState> {
  @override
  LoginViewState build() {
    return LoginViewState(isBusy: false);
  }
 
  Future<void> login(
      BuildContext context, String userid, String password) async {
    if (userid.isNotEmpty && password.isNotEmpty) {
      state = state.copyWith(isBusy: true);
 
      final storage = ref.read(secureStorageProvider);
      final mobileNO = await storage.read(key: MNumber) ?? '';
      final deviceId = await storage.read(key: DeviceID) ?? '';
 
      LoginResponse result = await ref.read(restClientProvider).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 ContactResultProviderPage(),
          ),
        );
        state = state.copyWith(isBusy: false);
      } else {
        Utils.showAlert(context, result.status, result.message);
        state = state.copyWith(isBusy: false);
      }
    } else {
      if (userid.isEmpty) {
        Utils.showSnackBar(context, '아이디를 입력하세요');
        state = state.copyWith(isBusy: false);
        return;
      }
      if (password.isEmpty) {
        Utils.showSnackBar(context, '비밀번호를 입력하세요');
        state = state.copyWith(isBusy: false);
        return;
      }
    }
 
  }
}
 

 

상속받은 BaseView 에 대한 코드는 제가 직접 구현한 것이 아니기 때문에 오픈할 수 없다.

riverpod 기반 BaseView 구현 코드를 얻고 싶은 분은 인프런 사이트 강좌 DevStory님의 "Flutter 앱 개발 실전" 강좌 수강을 추천한다. Flutter 에 대한 기본 개념을 확실하게 잡을 수 있고, 이를 바탕으로 응용하는데 도움이 될 것이다.

블로그 이미지

Link2Me

,