728x90

1. 네이버 지도 띄우기 위한 첫번째 단계

    flutter pub add flutter_naver_map permission_handler 를 터미널 창에서 실행하여 dependencies 에 추가한다.

 

2. 프로젝트명 생성

import 'package:flutter/material.dart';
import 'package:flutter_naver_map/flutter_naver_map.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:nmap_test/presentation/view/naver_map_view.dart';
 
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
 
  await NaverMapSdk.instance.initialize(
      clientId: 'z4t12347e4',
      onAuthFailed: (error) {
        print('Auth failed: $error'); // 인증 실패시 메시지 출력
      });
 
  runApp(
    ProviderScope(
      child: const MyApp(),
    ),
  );
}
 
class MyApp extends StatelessWidget {
  const MyApp({super.key});
 
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'NaverMap View',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: NaverMapScreen(),
    );
  }
}
 

 

clientId 는 임의로 변경했기 때문에 그대로 넣으면 동작되지 않는다.

각자 ncloud.com 에 등록해서 사용해야 동작된다.

 

import 'package:flutter/material.dart';
import 'package:flutter_naver_map/flutter_naver_map.dart';
import 'package:nmap_test/core/component/default_layout.dart';
import 'package:nmap_test/core/res/palette.dart';
import 'package:permission_handler/permission_handler.dart';
 
class NaverMapScreen extends StatefulWidget {
  const NaverMapScreen({Key? key}) : super(key: key);
 
  @override
  State<NaverMapScreen> createState() => _NaverMapScreenState();
}
 
class _NaverMapScreenState extends State<NaverMapScreen> {
 
  @override
  void initState() {
    super.initState();
    _permission();
  }
 
  void _permission() async {
    var requestStatus = await Permission.location.request();
    var status = await Permission.location.status;
    if (requestStatus.isPermanentlyDenied || status.isPermanentlyDenied) {
      openAppSettings();
    }
  }
 
  @override
  Widget build(BuildContext context) {
    return DefaultLayout(
      title: 'Naver Map',
      appbarBgColor: Palette.primary,
      child: NaverMap(
          options: const NaverMapViewOptions(locationButtonEnable: true),
          onMapReady: (controller) {
            print('네이버맵 로딩됨');
          },
        ),
      );
  }
}
 

locationButtonEnable: true 로 설정하여 지도 왼쪽 하단에 현재 위치로 이동하는 버튼이다.

위험권한을 추가하여 현재 위치 파악을 할 수 있다.

 

 

3. ncloud.com 에 앱 등록

   https://link2me.tistory.com/1832 를 참조하여 앱을 등록한다.

 

4. Android 설정

- build.gradle

  compileSdkVersion 34

  minSdkVersion 23 으로 변경

android {
    namespace "com.link2me.flutter.nmap_test"
    compileSdkVersion 34
    ndkVersion flutter.ndkVersion
 
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
 
    kotlinOptions {
        jvmTarget = '1.8'
    }
 
    sourceSets {
        main.java.srcDirs += 'src/main/kotlin'
    }
 
    defaultConfig {
        applicationId "com.link2me.flutter.nmap_test"
        minSdkVersion 23
        targetSdkVersion flutter.targetSdkVersion
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }
 
    buildTypes {
        release {
            signingConfig signingConfigs.debug
        }
    }
}

 

- AndroidManifest.xml 파일 추가 사항

  퍼미션 추가 및 meta-data 추가

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
 
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
 
    <application
        android:label="nmap_test"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
 
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
 
        <meta-data
            android:name="com.naver.maps.map.CLIENT_ID"
            android:value="z4t12347e4" />
    </application>
</manifest>

 

 

5. ios 설정

  기본 테스트는 Windows 환경에서 하고, 나중에 맥북에서 ios 테스트를 하는 순서로 진행 한다.

  아이폰에서 단순 지도 띄우는 것은 추가 사항 없어도 동작됨을 확인했다.

  현재 위치 정보를 가져오기 위해서는 권한 설정이 필요하다.

 

Info.plist 하단 dict안에 추가할 사항

1
2
3
4
5
6
7
8
9
10
    <!-- Permission options for the `location` group -->
    <key>NSLocationWhenInUseUsageDescription</key>
    <string>Need location when in use</string>
    <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
    <string>Always and when in use!</string>
    <key>NSLocationUsageDescription</key>
    <string>Older devices need location.</string>
    <key>NSLocationAlwaysUsageDescription</key>
    <string>Can I have location always?</string>

 

 

