Skip to content

Commit

Permalink
introduce bloc for profile contact state management
Browse files Browse the repository at this point in the history
  • Loading branch information
LGro committed Feb 3, 2024
1 parent 0a8d09c commit 6e94d58
Show file tree
Hide file tree
Showing 11 changed files with 413 additions and 225 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ Also, add your secret API token to a file `android/app/src/main/res/values/devel
<string name="mapbox_access_token">{SECRET-API-TOKEN}</string>
```

### Building

To (re-)generate all code from templates, run
```
flutter packages pub run build_runner build
```

## User Stories

### Open app first time
Expand Down
1 change: 1 addition & 0 deletions devtools_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
extensions:
106 changes: 64 additions & 42 deletions lib/app.dart
Original file line number Diff line number Diff line change
@@ -1,52 +1,74 @@
import 'package:animated_theme_switcher/animated_theme_switcher.dart';
import 'package:flutter/foundation.dart';
// Copyright 2024 Lukas Grossberger
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:path_provider/path_provider.dart';

import 'router/router.dart';
import 'tick.dart';
import 'contact_list.dart';
import 'contact_page.dart';
import 'map.dart';
import 'profile.dart';
import 'updates.dart';

class VeilidChatApp extends ConsumerWidget {
const VeilidChatApp({
required this.theme,
super.key,
});

final ThemeData theme;
class CoagulateApp extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp(
title: 'Coagulate',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: CoagulateAppView(),
// https://docs.flutter.dev/cookbook/navigation/navigate-with-arguments
routes: {
ContactPage.routeName: (context) => const ContactPage(),
},
);
}

class CoagulateAppView extends StatefulWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(routerProvider);
final localizationDelegate = LocalizedApp.of(context).delegate;
_CoagulateAppViewState createState() => _CoagulateAppViewState();
}

class _CoagulateAppViewState extends State<CoagulateAppView> {
int _selectedIndex = 0;

return ThemeProvider(
initTheme: theme,
builder: (_, theme) => LocalizationProvider(
state: LocalizationProvider.of(context).state,
child: BackgroundTicker(
builder: (context) => MaterialApp.router(
debugShowCheckedModeBanner: false,
routerConfig: router,
title: translate('app.title'),
theme: theme,
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
FormBuilderLocalizations.delegate,
localizationDelegate
],
supportedLocales: localizationDelegate.supportedLocales,
locale: localizationDelegate.currentLocale,
)),
));
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<ThemeData>('theme', theme));
}
Widget build(BuildContext context) => Scaffold(
body: [
ProfilePage(),
UpdatesPage(),
ContactListPage(),
MapPage(),
].elementAt(_selectedIndex),
bottomNavigationBar: BottomNavigationBar(
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: 'Profile',
),
BottomNavigationBarItem(
icon: Icon(Icons.update),
label: 'Updates',
),
BottomNavigationBarItem(
icon: Icon(Icons.contacts),
label: 'Contacts',
),
BottomNavigationBarItem(
icon: Icon(Icons.map),
label: 'Map',
),
],
currentIndex: _selectedIndex,
unselectedItemColor: Colors.black,
selectedItemColor: Colors.deepPurpleAccent,
onTap: _onItemTapped,
),
);
}
47 changes: 47 additions & 0 deletions lib/cubit/profile_contact_cubit.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import 'package:equatable/equatable.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:flutter_contacts/flutter_contacts.dart';

part 'profile_contact_cubit.g.dart';
part 'profile_contact_state.dart';

// TODO: Add contact refresh as listener via
// FlutterContacts.addListener(() => print('Contact DB changed'));

