[flutter13] 랜덤 숫자 생성기

한별onestar·2024년 6월 12일

flutter 실전

목록 보기
14/15
post-thumbnail

랜덤 숫자 생성기

UI 잡는 건 어렵지 않아 넘어가고 랜덤 숫자 map()함수 사용해 매핑하는 것부터 정리해 본다.

✔️ 숫자 부분 Layout

class _Body extends StatelessWidget {
  const _Body({super.key});

  @override
  Widget build(BuildContext context) {
    return Text('''
              123
              456
              789
              ''');
  }
}

_Body 클래스로 분리하여 UI를 잡아주었다. 그냥 숫자만 나타나는 거라 별거 없다.





기능 구현하기

✔️ 공통 코드 매핑하기

화면에 보이게 되는 9개의 숫자는 스타일이 모두 같아 공통으로 묶어주지 않으면 스타일을 글자 하나씩 지정을 해줘야 돼서 코드가 길어진다.

class _Body extends StatelessWidget {
  const _Body({super.key});

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Column(
        children: [
          Row(
            children: [
              '1',
              '2',
              '3',
            ].map(
                  (e) => Text(
                    e,
                    style: TextStyle(
                      color: Colors.white,
                    ),
                  ),
                )
                .toList(),
          ),
        ],
      ),
    );
  }
}

맵 함수로 텍스트 하나하나에 같은 스타일을 줄 수 있게 되었다.


class _Body extends StatelessWidget {
  const _Body({super.key});

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Column(
        children: [
          Row(
            children: [
              '1',
              '2',
              '3',
            ].map(
                  (e) => Text(
                    e,
                    style: TextStyle(
                      color: Colors.white,
                    ),
                  ),
                )
                .toList(),
          ),
          Row(
            children: [
              '4',
              '5',
              '6',
            ].map(
                  (e) => Text(
                e,
                style: TextStyle(
                  color: Colors.white,
                ),
              ),
            )
                .toList(),
          ),
          Row(
            children: [
              '7',
              '8',
              '9',
            ].map(
                  (e) => Text(
                e,
                style: TextStyle(
                  color: Colors.white,
                ),
              ),
            )
                .toList(),
          ),
        ],
      ),
    );
  }
}

이렇게 복사해 Row위젯을 3개로 만들어 주었다. 이제 9개의 숫자가 모두 같은 스타일이 적용되어 화면에 나타난다.



✔️ 중복 코드 없애기

그런데 이 코드 역시 겹치는 부분이 많아 정리를 해줘야 한다.

class _Body extends StatelessWidget {
  const _Body({super.key});

  @override
  Widget build(BuildContext context) {
    return Expanded(
      flex: 1,
      child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
          Row(
          [1, 2, 3],
          [4, 5, 6],
          [7, 8, 9],
          ].map((e) => null,

    ).toList(),)
    ,
    );
  }
}

리스트에 숫자를 넣어주고 Row 위젯에 작성해 준다. map(e) 안에는 리스트 하나가 들어가게 된다. e = [1, 2, 3]
리스트 안의 숫자들은 가로로 나와야 하기 때문에 Row() 위젯으로 감싸줘야 한다.


class _Body extends StatelessWidget {
  const _Body({super.key});

  @override
  Widget build(BuildContext context) {
    return Expanded(
      flex: 1,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          [1, 2, 3],
          [4, 5, 6],
          [7, 8, 9]
        ]
            .map(
              (e) => Row(
                children: e
                    .map(
                      (number) => Text(
                        number.toString(),
                      ),
                    )
                    .toList(),
              ),
            )
            .toList(),
      ),
    );
  }
}

리스트 안에 리스트를 넣고 첫번째 매핑해 각각 Row()에 넣어주고 리스트 안의 숫자도 하나씩 매핑해 Text() 위젯 안으로 넣어준다.

  • 결과

    그럼 이렇게 화면에 숫자가 정상적으로 출력된다.



✔️ 숫자를 이미지로 바꾸기

class _Body extends StatelessWidget {
  const _Body({super.key});

  @override
  Widget build(BuildContext context) {
    return Expanded(
      flex: 1,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          [1, 2, 3],
          [4, 5, 6],
          [7, 8, 9]
        ]
            .map(
              (e) => Row(
                children: e
                    .map(
                      (number) => Image.asset(
                        'asset/img/$number.png',
                        width: 50,
                        height: 70,
                      ),
                    )
                    .toList(),
              ),
            )
            .toList(),
      ),
    );
  }
}

