Flutter에서 서버 데이터로 동적 TextField 관리하기 : 개선된 사용자 입력 솔루션

2024. 4. 2. 21:17프로그래밍/Flutter

Flutter에서 서버 데이터로 동적 TextField 관리하기 : 개선된 사용자 입력 솔루션

 

 

문제 상황

  • 서버로부터 랜덤한 갯수의 데이터를 받아서 ListView.Builder를 통해 만들어진 TextField의 값으로 넣어야 하는 상황으로
    2가지 문제가 있다
    • TextField를 TextFormField로 바꾸고 initialValue로 사용시 처음 출력은 잘 되지만 해당 리스트를 삭제했을 때 문제가 발생한다 (데이터는 변경이 되지만 초기 값이라서 화면단에 적용이 안됨)
    • 각 TextField를 TextEditingController로 관리하자니 랜덤한 갯수의 데이터를 받기 때문에 모두 준비할 수가 없다

해결 방안

  1. TextField를 Stack위젯으로 감싸고 유저가 상호작용할 수 있는  Container를  TextField위젯의 위에 배치한다
  2. String형태의 flag변수를 하나 선언하고 기본값으로 ‘default’를 할당한다
  3. 탭이벤트가 발생하변 해당 List의 index를 String형태로 flag변수에 할당한다
  4. TextField의 index가 flag변수와 동일하다면 화면에 보여주도록 하고 TextField의 autofocus를 true로 설정한다
  5. TextField의 값을 변경하고 onSubmitted 이벤트가 발생하면 해당 값을 저장한다

코드

//...
List<Map<String, dynamic>> exampleList = [...] // 임의의 리스트
String selectedInterestAreaTextFieldFlag = 'default'; // flag 변수
//...

ListView.builder(
	itemCount: exampleList.length,
	itemBuilder: (BuildContext context, int index) {
	//...
	
		Stack(
		  children: [
			//* Container
		    GestureDetector(
		      onTap: () {
		        setState(() {
		          selectedInterestAreaTextFieldFlag = index.toString();
		        });
		      },
		      child: Container(
		        width: 128.w,
		        height: 44.w,
		        margin: EdgeInsets.only(left: 8.w),
		        padding: EdgeInsets.only(left: 12.w),
		        decoration: BoxDecoration(
		          borderRadius:BorderRadius.circular(25),
		          border: Border.all(
		            color: Colors.black,
		            width: 1
		          ),
		          color: Colors.grey,
		        ),
		        child: Align(
		          alignment: Alignment.centerLeft,
		          child: Text(examplelist[index]['name'],
		            style: TextStyle(
		              fontSize: 13,
		              color: // 조건에 따라 값이 있으면 검정색으로, 없다면 회색으로 
		            ),
		          ),
		        )
		      ),
		    ),
	
		    //* Container의 탭이벤트가 발생하면 나오는 텍스트 필드
		    if(selectedInterestAreaTextFieldFlag==index.toString()) 
		    Container(
		      width: 128.w,
		      height: 44.w,
		      margin: EdgeInsets.only(left: 8.w),
		      decoration: BoxDecoration(
		        borderRadius:BorderRadius.circular(25),
		        border: Border.all(
		          color: Colors.black,
		          width: 1
		        ),
		        color: Colors.grey,
		      ),
		      child: TextField(
		        magnifierConfiguration: TextMagnifierConfiguration.disabled,
		        autocorrect: false,
		        autofocus: true,
		        maxLines: 1,
		        maxLength: 10,
		        style: TextStyle(
		          fontSize:13
		        ),
		        decoration: InputDecoration(
		          hintText: // 힌트 텍스트
		          hintStyle: TextStyle(
		            fontSize: 13,
		            fontFamily: "NotoSans",
		            color: Colors.grey
		          ),
		          enabledBorder: OutlineInputBorder(
		            borderRadius: BorderRadius.circular(22),
		            borderSide: BorderSide(color: Colors.grey)
		          ),
		          focusedBorder: OutlineInputBorder(
		            borderRadius: BorderRadius.circular(22),
		            borderSide: BorderSide(color: Colors.grey)
		          ),
		          filled: true,
		          fillColor: Colors.white,
		          contentPadding: EdgeInsets.fromLTRB(15,15,15,15),
		          counterText: ''
		        ),
		        onSubmitted: (value) {
		          setState(() {
		            selectedInterestAreaTextFieldFlag = 'default'; // flag변수 초기화
		          });
		          examplelist[index]['name'] = value; // 값 변경
		        },
		      ),
		    ), 
		  ],
		)
	}
)

 

