Last active
January 10, 2019 14:08
-
-
Save rsajob/cf6f2951d3e7f49afdbfb6a3bc7e02aa to your computer and use it in GitHub Desktop.
Правильное управление Toothpick Scopes + Moxy
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ============================================================ | |
// Пример фрагмента обычного экрана | |
// ============================================================ | |
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() | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ============================================================ | |
// Пример 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