728x90

플러터에서 전화걸기를 하는 기능을 구현해보고 있다.

 

IconButton(
  iconSize: 30,
  icon: Icon(Icons.call),
  onPressed: (){
    launchUrl(Uri.parse('tel:${widget.mobileNO}'));
  },
),

 

url_launcher 패키지를 사용하는 것은 Direct 로 전화걸기가 되지 않고 2단계로 거치는 불편함이 있다.

 

그래서 https://pub.dev/packages/flutter_phone_direct_caller 패키지를 이용하여 구현해봤다.

 

화면 전환이 발생하면서 로그에 이런 메시지를 출력한다.

MSG_WINDOW_FOCUS_CHANGED 1 1

이러면서 앱이 초기상태로 변경되어 로그인 창이 나온다.

로그인 후 상태관리로 로그인 상태 표시를 확실하게 해줘야 하는가보다.

 

전화거는 코드는 아래와 같다.

IconButton(
  iconSize: 30,
  onPressed: () async {
    FlutterPhoneDirectCaller.callNumber(Utils.PhoneNO(mobileNO));
  },
  icon: Icon(Icons.call),
),

 

전화버튼은 IconButton 이 가장 무난한 거 같다. 크기도 적당히 조절할 수도 있어 편리한 거 같다.

DirectCall 을 하면 화면 갱신이 발생하는 현상을 방지하기 위한 방안을 찾아보고, 나중에 업데이트하고자 한다.

 

 

블로그 이미지

Link2Me

,
728x90

플러터에서 사진촬영 또는 사진 이미지에서 위치좌표를 추출하는 방법을 알아보자.

 

사진을 찍을 때 위치좌표값이 포함되어 저장되도록 하는 방법은 카메라에서 위치정보를 활성화시켜야 한다.

아이폰

설정 → 개인정보 보호 및 보안 → 위치서비스 → 카메라 → 앱을 사용하는 동안

안드로이드폰

카메라 앱 → 카메라 설정 → 위치태그 활성화

 

위와 같이 하면 기본적으로 사진을 찍으면 이미지에 위치정보가 포함된다.

하지만 구현하는 앱에서 카메라로 촬영해도 위치정보가 포함되지 않을 수도 있다.

이 경우에는 사진을 찍는 시점의 GPS 위치정보를 기준으로 위치정보를 수집해야 한다.

 

pubspec.yaml 파일

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  dio: ^5.4.0
  retrofit: ^4.0.3
  flutter_riverpod: ^2.4.9
  json_annotation: ^4.8.1
  freezed_annotation: ^2.4.1
  permission_handler: ^11.2.0
  flutter_secure_storage: ^9.0.0
  image_picker: ^1.0.7
  exif: ^3.3.0
  geolocator: ^10.1.0
 
 
dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0
  build_runner: ^2.4.8
  json_serializable: ^6.7.1
  freezed: ^2.4.6
  retrofit_generator: ^8.0.6

 

 

앨범 및 사진촬영한 이미지를 앱 화면에 보여주는 예제 코드

import 'dart:io';
 
import 'package:exif/exif.dart';
import 'package:fileupload/presentation/view/file_upload_screen.dart';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:image_picker/image_picker.dart';
 
class ImageViewScreen extends StatefulWidget {
  const ImageViewScreen({super.key});
 
  @override
  State<ImageViewScreen> createState() => _ImageViewScreenState();
}
 
class _ImageViewScreenState extends State<ImageViewScreen> {
  XFile? _image; //이미지 담을 변수 선언
  double latitude = 0;
  double longitude = 0;
  bool isLocation = false;
 
  @override
  void initState() {
    super.initState();
    getLocationPermission();
  }
 
  Future<void> getLocationPermission() async {
    LocationPermission permission = await Geolocator.checkPermission();
    if (permission == LocationPermission.denied) {
      permission = await Geolocator.requestPermission();
    }
  }
 
