From 05100f0f3267d25aaa77cba20cabe1855a6cd69b Mon Sep 17 00:00:00 2001
From: Fakhira Devina <fakhira.devina@ui.ac.id>
Date: Tue, 7 Apr 2020 00:04:11 +0700
Subject: [PATCH] [CHORE] Pencarian Page Cookie is functional

- test not done yet
---
 .flutter-plugins-dependencies        |   2 +-
 .gitignore                           |   2 +
 .gitlab-ci.yml                       |   2 +-
 README.md                            |  15 +-
 lib/bloc/LokasiResponseBloc.dart     |  60 +++++
 lib/config/strings.dart              |  10 +-
 lib/model/lokasi.dart                |  25 +++
 lib/model/lokasi.g.dart              |  40 ++++
 lib/network/CustomException.dart     |  27 +++
 lib/network/cookies_interface.dart   |  98 +++++++++
 lib/network/data/network_model.dart  |  16 ++
 lib/network/network_interface.dart   |  94 ++++++++
 lib/page/dashboard/dashboard.dart    |   3 +-
 lib/page/pencarian/pencarian.dart    | 314 +++++++++++++++++----------
 lib/repository/LokasiRepository.dart |  36 +++
 pubspec.yaml                         |   5 +-
 test/cookie_test.dart                |  63 ++++++
 test/mock_test.dart                  |  87 ++++++++
 test/navigation_test.dart            |  65 ------
 test/pencarian_test.dart             |  50 ++++-
 test/widget_test.dart                |   2 +-
 21 files changed, 825 insertions(+), 191 deletions(-)
 create mode 100644 lib/bloc/LokasiResponseBloc.dart
 create mode 100644 lib/model/lokasi.dart
 create mode 100644 lib/model/lokasi.g.dart
 create mode 100644 lib/network/CustomException.dart
 create mode 100644 lib/network/cookies_interface.dart
 create mode 100644 lib/network/data/network_model.dart
 create mode 100644 lib/network/network_interface.dart
 create mode 100644 lib/repository/LokasiRepository.dart
 create mode 100644 test/cookie_test.dart
 create mode 100644 test/mock_test.dart
 delete mode 100644 test/navigation_test.dart

diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies
index e49cab89..52b1c6a0 100644
--- a/.flutter-plugins-dependencies
+++ b/.flutter-plugins-dependencies
@@ -1 +1 @@
-{"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 22:29:07.715265","version":"1.15.17"}
\ No newline at end of file
+{"_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":[]}]}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 93baea49..a1b47e53 100644
--- a/.gitignore
+++ b/.gitignore
@@ -289,3 +289,5 @@ modules.xml
 # End of https://www.gitignore.io/api/linux,django,python,pycharm+all
 
 tests.output
+
+.flutter-plugins-dependencies
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c73806b7..da9a47c7 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -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:
diff --git a/README.md b/README.md
index 67bdfefd..6c0f425d 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/lib/bloc/LokasiResponseBloc.dart b/lib/bloc/LokasiResponseBloc.dart
new file mode 100644
index 00000000..ace530a1
--- /dev/null
+++ b/lib/bloc/LokasiResponseBloc.dart
@@ -0,0 +1,60 @@
+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();
+      lokasiListSink.add(NetworkModel.completed(lokasiListResponse));
+    } catch (e) {
+      lokasiListSink.add(NetworkModel.error(e.toString()));
+    }
+  }
+
+  fetchRecentSearch() async {
+    recentSearchSink.add(NetworkModel.loading('Getting Recent Search'));
+    try {
+      LokasiListResponse recentSearchData = await _lokasiRepository.fetchRecentSearch();
+      recentSearchSink.add(NetworkModel.completed(recentSearchData));
+    } catch (e) {
+      recentSearchSink.add(NetworkModel.error(e.toString()));
+    }
+  }
+
+  saveRecentSearch(Lokasi search) async {
+    await _lokasiRepository.saveRecentSearch(search);
+  }
+
+  dispose() {
+    _recentSearchController?.close();
+    _lokasiListController?.close();
+  }
+}
diff --git a/lib/config/strings.dart b/lib/config/strings.dart
index f680bdbc..0162847d 100644
--- a/lib/config/strings.dart
+++ b/lib/config/strings.dart
@@ -5,12 +5,12 @@ final String devBaseURL = "poipole.herokuapp.com";
 final String baseURL = "poipole.herokuapp.com";
 String key = "";
 String csrf = "";
