shinonome-inc / qiita_client_yo

【模擬開発案件】Qiitaクライアントアプリ(PlayGroundモバイルコース最終課題)
3 stars 0 forks source link

【質問】QiitaAPIを利用した記事の取得について #7

Closed KobayashiYoh closed 2 years ago

KobayashiYoh commented 2 years ago

概要

Flutterの最終課題でFeedPageを作成していたところ、昨日まで可能だった記事の取得ができなくなってしまいました。 解決方法を教えていただければ幸いです。 よろしくお願いします。

実装したい内容

QiitaAPIを利用して記事一覧を表示する

現状

・QiitaAPIで記事を取得しようとするとresponse.statusCode = 403が返されて記事の取得ができない ・昨日までは記事もタグも問題なく取得できていた ・取得に成功することもある ・エミュレータはインターネットに接続されていて機内モードにもなっていない ・エミュレータのブラウザの使用やWebViewでQiitaのログインページを表示することは可能

試したこと

dart pub global activate devtools -v 2.8.0を実行したら "/Users/develien/.pub-cache/hosted/pub.dartlang.org/devtools-2.9.2/build" could not be foundのようなエラーは消えた(参考:https://github.com/flutter/devtools/issues/3549) ・PC本体、エミュレータ、AndroidStudioの再起動 ・エミュレータの初期化、変更 ・Chromeでの実行

該当箇所のソースコード

client.dart

import 'package:mobile_qiita_app/services/article.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

class Client {
  // QiitaAPIで記事を取得
  static Future<List<Article>> fetchArticle() async {
    var url = 'https://qiita.com/api/v2/items';
    var response = await http.get(Uri.parse(url));
    if (response.statusCode == 200) {
      final List<dynamic> jsonResponse = json.decode(response.body);
      return jsonResponse.map((json) => Article.fromJson(json)).toList();
    }
    else {
      throw Exception('Request failed with status: ${response.statusCode}');
    }
  }
}

feed_page.dart

import 'package:flutter/material.dart';
import 'package:mobile_qiita_app/services/client.dart';
import 'package:mobile_qiita_app/services/article.dart';
import 'package:mobile_qiita_app/views/error_views.dart';

class FeedPage extends StatefulWidget {
  const FeedPage({Key? key}) : super(key: key);

  @override
  _FeedPageState createState() => _FeedPageState();
}

class _FeedPageState extends State<FeedPage> {

  late Future<List<Article>> _futureArticles;

  // 取得した記事の内容を整理して表示
  Widget _articleWidget(Article article) {
    return ListTile(
      onTap: () {
        print(article.title);
        // ・記事項目タップで13-Qiita Article Pageへ遷移する
      },
      leading: CircleAvatar(
        radius: 25,
        backgroundImage: NetworkImage(article.user.iconUrl),
      ),
      title: Text(
        article.title,
        overflow: TextOverflow.ellipsis,
        maxLines: 2,
      ),
      subtitle: Container(
        padding: const EdgeInsets.only(bottom: 10),
        decoration: BoxDecoration(
          border: Border(
            bottom: BorderSide(
              color: const Color(0xEFEFF0FF),
              width: 1.0,
            ),
          ),
        ),
        child: Text(
          '${article.user.id} 投稿日: ${article.created_at.substring(0, 10)} LGTM: ${article.likes_count}',
        ),
      ),
    );
  }

  // 再読み込みする
  void _reload() {
    setState(() {
      _futureArticles = Client.fetchArticle();
    });
  }

  @override
  void initState() {
    super.initState();
    _futureArticles = Client.fetchArticle();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: PreferredSize(
        preferredSize: const Size.fromHeight(128.0),
        child: Container(
          decoration: BoxDecoration(
            border: Border(
              bottom: BorderSide(
                color: const Color(0xEFEFF0FF),
                width: 1.5,
              ),
            ),
          ),
          child: SafeArea(
            child: Column(
              children: <Widget>[
                Container(
                  height: 70.0,
                  alignment: Alignment.center,
                  child: const Text(
                    'Feed',
                    style: TextStyle(
                      color: Colors.black,
                      fontFamily: 'Pacifico',
                      fontSize: 22.0,
                    ),
                  ),
                ),
                Container(
                  width: 380.0,
                  height: 40.0,
                  padding: const EdgeInsets.only(left: 10.0),
                  decoration: BoxDecoration(
                    color: const Color(0xEFEFF0FF),
                    borderRadius: BorderRadius.circular(10.0),
                  ),
                  child: TextField(
                    enabled: true,
                    decoration: InputDecoration(
                      border: InputBorder.none,
                      icon: const Icon(Icons.search),
                      hintText: 'search',
                      hintStyle: TextStyle(
                        color: const Color(0xFF828282),
                        fontSize: 18.0,
                      ),
                    ),
                    onChanged: (e) {
                      print(e);
                      // ・Search Barに任意のテキストを入力すると記事の検索ができる
                    },
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
      body: FutureBuilder(
        future: _futureArticles,
        builder: (BuildContext context, AsyncSnapshot snapshot) {
          List<Widget> children = [];
          MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start;
          if (snapshot.connectionState == ConnectionState.done) {
            if (snapshot.hasData) {
              print('snapshot.hasData');
              children = <Widget> [
                Flexible(
                  child: ListView.builder(
                    shrinkWrap: true,
                    itemCount: snapshot.data.length,
                    itemBuilder: (context, index) {
                      return _articleWidget(snapshot.data[index]);
                    },
                  ),
                ),
              ];
            }
            else if (snapshot.hasError) {
              print('snapshot.hasError');
              children = <Widget> [
                ErrorView.errorViewWidget(_reload),
              ];
            }
          } else {
            print('loading...');
            mainAxisAlignment = MainAxisAlignment.center;
            children = <Widget> [
              Center(
                child: CircularProgressIndicator(),
              ),
            ];
          }
          return Column(
            mainAxisAlignment: mainAxisAlignment,
            children: children,
          );
        },
      ),
    );
  }
}
KobayashiYoh commented 2 years ago

現在は取得できています。

KobayashiYoh commented 2 years ago

僕がstatusCode = 403とstatusCode = 404を混同していました。

どうやら原因はこれっぽいです。 利用制限 認証している状態ではユーザごとに1時間に1000回まで、認証していない状態ではIPアドレスごとに1時間に60回までリクエストを受け付けます。

引用:https://qiita.com/api/v2/docs

KobayashiYoh commented 2 years ago

やはり、原因は認証していない状態で1時間に60回以上リクエストを送っていたからでした。

https://user-images.githubusercontent.com/82624334/147049428-b7c2fde9-2d6a-4827-b81b-29694b38af06.mp4