Skip to content

Commit

Permalink
feat: refactor to follow clean architecture & implement networking
Browse files Browse the repository at this point in the history
- implement networking side of the application
- Refactored the codebase to follow clean architecture structure
- Implemented a single ViewModel for all screens
  • Loading branch information
polendina committed Aug 27, 2023
1 parent 1bc984e commit be1a722
Show file tree
Hide file tree
Showing 19 changed files with 307 additions and 106 deletions.
5 changes: 5 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

// ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
// ViewModel utilities for Compose
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")

// Icons
implementation ("androidx.compose.material:material-icons-extended:1.6.0-alpha02")
}
2 changes: 2 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission-sdk-23 android:name="android.permission.INTERNET"/>

<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
Expand Down
64 changes: 0 additions & 64 deletions app/src/main/java/com/polendina/egyptpostalcode/SampleData.kt

This file was deleted.

45 changes: 45 additions & 0 deletions app/src/main/kotlin/com/polendina/egyptpostalcode/SampleData.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.polendina.egyptpostalcode

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Atm
import androidx.compose.material.icons.outlined.AttachMoney
import androidx.compose.material.icons.outlined.Mail
import androidx.compose.material.icons.outlined.PointOfSale
import androidx.compose.material.icons.outlined.SwapHoriz
import androidx.compose.material.icons.outlined.SwapVert
import androidx.compose.material.icons.outlined.Window
import androidx.compose.ui.graphics.vector.ImageVector
import com.polendina.egyptpostalcode.domain.model.PostOffice

val offices = listOf<PostOffice>(
PostOffice( tel_code = "047", visits = "10029", id = "1585", postal_code = "33757", office = "مكتب بريد مجاز الحامول", address = "مجاز الحامول الحامول برارى", tel = "3825171", link = "https://egpostal.com/ar/مكتب-بريد-مجاز-الحامول" ),
PostOffice( tel_code = "048", visits = "11334", id = "1325", postal_code = "32657", office = "مكتب بريد شنتنا الحجر", address = "ش داير الناحية شنتنا الحجر بركة السبع", tel = "2997011", link = "https://egpostal.com/ar/مكتب-بريد-شنتنا-الحجر" ),
PostOffice( tel_code = "013", visits = "14000", id = "532", postal_code = "13637", office = "مكتب بريد عزبة زكى", address = "قرية عزبة زكى بنها ", tel = "3233801", link = "https://egpostal.com/ar/مكتب-بريد-عزبة-زكى" ),
PostOffice( tel_code = "013", visits = "18910", id = "689", postal_code = "13887", office = "مكتب بريد كفر رجب", address = "قرية كفر رجب ", tel = "2554456", link = "https://egpostal.com/ar/مكتب-بريد-كفر-رجب" ),
PostOffice( tel_code = "03", visits = "175369", id = "815", postal_code = "21928", office = "مكتب بريد العجمى البيطاش", address = "مساكن المعلمين بيطاش / الدخيلة", tel = "0", link = "https://egpostal.com/ar/مكتب-بريد-العجمى-البيطاش" ),
PostOffice( tel_code = "048", visits = "10104", id = "1422", postal_code = "32855", office = "مكتب بريد جزيرة الحجر", address = "ش داير الناحية قرية نادر / الشهداء", tel = "2773574", link = "https://egpostal.com/ar/مكتب-بريد-جزيرة-الحجر" ),
PostOffice( tel_code = "040", visits = "6085", id = "1256", postal_code = "31859", office = "مكتب بريد ابو النجا", address = "المحلة الكبرى / ابو النجا ", tel = "2025517", link = "https://egpostal.com/ar/مكتب-بريد-ابو-النجا" ),
PostOffice( tel_code = "040", visits = "79789", id = "1049", postal_code = "31589", office = "مكتب بريد قسم تفيش الجميزة", address = "السنطة ثانى", tel = "5344467", link = "https://egpostal.com/ar/مكتب-بريد-قسم-تفيش-الجميزة" ),
PostOffice( tel_code = "040", visits = "14859", id = "1087", postal_code = "31649", office = "مكتب بريد الغريب مركز زفتى", address = "الغريب/ مركز زفتى ", tel = "5632943", link = "https://egpostal.com/ar/مكتب-بريد-الغريب-مركز-زفتى" ),
PostOffice( tel_code = "040", visits = "9233", id = "1237", postal_code = "31837", office = "مكتب بريد السجاعية", address = "بجوار نقطة الشرطة", tel = "2062023", link = "https://egpostal.com/ar/مكتب-بريد-السجاعية" )
)

