
マイケル
みなさんこんにちは!
マイケルです!
マイケルです!

エレキベア
クマ〜〜〜〜〜〜

マイケル
突然ですが、みなさんは
お気に入りのアダルトサイトのアプリがないか探したこと
はありませんか?
お気に入りのアダルトサイトのアプリがないか探したこと
はありませんか?

エレキベア
(マジで突然クマ・・・。)

マイケル
そう健全な男子諸君なら一度はあるはず!
しかしAndroidはともかく、iOSアプリは審査が厳しいので
アダルト系はほぼ皆無状態ですよね・・・。
しかしAndroidはともかく、iOSアプリは審査が厳しいので
アダルト系はほぼ皆無状態ですよね・・・。

マイケル
だったら自分で作ってしまえばいいのではないか!
そう考えた私は、このようなアプリを作りました。
そう考えた私は、このようなアプリを作りました。


マイケル
みんな大好きな有名アダルトサイト、
Pornhubの検索アプリです!!
Pornhubの検索アプリです!!

エレキベア
(気合が入ってるクマ・・・。)

マイケル
今回はこちらのアプリの実装内容を紹介します!
みなさんも是非、お気に入りのアダルトサイト検索アプリ作成
にトライしてみてくださいね!
みなさんも是非、お気に入りのアダルトサイト検索アプリ作成
にトライしてみてくださいね!

ジャギィ
(やってみようかな・・・。)

エレキベア
く、クマは熊だからみないクマが
手伝ってあげるクマ〜〜〜〜
手伝ってあげるクマ〜〜〜〜

マイケル
それでは早速みていこう!!
Flutterとは

マイケル
このアプリは、モバイルアプリケーションフレームワークである
「Flutter(フラッター)」を使用して開発しました!
「Flutter(フラッター)」を使用して開発しました!

エレキベア
Flutterって何クマ??

マイケル
Flutterはこのような特徴があるフレームワークなんだ!
Flutterとは
- Googleによって開発されたモバイルアプリケーションフレームワーク!
- マテリアルデザイン(※1)を基調しており、美しく統一感のあるコンポーネントを使用可能!
- ホットリロード(※2)により高速に開発を行うことが可能!
- マルチプラットフォームであり、iOS / Androidアプリの開発が可能!
(※1) マテリアルデザイン ・・・ Googleが提唱したデザインシステム
(※2) ホットリロード ・・・ 開発しながらリアルタイムに変更を反映できる機能
(※2) ホットリロード ・・・ 開発しながらリアルタイムに変更を反映できる機能


エレキベア
またすごいのが出てきたクマね〜〜〜

マイケル
似たようなフレームワークで React Native もあるけど、
最近勢いが増しているFlutterを使用することにしたよ!
最近勢いが増しているFlutterを使用することにしたよ!

マイケル
ホットリロードがほんとに便利で、
開発が気持ちよかったです・・・。
開発が気持ちよかったです・・・。


エレキベア
(意味深に聞こえるクマ・・・。)
でもなんだかワクワクしてきたクマ〜〜〜!!
でもなんだかワクワクしてきたクマ〜〜〜!!

マイケル
よーし、それじゃ開発にとりかかるぞ!
参考書籍

マイケル
Flutterで開発するにあたって、
下記の書籍を参考にさせていただきました!
下記の書籍を参考にさせていただきました!

マイケル
Flutterの実装内容が豊富なため、辞書的な使い方になるかと思います!
サンプルアプリは少ないため、何でもいいから一つ作りたい!という方には
下記の書籍をおすすめします!
サンプルアプリは少ないため、何でもいいから一つ作りたい!という方には
下記の書籍をおすすめします!

マイケル
2020年12月現在、参考書は大体この3つになると思います。

エレキベア
精進するクマ〜〜〜・・・。

ジャギィ
(勉強してみようかな)
検索アプリの設計

マイケル
それでは今回作ったアプリの仕様を簡単に説明します!

エレキベア
待ってましたクマ


マイケル
① 検索条件を入力後、表示件数を選択して検索ボタンを押下する。
② Pornhubのサイトをスクレイピングして動画情報の一覧を取得する。
③ 動画をクリックすると該当動画のURLを内部ブラウザとして表示する
というシンプルな仕様になります!

エレキベア
動画視聴のページはURLから表示することで
実装も最小限に抑えているクマね
実装も最小限に抑えているクマね

マイケル
広告抜いたら営業妨害になりそうだしね。
このようなアプリを開発・使用することで
得られるメリットは大きく下記3点になります!
このようなアプリを開発・使用することで
得られるメリットは大きく下記3点になります!
アダルト検索アプリを自作するメリット
- 自分で開発しているため、安全性が保たれる。
- 検索件数等、条件を自分でカスタマイズできる。
- ブラウザを毎回開かずにすみ、悪意ある広告によるクラッシュも回避できる。

マイケル
特にWEBに落ちている怪しいアプリをインストールせずにすむ
というのは安全面的にかなりいいかと思います!
というのは安全面的にかなりいいかと思います!

エレキベア
そういうのはウイルスが含まれていたり
広告の数も半端なかったりするクマからね
広告の数も半端なかったりするクマからね

マイケル
自分で作ったアプリだと愛着も湧くだろうしね。
それじゃこの仕様で開発に入っていこう!
それじゃこの仕様で開発に入っていこう!

エレキベア
クマ〜〜〜〜〜〜〜
検索アプリの開発
使用するパッケージ

マイケル
まずは事前準備として、今回使用する外部パッケージを
取り込みましょう!
今回使用した外部パッケージは下記になります。
取り込みましょう!
今回使用した外部パッケージは下記になります。
[外部パッケージ]
・page_transition(ページ遷移アニメーション)
・webview_flutter(内部ブラウザ表示)
・universal_html(スクレイピング)
・google_fonts(フォント)

マイケル
このパッケージをpubspec.yamlに記述し、
pub getして取り込んでおきましょう!
pub getして取り込んでおきましょう!
・・・略・・・
dependencies:
flutter:
sdk: flutter
page_transition: ^1.1.7+2
webview_flutter: ^1.0.7
universal_html: ^1.2.1
google_fonts: ^1.1.1
・・・略・・・
↑外部パッケージの記述(2020年12月時点の最新バージョン)
エレキベア
これだけで使用できるのは便利くまね
画面の実装

