My Project/Flutter Project

1. Flutter,Firebase Riverpod을 이용하여 DropDownButton다루기.

코딩 잘 할거얌:) 2021. 10. 4. 02:34
반응형

NullSafety가 적용되지 않은 코드입니다!

 

이번에는 Flutter와 Firebase를 연동하고 StateManage 중 Riverpod을 이용하여 DropDownButton을 만들도록 하겠다. 내용이 다소 어려울 수 있지만 주석을 천천히 읽어보면 할 수 있을 것이다.

 

이거말고

 

  1. riverpod과 ConsumerWidget.
    1. riverpod에서의 Future; ChangeNotifier
    2. CustomDropDown 그리고 ConsumerWidget
    3. 초기값 설정.

riverpod과 ConsumerWidget

 

플러터에서 상태관리는 아주 중요하다. 상태란 데이터를 다른 말이라고 하면 이해하기 쉽다. 데이터의 관리이다. riverpod은 Provider의 연장선으로 Provider의 단점을 보완하여 나온 것이다. provider과 riverpod은 구분하는 것이 아니라 같이 보완하여 사용하면 된다. 그래서 provider에서 사용한 형태와 riverpod에서 사용한 형태 두 가지 모두 존재해서 사용방법이 굉장히 다양하다. 무엇을 사용해도 상관없고 정답 또한 없으니 너무 걱정하지 않아도 된다. 그렇다고 '작동만' 하는 코드는 지양하자.

들어가기에 앞서 매커니즘을 설명하겠다.

 

ConsumerWidget으로 위젯을 선언하고, ConsumerWidget에서 사용하는 watch를 이용하여 Provider를 관찰한다. ChangeNotifier를 Mixin 한 class에서 NotifyListeners를 실행하면 ConsumerWidget이 rebuild 된다.

 

이걸 모두 이해했다면, 코드는 아주 쉬울 것이다. 그래도 이해를 하나도 못 한 분들을 위해 천천히 설명하겠다.

 

riverpod에서 Future; ChangeNotifier

 

riverpod에서 Future 다루는 방법은 내가 아는 것 만 두 가지이고 구글링이나 유튜브에 검색해서 편한 걸 사용하면 된다. 나는 ChangeNotifier를 사용하여 다루는 방법을 작성하도록 하겠다.

 

우선 Firebase에서 데이터를 가져오는 부분을 먼저 작성하도록 하자. 서버에서 가져온 데이터를 가공하는 단계라고 보면 된다.

 

ChangeNotifier은 말 그대로 바뀐다면 알려준다.라고 이해하면 된다. 우리가 어떠한 데이터를 가져오고 완성이 되었다면 NotifyListeners를 사용하여 rebuild를 한다.

 

import 'package:cakeorder/StateManagement/DeclareData/cakePriceData.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter/cupertino.dart';

//PartTimer Object 선언
class PartTimer {
  final name;
  final position;
  PartTimer({this.name, this.position});
}

//RealtimeDatabase에 있는 partTimer 데이터를 가져오는 Provider.
//전역변수로 선언이 되어 모든 클래스에서 접근이 가능하다.
final partTimerProvider =
    ChangeNotifierProvider<PartTimerProvider>((ref) => PartTimerProvider());

class PartTimerProvider with ChangeNotifier {
//Firebase에서 가져온 cakeData를 저장하는 변수
  final _partTimerList = <PartTimer>[];
  
  //DropDown에 사용하기 위한 리스트.
  final _dropDownList = [];
  
  //Firebase에서 데이터 가져오는중인지 상태를 확인하는 변수
  bool _isFetch = false;
  
  //getter
  get isFetching => _isFetch;
  get getData => _partTimerList;
  get dropDownData => _dropDownList;
  
  //데이터를 가져오는 함수. 비동기로 작동한다.
  //반환값은 Future<dynamic>이다.
  Future fetchPartTimerData() async {
  
  //fetch중이라면 종료한다.
    if (_isFetch) return;
    
    //fetch를 true로 반환한다.
    _isFetch = true;
    
    //기존 데이터 초기화
    _partTimerList.clear();
    _dropDownList.clear();
    
    //데이터를 가져오는 Future부분. then, catchError, whenComplete로 바꿔서 사용할 수 있음.
    //try catch finally로 오류가 뜨더라도 앱이 먹통되는 것을 방지.
    try {
    
      //Realtime Database에 있는 cakePrice에서 Key-Value형식의 데이터를 가지고 옴.
      //await사용.
      DataSnapshot dataSnapshot =
          await FirebaseDatabase.instance.reference().child("partTimer").once();
          
      //Iterable에서 Map으로 변경한다.
      Map<String, dynamic> partTimerMap =
          Map<String, dynamic>.from(dataSnapshot.value);
          
      //Cake이름을 기준으로 사이즈-가격(Key-Value)형식으로 데이터를 저장한다.
      partTimerMap.forEach((partTimerPosition, partTimerName) {
        _partTimerList.add(
            new PartTimer(position: partTimerPosition, name: partTimerName));
            
      //cake데이터를 dropdown되었을 때 보여지는 리스트 생성한다.
        _dropDownList.add("$partTimerPosition : $partTimerName");
      });
      
      //ChangeNotifier에서는 notifiyListeners가 statefulWidget에서 setState와 동일하다고 보면 된다.
      //이 함수를 watch하는 consumerWidget을 rebuild하라는 뜻.
      // 상태변경되어 rebuild를 하는 함수.
      notifyListeners();
      
    } catch (error) {
   	//데이터가 오류가 나타났을 때
      print("ERROR fetchPartTimerData in PartTimerProvider.dart : $error");
      
    } finally {    
    //모든 작업이 끝나면 fetch status를 false로 바꿔준다.
      _isFetch = false;
    }
  }
}

 

