【Flutter 2.2.1】hooks_riverpod나 retrofit, freeezed를 사용해 Qiita API로부터 기사를 취득

소개



이번 샘플의 화면은 이하가 됩니다.



또한 과거에 flutter_riverpod와 AsyncValue를 사용하여 비슷한 기사를 여러 번 게시했습니다.

Riverpod+StateNotifier+freezed+Retrofit에서 Qiita 기사 얻기

Qiita 클라이언트 개발에서 Riverpod AsyncValue를 사용해 보았습니다.

반복적으로 유사한 기사를 쓰는 이유로서, Flutter 자체나 라이브러리의 성장이 빨라, 과거의 기사의 아직도 정상적으로 동작하지 않게 되어 있는 것을 코멘트 받은 것이 큰 이유입니다.

또 나 자신, flutter_riverpod는 사용한 적이 있어도, hooks_riverpod는 만지지 않았기 때문에, 이 시기에 도전해 보았습니다.

본 기사의 대상 독자



본 기사의 대상 독자로서는, Flutter의 상태 관리의 라이브러리를 사용한 적이 있는 분을 상정하고 있습니다.
그 때문에 주로 과거의 기사와의 차이만 기재하고 싶습니다.

Flutter 버전



사용할 Flutter 및 주요 라이브러리 버전
Flutter: 2.2.1
dependencies:
  hooks_riverpod: ^0.14.0+4
  flutter_hooks: ^0.17.0

  retrofit: ^2.0.0
  freezed_annotation: ^0.14.2
  webview_flutter: ^2.0.8
  state_notifier: ^0.7.0
  flutter_state_notifier: ^0.7.0

dev_dependencies:
  build_runner: ^2.0.4
  freezed: ^0.14.2
  retrofit_generator: ^2.0.0+1
  json_serializable: ^4.1.3

※Flutter 자체나 라이브러리의 장래의 버전 업에 의해, 동작하지 않게 될 가능성이 있습니다.

과거 기사의 변화



freezed 클래스가 abstract 클래스 더 이상 필요하지 않습니다.



이전에는 abstract 클래스인 것이 필수였지만, 현재는 필수가 아니게 되어 있습니다.

또 null safety 대응으로서, user.dart 이나 article_state.dart 와 같이 Default 로 초기치를 설정하고 있습니다.

user.dart
@freezed
class User with _$User {
  factory User({
    @Default('') @JsonKey(name: 'profile_image_url') final String profileImageUrl,
  }) = _User;

  factory User.fromJson(Map<String, dynamic> json) =>
      _$UserFromJson(json);
}

article_state.dart
@freezed
class ArticleState with _$ArticleState {
  const factory ArticleState({
    @Default(AsyncValue.loading()) AsyncValue<List<Article>> articles,
  }) = _ArticleState;
}

혹은 article.dart 와 같이 required로 필수로 하고 있습니다.

article.dart
@freezed
class Article with _$Article {
  factory Article({
    required String title,
    required String url,
    required User user,
  }) = _Article;

  factory Article.fromJson(Map<String, dynamic> json) =>
      _$ArticleFromJson(json);
}

hooks_riverpod를 사용해 보았습니다.



과거 기사에서는 flutter_riverpod를 사용했지만 이번에는 hooks_riverpod를 사용했습니다. (동시에 flutter_hooks를 도입하고 있습니다)
useProvider를 사용할 수 있게 된 것으로, 이전에는,
final state = watch(articleProvider.state);

같이 쓰고 있던 곳을,
final state = useProvider(articleProvider);

라고 쓸 수 있게 되었습니다.

riverpod 변경 사항



이전에는 다음과 같이 articleProvider를 작성했습니다.
final articleProvider = StateNotifierProvider(
      (_) => ArticleStateNotifier(
        ArticleRepository(),
      ),
    );