マイケル
設定ができたら実際に画面を実装します!
少し長いですが、完成系を載せています。
まずはmain.dart を下記内容で置き換えましょう!
少し長いですが、完成系を載せています。
まずはmain.dart を下記内容で置き換えましょう!
import 'package:flutter/material.dart';
import 'package:porn_clone/video_page.dart';
import 'package:page_transition/page_transition.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:universal_html/driver.dart' as driver;
void main() {
runApp(MainPage());
}
class MainPage extends StatefulWidget {
@override
MainPageState createState() => MainPageState();
}
class MainPageState extends State<MainPage> {
final List<String> _dispMaxCountList = [
"20",
"50",
"100",
"200",
"500"
]; // 表示件数リスト
String _selectDispMaxCount = "20"; // 表示件数(選択)
List<MovieInfo> _movieList; // 動画リスト
String _searchWord; // 検索キーワード
bool _loading = false; // ローディング状態
// 動画リスト取得処理
void getMovieList() async {
// 入力されていない場合は処理なし
if (_searchWord == null || _searchWord.length == 0) {
return;
}
// ローディング状態にする
setState(() {
_loading = true;
});
// 検索結果ページを取得
final url = "https://jp.pornhub.com/video/search?search=" + _searchWord;
final client = driver.HtmlDriver();
await client.setDocumentFromUri(Uri.parse(url));
// 検索件数文章取得
final searchCountElm = client.document.querySelector(".showingCounter");
if (searchCountElm == null) {
// 取得結果無しの場合
setState(() {
_loading = false;
});
return;
}
// 検索結果件数を取得
int searchCount = 0;
var regExp = new RegExp('[0-9]{2,}');
Iterable<Match> matches = regExp.allMatches(searchCountElm.text.trim());
if (matches.isNotEmpty) {
searchCount = int.parse(
matches.last.group(0).toString()); // 検索結果件数:「1~XXを表示中(XXの中から)」から抽出
}
// 最大表示件数以上の場合、最大表示件数を設定
if (searchCount > int.parse(_selectDispMaxCount)) {
searchCount = int.parse(_selectDispMaxCount);
}
// 表示ページ数取得
int dispPageCount = (searchCount / 20).ceil(); // ページ数:検索結果件数 / 20件 (繰上)
// 検索結果のリスト取得
List<MovieInfo> tmpList = new List<MovieInfo>();
for (var i = 0; i < dispPageCount; i++) {
// 2ページ目以降の場合、URLを再取得
if (i > 0) {
final nextUrl = url + "&page=" + (i + 1).toString();
await client.setDocumentFromUri(Uri.parse(nextUrl));
}
// 動画情報の一覧を取得
final elements = client.document
.querySelectorAll("#videoSearchResult > .pcVideoListItem");
// タイトル、画像、URLを取得
for (final elem in elements) {
final titleElm = elem.querySelector(".title");
final imageElm = elem
.querySelector(".videoPreviewBg > img")
.getAttribute('data-src');
final urlElm = "https://jp.pornhub.com" +
elem.querySelector("a").getAttribute('href');
tmpList.add(new MovieInfo(titleElm.text.trim(), urlElm, imageElm));
}
}
// 取得した動画リストを設定
setState(() {
_movieList = tmpList;
_loading = false;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: HexColor("1B1B1B"),
),
home: Scaffold(
appBar: AppBar(
centerTitle: true,
leading: Padding(
padding: EdgeInsets.only(left: 10.0),
child: Icon(
Icons.camera,
color: HexColor("ff9900"),
),
),
title: Text(
"Pornhub Viewer",
style: GoogleFonts.mPlus1p(),
textAlign: TextAlign.justify,
),
),
body: Container(
color: Colors.black,
child: Column(
children: <Widget>[
// *** 検索条件入力部 ***
Row(
children: <Widget>[
// 表示件数プルダウン
DropdownButton<String>(
itemHeight: 55,
value: _selectDispMaxCount,
selectedItemBuilder: (context) {
return _dispMaxCountList.map((String item) {
return Center(
child: SizedBox(
height: 20,
width: 35,
child: Text(
item,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
),
),
),
);
}).toList();
},
items: _dispMaxCountList.map((String item) {
return DropdownMenuItem(
value: item,
child: Text(
item,
style: TextStyle(
color: Colors.black,
),
));
}).toList(),
onChanged: (String newValue) {
setState(() {
_selectDispMaxCount = newValue;
});
}),
// 検索条件入力蘭
Expanded(
child: Padding(
padding: EdgeInsets.all(3.0),
child: Container(
height: 40,
decoration: new BoxDecoration(
color: Colors.white,
),
child: TextFormField(
decoration: const InputDecoration(
icon: Icon(Icons.search),
hintText: "input search word.",
),
onChanged: (text) {
setState(() {
_searchWord = text;
});
},
),
),
),
),
// 検索ボタン
Container(
height: 45,
padding: EdgeInsets.all(3),
child: RaisedButton(
child: Text(
"SEARCH",
style: TextStyle(color: HexColor("1B1B1B")),
),
color: HexColor("ff9900"),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
onPressed: _loading
? null
: () {
getMovieList(); // 動画リスト取得
}),
),
],
),
// *** インジケータ表示部 ***
_loading ? LinearProgressIndicator() : Container(),
// *** 動画リスト表示部 ***
Expanded(
child: _movieList == null
? Text(
"Please input search word!!",
style: TextStyle(color: Colors.white),
)
: ListView.builder(
itemCount: _movieList.length,
itemBuilder: (context, index) {
return ListTile(
contentPadding: EdgeInsets.all(5.0),
// 引数にURLを設定
onTap: () async {
await Navigator.of(context).pushNamed('/movie',
arguments: '${_movieList[index].url}');
},
// 画像・タイトル表示
leading:
Image.network('${_movieList[index].image}'),
title: Text(
'${_movieList[index].title}',
style: TextStyle(
color: Colors.white,
fontSize: 10,
),
),
trailing: Padding(
padding: EdgeInsets.all(5.0),
child: Icon(
Icons.arrow_right,
size: 20,
color: Colors.white,
),
),
);
},
),
),
],
)),
),
// 画面遷移設定
// ※PageTransitionを使用
onGenerateRoute: (settings) {
switch (settings.name) {
case '/movie':
return PageTransition(
child: VideoPage(),
type: PageTransitionType.rightToLeftWithFade,
settings: settings);
break;
default:
return null;
}
},
);
}
}
// 動画情報クラス
class MovieInfo {
final String title, url, image;
MovieInfo(this.title, this.url, this.image);
}
// カラーコード指定
class HexColor extends Color {
static int _getColorFromHex(String hexColor) {
hexColor = hexColor.toUpperCase().replaceAll('#', '');
if (hexColor.length == 6) {
hexColor = 'FF' + hexColor;
}
return int.parse(hexColor, radix: 16);
}
HexColor(final String hexColor) : super(_getColorFromHex(hexColor));
}
↑main.dartの記述
マイケル
そして、動画画面として新たに video_page.dart ファイルを作成し、
下記内容に置き換えます!
下記内容に置き換えます!
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:google_fonts/google_fonts.dart';
class VideoPage extends StatefulWidget {
@override
VideoPageState createState() => VideoPageState();
}
class VideoPageState extends State<VideoPage> {
WebViewController _controller;
@override
void initState() {
super.initState();
// Enable hybrid composition.
if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView();
}
@override
Widget build(BuildContext context) {
// 遷移元よりURLを取得
final String movieUrl = ModalRoute.of(context).settings.arguments;
print("Request URL:" + movieUrl);
return Scaffold(
appBar: AppBar(
title: Text(
"Good Choice Σd(T□T)",
style: GoogleFonts.mPlus1p(),
),
),
body: WebView(
initialUrl: movieUrl,
javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (WebViewController controller) {
_controller = controller;
},
),
);
}
}
↑video_page.dartの記述
マイケル
これで実装は全てになります!

