Flutter/Flutter로 웹툰 앱 만들기

[노마드코더: Flutter 로 웹툰 앱 만들기] #6.16 Url Launcher

유호야 2022. 12. 20. 09:36
반응형

지난 시간 버튼에 outline 주는 challenger가 있었는데, 그림자까지 넣어보았다

 

detail_screen.dart

import 'package:flutter/material.dart';
import 'package:toonflix/models/webtoon_detail_model.dart';
import 'package:toonflix/models/webtoon_episode_model.dart';
import 'package:toonflix/services/api_service.dart';

class DetailScreen extends StatefulWidget {
  final String title, thumb, id;
  const DetailScreen({
    super.key,
    required this.title,
    required this.thumb,
    required this.id,
  });

  @override
  State<DetailScreen> createState() => _DetailScreenState();
}

class _DetailScreenState extends State<DetailScreen> {
  late Future<WebtoonDetailModel> webtoon;
  late Future<List<WebtoonEpisodeModel>> episodes;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    webtoon = ApiService.getToonById(widget.id);
    episodes = ApiService.getLatestEpisodesById(widget.id);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        elevation: 3,
        backgroundColor: Colors.white,
        foregroundColor: const Color.fromARGB(255, 13, 10, 0),
        title: Text(widget.title,
            style: const TextStyle(
              color: Colors.black,
            )),
      ),
      body: SingleChildScrollView(
        child: Padding(
          padding: const EdgeInsets.all(50),
          child: Column(
            children: [
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Hero(
                    tag: widget.id,
                    child: Container(
                      width: 250,
                      clipBehavior: Clip.hardEdge,
                      decoration: BoxDecoration(
                        borderRadius: BorderRadius.circular(15),
                        boxShadow: [
                          BoxShadow(
                            blurRadius: 15,
                            offset: const Offset(10, 10),
                            color: Colors.black.withOpacity(0.3),
                          )
                        ],
                      ),
                      child: Image.network(widget.thumb),
                    ),
                  ),
                ],
              ),
              const SizedBox(
                height: 25,
              ),
              FutureBuilder(
                builder: (context, snapshot) {
                  if (snapshot.hasData) {
                    return Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(snapshot.data!.about,
                            style: const TextStyle(
                              fontSize: 16,
                            )),
                        const SizedBox(
                          height: 15,
                        ),
                        Text('${snapshot.data!.genre} / ${snapshot.data!.age}',
                            style: const TextStyle(
                              fontSize: 16,
                            )),
                      ],
                    );
                  }
                  return const Text('.....');
                },
                future: webtoon,
              ),
              const SizedBox(
                height: 25,
              ),
              FutureBuilder(
                future: episodes,
                builder: (context, snapshot) {
                  if (snapshot.hasData) {
                    return Column(
                      children: [
                        for (var episode in snapshot.data!)
                          Container(
                              margin: const EdgeInsets.only(
                                bottom: 5,
                              ),
                              decoration: BoxDecoration(
                                  boxShadow: const [
                                    BoxShadow(
                                      color: Color.fromARGB(255, 68, 68, 72),
                                      spreadRadius: 1,
                                      blurRadius: 3,
                                      offset: Offset(5, 6),
                                    )
                                  ],
                                  color: Colors.white,
                                  border: Border.all(
                                    width: 1,
                                    color:
                                        const Color.fromARGB(255, 13, 92, 47),
                                  ),
                                  borderRadius: BorderRadius.circular(25)),
                              child: Padding(
                                padding: const EdgeInsets.symmetric(
                                    vertical: 12, horizontal: 30),
                                child: Row(
                                  mainAxisAlignment:
                                      MainAxisAlignment.spaceBetween,
                                  children: [
                                    Text(episode.title,
                                        style: const TextStyle(
                                          fontSize: 15,
                                          color:
                                              Color.fromARGB(255, 17, 112, 20),
                                        )),
                                    const Icon(
                                      Icons.chevron_right_rounded,
                                      color: Color.fromARGB(255, 9, 112, 13),
                                    ),
                                  ],
                                ),
                              )),
                      ],
                    );
                  }
                  return Container();
                },
              )
            ],
          ),
        ),
      ),
    );
  }
}

 