Podfile 추가사항

파일 하단에서 아래 부분에 코드를 추가한다.

권한설정은 Info.plist 와 Podfile 을 모두 설정해야 정상 동작되고, 앱에 권한 설정하는 기능이 추가된다.

post_install do |installer|
  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)
 
    target.build_configurations.each do |config|
      # You can remove unused permissions here
      # for more information: https://github.com/BaseflowIT/flutter-permission-handler/blob/master/permission_handler/ios/Classes/PermissionHandlerEnums.h
      # e.g. when you don't need camera permission, just add 'PERMISSION_CAMERA=0'
      config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
        '$(inherited)',
 
        ## dart: PermissionGroup.calendar
        #'PERMISSION_EVENTS=1',
 
        ## dart: PermissionGroup.calendarFullAccess
        #'PERMISSION_EVENTS_FULL_ACCESS=1',
 
        ## dart: PermissionGroup.reminders
        #'PERMISSION_REMINDERS=1',
 
        ## dart: PermissionGroup.contacts
        #'PERMISSION_CONTACTS=1',
 
        ## dart: PermissionGroup.camera
        #'PERMISSION_CAMERA=1',
 
        ## dart: PermissionGroup.microphone
        #'PERMISSION_MICROPHONE=1',
 
        ## dart: PermissionGroup.speech
        #'PERMISSION_SPEECH_RECOGNIZER=1',
 
        ## dart: PermissionGroup.photos
        #'PERMISSION_PHOTOS=1',
 
        ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse]
        'PERMISSION_LOCATION=1',
 
        ## dart: PermissionGroup.notification
        #'PERMISSION_NOTIFICATIONS=1',
 
        ## dart: PermissionGroup.mediaLibrary
        #'PERMISSION_MEDIA_LIBRARY=1',
 
        ## dart: PermissionGroup.sensors
        #'PERMISSION_SENSORS=1',
 
        ## dart: PermissionGroup.bluetooth
        #'PERMISSION_BLUETOOTH=1',
 
        ## dart: PermissionGroup.appTrackingTransparency
        #'PERMISSION_APP_TRACKING_TRANSPARENCY=1',
 
        ## dart: PermissionGroup.criticalAlerts
        #'PERMISSION_CRITICAL_ALERTS=1',
 
        ## dart: PermissionGroup.criticalAlerts
        #'PERMISSION_ASSISTANT=1',
      ]
 
    end
  end
end
 
 

 

복사가 잘 안되면 https://github.com/Baseflow/flutter-permission-handler/blob/main/permission_handler_apple/example/ios/Podfile 파일을 참조하여 내용을 추가하고 필요없는 권한은 주석처리(#)한다.

 

블로그 이미지

Link2Me

,
728x90

네이버 지도를 아이폰에서 띄우는 걸 테스트 해보다가 완전 맨붕에 빠졌다.

Android 폰에서는 잘 동작하는데, iOS 환경에서 네이버 지도가 화면에 나오지 않고, 에러 메시지는 다음과 같이 나온다.

 

******** Authorize Error : 잘못된 클라이언트 ID를 지정. 콘솔에서 앱 Bundle Identifier를 잘못 등록함
[ERROR:flutter/shell/common/shell.cc(1015)] The 'flutter_naver_map_sdk' channel sent a message from native to Flutter on a non-platform thread. Platform channel messages must be sent on the platform thread. Failure to do so may result in data loss or crashes, and must be fixed in the plugin or application code creating that channel.
See https://docs.flutter.dev/platform-integration/platform-channels#channels-and-platform-threading for more information.
flutter: Auth failed: NAuthFailedException(code: 401, message: 잘못된 클라이언트 ID를 지정. 콘솔에서 앱 Bundle Identifier를 잘못 등록함)

 

문제가 된 사항부터 파악하기 위해 순차적으로 적어보자.

 

https://console.ncloud.com/   에 등록할 때부터 주의를 해야 한다는 걸 확인했다.

안드로이드에서는 com.link2me.flutter.nmap_test 로 등록하면 정상적으로 네이버 지도가 폰에 출력된다.

하지만, 아이폰에서는 네이버 지도가 화면에 출력되지 않으면서, 위와 같은 에러메시지가 나온다.

아예 언더바를 사용하지 않고 테스트를 해봤더니 정상적으로 네이버지도가 출력되는 걸 확인했다.

그래서 다시 com.link2me.flutter.nampTest 로 변경하여 등록했더니 네이버지도가 출력된다.

