ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Flutter - 코딩마스터하기|Provider, 카메라 페이지 만들기2
    개발/Flutter 2021. 11. 4. 19:16
    반응형

    프로바이더

    프로바이더를 통해서 데이터를 전달해줄때 프로바이더는 상위에서 하나 만들어 두면 그 아래에서는 모두 접근이 가능. 예로 탈것이라는것이 있고 차,배,비행이가 있고 각 부분에 부품들이 있으면 차에 프로바이더를 주면 차의 부품들은 접근이 가능하단거임. 배 비행기는 접근 불가. 탈것에 프로바이더를 줘서 데이터를 주면 차,배,비행기 그아래 모든 부품들도 데이터 접근 가능. 주로 사용하는 것은 ChangeNotifierProvider. 사용하기 위해서 pub.dev에서 provider를 찾아서 설치해준다. Consumer와 Provider.of 로 사용함. 데이터를 가져오기 위해서 Consumer는 context가 필요없음. context를 제공해준다. Provider.of는 context가 필요하다. 이점을 이용해서 만들면 된다.


    create camera state file(change notifier model)

    lib 폴더에 models라는 새 폴더를 만들어주고, 그 안에 camera_state.dart 파일을 만들어 준다. 그리고 이번에는 CameraState를 클래스 명으로 해주고 stf나 stl이 아닌 extends에 ChangeNotifier로 해준다. 해 줄 순서를 보자.

    1.  available camera 가져오기
    2. 카메라 리스트에서 첫번째 카메라 사용
    3. CameraController 인스턴스 생성
    4. CameraController.initialize()
    5. show preview
    6. set ready to take photo

    의 순서대로 만들어 주면 된다.

     

    build camera state model(change notifier model)

    만들어 보자. 우선 클래스 상단에 CameraController, CameraDescription, bool로 _controller, _cameraDescription, _readyTakePhoto = false;로 만들어 준다. void로 getReadyToTakePhoto 함수를 만들어 주고 그 안에 List<CameraDescription> cameras로 availableCameras를 받아준다. 이때 availableCameras가 Future 함수이므로 await를 해주고 getReadyToTakePhoto 를 async로 해준다. 1번은 이렇게 완료를 해주고, 2번을 위해서 아래에 또 void로 setCameraDescription을 만들어주고 CameraDescription을 옵션으로 하는 함수를 만들어 준다. 여기서 _cameraDescription 을 옵션으로 받은 cameraDescription으로 해준뒤 _controller에는 CameraController(_cameraDescription, ResolutionPreset.medim)을 해주면 컨트롤러 인스턴스는 생성이다. 그리고 다시 getReadyToTakePhoto로 가서 cameras가 null이 아니고 isNotEmpty라면 setCameraDescription(cameras[0])을 줘서 첫번째 카메라를 사용하게 하므로써 2,3까지 완성이다. 그리고 _controller.initialize();로 생성하면 4번까지 완성이지만 카메라를 불러오는 부분을 위해서 다시 하단에 Future<bool>로 initialize() 함수를 만들어 준다. async를 해주고, try catch문을 만들어 준다. try에는 await _contorller.initialize();를 해주고 return으로 true, catch에는 return false;를 해준다. getReadyToTakePhoto에서 못불러올 가능성을 위해서 bool init = false를 만들어 주고 while(!init)를 해서 그 안에 init = initialize()를 넣어주면 불러올때까지 while을 돌게 해준다. 이렇게 4번까지 완성이다. 그게 되고 나서 _readyTakePhoto = true; 를 해주면 4,5,6,이 된 상태이다. 이것을 notifiListeners();를 해줘서 다른 위젯들이 사용할 수 있게 해준다.(이부분은 보면서 해도 이해하기에는 어려운 부분이 있음. 코드로 이해하는게 좋을듯) 그리고 하단에 다른 곳에서 값을 바꾸지 못하도록 _ private를 해줬으므로 값을 가져갈수 있게 get을 만들어 준다.


    class CameraState extends ChangeNotifier{
      CameraController _controller;
      CameraDescription _cameraDescription;
      bool _readyTakePhoto = false;
    
      void getReadyToTakePhoto() async {
        List<CameraDescription> cameras = await availableCameras(); // Future이므로 기다려줘야함 await사용 메소드에 async까지 해줘야함
    
        if(cameras != null && cameras.isNotEmpty){
          setCameraDescription(cameras[0]);
        }
    
        bool init = false;
        while(!init) {
          init = await initialize();
        }
        _readyTakePhoto = true;
        notifyListeners(); // consumer나 provider.of로 데이터를 보고있는 모든 위젯들이 알림을 받게 됨.
      }
    
      void setCameraDescription(CameraDescription cameraDescription){
        _cameraDescription = cameraDescription;
        _controller = CameraController(_cameraDescription, ResolutionPreset.medium);
      }
    
      Future<bool> initialize() async{
        try{
          await _controller.initialize();
          return true;
        }catch(e){
          return false;
        }
      }
    
      CameraController get controller => _controller;
      CameraDescription get description => _cameraDescription;
      bool get isReadyToTakePhoto => _readyTakePhoto;
    }

    use multi provider

    만든 카메라를 take_photo에서 사용하게 해줄껀데 camera_screen에 들어오자마자 준비를 해줄꺼임. camera_screen으로 가서 state가 아닌 CameraScreen class에 CameraState _cameraState = CameraState();를 생성해주고 _CameraScreenState 불러오는 부분을 바꿔준다.


      @override
      _CameraScreenState createState() {
        _cameraState.getReadyToTakePhoto();
        return _CameraScreenState();
      }

    위와같이 해줘서 사진찍을 준비를 미리 시켜준다. 그리고 Scaffold 부분에서 MultiProvider로 감싸준다. 그로기 proviers옵션에 ChangeNotifierProvider<CameraState>.value(value:widget._cameraState)로 해주면 준비는 끝이다. 다른 방법도 있지미나 .value로 하는 이유는 getReadyToTakePhoto로 사진찍을 준비를 미리 하기 위해서이다. 그리고 여러개의 Provider를 해줄 수 있는데 겹치지 않는 타입을 사용해서 해줘야 한다. 같으면 2개의 정보를 받아서 에러가 나거나 원하는 정보가 아닌 다른정보를 받아서 쓸수있기 때문이다.


    class CameraScreen extends StatefulWidget {
    
      CameraState _cameraState = CameraState();
    
      @override
      _CameraScreenState createState() {
        _cameraState.getReadyToTakePhoto();
        return _CameraScreenState();
      }
    }
    
    class _CameraScreenState extends State<CameraScreen> {
      int _currentIndex = 1;
      PageController _pageController = PageController(
        initialPage: 1,
      );
      String _title = 'PHOTO';
    
      @override
      void dispose() {
        _pageController.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return MultiProvider(
          providers: [
            //겹치지 않는 타입으로 제공해줄수있음.
            ChangeNotifierProvider<CameraState>.value(value: widget._cameraState), //이렇게 하는 이유는 getReadyToTakePhoto를 실행하기위해서 아래가 아닌 위처럼 함.
            // ChangeNotifierProvider(create: (context)=>CameraState(),)
          ],
          child: Scaffold(
            appBar: AppBar(
              title: Text(_title),
            ),
            body: PageView(
              controller: _pageController,
              children: [
                Container(
                  color: Colors.cyanAccent,
                ),
                TakePhoto(),
                Container(
                  color: Colors.greenAccent,
                ),
              ],
              onPageChanged: (index) {
                setState(() {
                  _currentIndex = index;
                  switch (_currentIndex) {
                    case 0:
                      _title = 'GALLERY';
                      break;
                    case 1:
                      _title = 'PHOTO';
                      break;
                    case 2:
                      _title = 'VIDEO';
                      break;
                  }
                });
              },
            ),
            bottomNavigationBar: BottomNavigationBar(
              iconSize: 0,
              selectedLabelStyle: TextStyle(
                fontWeight: FontWeight.bold,
              ),
              selectedItemColor: Colors.black,
              unselectedItemColor: Colors.black54,
              items: [
                BottomNavigationBarItem(
                  icon: Icon(Icons.forward),
                  label: 'GALLERY',
                ),
                BottomNavigationBarItem(
                  icon: Icon(Icons.forward),
                  label: 'PHOTH',
                ),
                BottomNavigationBarItem(
                  icon: Icon(Icons.forward),
                  label: 'VIDEO',
                ),
              ],
              currentIndex: _currentIndex,
              onTap: _onItemTabbed,
            ),
          ),
        );
      }
    
      void _onItemTabbed(index) {
        setState(() {
          _currentIndex = index;
          _pageController.animateToPage(
            _currentIndex,
            duration: duration,
            curve: Curves.fastOutSlowIn,
          );
        });
      }
    }

    use camera_state model to show preview

    camera_state에서 카메라 작동할 수 있는걸 만들었으므로 take_photo에서 기존것을 빼주자. 해당 부분은 코드를 참조하자.


    class _TakePhotoState extends State<TakePhoto> {
      Widget _progress = MyProgressIndicator();
    
      @override
      Widget build(BuildContext context) {
        return Consumer<CameraState>(
          builder: (BuildContext context, CameraState cameraState, Widget child){
            return Column(
              children: [
                Container(
                  width: size.width,
                  height: size.width,
                  color: Colors.black,
                  child:
                  (cameraState.isReadyToTakePhoto) ? _getPreview(cameraState) : _progress,
                ),
                // Expanded(
                //   child: InkWell(
                //     onTap: (){},
                //     child: Padding(
                //       padding: const EdgeInsets.all(common_s_gap),
                //       child: Container(
                //         decoration: ShapeDecoration(
                //           shape: CircleBorder(side: BorderSide(color: Colors.black12,width: 20,),),
                //         ),
                //       ),
                //     ),
                //   ),
                // ),
                Expanded(
                    child: OutlineButton(
                      onPressed: () {},
                      shape: CircleBorder(),
                      borderSide: BorderSide(color: Colors.black12, width: 20),
                    )),
              ],
            );
          },
        );
      }
    
      Widget _getPreview(CameraState cameraState) {
        return ClipRect(// Overflow 된 부분을 잘라줌
          child: OverflowBox( // 밖에는 사이즈가 size.width인데 이거밖으로 나가게 해주는 것임.
            alignment: Alignment.center,
            child: FittedBox(
              fit: BoxFit.fill,
              child: Container(
                width: size.width,
                height: size.width / cameraState.controller.value.aspectRatio,
                child: CameraPreview(cameraState.controller),
              ),
            ),
          ),
        );
      }
    }

    카메라 실행시 정상 작동하는것을 알 수 있다.

    카메라가 이상함...코드를 똑같이 따라했는데 비율이 맞지 않아서 (size.width > size.width / cameraState.controller.value.aspectRatio)?BoxFit.fitHeight:BoxFit.fitWidth, 로 바꿔버림.

     

    prevent memory leak by disposing the controller

    camera state를 만들고 dispose를 안만들어 줬음.  camera_state에 dispose함수를 만들어 주면 됨.


      void dispose(){
        if(_controller != null){
          _controller.dispose();
        }
        _controller = null;
        _cameraDescription = null;
        _readyTakePhoto = false;
        notifyListeners();
      }

    위와같이 해준후 camera_screen에서 dispose 안에 widget._camera_state.dispose();를 해주면 된다.

     

    take picture

    사진을 찍기 위해서. 찍은 사진을 저장하기 위해서 우선 path, path_provider라는 라이브러리를 설치해준다. 그리고 사진찍는 버튼의 onPressed에 메소드 _attemptTakePhoto(cameraState)를 생성해준다. 그리고 그 안에 시간에 대한 부분을 만들어 준다. final String timeInMilli = DateTime.now().microsecondsSinceEpoch.toString()로 시간을 글자로 변환해준다. 그리고 try catch를 만들어 주고 try에는 우선 경로와 파일명을 합쳐서 path에 넣어준다. 우선 사진 임시 저장의 경로를 가져오는 getTemporaryDirectory()가 있는데 이를 사용하려면 Future이기 때문에 async과 await를 하용해야 한다. 함수 옆에 async를 넣어주고(await getTemporaryDirectory()).path()로 경로를 가져오고 시간을가져와서 이걸 join해주면 된다. final path = join((await getTemporaryDirectory()).path, '$timeInMilli.png');를 해주면 된다. 그리고 await cameraState.controller.takePicture(path);를 해주면 사진을 찍는다.(여기서 takePictur(path)가 안된다. 강의에서는 되는데 이게 바뀐것으로 보인다. 라이브러리 버전을 낮춰야 될 수도 있다.)(camera 라이브 러리를 0.5.8^2로 변경하니 작동함. 나중에 최신버전 해볼것, 맨밑에 추가강의 있던데 거기서 알려줄듯)


      void _attemptTakePhoto(CameraState cameraState) async {
        final String timeInMilli = DateTime.now()
            .microsecondsSinceEpoch
            .toString(); //1970-01-01 부터 millisecond의 시간
        try {
          //((await getTemporaryDirectory()).path+'$timeInMilli.png')로 만들어도 되지만 위험성이 있어서 사용x Join 추천
          final path = join((await getTemporaryDirectory()).path,
              '$timeInMilli.png'); //getTemporaryDirectory는 Future사용 .path로 임시저장폴더 주소, 파일명 순서로 경로임
          await cameraState.controller.takePicture(path);
        } catch (e) {}
      }

    share post screen

    사진 찍으면 보여주는 스크린을 만들자. 우선 screens에 share_post_screen.dart를 만들어 준다. 그리고 final File imageFile;을 해준뒤 construct에 중괄호 밖에 imageFile를 넣어줘서 무조건 받아오게 한다. 그리고 return에 Image.file(imageFile)를 해줘서 찍은 이미지를 보여주게 한다. 이 함수를 _attemptTakePhoto에서 사용한다. try 하단에 File imageFile = File(paht);를 해주고 Navigator.of로 push로 SharePostScreen(imageFile)를 해주면 버튼을 누르면 사진이 찍히고 그 사진이 보이는 것을 볼 수 있다.


    take_photo.dart
      void _attemptTakePhoto(CameraState cameraState, BuildContext context) async {
        final String timeInMilli = DateTime.now()
            .microsecondsSinceEpoch
            .toString(); //1970-01-01 부터 millisecond의 시간
        try {
          //((await getTemporaryDirectory()).path+'$timeInMilli.png')로 만들어도 되지만 위험성이 있어서 사용x Join 추천
          final path = join((await getTemporaryDirectory()).path,
              '$timeInMilli.png'); //getTemporaryDirectory는 Future사용 .path로 임시저장폴더 주소, 파일명 순서로 경로임
          await cameraState.controller.takePicture(path);
    
          File imageFile = File(path);
    
          Navigator.of(context).push(MaterialPageRoute(builder: (_)=>SharePostScreen(imageFile)));
        } catch (e) {}
      }
    shara_post_screen.dart
    class SharePostScreen extends StatelessWidget {
    
      final File imageFile;
    
      SharePostScreen(this.imageFile,{Key key}) : super(key: key);
      
      @override
      Widget build(BuildContext context) {
        return Image.file(imageFile);
      }
    }

     

    반응형

    댓글

Designed by Tistory.