#6.16 Url Launcher

flutter에서 브라우저 여는 법을 배울 것이다.

그러기 위해서는 플러터 개발자들이 만든 url_launche라는 패키지를 설치해야 한다.

 

url_launcher | Flutter Package

Flutter plugin for launching a URL. Supports web, phone, SMS, and email schemes.

pub.dev

$ flutter pub add url_launcher

그리고 configuration 파일에 무언가를 복사 붙여넣기 해야 한다. 

ios로 작업하고 있다면 info.plist 파일에 아래 코드를 붙여넣어야 하고

 

안드로이드로 작업하고 있다면 아래 xml을 AndroidMenifest.xml에 추가해주어야 한다. 

 

보다시피 어떤 종류의 url을 열 건지 명시해야 한다. 

url launcher http url만 실행하는 것이 아니라 sms url 이나 telephone url도 실행할 수 있다

우리의 경우에는 그냥 http를 사용할 것이다. 

(pub.dev에서 url_launcher example을 참고한다)

나는 안드로이드를 사용중이기 때문에 

AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.toonflix">
   <application
        android:label="toonflix"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <!-- Specifies an Android theme to apply to this Activity as soon as
                 the Android process has started. This theme is visible to the user
                 while the Flutter UI initializes. After that, this theme continues
                 to determine the Window background behind the Flutter UI. -->
            <meta-data
              android:name="io.flutter.embedding.android.NormalTheme"
              android:resource="@style/NormalTheme"
              />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
    <queries>
    <!-- If your app checks for SMS support -->
    <intent>
        <action android:name="android.intent.action.VIEW" />
        <data android:scheme="https" />
    </intent>
    <!-- If your app checks for call support -->
    <intent>
        <action android:name="android.intent.action.VIEW" />
        <data android:scheme="https" />
    </intent>
    </queries>
</manifest>

방금 flutter가 실행되는 플랫폼의 configuration 파일인 xml파일과 .plist 파일을 수정했기 때문에 비주얼 스튜디어 코드를 재시작한다. 

xCode / Android 파일을 변경했기 때문에 재시작 rebuild 해야 한다.

hot reloading 은 코드 변경만 감지하기 때문에 적용되지 않는다.


 이제 detail_screen에서
방금 설치한 url launch를 사용하기 위한 method를 하나 작성해보자 

중간에 에러 떠서 헤맸지만 해결하고

 

[Flutter] Building with plugins requires symlink support

Flutter: Building with plugins requires symlink support Exception: Building with plugins requires symlink support. Please enable Developer Mode in your system settings. Run start ms-settings:developers to open settings. Flutter: Building with plugins requi

ninetynine-2026.tistory.com

 

LaunchUrl은 Future을 가져다주는 function이기 때문에
Future를 주니까 우리는 await 해야 한다.  

이제 버튼에 Gesture를 추가해보자

 

onButtonTap은 DetailScreenState에 있을 필요가 없다. Episode widget으로 옮겨주자 => 왜?

onTap: onButtonTap,

onTap했을 때 onButtonTap을 실행해주세요!

 widget.id는 detail screen의 아이디를 의미하는데
 
그게 바로 user가 클릭한 webtoon이다

 

        <!-- android:usesCleartextTraffic="true" -->

에러가 생다

https://stackoverflow.com/questions/52707918/webview-showing-err-cleartext-not-permitted-although-site-is-https

 

WebView showing ERR_CLEARTEXT_NOT_PERMITTED although site is HTTPS

I'm starting to work on an app on Android, so I don't have much. All I have is just a WebView so far. I created the project in Android Studio, and my project got set as an Android InstantApp. I'm not

stackoverflow.com

 

반응형