-String sessionID = "";
+String sessionId = "";
 
-setKey(String key) {
-  key = key;
+setKey(String newKey) {
+  key = newKey;
 }
 
-setSessionId(String sessionId) {
-  sessionID = sessionId;
+setSessionId(String newSessionId) {
+  sessionId = newSessionId;
 }
\ No newline at end of file
diff --git a/lib/model/lokasi.dart b/lib/model/lokasi.dart
new file mode 100644
index 00000000..7981aec8
--- /dev/null
+++ b/lib/model/lokasi.dart
@@ -0,0 +1,25 @@
+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);
+}
diff --git a/lib/model/lokasi.g.dart b/lib/model/lokasi.g.dart
new file mode 100644
index 00000000..a5ec8305
--- /dev/null
+++ b/lib/model/lokasi.g.dart
@@ -0,0 +1,40 @@
+// 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,
+};
diff --git a/lib/network/CustomException.dart b/lib/network/CustomException.dart
new file mode 100644
index 00000000..7d533760
--- /dev/null
+++ b/lib/network/CustomException.dart
@@ -0,0 +1,27 @@
+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
diff --git a/lib/network/cookies_interface.dart b/lib/network/cookies_interface.dart
new file mode 100644
index 00000000..09b6bf6b
--- /dev/null
+++ b/lib/network/cookies_interface.dart
@@ -0,0 +1,98 @@
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:path_provider/path_provider.dart';
+import 'package:ppl_disabilitas/config/strings.dart';
+
+class CookiesInterface {
+  Future<bool> checkCookieFileAvailability({String fileName}) async {
+    Directory dir;
+    await getApplicationDocumentsDirectory().then((Directory directory) {
+      dir = directory;
+    });
+    File cookieFile = File("${dir.path}/$fileName.json");
+    bool cookiesExist = cookieFile.existsSync();
+
+    return cookiesExist;
+  }
+
+  Future<File> createSignInCookie({
+      Map<String, dynamic> responseHeaders}) async {
+    try {
+      String setCookie;
+      String csrfToken;
+      String sessionId;
+      String userKey;
+      List<String> cookiesList;
+      Directory dir;
+
+      await getApplicationDocumentsDirectory().then((Directory directory) {
+        dir = directory;
+      });
+      File cookieFile = File("${dir.path}/usercookies.json");
+      cookieFile.createSync();
+
+      setCookie = responseHeaders["set-cookie"];
+      if (setCookie != null) {
+        csrfToken = setCookie.split(";")[0].split("=")[1];
+        sessionId = setCookie.split(";")[4].split(",")[1].split("=")[1];
+        userKey = key;
+      }
+      cookiesList = <String>[
+        csrfToken,
+        sessionId,
+        userKey,
+      ];
+      cookieFile.writeAsStringSync(json.encode(cookiesList));
+      return cookieFile;
+    } on Exception catch (e) {
+      print(e.toString());
+      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 = File(dir.path + "/searchhistory.json");
+      cookieFile.createSync();
+      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.insert(0, recentSearch);
+          });
+        } else {
+          currentSearchHistory = [];
+        }
+        await cookieFile.writeAsString(json.encode(currentSearchHistory));
+      });
+      return cookieFile;
+    } on Exception catch (e) {
+      print(e.toString());
+      rethrow;
+    }
+  }
+
+  Future<dynamic> getCookieFile({String fileName}) async {
+    Directory dir;
+    await getApplicationDocumentsDirectory().then((Directory directory) {
+      dir = directory;
+    });
+    File file = File("${dir.path}/$fileName.json");
+    dynamic res = file.readAsStringSync();
+    return res;
+  }
+}
diff --git a/lib/network/data/network_model.dart b/lib/network/data/network_model.dart
new file mode 100644
index 00000000..f42bffaf
--- /dev/null
+++ b/lib/network/data/network_model.dart
@@ -0,0 +1,16 @@
+class NetworkModel<T> {
+  Status status;
+  T data;
+  String message;
+
+  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 }
diff --git a/lib/network/network_interface.dart b/lib/network/network_interface.dart
new file mode 100644
index 00000000..a451cf39
--- /dev/null
+++ b/lib/network/network_interface.dart
@@ -0,0 +1,94 @@
+import 'dart:convert';
+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<dynamic> post({
+    String url, //url nya apa
+    dynamic bodyParams, //data apa yang mau dikasih
+    bool isLogin, //dia login apa ngga
+  }) async {
+    var responseJson;
+    Map<String, String> headersJson =
+        await _buildRequestHeader(isLogin); //butuh header apa ngga
+    try {
+      final response = await http.post(
+        "$url",
+        body: json.encode(bodyParams),
+        headers: headersJson,
+      );
+      responseJson = _response(response);
+    } on SocketException {
+      throw FetchDataException("No Internet Connection");
+    }
+    return responseJson;
+  }
+
+  // GET request
+  Future<dynamic> get({
+    String url,
+    bool isLogin,
+  }) async {
+    var responseJson;
+    Map<String, dynamic> headersJson = await _buildRequestHeader(isLogin);
+    try {
+      final response = await http.get(
+        "$url",
+        headers: headersJson,
+      );
+      responseJson = _response(response);
+    } on SocketException {
+      throw FetchDataException("No Internet Connection");
+    }
+    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(
+      //    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;
+  }
+
+  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}');
+    }
+  }
+}
diff --git a/lib/page/dashboard/dashboard.dart b/lib/page/dashboard/dashboard.dart
index 1f4dcea1..a8c24ec0 100644
--- a/lib/page/dashboard/dashboard.dart
+++ b/lib/page/dashboard/dashboard.dart
@@ -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"),
diff --git a/lib/page/pencarian/pencarian.dart b/lib/page/pencarian/pencarian.dart
index 5e4a680c..e9ee7709 100644
--- a/lib/page/pencarian/pencarian.dart
+++ b/lib/page/pencarian/pencarian.dart
@@ -1,14 +1,64 @@
+import 'package:flutter/foundation.dart';
 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';
 