이것 때문에 몇시간을 삽질했는지 모르겠다. ㅠㅠㅠ

 

콘솔창에서 open ios/Runner.xcworkspace 를 하면 Xcode 가 실행된다.

여기서 보면 Bundle Identifier 가 자동으로 변경해준 것을 확인할 수 있다.

이 identifier 를 ncloud.com 에 등록해야 한다.

 

위 이미지에 보이는 Bundle Identifier 를 앱에서 찾는 방법

ios/Runner.xcodeproj/project.pbxproj 파일을 열어서 PRODUCT_BUNDLE_IDENTIFIER 를 검색한다.

그러면 PRODUCT_BUNDLE_IDENTIFIER = com.link2me.flutter.nmapTest; 와 같이 검색된 것을 확인할 수 있다.

 

 

 

아이폰 설정 사항

설정 앱 -> 개발자 -> 신뢰하는 컴퓨터 지우기

이 컴퓨터를 신뢰하겠습니까? 신뢰 선택 -> 설정한 폰 비밀번호 입력

 

관련이 있을지 없을지 여부는 모르겠는데 나중에 찾는데 도움될 거 같아서 적어둔다.

https://hsdev.tistory.com/entry/iOS-%EC%95%B1-%EB%B0%B0%ED%8F%AC-2-Identifiers-%EC%8B%9D%EB%B3%84%EC%9E%90-App-ID-%EB%93%B1%EB%A1%9D%ED%95%98%EA%B8%B0

 

[iOS 앱 배포] 2. Identifiers (식별자) App ID 등록하기

애플 개발자 계정에서 App ID 를 등록하는 과정입니다. 1. https://developer.apple.com/ 에 들어간다. (Account > Certificates, IDs & Profiles > Identifiers) 클릭한다. Apple Developer Submit your apps today. Build your apps using Xcod

hsdev.tistory.com

 

 

블로그 이미지

Link2Me

,
728x90

News App 은 android 로 구현 테스트 해본 적이 있다.

riverpod 기능을 좀 더 익히기 위해서 검색해보니 https://www.youtube.com/watch?v=HgvKWMrbBe4 에 자료가 있더라.

원 코드 그대로 구현한 것은 아니며, Retrofit 라이브러리 사용, 디렉토리 구조는 Clean Architecture 로의 전환을 위한 걸 고려중이고, WebView 도 추가하여 실제 게시글로 이동하여 볼 수 있도록 했다.

 

NewsDataResult 클래스로 정의한 아래 코드가 동영상에서는 NewsModel 클래스로 정의되어 있다.

NewsDataResultBase 클래스는 테스트해보니 굳이 정의하지 않아도 되는 거 같다.

NewsDataResult.fromJson 부분이 구현의 핵심이다.

import 'package:news_app_riverpod/domain/model/news_data.dart';
 
sealed class NewsDataResultBase {}
 
class NewsDataResultLoading extends NewsDataResultBase {}
 
class NewsDataResultError extends NewsDataResultBase {
  final String errMsg;
 
  NewsDataResultError({
    required this.errMsg,
  });
}
 
class NewsDataResult extends NewsDataResultBase {
  String? status;
  int? totalResults;
  List<NewsData>? articles;
 
  NewsDataResult({
    this.status,
    this.totalResults,
    this.articles,
  });
 
  NewsDataResult.fromJson(Map < String, dynamic > json) {
    status = json['status'];
    totalResults = json['totalResults'];
    if (json['articles'!= null) {
      articles = <NewsData> [];
      json['articles'].forEach((v) {
        articles!.add(NewsData.fromJson(v));
      });
    }
  }
}

 

factory 생성자로 구현시 오류 발생 여부를 좀 더 테스트 해본 결과 정상동작됨을 확인했다.

Class 명을 NewsModel 로 변경해서 테스트를 했다. 이 사항도 GitHub 에 3rd버전으로 반영했다.

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:news_app_riverpod/domain/models/news_data.dart';
 
part 'news_model.g.dart';
 
sealed class NewsModelBase {}
 
class NewsDataResultLoading extends NewsModelBase {}
 
class NewsModelError extends NewsModelBase {
  final String errMsg;
 
  NewsModelError({
    required this.errMsg,
  });
}
 
@JsonSerializable()
class NewsModel extends NewsModelBase {
  String? status;
  int? totalResults;
  List<NewsData>? articles;
 
  NewsModel({
    this.status,
    this.totalResults,
    this.articles,
  });
 
  factory NewsModel.fromJson(Map<String, dynamic> json) => _$NewsModelFromJson(json);
 