  Future<void> getImage(ImageSource imageSource) async {
    var picker = ImagePicker();
    var pickerImage = await picker.pickImage(source: imageSource);
 
    // 이미지를 읽을 때마다 위치정보 좌표값 초기화 처리
    latitude = 0;
    longitude = 0;
 
    if (pickerImage != null) {
      var metadata = await readExifFromBytes(File(pickerImage.path).readAsBytesSync());
      if (metadata.containsKey('GPS GPSLatitude')) {
        print("location enabled");
        isLocation = true;
        getCurrentLocationFromexif(metadata);
      } else {
        isLocation = false;
        print("location disabled");
        try {
          Position position = await Geolocator.getCurrentPosition(
              desiredAccuracy: LocationAccuracy.high);
          latitude = position.latitude;
          longitude = position.longitude;
        } catch (e) {
          print(e);
        }
      }
      // 이미지를 출력하기 위해 상태 변경
      setState(() {
        _image = XFile(pickerImage.path);
      });
    }
  }
 
  Future<void> getCurrentLocationFromexif(var data) async {
    if (data.containsKey('GPS GPSLongitude')) {
      final gpsLatitude = data['GPS GPSLatitude'];
      final latitudeSignal = data['GPS GPSLatitudeRef']!.printable;
      List latitudeRation = gpsLatitude!.values.toList();
      List latitudeValue = latitudeRation.map((item) {
        return (item.numerator.toDouble() / item.denominator.toDouble());
      }).toList();
      latitude = latitudeValue[0+ (latitudeValue[1/ 60+
          (latitudeValue[2/ 3600);
      if (latitudeSignal == 'S') latitude = -latitude;
      print('latitude ::: ${latitude}');
 
      final gpsLongitude = data['GPS GPSLongitude'];
      final longitudeSignal = data['GPS GPSLongitude']!.printable;
      List longitudeRation = gpsLongitude!.values.toList();
      List longitudeValue = longitudeRation.map((item) {
        return (item.numerator.toDouble() / item.denominator.toDouble());
      }).toList();
      longitude = longitudeValue[0+ (longitudeValue[1/ 60+
          (longitudeValue[2/ 3600);
      if (longitudeSignal == 'W') longitude = -longitude;
      print('longitude ::: ${longitude}');
    }
  }
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          _buildPhotoArea(),
          if(latitude > 0) _buildLocation(),
        ],
      ),
      floatingActionButton: Stack(
        children: [
          Align(
            alignment: Alignment(
                Alignment.bottomRight.x, Alignment.bottomRight.y - 0.4),
            child: FloatingActionButton(
              onPressed: () {
                Navigator.of(context).push(MaterialPageRoute(
                  builder: (context) => const FileUploadScreen(),
                ));
              },
              tooltip: 'back',
              heroTag: UniqueKey(),
              child: const Icon(Icons.arrow_back),
            ),
          ),
          Align(
            alignment: Alignment(
                Alignment.bottomRight.x, Alignment.bottomRight.y - 0.2),
            child: FloatingActionButton(
              onPressed: () async {
                getImage(ImageSource.camera);
              },
              tooltip: 'image',
              heroTag: UniqueKey(),
              child: const Icon(Icons.camera_alt),
            ),
          ),
          Align(
            alignment: Alignment.bottomRight,
            child: FloatingActionButton(
              onPressed: () async {
                getImage(ImageSource.gallery);
              },
              tooltip: 'image',
              heroTag: UniqueKey(),
              child: const Icon(Icons.image),
            ),
          ),
        ],
      ),
    );
  }
 
  Widget _buildPhotoArea() {
    return _image != null
        ? Container(
            width: 400,
            height: 400,
            child: Image.file(File(_image!.path)), //가져온 이미지 화면에 띄워주는 코드
          )
        : const Center(
            child: Text("불러온 이미지가 없습니다."),
          );
  }
 
  Widget _buildLocation() {
    return Column(
      children: [
        Container(
          child: Text('GPS Location ::: $isLocation'),
        ),
        Container(
          child: Text('위도: ${latitude} ,경도: ${longitude}'),
        ),
      ],
    );
  }
 
}

 

 

위 소스코드를 보다 정리한 모든 소스코드 파일은 GitHub 에 올려두었다.

https://github.com/jsk005/Flutter/tree/main/fileupload/2nd

 

블로그 이미지

Link2Me

,
728x90

플러터에서 앱의 사진 이미지나 직접 촬영한 이미지를 서버에 업로드하는 예제이다.

이미지를 업로드하는 코드는 Retrofit 라이브러리를 이용하는 방법과 Dio 라이브러리에서 직접 업로드하는 방법이 있다.

두가지 사례를 모두 하나의 파일에 구현했다.

 

pubspec.yaml 파일

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  dio: ^5.4.0
  retrofit: ^4.0.3
  flutter_riverpod: ^2.4.9
  json_annotation: ^4.8.1
  freezed_annotation: ^2.4.1
  flutter_naver_map: ^1.1.2
  permission_handler: ^11.2.0
  flutter_secure_storage: ^9.0.0
  image_picker: ^1.0.7
 
 
dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0
  build_runner: ^2.4.8
  json_serializable: ^6.7.1
  freezed: ^2.4.6
  retrofit_generator: ^8.0.6

 

 

ios > Runner > Info.plist 파일에 아래 키들을 추가해 준다.

<key>NSPhotoLibraryUsageDescription</key>
<string>This app requires access to the photo library.</string>
<key>NSCameraUsageDescription</key>
<string>This app requires access to the camera.</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app does not require access to the microphone.</string>

 

 

Android
기존에는 AndroidManifest.xml에 권한을 추가해주어야 했지만 지금은 따로 추가하지 않아도 된다.
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
는 추가하지 않아도 된다.
image_picker 버전에 맞게 app build.gradle 파일내에서 
compileSdkVersion 34
minSdkVersion 23
으로 변경 요청해서 변경해줬다.

 

주요 소스코드

서버에서 POST 변수로 받는 항목이 userID, code, file 이다.

로그인 후 userID 정보, code 파일이 필요하다는 가정하에 항목을 넣었다.

 

Android폰에서는 카메라 촬영 이미지, 갤러리 이미지 모두 잘 가져와서 서버로 업로드 되는 걸 확인했다.

하지만 아이폰 15 Pro 에서 갤러리 이미지를 가져오려고 하면 에러가 발생하는 현상을 겪었다.

image_picker 버전 문제인지 여부는 아직 모르겠다. 위치정보 가져오기를 포함해서 테스트해보니 정상 동작된다.

그래서 카메라 영상을 직접 촬영하여 이미지를 업로드하는 방법으로 아이폰에서는 테스트를 했다.

서버에 업로드하는 이미지를 줄이기 위한 옵션은 선택하면 된다.

 

Retofit 라이브러리는 서버로 업로드하는 변수와 서버에서 결과를 Return 하는 JSON 변수를 파싱처리할 수 있게 할 수 있으나 본 예제에서는 그것까지는 처리하지 않았다. 향후 Riverpod 상태관리 코드 기준으로 합쳐서 작성하기 위해서 관련 사항을 염두에 두고 소스 일부를 구현했다. 현재 코드는 View 와 ViewModel 이 분리되어 있지 않다.

 

class RetrofitURL {
  static const baseUrl = "https://www.abc.com";
  static const mLogin = "/androidSample/loginChk.php";
  static const contactData = "/androidSample/ContactList.php";
  static const imgUpload = "/androidSample/photo_upload.php";
}

 

import 'dart:io';
 
import 'package:dio/dio.dart' hide Headers;
import 'package:fileupload/core/network/retrofit_url.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.imgUpload)
  @MultiPart()
  Future<String> uploadFile(
      @Part() File file,
      @Part(name: "userID"String userID,
      @Part(name: "code"String code,
      {@SendProgress() ProgressCallback? onSendProgress});
 
}

코드를 수정하면 터미널 창에서 dart run build_runner build 를 실행하여 업데이트 해야 한다.

 

import 'dart:io';
 
import 'package:dio/dio.dart';
import 'package:fileupload/core/network/custlog_interceptor.dart';
import 'package:fileupload/core/network/retrofit_url.dart';
import 'package:fileupload/data/provider/base_provider.dart';
import 'package:fileupload/data/repository/rest_client.dart';
import 'package:fileupload/presentation/view/imag_view_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:image_picker/image_picker.dart';
 
class FileUploadScreen extends ConsumerStatefulWidget {
  const FileUploadScreen({Key? key}) : super(key: key);
 
  @override
  ConsumerState<FileUploadScreen> createState() => _FileUploadScreenState();
}
 
class _FileUploadScreenState extends ConsumerState<FileUploadScreen> {
  String? _uploadProgress;
  RestClient? _apiService;
 
  @override
  void initState() {
    super.initState();
    var dio = Dio();
    _apiService = RestClient(dio);
  }
 
  Future<void> _uploadFile() async {
    final userID = "jsk005";
    final code = "XFD0001";
 
    final ImagePicker picker = ImagePicker();
    final XFile? pickedImage = await picker.pickImage(
      //이미지를 선택
      source: ImageSource.camera, // 위치는 카메라
      maxHeight: 200,
      //maxWidth: 200,
      //imageQuality: 70, // 이미지 크기 압축을 위해 퀄리티를 70으로 낮춤.
    );
 
    if (pickedImage == null) {
      // No image selected
      return null;
    }
 
    try {
      // Get the path of the selected image
      String imagePath = pickedImage.path;
 
      // Create a File object from the image path
      File imageFile = File(imagePath);
 
      print(imageFile);
 
      final response = await ref.read(restClientProvider).uploadFile(
        imageFile,
        userID,
        code,
        onSendProgress: (sent, total) {
          setState(() {
            _uploadProgress = "${(sent / total) * 100}%";
          });
        },
      );
 
      print(response);
    } catch (error) {
      // Handle error
      print('Error converting image picker result to File: $error');
    }
  }
 
  Future<void> uploadFile() async {
    final userID = "jsk005";
    final code = "XFD0001";
 
    final ImagePicker _picker = ImagePicker();
 
    // Select image from gallery
    XFile? pickedImage = await _picker.pickImage(
      //이미지를 선택
      source: ImageSource.gallery, //위치는 갤러리
      maxHeight: 200,
      //maxWidth: 200,
      //imageQuality: 70, // 이미지 크기 압축을 위해 퀄리티를 70으로 낮춤.
    );
 
    if (pickedImage != null) {
      try {
        String fileName = pickedImage.path
            .split('/')
            .last; // Extract the file name from its path
 
        // Create FormData object with the image file
        FormData formData = FormData.fromMap({
          'file': await MultipartFile.fromFile(pickedImage.path,
              filename: fileName),
          "userID": userID,
          "code": code,
        });
 
        final storage = ref.read(secureStorageProvider);
 
        // Create Dio instance
        BaseOptions options = BaseOptions(
          baseUrl: RetrofitURL.baseUrl,
        );
        Dio dio = Dio(options);
        dio.interceptors.add(LogInterceptor());
        dio.interceptors.add(
          CustLogInterceptor(storage: storage,),
        );
 
        // Send POST request with FormData
        final response = await dio.post(
          '${RetrofitURL.baseUrl}${RetrofitURL.imgUpload}',
          data: formData,
          options: Options(
            headers: {
              'Content-Type''multipart/form-data',
              // Add any additional headers required by your server
            },
          ),
        );
 
        final Map<String, dynamic> body = response.data;
        print('${body}');
 
      } catch (error) {
        // Handle error
        print('Error uploading file: $error');
      }
    }
  }
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: _uploadFile,
              child: Text("Upload File"),
            ),
            if (_uploadProgress != null)
              Text("Upload Progress: $_uploadProgress"),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.of(context).push(MaterialPageRoute(
            builder: (context) => const ImageViewScreen(),
          ));
        },
        tooltip: 'scan',
        child: const Icon(Icons.camera_alt),
      ),
    );
  }
}

 

 

 