class ProfileContactCubit extends HydratedCubit<ProfileContactState> {
ProfileContactCubit() : super(ProfileContactState());

// TODO: Separate the emits out?
Future<void> updateContact() async {
print('emitting loading');
emit(state.copyWith(status: ProfileContactStatus.loading));

if (state.profileContact == null) {
print('emitting unavailable');
emit(state.copyWith(status: ProfileContactStatus.unavailable));
return;
}

print('emitting success');
emit(state.copyWith(
status: ProfileContactStatus.success,
profileContact:
await FlutterContacts.getContact(state.profileContact!.id)));
}

void setContact(Contact? contact) {
emit(state.copyWith(
status: (contact == null)
? ProfileContactStatus.unavailable
: ProfileContactStatus.success,
profileContact: contact));
}

@override
ProfileContactState fromJson(Map<String, dynamic> json) =>
ProfileContactState.fromJson(json);

@override
Map<String, dynamic> toJson(ProfileContactState state) => state.toJson();
}
31 changes: 31 additions & 0 deletions lib/cubit/profile_contact_cubit.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions lib/cubit/profile_contact_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
part of 'profile_contact_cubit.dart';

enum ProfileContactStatus { initial, loading, success, unavailable }

extension ProfileContactStatusX on ProfileContactStatus {
bool get isInitial => this == ProfileContactStatus.initial;
bool get isLoading => this == ProfileContactStatus.loading;
bool get isSuccess => this == ProfileContactStatus.success;
bool get isUnavailable => this == ProfileContactStatus.unavailable;
}

@JsonSerializable()
final class ProfileContactState extends Equatable {
ProfileContactState({
this.status = ProfileContactStatus.initial,
this.profileContact,
});

factory ProfileContactState.fromJson(Map<String, dynamic> json) =>
_$ProfileContactStateFromJson(json);

final ProfileContactStatus status;
final Contact? profileContact;

ProfileContactState copyWith(
{ProfileContactStatus? status, Contact? profileContact}) =>
ProfileContactState(
status: status ?? this.status,
profileContact: profileContact ?? this.profileContact,
);

Map<String, dynamic> toJson() => _$ProfileContactStateToJson(this);

@override
List<Object?> get props => [status, profileContact];
}
96 changes: 12 additions & 84 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,87 +1,15 @@
// Copyright 2024 Lukas Grossberger

import 'package:change_case/change_case.dart';
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:shared_preferences/shared_preferences.dart';

import 'contact_list.dart';
import 'contact_page.dart';
import 'map.dart';
import 'updates.dart';
import 'profile.dart';

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp(
title: 'Navigation App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
// https://docs.flutter.dev/cookbook/navigation/navigate-with-arguments
routes: {
ContactPage.routeName: (context) => ContactPage(),
},
);
}

class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
int _selectedIndex = 0;
static const _bottomNavigationItems = [
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: 'Profile',
),
BottomNavigationBarItem(
icon: Icon(Icons.update),
label: 'Updates',
),
BottomNavigationBarItem(
icon: Icon(Icons.contacts),
label: 'Contacts',
),
BottomNavigationBarItem(
icon: Icon(Icons.map),
label: 'Map',
),
];

static final List<Widget> _widgetOptions = <Widget>[
ProfilePage(),
UpdatesPage(),
ContactListPage(),
MapPage(),
];

void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}

@override
Widget build(BuildContext context) => Scaffold(
// appBar: AppBar(
// title: Text(_bottomNavigationItems.elementAt(_selectedIndex).label!),
// ),
body: _widgetOptions.elementAt(_selectedIndex),
// bottomSheet: TabBar(tabs: [Text("Hi")]),
bottomNavigationBar: BottomNavigationBar(
items: _bottomNavigationItems,
currentIndex: _selectedIndex,
unselectedItemColor: Colors.black,
selectedItemColor: Colors.deepPurpleAccent,
onTap: _onItemTapped,
),
);
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:path_provider/path_provider.dart';

import 'app.dart';
import 'profile_contact_bloc_observer.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();
Bloc.observer = const ProfilecontactBlocObserver();
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: await getApplicationDocumentsDirectory());
runApp(CoagulateApp());
}
Loading

0 comments on commit 6e94d58

Please sign in to comment.