그냥 텍스트 위젯을 이미지 위젯으로 변경만 하면된다.
파일명을 $number.png 가 적용될 수 있도록 0.png, 1.png, 2,png ... 이런식으로 저장했다.



✔️ 난수 생성하기

현재 이미지로 나타나는 숫자가 어쨋든 숫자의 형태를 받아야 난수를 생성할 수 있다. 그 작업을 해보자.

➕ 한번에 쓰인 문자를 리스트로 나눠 출력하기

void main() {
  
  print('apple, banana, melon');
}
  • 결과
apple, banana, melon

한 문자열 안에 텍스트를 함께 쓴 걸 리스트로 나눠 출력할 수 있다.

void main() {
  
  print('apple, banana, melon'.split(','));
}
  • 결과
[apple,  banana,  melon]

','를 기준으로 글자가 나누어져 리스트 안에 담겼다. 이걸 이용해 숫자를 랜덤으로 생성하여 이미지로 출력해 보자


class _Body extends StatelessWidget {
  const _Body({super.key});

  @override
  Widget build(BuildContext context) {
    return Expanded(
      flex: 1,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          123,
          456,
          789
        ]
        .map((e) => e.toString().split(''))
            .map(
              (e) => Row(
                children: e
                    .map(
                      (number) => Image.asset(
                        'asset/img/$number.png',
                        width: 50,
                        height: 70,
                      ),
                    )
                    .toList(),
              ),
            )
            .toList(),
      ),
    );
  }
}

리스트를 빼주고 숫자를 그냥 쓰고 매핑을 한 번 더 해준다.


난수를 생성하기 전에 먼저 상태를 위로 올려주는 작업을 한다. 모든 상태를 중앙화 해주는 것이 좋고 flutter에서는 상태관리를 글로벌 상태관리 툴을 이용하지만 (이게 뭔데) 일단 SetState를 사용한다.

상태 관리는 HomeScreen위젯에서 해보자. 우리가 관리해야 할 상태는 숫자이다.

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  final List<int> numbers = [
      123,
      456,
      789
    ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: primaryColor,
      body: SafeArea(
        child: Padding(
          padding: EdgeInsets.all(20.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              //상단바
              _Header(),

              //숫자
              _Body(
                numbers: numbers,
              ),

              //버튼
              _Footer()
            ],
          ),
        ),
      ),
    );
  }
}

class _Body extends StatelessWidget {
  List<int> numbers;

  const _Body({super.key,
  required this.numbers
  });

  @override
  Widget build(BuildContext context) {
    return Expanded(
        flex: 1,
        child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children:
                numbers.map((e) => e.toString().split(''))
        .map(
    (e) => Row(
    children: e
        .map(
    (number) => Image.asset(
    'asset/img/$number.png',
    width: 50,
    height: 70,
    ),
    )
        .toList(),
    ),
    )
        .toList
    (
    )
    ,
    )
    ,
    );
  }
}
  • 숫자가 적혀 있었던 리스트 부분을 지워주고 _Body에서 숫자를 받을 수 있게 numbers라는 변수를 생성하고 파라미터에 넣어준다.
  • HomeScreen 위젯은 StatefulWidget으로 변경해 준다.
  • 숫자 리스트를 HomeScreen 위젯으로 옮겨준다.
  • _Body 위젯에도 에러가 나니 파라미터로 numbers : numbers로 받아준다.

잊고 있었던 버튼 동작도 생각해야 된다. 버튼을 누르면 난수가 생성되는 구조이다. 숫자 상태와 버튼 상태를 HomeScreen 위젯에서 한 눈에 보이게 관리하는 것이 좋다.

class _Footer extends StatelessWidget {
  final VoidCallback onPressed;

  const _Footer({
    super.key,
    required this.onPressed
  });

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: onPressed,
      style: ElevatedButton.styleFrom(
          backgroundColor: redColor, foregroundColor: Colors.white),
      child: Text('생성하기'),
    );
  }
}

