Fakultas Ilmu Komputer UI

Commit e41f2667 authored by Fakhira Devina's avatar Fakhira Devina
Browse files

Merge branch 'dev-hira' into 'PBI-2-pencarian_lokasi'

API Handling with BLoC and implement Search History

See merge request !27
parents c4f19ef5 a5300189
Pipeline #37299 passed with stages
in 4 minutes and 49 seconds
<<<<<<< HEAD
{"_info":"// This is a generated file; do not edit or check into version control.","dependencyGraph":[{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"google_maps_flutter","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"location","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_macos"]},{"name":"path_provider_macos","dependencies":[]}]}
=======
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"google_maps_flutter","path":"D:\\\\Flutter\\\\flutter_windows_v1.9.1+hotfix.2-stable\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\google_maps_flutter-0.5.24+1\\\\","dependencies":[]},{"name":"location","path":"D:\\\\Flutter\\\\flutter_windows_v1.9.1+hotfix.2-stable\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\location-2.5.3\\\\","dependencies":[]}],"android":[{"name":"flutter_plugin_android_lifecycle","path":"D:\\\\Flutter\\\\flutter_windows_v1.9.1+hotfix.2-stable\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\flutter_plugin_android_lifecycle-1.0.6\\\\","dependencies":[]},{"name":"google_maps_flutter","path":"D:\\\\Flutter\\\\flutter_windows_v1.9.1+hotfix.2-stable\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\google_maps_flutter-0.5.24+1\\\\","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"location","path":"D:\\\\Flutter\\\\flutter_windows_v1.9.1+hotfix.2-stable\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\location-2.5.3\\\\","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"google_maps_flutter","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"location","dependencies":[]}],"date_created":"2020-03-25 16:26:56.154526","version":"1.15.17"}
>>>>>>> staging
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"google_maps_flutter","path":"D:\\\\Flutter\\\\flutter_windows_v1.9.1+hotfix.2-stable\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\google_maps_flutter-0.5.24+1\\\\","dependencies":[]},{"name":"location","path":"D:\\\\Flutter\\\\flutter_windows_v1.9.1+hotfix.2-stable\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\location-2.5.3\\\\","dependencies":[]},{"name":"path_provider","path":"D:\\\\Flutter\\\\flutter_windows_v1.9.1+hotfix.2-stable\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\path_provider-1.6.5\\\\","dependencies":[]}],"android":[{"name":"flutter_plugin_android_lifecycle","path":"D:\\\\Flutter\\\\flutter_windows_v1.9.1+hotfix.2-stable\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\flutter_plugin_android_lifecycle-1.0.6\\\\","dependencies":[]},{"name":"google_maps_flutter","path":"D:\\\\Flutter\\\\flutter_windows_v1.9.1+hotfix.2-stable\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\google_maps_flutter-0.5.24+1\\\\","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"location","path":"D:\\\\Flutter\\\\flutter_windows_v1.9.1+hotfix.2-stable\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\location-2.5.3\\\\","dependencies":[]},{"name":"path_provider","path":"D:\\\\Flutter\\\\flutter_windows_v1.9.1+hotfix.2-stable\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\path_provider-1.6.5\\\\","dependencies":[]}],"macos":[{"name":"path_provider_macos","path":"D:\\\\Flutter\\\\flutter_windows_v1.9.1+hotfix.2-stable\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\path_provider_macos-0.0.4\\\\","dependencies":[]}],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"google_maps_flutter","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"location","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_macos"]},{"name":"path_provider_macos","dependencies":[]}],"date_created":"2020-03-25 17:23:41.601639","version":"1.15.17"}
\ No newline at end of file
......@@ -289,3 +289,5 @@ modules.xml
# End of https://www.gitignore.io/api/linux,django,python,pycharm+all
tests.output
.flutter-plugins-dependencies
......@@ -22,8 +22,8 @@ Lint:
Test:
stage: test
script:
- flutter test --machine > tests.output
- flutter test --coverage
- flutter test --machine > tests.output
- lcov --summary coverage/lcov.info
- genhtml coverage/lcov.info --output=coverage
artifacts:
......
......@@ -50,4 +50,17 @@ MAPS_API_KEY=Bu***************
Run the app using the development flavor
```bash
flutter run -t lib/main_dev.dart
```
\ No newline at end of file
```
## Building Models with JsonSerializable
Jadi abis get dari API, jsonnya di map ke models biar rapih.
1. Tulis ada field apa aja dari jsonnya (bisa liat contoh yang di models/lokasi.dart)
2. bagian 'part of {nama models}.g.dart' itu harus ditulis di model yg mau dibuat. di awal emang merah, tapi biarin aja
3. kalo semua field udah di tulis, run
```bash
flutter pub run build_runner build
```
4. nanti akan ke build file {nama models}.g.dart, yang di nomor 2 merah harusnya udah gak merah lagi
## Passing Data with BLoC
Udah ada contohnya di /bloc (implementasi di screen nya ada di page/pencarian/pencarian.dart)
Bisa baca [disini]https://itnext.io/flutter-handling-your-network-api-calls-like-a-boss-936eef296547 sebagai panduannya
\ No newline at end of file
import 'dart:async';
import 'package:ppl_disabilitas/model/lokasi.dart';
import 'package:ppl_disabilitas/network/data/network_model.dart';
import 'package:ppl_disabilitas/repository/LokasiRepository.dart';
class LokasiResponseBloc {
StreamController _recentSearchController;
LokasiRepository _lokasiRepository;
StreamController _lokasiListController;
StreamSink<NetworkModel<LokasiListResponse>> get recentSearchSink =>
_recentSearchController.sink;
Stream<NetworkModel<LokasiListResponse>> get recentSearchStream =>
_recentSearchController.stream;
StreamSink<NetworkModel<LokasiListResponse>> get lokasiListSink =>
_lokasiListController.sink;
Stream<NetworkModel<LokasiListResponse>> get lokasiListStream =>
_lokasiListController.stream;
LokasiResponseBloc() {
_lokasiListController =
StreamController<NetworkModel<LokasiListResponse>>();
_recentSearchController = StreamController<NetworkModel<LokasiListResponse>>();
_lokasiRepository = LokasiRepository();
fetchLokasiList();
fetchRecentSearch();
}
fetchLokasiList() async {
lokasiListSink.add(NetworkModel.loading('Getting Locations'));
try {
LokasiListResponse lokasiListResponse =
await _lokasiRepository.fetchLokasi();
print("lokasi list response: $lokasiListResponse");
lokasiListSink.add(NetworkModel.completed(lokasiListResponse));
} catch (e) {
lokasiListSink.add(NetworkModel.error(e.toString()));
print("$e");
}
}
fetchRecentSearch() async {
recentSearchSink.add(NetworkModel.loading('Getting Recent Search'));
try {
LokasiListResponse recentSearchData = await _lokasiRepository.fetchRecentSearch();
print("recentSearchData ${recentSearchData.listLokasi}");
recentSearchSink.add(NetworkModel.completed(recentSearchData));
} catch (e) {
recentSearchSink.add(NetworkModel.error(e.toString()));
print("line 53 ${e.toString()}");
}
}
saveRecentSearch(Lokasi search) async {
await _lokasiRepository.saveRecentSearch(search);
}
dispose() {
_recentSearchController?.close();
_lokasiListController?.close();
}
}
import 'package:json_annotation/json_annotation.dart';
part 'lokasi.g.dart';
@JsonSerializable()
class LokasiListResponse {
List<Lokasi> listLokasi;
LokasiListResponse();
factory LokasiListResponse.fromJson(List json) => _$LokasiListResponseFromJson(json);
Map<String, dynamic> toJson() => _$LokasiListResponseToJson(this);
}
@JsonSerializable(nullable: true)
class Lokasi {
String nama;
double latitude;
double longitude;
String alamat;
String foto;
String telp;
Lokasi();
factory Lokasi.fromJson(Map<String, dynamic> json) => _$LokasiFromJson(json);
Map<String, dynamic> toJson() => _$LokasiToJson(this);
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'lokasi.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
LokasiListResponse _$LokasiListResponseFromJson(List json) {
return LokasiListResponse()
..listLokasi = json
?.map((e) =>
e == null ? null : Lokasi.fromJson(e as Map<String, dynamic>))
?.toList();
}
Map<String, dynamic> _$LokasiListResponseToJson(LokasiListResponse instance) =>
<String, dynamic>{
'listLokasi': instance.listLokasi,
};
Lokasi _$LokasiFromJson(Map<String, dynamic> json) {
return Lokasi()
..nama = json['nama'] as String
..latitude = (json['latitude'] as num)?.toDouble()
..longitude = (json['longitude'] as num)?.toDouble()
..alamat = json['alamat'] as String
..foto = json['foto'] as String
..telp = json['telp'] as String;
}
Map<String, dynamic> _$LokasiToJson(Lokasi instance) => <String, dynamic>{
'nama': instance.nama,
'latitude': instance.latitude,
'longitude': instance.longitude,
'alamat': instance.alamat,
'foto': instance.foto,
'telp': instance.telp,
};
class CustomException implements Exception {
final _message;
final _prefix;
CustomException([this._message, this._prefix]);
String toString() {
return "$_prefix$_message";
}
}
class FetchDataException extends CustomException {
FetchDataException([String message])
: super(message, "Error During Communication: ");
}
class BadRequestException extends CustomException {
BadRequestException([message]) : super(message, "Invalid Request: ");
}
class UnauthorisedException extends CustomException {
UnauthorisedException([message]) : super(message, "Unauthorised: ");
}
class InvalidInputException extends CustomException {
InvalidInputException([String message]) : super(message, "Invalid Input: ");
}
\ No newline at end of file
......@@ -10,7 +10,7 @@ class CookiesInterface {
await getApplicationDocumentsDirectory().then((Directory directory) {
dir = directory;
});
File cookieFile = File("${dir.path}/$fileName");
File cookieFile = File("${dir.path}/$fileName.json");
bool cookiesExist = cookieFile.existsSync();
return cookiesExist;
......@@ -29,7 +29,7 @@ class CookiesInterface {
await getApplicationDocumentsDirectory().then((Directory directory) {
dir = directory;
});
File cookieFile = new File("${dir.path}/usercookies.json");
File cookieFile = File("${dir.path}/usercookies.json");
cookieFile.createSync();
setCookie = responseHeaders["set-cookie"];
......@@ -47,39 +47,52 @@ class CookiesInterface {
return cookieFile;
} on Exception catch (e) {
print(e.toString());
throw e;
rethrow;
}
}
Future<File> createSearchHistoryCookie({
Map<String, dynamic> recentSearch}) async {
print("recent searrch $recentSearch");
Directory dir;
List currentSearchHistory;
try {
await getApplicationDocumentsDirectory().then((Directory directory) {
dir = directory;
});
File cookieFile = new File(dir.path + "/searchhistory.json");
File cookieFile = File(dir.path + "/searchhistory.json");
cookieFile.createSync();
List currentSearchHistory = await getCookieFile(fileName: "searchhistory.json");
if (currentSearchHistory.length == 0) {
currentSearchHistory = [];
}
currentSearchHistory.add(recentSearch);
cookieFile.writeAsString(json.encode(currentSearchHistory));
await checkCookieFileAvailability(fileName: "searchhistory").then((available) async {
if (available) {
await getCookieFile(fileName: "searchhistory").then((cookie) {
bool test = cookie == null;
print("cookie is null? $test");
if (cookie == null) {
currentSearchHistory = [];
} else {
currentSearchHistory = json.decode(cookie);
}
currentSearchHistory.add(recentSearch);
});
} else {
currentSearchHistory = [];
}
await cookieFile.writeAsString(json.encode(currentSearchHistory));
});
return cookieFile;
} on Exception catch (e) {
print(e.toString());
throw e;
rethrow;
}
}
Future<List<dynamic>> getCookieFile({String fileName}) async {
Future<dynamic> getCookieFile({String fileName}) async {
Directory dir;
await getApplicationDocumentsDirectory().then((Directory directory) {
dir = directory;
});
File file = File("${dir.path}/fileName");
File file = File("${dir.path}/$fileName.json");
dynamic res = file.readAsStringSync();
return json.decode(res);
return res;
}
}
class NetworkModel {
var response;
int statusCode;
String errorMessage;
class NetworkModel<T> {
Status status;
T data;
String message;
NetworkModel({
this.response,
this.statusCode,
this.errorMessage,
});
}
\ No newline at end of file
NetworkModel.loading(this.message) : status = Status.LOADING;
NetworkModel.completed(this.data) : status = Status.COMPLETED;
NetworkModel.error(this.message) : status = Status.ERROR;
@override
String toString() {
return "Status : $status \n Message : $message \n Data : $data";
}
}
enum Status { LOADING, COMPLETED, ERROR }
import 'dart:convert';
import 'package:ppl_disabilitas/network/data/network_model.dart';
import 'package:ppl_disabilitas/network/CustomException.dart';
import 'package:http/http.dart' as http;
import 'dart:io';
class NetworkInterface {
//String key = KEY;
// POST request
Future<NetworkModel> post({
String url, //url nya apa
Future<dynamic> post({
String url, //url nya apa
dynamic bodyParams, //data apa yang mau dikasih
bool isLogin, //dia login apa ngga
}) async {
Map<String, String> headersJson = await _buildRequestHeader(isLogin); //butuh header apa ngga
NetworkModel model;
var responseJson;
Map<String, String> headersJson =
await _buildRequestHeader(isLogin); //butuh header apa ngga
try {
model = await http.post(
final response = await http.post(
"$url",
body: json.encode(bodyParams),
headers: headersJson,
).then((response) async {
Map<String, dynamic> responseBody = json.decode(response.body);
print(responseBody.toString());
//if (!isLogin) {
//if (responseBody.containsKey("key")) {
//setKey(responseBody["key"]);
//}
//await CookiesInterface.createCookieFile(response.headers);
//}
print(responseBody);
return NetworkModel(statusCode: response.statusCode,
response: responseBody,
);
});
if (model.statusCode >= 400) {
throw Exception();
}
} on Exception catch (e) {
print("status code --> ${model.statusCode}");
NetworkModel errorModel = NetworkModel(
statusCode: model.statusCode,
errorMessage: e.toString(),
response: model.response);
model = errorModel;
);
responseJson = _response(response);
} on SocketException {
throw FetchDataException("No Internet Connection");
}
return model;
return responseJson;
}
// GET request
Future<NetworkModel> get({
Future<dynamic> get({
String url,
bool isLogin,
}) async {
var responseJson;
Map<String, dynamic> headersJson = await _buildRequestHeader(isLogin);
NetworkModel model;
try {
model = await http
.get(
final response = await http.get(
"$url",
headers: headersJson,
)
.then((response) {
print("masuk sini");
dynamic responseBody = json.decode(response.body);
print(responseBody);
return NetworkModel(
statusCode: response.statusCode,
response: responseBody,
);
});
if (model.statusCode >= 400) {
print("gamasuk sini");
throw Exception();
}
} on Exception catch (e) {
NetworkModel errorModel = NetworkModel(
statusCode: model.statusCode,
errorMessage: e.toString(),
response: model.response);
model = errorModel;
);
responseJson = _response(response);
} on SocketException {
throw FetchDataException("No Internet Connection");
}
return model;
return responseJson;
}
// buildRequestHeader: untuk nentuin pake header apa aja berdasarkan login apa ngga
Future<Map<String, dynamic>> _buildRequestHeader(bool isLogin) async {
Map<String, String> headers = Map<String, String>();
headers.putIfAbsent("Content-Type", () => "application/json");
//if (isLogin) {
//List<dynamic> cookieFile = await CookiesInterface.getCookieFile(); //ngambil data dari yg udh disimpen di cookie
//print("cookieFile list --> ${cookieFile.toString()}");
//print("check key here >>> $key");
//setKey(cookieFile[2]);
//key = cookieFile[2];
//headers.putIfAbsent("Authorization", () => 'Token $key'); //ini kalau authorization nya ngga ada baru taro token nya
//headers.putIfAbsent("X-CSRFToken", () => cookieFile[0]); //csrf token
//headers.putIfAbsent("Cookie",
//() => "csrftoken=${cookieFile[0]};sessionid=${cookieFile[1]}"); //cookie file
//print("headers --> ${headers}");
//List<dynamic> cookieFile = await CookiesInterface().getCookieFile(
// fileName:
// "userCookies"); //ngambil data dari yg udh disimpen di cookie
//print("cookieFile list --> ${cookieFile.toString()}");
//print("check key here >>> $key");
//setKey(cookieFile[2]);
//key = cookieFile[2];
//headers.putIfAbsent(
// "Authorization",
// () =>
// 'Token $key'); //ini kalau authorization nya ngga ada baru taro token nya
//headers.putIfAbsent("X-CSRFToken", () => cookieFile[0]); //csrf token
//headers.putIfAbsent(
// "Cookie",
// () =>
// "csrftoken=${cookieFile[0]};sessionid=${cookieFile[1]}"); //cookie file
//print("headers --> ${headers}");
//}
return headers;
}
}
\ No newline at end of file
dynamic _response(http.Response response) {
switch (response.statusCode) {
case 200:
var responseJson = json.decode(response.body.toString());
return responseJson;
case 400:
throw BadRequestException(response.body.toString());
case 401:
case 403:
throw UnauthorisedException(response.body.toString());
case 500:
default:
throw FetchDataException(
'Error occured while Communication with Server with status : ${response.statusCode}');
}
}
}
......@@ -46,6 +46,7 @@ class DashboardState extends State<Dashboard> {
}
void enableLocationService() async {
await location.changeSettings(accuracy: LocationAccuracy.HIGH);
_serviceEnabled = await location.serviceEnabled();
if (!_serviceEnabled) {
_serviceEnabled = await location.requestService();
......@@ -72,7 +73,7 @@ class DashboardState extends State<Dashboard> {
Widget build(BuildContext context) {
return Scaffold(
drawer: BisaGoDrawer(),
body: Stack(children: <Widget>[
body: Stack(key: Key("Stack"),children: <Widget>[
_buildGoogleMap(context),
InkWell(
key: Key("Navigate to Pencarian"),
......
import 'package:flutter/material.dart';
import 'package:ppl_disabilitas/bloc/LokasiResponseBloc.dart';
import 'package:ppl_disabilitas/config/styles.dart';
import 'package:ppl_disabilitas/model/lokasi.dart';
import 'package:ppl_disabilitas/network/data/network_model.dart';
import 'package:ppl_disabilitas/network/network_interface.dart';
class Pencarian extends StatefulWidget {
@override
......@@ -11,14 +12,12 @@ class Pencarian extends StatefulWidget {
class PencarianState extends State<Pencarian> {
Icon searchIcon = Icon(Icons.search);
Widget appBarText = Text("Pencarian Lokasi");
NetworkInterface networkInterface = NetworkInterface();
Future<NetworkModel> data;
LokasiResponseBloc _bloc = LokasiResponseBloc();
LokasiListResponse lokasiFromApi;
LokasiListResponse recentSearch;
@override
void initState() {
data = networkInterface.get(
url: 'https://my.api.mockaroo.com/mall.json?key=dbcde960');
super.initState();
print("aku");
}
@override
......@@ -64,191 +63,154 @@ class PencarianState extends State<Pencarian> {
),
),
),
body: FutureBuilder<NetworkModel>(
future: data,
builder: (context, snapshot) {
if (snapshot.hasData) {
print("snapshot data");
List places = snapshot.data.response;
return ListView.builder(
itemCount: places.length,
itemBuilder: (context, index) {
return Container(
decoration: BoxDecoration(
color: Colors.transparent,
border: Border(
bottom: BorderSide(color: Colors.grey[400]))),
margin:
EdgeInsets.only(left: doubleSpace, right: doubleSpace),
height: 90,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
StreamBuilder<NetworkModel<LokasiListResponse>>(
stream: _bloc.recentSearchStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
switch (snapshot.data.status) {
case Status.LOADING:
return Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(greenPrimary),
),
);
break;
case Status.COMPLETED:
recentSearch = snapshot.data.data;
Widget displayWidget;
if (recentSearch.listLokasi.isEmpty) {
displayWidget = Center(
child: Text("Anda belum pernah melakukan pencarian"));
} else {
displayWidget = makeLokasiWidget("history",recentSearch);
}
return Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
CircleAvatar(
backgroundColor: greenPrimary,
child: Text('Test'),
),
Container(
padding: EdgeInsets.all(doubleSpace),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
places[index]['nama'],