위의 코드는 다음과 같이 성능 및 사용성 개선할 수 있다

  1. Stack 내부에서 if문으로 Container가 조건부 렌더링하는데 별도의 함수나 위젯으로 분리하면 가독성이 좋아질 수 있다
  2. ListView.builder가 재빌드 될 때마다 모든 Stack이 재계산되므로 성능을 고려해 좀 더 효율적인 상태 관리 방법을 찾을 수 있다
  3. 사용자가 잘못된 입력을 한 경우나 네트워크 오류 등의 예외 상황에 대한 처리가 필요하다
  4. 이런 패턴이 여러곳에서 사용될 경우를 위해 재사용 가능한 위젯으로 수정할 수 있다

해당 개선내용을 적용한 코드는 아래에 작성한다

// 별도의 위젯으로 분리

class EditableTextField extends StatelessWidget {
  final int index;
  final bool isSelected;
  final String text;
  final Function(String) onSubmitted;

  EditableTextField({
    required this.index,
    required this.isSelected,
    required this.text,
    required this.onSubmitted,
  });

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        GestureDetector(
          onTap: () {
            // 상태 변경 로직
            setState(() {
              selectedInterestAreaTextFieldFlag = index.toString();
            });
          },
          child: Container(
            // Container 설정
            width: 128.w,
            height: 44.w,
            margin: EdgeInsets.only(left: 8.w),
            padding: EdgeInsets.only(left: 12.w),
            decoration: BoxDecoration(
              borderRadius:BorderRadius.circular(25),
              border: Border.all(
                color: Colors.black,
                width: 1
              ),
              color: Colors.grey,
            ),
            child: Align(
              alignment: Alignment.centerLeft,
              child: Text(
                text,
                style: TextStyle(fontSize: 13, color: Colors.black),
              ),
            ),
          ),
        ),
        Visibility(
          visible: isSelected,
          maintainState: true,
          child: TextField(
            // TextField 설정
            magnifierConfiguration: TextMagnifierConfiguration.disabled,
            autocorrect: false,
            autofocus: true,
            maxLines: 1,
            maxLength: 10,
            style: TextStyle(
              fontSize:13
            ),
            decoration: InputDecoration(
              hintText: // 힌트 텍스트
              hintStyle: TextStyle(
                fontSize: 13,
                fontFamily: "NotoSans",
                color: Colors.grey
              ),
              enabledBorder: OutlineInputBorder(
                borderRadius: BorderRadius.circular(22),
                borderSide: BorderSide(color: Colors.grey)
              ),
              focusedBorder: OutlineInputBorder(
                borderRadius: BorderRadius.circular(22),
                borderSide: BorderSide(color: Colors.grey)
              ),
              filled: true,
              fillColor: Colors.white,
              contentPadding: EdgeInsets.fromLTRB(15,15,15,15),
              counterText: ''
            ),
            onSubmitted: onSubmitted,
          ),
        ),
      ],
    );
  }
}
// ListView.builder에서 사용

ListView.builder(
  itemCount: exampleList.length,
  itemBuilder: (BuildContext context, int index) {
    bool isSelected = selectedInterestAreaTextFieldFlag == index.toString();
    String text = exampleList[index]['name'];

    return EditableTextField(
      index: index,
      isSelected: isSelected,
      text: text,
      onSubmitted: (value) {
        // 값 변경 처리
        setState(() {
          selectedInterestAreaTextFieldFlag = 'default';
          exampleList[index]['name'] = value;
        });
      },
    );
  },
)