-
-
Save romannjoroge/6d419ddadae01760540973028d86e373 to your computer and use it in GitHub Desktop.
Code that I have written for the CPIMS Mobile App development Hackathon flutter test
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:convert'; | |
import 'package:flutter/material.dart'; | |
import 'package:http/http.dart' as http; | |
import 'package:shared_preferences/shared_preferences.dart'; | |
void main() { | |
runApp(const MyApp()); | |
} | |
var myAppTheme = ThemeData( | |
colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal) | |
); | |
class MyApp extends StatelessWidget { | |
const MyApp({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
title: "CPIMS Mobile App", | |
theme: myAppTheme, | |
home: const LoginPage(), | |
debugShowCheckedModeBanner: false, | |
); | |
} | |
} | |
// This widget allows me to avoid repeating the same Scaffold definition | |
class MyScaffold extends StatelessWidget { | |
final String title; | |
final Widget body; | |
const MyScaffold({ | |
required this.body, | |
required this.title, | |
super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: Text(title), | |
), | |
body: body, | |
); | |
} | |
} | |
// Login page widget | |
class LoginPage extends StatelessWidget { | |
const LoginPage({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return const MyScaffold(body: LoginContent(), title: "Login"); | |
} | |
} | |
class LoginContent extends StatefulWidget { | |
const LoginContent({super.key}); | |
@override | |
State<LoginContent> createState() => _LoginContentState(); | |
} | |
// A formfield with validation | |
class MyTextFormField extends StatelessWidget { | |
final TextEditingController textEditingController; | |
final String labelText; | |
final String hintText; | |
final String errorMessage; | |
const MyTextFormField({ | |
required this.errorMessage, | |
required this.hintText, | |
required this.labelText, | |
required this.textEditingController, | |
super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return TextFormField( | |
controller: textEditingController, | |
decoration: InputDecoration( | |
label: Text(labelText), | |
hintText: hintText | |
), | |
validator: (val) { | |
if (val == null || val.isEmpty) { | |
return errorMessage; | |
} | |
return null; | |
}, | |
); | |
} | |
} | |
const baseUrl = "https://dev.cpims.net/api"; | |
class _LoginContentState extends State<LoginContent> { | |
// State management | |
bool loading = false; | |
bool error = false; | |
String errorMessage = ""; | |
var usernameController = TextEditingController(); | |
var passwordController = TextEditingController(); | |
var formKey = GlobalKey<FormState>(); | |
Future<void> login(TextEditingController usernameController, TextEditingController passwordController) async{ | |
try { | |
debugPrint("This has ran"); | |
final SharedPreferences prefs = await SharedPreferences.getInstance(); | |
// Get details stored in preferences | |
final String refresh = prefs.getString('refresh') ?? ""; | |
final String access = prefs.getString('access') ?? ""; | |
debugPrint("1"); | |
// Make request | |
var response = await postData('$baseUrl/token/', {"username": usernameController.text, "password": passwordController.text}, access); | |
debugPrint(response); | |
var responseBody = jsonDecode(response); | |
if (responseBody['detail'] != null) { | |
throw responseBody['detail']; | |
} | |
prefs.setString('refresh', responseBody['refresh']); | |
prefs.setString('access', responseBody['access']); | |
if (context.mounted == false) { | |
return; | |
} | |
ScaffoldMessenger.of(context).showSnackBar( | |
const SnackBar(content: Text("Logged In Successfully")) | |
); | |
Navigator.of(context).push( | |
MaterialPageRoute(builder: (context) => const DetailsPage()) | |
); | |
} catch(err) { | |
debugPrint("Caught"); | |
if (err is String) { | |
rethrow; | |
} else { | |
throw "Could Not Login"; | |
} | |
} | |
} | |
@override | |
Widget build(BuildContext context) { | |
// Loading State | |
if (loading == true) { | |
return const LoadingScreen(loadingText: "Logging In"); | |
} | |
// Succesful state | |
else if (loading == false && error == false) { | |
return LayoutBuilder( | |
builder: (context, constraints) { | |
return Stack( | |
children: [ | |
Container( | |
height: constraints.maxHeight, | |
width: constraints.maxWidth, | |
decoration: const BoxDecoration( | |
gradient: LinearGradient( | |
begin: Alignment.centerLeft, | |
end: Alignment.centerRight, | |
colors: [ | |
Color(0xff02aab0), | |
Color(0xff00cdac) | |
], | |
tileMode: TileMode.mirror | |
) | |
), | |
), | |
Center( | |
child: SizedBox( | |
height: MediaQuery.of(context).size.height * 0.60, | |
width: MediaQuery.of(context).size.width * 0.75, | |
child: Card( | |
elevation: 2.0, | |
child: Form( | |
key: formKey, | |
child: Padding( | |
padding: const EdgeInsets.all(20.0), | |
child: ListView( | |
children: [ | |
Align(alignment: Alignment.topCenter, child: Text("Log In", style: Theme.of(context).textTheme.titleLarge,)), | |
const SizedBox(height: 50.0,), | |
MyTextFormField(errorMessage: "Cannot Be Empty", hintText: "Enter Username", labelText: "Username", textEditingController: usernameController), | |
const SizedBox(height: 50.0,), | |
MyTextFormField(errorMessage: "Cannot Be Empty", hintText: "Enter Password", labelText: "Password", textEditingController: passwordController), | |
const SizedBox(height: 50.0,), | |
Align( | |
alignment: Alignment.topCenter, | |
child: ElevatedButton( | |
onPressed: () async{ | |
if (formKey.currentState!.validate()) { | |
try { | |
await login(usernameController, passwordController); | |
} catch(err) { | |
debugPrint("Caught 2"); | |
errorMessage = err.toString(); | |
showDialog( | |
context: context, | |
builder: (context) => SizedBox( | |
height: constraints.maxHeight / 2, | |
width: constraints.maxWidth / 2, | |
child: ErrorDialog(errorMessage: errorMessage, title: "Error Occured") // Error state | |
) | |
); | |
} | |
} | |
}, | |
child: const Text("Log In"), | |
), | |
) | |
], | |
), | |
), | |
), | |
), | |
), | |
) | |
], | |
); | |
}, | |
); | |
} | |
return const ErrorDialog(errorMessage: "Please Reload", title: "Internal Error"); | |
} | |
} | |
Future<String> getData(String url, String accessToken) async { | |
try { | |
var response = await http.get( | |
Uri.parse(url), | |
headers: <String, String> { | |
"Authorization": "Bearer $accessToken" | |
} | |
); | |
debugPrint("6"); | |
var responseBody = jsonDecode(response.body); | |
if (response.statusCode != 200) { | |
throw responseBody['details']; | |
} | |
return response.body; | |
} catch(err) { | |
if (err is String) { | |
rethrow; | |
} else { | |
throw "Could Not Get Data"; | |
} | |
} | |
} | |
Future<String> postData(String url, Map<String, dynamic> data, String accessToken) async{ | |
try { | |
debugPrint("2"); | |
var response = await http.post( | |
Uri.parse(url), | |
body: jsonEncode(data), | |
headers: <String, String> { | |
"Content-Type": "application/json", | |
"Authorization": "Bearer $accessToken" | |
} | |
); | |
debugPrint("2"); | |
var responseBody = jsonDecode(response.body); | |
if (response.statusCode != 200) { | |
debugPrint("bad"); | |
throw responseBody['detail']; | |
} | |
debugPrint("2"); | |
return response.body; | |
} catch(err) { | |
if (err is String) { | |
rethrow; | |
} else { | |
throw "Could Not Send Data"; | |
} | |
} | |
} | |
// The widget to show while loading | |
class LoadingScreen extends StatelessWidget { | |
final String loadingText; | |
const LoadingScreen({ | |
required this.loadingText, | |
super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return Center( | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
const CircularProgressIndicator(), | |
const SizedBox(height: 10.0,), | |
Text(loadingText) | |
], | |
), | |
); | |
} | |
} | |
// A widget I show when an error occurs. It will be a popup | |
class ErrorDialog extends StatelessWidget { | |
final String title; | |
final String errorMessage; | |
const ErrorDialog({ | |
required this.errorMessage, | |
required this.title, | |
super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return AlertDialog( | |
icon: const Icon(Icons.error_outline), | |
iconColor: Colors.red, | |
title: Text(title), | |
content: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
Text(errorMessage), | |
const SizedBox(height: 20.0,), | |
Align( | |
alignment: Alignment.topLeft, | |
child: ElevatedButton( | |
onPressed: () { | |
Navigator.of(context).pop(); | |
}, | |
child: const Text("GO BACK"), | |
), | |
) | |
], | |
), | |
); | |
} | |
} | |
// Class to process data from dashboard route | |
class DetailsPageResponse { | |
final int children; | |
final int caregivers; | |
final int government; | |
final int ngo; | |
final int caseRecords; | |
final int pendingCases; | |
final int orgUnits; | |
final int workforceMembers; | |
final int household; | |
final int children_all; | |
final OvcSummary ovc; | |
final List<dynamic> ovcRegs; | |
final List<dynamic> caseRegs; | |
final Map<String, dynamic> caseCats; | |
final Map<String, dynamic> criteria; | |
final String orgUnit; | |
final int orgUnitID; | |
const DetailsPageResponse({ | |
required this.children, | |
required this.caregivers, | |
required this.government, | |
required this.ngo, | |
required this.caseRecords, | |
required this.pendingCases, | |
required this.orgUnit, | |
required this.workforceMembers, | |
required this.household, | |
required this.children_all, | |
required this.ovc, | |
required this.caseRegs, | |
required this.caseCats, | |
required this.criteria, | |
required this.orgUnitID, | |
required this.orgUnits, | |
required this.ovcRegs | |
}); | |
factory DetailsPageResponse.fromJSON(Map<String, dynamic> json) { | |
return DetailsPageResponse( | |
children: json['children'], | |
caregivers: json['caregivers'], | |
government: json['government'], | |
ngo: json['ngo'], | |
caseRecords: json['case_records'], | |
pendingCases: json['pending_cases'], | |
orgUnit: json['org_unit'], | |
workforceMembers: json['workforce_members'], | |
household: json['household'], | |
children_all: json['children_all'], | |
ovc: OvcSummary.fromJSON(json['ovc_summary']), | |
caseRegs: json['case_regs'], | |
caseCats: json['case_cats'], | |
criteria: json['criteria'], | |
orgUnitID: json['org_unit_id'], | |
orgUnits: json['org_units'], | |
ovcRegs: json['ovc_regs'], | |
); | |
} | |
} | |
class OvcSummary { | |
final int m0; | |
final int m1; | |
final int m2; | |
final int m3; | |
final int m4; | |
final int f0; | |
final int f1; | |
final int f2; | |
final int f3; | |
final int f4; | |
const OvcSummary({ | |
required this.f0, | |
required this.f1, | |
required this.f2, | |
required this.f3, | |
required this.f4, | |
required this.m0, | |
required this.m1, | |
required this.m2, | |
required this.m3, | |
required this.m4 | |
}); | |
factory OvcSummary.fromJSON(Map<String, dynamic> json) { | |
return OvcSummary( | |
f0: json['f0'], | |
f1: json['f1'], | |
f2: json['f2'], | |
f3: json['f3'], | |
f4: json['f4'], | |
m0: json['m0'], | |
m1: json['m1'], | |
m2: json['m2'], | |
m3: json['m3'], | |
m4: json['m4'], | |
); | |
} | |
} | |
class DetailsPage extends StatelessWidget { | |
const DetailsPage({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return const MyScaffold(body: Padding(padding: EdgeInsets.all(20.0), child: DetailsPageContent(),), title: "Details"); | |
} | |
} | |
Map<String, dynamic> emptyMap = {}; | |
const verticalSpacing = SizedBox(height: 20.0,); | |
class DetailsPageContent extends StatefulWidget { | |
const DetailsPageContent({super.key}); | |
@override | |
State<DetailsPageContent> createState() => _DetailsPageContentState(); | |
} | |
class _DetailsPageContentState extends State<DetailsPageContent> { | |
bool loading = false; | |
bool error = false; | |
String errorMessage = ""; | |
late SharedPreferences prefs; | |
DetailsPageResponse? detailsPageResponse; | |
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey = | |
GlobalKey<RefreshIndicatorState>(); | |
@override | |
void initState() { | |
super.initState(); | |
initData(); | |
} | |
Future<void> initData() async { | |
setState(() { | |
loading = true; | |
}); | |
try { | |
debugPrint("5"); | |
prefs = await SharedPreferences.getInstance(); | |
String access = prefs.getString('access') ?? ""; | |
var response = await getData("$baseUrl/dashboard/", access); | |
debugPrint("5"); | |
var json = jsonDecode(response); | |
detailsPageResponse = DetailsPageResponse.fromJSON(json); | |
debugPrint("5"); | |
setState(() { | |
detailsPageResponse = detailsPageResponse; | |
loading = false; | |
}); | |
} catch(err) { | |
setState(() { | |
loading = false; | |
error = true; | |
errorMessage = err.toString(); | |
}); | |
} | |
} | |
@override | |
Widget build(BuildContext context) { | |
return RefreshIndicator( | |
onRefresh: initData, | |
key: _refreshIndicatorKey, | |
child: LayoutBuilder( | |
builder: (context, constraints) { | |
if (loading == true) { | |
return const Center(child: LoadingScreen(loadingText: "Details Are Loading")); | |
} | |
else if (error == true) { | |
return Center(child: ErrorDialog(errorMessage: errorMessage, title: "Error")); | |
} | |
else if (loading == false && error == false) { | |
return ListView( | |
children: [ | |
OrganizationExpansionTile( | |
orgunits: detailsPageResponse?.orgUnits ?? 0, | |
orgUnit: detailsPageResponse?.orgUnit ?? "", | |
orgunitID: detailsPageResponse?.orgUnitID ?? 0, | |
governmentID: detailsPageResponse?.government ?? 0, | |
ngoID: detailsPageResponse?.ngo ?? 0, | |
workforcemembers: detailsPageResponse?.workforceMembers ?? 0, | |
), | |
verticalSpacing, | |
CaseExpansionTile( | |
caseCategories: detailsPageResponse?.caseCats ?? {}, | |
caseRegs: detailsPageResponse?.caseRegs ?? [], | |
criteria: detailsPageResponse?.criteria ?? {}, | |
pending: detailsPageResponse?.pendingCases ?? 0, | |
records: detailsPageResponse?.caseRecords ?? 0, | |
), | |
verticalSpacing, | |
HouseholdExpansionTile( | |
household: detailsPageResponse?.household ?? 0, | |
caregivers: detailsPageResponse?.caregivers ?? 0, | |
children: detailsPageResponse?.children ?? 0, | |
childrenAll: detailsPageResponse?.children_all ?? 0, | |
), | |
verticalSpacing, | |
OvcExpansionTile( | |
ovcRegs: detailsPageResponse?.ovcRegs ?? [], | |
ovc: detailsPageResponse!.ovc, | |
), | |
], | |
); | |
} | |
return const Center(child: ErrorDialog(errorMessage: "Please Reload", title: "Internal Error")); | |
}, | |
), | |
); | |
} | |
} | |
class OvcExpansionTile extends StatelessWidget { | |
final OvcSummary ovc; | |
final List<dynamic> ovcRegs; | |
const OvcExpansionTile({ | |
required this.ovcRegs, | |
required this.ovc, | |
super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return ExpansionTile( | |
title: const Text("OVC Summary"), | |
children: [ | |
TextThenBold(boldText: ovc.f0.toString(), normalText: "F0:"), | |
verticalSpacing, | |
TextThenBold(boldText: ovc.f1.toString(), normalText: "F1:"), | |
verticalSpacing, | |
TextThenBold(boldText: ovc.f2.toString(), normalText: "F2:"), | |
verticalSpacing, | |
TextThenBold(boldText: ovc.f3.toString(), normalText: "F3:"), | |
verticalSpacing, | |
TextThenBold(boldText: ovc.f4.toString(), normalText: "F4:"), | |
verticalSpacing, | |
TextThenBold(boldText: ovc.m0.toString(), normalText: "M0:"), | |
verticalSpacing, | |
TextThenBold(boldText: ovc.m1.toString(), normalText: "M1:"), | |
verticalSpacing, | |
TextThenBold(boldText: ovc.m2.toString(), normalText: "M2:"), | |
verticalSpacing, | |
TextThenBold(boldText: ovc.m3.toString(), normalText: "M3:"), | |
verticalSpacing, | |
TextThenBold(boldText: ovc.m4.toString(), normalText: "M4:"), | |
verticalSpacing, | |
if (ovcRegs.isEmpty) | |
const TextThenBold(boldText: "Empty", normalText: "OVC Regs"), | |
], | |
); | |
} | |
} | |
class HouseholdExpansionTile extends StatelessWidget { | |
final int caregivers; | |
final int children; | |
final int childrenAll; | |
final int household; | |
const HouseholdExpansionTile({ | |
required this.household, | |
required this.caregivers, | |
required this.children, | |
required this.childrenAll, | |
super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return ExpansionTile( | |
title: const Text("House Hold"), | |
children: [ | |
TextThenBold(boldText: '$caregivers', normalText: "Caregivers:"), | |
verticalSpacing, | |
TextThenBold(boldText: '$children / $childrenAll', normalText: "Children:"), | |
verticalSpacing, | |
TextThenBold(boldText: '$household', normalText: "Household:"), | |
], | |
); | |
} | |
} | |
class OrganizationExpansionTile extends StatelessWidget { | |
final String orgUnit; | |
final int orgunitID; | |
final int orgunits; | |
final int governmentID; | |
final int ngoID; | |
final int workforcemembers; | |
const OrganizationExpansionTile({ | |
required this.orgUnit, | |
required this.workforcemembers, | |
required this.governmentID, | |
required this.orgunits, | |
required this.ngoID, | |
required this.orgunitID, | |
super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return ExpansionTile( | |
title: Text(orgUnit), | |
children: [ | |
TextThenBold(boldText: '$orgunitID', normalText: "Organization Unit ID:"), | |
verticalSpacing, | |
TextThenBold(boldText: '$orgunits', normalText: "Number of Units:"), | |
verticalSpacing, | |
TextThenBold(boldText: '$governmentID', normalText: "Government ID:"), | |
verticalSpacing, | |
TextThenBold(boldText: '$ngoID', normalText: "NGO ID:"), | |
verticalSpacing, | |
TextThenBold(boldText: '$workforcemembers', normalText: "Workforce Members:"), | |
], | |
); | |
} | |
} | |
class CaseExpansionTile extends StatelessWidget { | |
final int pending; | |
final int records; | |
final List<dynamic> caseRegs; | |
final Map<String, dynamic> criteria; | |
final Map<String, dynamic> caseCategories; | |
const CaseExpansionTile({ | |
required this.criteria, | |
required this.caseCategories, | |
required this.caseRegs, | |
required this.pending, | |
required this.records, | |
super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return ExpansionTile( | |
title: const Text("Cases"), | |
children: [ | |
TextThenBold(boldText: '$pending', normalText: "Pending Cases:"), | |
verticalSpacing, | |
TextThenBold(boldText: '$records', normalText: "Case Records:"), | |
verticalSpacing, | |
if (caseRegs.isEmpty) | |
const Column( | |
children: [ | |
TextThenBold(boldText: "Empty", normalText: "Case Regs:"), | |
verticalSpacing, | |
], | |
), | |
if (caseCategories.isEmpty) | |
const Column( | |
children: [ | |
TextThenBold(boldText: "Empty", normalText: "Case Categories:"), | |
verticalSpacing | |
], | |
), | |
if (criteria.isEmpty) | |
const Column( | |
children: [ | |
TextThenBold(boldText: "Empty", normalText: "Criteria:"), | |
verticalSpacing | |
], | |
) | |
], | |
); | |
} | |
} | |
class TextThenBold extends StatelessWidget { | |
final String normalText; | |
final String boldText; | |
const TextThenBold({ | |
required this.boldText, | |
required this.normalText, | |
super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return Row( | |
children: [ | |
Text(normalText), | |
SizedBox(width: 5.0,), | |
Text(boldText, style: Theme.of(context).textTheme.bodyMedium!.copyWith(fontWeight: FontWeight.bold),) | |
], | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment