Skip to content

Instantly share code, notes, and snippets.

@rsajob
Last active January 10, 2019 14:08
Show Gist options
  • Save rsajob/cf6f2951d3e7f49afdbfb6a3bc7e02aa to your computer and use it in GitHub Desktop.
Save rsajob/cf6f2951d3e7f49afdbfb6a3bc7e02aa to your computer and use it in GitHub Desktop.
Правильное управление Toothpick Scopes + Moxy
package com.rsajob.toothpick
import android.os.Bundle
import android.support.v4.app.Fragment
import android.util.Log
import toothpick.Toothpick
import toothpick.configuration.MultipleRootException
import kotlin.reflect.KProperty
/**
* Класс-делегат предназначен для гарантии однократной инициализации скоупа Toothpick
* при создании и восстановлени фрагментов.
*
* Проблема в следующем.
* Скоуп должен открываться и инициализироваться в момент создания фрагмента, в методе Fragment.onCreate()
* Потому что это является основной точкой входа в экран.
*
* Первая проблема в том, что у фрагментов этот метод вызывается всегда, не только при создании но и при восстановлении.
* А нам надо проинициализировать скоуп только один раз.
*
* Стратегия следующая: добавляем в аргументы фрагмента параметр ARG_SCOPE_NAME
* Если его ещё нет - это значит, что фрагмент создаётся первый раз, и нужно инициализировать скоуп
* и тут же сохраняем ARG_SCOPE_NAME в аргументы фрагмента
*
* Если ARG_SCOPE_NAME уже есть - это значит, что происходит восстановление фрагмента,
* например была смена конфигурации (переворот экрана). В этом случае скоуп уже проинициализирован
* и ничего делать не надо.
*
* Но при убийстве процесса системой, при восстановлении фрагментов, аргумент ARG_SCOPE_NAME будет присутствовать,
* но скоуп не будет инициализирован, поэтому если аргумент установлен, мы проверяем не открыт ли скоуп, если не
* открыт то инициализируем.
*
* Есть только один (костыльный) вариант проверить, открыт ли non-root скоуп в Toothpick уже или нет - это явно открыть
* скоуп методом Toothpick.openScope(scopeName), без указания родителя, и проверить есть ли у него родитель
* после открытия. Если родителя нет (или получаем MultipleRootException) то это значит, что открылся (создался) новый
* root скоуп, что означает что данный скоуп небыл ранее открыт. После проверки закрываем его.
*
* Сохранение имени скоупа в аргументы фрагмента ещё нужно для динамических скоупов, когда мы генерируем имя скоупа при
* открытии фрагмента первый раз.
*
* Это также корректно работает с DKA (Don't keep activity). Когда activity уминает, то умирает и презентер и вместе с
* ним закрываются скоупы. Закрытие скоупов должно происходить в mvp-презнторах (moxy) в onDestroy(). Тоесть
* время жизни скоупов должно равняться времени жизни презентеров.
*
*
* @author Roman Savelev (aka fantom and rsajob). Date: 23.10.18
*/
class ScopeInitDelegate(
private val initScopeName: String,
private val fragment: Fragment,
private val initScope: ((String) -> Unit)? = null
){
operator fun getValue(thisRef: Any?, property: KProperty<*>): String
{
var scopeName = fragment.arguments?.getString(ARG_SCOPE_NAME)
if (scopeName == null) {
scopeName = initScopeName
// Save scope name to fragment arguments
fragment.arguments = (fragment.arguments ?: Bundle()).apply { putString(ARG_SCOPE_NAME, scopeName) }
Log.v(LOG_TAG, "int scope: $scopeName")
initScope?.invoke(scopeName)
}else
if (!isNonRootScopeOpened(scopeName)) {
Log.v(LOG_TAG, "int scope: $scopeName")
initScope?.invoke(scopeName)
}else{
// Log.v(LOG_TAG, "reuse scope: $scopeName")
}
return scopeName
}
/**
* Проверяем открыт ли скоуп в Toothpick. Предполагается что мы проверяем только не root скоупы.
*
* Есть только один костыльный вариант проверить, открыт ли non-root скоуп в Toothpick уже или нет - это явно открыть
* скоуп методом Toothpick.openScope(scopeName), без указания родителя, и проверить есть ли у него родитель
* после открытия. Если родителя нет (или получаем MultipleRootException) то это значит, что открылся (создался) новый
* root скоуп, что означает что данный скоуп небыл ранее открыт. После проверки закрываем его.
*/
private fun isNonRootScopeOpened(scopeName:String) =
try {
val isRootScope = Toothpick.openScope(scopeName).parentScope == null
if (isRootScope) {
Toothpick.closeScope(scopeName)
false
}else
true
} catch (e: MultipleRootException) {
false
}
companion object {
const val LOG_TAG = "Toothpick"
const val ARG_SCOPE_NAME = "arg_scope_name"
}
}
fun uniqueScopeName(baseScopeName:String):String = "${baseScopeName}_${System.currentTimeMillis()}"
fun Fragment.initScope(scopeName:String, initScope: ((String) -> Unit)?) = ScopeInitDelegate(scopeName, this, initScope)
fun Fragment.initDynamicScope(baseScopeName:String, initScope: ((String) -> Unit)?) =
ScopeInitDelegate(uniqueScopeName(baseScopeName), this, initScope)
fun Fragment.initDynamicScope(initScope: ((String) -> Unit)?) =
ScopeInitDelegate(uniqueScopeName(this::class.java.simpleName), this, initScope)
package com.rsajob.toothpick
import android.util.Log
import com.arellomobile.mvp.MvpPresenter
import com.arellomobile.mvp.MvpView
import toothpick.Scope
import toothpick.Toothpick
import javax.inject.Inject
/**
* Created by Roman Savelev (aka @rsa) on 9/19/18.
*
*/
open class ScopedMvpPresenter<T : MvpView> : MvpPresenter<T>()
{
@Inject
lateinit var scope: Scope
override fun onDestroy() {
super.onDestroy()
Log.v("Toothpick", "Close scope ${scope.name}")
Toothpick.closeScope(scope.name)
}
}
// ============================================================
// Пример фрагмента обычного экрана
// ============================================================
class DetailsFragment : MvpAppCompatFragment(), IDetailsView, BackButtonListener {
val layoutRes: Int = R.layout.fragment_details
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(layoutRes, container, false)
private val scopeName:String by initDynamicScope { realScopeName ->
Toothpick.openScopes(parentScopeName, realScopeName).apply {
installModules(
object : Module() {
init {
bind(PrimitiveWrapper::class.java).withName(ListingId::class.java).toInstance(PrimitiveWrapper(listingId))
bind(DetailsInteractor::class.java).singletonInScope()
}
}
)
}
}
companion object {
private const val ARG_LISTING_ID = "arg_listing_id"
fun newInstance(listingId:Long, parentScope:String) : DetailsFragment
{
return DetailsFragment().withArguments(
ARG_PARENT_SCOPE_NAME to parentScope,
ARG_LISTING_ID to listingId
)
}
}
private val parentScopeName:String by argument(ARG_PARENT_SCOPE_NAME)
private val listingId:Long by argument(ARG_LISTING_ID)
@InjectPresenter
lateinit var presenter: DetailsPresenter
@ProvidePresenter
fun providePresenter(): DetailsPresenter = Toothpick.openScope(scopeName).getInstance(DetailsPresenter::class.java)
override fun onCreate(savedInstanceState: Bundle?) {
Toothpick.inject(this, Toothpick.openScope(scopeName))
super.onCreate(savedInstanceState)
}
}
@InjectViewState
class DetailsPresenter @Inject constructor(
@ListingId listingIdWrapper: PrimitiveWrapper<Long>
) : ScopedMvpPresenter<IDetailsView>()
{
// Скоуп закрывается в ScopedMvpPresenter::onDestroy()
}
object DI {
const val APP_SCOPE = "APP_SCOPE"
const val SERVER_SCOPE = "SERVER_SCOPE"
// Dynamic scopes
var PROFILE_FLOW_SCOPE = "PROFILE_FLOW_SCOPE"
// ...
// Static scopes
const val FILTER_FLOW_SCOPE = "FILTER_FLOW_SCOPE"
// ...
}
val ARG_SCOPE_NAME = "arg_scopeName"
val ARG_PARENT_SCOPE_NAME = "arg_parentScopeName"
// ============================================================
// Пример FlowFragment
// ============================================================
class ProfileFlowFragment: FlowFragment(), MvpView
{
private val scopeName:String by initDynamicScope { realScopeName ->
DI.PROFILE_FLOW_SCOPE = realScopeName
val parentScope = arguments?.getString(ARG_PARENT_SCOPE_NAME) ?: DI.TOP_FLOW_SCOPE
val scope = Toothpick.openScopes(parentScope, realScopeName)
scope.installModules(
FlowNavigationModule(scope.getInstance(FlowRouter::class.java))
)
}
@InjectPresenter
lateinit var presenter: ProfileFlowPresenter
@ProvidePresenter
fun providePresenter(): ProfileFlowPresenter = Toothpick.openScope(scopeName).getInstance(ProfileFlowPresenter::class.java)
override fun onCreate(savedInstanceState: Bundle?) {
Toothpick.inject(this, Toothpick.openScope(scopeName))
super.onCreate(savedInstanceState)
navigator.setLaunchScreen(Screens.Profile.Login())
}
}
override fun onExit() { presenter.onExit() }
}
@InjectViewState
class ProfileFlowPresenter @Inject constructor(
private val router: FlowRouter
) : ScopedMvpPresenter<MvpView>()
{
// Скоуп закрывается в ScopedMvpPresenter::onDestroy()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment