Create flutter apps quickly by utilizing commonly used tools.
The aim of this package is to make building apps quick and seamless, incorperating some frequently used widgets in app development.
Getting started
- add package
flutter pub add close_range_util
- Inside your main function when you call runApp, pass in an
Entry
object
runApp(Entry(
title: "Testing 123",
home: MyFirstPage(...), // Widget of inital page page
));
The point of this is to allow for global bindings such as themes to the whole project.
Setup
Some features require an initilization phase.
This can be done right inside of main before calling runApp
For example:
void main() async {
await CREnv.init();
await CRSave.init();
await CRDatabase.init(
url: CREnv["SUPABASE_URL"]!,
anonKey: CREnv["SUPABASE_ANON_KEY"]!,
);
Debug.setActive(false);
runApp(...);
}
Features
Login
Documentation
App Bar
Calendar
Money Counter
Debug Page
Settings
Region
Profile Banner
Systems
Enviroment
Save System
User
Database
Debug
Theme
Images
Profile Pictures
Messaging
Login
First, ensure User tables are defined and that you enabled Database
CRLoginPage(
homePage: Widget
devLogins: [],
// image: "res/images/christine.png",
image:
"https://upload.wikimedia.org/wikipedia/commons/thumb/7/79/Flutter_logo.svg/2048px-Flutter_logo.svg.png",
imageIsNetwork: true,
imageHeight: 200,
imagePadding: 25,
);
Docs
Docs is a way to easily integrate a documentation page into your flutter apps. This can help users of your app learn more about how it works.

App Bar


Calendar


Money Counter
pageGoto(context, CRMoneyCounter());
Debug Page


Settings


Region

Profile Banner


