포스트

chatgpt로 simple flutter code 만들기(21) - 리펙토링. 자주 쓰는 코드 모듈화

앱을 만들다보니 snackbar와 loadingIndicator를 제가 자주 쓰고 있었습니다. 그래서 따로 위젯으로 빼서 모듈화 하기로 했습니다.

1. Snackbar snackbar는 다음과 같이 꺼내서 쓰면됩니다.

1
2
3
4
5
6
7
8
9
10
// Open Snackbar with text and duration
void _showSnackbar(BuildContext context, String text, int duration) {
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: Text(text, style: const TextStyle(color: Colors.white)),
      backgroundColor: Colors.black,
      duration: Duration(seconds: duration),
    ),
  );
}

'몇 초' 동안 '어떤 텍스트'를 스낵바에 띄울 것인지 전달하면 됩니다.

사용법은 다음과 같습니다.

onPressed(), onMessageReceived(), onTap() 등등 이벤트가 일어나는 곳에 스낵바를 호출할 때 간편하게 쓰면됩니다.

2. CircularProgressIndicator

로딩 상태임을 나타내주는 인디케이터 외에는 반투명 검정색의 ModalBarrier(덮은 다른 위젯들은 터치불가한 상태)으로 덮어주는 역할을 모듈화했습니다.

CircularProgressIndicator는 사용할 때 Stack(children:[])에 child로 들어가 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class LoadingIndicatorOverlay extends StatelessWidget {
  const LoadingIndicatorOverlay({super.key});

  @override
  Widget build(BuildContext context) {
    return const Stack(
      children: [
        Opacity(
          opacity: 0.5,
          child: ModalBarrier(
            dismissible: false,
            color: Colors.black,
          ),
        ),
        Center(
          child: CircularProgressIndicator(
            color: Colors.lightBlueAccent,
          ),
        ),
      ],
    );
  }
}

사용할 때는 이렇게 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
     scaffold(
              // other codes

        body: Stack(
          children: [
                      // other codes

                      // call LoadingIndicatorOverlay custom function.
                      if (isLoading) const LoadingIndicatorOverlay(),
                    ]
                    )
              )

코드에 있는 bool isLoading 은 로딩중인지 여부를 나타내는 변수입니다.

한 위젯에서 isLoading의 상태변화가 일어나면 좋겠지만, 그렇지 않은 경우 provider로 상태 변화를 저장해주는 방법이 저에게는 아직 최선이라고 생각했습니다.

그래서 저는 riverpod의 provider를 사용해서 이 변수의 상태관리를 했습니다.

위젯 빌드 오버라이드에는 다음을 선언해주고,

1
bool isLoading = ref.watch(isLoadingProvider);

코드 맨 처음에는 provider를 선언했었습니다.

1
2
// Create a provider for the loading indicator
final isLoadingProvider = StateProvider((ref) => false);

매번 상태 저장 함수를 긴 명령어로 Call 하는 것이 번거롭다고 생각되어 static클래스에 넣어서 사용해보았습니다.

1
2
3
4
5
6
7
8
9
class LoadingUtil {
  static void showLoading(WidgetRef ref) {
    ref.read(isLoadingProvider.notifier).state = true;
  }

  static void hideLoading(WidgetRef ref) {
    ref.read(isLoadingProvider.notifier).state = false;
  }
}

여담)

