본문 바로가기
Flutter

[Flutter] 카메라 및 사진 불러오기 기능 사용하기 . Part. 1

by 개발_블로그 2024. 12. 4.

Flutter 에서 카메라 및 사진 불러오기 기능 사용하는 방법. 

 

1. 안드로이드 AndroidManifest.xml  권한 추가. 

 

<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />

 

Android 14에서부터 ‘사진/동영상의 일부 접근 권한’

(READ_MEDIA_VISUAL_USER_SELECTED) 이라는 개념이 추가되었습니다.

 

https://developer.android.com/about/versions/14/changes/partial-photo-video-access?hl=ko

 

사진 및 동영상에 대한 일부 액세스 권한 부여  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. 사진 및 동영상에 대한 일부 액세스 권한 부여 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Android 14

developer.android.com

 

아래는 홈페이지에 나와있는 내용입니다.

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />

<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" /> // 33이상.

<uses-permission android:name="android.permission.CAMERA"/>

 

 

 

2. IOS  권한 설정 

 

2-1. Runner / info.plist 추가 

<key>NSCameraUsageDescription</key>
<string>We need access to the camera to take photos.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need access to your photo library to upload images.</string>

 

2-2. Podfile 추가 

post_install do |installer|
  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)

	  // 이 부분 부터 
      target.build_configurations.each do |config|
          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',

               ## The 'PERMISSION_LOCATION' macro enables the `locationWhenInUse` and `locationAlways` permission. If
               ## the application only requires `locationWhenInUse`, only specify the `PERMISSION_LOCATION_WHENINUSE`
               ## macro.
               ##
               ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse]
               'PERMISSION_LOCATION=1',
               'PERMISSION_LOCATION_WHENINUSE=0',

               ## 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

 

3. pubspec.yaml - dependencies 추가 . 

image_picker: ^1.1.2
permission_handler: ^10.2.0

 

 

4. Flutter Code 추가 

4-1. 

class _MyHomePageState extends State<MyHomePage> {
  String? imagePath;

  Future<void> _pickImage() async {
    // 이미지 선택
    final ImagePicker _picker = ImagePicker();
    final XFile? pickedFile =
        await _picker.pickImage(source: ImageSource.gallery);

    if (pickedFile != null) {
      setState(() {
        imagePath = pickedFile.path; 
      });
    }
  }

  Future<void> _takePhoto() async {
    // 사진 촬영
    final ImagePicker _picker = ImagePicker();
    final XFile? takenFile =
        await _picker.pickImage(source: ImageSource.camera);

    if (takenFile != null) {
      setState(() {
        imagePath = takenFile.path; 
      });
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Image Picker Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            imagePath != null
                ? Image.file(
                    File(imagePath!),
                    width: 300, // 너비
                    height: 300, // 높이
                    fit: BoxFit.cover, // 이미지 비율 유지
                  )
                : const Text('이미지를 선택하거나 촬영하세요.'),
            const SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: _pickImage,
                  child: const Text('앨범에서 이미지 선택하기'),
                ),
                const SizedBox(width: 20),
                ElevatedButton(
                  onPressed: _takePhoto,
                  child: const Text('사진 찍기'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

 

 

4-2. 여기에 퍼미션 권한을 확인하고 Alert 를 띄워서 설정 페이지에 갔다오는 코드를 추가하겠습니다. 

void _showPermissionDeniedAlert(bool type) { // 파라미터 타입 추가
  showDialog(
    context: context,
    builder: (BuildContext context) {
      return AlertDialog(
        title: Text('권한 필요'),
        content: Text(
            type ? '사진 앨범에 접근하기 위한 권한이 필요합니다.' : "카메라에 접근하기 위한 권한이 필요합니다."),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.of(context).pop();
              openAppSettings(); // 설정 화면으로 이동
            },
            child: Text('설정으로 이동'),
          ),
          TextButton(
            onPressed: () {
              Navigator.of(context).pop();
            },
            child: Text('취소'),
          ),
        ],
      );
    },
  );
}

 

 

4-3. _pickImage() , _takePhoto() 메서드에 코드 추가 

