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

,