위의 코드는 RealtimeDatabase에서 데이터를 가져오는 함수이다. 만약 cloud Firebase에서 데이터를 가져와야 한다면, DocumentSnapshot이나 QuerySnapshot을 사용하면 된다.

 

자 여기까지 따라왔다면 아주 잘 따라온 것이다. 그다음 위젯을 다뤄보자.

 

CustomDropDown 그리고 ConsumerWidget

 

단 한 개의 DropDownButton이 필요하다면 상관없지만 나는 여러 개의 DropDownButton이 필요하므로 커스텀을 해주려고 한다. 길지만 천천히 코드 읽어보면 어렵지 않으므로 잘 읽고 따라오길 바란다.

 

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class CustomDropDown extends ConsumerWidget {
  //padding값이 필요한 경우,
  //현재 상황에 맞는 provider를 받아오기
  //DropDownButton에서 선택된 값을 띄워주기 위한 textEditingController를 받아온다.
  //textEditingController를 매개변수로 가져오는 것이 아닌 Callback함수를 받아와서 textEditingController를 업데이트 시켜도 무방하다.
  CustomDropDown(
      {this.paddingEdgeInsets, this.provider, this.textEditingController});

  //null safety
  final EdgeInsetsGeometry paddingEdgeInsets;
  final provider;
  final TextEditingController textEditingController;

  @override
  //watch는 Provider를 관찰하는 역할
  //watch된 Provider에서 notifyListener()가 작동하는지 확인한다.
  Widget build(BuildContext context, ScopedReader watch) {

    //Provider를 watch
    final watchProvider = watch(provider);

    //Provider에서 isFetch가 true인 경우 데이터를 가져오는 중
    if (watchProvider.isFetching) {
      
      return Center(
        child: CupertinoActivityIndicator(),
      );
    } else {

      //Provider에서 isFetch가 false인 경우 widget을 띄운다.
      return Container(
          padding: paddingEdgeInsets,
          child: CustomDropDownWidget(
            textEditingController: textEditingController,
            
            //Provider에서 DropDownList를 매개변수로 넘겨준다.
            dropDownDataList: watchProvider.dropDownData,
          ));
    }
  }
}

//DropDown은 StatefulWidget으로 상속받아 만들어져있다.
class CustomDropDownWidget extends StatefulWidget {

  //매개변수로 받아온 List와 textEditingController.
  CustomDropDownWidget({this.dropDownDataList, this.textEditingController});
  final List dropDownDataList;
  final TextEditingController textEditingController;

  @override
  _CustomDropDownWidgetState createState() => _CustomDropDownWidgetState();
}

class _CustomDropDownWidgetState extends State<CustomDropDownWidget> {
  List dropDownDataList = [];
  TextEditingController textEditingController;

  @override
  void initState() {
    //'widget. '으로 선언된 데이터를 가져올 수 있다.
    dropDownDataList = widget.dropDownDataList;
    textEditingController = widget.textEditingController;
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    //Value : DropDownButton에서 선택된 값.
    //Items : DropDownbutton 데이터

    return DropdownButton(
      //textEditingController에 있는 text를 value로 띄워준다.
      //null이라면 아무것도 선택되지 않은 상태이다.
      value:
          textEditingController.text == "" ? null : textEditingController.text,

      //Items는 DropDown에서 보여주는 목록리스트이다.
      items: dropDownDataList
          .map((element) => DropdownMenuItem(
                child: Text(element),

                //value는 DropDownList에서 한 개가 선택되었을 때 어떤값이 반환되는 지 정한다.
                value: element,
              ))
          .toList(),
          
      //DropDownButton에서 데이터 선택하면 실행되는 함수. items에서 value값을 매개변수로 가져온다.      
      //DropDownButton에서 선택된 데이터를 띄워주기 위한 setState.
      //setState는 DropDownButton에만 작동한다.
      onChanged: (value) => 
        setState(() {
          textEditingController..text = value;
        }),
      
      icon: Icon(Icons.keyboard_arrow_down),
    );
  }
}

 

