Skip to content

Instantly share code, notes, and snippets.

@romannjoroge
Created August 18, 2023 07:19
Show Gist options
  • Save romannjoroge/6d419ddadae01760540973028d86e373 to your computer and use it in GitHub Desktop.
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
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