테스트에 사용한 전체 소스코드 및 서버 PHP 파일(DB테이블 포함) 자료는

https://github.com/jsk005/Flutter/tree/main/fileupload 에 올려두었다.

블로그 이미지

Link2Me

,
728x90

플러터에서 QR코드를 스캔하는 코드 예제이다.

맥북에서 IOS 와 Android 모두 정상 동작함을 확인하고 기록한다.

 

pubspec.yaml 에 추가한 dependencies

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  dio: ^5.4.0
  retrofit: ^4.0.3
  flutter_riverpod: ^2.4.9
  json_annotation: ^4.8.1
  freezed_annotation: ^2.4.1
  flutter_naver_map: ^1.1.2
  permission_handler: ^11.2.0
  geolocator: ^10.1.0
  flutter_secure_storage: ^9.0.0
  qr_code_scanner: ^1.0.1
 
dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0
  build_runner: ^2.4.8
  json_serializable: ^6.7.1
  freezed: ^2.4.6
  retrofit_generator: ^8.0.6

 

 

안드로이드 Manifest.xml 파일

QR코드 스캔을 위해서는 CAMERA 기능을 ON할 수 있어야 한다.

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
 
    <uses-feature
        android:name="android.hardware.camera"
        android:required="false" />
 
    <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" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
 
    <application
        android:label="nmap_test"
        android:name="${applicationName}"

 