エレキベア
これだけの記述でアプリが作成できるなんてすごいクマ〜〜〜
各処理の概要

マイケル
それでは実装内容の主な処理について解説します!
検索画面表示


マイケル
まずは検索画面のUI表示部分から!
こちらはMainPageStateクラスのbuildメソッド内に記述しています!
こちらはMainPageStateクラスのbuildメソッド内に記述しています!
・・・略・・・
class MainPageState extends State<MainPage> {
final List<String> _dispMaxCountList = [
"20",
"50",
"100",
"200",
"500"
]; // 表示件数リスト
String _selectDispMaxCount = "20"; // 表示件数(選択)
List<MovieInfo> _movieList; // 動画リスト
String _searchWord; // 検索キーワード
bool _loading = false; // ローディング状態
・・・略・・・
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: HexColor("1B1B1B"),
),
home: Scaffold(
appBar: AppBar(
centerTitle: true,
leading: Padding(
padding: EdgeInsets.only(left: 10.0),
child: Icon(
Icons.camera,
color: HexColor("ff9900"),
),
),
title: Text(
"Pornhub Viewer",
style: GoogleFonts.mPlus1p(),
textAlign: TextAlign.justify,
),
),
body: Container(
color: Colors.black,
child: Column(
children: <Widget>[
// *** 検索条件入力部 ***
Row(
children: <Widget>[
// 表示件数プルダウン
DropdownButton<String>(
itemHeight: 55,
value: _selectDispMaxCount,
selectedItemBuilder: (context) {
return _dispMaxCountList.map((String item) {
return Center(
child: SizedBox(
height: 20,
width: 35,
child: Text(
item,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
),
),
),
);
}).toList();
},
items: _dispMaxCountList.map((String item) {
return DropdownMenuItem(
value: item,
child: Text(
item,
style: TextStyle(
color: Colors.black,
),
));
}).toList(),
onChanged: (String newValue) {
setState(() {
_selectDispMaxCount = newValue;
});
}),
// 検索条件入力蘭
Expanded(
child: Padding(
padding: EdgeInsets.all(3.0),
child: Container(
height: 40,
decoration: new BoxDecoration(
color: Colors.white,
),
child: TextFormField(
decoration: const InputDecoration(
icon: Icon(Icons.search),
hintText: "input search word.",
),
onChanged: (text) {
setState(() {
_searchWord = text;
});
},
),
),
),
),
// 検索ボタン
Container(
height: 45,
padding: EdgeInsets.all(3),
child: RaisedButton(
child: Text(
"SEARCH",
style: TextStyle(color: HexColor("1B1B1B")),
),
color: HexColor("ff9900"),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
onPressed: _loading
? null
: () {
getMovieList(); // 動画リスト取得
}),
),
],
),
// *** インジケータ表示部 ***
_loading ? LinearProgressIndicator() : Container(),
// *** 動画リスト表示部 ***
Expanded(
child: _movieList == null
? Text(
"Please input search word!!",
style: TextStyle(color: Colors.white),
)
: ListView.builder(
itemCount: _movieList.length,
itemBuilder: (context, index) {
return ListTile(
contentPadding: EdgeInsets.all(5.0),
// 引数にURLを設定
onTap: () async {
await Navigator.of(context).pushNamed('/movie',
arguments: '${_movieList[index].url}');
},
// 画像・タイトル表示
leading:
Image.network('${_movieList[index].image}'),
title: Text(
'${_movieList[index].title}',
style: TextStyle(
color: Colors.white,
fontSize: 10,
),
),
trailing: Padding(
padding: EdgeInsets.all(5.0),
child: Icon(
Icons.arrow_right,
size: 20,
color: Colors.white,
),
),
);
},
),
),
],
)),
),
・・・略・・・
);
}
}
・・・略・・・
// カラーコード指定
class HexColor extends Color {
static int _getColorFromHex(String hexColor) {
hexColor = hexColor.toUpperCase().replaceAll('#', '');
if (hexColor.length == 6) {
hexColor = 'FF' + hexColor;
}
return int.parse(hexColor, radix: 16);
}
HexColor(final String hexColor) : super(_getColorFromHex(hexColor));
}
↑検索画面表示の実装
マイケル
Scaffoldという共通部品の中に、appBar、title、body等枝分かれしていて、
bodyの中にプルダウンやテキストフィールドを並べて・・・
というように、階層を深くしていきながらWidget(※1)を配置していくような実装イメージになります!
(※1)Widget → テキストやボタン、レイアウト等の配置できるコンポーネントのこと。
bodyの中にプルダウンやテキストフィールドを並べて・・・
というように、階層を深くしていきながらWidget(※1)を配置していくような実装イメージになります!
(※1)Widget → テキストやボタン、レイアウト等の配置できるコンポーネントのこと。