외부에서 불러온 함수를 ElevatedButton에 적용할 수 있다. 점점 뭔 말인지 모르겠다. 원래도 몰랐지만 아무튼 상태 관리를 HomeScreen위젯에서 한꺼번에 할 수 있게 됐다는 건 뭔지 알겠다. 이렇게 써놓은 파라미터? 들은 다 HomeScreen 위젯에서 받으니깐?

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  List<int> numbers = [123, 456, 789];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: primaryColor,
      body: SafeArea(
        child: Padding(
          padding: EdgeInsets.all(20.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              //상단바
              _Header(),

              //숫자
              _Body(
                numbers: numbers,
              ),

              //버튼
              _Footer(
                onPressed: () {},
              )
            ],
          ),
        ),
      ),
    );
  }
}

요로게 _Footer에더 파라미터가 붙었따.


이게 정상적으로 먹히는지 보자

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  List<int> numbers = [123, 456, 789];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: primaryColor,
      body: SafeArea(
        child: Padding(
          padding: EdgeInsets.all(20.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              //상단바
              _Header(),

              //숫자
              _Body(
                numbers: numbers,
              ),

              //버튼
              _Footer(
                onPressed: () {
                  setState(() {
                    numbers = [
                      999,
                      888,
                      777
                    ];
                  });
                },
              )
            ],
          ),
        ),
      ),
    );
  }
}

onPressed함수 안에 setState로 숫자를 변경되게 코드를 작성해 보았다.

  • 결과

그나저나 부제목 난수생성인데 난수생성 아직도 시작 안한 거 킹받네 이제 찐 난수 생성을 해보자.

import 'package:flutter/material.dart';
import 'package:rendom_number/constant/color.dart';
import 'dart:math';

먼저 최상단 import에서 dart:math를 import 해준다. 여기서 난수를 생성해 주는 기능을 제공한다.

_Footer(
  onPressed: () {
    final rand = Random();
    
    final randomNumber = rand.nextInt(1000);
    final randomNumber2 = rand.nextInt(1000);
    final randomNumber3 = rand.nextInt(1000);

    setState(() {
      numbers = [
        randomNumber,
        randomNumber2,
        randomNumber3
        ];
    });
  },
)
  • final rand = Random(); _ 랜덤 숫자
  • final randomNumber = rand.nextInt(1000); _ 정수를 생성해 주는 nextInt에 파라미터 값으로 최대값을 작성해 준다. 최대값으로 작성한 값 제외한 그 전 값들을 포함한다.

그런데 여기서도 코드 반복 발생. 수정해 주자

_Footer(
                onPressed: () {
                  final rand = Random();

                  //Set은 중복값을 들어가지 못하게 한다.
                  final Set<int> newNumbers = {};

                  //while _ 조건이 성립될 때까지 실행된다.
                  //조건 _ newNumbers의 길이가 3보다 작을 때까지
                  while(newNumbers.length < 3) {
                    final randomNumber = rand.nextInt(1000);

                    newNumbers.add(randomNumber);
                  }

                  setState(() {
                    numbers = newNumbers.toList();
                  });
                },
              )

그냥 코드 반복 하고싶다. 먼 말인지....



✔️ 코드 정리하기

class _HomeScreenState extends State<HomeScreen> {
  List<int> numbers = [123, 456, 789];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: primaryColor,
      body: SafeArea(
        child: Padding(
          padding: EdgeInsets.all(20.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              //상단바
              _Header(),

              //숫자
              _Body(
                numbers: numbers,
              ),

              //버튼
              _Footer(
                onPressed: generateRandomNumber
              )
            ],
          ),
        ),
      ),
    );
  }

  generateRandomNumber() {
    final rand = Random();

    final Set<int> newNumbers = {};

    while(newNumbers.length < 3) {
      final randomNumber = rand.nextInt(1000);

      newNumbers.add(randomNumber);
    }

    setState(() {
      numbers = newNumbers.toList();
    });
  }
}

클래스 내부에서 함수를 변수로 생성해 주고 Footer(onPressed: generateRandomNumber)로 작성해 주니 훨씬 깔끔해 졌다.





세팅 화면 올리기

플러터는 화면을 이동? 다른 화면을 보이기 위해 화면들을 스택 방식? 으로 쌓아 놓는다고 한다. 제대로 이해한 건진 모르겠지만 암튼 그런데 상단바에 나는 설정 아이콘을 넣어두었다.

class _Header extends StatelessWidget {
  final VoidCallback onPressed;