iOS 파일

Info.plist

    <key>NSCameraUsageDescription</key>
    <string>This app needs camera access to scan QR codes</string>

 

Podfile

        ## dart: PermissionGroup.camera
        'PERMISSION_CAMERA=1',

 

보다 세부적인  사항은 https://link2me.tistory.com/2381 파일내의 ios 부분을 참조하면 된다.

 

 

네이버 MAP에서 QR코드 버튼을 누르면 QR코드 스캔이 활성화되고 QR코드를 찍으면 해당 정보를 읽어낸다.

  @override
  Widget build(BuildContext context) {
    return DefaultLayout(
      title: 'Naver Map',
      appbarBgColor: Palette.primary,
      child: NaverMap(
        options: const NaverMapViewOptions(locationButtonEnable: true),
        onMapReady: onMapReady,
        onMapTapped: onMapTapped,
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.of(context).push(MaterialPageRoute(
            builder: (context) => const QRScanView(),
          ));
        },
        tooltip: 'scan',
        child: const Icon(Icons.camera_alt),
      ),
    );
  }
 
  void onMapReady(NaverMapController controller) async {
    mapController = controller;
    print('네이버맵 로딩됨');
  }
 
  void onMapTapped(NPoint point, NLatLng latLng) {
    // 터치를 하면 해당 좌표를 반환한다.
    print("${latLng.latitude}、${latLng.longitude}");
  }
 

 

 

QRScanView 파일

import 'dart:io';
 
import 'package:flutter/material.dart';
import 'package:nmap_test/core/component/default_layout.dart';
import 'package:nmap_test/core/res/palette.dart';
import 'package:qr_code_scanner/qr_code_scanner.dart';
 
class QRScanView extends StatefulWidget {
  const QRScanView({super.key});
 
  @override
  State<QRScanView> createState() => _QRScanViewState();
}
 
class _QRScanViewState extends State<QRScanView> {
  final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
  Barcode? barcode;
  late QRViewController controller;
 
  @override
  void reassemble() {
    super.reassemble();
    if (Platform.isAndroid) {
      controller.pauseCamera();
    } else if (Platform.isIOS) {
      controller.resumeCamera();
    }
  }
 
  @override
  void dispose() {
    controller?.dispose();
    super.dispose();
  }
 
  void _onQRViewCreated(QRViewController controller) {
    this.controller = controller;
    controller.scannedDataStream.listen((scanData) async {
      setState(() {
        barcode = scanData;
      });
    });
  }
 
  @override
  Widget build(BuildContext context) {
    return DefaultLayout(
      title: 'QR Code Scanner',
        child: Stack(
          alignment: Alignment.center,
          children: [
            _buildQRView(context),
            Positioned(bottom: 15, child: _buildResult()),
          ],
        ),
    );
  }
 
  Widget _buildQRView(BuildContext context) {
    return QRView(
      key: qrKey,
      onQRViewCreated: _onQRViewCreated,
    );
  }
 
  Widget _buildResult() {
    if (barcode != null) {
      controller?.pauseCamera();
      Future.microtask(() {
        // Navigator.push(
        //     context,
        //     MaterialPageRoute(builder: (context) => ),
        // );
        controller?.resumeCamera();
      });
 
      return Center(
        child: Text(
          'QR Code Result: ${barcode!.code}',
          style: TextStyle(fontSize: 20),
        ),
      );
    } else {
      return Container();
    }
  }
 
}
 

 

QR코드 스캔 결과가 나오면 해당 스캔 값을 가지고 서버에 전송하고 결과를 받아서 처리할 수도 있다.

그래서 _buildResult() 위젯 결과를 실행해보고 수정하여 사용하면 된다.

 

 

 

 

 

블로그 이미지

Link2Me

,
728x90

예제를 찾아서 테스트를 하다보면 Android폰에서는 잘 동작되는데, 아이폰에서는 동작이 안되기도 한다.

그래서 양쪽에서 모두 동작하는 걸 찾아서 테스트하기가 쉽지 않다.