enum class OfficeFeatures(var stringResource: Int, var available: Boolean, var icon: ImageVector) {
// todo: Some of these icons aren't exactly what I'm looking after
ATM(R.string.atm, true, Icons.Outlined.Atm),
POS(R.string.pos, true, Icons.Outlined.PointOfSale),
IFS(R.string.ifs, false, Icons.Outlined.SwapVert),
ONE_WINDOW(R.string.one_window, true, Icons.Outlined.Window),
INTERNAL_IFS(R.string.internal_ifs, false, Icons.Outlined.SwapHoriz),
SPEED_MAIL(R.string.speed_mail, true, Icons.Outlined.Mail),
}

val iconList = listOf(
Icons.Outlined.Atm,
Icons.Outlined.AttachMoney,
Icons.Outlined.AttachMoney,
Icons.Outlined.AttachMoney,
Icons.Outlined.AttachMoney,
Icons.Outlined.AttachMoney,
)

Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.polendina.egyptpostalcode.data.repository

import com.polendina.egyptpostalcode.domain.model.OfficeList
import com.polendina.egyptpostalcode.domain.model.OfficeResponse
import com.polendina.egyptpostalcode.domain.model.ShipmentResponse
import okhttp3.Headers
import okhttp3.OkHttpClient
import retrofit2.Call
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query

class EgyptPostCodeRepository {

fun getOffices(
city: String,
responseBody: (OfficeList?) -> Unit,
failureThrowable: (Throwable) -> Unit
) {
println("Getting offices")
retrofit.searchPostOffice(city = city).enqueue(object : retrofit2.Callback<OfficeList> {
override fun onResponse(
call: Call<OfficeList>,
response: Response<OfficeList>
) {
// if (response.isSuccessful) {
println("Response")
println(response.isSuccessful)
println(response.body())
responseBody(response.body())
// }
}
override fun onFailure(call: Call<OfficeList>, t: Throwable) {
println("Failure")
failureThrowable(t)
}
})
}

fun trackShipment(
trackNumber: Int,
responseBody: (ShipmentResponse?) -> Unit,
failureThrowable: (Throwable) -> Unit
) {
retrofit.trackShipment(trackNumber = trackNumber).enqueue(object: retrofit2.Callback<ShipmentResponse> {
override fun onResponse(
call: Call<ShipmentResponse>,
response: Response<ShipmentResponse>
) {
responseBody(response.body())
}
override fun onFailure(call: Call<ShipmentResponse>, t: Throwable) {
failureThrowable(t)
}
})
}

}


interface RemoteAPI{
@GET(value = "api/autocomplete")
fun searchPostOffice(@Query("query") city: String): Call<OfficeList>
@GET(value = "api/autocomplete")
fun getOffices(@Query("query") city: String): Call<OfficeResponse>
@GET(value = "api/track")
fun trackShipment(@Query("track_no") trackNumber: Int): Call<ShipmentResponse>
}

val retrofit = Retrofit.Builder()
.baseUrl("https://egpostal.com/")
.addConverterFactory(GsonConverterFactory.create())
.client(OkHttpClient.Builder().addInterceptor {
it.proceed(it.request().newBuilder()
.headers(Headers.of(generateHeaders()))
.build()
)
}.build())
.build()
.create(RemoteAPI::class.java)
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.polendina.egyptpostalcode.data.repository

import java.security.MessageDigest
import java.util.Calendar
import java.util.TimeZone

fun generateHeaders(): Map<String, String> {
val id = Calendar.getInstance(TimeZone.getTimeZone("GMT")).timeInMillis.toString()
val auth = mapOf(
"sender" to encode(paramString = "com.egyptcodebase.egyptcodebase"),
"id" to id,
"token" to encode(paramString = id)
)
return (mapOf(
"Host" to "egpostal.com",
// "Auth" to Base64.getEncoder().encodeToString(Json.encodeToString(auth).toByteArray()),
"Auth" to "eyJzZW5kZXIiOiJhNTBlN2EyMGFkNjJiZDQ1ZWNhMjZjYTJjNDUwOTYxYyIsImlkIjoiMTY5MDM3NTUwODAyNCIsInRva2VuIjoiMWQ4YjA2ZDExYzBmOGYwNmI0NzFhZmUxOTY0YzEyODQifQ==",
// "Accept-Encoding" to "gzip, deflate",
"User-Agent" to "okhttp/3.9.0",
"Connection" to "close"
))
}

private fun encode(paramString: String): String {
val byteArray = MessageDigest
.getInstance("MD5")
.apply {
this.update(paramString.toByteArray())
}
.digest()
val stringBuffer = StringBuffer()
for (byte in byteArray) {
val hexString = "%02x".format(byte)
stringBuffer.append(hexString)
}
return (stringBuffer.toString())
}

//fun encodeString(value: String) {
// val messageDigest = MessageDigest.getInstance("MD5")
// messageDigest.update(value.toByte())
// val byteArray = messageDigest.digest()
// val stringBuffer = StringBuffer()
// for (i in 0..byteArray.size) {
// for (paramString = Integer.toHexString(byteArray[i] & 0xFF)) {
//
// }
// }
//}

