refactor: Store tab preferences as polymorphic JSON

This commit is contained in:
Nik Clayton 2024-03-12 10:43:33 +01:00
parent e93e4ffb53
commit a973f67ac8
10 changed files with 152 additions and 116 deletions

View File

@ -77,7 +77,7 @@ import app.pachli.core.common.extensions.viewBinding
import app.pachli.core.data.repository.Lists
import app.pachli.core.data.repository.ListsRepository
import app.pachli.core.database.model.AccountEntity
import app.pachli.core.database.model.TabKind
import app.pachli.core.database.model.TabData
import app.pachli.core.designsystem.EmbeddedFontFamily
import app.pachli.core.designsystem.R as DR
import app.pachli.core.navigation.AboutActivityIntent
@ -866,8 +866,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
tabLayoutMediator = TabLayoutMediator(activeTabLayout, binding.viewPager, true) {
tab: TabLayout.Tab, position: Int ->
tab.icon = AppCompatResources.getDrawable(this@MainActivity, tabs[position].icon)
tab.contentDescription = when (tabs[position].kind) {
TabKind.LIST -> tabs[position].arguments[1]
tab.contentDescription = when (tabs[position].tabData) {
is TabData.UserList -> tabs[position].title(this@MainActivity)
else -> getString(tabs[position].text)
}
}.also { it.attach() }
@ -877,7 +877,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
// - The previously selected tab (if it hasn't been removed)
// - Left-most tab
val position = if (selectNotificationTab) {
tabs.indexOfFirst { it.kind == TabKind.NOTIFICATIONS }
tabs.indexOfFirst { it.tabData is TabData.Notifications }
} else {
previousTab?.let { tabs.indexOfFirst { it == previousTab } }
}.takeIf { it != -1 } ?: 0

View File

@ -49,7 +49,6 @@ import app.pachli.core.common.extensions.visible
import app.pachli.core.data.repository.Lists
import app.pachli.core.data.repository.ListsRepository
import app.pachli.core.database.model.TabData
import app.pachli.core.database.model.TabKind
import app.pachli.core.designsystem.R as DR
import app.pachli.core.navigation.ListActivityIntent
import app.pachli.core.network.model.MastoList
@ -126,7 +125,7 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener {
MaterialDividerItemDecoration(this, MaterialDividerItemDecoration.VERTICAL),
)
addTabAdapter = TabAdapter(listOf(TabViewData.from(TabData(TabKind.DIRECT))), true, this)
addTabAdapter = TabAdapter(listOf(TabViewData.from(TabData.Direct)), true, this)
binding.addTabRecyclerView.adapter = addTabAdapter
binding.addTabRecyclerView.layoutManager = LinearLayoutManager(this)
@ -189,12 +188,12 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener {
override fun onTabAdded(tab: TabViewData) {
toggleFab(false)
if (tab.kind == TabKind.HASHTAG) {
if (tab.tabData is TabData.Hashtag) {
showAddHashtagDialog()
return
}
if (tab.kind == TabKind.LIST) {
if (tab.tabData is TabData.UserList) {
showSelectListDialog()
return
}
@ -212,14 +211,16 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener {
saveTabs()
}
override fun onActionChipClicked(tab: TabViewData, tabPosition: Int) {
showAddHashtagDialog(tab, tabPosition)
override fun onActionChipClicked(tabData: TabData.Hashtag, tabPosition: Int) {
showAddHashtagDialog(tabData, tabPosition)
}
override fun onChipClicked(tab: TabViewData, tabPosition: Int, chipPosition: Int) {
val newArguments = tab.arguments.filterIndexed { i, _ -> i != chipPosition }
val newTab = tab.copy(tabData = tab.tabData.copy(arguments = newArguments))
currentTabs[tabPosition] = newTab
override fun onChipClicked(tabData: TabData.Hashtag, tabPosition: Int, chipPosition: Int) {
currentTabs[tabPosition] = currentTabs[tabPosition].copy(
tabData = tabData.copy(
tags = tabData.tags.filterIndexed { i, _ -> i != chipPosition },
),
)
saveTabs()
currentTabsAdapter.notifyItemChanged(tabPosition)
@ -243,7 +244,7 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener {
onFabDismissedCallback.isEnabled = expand
}
private fun showAddHashtagDialog(tab: TabViewData? = null, tabPosition: Int = 0) {
private fun showAddHashtagDialog(tabData: TabData.Hashtag? = null, tabPosition: Int = 0) {
val frameLayout = FrameLayout(this)
val padding = Utils.dpToPx(this, 8)
frameLayout.updatePadding(left = padding, right = padding)
@ -259,13 +260,14 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener {
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.action_save) { _, _ ->
val input = editText.text.toString().trim()
if (tab == null) {
val newTab = TabViewData.from(TabData(TabKind.HASHTAG, listOf(input)))
if (tabData == null) {
val newTab = TabViewData.from(TabData.Hashtag(listOf(input)))
currentTabs.add(newTab)
currentTabsAdapter.notifyItemInserted(currentTabs.size - 1)
} else {
val newTab = tab.copy(tabData = tab.tabData.copy(arguments = tab.arguments + input))
currentTabs[tabPosition] = newTab
currentTabs[tabPosition] = currentTabs[tabPosition].copy(
tabData = tabData.copy(tags = tabData.tags + input),
)
currentTabsAdapter.notifyItemChanged(tabPosition)
}
@ -315,7 +317,7 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener {
.setView(statusLayout)
.setAdapter(adapter) { _, position ->
adapter.getItem(position)?.let { item ->
val newTab = TabViewData.from(TabData(TabKind.LIST, listOf(item.id, item.title)))
val newTab = TabViewData.from(TabData.UserList(item.id, item.title))
currentTabs.add(newTab)
currentTabsAdapter.notifyItemInserted(currentTabs.size - 1)
updateAvailableTabs()
@ -377,45 +379,45 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener {
private fun updateAvailableTabs() {
val addableTabs: MutableList<TabViewData> = mutableListOf()
val homeTab = TabViewData.from(TabData(TabKind.HOME))
val homeTab = TabViewData.from(TabData.Home)
if (!currentTabs.contains(homeTab)) {
addableTabs.add(homeTab)
}
val notificationTab = TabViewData.from(TabData(TabKind.NOTIFICATIONS))
val notificationTab = TabViewData.from(TabData.Notifications)
if (!currentTabs.contains(notificationTab)) {
addableTabs.add(notificationTab)
}
val localTab = TabViewData.from(TabData(TabKind.LOCAL))
val localTab = TabViewData.from(TabData.Local)
if (!currentTabs.contains(localTab)) {
addableTabs.add(localTab)
}
val federatedTab = TabViewData.from(TabData(TabKind.FEDERATED))
val federatedTab = TabViewData.from(TabData.Federated)
if (!currentTabs.contains(federatedTab)) {
addableTabs.add(federatedTab)
}
val directMessagesTab = TabViewData.from(TabData(TabKind.DIRECT))
val directMessagesTab = TabViewData.from(TabData.Direct)
if (!currentTabs.contains(directMessagesTab)) {
addableTabs.add(directMessagesTab)
}
val trendingTagsTab = TabViewData.from(TabData(TabKind.TRENDING_TAGS))
val trendingTagsTab = TabViewData.from(TabData.TrendingTags)
if (!currentTabs.contains(trendingTagsTab)) {
addableTabs.add(trendingTagsTab)
}
val trendingLinksTab = TabViewData.from(TabData(TabKind.TRENDING_LINKS))
val trendingLinksTab = TabViewData.from(TabData.TrendingLinks)
if (!currentTabs.contains(trendingLinksTab)) {
addableTabs.add(trendingLinksTab)
}
val trendingStatusesTab = TabViewData.from(TabData(TabKind.TRENDING_STATUSES))
val trendingStatusesTab = TabViewData.from(TabData.TrendingStatuses)
if (!currentTabs.contains(trendingStatusesTab)) {
addableTabs.add(trendingStatusesTab)
}
val bookmarksTab = TabViewData.from(TabData(TabKind.BOOKMARKS))
val bookmarksTab = TabViewData.from(TabData.Bookmarks)
if (!currentTabs.contains(trendingTagsTab)) {
addableTabs.add(bookmarksTab)
}
addableTabs.add(TabViewData.from(TabData(TabKind.HASHTAG)))
addableTabs.add(TabViewData.from(TabData(TabKind.LIST)))
addableTabs.add(TabViewData.from(TabData.Hashtag(emptyList())))
addableTabs.add(TabViewData.from(TabData.UserList("", "")))
addTabAdapter.updateData(addableTabs)

View File

@ -27,7 +27,6 @@ import app.pachli.components.timeline.TimelineFragment
import app.pachli.components.trending.TrendingLinksFragment
import app.pachli.components.trending.TrendingTagsFragment
import app.pachli.core.database.model.TabData
import app.pachli.core.database.model.TabKind
import app.pachli.core.network.model.TimelineKind
/**
@ -43,13 +42,9 @@ data class TabViewData(
val tabData: TabData,
@StringRes val text: Int,
@DrawableRes val icon: Int,
val fragment: (List<String>) -> Fragment,
val fragment: () -> Fragment,
val title: (Context) -> String = { context -> context.getString(text) },
) {
val kind get() = this.tabData.kind
val arguments get() = this.tabData.arguments
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
@ -64,64 +59,62 @@ data class TabViewData(
override fun hashCode() = tabData.hashCode()
companion object {
fun from(tabKind: TabKind) = from(TabData.from(tabKind))
fun from(tabData: TabData) = when (tabData.kind) {
TabKind.HOME -> TabViewData(
fun from(tabData: TabData) = when (tabData) {
TabData.Home -> TabViewData(
tabData = tabData,
text = R.string.title_home,
icon = R.drawable.ic_home_24dp,
fragment = { TimelineFragment.newInstance(TimelineKind.Home) },
)
TabKind.NOTIFICATIONS -> TabViewData(
TabData.Notifications -> TabViewData(
tabData = tabData,
text = R.string.title_notifications,
icon = R.drawable.ic_notifications_24dp,
fragment = { NotificationsFragment.newInstance() },
)
TabKind.LOCAL -> TabViewData(
TabData.Local -> TabViewData(
tabData = tabData,
text = R.string.title_public_local,
icon = R.drawable.ic_local_24dp,
fragment = { TimelineFragment.newInstance(TimelineKind.PublicLocal) },
)
TabKind.FEDERATED -> TabViewData(
TabData.Federated -> TabViewData(
tabData = tabData,
text = R.string.title_public_federated,
icon = R.drawable.ic_public_24dp,
fragment = { TimelineFragment.newInstance(TimelineKind.PublicFederated) },
)
TabKind.DIRECT -> TabViewData(
TabData.Direct -> TabViewData(
tabData = tabData,
text = R.string.title_direct_messages,
icon = R.drawable.ic_reblog_direct_24dp,
fragment = { ConversationsFragment.newInstance() },
)
TabKind.TRENDING_TAGS -> TabViewData(
TabData.TrendingTags -> TabViewData(
tabData = tabData,
text = R.string.title_public_trending_hashtags,
icon = R.drawable.ic_trending_up_24px,
fragment = { TrendingTagsFragment.newInstance() },
)
TabKind.TRENDING_LINKS -> TabViewData(
TabData.TrendingLinks -> TabViewData(
tabData = tabData,
text = R.string.title_public_trending_links,
icon = R.drawable.ic_trending_up_24px,
fragment = { TrendingLinksFragment.newInstance() },
)
TabKind.TRENDING_STATUSES -> TabViewData(
TabData.TrendingStatuses -> TabViewData(
tabData = tabData,
text = R.string.title_public_trending_statuses,
icon = R.drawable.ic_trending_up_24px,
fragment = { TimelineFragment.newInstance(TimelineKind.TrendingStatuses) },
)
TabKind.HASHTAG -> TabViewData(
is TabData.Hashtag -> TabViewData(
tabData = tabData,
text = R.string.hashtags,
icon = R.drawable.ic_hashtag,
fragment = { args -> TimelineFragment.newInstance(TimelineKind.Tag(args)) },
fragment = { TimelineFragment.newInstance(TimelineKind.Tag(tabData.tags)) },
title = { context ->
tabData.arguments.joinToString(separator = " ") {
tabData.tags.joinToString(separator = " ") {
context.getString(
R.string.title_tag,
it,
@ -129,18 +122,18 @@ data class TabViewData(
}
},
)
TabKind.LIST -> TabViewData(
is TabData.UserList -> TabViewData(
tabData = tabData,
text = R.string.list,
icon = R.drawable.ic_list,
fragment = { args ->
fragment = {
TimelineFragment.newInstance(
TimelineKind.UserList(args.first(), args.last()),
TimelineKind.UserList(tabData.listId, tabData.title),
)
},
title = { tabData.arguments.getOrNull(1).orEmpty() },
title = { tabData.title },
)
TabKind.BOOKMARKS -> TabViewData(
TabData.Bookmarks -> TabViewData(
tabData = tabData,
text = R.string.title_bookmarks,
icon = R.drawable.ic_bookmark_active_24dp,

View File

@ -26,7 +26,7 @@ import app.pachli.R
import app.pachli.TabViewData
import app.pachli.core.common.extensions.hide
import app.pachli.core.common.extensions.show
import app.pachli.core.database.model.TabKind
import app.pachli.core.database.model.TabData
import app.pachli.core.designsystem.R as DR
import app.pachli.databinding.ItemTabPreferenceBinding
import app.pachli.databinding.ItemTabPreferenceSmallBinding
@ -39,8 +39,8 @@ interface ItemInteractionListener {
fun onTabRemoved(position: Int)
fun onStartDelete(viewHolder: RecyclerView.ViewHolder)
fun onStartDrag(viewHolder: RecyclerView.ViewHolder)
fun onActionChipClicked(tab: TabViewData, tabPosition: Int)
fun onChipClicked(tab: TabViewData, tabPosition: Int, chipPosition: Int)
fun onActionChipClicked(tabData: TabData.Hashtag, tabPosition: Int)
fun onChipClicked(tabData: TabData.Hashtag, tabPosition: Int, chipPosition: Int)
}
class TabAdapter(
@ -81,8 +81,8 @@ class TabAdapter(
} else {
val binding = holder.binding as ItemTabPreferenceBinding
if (tab.kind == TabKind.LIST) {
binding.textView.text = tab.arguments.getOrNull(1).orEmpty()
if (tab.tabData is TabData.UserList) {
binding.textView.text = tab.tabData.title
} else {
binding.textView.setText(tab.text)
}
@ -107,7 +107,7 @@ class TabAdapter(
(if (removeButtonEnabled) android.R.attr.textColorTertiary else DR.attr.textColorDisabled),
)
if (tab.kind == TabKind.HASHTAG) {
if (tab.tabData is TabData.Hashtag) {
binding.chipGroup.show()
/*
@ -115,7 +115,7 @@ class TabAdapter(
* The other dynamic chips are inserted in front of the actionChip.
* This code tries to reuse already added chips to reduce the number of Views created.
*/
tab.arguments.forEachIndexed { i, arg ->
tab.tabData.tags.forEachIndexed { i, arg ->
val chip = binding.chipGroup.getChildAt(i).takeUnless { it.id == R.id.actionChip } as Chip?
?: Chip(context).apply {
@ -126,23 +126,23 @@ class TabAdapter(
chip.text = arg
if (tab.arguments.size <= 1) {
if (tab.tabData.tags.size <= 1) {
chip.isCloseIconVisible = false
chip.setOnClickListener(null)
} else {
chip.isCloseIconVisible = true
chip.setOnClickListener {
listener.onChipClicked(tab, holder.bindingAdapterPosition, i)
listener.onChipClicked(tab.tabData, holder.bindingAdapterPosition, i)
}
}
}
while (binding.chipGroup.size - 1 > tab.arguments.size) {
binding.chipGroup.removeViewAt(tab.arguments.size)
while (binding.chipGroup.size - 1 > tab.tabData.tags.size) {
binding.chipGroup.removeViewAt(tab.tabData.tags.size)
}
binding.actionChip.setOnClickListener {
listener.onActionChipClicked(tab, holder.bindingAdapterPosition)
listener.onActionChipClicked(tab.tabData, holder.bindingAdapterPosition)
}
} else {
binding.chipGroup.hide()

View File

@ -25,7 +25,7 @@ class MainPagerAdapter(var tabs: List<TabViewData>, activity: FragmentActivity)
override fun createFragment(position: Int): Fragment {
val tab = tabs[position]
return tab.fragment(tab.arguments)
return tab.fragment()
}
override fun getItemCount() = tabs.size

View File

@ -31,7 +31,7 @@ import app.pachli.components.notifications.createNotificationChannelsForAccount
import app.pachli.components.notifications.makeNotification
import app.pachli.core.accounts.AccountManager
import app.pachli.core.database.model.AccountEntity
import app.pachli.core.database.model.TabKind
import app.pachli.core.database.model.TabData
import app.pachli.core.database.model.defaultTabs
import app.pachli.core.navigation.AccountListActivityIntent
import app.pachli.core.network.model.Account
@ -154,7 +154,7 @@ class MainActivityTest {
rule.launch(intent)
rule.getScenario().onActivity {
val currentTab = it.findViewById<ViewPager2>(R.id.viewPager).currentItem
val notificationTab = defaultTabs().indexOfFirst { it.kind == TabKind.NOTIFICATIONS }
val notificationTab = defaultTabs().indexOfFirst { it is TabData.Notifications }
assertEquals(currentTab, notificationTab)
}
}

View File

@ -39,4 +39,7 @@ dependencies {
implementation(libs.moshi)
implementation(libs.moshi.adapters)
ksp(libs.moshi.codegen)
implementation(libs.moshix.sealed.runtime)
ksp(libs.moshix.sealed.codegen)
}

View File

@ -33,7 +33,6 @@ import app.pachli.core.network.model.TranslatedPoll
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import java.net.URLDecoder
import java.net.URLEncoder
import java.time.Instant
import java.util.Date
import javax.inject.Inject
@ -68,17 +67,47 @@ class Converters @Inject constructor(
@TypeConverter
fun stringToTabData(str: String?): List<TabData>? {
return str?.split(";")
?.map {
val data = it.split(":")
TabData.from(data[0], data.drop(1).map { s -> URLDecoder.decode(s, "UTF-8") })
str ?: return null
// Two possible storage formats. Newer (from Pachli 2.4.0) is polymorphic
// JSON, and the first character will be a '['
if (str.startsWith('[')) {
return moshi.adapter<List<TabData>>().fromJson(str)
}
// Older is string of ';' delimited tuples, one per tab.
// The per-tab data is ':' delimited tuples where the first item is the tab's kind,
// any subsequent entries are tab-specific data.
//
// The "Trending_..." / "Trending..." is to work around
// https://github.com/pachli/pachli-android/issues/329
return str.split(";").map {
val data = it.split(":")
val kind = data[0]
val arguments = data.drop(1).map { s -> URLDecoder.decode(s, "UTF-8") }
when (kind) {
"Home" -> TabData.Home
"Notifications" -> TabData.Notifications
"Local" -> TabData.Local
"Federated" -> TabData.Federated
"Direct" -> TabData.Direct
// Work around for https://github.com/pachli/pachli-android/issues/329
// when the Trending... kinds may have been serialised without the '_'
"TrendingTags", "Trending_Tags" -> TabData.TrendingTags
"TrendingLinks", "Trending_Links" -> TabData.TrendingLinks
"TrendingStatuses", "Trending_Statuses" -> TabData.TrendingStatuses
"Hashtag" -> TabData.Hashtag(arguments)
"List" -> TabData.UserList(arguments[0], arguments[1])
"Bookmarks" -> TabData.Bookmarks
else -> throw IllegalStateException("Unrecognised tab kind: $kind")
}
}
}
@TypeConverter
fun tabDataToString(tabData: List<TabData>?): String? {
// List name may include ":"
return tabData?.joinToString(";") { it.kind.repr + ":" + it.arguments.joinToString(":") { s -> URLEncoder.encode(s, "UTF-8") } }
return moshi.adapter<List<TabData>>().toJson(tabData)
}
@TypeConverter

View File

@ -17,48 +17,54 @@
package app.pachli.core.database.model
/**
* A tab's kind.
*
* @param repr String representation of the tab in the database
*/
enum class TabKind(val repr: String) {
HOME("Home"),
NOTIFICATIONS("Notifications"),
LOCAL("Local"),
FEDERATED("Federated"),
DIRECT("Direct"),
TRENDING_TAGS("Trending_Tags"),
TRENDING_LINKS("Trending_Links"),
TRENDING_STATUSES("Trending_Statuses"),
HASHTAG("Hashtag"),
LIST("List"),
BOOKMARKS("Bookmarks"),
}
import com.squareup.moshi.JsonClass
import dev.zacsweers.moshix.sealed.annotations.TypeLabel
/** this would be a good case for a sealed class, but that does not work nice with Room */
@JsonClass(generateAdapter = true, generator = "sealed:kind")
sealed interface TabData {
@TypeLabel("home")
data object Home : TabData
data class TabData(val kind: TabKind, val arguments: List<String> = emptyList()) {
companion object {
fun from(kind: TabKind, arguments: List<String> = emptyList()) =
TabData(kind, arguments)
@TypeLabel("notifications")
data object Notifications : TabData
fun from(kind: String, arguments: List<String> = emptyList()): TabData {
// Work around for https://github.com/pachli/pachli-android/issues/329,
// as the Trending... kinds may have been serialised without the `_`
return when (kind) {
"TrendingTags" -> TabData(TabKind.TRENDING_TAGS, arguments)
"TrendingLinks" -> TabData(TabKind.TRENDING_LINKS, arguments)
"TrendingStatuses" -> TabData(TabKind.TRENDING_STATUSES, arguments)
else -> TabData(TabKind.valueOf(kind.uppercase()), arguments)
}
}
}
@TypeLabel("local")
data object Local : TabData
@TypeLabel("federated")
data object Federated : TabData
@TypeLabel("direct")
data object Direct : TabData
@TypeLabel("trending_tags")
data object TrendingTags : TabData
@TypeLabel("trending_links")
data object TrendingLinks : TabData
@TypeLabel("trending_statuses")
data object TrendingStatuses : TabData
/**
* @property tags List of one or more hashtags (without the leading '#')
* to show in the tab.
*/
@TypeLabel("hashtag")
@JsonClass(generateAdapter = true)
data class Hashtag(val tags: List<String>) : TabData
@TypeLabel("list")
@JsonClass(generateAdapter = true)
data class UserList(val listId: String, val title: String) : TabData
@TypeLabel("bookmarks")
data object Bookmarks : TabData
}
fun defaultTabs() = listOf(
TabData.from(TabKind.HOME),
TabData.from(TabKind.NOTIFICATIONS),
TabData.from(TabKind.LOCAL),
TabData.from(TabKind.DIRECT),
TabData.Home,
TabData.Notifications,
TabData.Local,
TabData.Direct,
)

View File

@ -54,6 +54,7 @@ material-typeface = "4.0.0.2-kotlin"
mockito-inline = "5.2.0"
mockito-kotlin = "5.2.1"
moshi = "1.15.1"
moshix = "0.25.1"
networkresult-calladapter = "1.0.0"
okhttp = "4.12.0"
quadrant = "1.9.1"
@ -177,6 +178,8 @@ mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "
moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
moshi-adapters = { module = "com.squareup.moshi:moshi-adapters", version.ref = "moshi" }
moshi-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" }
moshix-sealed-runtime = { module = "dev.zacsweers.moshix:moshi-sealed-runtime", version.ref = "moshix" }
moshix-sealed-codegen = { module = "dev.zacsweers.moshix:moshi-sealed-codegen", version.ref = "moshix" }
networkresult-calladapter = { module = "at.connyduck:networkresult-calladapter", version.ref = "networkresult-calladapter" }
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }