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

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

 

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

 

 

블로그 이미지

Link2Me

,
728x90

플러터에서 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

하나의 클래스를 다른 클래스에 위임하도록 선언하여 위임된 클래스가 가지는 인터페이스 메소드를 참조 없이 호출할 수 있도록 생성해주는 기능이다.

 

interface Fruit를 구현하고 있는 class Apple이 있다면, 
Fruit에서 정의하고 있는 Apple의 모든 메소드를 클래스 GreenApple로 위임할 수 있다. 
즉, GreenApple은 Apple이 가지는 모든 Fruit의 메소드를 가지며, 이를 클래스 위임(Class delegation)이라고 한다.

interface Fruit {
    val name: String
    val color: String
    fun bite()
}
 
class Apple: Fruit {
    override val name: String
        get() = "사과"
    override val color: String
        get() = "빨간색"
 
    override fun bite() {
        print("사과 아삭~ 아삭~")
    }
}
 
// 클래스 위임 : 사용할 기능은 그대로 사용하고 새로 정의할 것만 override로 재정의
class GreenApple(
    private val apple: Apple
) : Fruit by apple {
    override val color: String
        get() = "초록색"
}

 

fun main() {
    val greenApple = GreenApple(Apple())
    println(greenApple.color)
    println(greenApple.name)
    greenApple.bite()
}

 

 

'안드로이드 > Kotlin 문법' 카테고리의 다른 글

코틀린 제네릭 공변, 반공변  (0) 2024.02.04
[코틀린] ArrayList, mutableListOf  (0) 2021.06.14
[코틀린] Inner Class  (0) 2020.08.21
[코틀린] Nested Class (중첩 클래스)  (0) 2020.08.21
코틀린 클래스  (0) 2020.05.06
블로그 이미지

Link2Me

,
728x90

Kotlin에서 List, Map, Set같은 Collections 클래스를 사용할 때, List<String> 이런식으로 제네릭 타입을 지정할 수 있다.

제네릭을 사용하면 타입 안정성을 얻을 수 있다.
하지만 제네릭으로 지정한 타입 이외에도 좀 더 유연하게 사용하고 싶을 수 있다.
그래서 코틀린에서는 공변성과 반공변성을 허용한다.

 

package generic
 
abstract class Animal(val name: String)
 
abstract class Fish(name: String) : Animal(name)
 
// 금붕어
class GoldFish(name: String) : Fish(name)
 
// 잉어
class Carp(name: String) : Fish(name)

 

package generic
 
fun main(){
    val goldFishCage = Cage<GoldFish>()
    goldFishCage.put(GoldFish("금붕어")) // Type Casting없이 바로 금붕어를 가져올 수 있다.
 
    val cage = Cage<Fish>()
    cage.moveFrom(goldFishCage)
 
    val fish: Fish = cage.getFirst()
    // Cage<Fish>에서 데이터를 가져오면 GoldFish인지 Carp인지 모른다.
}
 
/***
 * 제네릭 클래스 : 타입 파라미터를 사용한 클래스
 * 코틀린에서는 Raw 타입 사용이 불가능하다.
 * Raw Type : 제네릭 클래스에서 타입 매개변수를 사용하지 않고 인스턴스화 하는 것
 * in-variant(무공변) : 타입 파라미터끼리는 상속관계이더라도, 제네릭 클래스 간에는 상속관계가 없다는 의미
 * co-variant(공변) : 타입 파라미터간의 상속관계가 제네릭 클래스에도 동일하게 유지된다는 의미
 * 코틀린에서는 타입 파리미터 앞에 out 변성 어노테이션을 사용한다.
 * contra_variant(반공변) : 타입 파라미터간의 상속관계가 제네릭 클래스에서는 반대로 유지된다는 의미
 * 코틀린에서는 타입 파리미터 앞에 in 변성 어노테이션을 사용한다.
 */
class Cage<T : Animal> {
    private val animals: MutableList<T> = mutableListOf()
 
    fun getFirst() : T {
        return animals.first();
    }
 
    fun getAll() : List<T> {
        return  this.animals
    }
 
    fun put(animal: T) {
        this.animals.add(animal)
    }
 
    fun moveFrom(otherCage: Cage<out T>) {
        // out을 붙이면 moveFrom 함수를 호출할 때 Cage는 공변하게 된다.
        // out을 붙이면, otherCage로부터 데이터를 꺼낼 수만 있다.
        this.animals.addAll(otherCage.animals)
    }
 
    fun moveTo(otherCage: Cage<in T>){
        // in을 붙인 otherCage는 데이터를 받을 수만 있다.
        otherCage.animals.addAll(this.animals)
    }
}

 

 

 

'안드로이드 > Kotlin 문법' 카테고리의 다른 글

코틀린 클래스 위임(Class Delegation)  (1) 2024.02.06
[코틀린] ArrayList, mutableListOf  (0) 2021.06.14
[코틀린] Inner Class  (0) 2020.08.21
[코틀린] Nested Class (중첩 클래스)  (0) 2020.08.21
코틀린 클래스  (0) 2020.05.06
블로그 이미지

Link2Me

,
728x90

Macbook에서 아이폰과 USB 케이블 연결하여 컴파일을 하면 앱 실행이 잘 되는데, 케이블을 제거하면 앱 실행이 안되는 현상을 겪었다.

아이폰과 안드로이드폰 2개 모두 USB 케이블로 연결하고 테스트했다.

 

해결방법 : flutter run --release

 

컴파일을 하다보니 에러가 발생하는 현상이 있어서 다른 것도 추가하여 기록한다.

블로그 이미지

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

,