제가 로딩바의 모듈화를 고민했던 계기는 바로 다음 함수 때문입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Improved function to handle shared URLs and YouTube videos
  void _handleSharedText(String sharedUrl) async {
    if (_instance != null) {
      // Show the loading indicator
      LoadingUtil.showLoading(ref);

      // Use the existing instance to handle the shared URL
      String title = await fetchPageTitle(sharedUrl);
      String? siteImageUrl;

      // Check if the shared URL is a YouTube video
        if (sharedUrl.contains("youtube.com") || sharedUrl.contains("youtu.be")) {
          siteImageUrl = await fetchYouTubeThumbnail(sharedUrl);

          // Hide the loading indicator
          LoadingUtil.hideLoading(ref);
          _instance!._showSharedYouTubeContentDialog(
              sharedUrl, title, siteImageUrl ?? '');
        } else { // Handle other shared URLs
          siteImageUrl = await fetchSiteImageUrl(sharedUrl);

          // Hide the loading indicator
          LoadingUtil.hideLoading(ref);
          _instance!
              ._showSharedContentDialog(sharedUrl, title, siteImageUrl ?? '');
        }

    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Function to fetch page thumbnail from URL
Future fetchSiteImageUrl(String blogUrl) async {
  try {
    Uri uri = Uri.parse(blogUrl);
    String? siteImageUrl;

    printDebug("Fetching SiteImage: $uri");
    final response = await http.get(uri);

    if (response.statusCode == 200) {
      printDebug("Fetching SiteImage: ${response.statusCode}");
      var document = parse(response.body);
      var metaTag = document.querySelector('meta[property="og:image"]');
      siteImageUrl = metaTag?.attributes['content'];
    }
    // if the siteImageUrl is null, try to fetch the favicon
    if (siteImageUrl == null) {
      printDebug("Fetching SiteImage: Favicon");
      siteImageUrl = '${uri.scheme}://${uri.host}/favicon.ico';
    }

    // Check if the URL is valid
    // final imageResponse = await http.get(Uri.parse(siteImageUrl));
    return siteImageUrl;
  } catch (e) {
    printDebug("Error fetching SiteImage: $e");
  }
  return null;
}

원래는 로딩 지시자가 필요 없을만큼 fetching이 빠르게 일어났던 함수였는데,

구글에 연결할 때는 빠르지만, 네이버에 연결할 때는 너무 시간이 오래걸렸습니다.

그래서 기존에 잘 만들었던 다른 여러 기능들에도 분명 새로운 기능을 더하거나 새로운 테스트를 하게 될 경우 로딩 지시자가 필요해질 때가 있을 것 같았습니다.

구글에서 reponse를 받을 때는 굉장히 빠릅니다. 반면 네이버에서 받아올 떄는 굉장히 느립니다.

[영상]

+추가)

제가 자주 쓰는 printDebug모듈입니다.

1
2
3
4
5
6
void printDebug(Object message) {
  if (kDebugMode) {
    print(message);
  }
}

+추가) LoadingIndicator에 String Key를 넣어 개선.

1
2
3
4
              const LoadingIndicatorOverlay(keyIdentifier: 'fetching'),
              const LoadingIndicatorOverlay(keyIdentifier: 'settings'),
              const LoadingIndicatorOverlay(keyIdentifier: 'clearCache'),
              const LoadingIndicatorOverlay(keyIdentifier: 'resetThumbnails'),
1
2
3
// Create a provider for the loading indicator
final isLoadingProvider = StateProvider>((ref) => {});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class LoadingUtil {
  static void showLoading(WidgetRef ref, String key) {
// Update the map with the new loading state
    ref.read(isLoadingProvider.notifier).update((state) {
      final newState = Map.from(state ?? {});
      newState[key] = true;
      return newState;
    });
  }

  static void hideLoading(WidgetRef ref, String key) {
// Remove the loading state from the map
    ref.read(isLoadingProvider.notifier).update((state) {
      final newState = Map.from(state ?? {});
      newState.remove(key);
      return newState;
    });
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class LoadingIndicatorOverlay extends StatelessWidget {
  final String keyIdentifier;

  const LoadingIndicatorOverlay({super.key, required this.keyIdentifier});

  @override
  Widget build(BuildContext context) {
    return Consumer(builder: (context, ref, child) {
      final isLoadingMap = ref.watch(isLoadingProvider);
      final isCurrentKeyLoading = isLoadingMap[keyIdentifier] ?? false;

      if (isCurrentKeyLoading) {
        return const Stack(
          children: [
            Opacity(
              opacity: 0.5,
              child: ModalBarrier(
                dismissible: false,
                color: Colors.black,
              ),
            ),
            Center(
              child: CircularProgressIndicator(
                color: Colors.lightBlueAccent,
              ),
            ),
          ],
        );
      } else {
        return const SizedBox.shrink();
      }
    });
  }
}

+추가) 스낵바에 밀리초 단위를 추가하여 개선.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Open Snackbar with text and duration
void _showSnackbar(BuildContext context, String text, int duration, {bool isMillis = false}) {
  if (isMillis == false ){
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(text, style: const TextStyle(color: Colors.white)),
        backgroundColor: Colors.black,
        duration: Duration(seconds: duration),
      ),
    );
  }else{
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(text, style: const TextStyle(color: Colors.white)),
        backgroundColor: Colors.black,
        duration: Duration(milliseconds: duration),
      ),
    );
  }
}
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.