  Future<void> _pickImage() async {
    // 권한 요청
    var status = await Permission.photos.request();

    if (!status.isGranted) {
      _showPermissionDeniedAlert(true);
      return;
    }
    // 이미지 선택
    final ImagePicker _picker = ImagePicker();
    final XFile? pickedFile =
        await _picker.pickImage(source: ImageSource.gallery);

    if (pickedFile != null) {
      setState(() {
        imagePath = pickedFile.path; 
      });
    }
  }

  Future<void> _takePhoto() async {
    // 카메라 권한 요청
    var status = await Permission.camera.request();
    if (!status.isGranted) {
      _showPermissionDeniedAlert(false);
      return;
    }

    // 사진 촬영
    final ImagePicker _picker = ImagePicker();
    final XFile? takenFile =
        await _picker.pickImage(source: ImageSource.camera);

    if (takenFile != null) {
      setState(() {
        imagePath = takenFile.path; 
      });
    }
  }

 

 

5. Error 확인. 

나 같은 경우는 셋팅화면을 갔다오면 화면이 날아가는 현상이 있었다. 생명주기 확인이 필요하였다. 

5-1. class 에 WidgetsBindingObserver 을 상속.

class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {

 

 

 

5-2. 코드  추가

@override
void initState() {
  super.initState();
  WidgetsBinding.instance.addObserver(this);
}

@override
void dispose() {
  WidgetsBinding.instance.removeObserver(this);
  super.dispose();
}

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
  if (state == AppLifecycleState.resumed) {
    print("앱이 resumed .");
    setState(() {
      imagePath = imagePath;
    });
  } else if (state == AppLifecycleState.paused) {
  } else if (state == AppLifecycleState.detached) {
  } else if (state == AppLifecycleState.inactive) {
  }
}

 

 

 

전체 코드 

class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
  String? imagePath;


  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) {
      print("앱이 resumed .");
      setState(() {
        imagePath = imagePath;
      });
    } else if (state == AppLifecycleState.paused) {
    } else if (state == AppLifecycleState.detached) {
    } else if (state == AppLifecycleState.inactive) {
    }
  }


  Future<void> _pickImage() async {
    // 권한 요청
    var status = await Permission.photos.status;

    if (!status.isGranted) {
      _showPermissionDeniedAlert(true);
      return;
    }
    // 이미지 선택
    final ImagePicker _picker = ImagePicker();
    final XFile? pickedFile =
    await _picker.pickImage(source: ImageSource.gallery);

    if (pickedFile != null) {
      setState(() {
        imagePath = pickedFile.path; 
      });
    }
  }

  Future<void> _takePhoto() async {
    // 카메라 권한 요청
    var status = await Permission.camera.status;
    if (!status.isGranted) {
      _showPermissionDeniedAlert(false);
      return;
    }

    // 사진 촬영
    final ImagePicker _picker = ImagePicker();
    final XFile? takenFile =
    await _picker.pickImage(source: ImageSource.camera);

    if (takenFile != null) {
      setState(() {
        imagePath = takenFile.path; 
      });
    }
  }



  void _showPermissionDeniedAlert(bool type) {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: Text('권한 필요'),
          content: Text(
              type ? '사진 앨범에 접근하기 위한 권한이 필요합니다.' : "카메라에 접근하기 위한 권한이 필요합니다."),
          actions: [
            TextButton(
              onPressed: () {
                Navigator.of(context).pop();
                openAppSettings(); // 설정 화면으로 이동
              },
              child: Text('설정으로 이동'),
            ),
            TextButton(
              onPressed: () {
                Navigator.of(context).pop();
              },
              child: Text('취소'),
            ),
          ],
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Image Picker Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            imagePath != null
                ? Image.file(
              File(imagePath!),
              width: 300, // 너비
              height: 300, // 높이
              fit: BoxFit.cover, // 이미지 비율 유지
            )
                : const Text('이미지를 선택하거나 촬영하세요.'),
            const SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: _pickImage,
                  child: const Text('앨범에서 이미지 선택하기'),
                ),
                const SizedBox(width: 20),
                ElevatedButton(
                  onPressed: _takePhoto,
                  child: const Text('사진 찍기'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}