エレキベア
作っていくにつれて階層が深くなって、
コードが読みにくくなりそうクマね
コードが読みにくくなりそうクマね

マイケル
そこが難点だよね。
作っている時はホットリロードで確認しながらでサクサク作れるけど、作り込むにつれて可読性が悪くなってしまう。
初めのうちはテキストとボタンだけとか、最小限のWidgetを使うような簡単なアプリを作成してみる方が理解が早いかもしれないね。
作っている時はホットリロードで確認しながらでサクサク作れるけど、作り込むにつれて可読性が悪くなってしまう。
初めのうちはテキストとボタンだけとか、最小限のWidgetを使うような簡単なアプリを作成してみる方が理解が早いかもしれないね。

エレキベア
ここは今後の課題クマね〜〜〜〜
スクレイピング処理

マイケル
そして次は動画取得の処理について!
こちらはスクレイピングというテクニックを使って
HTMLから動画一覧を取得しています!
こちらはスクレイピングというテクニックを使って
HTMLから動画一覧を取得しています!

マイケル
getMoveList()メソッドに取得処理を記述し、検索ボタン押下時に呼び出すように設定しています。
また、スクレイピング処理を行うため、外部パッケージとして universal_htmlを使用しました!
また、スクレイピング処理を行うため、外部パッケージとして universal_htmlを使用しました!
// 動画リスト取得処理
void getMovieList() async {
// 入力されていない場合は処理なし
if (_searchWord == null || _searchWord.length == 0) {
return;
}
// ローディング状態にする
setState(() {
_loading = true;
});
// 検索結果ページを取得
final url = "https://jp.pornhub.com/video/search?search=" + _searchWord;
final client = driver.HtmlDriver();
await client.setDocumentFromUri(Uri.parse(url));
// 検索件数文章取得
final searchCountElm = client.document.querySelector(".showingCounter");
if (searchCountElm == null) {
// 取得結果無しの場合
setState(() {
_loading = false;
});
return;
}
// 検索結果件数を取得
int searchCount = 0;
var regExp = new RegExp('[0-9]{2,}');
Iterable<Match> matches = regExp.allMatches(searchCountElm.text.trim());
if (matches.isNotEmpty) {
searchCount = int.parse(
matches.last.group(0).toString()); // 検索結果件数:「1~XXを表示中(XXの中から)」から抽出
}
// 最大表示件数以上の場合、最大表示件数を設定
if (searchCount > int.parse(_selectDispMaxCount)) {
searchCount = int.parse(_selectDispMaxCount);
}
// 表示ページ数取得
int dispPageCount = (searchCount / 20).ceil(); // ページ数:検索結果件数 / 20件 (繰上)
// 検索結果のリスト取得
List<MovieInfo> tmpList = new List<MovieInfo>();
for (var i = 0; i < dispPageCount; i++) {
// 2ページ目以降の場合、URLを再取得
if (i > 0) {
final nextUrl = url + "&page=" + (i + 1).toString();
await client.setDocumentFromUri(Uri.parse(nextUrl));
}
// 動画情報の一覧を取得
final elements = client.document
.querySelectorAll("#videoSearchResult > .pcVideoListItem");
// タイトル、画像、URLを取得
for (final elem in elements) {
final titleElm = elem.querySelector(".title");
final imageElm = elem
.querySelector(".videoPreviewBg > img")
.getAttribute('data-src');
final urlElm = "https://jp.pornhub.com" +
elem.querySelector("a").getAttribute('href');
tmpList.add(new MovieInfo(titleElm.text.trim(), urlElm, imageElm));
}
}
// 取得した動画リストを設定
setState(() {
_movieList = tmpList;
_loading = false;
});
}
・・・略・・・
// 動画情報クラス
class MovieInfo {
final String title, url, image;
MovieInfo(this.title, this.url, this.image);
}
↑スクレイピング処理
マイケル
入力した _searchWord 変数から、検索結果URLを作成し、
querySelector()や正規表現を駆使して動画情報を取得しています!
querySelector()や正規表現を駆使して動画情報を取得しています!