  Map<String, dynamic> toJson() => _$NewsModelToJson(this);
 
  /*
  NewsModel.fromJson(Map < String, dynamic > json) {
    status = json['status'];
    totalResults = json['totalResults'];
    if (json['articles'] != null) {
      articles = <NewsData> [];
      json['articles'].forEach((v) {
        articles!.add(NewsData.fromJson(v));
      });
    }
  }
  */
}

 

 

 

Retrofit 라이브러리를 활용하면 코드를 더 심플하게 줄일 수 있다(?).

import 'package:dio/dio.dart' hide Headers;
import 'package:retrofit/retrofit.dart';
import 'package:news_app_riverpod/domain/model/news_data_result.dart';
import 'package:news_app_riverpod/data/repository/retrofit_url.dart';
 
part 'rest_client.g.dart';
 
@RestApi(baseUrl: RetrofitURL.baseUrl)
abstract class RestClient {
  factory RestClient(Dio dio, {String baseUrl}) = _RestClient;
 
  @GET(RetrofitURL.fetchNews)
  Future<NewsDataResult> fetchNews();
 
  @GET(RetrofitURL.fetchNewsBySearching)
  Future<NewsDataResult> fetchNewsBySearching(@Path() String title);
}

 

 

repository 정의 및 repositoryImpl 구현  => GitHub 마지막 버전에서는 삭제 처리

import 'package:news_app_riverpod/domain/model/news_data_result.dart';
 
abstract class NewsRepository {
  Future<NewsDataResult> fetchNews();
  Future<NewsDataResult> fetchNewsBySearching(String title);
}
 
/***
* Retrofit 라이브러리를 이용하여 구현하면 별도의 함수 정의는 하지 않아야 된다.
 */
 

 

 
import 'package:dio/dio.dart';
import 'package:news_app_riverpod/core/utils/app_utils.dart';
import 'package:news_app_riverpod/data/repository/retrofit_url.dart';
import 'package:news_app_riverpod/domain/model/news_data_result.dart';
import 'package:news_app_riverpod/domain/repository/news_repository.dart';
 
class NewsRepositoryImpl extends NewsRepository {
  final Dio _dio = Dio(
      BaseOptions(
          baseUrl: RetrofitURL.baseUrl,
          responseType: ResponseType.json
      )
  );
 
  @override
  Future<NewsDataResult> fetchNewsBySearching(String title) async {
    final response = await _dio.get('/v2/everything?q='+title+'&apiKey=${AppUtils.newsKey}');
    //print(response.data);
    final newsDataResult = NewsDataResult.fromJson(response.data);
    return newsDataResult;
  }
 
  @override
  Future<NewsDataResult> fetchNews() async {
    final response = await _dio.get('/v2/top-headlines?country=kr&category=science&apiKey=${AppUtils.newsKey}');
    final newsDataResult = NewsDataResult.fromJson(response.data);
    return newsDataResult;
  }
}

 

 

newsProvider 코드

 
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:news_app_riverpod/data/datasource/news_repositoryimpl.dart';
import 'package:news_app_riverpod/data/provider/base_provider.dart';
import 'package:news_app_riverpod/domain/model/news_data_result.dart';
 
part 'news_provider.freezed.dart';
 
@freezed
class NewsState with _$NewsState {
  factory NewsState({
    @Default(true) bool isLoading,
    required NewsDataResult newsDataResult,
  }) = _NewsState;
 
  const NewsState._();
}
 
final newsProvider = NotifierProvider<NewsNotifier, NewsState>(NewsNotifier.new);
 
class NewsNotifier extends Notifier<NewsState> {
  @override
  NewsState build() {
    return NewsState(newsDataResult: NewsDataResult(articles: []));
  }
 
  void loadNews() async {
    state = state.copyWith(isLoading: true);
    NewsDataResult resp = await ref.read(restClientProvider).fetchNews();
    state = state.copyWith(newsDataResult: resp, isLoading: false);
  }
 
  void loadSearchedNews(String title) async {
    state = state.copyWith(isLoading: true);
    //final newsResponse = await NewsRepositoryImpl().fetchNewsBySearching(title);
    NewsDataResult newsResponse = await ref.read(restClientProvider).fetchNewsBySearching(title);
    state = state.copyWith(newsDataResult: newsResponse, isLoading: false);
  }
 
}

 

 

위 전체코드는 GitHub 에 올려두었다.

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

 

 

 

블로그 이미지

Link2Me

,
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

,
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

,