Flutter

[Flutter] 유저에게 앱 권한 요청하기 & A dependency may only have one source 에러 해결

hminor 2023. 8. 7. 16:09

유저에게 앱 권한 요청하기

  • 모바일에서 유저의 모바일에 있는 데이터를 사용하기 위해선 권한 요청을 하게 되는데
  • 권한 요청을 하기 위해선 패키지를 하나 설치해야 한다.
    • 프로젝트에 있는 pubspec.yaml에서 dependencies 아래 부분에 permission_handler를 작성해주면 되는데 우선은 강사님과 버전을 맞추기 위해 아래와 같이 작성
    dependencies:
      flutter:
        sdk: flutter
      permission_handler: ^8.3.0
    
    • 이후 permission_handler 를 선택 후 전구를 클릭 하여
      • Pub get을 클릭하면 설치가 된다.
    • 다만 여기서 **A dependency may only have one source** 와 같은 에러가 발생하게 된다면
      • 문제는 들여쓰기가 잘못되어있다는 점!
      • 예시로 아래와 같이 작성하면 **위와 같이 들여쓰기**를 해주면 된다.
      dependencies:
        flutter:
          sdk: flutter
      	  permission_handler: ^8.3.0
      
  • 이후 main.dart 파일로 다시 돌아와서 상단에 import로 불러올 코드로
    • import 'package:permission_handler/permission_handler.dart'; 를 작성해주면 된다.
  • 그리고 Android 기기에 셋팅을 하겠다고 하면
    • android 폴더에서 [gradle.properties](<http://gradle.properties>) 로 들어가서 아래의 코드처럼 작성되어 있는지 확인
    org.gradle.jvmargs=-Xmx1536M
    android.useAndroidX=true
    android.enableJetifier=true
    
    • app 폴더의 build.gradle 파일로 들어가서 아래와 같이 코드가 작성되어 있는지 확인하기
      • 숫자가 아닌 두 번째처럼 문자로 기입이 되어있다면 그냥 넘어가기
        • android{ compileSdkVersion 31 }
        • android{ compileSdkVersion flutter.compileSdkVersion }
    • app 폴더의 src 폴더에 있는 AndroidManifest.xml 파일을 클릭해서 코드 추가 하기
      • 현재 아래는 CONTACTS에 대한 읽기, 쓰기 권한을 부여하는 것인데
      • 나중에는 필요한 다른 기능들도 추가하면 권한을 요청할 수 있을 것!
      <manifest xmlns:android="http://schemas.android.com/apk/res/android">
          
          <uses-permission android:name="android.permission.READ_CONTACTS"/>
          <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
    • 이후 main.dart로 돌아와서 상단에 import 하기
    import 'package:permission_handler/permission_handler.dart';
    
    … 이거 휴대폰에 적용하는거 보이려고 포맷도 하고 버전도 변경하게 난리를 피웠다가 하다보니 그냥 어찌어찌 되었는데… 그게 며칠 동안 이런건지…
  • 유저에게 허락받는 방법
    • 이제 permisson에 대한 셋팅을 완료했으니 함수를 작성해서 간단하게 요청하기
    getPermission() async{
        var status = await Permission.contacts.status; // 연락처 권한을 주었는지 대한 여부
        if (status.isGranted) {
          print('허락됨');
        } else if (status.isDenied) {
          print('허락됨');
          Permission.contacts.request(); // 허락해달라고 팝업띄우는 코드
        }
      }
    
    • 이후 해당 함수는 연락처 앱에 들어갈 때 한 번만 요청을 하면 되기에 JavaScript의 useEffect 와 유사한 것으로 initState()가 있다
    • initState는 init을 작성 후 자동완성하면 된다.
    @override
      void initState() { // 위젯이 로드될 때 한번 실행
        super.initState();
        getPermission();
      }
    
    • 이후 재실행하게 되면 요청을 하게 되는데
      • 여기서 조심해야할 부분은
        • Android 11이상, IOS 환경에서는 거절을 2번 이상하게 되면 팝업이 더 이상 생성되지 않기에 조심해야 한다.
      • 이유는 앱 정책상 사람들이 계속해서 거절하는 경우도 있는데 매번 거절을 하는 것 자체가 불편한 반복 경험을 발생하는 것이기에 그럴 듯?
      • 그래서 만약 다시 문구를 발생시켜서 허락을 원한다면
        • openAppSettings() 를 작성하게 해주면 셋팅을 다시 할 수 있게 설정 창으로 보내주게 된다.
        @override
          void initState() { // 위젯이 로드될 때 한번 실행
            super.initState();
            getPermission();
            openAppSettings();
          }
        
         
  • 마지막으로 지금까지의 main.dart 코드 전체
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';

void main() {
  runApp(
      MaterialApp(
        home: MyApp()
      )
  );
}

class MyApp extends StatefulWidget {
  MyApp({super.key});
  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {

  getPermission() async{
    var status = await Permission.contacts.status; // 연락처 권한을 주었는지 대한 여부
    if (status.isGranted) {
      print('허락됨');
    } else if (status.isDenied) {
      print('허락됨');
      Permission.contacts.request(); // 허락해달라고 팝업띄우는 코드
    }
  }

  // @override
  // void initState() { // 위젯이 로드될 때 한번 실행
  //   super.initState();
  //   // getPermission();
  //   openAppSettings();
  // }

  var a = 1;
  var total = 3;
  var person = [['메시', 0, '010-1234-5678'], ['날강두', 0, '010-1234-5678'], ['홀란', 0, '010-1234-5678']];
  // var cnt = [0, 0, 0];
  var title = 'Contact';

  changeNumber(){
    setState(() {
      total++;
    });
  }

  changeState(value){
    setState(() {
      person.add(value);
    });
  }

  deleteState(idx){
    setState(() {
      person.removeAt(idx);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: (){
          showDialog(context: context, builder: (context){
            return Dialog(
              child: Modal(state: title, changeNumber: changeNumber, changeState:changeState)
            );
          });
        },
      ),
      appBar: AppBar(title: Text(total.toString()),actions: [
        IconButton(onPressed: (){
          getPermission();
        }, icon: Icon(Icons.contacts))
      ],),
      body: ListView.builder(
          itemCount: person.length,
          itemBuilder: (c,i){
            return ListTile(
              leading: Text(person[i][1].toString()),
              title: SingleChildScrollView(
                scrollDirection: Axis.horizontal,
                child: Row(
                  crossAxisAlignment: CrossAxisAlignment.end,
                  children: [
                    Text(person[i][0].toString()),
                    Text('  '),
                    Text(person[i][2].toString()),
                    TextButton(onPressed: (){
                      deleteState(i);
                    }, child: Text('삭제'))
                  ],
                ),
              ),
              trailing: TextButton(
                child: Text('좋아요'),
                onPressed: (){
                  setState(() {
                    person[i][1] = (person[i][1] as int) +1;
                  });
                },
              ),
            );
          }
      ),
    );
  }
}

class Modal extends StatelessWidget {
  Modal({super.key, this.state, this.changeNumber, this.changeState});
  final state;
  final changeNumber;
  var changeState;
  var inputData = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 400,
      margin: EdgeInsets.fromLTRB(30, 0, 30, 0),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Container(
            margin: EdgeInsets.all(30),
            child: Text(state, style: TextStyle(fontSize: 40, fontWeight: FontWeight.w900)),
          ),
          Container(
            margin: EdgeInsets.all(30),
            child: TextField( controller: inputData, style: TextStyle(fontSize: 30),),
          ),
          Container(
            margin: EdgeInsets.all(30),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.end,
              children: [
                Container(
                  margin: EdgeInsets.fromLTRB(20, 0, 20, 0),
                  child: TextButton(onPressed: (){
                    Navigator.pop(context);
                  }, child: Text('Cencel', style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),)),
                ),
                Container(
                  margin: EdgeInsets.fromLTRB(20, 0, 20, 0),
                  child: TextButton(onPressed: (){
                    if (inputData.text != ''){
                      changeState([inputData.text.toString(), 0, '010-1234-5678']);
                      Navigator.pop(context);
                    }
                  }, child: Text('OK', style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),)),
                ),
              ],
            ),
          )
        ],
      ),
    );
  }
}