https://github.com/anasfik/flutter_nostr
Build scalable/complex Nostr apps effortlessly — even as a beginner!
https://github.com/anasfik/flutter_nostr
beginner-friendly developer-tools flutter flutter-package nostr nostr-protocol nostr-sdk nostr-tools web3
Last synced: 9 months ago
JSON representation
Build scalable/complex Nostr apps effortlessly — even as a beginner!
- Host: GitHub
- URL: https://github.com/anasfik/flutter_nostr
- Owner: anasfik
- License: other
- Created: 2025-10-12T20:46:58.000Z (10 months ago)
- Default Branch: main
- Last Pushed: 2025-10-25T21:18:28.000Z (9 months ago)
- Last Synced: 2025-10-25T23:19:34.493Z (9 months ago)
- Topics: beginner-friendly, developer-tools, flutter, flutter-package, nostr, nostr-protocol, nostr-sdk, nostr-tools, web3
- Language: Dart
- Homepage:
- Size: 145 KB
- Stars: 5
- Watchers: 1
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Funding: .github/FUNDING.yml
- License: LICENSE
Awesome Lists containing this project
- awesome-nostr - flutter_nostr - Build scalable/complex Nostr apps effortlessly with Flutter (Libraries / Client reviews and/or comparisons)
README
# 🚀 flutter_nostr