+/// Create Pencarian page widget with a state
 class Pencarian extends StatefulWidget {
   @override
   PencarianState createState() => PencarianState();
 }
 
+/// State of Pencacrian page
 class PencarianState extends State<Pencarian> {
+  /// Controller for textFormField
+  TextEditingController myController = TextEditingController();
+
+  /// Search Icon for textFormField
   Icon searchIcon = Icon(Icons.search);
-  Widget appBarText = Text("Pencarian Lokasi");
+
+  /// Text for appbar
+  Widget appBarText = Text('Pencarian Lokasi');
+
+  /// List of places currently searched on the textFormField
+  List<Lokasi> currentSearch = [];
+
+  /// List for places from API
+  List<Lokasi> places = [];
+
+  /// BLoC for pencarian
+  LokasiResponseBloc _bloc = LokasiResponseBloc();
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties
+      ..add(DiagnosticsProperty<List>('places', places))
+      ..add(DiagnosticsProperty<List>('currentSearch', currentSearch));
+  }
+
+  @override
+  void initState() {
+    myController.addListener(() {
+      List<Lokasi> tempList = [];
+      for (int i = 0; i < places.length; i++) {
+        if (myController.text.isNotEmpty) {
+          if (places[i]
+              .nama
+              .toLowerCase()
+              .contains(myController.text.toLowerCase())) {
+            tempList.add(places[i]);
+          }
+        }
+      }
+      setState(() {
+        currentSearch = tempList;
+      });
+    });
+    super.initState();
+  }
 
   @override
   Widget build(BuildContext context) {
@@ -17,7 +67,7 @@ class PencarianState extends State<Pencarian> {
         backgroundColor: greenPrimary,
         leading: IconButton(
             icon: Icon(Icons.arrow_back_ios, size: 25),
-            key: Key("Back Icon Key"),
+            key: Key('Back Icon Key'),
             onPressed: () => Navigator.pop(context, 'Take me back')),
         title: Container(
           margin: EdgeInsets.only(top: doubleSpace, bottom: doubleSpace),
@@ -26,7 +76,8 @@ class PencarianState extends State<Pencarian> {
               borderRadius: doubleBorderRadius,
               boxShadow: regularShadow),
           child: TextFormField(
-            key: Key("Text Field Mau Kemana"),
+            controller: myController,
+            key: Key('Text Field Mau Kemana'),
             decoration: InputDecoration(
                 contentPadding: EdgeInsets.all(0),
                 isDense: false,
@@ -44,127 +95,170 @@ class PencarianState extends State<Pencarian> {
                     fontFamily: 'Muli',
                     fontWeight: FontWeight.w700),
                 suffixIcon: IconButton(
-                    icon: Icon(
-                      Icons.mic,
-                      color: greenPrimary,
-                      size: 25,
-                    ),
-                    onPressed: () {})),
+                  icon: Icon(
+                    Icons.mic,
+                    color: greenPrimary,
+                    size: 25,
+                  ),
+                  onPressed: () {},
+                )),
           ),
         ),
       ),