자 이제 정말 마무리 단계이다. 조금만 더 힘을 내도록 하자.

 

초기값 설정

DropDown을 이제 띄워보자.

 

import 'package:cakeorder/StateManagement/Riverpod/defineProvider.dart';
import 'package:cakeorder/addOrderPackage/Customdropdown/partTimerDropDown.dart';
import 'package:cakeorder/addOrderPackage/selectDate.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:intl/intl.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// TextEditingController를 사용하기 위해 State를 관리해야함 그래서 StatefulWidget을 사용
//
class AddOrder extends StatefulWidget {
  AddOrder({Key key}) : super(key: key);

  @override
  _AddOrderState createState() => _AddOrderState();
}

class _AddOrderState extends State<AddOrder> {

  TextEditingController partTimerTextController;

  @override
  void initState() {
    initTextEdit();
    
    //ChangeNotifier에서 선언한 함수를 동작하게 하는 명령어.
    //전역변수로 선언된 partTimerProvider를 이용한다.
    //context.read()로 작동하는데, vscode기준으로 간혹 자동완성에서 context와 혼동되어서
    //오류가 뜨는데, 이럴때는 'import 'package:flutter_riverpod/flutter_riverpod.dart';'를 추가해주면 된다.
    context.read(partTimerProvider).fetchPartTimerData();
    super.initState();
  }
  
  initTextEdit() {
    partTimerTextController = TextEditingController();
  }
  
  @override
  void dispose() {
    partTimerTextController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Container(
          padding: EdgeInsets.symmetric(vertical: 5.h, horizontal: 5.w),
          child: Column(
            children: [
            
              CustomDropDown(
              
              	//전역변수로 선언된 Provider를 넘겨준다.
                provider: partTimerProvider,
                
                //textEditingController를 넘겨준다.
                textEditingController: partTimerTextController,
              ),
            ],
          ),
        ),
      ),
    );
  }


}

 

완성!!

 

 

좀 더 깊은 내용

customDropDown에서 provider를 잘 살펴보면, provider가 dynamic으로 선언이 되어서 watchProvider.isFetching과 watchProvider.dropDownData가 오류 없이 실행이 된다. 하지만 이건 불안정한 형태이다. 들어오는 provider 모두 isFetching, dropDownData가 있다는 것을 보장할 수 없다. 따라서 ChangeNotifier를 Mixin 한 클래스에 java의 interface 같은 클래스를 결합하여 isFetching과 dropDownData를 반드시 존재하게 하면 된다. 

 

import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';

//Java의 Interface같이 사용한다.
//이 클래스를 implements한 클래스에서는 반드시 dropDownData, getData, isFetching, fetchData를
//선언해야지 사용할 수 있다.
abstract class CustomProviderInterface {
  get dropDownData;
  get getData;
  get isFetching;
  fetchData();
}

class PartTimerProvider with ChangeNotifier implements CustomProviderInterface {
  final _partTimerList = <PartTimer>[];
  final _dropDownList = [];
  bool _isFetch = false;

  @override
  get isFetching => _isFetch;

  @override
  get getData => _partTimerList;

  @override
  get dropDownData => _dropDownList;

  @override
  fetchData() async {
    if (_isFetch) return;

    _isFetch = true;
    _partTimerList.clear();
    _dropDownList.clear();

    try {
      //Realtime Database에 있는 partTimer에서 Key-Value형식의 데이터를 가지고 옴.
      DataSnapshot dataSnapshot =
          await FirebaseDatabase.instance.reference().child("partTimer").once();
      //Iterable에서 Map으로 변경한다.
      Map<String, dynamic> partTimerMap =
          Map<String, dynamic>.from(dataSnapshot.value);
      //PartTimer의 position(key)와 name(value)를 저장한다.
      partTimerMap.forEach((partTimerPosition, partTimerName) {
        print("$partTimerPosition");
        _partTimerList.add(
            new PartTimer(position: partTimerPosition, name: partTimerName));
        _dropDownList.add("$partTimerPosition : $partTimerName");
      });

      notifyListeners();
    } catch (error) {
      print("ERROR fetchPartTimerData in PartTimerProvider.dart : $error");
    } finally {
      _isFetch = false;
    }
  }
}

 

만약 extends와 implements의 차이점이 헷갈리거나 interface가 무엇인지 모른다면 아래 링크를 참고하면 좋을 듯하다.

https://masswhale.tistory.com/entry/Dart%EC%96%B8%EC%96%B4%EA%B3%B5%EB%B6%80-23Dart-Interface

 

Dart언어공부-23.Dart Interface

이번 스터디에서는 Dart의 Interface에 대해 다뤄보려 한다. Interface는 class가 꼭 선언해야(가져야)하는 메소드와 변수를 지정해주는 역할을 한다. 참고로 다른언어에서는 Interface 키워드를 사용하거

masswhale.tistory.com


오류, 지적사항 그리고 궁금한 것은 댓글 부탁드립니다.

 

728x90