**Beautiful, pragmatic Flutter primitives for building Nostr-powered feeds and social UX**
[📖 Documentation](#-documentation) • [🎯 Quick Start](#-quick-start) • [💡 Examples](#-examples) • [🔧 API Reference](#-api-reference) • [🤝 Contributing](#-contributing)
---
## ✨ What is flutter_nostr?
`flutter_nostr` is a powerful Flutter package that provides **building blocks** for creating Nostr-powered social applications. It handles the complexity of fetching events, enriching them with related data, and rendering them in beautiful, performant Flutter widgets.
### 🎯 Key Features
- 🔄 **Parallel Data Fetching**: Efficiently fetch related data (profiles, reactions, etc.) using typed parallel requests
- 📱 **Flutter-Native**: Built specifically for Flutter with proper state management and widget lifecycle
- 🎨 **Customizable**: Flexible builder patterns and adapters for any data structure
- ⚡ **Performant**: Smart caching, pagination, and visibility-based loading
- 🔗 **Type-Safe**: Full TypeScript-like type safety with Dart generics
- 📚 **Example-Rich**: Complete example app with multiple use cases
## 🚀 Quick Start
### 1. Installation
Add to your `pubspec.yaml`:
```yaml
dependencies:
flutter_nostr: ^0.1.0
```
### 2. Initialize
```dart
import 'package:flutter/material.dart';
import 'package:flutter_nostr/flutter_nostr.dart';
void main() async {
// Initialize with one or more relays
await FlutterNostr.init(relays: [
'wss://relay.nostr.band',
'wss://nos.lol',
]);
runApp(const MyApp());
}
```
### 3. Enjoy the package capabilities.
## Feeds
Feeds are the core building block of social applications, it is that simple scrollable list of events enriched with related data customized to your needs, with support to all states like loading, error, empty, pull-to-refresh, infinite scroll and more.
### Simple Feed (1-layer Feed)
#### Key Components
The main component that you will always use to build a feed is `FlutterNostrFeed`, it is responsible for fetching the data and providing it to the `builder` function.
Inside the builder function, you can take full control on how/what you want to support in your feed, or you can simply use the pre-built `FlutterNostrFeedList` which abstract general handling for a feed like loading, error handling... and only prompt you to build the UI for your list of items.
The simple feed is refered to a feed that only loads requests sequentianlly only, and does require loading request events **only**
#### Example:
```dart
FlutterNostrFeed(
filters: [
NostrFilter(
limit: 10, // limit each loading to 10 events
kinds: [30402], // listings kinds
),
],.
builder: (context, data, options) {
return FlutterNostrFeedList(
data: data,
options: options,
itemBuilder: (context, event, index, data, options) {
return ListTile(
title: Text(event.content ?? 'No content'),
subtitle: Text('Author: ${event.pubkey.substring(0, 8)}...'),
);
},
);
},
),
```
**That's it!** 🎉 You now have a fully functional Nostr listings feed with:
- ✅ Pull-to-refresh
- ✅ Infinite scroll
- ✅ Loading states
- ✅ Error handling
- ✅ Smooth and lag-free rendering (using `ListView.builder` and some other techniques..)
- ✅ Much more handling that you propably don't know about but happens under the hood.
### Rich Feed (Multi-layer Feed)
In order for a Nostr Feed to be functional and usable, loading more entities that relates to what actually was requested initially is requested. as an example, creating a posts feed shoulf also fetch for these posts authors details such name, username, picture...
This is where the package also abstracts the complexity of doing so in a multi-layer feed, which basically have parallel loading for the initial requested events, so lets take this use case:
- When the posts feed is loaded. (**Layer 1**)
- For each post (Loaded in **Layer 1**) that the end-user sees, more parallel requests executes to get their authors user details, reactions, zaps, comments, referenced events/pubkeys if any with `nevent` ,`npub`,`note`, `nprofile`..., parent events if any... (**Layer 2**)
- For each post (Loaded in **Layer 1**) reaction/comment (Loaded in **Layer 2**), more parallel requests execute to get their user details or more related data (**Layer 3**)
- More parallel fetching if needed...
With this mechanism, you will be basically be able to build your goal feed even if it will require much more fetching layers.
#### Key Components
Note: I assume you read the [Simple Feed](#simple-feed-1-layer-feed) section before continuing.
- The `FlutterNostrFeed` still the main component that you will use to build a feed, but now you will also use the `parallelRequestRequestsHandler` parameter to define your parallel requests using the already loaded data from previous layers.
- Each parallel request is represented by the `ParallelRequest` class, which is a typed request that holds the filters to be used to fetch the related data, and an adapter function to convert the fetched `NostrEvent` into the desired type `T`.
- Each parallel request is identified from other parallel requests if any with the `ParallelRequestId`, where you basically create a unique `id` for each request you want to make.
- Results of each request is passed to the `builder` via the `FeedBuilderData data` parameter, where you can access the results of each request by its `ParallelRequestId`.
- To execute more parallel requests based on the results of previous parallel requests, you can use the `.then()` method on the `ParallelRequest` instance to chain more requests, like:
```dart
ParallelRequest(
//...
).then((List previousResults) {
return ParallelRequest(
//...
);
}).then((List previousResults) {
return ParallelRequest(
//...
);
}).then((List previousResults) {
return ParallelRequest(
//...
);
});
```
#### Example 1: Feed with User Profiles
```dart
final profileFetchRequestId = ParallelRequestId(id: 'unique-id-1');
FlutterNostrFeed(
filters: [
NostrFilter(
limit: 25,
kinds: [1], // posts kinds
),
],
parallelRequestRequestsHandler: (_, List postEvents) {
return ParallelRequest(
id: profileFetchRequestId,
filters: [
NostrFilter(
kinds: [0], // user details kind
authors: postEvents.map((e) => e.pubkey).toList(),
),
],
adapter: (event) {
return UserInfo.fromEvent(event);
},
);
},
builder: (context, data, options) {
return FlutterNostrFeedList(
data: data,
options: options,
itemBuilder: (context, NostrEvent postEvent, index, data, options) {
final postContent = postEvent.content != null ? postEvent.content! : "";
// This is how we access the requests results for a specific parallel request by its id
final profileFetchResults = data.parallelRequestResultsFor(
profileFetchRequestId,
);
List userResults = profileFetchResults?.adaptedResults ?? [];
UserInfo? user =
userResults
.where((element) => element.event.pubkey == postEvent.pubkey)
.firstOrNull;
final postOwnerName =
user?.name.isEmpty ?? true ? "Loading Or Unknown" : user!.name;
return ListTile(
title: Text(postOwnerName),
subtitle: Text(postContent),
);
},
);
},
),
```
#### Example 2: Feed with user profiles, user followings and user followers (Multi-Layer Feed)
```dart
final profileFetchRequestId = ParallelRequestId(id: 'unique-id-1');
final followingsFetchRequestId = ParallelRequestId(id: 'unique-id-2');
FlutterNostrFeed(
filters: [
NostrFilter(
limit: 25,
kinds: [1], // posts kinds
),
],
parallelRequestRequestsHandler: (_, List postEvents) {
return ParallelRequest(
id: profileFetchRequestId,
filters: [
NostrFilter(
kinds: [0], // user details kind
authors: postEvents.map((e) => e.pubkey).toList(),
),
],
adapter: (event) {
return UserInfo.fromEvent(event);
},
).then((List users) {
return ParallelRequest(
id: followingsFetchRequestId,
filters: [
NostrFilter(
kinds: [3], // user followings kind
authors: users.map((u) => u.event.pubkey).toList(),
),
],
adapter: (event) {
return UserFollowings.fromEvent(event);
},
);
});
},
builder: (context, data, options) {
return FlutterNostrFeedList(
data: data,
options: options,
itemBuilder: (context, NostrEvent postEvent, index, data, options) {
final postContent = postEvent.content != null ? postEvent.content! : "";
final profileFetchResults = data.parallelRequestResultsFor(
profileFetchRequestId,
);
final followingsFetchResults = data.parallelRequestResultsFor(
followingsFetchRequestId,
);
List userResults = profileFetchResults?.adaptedResults ?? [];
List followingsResults =
followingsFetchResults?.adaptedResults ?? [];
UserInfo? user =
userResults
.where((element) => element.event.pubkey == postEvent.pubkey)
.firstOrNull;
UserFollowings? userFollowings =
followingsResults
.where((element) => element.pubkey == postEvent.pubkey)
.firstOrNull;
final postOwnerName =
user?.name.isEmpty ?? true ? "Loading Or Unknown" : user!.name;
final postOwnerFollowingsCount = userFollowings?.followings.length ?? 0;
return ListTile(
title: Text(postOwnerName),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(postContent),
SizedBox(height: 4),
Text(
'Followings: $postOwnerFollowingsCount',
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
);
},
);
},
)
```
## 🎮 Example App
The package includes a comprehensive example app you can run and explore visually different use cases and implementations, simply run:
```bash
cd example
flutter pub get
flutter run
```
---
## 🛠️ Advanced Topics
### Error Handling
```dart
FlutterNostrFeed(
builder: (context, data, options) {
if (options.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error, size: 64, color: Colors.red),
SizedBox(height: 16),
Text('Failed to load feed'),
SizedBox(height: 8),
ElevatedButton(
onPressed: options.refresh,
child: Text('Retry'),
),
],
),
);
}
return FlutterNostrFeedList(/* ... */);
},
)
```
## 🤝 Contributing
We welcome contributions! Here's how you can help:
### 🐛 Bug Reports
- Use the issue template
- Include steps to reproduce
- Provide Flutter/Dart version info
### 💡 Feature Requests
- Describe the use case
- Explain why it would be valuable
- Consider contributing a PR
### 🔧 Pull Requests
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Update documentation
- Submit a PR with a clear description
## 📋 Roadmap
### 🎯 Version 0.2
- [ ] Chat primitives (NIP-44)
- [ ] Identity helpers and key management
- [ ] Enhanced error handling
- [ ] Performance improvements
### 🚀 Future Versions
- [ ] Payment integration (Lightning)
- [ ] NostrConnect support
- [ ] Relay moderation tools
- [ ] Advanced caching strategies
- [ ] WebSocket connection pooling
---
## 📄 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
---
## 🙏 Acknowledgments
- Built on top of [dart_nostr](https://pub.dev/packages/dart_nostr) package
- Inspired by the Nostr protocol's simplicity and power
- Community feedback and contributions
---
**Made with ❤️ for the Nostr community**
[⭐ Star this repo](https://github.com/anasfik/flutter_nostr) • [🐛 Report issues](https://github.com/anasfik/flutter_nostr/issues) • [💬 Join discussions](https://github.com/anasfik/flutter_nostr/discussions)