-      body: ListView(
-        padding: const EdgeInsets.all(8),
+      body: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
         children: <Widget>[
-          Padding(
-            padding: EdgeInsets.only(left: doubleSpace, top: 10),
-            child: Text(
-              'Hasil Pencarian',
-              style: TextStyle(
-                fontSize: 15,
-                color: Colors.black,
-                fontFamily: 'Muli',
-              ),
-            ),
-          ),
-          Container(
-            height: 90,
-            color: Colors.transparent,
-            child: Row(
-              mainAxisAlignment: MainAxisAlignment.center,
-              children: <Widget>[
-                CircleAvatar(
-                  backgroundColor: greenPrimary,
-                  child: Text('Test'),
-                ),
-                Padding(
-                  padding: EdgeInsets.all(doubleSpace),
-                  child: Column(
-                    crossAxisAlignment: CrossAxisAlignment.start,
-                    mainAxisAlignment: MainAxisAlignment.center,
-                    children: <Widget>[
-                      Text(
-                        'Margo City',
-                        style: TextStyle(
-                          fontSize: 18,
-                          fontWeight: FontWeight.w800,
-                          color: Colors.black,
-                          fontFamily: 'Muli',
-                        ),
-                      ),
-                      Text(
-                        'Jl. Margonda Raya No.358, Kemir...',
-                        style: TextStyle(
-                          fontSize: 15,
-                          color: Colors.black,
-                          fontFamily: 'Muli',
-                        ),
+          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),
                       ),
-                    ],
-                  ),
-                ),
-                Icon(
-                  Icons.arrow_forward_ios,
-                  color: Colors.grey[400],
-                  size: 20,
-                )
-              ],
-            ),
+                    );
+                    break;
+                  case Status.COMPLETED:
+                    final recentSearch = snapshot.data.data;
+                    Widget displayWidget;
+                    if (recentSearch.listLokasi.isEmpty) {
+                      displayWidget = Center(
+                          child: Text("Anda belum pernah melakukan pencarian"));
+                    } else {
+                      displayWidget = makeLokasiWidget(
+                          "history", recentSearch.listLokasi.take(3).toList());
+                    }
+                    return Expanded(
+                        child: Column(
+                      crossAxisAlignment: CrossAxisAlignment.start,
+                      children: <Widget>[
+                        Container(
+                            margin: EdgeInsets.all(doubleSpace),
+                            child: Text(
+                              "Pencarian terdahulu",
+                              style:
+                                  TextStyle(fontFamily: 'Muli', fontSize: 15),
+                            )),
+                        Flexible(child: displayWidget),
+                      ],
+                    ));
+                    break;
+                  case Status.ERROR:
+                    return Center(
+                      child: Text("${snapshot.data.status}"),
+                    );
+                    break;
+                }
+              }
+              return Container();
+            },
           ),
           Container(
-            decoration: BoxDecoration(
-                border: Border(top: BorderSide(color: Colors.grey[400]))),
+            margin: EdgeInsets.only(
+                left: doubleSpace, top: regularSpace, bottom: smallSpace),
+            child: Text("Hasil Pencarian"),
           ),