location 라이브러리를 추가해서 테스트해보니 Android폰에서는 잘 동작하는데 ios 에서는 설정을 잘못했는지 에러가 발생하여 포기했다. 그리고 geolocator 라이브러리를 활용하여 구현했다.

잘 동작되기는 하는데, onMapReady 보다 늦게 결과가 나와 화면에서 바로 현재 위치로 이동시키는 것은 아직 미해결 상태다.

 

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  dio: ^5.4.0
  json_annotation: ^4.8.1
  freezed_annotation: ^2.4.1
  retrofit: ^4.0.3
  logger: ^2.0.2+1
  flutter_riverpod: ^2.4.9
  flutter_naver_map: ^1.1.2
  geolocator: ^10.1.0
  permission_handler: ^11.2.0
  flutter_secure_storage: ^4.2.1
 
 
dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0
  json_serializable: ^6.7.1
  build_runner: ^2.4.8
  freezed: ^2.4.6
  retrofit_generator: ^8.0.6

 

AndroidManifest.xml 추가사항

<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

 

ios 설정 추가사항은 https://link2me.tistory.com/2381 게시글의 ios를 참조하면 된다.

 

import 'package:geolocator/geolocator.dart';
 
class LocationService {
  double latitude = 0;
  double longitude = 0;
 
  Future<void> getCurrentLocation() async {
    LocationPermission permission = await Geolocator.checkPermission();
    // print(permission);
    if (permission == LocationPermission.denied) {
      permission = await Geolocator.requestPermission();
    }
    try {
      Position position = await Geolocator.getCurrentPosition(
          desiredAccuracy: LocationAccuracy.high);
      latitude = position.latitude;
      longitude = position.longitude;
    } catch (e) {
      print(e);
    }
  }
}

 

 

import 'package:flutter/material.dart';
import 'package:flutter_naver_map/flutter_naver_map.dart';
import 'package:geolocator/geolocator.dart';
import 'package:location/location.dart';
import 'package:nmap_test/core/component/default_layout.dart';
import 'package:nmap_test/core/res/palette.dart';
import 'package:nmap_test/presentation/view/location_service.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> {
  final location = LocationService();
 
  void _getLocation() async {
    await location.getCurrentLocation();
    double lat = location.latitude;
    double lng = location.longitude;
    print('lat:: $lat  lng::: $lng');
  }
 
  @override
  void initState() {
    super.initState();
    _permission();
    WidgetsBinding.instance.addPostFrameCallback((_) {
     _getLocation();
    });
  }
 
  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('네이버맵 로딩됨');
          },
        ),
      );
  }
}

 

 

 

Android 에서만 동작이 되는 걸 성공한 코드도 적어둔다.

location: ^5.0.3

class _NaverMapScreenState extends State<NaverMapScreen> {
  final Location location = Location();
  late double lat;
  late double lng;
  late LocationData _locationData;
  late bool _serviceEnabled;
 
  void _getCurrentLocation() async {
    _serviceEnabled = await location.serviceEnabled();
    if (!_serviceEnabled) {
      _serviceEnabled = await location.requestService();
      if (!_serviceEnabled) {
        return;
      }
    }
 
    _locationData = await location.getLocation();
    setState(() {
      lat = _locationData.latitude!;
      lng = _locationData.longitude!;
    });
    print('lat:: $lat  lng::: $lng');
  }
 
  @override
  void initState() {
    super.initState();
    _permission();
    _getCurrentLocation();
  }
 
  void _permission() async {
    var requestStatus = await Permission.location.request();
    var status = await Permission.location.status;
    if (requestStatus.isPermanentlyDenied || status.isPermanentlyDenied) {
      openAppSettings();
    }
  }
 

 

 

 

블로그 이미지

Link2Me

,
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

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

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

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

 

Android 설정

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

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

 

IOS 설정

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

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

 

pubspec.yaml

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

 

webview 소스코드

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

 

lib/screen/home_screen.dart

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

 

flutter webView 4.0 버전

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

 

main.dart

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

 

lib/screen/home_detail_view.dart

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

 

- last Update : 2024.1.24일

 

 

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

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

 

GitHub - jsk005/Flutter: Flutter Study

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

github.com

 

 

 

 

블로그 이미지

Link2Me

,