  const _Header({
    super.key,
    required this.onPressed
  });

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        Text(
          '랜덤 숫자 생성기',
          style: TextStyle(color: Colors.white, fontSize: 30),
        ),
        IconButton(
          color: redColor,
          //누르면 세팅 화면이 보이도록
          onPressed: onPressed, //onPressed라는 변수를 받는다.
          icon: Icon(Icons.settings),
        )
      ],
    );
  }
}
  • 결과



✔️ 파일 세팅하기

가장 먼저 세팅 화면 다트 파일을 만들어준다.

screen 폴더 안에 setting_screen.dart생성


📄 setting_screen.dart

import 'package:flutter/material.dart';

class SettingScreen extends StatelessWidget {
  const SettingScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text('hihi'),
      ),
    );
  }
}

세팅 화면이다. 텍스트는 임시로 넣어 두었고, 아이콘을 눌렀을 때 세팅화면을 새롭게 보여주는 거라 Scaffold로 감쌌다.



✔️ 동작 구현하기

header의 IconButton을 눌렀을 때 위 세팅화면이 나오도록 구현하기 위해 IconButton에 동작을 넣어준다.

근데 이제 상태관리는 📄 setting_screen.dart 에서!

class _Header extends StatelessWidget {
  final VoidCallback onPressed;

  const _Header({
    super.key,
    required this.onPressed
  });

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        Text(
          '랜덤 숫자 생성기',
          style: TextStyle(color: Colors.white, fontSize: 30),
        ),
        IconButton(
          color: redColor,
          onPressed: onPressed,
          icon: Icon(Icons.settings),
        )
      ],
    );
  }
}

IconButton onPressed함수에서 변수 onPressed를 받도록 한다.


class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  List<int> numbers = [123, 456, 789];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: primaryColor,
      body: SafeArea(
        child: Padding(
          padding: EdgeInsets.all(20.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              //상단바
              _Header(
                onPressed: onSettingIconPressed(),
              ),

              //숫자
              _Body(
                numbers: numbers,
              ),

              //버튼
              _Footer(onPressed: generateRandomNumber)
            ],
          ),
        ),
      ),
    );
  }
  
onSettingIconPressed() {

}

그리고 onSettingIconPressed 함수 안에 코드를 작성해 준다. 이 함수는 _Header onPressed의 파라미터 값으로 입력해 준다.

onPressed를 했을 때 일어나는 상태를 정의해 주는 onSettingIconPressed() {}함수를 작성해 보자


✅ Navigator.of(coontext)

라우트 스택을 이동할 때 사용하는 클래스이다. 사용할 수 있는 기능은 psuh, pop인데 우리는 HomeScreen화면 위에 새로운 스크린인 SettingScreen을 올리는 거라 push를 사용한다.

Navigator.of(context).push();
➕ 위젯은 클래스 내부에서 전역적으로 컨텍스트를 접근할 수 있다. context는 위 _HomeScreenState build 함수의 context랑 같고 위젯 트리가 어떻게 구성되어 있는지 코드상으로 알아야하기 때문에 context를 넣어준다는데 이게 먼말이긔

그냥 모르겠으니까 내가 이해한 바로는 context로 위젯 상관관계를 나타내는 머 그런거 같음

그리고 이제 SettingScreen 화면을 데려올 데 그냥 냅다 데려오면 안 되고 클래스를 하나 이용해 주어야 한다.

✅ MaterialPageRoute()

  • 페이지 간의 전환을 관리하는 클래스이다. 이 클래스는 화면 전환 애니메이션과 함께 새로운 페이지를 네비게이션 스택에 추가하는 데 사용된다.
  • builder는 새로운 페이지의 위젯을 빌드하는 함수이다. BuildContext를 매개변수로 받아 페이지의 위젯을 반환한다.
.
.
.
_Header(
  onPressed: onSettingIconPressed,
),
.
.
.            
  onSettingIconPressed() {
    Navigator.of(context).push(
        MaterialPageRoute(
        builder: (BuildContext context){
          return SettingScreen();
        },
      )
    );
  }

이렇게 onSettingIconPressed 함수를 세팅했고, _Hedaer의 아이콘버튼을 누르면 SettingScreen 화면을 리턴한다.


  • 결과



✔️ 레이아웃 구현하기

📄 setting_screen.dart