-          Container(
-            height: 90,
-            color: Colors.transparent,
-            child: Row(
-              mainAxisAlignment: MainAxisAlignment.center,
-              children: <Widget>[
-                CircleAvatar(
-                  backgroundColor: greenPrimary,
-                  child: Text('Test'),
-                ),
-                Padding(
-                  padding: EdgeInsets.all(doubleSpace),
-                  child: Column(
-                    crossAxisAlignment: CrossAxisAlignment.start,
-                    mainAxisAlignment: MainAxisAlignment.center,
+          StreamBuilder<NetworkModel<LokasiListResponse>>(
+            stream: _bloc.lokasiListStream,
+            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:
+                    places = snapshot.data.data.listLokasi;
+                    return Expanded(
+                        flex: 2,
+                        child: currentSearch.isEmpty
+                            ? Center(child: Text('Cari lokasi'))
+                            : makeLokasiWidget("api", currentSearch));
+                    break;
+                  case Status.ERROR:
+                    return Center(
+                      child: Text(snapshot.data.data.toString()),
+                    );
+                    break;
+                }
+              }
+              return Container();
+            },
+          ),
+        ],
+      ),
+    );
+  }
+
+  Widget makeLokasiWidget(String key, List<Lokasi> places) {
+    print('$key - $places');
+    return ListView.builder(
+        shrinkWrap: true,
+        itemCount: places.length,
+        itemBuilder: (context, index) {
+          return InkWell(
+            key: Key("$key-${places[index].nama}"),
+            onTap: () {
+              _bloc.saveRecentSearch(places[index]);
+            },
+            child: 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(
                     children: <Widget>[
-                      Text(
-                        'Margo City',
-                        style: TextStyle(
-                          fontSize: 18,
-                          fontWeight: FontWeight.w800,
-                          color: Colors.black,
-                          fontFamily: 'Muli',
-                        ),
+                      CircleAvatar(
+                        backgroundColor: greenPrimary,
+                        child: Text('Test'),
                       ),
-                      Text(
-                        'Jl. Margonda Raya No.358, Kemir...',
-                        style: TextStyle(
-                          fontSize: 15,
-                          color: Colors.black,
-                          fontFamily: 'Muli',
+                      Container(
+                        padding: EdgeInsets.all(doubleSpace),
+                        child: Column(
+                          crossAxisAlignment: CrossAxisAlignment.start,
+                          mainAxisAlignment: MainAxisAlignment.center,
+                          children: <Widget>[
+                            Text(
+                              places[index].nama,
+                              style: TextStyle(
+                                fontSize: 18,
+                                fontWeight: FontWeight.w800,
+                                color: Colors.black,
+                                fontFamily: 'Muli',
+                              ),
+                            ),
+                            Text(
+                              places[index].alamat,
+                              style: TextStyle(
+                                fontSize: 15,
+                                color: Colors.black,
+                                fontFamily: 'Muli',
+                              ),
+                            ),
+                          ],
                         ),
                       ),
                     ],
                   ),
-                ),
-                Icon(
-                  Icons.arrow_forward_ios,
-                  color: Colors.grey[400],
-                  size: 20,
-                )
-              ],
+                  Icon(
+                    Icons.arrow_forward_ios,
+                    color: Colors.grey[400],
+                    size: 20,
+                  )
+                ],
+              ),
             ),
-          ),
-          Container(
-            decoration: BoxDecoration(
-                border: Border(top: BorderSide(color: Colors.grey[400]))),
-          ),
-        ],
-      ),
-    );
+          );
+        });
   }
 }
