{"id":32434988,"url":"https://github.com/natsuk4ze/npm","last_synced_at":"2026-03-07T08:03:13.777Z","repository":{"id":196306260,"uuid":"694644451","full_name":"natsuk4ze/npm","owner":"natsuk4ze","description":"npm client app as minimal flutter project example | riverpod x freezed x hooks x dio x shared_preferences","archived":false,"fork":false,"pushed_at":"2026-02-02T02:02:22.000Z","size":197900,"stargazers_count":15,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-02-02T11:49:00.034Z","etag":null,"topics":["android","app","application","dart","dio","flutter","freezed","ios","npm","npm-package","packages","python","riverpod","sharedpreferences"],"latest_commit_sha":null,"homepage":"","language":"Dart","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/natsuk4ze.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"natsuk4ze"}},"created_at":"2023-09-21T12:10:09.000Z","updated_at":"2026-02-02T02:02:26.000Z","dependencies_parsed_at":"2025-10-06T03:17:17.652Z","dependency_job_id":null,"html_url":"https://github.com/natsuk4ze/npm","commit_stats":null,"previous_names":["natsuk4ze/npm"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/natsuk4ze/npm","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/natsuk4ze%2Fnpm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/natsuk4ze%2Fnpm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/natsuk4ze%2Fnpm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/natsuk4ze%2Fnpm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/natsuk4ze","download_url":"https://codeload.github.com/natsuk4ze/npm/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/natsuk4ze%2Fnpm/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30209797,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-07T05:23:27.321Z","status":"ssl_error","status_checked_at":"2026-03-07T05:00:17.256Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","app","application","dart","dio","flutter","freezed","ios","npm","npm-package","packages","python","riverpod","sharedpreferences"],"created_at":"2025-10-25T22:20:16.556Z","updated_at":"2026-03-07T08:03:13.764Z","avatar_url":"https://github.com/natsuk4ze.png","language":"Dart","funding_links":["https://github.com/sponsors/natsuk4ze"],"categories":[],"sub_categories":[],"readme":"# 📦 npm client\n[![Codacy Badge](https://app.codacy.com/project/badge/Grade/38b5b48aa5e24a229b267c0c3a134bbe)](https://app.codacy.com/gh/natsuk4ze/npm/dashboard?utm_source=gh\u0026utm_medium=referral\u0026utm_content=\u0026utm_campaign=Badge_grade)\n[![CodeFactor](https://www.codefactor.io/repository/github/natsuk4ze/npm/badge)](https://www.codefactor.io/repository/github/natsuk4ze/npm)\n![Test](https://github.com/natsuk4ze/npm/actions/workflows/ci.yml/badge.svg?branch=master)\n![Preview](https://github.com/natsuk4ze/npm/actions/workflows/deploy_preview.yml/badge.svg?branch=master)\n\n### A [npm](https://www.npmjs.com) client app as minimal flutter project example\n\n### [🌐 Device Preview on Flutter Web](https://natsuk4ze.github.io/npm)\n\n### Using\n-  *hooks_riverpod* for state management\n-  *freezed* for serializing (deserializing) json objects\n-  *dio* for network request\n-  *shared_preferences* for local database\n-  *slang* for localization\n\n\u003cimg src=\"https://github.com/natsuk4ze/npm/raw/master/assets/readme/example.gif\" width=280 alt=\"example\"/\u003e\n\n## How to install\n\n1. install [flutter](https://docs.flutter.dev/get-started/install)\n2. clone this repository\n3. run `cd $PATH_TO_REPOSITORY`\n4. run `flutter pub get`\n5. run `dart run build_runner build`\n6. run `dart run slang`\n7. run `flutter run`\n\nNew to flutter? See: [How to install flutter app on your device](https://www.youtube.com/watch?v=aohkII1C4JY)\n\n## Features\n\n### 🛜 Real time fetch\nReal-time fetching with *dio* and *riverpod*.\nListen to `TextEditingController` to rebuild the widget.\n\n\u003cdetails\u003e\n\u003csummary\u003eShow codes\u003c/summary\u003e\n\n```dart\nfinal searchController = useTextEditingController(text: initialSearchText);\nuseListenable(searchController);\n```\nSee: [packages_page.dart](https://github.com/natsuk4ze/npm/blob/master/lib/features/packages/packages_page.dart)\n\n\u003c/details\u003e\n\n\u003cimg src=\"https://github.com/natsuk4ze/npm/raw/master/assets/readme/real_time_fetch.gif\" width=200 alt=\"Real time fetch\"/\u003e\n\n### 👌 Pull to refresh\nPull to refresh with `RefreshIndicator`.\nBy returning `.future` to `onRefresh`, the indicator will continue to be displayed until the data fetching is complete.\n\n\u003cdetails\u003e\n\u003csummary\u003eShow codes\u003c/summary\u003e\n\n```dart\nRefreshIndicator(\n  onRefresh: () =\u003e ref.refresh(packagesProvider(\n    search: searchText,\n    debounce: false,\n  ).future),\n  child: ListView.separated(\n    separatorBuilder: (_, __) =\u003e const Divider(),\n    itemCount: sortedPackages.length,\n    itemBuilder: (_, int i) =\u003e PackageItem(sortedPackages[i]),\n  ),\n);\n```\nSee: [packages_page.dart](https://github.com/natsuk4ze/npm/blob/master/lib/features/packages/packages_page.dart)\n\n\u003c/details\u003e\n\n\u003cimg src=\"https://github.com/natsuk4ze/npm/raw/master/assets/readme/pull_to_refresh.gif\" width=200 alt=\"Pull to refresh\"/\u003e\n\n### 📊 Sort\nSort with *riverpod*.\n\n\u003cdetails\u003e\n\u003csummary\u003eShow codes\u003c/summary\u003e\n\n```dart\n@riverpod\nclass Sort extends _$Sort {\n  @override\n  ScoreType? build() =\u003e null;\n\n  void update(ScoreType type) =\u003e state = type;\n}\n\n@riverpod\nFuture\u003cList\u003cPackage\u003e\u003e sortedPackages(SortedPackagesRef ref,\n    {required String search}) async {\n  final packages = await ref.watch(packagesProvider(search: search).future);\n  final sort = ref.watch(sortProvider);\n\n  return sort == null\n      ? List.of(packages)\n      : packages.sortedByCompare(\n          (package) =\u003e sort.getValue(package.score), (a, b) =\u003e b.compareTo(a));\n}\n```\nSee: [packages_page.dart](https://github.com/natsuk4ze/npm/blob/master/lib/features/packages/packages_page.dart)\n\n\u003c/details\u003e\n\n\u003cimg src=\"https://github.com/natsuk4ze/npm/raw/master/assets/readme/sort.gif\" width=200 alt=\"Pull to refresh\"/\u003e\n\n### ☁️ Empty state\nSwitching widget according to status with `AsyncValue`.\n\n\u003cdetails\u003e\n\u003csummary\u003eShow codes\u003c/summary\u003e\n\n```dart\nreturn sortedPackages.isEmpty\n    ? SingleChildScrollView(\n        child: EmptyImage(text: l10n.packagesPage.packageNotFound),\n      )\n    : RefreshIndicator(\n        onRefresh: () async =\u003e ref.refresh(packagesProvider(\n          search: searchText,\n          debounce: false,\n        ).future),\n        child: ListView.separated(\n          separatorBuilder: (_, __) =\u003e const Divider(),\n          itemCount: sortedPackages.length,\n          itemBuilder: (_, int i) =\u003e PackageItem(sortedPackages[i]),\n        ),\n      );\n```\nSee: [packages_page.dart](https://github.com/natsuk4ze/npm/blob/master/lib/features/packages/packages_page.dart)\n\n\u003c/details\u003e\n\n\u003cimg src=\"https://github.com/natsuk4ze/npm/raw/master/assets/readme/empty_state.gif\" width=200 alt=\"Empty state\"/\u003e\n\n### 🪽 Jump to repository\nJumping to repository with *url_launcher*.\n\n\u003cdetails\u003e\n\u003csummary\u003eShow codes\u003c/summary\u003e\n\n```dart\nclass LinkText extends StatelessWidget {\n  const LinkText(\n    this.url, {\n    this.text,\n    super.key,\n  });\n\n  final String? text;\n  final String url;\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      onTap: () async =\u003e launchUrl(Uri.parse(url)),\n      child: Text(\n        text ?? url,\n        style: const TextStyle(\n          fontWeight: FontWeight.bold,\n          decoration: TextDecoration.underline,\n        ),\n      ),\n    );\n  }\n}\n```\nSee: [link_text.dart](https://github.com/natsuk4ze/npm/blob/master/lib/common_widgets/link_text.dart)\n\n\u003c/details\u003e\n\n\u003cimg src=\"https://github.com/natsuk4ze/npm/raw/master/assets/readme/jump_to_repo.gif\" width=200 alt=\"Jump to repository\"/\u003e\n\n### 🔍 See package details\nGetting package details for requesting api with *dio* and *freezed*.\n\n\u003cdetails\u003e\n\u003csummary\u003eShow codes\u003c/summary\u003e\n\n```dart\n@riverpod\nDio dio(DioRef ref) =\u003e Dio();\n\n@riverpod\nFuture\u003cPackageDetails\u003e packageDetails(PackageDetailsRef ref,\n    {required String id}) async {\n  final response = await ref.watch(dioProvider).getUri\u003cJson\u003e(\n        Uri.parse('https://registry.npmjs.org/$id'),\n      );\n  return PackageDetails.fromJson(response.data!);\n}\n\n@freezed\nclass PackageDetails with _$PackageDetails {\n  const PackageDetails._();\n\n  const factory PackageDetails({\n    required final String name,\n    final String? description,\n    final String? homepage,\n    final String? repository,\n    final String? readme,\n    final List\u003cString\u003e? keywords,\n    final String? license,\n  }) = _PackageDetails;\n\n  factory PackageDetails.fromJson(Json json) {\n    final git = json['repository']?['url'] as String?;\n\n    return PackageDetails(\n      name: json['name'],\n      description: json['description'],\n      keywords: ListX.fromOrNull\u003cString\u003e(json['keywords']),\n      license: json['license'],\n      homepage: json['homepage'],\n      repository: git == null ? null : Format.urlFromGit(git),\n      readme: json['readme'],\n    );\n  }\n}\n```\n\nSee:\n- [dio.dart](https://github.com/natsuk4ze/npm/blob/master/lib/util/dio.dart)\n- [package_details.dart](https://github.com/natsuk4ze/npm/blob/master/lib/features/package_details/package_details.dart)\n\n\u003c/details\u003e\n\n\u003cimg src=\"https://github.com/natsuk4ze/npm/raw/master/assets/readme/see_package_details.gif\" width=200 alt=\"See package details\"/\u003e\n\n### 🌙 Dark mode\nDynamic theming with *riverpod* and *shared_preferences*.\nUse `ref.invalidateSelf()` for SSOT design.\n\n\u003cdetails\u003e\n\u003csummary\u003eShow codes\u003c/summary\u003e\n\n```dart\n@Riverpod(keepAlive: true)\nSharedPreferences sharedPreferences(SharedPreferencesRef ref) =\u003e\n    throw UnimplementedError('SharedPreferences is not overridden.');\n\n@riverpod\nclass IsDarkMode extends _$IsDarkMode {\n  static const _key = 'isDarkMode';\n  @override\n  bool build() {\n    final prefs = ref.watch(sharedPreferencesProvider);\n    return prefs.getBool(_key) ?? false;\n  }\n\n  Future\u003cvoid\u003e toggle() async {\n    final prefs = ref.read(sharedPreferencesProvider);\n    await prefs.setBool(_key, !state);\n    ref.invalidateSelf();\n  }\n}\n```\n\nSee:\n- [shared_preferences.dart](https://github.com/natsuk4ze/npm/blob/master/lib/util/shared_preferences.dart)\n- [dark_mode.dart](https://github.com/natsuk4ze/npm/blob/master/lib/features/settings/dark_mode.dart)\n\u003c/details\u003e\n\n\u003cimg src=\"https://github.com/natsuk4ze/npm/raw/master/assets/readme/dark_mode.gif\" width=200 alt=\"Dark mode\"/\u003e\n\n### 🗣️ Localization\nDynamic localization with *slang*, *riverpod* and *shared_preferences*.\nUse `ref.invalidateSelf()` for SSOT design.\n\n\u003cdetails\u003e\n\u003csummary\u003eShow codes\u003c/summary\u003e\n\n```dart\n@riverpod\nStringsEn l10n(L10nRef ref) =\u003e ref.watch(languageProvider).stringsEn;\n\n@riverpod\nclass Language extends _$Language {\n  static const _key = 'language';\n  @override\n  LanguageType build() {\n    final prefs = ref.watch(sharedPreferencesProvider);\n    return LanguageType.fromName(prefs.getString(_key) ?? LanguageType.en.name);\n  }\n\n  Future\u003cvoid\u003e update(LanguageType type) async {\n    final prefs = ref.read(sharedPreferencesProvider);\n    await prefs.setString(_key, type.name);\n    ref.invalidateSelf();\n  }\n}\n\nenum LanguageType {\n  en,\n  ja;\n\n  StringsEn get stringsEn =\u003e switch (this) {\n        ja =\u003e AppLocale.ja.build(),\n        en =\u003e AppLocale.en.build(),\n      };\n\n  static LanguageType fromName(String name) =\u003e\n      LanguageType.values.firstWhere((e) =\u003e e.name == name);\n\n  @override\n  String toString() =\u003e switch (this) {\n        en =\u003e 'English',\n        ja =\u003e '日本語',\n      };\n}\n```\n\nSee:\n- [shared_preferences.dart](https://github.com/natsuk4ze/npm/blob/master/lib/util/shared_preferences.dart)\n- [language.dart](https://github.com/natsuk4ze/npm/blob/master/lib/settings/language.dart)\n- [i18n](https://github.com/natsuk4ze/npm/blob/master/lib/i18n)\n\n\u003c/details\u003e\n\n\u003cimg src=\"https://github.com/natsuk4ze/npm/raw/master/assets/readme/localization.gif\" width=200 alt=\"Localization\"/\u003e\n\n### 🪄 Responsive design\nDynamic layout for different screen sizes.\n\n\u003cdetails\u003e\n\u003csummary\u003eShow codes\u003c/summary\u003e\n\n```dart\nextension BuildContextX on BuildContext {\n  bool get isLargeScreen =\u003e MediaQuery.of(this).size.width \u003e 600;\n}\n\nchild: context.isLargeScreen\n    ? Row(\n        children: [\n          const _SortPanel(),\n          const VerticalDivider(),\n          Expanded(\n            child: _PackageItems(searchText: searchController.text),\n          ),\n        ],\n      )\n    : NestedScrollView(\n        headerSliverBuilder: (_, __) =\u003e [\n          const SliverAppBar(\n            surfaceTintColor: Colors.transparent,\n            toolbarHeight: 200,\n            title: _SortPanel(),\n          )\n        ],\n        body: _PackageItems(searchText: searchController.text),\n      ),\n```\n\nSee: \n- [extensions.dart](https://github.com/natsuk4ze/npm/blob/master/lib/util/extensions.dart)\n- [packages_page.dart](https://github.com/natsuk4ze/npm/blob/master/lib/features/packages/packages_page.dart)\n\n\u003c/details\u003e\n\n\u003cimg src=\"https://github.com/natsuk4ze/npm/raw/master/assets/readme/responsive.gif\" height=200 alt=\"Responsive\"/\u003e\n\n### ✅ Auto testing\nAuto testing with *github actions*.\n\n\u003cdetails\u003e\n\u003csummary\u003eShow codes\u003c/summary\u003e\n\n```yml\njobs:\n  test:\n    timeout-minutes: 30\n    strategy:\n      fail-fast: false\n    runs-on: macos-latest\n    steps:\n      - name: Check out\n        uses: actions/checkout@v4\n\n      - name: Setup JDK\n        uses: actions/setup-java@v3\n        with:\n          java-version: 11\n          distribution: temurin\n          cache: gradle\n\n      - name: Setup Flutter SDK\n        timeout-minutes: 10\n        uses: subosito/flutter-action@v2\n        with:\n          channel: beta\n\n      - name: Flutter Pub get\n        run: flutter pub get\n\n      - name: Flutter Analyze\n        run: flutter analyze\n\n      - name: Unit Test\n        timeout-minutes: 5\n        run: flutter test test/unit_test.dart\n\n      - name: Widget Test\n        timeout-minutes: 5\n        run: flutter test test/widget_test.dart\n\n      - name: Golden Test\n        timeout-minutes: 5\n        run: flutter test test/golden_test.dart\n\n      - name: Build iOS\n        timeout-minutes: 10\n        run: flutter build ios --no-codesign\n\n      - name: Build Android\n        timeout-minutes: 10\n        run: flutter build appbundle\n```\n\nSee:\n- [unit_test](https://github.com/natsuk4ze/npm/blob/master/test/unit_test.dart)\n- [widget_test](https://github.com/natsuk4ze/npm/blob/master/test/widget_test.dart)\n- [golden test](https://github.com/natsuk4ze/npm/blob/master/test/golden_test.dart)\n- [workflows](https://github.com/natsuk4ze/npm/actions)\n\n\u003c/details\u003e\n\n\u003cimg src=\"https://github.com/natsuk4ze/npm/raw/master/assets/readme/auto_test.gif\" width=330 alt=\"Auto test\"/\u003e\n\n\n## Discussion about folder structure\n\nThe project uses a feature-first folder structure.  \nThis will depend on the project, but I find it best to put things close together that are closely related.\n\n### Why is there no layer folder like \"presentation\"\n\nThis project is minimal and even if you create a layered folder, only one file can go in that folder.  \nPutting them in a folder then would only needlessly add to the hierarchy and make it harder to see.  \nThis should be best suited for the size of the project.\n\n### Should Providers and UI always be placed in separate files?\n\nI don't think so. Look at [packages_page.dart](https://github.com/natsuk4ze/npm/blob/master/lib/features/packages/packages_page.dart) for example, which is a UI file, but with providers.\nMy basic idea is **put things close to each other in close proximity.**\nI don't think it's necessary to put them in a data (domain) layer file, since all the providers here are only for this *packages page*.\nHowever, this should also be changed depending on the project. If it is a large project, these providers might be placed in a file called ~controller. But this seems a bit far from the declarative UI philosophy.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnatsuk4ze%2Fnpm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnatsuk4ze%2Fnpm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnatsuk4ze%2Fnpm/lists"}