マイケル
スクレイピングの詳細については省略させていただきますが、
実際にWEBサイトを開いて
開発者ツールでIDやクラス名を確認したり、
検索条件を入力した挙動や、画面遷移後のURLなどを観察
しながら、ゴリゴリと実装していきましょう!
実際にWEBサイトを開いて
開発者ツールでIDやクラス名を確認したり、
検索条件を入力した挙動や、画面遷移後のURLなどを観察
しながら、ゴリゴリと実装していきましょう!

エレキベア
トライアンドエラーで地道に作っていくクマね

マイケル
あとはWEBサイトの更新により構造自体が変わってしまうということもあるので、
IDで取得する等、なるべく一意に取得できるようにするのが望ましいですね・・・。
IDで取得する等、なるべく一意に取得できるようにするのが望ましいですね・・・。

マイケル
とはいえこればかりはWEBサイトの構造にもよるので、
メンテは必要だという覚悟で実装しましょう!!
メンテは必要だという覚悟で実装しましょう!!

エレキベア
汗と涙の結晶クマーー・・・。
内部ブラウザ表示

マイケル
そして最後は選択した動画の内部ブラウザ表示処理です!
外部パッケージとしては、
ページ遷移のアニメーションに page_transition、
内部ブラウザの表示に webview_flutter
を使用しました!
外部パッケージとしては、
ページ遷移のアニメーションに page_transition、
内部ブラウザの表示に webview_flutter
を使用しました!
・・・略・・・
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: HexColor("1B1B1B"),
),
・・・略・・・
body: Container(
color: Colors.black,
child: Column(
children: <Widget>[
// *** 検索条件入力部 ***
Row(
children: <Widget>[
・・・略・・・
// *** 動画リスト表示部 ***
Expanded(
child: _movieList == null
? Text(
"Please input search word!!",
style: TextStyle(color: Colors.white),
)
: ListView.builder(
itemCount: _movieList.length,
itemBuilder: (context, index) {
return ListTile(
contentPadding: EdgeInsets.all(5.0),
// 引数にURLを設定
onTap: () async {
await Navigator.of(context).pushNamed('/movie',
arguments: '${_movieList[index].url}');
},
・・・略・・・
),
],
)),
),
// 画面遷移設定
// ※PageTransitionを使用
onGenerateRoute: (settings) {
switch (settings.name) {
case '/movie':
return PageTransition(
child: VideoPage(),
type: PageTransitionType.rightToLeftWithFade,
settings: settings);
break;
default:
return null;
}
},
);
}
}
・・・略・・・
// 動画情報クラス
class MovieInfo {
final String title, url, image;
MovieInfo(this.title, this.url, this.image);
}
↑画面遷移処理import 'dart:io';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:google_fonts/google_fonts.dart';
class VideoPage extends StatefulWidget {
@override
VideoPageState createState() => VideoPageState();
}
class VideoPageState extends State<VideoPage> {
WebViewController _controller;
@override
void initState() {
super.initState();
// Enable hybrid composition.
if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView();
}
@override
Widget build(BuildContext context) {
// 遷移元よりURLを取得
final String movieUrl = ModalRoute.of(context).settings.arguments;
print("Request URL:" + movieUrl);
return Scaffold(
appBar: AppBar(
title: Text(
"Good Choice Σd(T□T)",
style: GoogleFonts.mPlus1p(),
),
),
body: WebView(
initialUrl: movieUrl,
javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (WebViewController controller) {
_controller = controller;
},
),
);
}
}
↑遷移元のパラメタよりURL取得
マイケル
動画リストの onTap() イベントでタップした動画URLを遷移パラメタとして
動画画面に渡す 動きにしています。
あとは動画画面で 受け取ったURLからWebViewを使用して内部ブラウザで表示 するように実装すれば完成です!!
動画画面に渡す 動きにしています。
あとは動画画面で 受け取ったURLからWebViewを使用して内部ブラウザで表示 するように実装すれば完成です!!

エレキベア
やったクマ〜〜〜〜〜
おわりに

マイケル
・・・・!!!!

マイケル
なかなかストレスフリーで使いやすいな〜〜〜〜

エレキベア
夜のお供として活躍しそうクマね

マイケル
今回作ったアプリは、アダルトサイトだけでなく
様々なサイトの情報取得にも応用出来ると思うので、
是非活用してみてくださいね!
様々なサイトの情報取得にも応用出来ると思うので、
是非活用してみてくださいね!

エレキベア
クマ〜〜〜〜〜〜〜〜

ジャギィ
お、俺のスマホにもインストールしてくれ!!!

マイケル
(ウイルス仕込んだろ・・・。)
【Flutter】自給自足!Pornhubの検索アプリを作ってみた!【R18】 〜完〜
コメント