Enviroment
Enviroment or CREnv
is a way to grab enviroment variables out of a file.
This is a direct mirror of the Env
package.
Initialize
Before calling runApp
ensure you initilize the enviroment
This takes a parameter for the name of the enviroment file, but can be ommitted for the default file name of .env
void main() async {
await CREnv.init();
runApp(...);
}
Creating the file
By default the naem of the file is .env
but can be changed as a parameter inside the init function.
The file consists of key value pairs such as:
DATABASE_PASSWORD = "..."
FIREBASE_SENDER_ID = "..."
Getting data
CREnv["env_name"];
Save System
The Save system or CRSave
is a way to save data to and from the device. It's a wrapper around SharedPreferences
Initialize
Before calling runApp
ensure you initilize the save system:
void main() async {
await CRSave.init();
runApp(...);
}
Save / Load
To save and load data to the device simply call save/load providing the variable key.
The load method takes a default value in case the value has yet to be saved into memory.
CRSave.save<int>("myValue", 123);
int v = CRSave.load<int>("myValue", 0);
Clearing data
You can either clear a single key, or clear all data:
await CRSave.clear("myValue");
await CRSave.clearAll();
Binding
Binding to the save system allows you to easily track variables that have changed.
CRSave.bind(
key: "myValue",
defaultValue: 0,
builder: (val) => Text(val.toString())
);
User
The user system controls who is logged in currently to an account and is completly depends on the Database system.
Initalize
Before using the user system, you need to initalize supabase
inside the main method
void main() async {
await CRDatabase.init(
url: CREnv["SUPABASE_URL"]!,
anonKey: CREnv["SUPABASE_ANON_KEY"]!,
);
runApp(...);
}
Supabase Setup
inside supabase itself, you will need to Enable Phone Provider
inside the authentication.
To setup the users in supabase you'll need to run the following sql:
-- Roles
CREATE TABLE roles (
name text NOT NULL,
level smallint DEFAULT 0,
PRIMARY KEY (name)
);
INSERT INTO roles (name, level) VALUES ('Owner', 10);
CREATE TABLE permissions (
name text NOT NULL,
description text DEFAULT '',
PRIMARY KEY (name)
);
INSERT INTO permissions (name, description) VALUES ('master', 'Full permissions');
CREATE TABLE has_permission (
permission text NOT NULL,
role text NOT NULL,
PRIMARY KEY (permission, role),
FOREIGN KEY (permission) REFERENCES permissions(name)
ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (role) REFERENCES roles(name)
ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO has_permission (permission, role) VALUES ('master', 'Owner');
-- create user table
CREATE TABLE profiles (
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE ON UPDATE CASCADE,
first_name text default '',
last_name text default '',
role text,
PRIMARY KEY (user_id),
FOREIGN KEY (role) REFERENCES roles(name)
ON DELETE CASCADE ON UPDATE CASCADE
);
-- 1. Create the function
create or replace function public.handle_new_user()
returns trigger
language plpgsql
security definer
as $$
begin
insert into public.profiles (user_id, phonenumber)
values (new.id, new.phonenumber);
return new;
end;
$$;
-- 2. Create the trigger
create trigger on_auth_user_created
after insert on auth.users
for each row
execute procedure public.handle_new_user();
-- RLS : Users
alter table "profiles" enable row level security;
create policy "Select - Any"
on "public"."profiles"
as PERMISSIVE for SELECT to authenticated
using (
true
);
create policy "Update - user_id"
on "public"."profiles"
as PERMISSIVE for UPDATE to public
using (
((SELECT auth.uid() AS uid) = user_id)
);
-- RLS : Roles
alter table "roles" enable row level security;
create policy "Select - Any"
on "public"."roles"
as PERMISSIVE for SELECT to authenticated
using (true);
-- RLS : Permissions
alter table "permissions" enable row level security;
create policy "Select - Any"
on "public"."permissions"
as PERMISSIVE for SELECT to authenticated
using (true);
create policy "All - Owner"
on "public"."permissions"
as PERMISSIVE for ALL to public
using (
(( SELECT auth.uid() AS uid) IN (
SELECT profiles.user_id FROM profiles WHERE (profiles.role = 'Owner'::text)))
);
-- RLS : Has Permission
alter table "has_permission" enable row level security;
create policy "Select - Any"
on "public"."has_permission"
as PERMISSIVE for SELECT to authenticated
using (true);
create policy "All - Owner"
on "public"."has_permission"
as PERMISSIVE for ALL to public
using (
(( SELECT auth.uid() AS uid) IN (
SELECT profiles.user_id FROM profiles WHERE (profiles.role = 'Owner'::text)))
);
You can provide more columns as well and they will be retrivable directly from the user object
Forgot password
await attemptPasswordReset(context);
Logging in
To log into an account provide either the phonenumber or email (but not both):
CRUser? user =
await CRUser.attemptLogin(phone: phone, password: pass);
if(user == null) {...} // invalid credentials
Once logged in you can retrieve any data from the provided user:
user["myColumn"];
user.getValue("myColumn");
When saving, you must provide a map of all values you want to change:
await user.setValue({
"myColumn": 25
});
By default it will automatically save directly to supabase, but you can set the save
parameter to false to only save locally.
Current User
When logging in you don't need to save the provided user, simply get it statically CRUser.current
Loading User
You can load a user and their details without actually logging in:
CRUser? user = await CRUser.load(uuid);
Reloading User
Sometimes you may need to refetch the data from a user and update the values:
await user.reload();
await CRUser.current!.reload();
Binding
You can bind widgets to the user in order to track changes made.
To do this simply call bind
from the user in question:
CRUser.current!.bind(
builder: (user) => Text(user.firstName)
);
Replication
When binding a widget (see above) you can choose to use replication, which will track changes to the database itself rather than tracking the local copy.
In supabase, edit the users
database and enable replication.
Then set replication to true
CRUser.current!.bind(
replication: true,
builder: (user) => Text(user.firstName)
);
NOTE: This will turn off the default binding, so any bindings will only update if the database itself is modified.
Database
the database system lets you easily use supabase with little work.
Initialize
Before using the database, call the init function
void main() async {
await CRDatabase.init(
url: CREnv["SUPABASE_URL"]!,
anonKey: CREnv["SUPABASE_ANON_KEY"]!,
);
runApp(...);
}
The CREnv is a nicer way to store the url and anon key from supabase
Timing
When calling most functions from the database, a fixed delay is added to test out lag within the calls.
This is only active when Debug is active.
You can access the delay manually as well by calling it
await CRDatabase.delay;
Supabase Raw
You can access any supabase function by simply grabbing the auth or client
CRDatabase.client...
CRDatabase.auth...
CRDatabase.getClient()...
CRDatabase.getAuth()...
using the get functions will also add the delay into the calls where getting the raw values will ommit the delay
Getting Data
The easist way to get data is with one of 2 ways:
CRDatabase.select(table, [columns])
CRDatabase.selectEq(table, column, value, [columns])
Where[columns]
is"*"
by default (select all)
Setting Data
For setting data you can insert, update, or delete:
CRDatabase.insert(table, values)
CRDatabase.update(table, values)
CRDatabase.delete(table, column, value)
For insert and update take in a map of string value pairs.
Delete takes a column name and a value to compare to and will delete any entry where the value = column
Debug
Debug is simply a way to log messages and have them show in app.
Enabling
You can check if debug mode is on by using Debug.active
You can disable debug mode with either setActive
or setReleaseMode
Debug.active; // true by default
Debug.releaseMode; // true by default
Debug.setActive(false); // Turns debug off
Debug.setReleaseMode(false); // will also make active false
Debug.toggleDebug();
Release Mode
Sometimes you may want to be able to disable or enable debug mode on the fly while testing.
Release mode is meant to be a way to fully disable debug mode, so if release mode is active, you can never turn debug mode on.
Database Delay
Inside Database any calls you make have an automatic delay of 3 seconds if debug mode is active.
This delay can be turned off or changed
Debug.setSqlDelay(false); // turns delay off
Debug.setSqlDelayTime(5000); // sets delay to 5 seconds
Debug Messages
You can also write debug messages that will show up in the Debug Page rather than the console.
Debug.log(message, [inner]);
Debug.warn(message, [inner]);
Debug.error(message, [inner]);
Debug.clearLog();
Theme
Themes is an easy way to change the color of the app.
Initialize
While there is no setup needed for themes directly, you will need to initialize the Save System first
void main() async {
await CRSave.init();
runApp(...);
}
Set Theme
To set a them simply call setTheme or setDark
CRTheme.setTheme(CRThemeType.sakura);
CRTheme.setDark(true);
This will automatically save the theme as well for the next time you open the app
Theme Settings
Within the Settings api there is also an option to create a atuo theme selector and dark mode selector:
CRSettings.colorTheme(context);
CRSettings.darkMode(context);
Images
Setup IOS
Add the following keys to your Info.plist file, located in <project root>/ios/Runner/Info.plist:
<key>NSPhotoLibraryUsageDescription</key>
<string>Access to photos for upload</string>
<key>NSCameraUsageDescription</key>
<string>Access to camera for upload</string>
<key>NSMicrophoneUsageDescription</key>
<string>Access to microphone</string>
Setup Android
Add the following to AndroidManifest.xml, located in
NOTE: add below the main /
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
Display Image
Showing an image is as simple as defining the image object and attributes:
CRImage(...);
This doubles as a widget so it can also be displayed. However without any loading function nothing will show:
Loading Image
Before an image is used you should call one of the loading functions on it:
CRImage().loadDatabase(bucket, name);
CRImage().loadUrl(url);
await CRImage().loadGallery();
await CRImage().loadCamera();
Loading from the gallery or camera must be done asynchronsouly due to them needing to retrive a photo from the device.
Saving to Database
It can be useful to save an image to your database, especially one from the gallery or camera:
CRImage().loadGallery().then(
(img) => img.saveDatabase(bucket, name)
);
NOTE: This action will fail if there is no proper policies in place. To fix, see Supabase Bucket Policy
Profile Pictures
Setup
To start using profile pictures with users ensure you Initalize the Database
You will also want to setup a bucket named profile_pictures
and give the following policy:
bucket_id = 'images' AND name = (select auth.uid()::text)
Display Profile Picture
The simplist way to show a profile picture is with the widget:
child: CRProfilePicture(user: CRUser.current!);
Of course you can only get the current user if you are Logged In otherwise you'll have to proved the user some other way


Edit Profile Picture
A clean way to edit the profile picture is with the provided action (shown below):
CRProfilePicture.editProfilePicutre(context);
otherwise you can upload the image manually:
CRImage image = ...
if(image.xfile != null) {
image.saveDatabase("profile_pictures", CRUser.current!.uuid);
CRUser.current!.reloadPfp();
}
Another way is use a raw image rather than this api's image:
await CRDatabase.storage.from("profile_pictures").upload(
CRUser.current!.uuid,
File(...),
fileOptions: const FileOptions(cacheControl: '3600', upsert: true),
);

Messaging
-- create tables
CREATE TABLE message_groups (
group_id uuid DEFAULT gen_random_uuid(),
last_message_time timestamptz,
last_message uuid,
length int DEFAULT 0,
PRIMARY KEY (group_id)
);
CREATE TABLE messages (
group_id uuid NOT NULL,
index int NOT NULL DEFAULT 0,
user_id uuid NOT NULL REFERENCES users(user_id)
on delete cascade,
message text,
time timestamptz,
PRIMARY KEY (group_id, index),
FOREIGN KEY (group_id) REFERENCES message_groups(group_id)
on delete cascade
);
CREATE TABLE message_user_groups (
user_id uuid NOT NULL REFERENCES users(user_id)
on delete cascade,
group_id uuid NOT NULL REFERENCES message_groups(group_id)
on delete cascade,
last_viewed_index int DEFAULT 0,
group_name varchar(31),
PRIMARY KEY (user_id, group_id)
);
CREATE TABLE contacts (
user_id uuid NOT NULL REFERENCES users(user_id)
on delete cascade,
contact_id uuid NOT NULL REFERENCES users(user_id)
on delete cascade,
PRIMARY KEY (user_id, contact_id)
);
-- Message index incrementer
create or replace function set_message_id() returns trigger as $$
begin
new.index = (select count(*) from messages where group_id = new.group_id) + 1;
return new;
end;
$$ language plpgsql;
create trigger on_message_set_id
before update on messages
for each row execute procedure set_message_id();
-- Enable RLS
alter table "messages" enable row level security;
alter table "message_groups" enable row level security;
alter table "message_user_groups" enable row level security;
alter table "contacts" enable row level security;
-- Create Policies
create policy "Enable if user in group"
on "public"."message_groups"
as PERMISSIVE for ALL to public
using (
(select auth.uid()) in (select user_id from message_user_groups where message_user_groups.group_id = group_id)
);
create policy "Authenticated can insert"
on "public"."message_groups"
as PERMISSIVE for INSERT to authenticated
with check (
true
);
create policy "Anyone can selet"
on "public"."message_groups"
as PERMISSIVE for SELECT to authenticated
using (
true
);
create policy "Enable if user in group"
on "public"."messages"
as PERMISSIVE for ALL to public
using (
(select auth.uid()) in (select user_id from message_user_groups where message_user_groups.group_id = group_id)
);
create policy "Anyone can insert"
on "public"."message_user_groups"
as PERMISSIVE for ALL to public
using (
true
);
create policy "Modify self"
on "public"."contacts"
as PERMISSIVE for ALL to public
using (
(select auth.uid()) = user_id
);
-- delete tables
DROP TABLE message_groups;
DROP TABLE messages;
DROP TABLE message_user_groups;
Contacts
Contacts are a list of users you have connected with.
To get a list of contacts call the getContacts
function:
List<CRUser> users = CRContacts.getContacts();
Contact Page
you can view a generic contact page by navigating to CRContactsPage()
this will also return a selected contact:
var user = await Navigator.push(
context,
pageCreateRoute(CRContactsPage(),
xbegin: 0.0, ybegin: 1.0)) as CRUser?;
Changing Contact Types
Contacts by default are just all users in the application, to change this, in the main method call the following:
main() {
CRContacts.contactType = CRContactType.added;
runApp(...);
}
This will ensure the contacts must be added via the users phone number before messaging them
Roles
To setup the roles in supabase you'll need to run the following sql:
-- create user table
CREATE TABLE roles (
role_id uuid DEFAULT gen_random_uuid(),
name text,
locked boolean DEFAULT false,
modify_roles boolean DEFAULT false,
debug_mode boolean DEFAULT false,
promote boolean DEFAULT false,
PRIMARY KEY (role_id)
);
CREATE TABLE role_user (
user_id uuid NOT NULL REFERENCES users(user_id)
on delete cascade,
role_id uuid NOT NULL REFERENCES users(user_id)
on delete cascade,
PRIMARY KEY (user_id)
);
-- Create inital
INSERT INTO roles (name, locked, modify_roles, debug_mode, promote)
VALUES ("Owner", true, true, false, true);
INSERT INTO roles (name, locked, modify_roles, debug_mode, promote)
VALUES ("Developer", true, true, true, true);
-- RLS
alter table "roles" enable row level security;
alter table "role_user" enable row level security;
create policy "Anyone can selet"
on "public"."roles"
as PERMISSIVE for SELECT to authenticated
using (
true
);
create policy "Anyone can selet"
on "public"."role_user"
as PERMISSIVE for SELECT to authenticated
using (
true
);
create policy "Modify can do all"
on "public"."role_user"
as PERMISSIVE for ALL to authenticated
using (
(select modify_roles from roles where role_id =
(select role_id from role_user where user_id = (select auth.uid()))) = true
);
create policy "Modify can do all"
on "public"."roles"
as PERMISSIVE for ALL to authenticated
using (
(select modify_roles from roles where role_id =
(select role_id from role_user where user_id = (select auth.uid()))) = true
);
Supabase Bucket Policy
Buckets are a storage method for supabase and the polcies control who can edit them. To create a new policy goto: Storage->Policies->New Policy->Full Custimization
All Authenticated Users
bucket_id = 'images' AND auth.role() = 'authenticated'
One Image Per User
bucket_id = 'images' AND name = (select auth.uid()::text)
Access to Folder
bucket_id = 'images' AND (storage.foldername(name))[1] = 'private'
XML Setup
Setup IOS
Add the following keys to your Info.plist file, located in <project root>/ios/Runner/Info.plist:
<key>NSPhotoLibraryUsageDescription</key>
<string>Access to photos for upload</string>
<key>NSCameraUsageDescription</key>
<string>Access to camera for upload</string>
<key>NSMicrophoneUsageDescription</key>
<string>Access to microphone</string>
Setup Android
Add the following to AndroidManifest.xml, located in
NOTE: add below the main /
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>