import 'package:flutter/material.dart';
import 'package:rendom_number/constant/color.dart';

class SettingScreen extends StatelessWidget {
  const SettingScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: primaryColor,
      body: SafeArea(
        child: Padding(
          padding: EdgeInsets.symmetric(
            horizontal: 20
          ),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              ElevatedButton(
                style: ElevatedButton.styleFrom(
                  backgroundColor: redColor, //버튼 색상
                  foregroundColor: Colors.white, //글자 색상
                ),
                onPressed: (){
                  Navigator.of(context).pop();
                },
                child: Text('저장'),
              )
            ],
          ),
        ),
      )
    );
  }
}
  • 결과

여기서 다시 홈 화면으로 돌아가게 하는 ElevatedButton의 onPressed함수를 보면

onPressed: (){
  Navigator.of(context).pop();
},

push로 setting 페이지를 스택에 올렸으니 홈으로 돌아가려면 현재 라우트 스택 가장 위의 세팅 스크린을 지워야 한다. 그걸 지워주는 것이 pop이다.
이렇게 해서 버튼이 잘 동작하는 것!





세팅 화면에 숫자 랜더링하기

✔️ 레이아웃

class SettingScreen extends StatelessWidget {
  const SettingScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: primaryColor,
      body: SafeArea(
        child: Padding(
          padding: EdgeInsets.symmetric(
            horizontal: 20
          ),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              _Number()
              _Slider()
              _Button()
            ],
          ),
        ),
      )
    );
  }
}

class _Button extends StatelessWidget {
  const _Button({super.key});

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      style: ElevatedButton.styleFrom(
        backgroundColor: redColor, //버튼 색상
        foregroundColor: Colors.white, //글자 색상
      ),
      onPressed: (){
        Navigator.of(context).pop();
      },
      child: Text('저장'),
    );
  }
}

_Button 클래스를 만들어 위젯을 분리하고 숫자 부분의 _Number 클래스와 _Slider 클래를 추가한 후 코드를 추가로 작성해 본다.



✔️ 숫자 랜더링하기

먼저 _SettingScreen 위젯을 StatefulWidget으로 변경해 준다. 상태관리는 이 위젯에서 해준다.

class SettingScreen extends StatefulWidget {
  const SettingScreen({super.key});

  @override
  State<SettingScreen> createState() => _SettingScreenState();
}

class _SettingScreenState extends State<SettingScreen> {
  double maxNumber = 1000;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        backgroundColor: primaryColor,
        body: SafeArea(
          child: Padding(
            padding: EdgeInsets.symmetric(horizontal: 20),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [_Number(), _Slider(), _Button()],
            ),
          ),
        ));
  }
}

변수를 _SettingScreenState 클래스 내부에 선언한다.


class _Number extends StatelessWidget {
  final double maxNumber;

  const _Number({
    required this.maxNumber,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Container(
        child: Row(
          children: e
              .map(
                (number) => Image.asset(
              'asset/img/$number.png',
              width: 50,
              height: 70,
            ),
          )
              .toList(),
        ),
      ),
    );
  }
}

숫자를 이미지로 바꾸는 로직을 HomeScreen _Body에서 복사해 왔다.
그런데 여기에서는 리스트 형태로 숫자를 가지고 있는 게 아니므로 코드 수정이 필요하다.

받아야 하는 숫자는 하나이므로 e를 maxNumber.toString().split('')로 수정해 준다.

children: maxNumber.toString().split('')
  .map(
    (number) => Image.asset(
    'asset/img/$number.png',
    width: 50,
    height: 70,
  ),
)

그런데 이렇게 하면 maxNumber가 double로 설정되어 있어 소숫점을 포함해 버려 Unable to load asset: "asset/img/..png".라며 에러가 난다.

children: maxNumber.toInt().toString().split('')

maxNumber를 정수로 바꾸어주면 된다.





공통 컴포넌트 제작하기

현재 HomeScreen에서 랜덤 숫자를 랜더링 하는 기능과 SettingScreen에서 숫자를 랜더링하는 기능의 코드가 겹치고 있다. 이걸 공통 컴포넌트로 묶어보자.


먼저 로직이 같은 것은 컴포넌트로 묶어 코드를 따로 작성하기 때문에 lib폴더 안에 component폴더를 추가해 준다.


profile
한별잉

0개의 댓글