fun makeRequest() {
val headers = generateHeaders()
val connectTimeOutMillis = 5000L
println("""
{"data":[{"tel_code":"02","visits":"1717856","id":"2","postal_code":"11511","office":"\u0645\u0643\u062a\u0628 \u0628\u0631\u064a\u062f \u0627\u0644\u0642\u0627\u0647\u0631\u0629 \u0627\u0644\u0631\u0626\u064a\u0633\u0649","address":"\u0645\u064a\u062f\u0627\u0646 \u0627\u0644\u0639\u062a\u0628\u0629\/1\u0634 \u0639\u0628\u062f \u0627\u0644\u062e\u0627\u0644\u0642 \u062b\u0631\u0648\u062a","tel":"23961695","link":"https:\/\/egpostal.com\/ar\/\u0645\u0643\u062a\u0628-\u0628\u0631\u064a\u062f-\u0627\u0644\u0642\u0627\u0647\u0631\u0629-\u0627\u0644\u0631\u0626\u064a\u0633\u0649"},{"tel_code":"02","visits":"61983","id":"17","postal_code":"11529","office":"\u0645\u0643\u062a\u0628 \u0628\u0631\u064a\u062f \u0645\u0631\u0648\u0631 \u0627\u0644\u0642\u0627\u0647\u0631\u0629","address":" \u0645\u0628\u0646\u0649 \u0625\u062f\u0627\u0631\u0629 \u0645\u0631\u0648\u0631 \u0627\u0644\u0642\u0627\u0647\u0631\u0629 \/ \u0627\u0644\u0639\u062a\u0628\u0629 ","tel":"24855408","link":"https:\/\/egpostal.com\/ar\/\u0645\u0643\u062a\u0628-\u0628\u0631\u064a\u062f-\u0645\u0631\u0648\u0631-\u0627\u0644\u0642\u0627\u0647\u0631\u0629"},{"tel_code":"02","visits":"76357","id":"39","postal_code":"11567","office":"\u0645\u0643\u062a\u0628 \u0628\u0631\u064a\u062f \u0628\u0631\u062c \u0627\u0644\u0642\u0627\u0647\u0631\u0629","address":"\u0645\u0628\u0646\u0649 \u0627\u0644\u0628\u0631\u062c \/ \u0627\u0644\u062d\u0632\u064a\u0631\u0629 \/ \u0642\u0635\u0631 \u0627\u0644\u0646\u064a\u0644","tel":"","link":"https:\/\/egpostal.com\/ar\/\u0645\u0643\u062a\u0628-\u0628\u0631\u064a\u062f-\u0628\u0631\u062c-\u0627\u0644\u0642\u0627\u0647\u0631\u0629"},
""".trimIndent().toByteArray(Charsets.UTF_8).decodeToString())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.polendina.egyptpostalcode.domain.model
data class OfficeList(
val data: List<PostOffice>
) {
}

/**
* A data class representing each postal office.
*/
data class PostOffice (
val tel_code: String,
val visits: String,
val id: String,
val postal_code: String,
val office: String,
val address: String,
val tel: String,
val link: String
)

data class OfficeResponse (
val data: List<PostOffice>,
val settings: Settings
) {
data class Settings (
val app_version: Int,
val disable_ads: Boolean,
val popup_frequency: Int
)
}

data class ShipmentResponse (
val data: List<Shipment>
) {
data class Shipment (
val time: String,
val country: String,
val location: String,
val event_type: String,
val extra_info: String
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.polendina.egyptpostalcode.domain.repository

interface PostalOfficeRepository {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.polendina.egyptpostalcode.presentation

import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import com.polendina.egyptpostalcode.data.repository.EgyptPostCodeRepository
import com.polendina.egyptpostalcode.domain.model.PostOffice
import com.polendina.egyptpostalcode.offices

class ScreensViewModel: ViewModel() {

private var _query = mutableStateOf("")
var query = _query
// TODO: Check if the provided value is alphanumeric & not empty etc...
// private val _officesList = mutableStateListOf<PostOffice>()
private val _officesList = offices.toMutableList()
val officeList = _officesList

fun updateQuery(value: String) {
_query.value = value
}

private val egyptPostCodeRepository = EgyptPostCodeRepository()
fun getOffices(
city: String
) {
egyptPostCodeRepository.getOffices(
city = city,
responseBody = { officeResponse ->
officeResponse?.let {
_officesList.clear()
println("office data is ${it.data}")
_officesList.addAll(it.data)
}
}
) {
it.printStackTrace()
}
}

}
Loading

0 comments on commit be1a722

Please sign in to comment.