lib_x 0.0.6 lib_x: ^0.0.6 copied to clipboard
lib_x is an Object-Oriented approach to flutter. It provides Routing, State Management & Data Providers Solutions.
Getting Started #
This example application will demonstrate how an application with lib_x should be structured.
In a real world application we need to structure our app to be maintainable, scalable, and to allow for collaboration between developers. And for that, we need a clear design pattern, and separation of concerns among other things.
lib_x was designed to help achieve these goals. And here's a porposal structure you can use as a starting point for your next application.
- lib/
- src/
- data/
- user_model.dart
- render/
- const/
- material_app.dart
- route_map.dart
- theme_data.dart
- ...
- forms/
- login.dart
- sign_up.dart
- ...
- layout/
- app_bar.dart
- drawer.dart
- navigation_bar.dart
- scaffold.dart
- ...
- pages/
- home_pages.dart
- not_found_page.dart
- ...
- my_app.dart
- const/
- services/
- auth.dart
- api.dart
- shared_prefs.dart
- ...
- src.dart
- data/
- main.dart
- src/
Now let's imagine we have an application that loads news stories from some source, and see how it could be structured properly.
After flutter create example
lib/
#
let's keep it clean and make a new folder src to be the container of the app and let's have a simple main.dart like this:
lib/main.dart
import 'package:example/src/src.dart';
void main() {
runApp(const MyApp());
}
src/:
#
we'll create a src.dart file and 3 folders.
src.dart
will serve as an index of the app's components.
lib/src/src.dart
export 'package:lib_x/lib_x.dart';
// Data
export 'data/news_list.dart';
export 'data/news_story.dart';
// Render
export 'render/const/material_app.dart';
export 'render/const/route_map.dart';
export 'render/const/theme_data.dart';
export 'render/my_app.dart';
export 'render/pages/home_page.dart';
export 'render/pages/not_found_page.dart';
export 'render/pages/story_page.dart';
// Services
export 'services/api.dart';
// and so on, all our declaration will be exported from here
-
services/
Inside this directory, we'll handle the services the application need, e.g. if the app requires authentication, we'll create a file auth.dart that will contain our auth logic. If we're dealing with firestore db, we'll create firestore.dart that deals with firestore. If dealing with firebase storage, there will be a storage.dart file... etc
#
The benefits of that: if you wanted at some point to migrate from firestore to another database service provider. All you'll need to do is making a new file for the new service that will replace firestore without touching any widgets or data models. Also it can be done by a completely different developer. they'll just see the ins and outs of the firestore service class and create the replacement to take the same inputs, and returns the expected output.
lib/src/services/api.dart
import 'package:example/src/src.dart';
abstract class Api {
static Future<List<NewsStory>> fetchStories() async {
// load from db logic
final List<NewsStory> results = [];
// fake it till you make it
final List<Map<String, dynamic>> snapshot = List.generate(
10,
(index) => {
'id': genId(),
'title': 'Title: $index',
'content': 'Some content here',
},
);
for (var value in snapshot) {
results.add(NewsStory.fromMap(value));
}
return results;
}
}
Note: each service class should be an encapsulation for only one service. Don't make a class that does everything.
-
data/
#
Here will be the data concerns only. lib_x provides 2 useful types that will help with data management concerns.
StatefulData
will make the data model listenable by the views.DataProvider<T>
will make the data accessible by context in the widget tree.
lib/src/data/news_story.dart
import 'package:example/src/src.dart';
// 2- create the data model provider
class StoryProvider extends DataProvider<NewsStory> {
// Declare the desired data you want the provider to provide
final NewsStory story;
const StoryProvider({
super.key,
required this.story, // first argument: require the declared data object
required super.child, // second argument: require & pass child to the super class
}) : super(data: story); // pass the data to the super class
// Declare a static method that returns the provider instance of(context)
static StoryProvider of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<StoryProvider>()!;
}
// 1- create the data model controller
class NewsStory extends StatefulData {
final String id;
final String title;
final String content;
bool readLater;
NewsStory({
required this.id,
required this.title,
required this.content,
this.readLater = false,
});
// it's best practice to make all class converters [from || to] class in factory method inside the class
factory NewsStory.fromMap(Map<String, dynamic> map) {
return NewsStory(
id: map['id'] as String,
title: map['title'] as String,
content: map['content'] as String,
readLater: map['readLater'] ?? false,
);
}
void toggleReadLater() {
readLater = !readLater;
update();
}
}
lib/src/data/news_list.dart
import 'package:example/src/src.dart';
class NewsListProvider extends DataProvider<NewsList> {
final NewsList newsList;
const NewsListProvider({
super.key,
required this.newsList,
required super.child,
}) : super(data: newsList);
static NewsListProvider of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<NewsListProvider>()!;
}
class NewsList extends StatefulData {
final List<NewsStory> newsList = [];
final List<NewsStory> readLaterList = [];
void loadNewsStories() async {
final List<NewsStory> stories = await Api.fetchStories();
newsList.addAll(stories);
update();
}
void loadReadLater() async {
readLaterList.clear();
final List<NewsStory> stories = await Future.value(
newsList.where((story) => story.readLater).toList(),
);
readLaterList.addAll(stories);
update();
}
NewsStory getById(String id) => newsList.singleWhere((story) => story.id == id);
}
Notes:
-
It's best to declare the provider of a type in the same file with its declaration. So, all the concerns of that type are in the same file. Single Source Of Truth files.
-
If there's a lot of data types, it best to group related types in sub-directories.. and so on.
-
render/ #
Here will be all the rendering concerns, separated from the data management, and services implementations. And again, we'll group and classify the views elements into sub-directories. So let's plan our steps to implement the views components.
1. First step: Define MyApp and its dependencies. #
my_app.dart #
which will contain MaterialApp
, the entery point and first concern for rendering the app.
lib/src/render/my_app.dart
import 'package:example/src/src.dart';
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
// App level providers
// providers can be nested as much as we need
final NewsList news = NewsList()..loadNewsStories();
return NewsListProvider(
newsList: news,
child: MaterialX(
materialApp: materialApp, // ToDo: declare
routeMap: routeMap, // ToDo: declare
),
);
}
}
render/const/ #
in this directory will have the constant definitions the app need. And for a minimal app, it'll be something like this:
lib/src/render/const/theme_data.dart
// themes and color scheme of the app.
const Color lightC = Color.fromARGB(255, 230, 226, 247);
const Color lightC1 = Color.fromARGB(255, 241, 239, 253);
const Color darkC = Color.fromARGB(255, 39, 37, 54);
const Color darkC1 = Color.fromARGB(255, 44, 42, 62);
const Color primaryC = Color.fromARGB(255, 255, 0, 212);
final ThemeData myLightTheme = ThemeData.light().copyWith(
visualDensity: VisualDensity.adaptivePlatformDensity,
primaryColor: primaryC,
scaffoldBackgroundColor: lightC,
canvasColor: transparent,
);
final ThemeData myDarkTheme = ThemeData.dark().copyWith(
visualDensity: VisualDensity.adaptivePlatformDensity,
primaryColor: primaryC,
scaffoldBackgroundColor: darkC,
canvasColor: transparent,
);
lib/src/render/const/route_map.dart
// routes consts & map
import 'package:example/src/src.dart';
// Root = '/'; // predefined in lib_x
// always define paths to avoid mistakes
const String StoryPath = '/story/:id'; // path pattern
// functional string path
String storyPath(String id) => '/story/$id';
final RouteMap routeMap = RouteMap(
routes: {
Root: (RouteData info) => const MaterialPage(child: HomePage()), // ToDo: declare
StoryPath: (RouteData info) =>
MaterialPage(child: StoryPage(id: info.pathParameters['id']!)), // ToDo: declare
},
onUnknownRoute: (_) => const MaterialPage(child: NotFoundPage()),
);
lib/src/render/const/material_app.dart
import 'package:example/src/src.dart';
final MaterialApp materialApp = MaterialApp(
title: 'Example App',
debugShowCheckedModeBanner: false,
theme: myLightTheme,
darkTheme: myDarkTheme,
);
2. Second step: Defining the pages we promised in the route_map. #
lib/src/render/pages/home_page.dart
import 'package:example/src/src.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>
with SingleTickerProviderStateMixin {
@override
void initState() {
super.initState();
// we need to asign the tabController in order to use it
NewsList.tabController = TabController(vsync: this, length: 2);
}
@override
Widget build(BuildContext context) {
return ScaffoldX(
appBar: const HomeAppBar(), // ToDo: declare
body: TabBarView(
controller: NewsList.tabController,
children: const [
StoriesListView(), // ToDo: declare
ReadLaterListView(), // ToDo: declare
],
),
);
}
}
lib/src/render/layout/app_bar.dart - add to src.dart
import 'package:example/src/src.dart';
class HomeAppBar extends StatelessWidget {
const HomeAppBar({super.key});
@override
Widget build(BuildContext context) {
final ValueController<int> indexController =
ValueController<int>(NewsList.tabController.index);
NewsList.tabController.addListener(() {
indexController.update(NewsList.tabController.index);
if (NewsList.tabController.index == 1) {
newsController.loadReadLater();
}
});
return ReactiveBuilder(
controller: indexController,
builder: (index) {
return ReactiveBuilder(
controller: X.themeMode,
builder: (themeMode) {
return AppBar(
backgroundColor: themeMode == ThemeMode.dark ? darkC1 : lightC1,
bottom: TabBar(
controller: NewsList.tabController,
indicatorColor: primaryC,
tabs: [
Tab(
icon: Icon(
Icons.menu_rounded,
color: index == 0 ? primaryC : black,
),
),
Tab(
icon: Icon(
Icons.schedule,
color: index == 1 ? primaryC : black,
),
),
],
),
);
},
);
},
);
}
}
lib/src/render/lists/stories_list.dart - add to src.dart
import 'package:example/src/src.dart';
class StoriesListView extends StatelessWidget {
const StoriesListView({super.key});
@override
Widget build(BuildContext context) {
final NewsList newsController = NewsListProvider.of(context).newsList;
return ReBuilder(
controller: newsController,
builder: () {
return ListView.builder(
itemCount: newsController.newsList.length,
itemBuilder: (context, index) {
return StoryProvider(
story: newsController.newsList[index],
child: const StoryCard(),
);
},
);
},
);
}
}
lib/src/render/lists/read_later_list.dart - add to src.dart
import 'package:example/src/src.dart';
class ReadLaterListView extends StatelessWidget {
const ReadLaterListView({super.key});
@override
Widget build(BuildContext context) {
final NewsList newsController = NewsListProvider.of(context).newsList
..loadReadLater();
return ReBuilder(
controller: newsController,
builder: () {
return ListView.builder(
itemCount: newsController.readLaterList.length,
itemBuilder: (context, index) {
return StoryProvider(
story: newsController.readLaterList[index],
child: const StoryCard(),
);
},
);
},
);
}
}
lib/src/render/cards/story_card.dart - add to src.dart
import 'package:example/src/src.dart';
class StoryCard extends StatelessWidget {
const StoryCard({super.key});
@override
Widget build(BuildContext context) {
final NewsStory story = StoryProvider.of(context).story;
return Column(
children: [
const SizedBox(height: 20),
GestureDetector(
onTap: () => X.to(path: storyPath(story.id)),
child: Text(story.title),
),
ReBuilder(
controller: story,
builder: () {
final bool added = story.readLater;
return TextButton(
onPressed: () => story.toggleReadLater(),
child:
Text(added ? 'Remove from read later' : 'Add to read later'),
);
},
),
],
);
}
}
lib/src/render/pages/story_page.dart
import 'package:example/src/src.dart';
class StoryPage extends StatelessWidget {
final String id;
const StoryPage({super.key, required this.id});
@override
Widget build(BuildContext context) {
final NewsList newsController = NewsListProvider.of(context).newsList;
final NewsStory story = newsController.getById(id);
return StoryProvider(
story: story,
child: Column(
children: const [
StoryTitle(),
StoryContent(),
],
),
);
}
}
class StoryTitle extends StatelessWidget {
const StoryTitle({super.key});
@override
Widget build(BuildContext context) {
final NewsStory story = StoryProvider.of(context).story;
return Text(story.title);
}
}
class StoryContent extends StatelessWidget {
const StoryContent({super.key});
@override
Widget build(BuildContext context) {
final NewsStory story = StoryProvider.of(context).story;
return Text(story.content);
}
}
I just realized I passed the 500 line, which is ironic cause no one will read this.
P.S. #
- Structure does matter.
- Try as possible to make your files describable as A Single Source Of Truth.
- Naming should be semantic and self-explanatory, even if you're working solo. It'll save you a lot of time when you want to debug or update something later.