riverpod의 갱신에 의해, StateNotifierProvider의 뒤에 <ArticleStateNotifier, ArticleState> 라고 명시하는 것이 필수가 되고 있습니다.
final articleProvider =
    StateNotifierProvider<ArticleStateNotifier, ArticleState>(
  (_) => ArticleStateNotifier(
    ArticleRepository(),
  ),
);

"hooks_riverpod를 사용해 보았다""riverpod의 변경점"의 수정을 반영한 article_screen은 다음과 같습니다.

article_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:qiita_sample/data/entities/qiita_info.dart';
import 'package:qiita_sample/screens/article/article_item.dart';
import 'package:qiita_sample/screens/article/article_repository.dart';
import 'package:qiita_sample/screens/article/article_state_notifier.dart';
import 'package:qiita_sample/screens/article_detail/article_detail_screen.dart';

import 'article_state.dart';

final articleProvider =
    StateNotifierProvider<ArticleStateNotifier, ArticleState>(
  (_) => ArticleStateNotifier(
    ArticleRepository(),
  ),
);

class ArticleScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(
          'Qiita Sample',
        ),
        centerTitle: true,
      ),
      body: _List(),
    );
  }
}

class _List extends HookWidget {
  @override
  Widget build(BuildContext context) {
    // hooksを導入したことでuseProviderを使用できるようになりました
    final state = useProvider(articleProvider);
    // 今回からpull to refreshを追加
    return RefreshIndicator(
      child: state.articles.when(
        data: (articles) => ListView.builder(
          itemCount: articles.length,
          itemBuilder: (context, int position) => ArticleItem(
            qiitaInfo: articles[position],
            onArticleClicked: (qiitaInfo) => _openArticleWebPage(
              context,
              qiitaInfo,
            ),
          ),
        ),
        loading: () => Center(
          child: CircularProgressIndicator(),
        ),
        error: (_, __) => Center(
          child: Text('データの取得に失敗しました。'),
        ),
      ),
      onRefresh: () => getArticle(context),
    );
  }

  void _openArticleWebPage(
    BuildContext context,
    QiitaInfo qiitaInfo,
  ) {
    Navigator.of(context).push(
      MaterialPageRoute(
        builder: (context) => ArticleDetailScreen(
          qiitaInfo: qiitaInfo,
        ),
      ),
    );
  }

  Future<void> getArticle(BuildContext context) async {
    await context.read(articleProvider.notifier).getFlutterArticles();
  }
}

기타 미세 수정



Flutter의 버전을 올렸기 때문에 안드로이드의 경우 minSdkVersion을 19로 수정해야합니다.
※6/24 추기 엄밀하게는 새로운 webview_flutter를 넣었기 때문에였습니다.

android/app/build.gradle
minSdkVersion 19

추가 요소



특히 이유는 없습니다만, 과거의 기사에는 도입하고 있지 않았던 pull-to-refresh(기사의 리스트를 아래로 당기면 재로드)에 의한 기사 갱신 기능을 추가하고 있습니다.

과제 등


  • hooks를 도입하면 riverpod가 간결하게 쓸 수있게되는 것은 조금 체감 할 수 있었지만, 아직 hooks인것 같은 글을 모른다
  • null safety 대응으로 required@Default 의 구분이 모호
  • 샘플이라면 기능이나 화면이 너무 적어, 아직 거기까지 사용감이 잡히지 않는다
    → 출시중인 provider 기반 앱을 riverpod_hooks로 재생할 것이라고 생각합니다.

    이번 코드



    마지막으로



    여전히 Flutter의 성장이 빠르고, 솔직히 2.0 발표 정도부터 두고 가고 있는 느낌이 들었습니다. 그러나 이번 라이브러리 등도 드디어 null safety 대응되고 있는 것도 많거나, 정말로 Flutter2.0에 메이저 업데이트했다고 하는 느낌이 듭니다.

    또 Flutter의 동향에도 주목해 가고 싶습니다.
  • 좋은 웹페이지 즐겨찾기