https://github.com/natsuk4ze/npm
npm client app as minimal flutter project example | riverpod x freezed x hooks x dio x shared_preferences
https://github.com/natsuk4ze/npm
android app application dart dio flutter freezed ios npm npm-package packages python riverpod sharedpreferences
Last synced: 4 months ago
JSON representation
npm client app as minimal flutter project example | riverpod x freezed x hooks x dio x shared_preferences
- Host: GitHub
- URL: https://github.com/natsuk4ze/npm
- Owner: natsuk4ze
- License: mit
- Created: 2023-09-21T12:10:09.000Z (almost 3 years ago)
- Default Branch: master
- Last Pushed: 2026-02-02T02:02:22.000Z (5 months ago)
- Last Synced: 2026-02-02T11:49:00.034Z (5 months ago)
- Topics: android, app, application, dart, dio, flutter, freezed, ios, npm, npm-package, packages, python, riverpod, sharedpreferences
- Language: Dart
- Homepage:
- Size: 189 MB
- Stars: 15
- Watchers: 1
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yml
- License: LICENSE
Awesome Lists containing this project
README
# 📦 npm client
[](https://app.codacy.com/gh/natsuk4ze/npm/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
[](https://www.codefactor.io/repository/github/natsuk4ze/npm)


### A [npm](https://www.npmjs.com) client app as minimal flutter project example
### [🌐 Device Preview on Flutter Web](https://natsuk4ze.github.io/npm)
### Using
- *hooks_riverpod* for state management
- *freezed* for serializing (deserializing) json objects
- *dio* for network request
- *shared_preferences* for local database
- *slang* for localization

## How to install
1. install [flutter](https://docs.flutter.dev/get-started/install)
2. clone this repository
3. run `cd $PATH_TO_REPOSITORY`
4. run `flutter pub get`
5. run `dart run build_runner build`
6. run `dart run slang`
7. run `flutter run`
New to flutter? See: [How to install flutter app on your device](https://www.youtube.com/watch?v=aohkII1C4JY)
## Features
### 🛜 Real time fetch
Real-time fetching with *dio* and *riverpod*.
Listen to `TextEditingController` to rebuild the widget.
Show codes
```dart
final searchController = useTextEditingController(text: initialSearchText);
useListenable(searchController);
```
See: [packages_page.dart](https://github.com/natsuk4ze/npm/blob/master/lib/features/packages/packages_page.dart)

### 👌 Pull to refresh
Pull to refresh with `RefreshIndicator`.
By returning `.future` to `onRefresh`, the indicator will continue to be displayed until the data fetching is complete.
Show codes
```dart
RefreshIndicator(
onRefresh: () => ref.refresh(packagesProvider(
search: searchText,
debounce: false,
).future),
child: ListView.separated(
separatorBuilder: (_, __) => const Divider(),
itemCount: sortedPackages.length,
itemBuilder: (_, int i) => PackageItem(sortedPackages[i]),
),
);
```
See: [packages_page.dart](https://github.com/natsuk4ze/npm/blob/master/lib/features/packages/packages_page.dart)

### 📊 Sort
Sort with *riverpod*.
Show codes
```dart
@riverpod
class Sort extends _$Sort {
@override
ScoreType? build() => null;
void update(ScoreType type) => state = type;
}
@riverpod
Future> sortedPackages(SortedPackagesRef ref,
{required String search}) async {
final packages = await ref.watch(packagesProvider(search: search).future);
final sort = ref.watch(sortProvider);
return sort == null
? List.of(packages)
: packages.sortedByCompare(
(package) => sort.getValue(package.score), (a, b) => b.compareTo(a));
}
```
See: [packages_page.dart](https://github.com/natsuk4ze/npm/blob/master/lib/features/packages/packages_page.dart)

### ☁️ Empty state
Switching widget according to status with `AsyncValue`.
Show codes
```dart
return sortedPackages.isEmpty
? SingleChildScrollView(
child: EmptyImage(text: l10n.packagesPage.packageNotFound),
)
: RefreshIndicator(
onRefresh: () async => ref.refresh(packagesProvider(
search: searchText,
debounce: false,
).future),
child: ListView.separated(
separatorBuilder: (_, __) => const Divider(),
itemCount: sortedPackages.length,
itemBuilder: (_, int i) => PackageItem(sortedPackages[i]),
),
);
```
See: [packages_page.dart](https://github.com/natsuk4ze/npm/blob/master/lib/features/packages/packages_page.dart)

### 🪽 Jump to repository
Jumping to repository with *url_launcher*.
Show codes
```dart
class LinkText extends StatelessWidget {
const LinkText(
this.url, {
this.text,
super.key,
});
final String? text;
final String url;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () async => launchUrl(Uri.parse(url)),
child: Text(
text ?? url,
style: const TextStyle(
fontWeight: FontWeight.bold,
decoration: TextDecoration.underline,
),
),
);
}
}
```
See: [link_text.dart](https://github.com/natsuk4ze/npm/blob/master/lib/common_widgets/link_text.dart)

### 🔍 See package details
Getting package details for requesting api with *dio* and *freezed*.
Show codes
```dart
@riverpod
Dio dio(DioRef ref) => Dio();
@riverpod
Future packageDetails(PackageDetailsRef ref,
{required String id}) async {
final response = await ref.watch(dioProvider).getUri(
Uri.parse('https://registry.npmjs.org/$id'),
);
return PackageDetails.fromJson(response.data!);
}
@freezed
class PackageDetails with _$PackageDetails {
const PackageDetails._();
const factory PackageDetails({
required final String name,
final String? description,
final String? homepage,
final String? repository,
final String? readme,
final List? keywords,
final String? license,
}) = _PackageDetails;
factory PackageDetails.fromJson(Json json) {
final git = json['repository']?['url'] as String?;
return PackageDetails(
name: json['name'],
description: json['description'],
keywords: ListX.fromOrNull(json['keywords']),
license: json['license'],
homepage: json['homepage'],
repository: git == null ? null : Format.urlFromGit(git),
readme: json['readme'],
);
}
}
```
See:
- [dio.dart](https://github.com/natsuk4ze/npm/blob/master/lib/util/dio.dart)
- [package_details.dart](https://github.com/natsuk4ze/npm/blob/master/lib/features/package_details/package_details.dart)

### 🌙 Dark mode
Dynamic theming with *riverpod* and *shared_preferences*.
Use `ref.invalidateSelf()` for SSOT design.
Show codes
```dart
@Riverpod(keepAlive: true)
SharedPreferences sharedPreferences(SharedPreferencesRef ref) =>
throw UnimplementedError('SharedPreferences is not overridden.');
@riverpod
class IsDarkMode extends _$IsDarkMode {
static const _key = 'isDarkMode';
@override
bool build() {
final prefs = ref.watch(sharedPreferencesProvider);
return prefs.getBool(_key) ?? false;
}
Future toggle() async {
final prefs = ref.read(sharedPreferencesProvider);
await prefs.setBool(_key, !state);
ref.invalidateSelf();
}
}
```
See:
- [shared_preferences.dart](https://github.com/natsuk4ze/npm/blob/master/lib/util/shared_preferences.dart)
- [dark_mode.dart](https://github.com/natsuk4ze/npm/blob/master/lib/features/settings/dark_mode.dart)

### 🗣️ Localization
Dynamic localization with *slang*, *riverpod* and *shared_preferences*.
Use `ref.invalidateSelf()` for SSOT design.
Show codes
```dart
@riverpod
StringsEn l10n(L10nRef ref) => ref.watch(languageProvider).stringsEn;
@riverpod
class Language extends _$Language {
static const _key = 'language';
@override
LanguageType build() {
final prefs = ref.watch(sharedPreferencesProvider);
return LanguageType.fromName(prefs.getString(_key) ?? LanguageType.en.name);
}
Future update(LanguageType type) async {
final prefs = ref.read(sharedPreferencesProvider);
await prefs.setString(_key, type.name);
ref.invalidateSelf();
}
}
enum LanguageType {
en,
ja;
StringsEn get stringsEn => switch (this) {
ja => AppLocale.ja.build(),
en => AppLocale.en.build(),
};
static LanguageType fromName(String name) =>
LanguageType.values.firstWhere((e) => e.name == name);
@override
String toString() => switch (this) {
en => 'English',
ja => '日本語',
};
}
```
See:
- [shared_preferences.dart](https://github.com/natsuk4ze/npm/blob/master/lib/util/shared_preferences.dart)
- [language.dart](https://github.com/natsuk4ze/npm/blob/master/lib/settings/language.dart)
- [i18n](https://github.com/natsuk4ze/npm/blob/master/lib/i18n)

### 🪄 Responsive design
Dynamic layout for different screen sizes.
Show codes
```dart
extension BuildContextX on BuildContext {
bool get isLargeScreen => MediaQuery.of(this).size.width > 600;
}
child: context.isLargeScreen
? Row(
children: [
const _SortPanel(),
const VerticalDivider(),
Expanded(
child: _PackageItems(searchText: searchController.text),
),
],
)
: NestedScrollView(
headerSliverBuilder: (_, __) => [
const SliverAppBar(
surfaceTintColor: Colors.transparent,
toolbarHeight: 200,
title: _SortPanel(),
)
],
body: _PackageItems(searchText: searchController.text),
),
```
See:
- [extensions.dart](https://github.com/natsuk4ze/npm/blob/master/lib/util/extensions.dart)
- [packages_page.dart](https://github.com/natsuk4ze/npm/blob/master/lib/features/packages/packages_page.dart)

### ✅ Auto testing
Auto testing with *github actions*.
Show codes
```yml
jobs:
test:
timeout-minutes: 30
strategy:
fail-fast: false
runs-on: macos-latest
steps:
- name: Check out
uses: actions/checkout@v4
- name: Setup JDK
uses: actions/setup-java@v3
with:
java-version: 11
distribution: temurin
cache: gradle
- name: Setup Flutter SDK
timeout-minutes: 10
uses: subosito/flutter-action@v2
with:
channel: beta
- name: Flutter Pub get
run: flutter pub get
- name: Flutter Analyze
run: flutter analyze
- name: Unit Test
timeout-minutes: 5
run: flutter test test/unit_test.dart
- name: Widget Test
timeout-minutes: 5
run: flutter test test/widget_test.dart
- name: Golden Test
timeout-minutes: 5
run: flutter test test/golden_test.dart
- name: Build iOS
timeout-minutes: 10
run: flutter build ios --no-codesign
- name: Build Android
timeout-minutes: 10
run: flutter build appbundle
```
See:
- [unit_test](https://github.com/natsuk4ze/npm/blob/master/test/unit_test.dart)
- [widget_test](https://github.com/natsuk4ze/npm/blob/master/test/widget_test.dart)
- [golden test](https://github.com/natsuk4ze/npm/blob/master/test/golden_test.dart)
- [workflows](https://github.com/natsuk4ze/npm/actions)

## Discussion about folder structure
The project uses a feature-first folder structure.
This will depend on the project, but I find it best to put things close together that are closely related.
### Why is there no layer folder like "presentation"
This project is minimal and even if you create a layered folder, only one file can go in that folder.
Putting them in a folder then would only needlessly add to the hierarchy and make it harder to see.
This should be best suited for the size of the project.
### Should Providers and UI always be placed in separate files?
I 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.
My basic idea is **put things close to each other in close proximity.**
I 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*.
However, 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.