diff --git a/lib/repository/LokasiRepository.dart b/lib/repository/LokasiRepository.dart
new file mode 100644
index 00000000..04c582f4
--- /dev/null
+++ b/lib/repository/LokasiRepository.dart
@@ -0,0 +1,36 @@
+import 'dart:convert';
+
+import 'package:ppl_disabilitas/model/lokasi.dart';
+import 'package:ppl_disabilitas/network/cookies_interface.dart';
+import 'package:ppl_disabilitas/network/network_interface.dart';
+
+class LokasiRepository {
+  NetworkInterface _network = NetworkInterface();
+
+  Future<LokasiListResponse> fetchLokasi() async {
+    final response = await _network.get(
+        url: 'https://my.api.mockaroo.com/mall.json?key=dbcde960',
+        isLogin: false);
+    return LokasiListResponse.fromJson(response);
+  }
+
+  Future<LokasiListResponse> fetchRecentSearch() async {
+    var response;
+    await CookiesInterface().checkCookieFileAvailability(fileName: "searchhistory").then((boolean) async {
+      if (!boolean) {
+        response = [];
+      } else {
+        await CookiesInterface().getCookieFile(fileName: "searchhistory").then((cookie) {
+          response = json.decode(cookie);
+        });
+      }
+    });
+    return LokasiListResponse.fromJson(response);
+  }
+
+  Future<void> saveRecentSearch(Lokasi recentSearch) async {
+    Map<String, dynamic> searchToMap = recentSearch.toJson();
+    await CookiesInterface()
+        .createSearchHistoryCookie(recentSearch: searchToMap);
+  }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 60b1bcba..1cb5a249 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -25,11 +25,11 @@ dependencies:
   location: ^2.5.3
   flutter_plugin_android_lifecycle: ^1.0.6
   flutter_polyline_points: ^0.1.0
-  # The following adds the Cupertino Icons font to your application.
-  # Use with the CupertinoIcons class for iOS style icons.
+  path_provider: ^1.6.5
   cupertino_icons: ^0.1.2
   google_maps_flutter: ^0.5.24+1
   flutter_dotenv: ^2.1.0
+  json_serializable: ^3.2.5
 
 dev_dependencies:
   flutter_test:
@@ -37,6 +37,7 @@ dev_dependencies:
   flutter_launcher_icons: ^0.7.4
   # Linter dependency
   pedantic: ^1.8.0 # The default Linter package used in Google
+  build_runner: ^1.8.0
 
 flutter_icons:
   android: "launcher_icon"
diff --git a/test/cookie_test.dart b/test/cookie_test.dart
new file mode 100644
index 00000000..7a35a8c5
--- /dev/null
+++ b/test/cookie_test.dart
@@ -0,0 +1,63 @@
+import 'dart:io';
+import 'package:flutter/services.dart';
+import 'package:mockito/mockito.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:ppl_disabilitas/network/cookies_interface.dart';
+
+class MockCookiesInterface extends Mock implements CookiesInterface {}
+
+void main() {
+  TestWidgetsFlutterBinding.ensureInitialized();
+  MethodChannel channel =
+      const MethodChannel('plugins.flutter.io/path_provider');
+  setUpAll(() async {
+    // Create a temporary directory.
+    final directory = await Directory.systemTemp.createTemp();
+
+    // Mock out the MethodChannel for the path_provider plugin.
+    channel.setMockMethodCallHandler((MethodCall methodCall) async {
+      // If you're getting the apps documents directory, return the path to the
+      // temp directory on the test environment instead.
+      if (methodCall.method == 'getApplicationDocumentsDirectory') {
+        return directory.path;
+      }
+      return null;
+    });
+  });
+  CookiesInterface mockHttpClient;
+  test('Creates cookie file for sign in session', () async {
+    final responseHeaderFromSignIn = {
+      "set-cookie":
+          "csrftoken=v4E6UNpTMUMAoDxMoSZUBVPuAh7mkIb96DfRcakdivghb0d57yvCZxbbya7L3kFv; expires=Fri, 05 Mar 2021 03:33:39 GMT; Max-Age=31449600; Path=/; SameSite=Lax;sessionid=vrarp9pga02bwr97duemf6ym94gjgepn; expires=Fri, 20 Mar 2020 03:33:39 GMT; HttpOnly; Max-Age=1209600; Path=/; SameSite=Lax",
+    };
+    mockHttpClient = MockCookiesInterface();
+    String rootDir =
+        await channel.invokeMethod('getApplicationDocumentsDirectory');
+    when(mockHttpClient.createSignInCookie(
+            responseHeaders: responseHeaderFromSignIn))
+        .thenAnswer((_) async {
+      await Future.delayed(Duration(milliseconds: 50));
+      return Future<File>.value(File("$rootDir/usercookies.json"));
+    });
+    // combine with sign in test here
+  });
+  test('Creates cookie file after search', () async {
+    final recentSearch = {
+      "nama": "Johnson",
+      "latitude": -2.9062039,
+      "longitude": 114.6905436,
+      "alamat": "2460 Comanche Crossing",
+      "telepon": "+62 805 612 4225"
+    };
+    String rootDir =
+        await channel.invokeMethod("getApplicationDocumentsDirectory");
+    when(mockHttpClient.createSearchHistoryCookie(
+      recentSearch: recentSearch
+    ))
+        .thenAnswer((_) async {
+      await Future.delayed(Duration(milliseconds: 50));
+      return Future<File>.value(File("$rootDir/usercookies.json"));
+    });
+    // combine with sign in test here
+  });
+}
diff --git a/test/mock_test.dart b/test/mock_test.dart
new file mode 100644
index 00000000..537257fa
--- /dev/null
+++ b/test/mock_test.dart
@@ -0,0 +1,87 @@
+import 'package:flutter/material.dart';
+import 'package:mockito/mockito.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:ppl_disabilitas/network/network_interface.dart';
+import 'package:ppl_disabilitas/page/dashboard/dashboard.dart';
+import 'package:http/http.dart' as http;
+import 'package:pedantic/pedantic.dart';
+
+class MockNavigatorObserver extends Mock implements NavigatorObserver {}
+
+class MockNetwork extends Mock implements NetworkInterface {}
+
+class MockHttp extends Mock implements http.Client {}
+
+void main() {
+  group('Dashboard navigation tests', () {
+    NavigatorObserver mockObserver;
+    NetworkInterface mockNetwork;
+    MockHttp mockHttp;
+    setUp(() {
+      mockObserver = MockNavigatorObserver();
+      mockNetwork = MockNetwork();
+      mockHttp = MockHttp();
+      when(mockHttp.get('http://wwww.google.com'))
+          .thenAnswer((_) async => http.Response('{"title": "Test"}', 200));
+      when(mockNetwork.get(isLogin: false, url: anyNamed('url')))
+          .thenAnswer((_) async {
+        await Future.delayed(Duration(milliseconds: 50));
+        return Future<dynamic>.value([
+          {
+            "nama": "Coolidge",
+            "latitude": -23.7169139,
+            "longitude": -46.8498038,
+            "alamat": "74809 Hooker Drive",
+            "telepon": "+55 956 836 5799"
+          }
+        ]);
+      });
+    });
+
+    Future<Null> _buildDashboardPage(WidgetTester tester) async {
+      await tester.pumpWidget(MaterialApp(
+        home: Dashboard(),
+
+        /// This mocked observer will now receive all navigation events
+        /// that happen in our app.
+        navigatorObservers: [mockObserver],
+      ));
+
+      /// The tester.pumpWidget() call above just built our app widget
+      /// and triggered the pushObserver method on the mockObserver once.
+      verify(mockObserver.didPush(any, any));
+    }
+
+    Future<Null> _navigateToPencarianPage(WidgetTester tester) async {
+      final textFieldKey = Key("Text Field Mau Kemana");
+      await tester.tap(find.byKey(textFieldKey));
+      await tester.pump();
+    }
+
+    testWidgets(
+        'when tapping text form field, should navigate to pencarian page',
+        (WidgetTester tester) async {
+      final textFieldKeyPencarian = Key("Text Field Mau Kemana");
+      await _buildDashboardPage(tester);
+      await _navigateToPencarianPage(tester);
+
+      verify(mockObserver.didPush(any, any));
+      expect(find.byKey(textFieldKeyPencarian), findsOneWidget);
+    });
+
+    testWidgets('tapping the back button should navigate back to the dashboard',
+        (WidgetTester tester) async {
+      final backIconKey = Key("Back Icon Key");
+      await _buildDashboardPage(tester);
+      await _navigateToPencarianPage(tester);
+      await tester.pump();
+      final Route pushedRoute =
+          verify(mockObserver.didPush(captureAny, any)).captured.single;
+      String popResult;
+      unawaited(pushedRoute.popped.then((result) => popResult = result));
+      await tester.tap(find.byKey(backIconKey));
+      await tester.pumpAndSettle();
+      expect(popResult, 'Take me back');
+    });
+  });
+}
diff --git a/test/navigation_test.dart b/test/navigation_test.dart
deleted file mode 100644
index d8952af1..00000000
--- a/test/navigation_test.dart
+++ /dev/null
@@ -1,65 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:mockito/mockito.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:ppl_disabilitas/page/dashboard/dashboard.dart';
-import 'package:pedantic/pedantic.dart';
-
-
-class MockNavigatorObserver extends Mock implements NavigatorObserver {}
-
-void main() {
-  group('Dashboard navigation tests', () {
-    NavigatorObserver mockObserver;
-
-    setUp(() {
-      mockObserver = MockNavigatorObserver();
-    });
-
-    Future<Null> _buildDashboardPage(WidgetTester tester) async {
-      await tester.pumpWidget(MaterialApp(
-        home: Dashboard(),
-
-        /// This mocked observer will now receive all navigation events
-        /// that happen in our app.
-        navigatorObservers: [mockObserver],
-      ));
-
-      /// The tester.pumpWidget() call above just built our app widget
-      /// and triggered the pushObserver method on the mockObserver once.
-      verify(mockObserver.didPush(any, any));
-    }
-
-    Future<Null> _navigateToPencarianPage(WidgetTester tester) async {
-      final textFieldKey = Key("Text Field Mau Kemana");
-      await tester.tap(find.byKey(textFieldKey));
-      await tester.pumpAndSettle();
-    }
-
-    testWidgets(
-        'when tapping text form field, should navigate to pencarina page',
-            (WidgetTester tester) async {
-          final textFieldKeyPencarian = Key("Text Field Mau Kemana");
-          await _buildDashboardPage(tester);
-          await _navigateToPencarianPage(tester);
-          verify(mockObserver.didPush(any, any));
-          expect(find.byKey(textFieldKeyPencarian), findsOneWidget);
-
-        });
-
-    testWidgets('tapping the back button should navigate back to the dashboard',
-            (WidgetTester tester) async {
-          final backIconKey = Key("Back Icon Key");
-          await _buildDashboardPage(tester);
-          await _navigateToPencarianPage(tester);
-          final Route pushedRoute =verify(mockObserver.didPush(captureAny, any)).captured.single;
-          String popResult;
-          unawaited(pushedRoute.popped.then((result) => popResult = result));
-          await tester.tap(find.byKey(backIconKey));
-          await tester.pumpAndSettle();
-          expect(popResult, 'Take me back');
-
-
-
-        });
-  });
-}
\ No newline at end of file
diff --git a/test/pencarian_test.dart b/test/pencarian_test.dart
index 43546747..db66d482 100644
--- a/test/pencarian_test.dart
+++ b/test/pencarian_test.dart
@@ -6,19 +6,52 @@
 // tree, read text, and verify that the values of widget properties are correct.
 
 
+import 'dart:io';
+
 import 'package:flutter/material.dart';
 import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/mockito.dart';
 import 'package:ppl_disabilitas/page/pencarian/pencarian.dart';
+import 'package:ppl_disabilitas/network/network_interface.dart';
 
+class MockNetwork extends Mock implements NetworkInterface {}
 
 void main() {
+  MockNetwork mockNetwork;
+  setUp(() {
+    mockNetwork = MockNetwork();
+    when(mockNetwork.get(isLogin: false, url: anyNamed('url'))).thenAnswer((_) async {
+      await Future.delayed(Duration(milliseconds: 50));
+      return Future<dynamic>.value(
+          [
+            {
+              "nama": "Coolidge",
+              "latitude": -23.7169139,
+              "longitude": -46.8498038,
+              "alamat": "74809 Hooker Drive",
+              "telepon": "+55 956 836 5799"
+            }
+          ]
+      );
+    });
+  });
   testWidgets('display list view in pencarian', (WidgetTester tester) async {
-    // Provide the childWidget to the Container.
     await tester.pumpWidget(MaterialApp(home: Pencarian()));
+    expect(find.byKey(const Key("Text Field Mau Kemana")), findsOneWidget);
+    //var textField = find.byKey(const Key("Text Field Mau Kemana"));
+    //await tester.tap(textField);
+    await tester.enterText(find.byKey(const Key("Text Field Mau Kemana")), "Coolidge");
+    await tester.pump();
+    expect(find.text("Hasil Pencarian"), findsOneWidget);
+
+    //expect(find.byType(CircleAvatar), findsWidgets);
+
+    //expect(find.byKey(const Key("api-Coolidge")), findsOneWidget);
+    // [TODO] tiap item itu punya key unik
     // Search for the childWidget in the tree and verify it exists.
-    expect(find.byType(ListView), findsOneWidget);
-    expect(find.byType(Container), findsNWidgets(7));
-    expect(find.byType(Icon), findsNWidgets(5));
+    //expect(find.byType(ListView), findsNWidgets);
+    //expect(find.byType(Container), findsWidgets);
+    //expect(find.byType(Icon), findsWidgets);
   });
 
   testWidgets('finds a text field in pencarian', (WidgetTester tester) async {
@@ -27,4 +60,13 @@ void main() {
     expect(find.byKey(textFieldKey), findsOneWidget);
   });
 
+
+  testWidgets('test textfield result', (WidgetTester tester) async {
+    final textFieldKey = Key("Text Field Mau Kemana");
+    await tester.pumpWidget(MaterialApp(home: Pencarian()));
+    await tester.enterText(find.byKey(textFieldKey), 'Mallory');
+    await tester.pump();
+    expect(find.text('Mallory'), findsOneWidget);
+});
+
 }
diff --git a/test/widget_test.dart b/test/widget_test.dart
index 0337d309..0a223972 100644
--- a/test/widget_test.dart
+++ b/test/widget_test.dart
@@ -14,7 +14,7 @@ void main() {
     await tester.pumpWidget(MaterialApp(home: Dashboard()));
     // Search for the childWidget in the tree and verify it exists.
     expect(find.byType(Scaffold), findsOneWidget);
-    expect(find.byType(Stack), findsNWidgets(2));
+    expect(find.byKey(Key("Stack")), findsOneWidget);
     expect(find.byType(TextFormField), findsOneWidget);
     expect(find.byType(Icon), findsNWidgets(3));
     expect(find.text('Kamu mau kemana?'), findsOneWidget);
-- 
GitLab