Я пытаюсь создать приложение, чтобы помочь мастерам подземелий D&D провести кампанию.
Для этого, например, требуется установить очки способностей персонажа, которого игрок использует в кампании.
Учитывая, что любой персонаж в D&D имеет шесть базовых c оценок способностей, я полагал, что кодирование всего на Fragment
, хотя и возможно, приведет к беспорядку спагетти-кода, даже если один прибегнет к привязке данных и MVVM паттерны и другие махинации.
Итак, я пошел и попытался создать собственное представление, которое само обрабатывало бы установку одной оценки способности.
Проблема: при попытке для синтаксического анализа атрибутов настраиваемого представления из файла макета XML.
android.view.InflateException: Binary XML file line #19 in com.callisto.kd205e:layout/scores_fragment: Binary XML file line #19 in com.callisto.kd205e:layout/scores_fragment: Error inflating class com.callisto.kd205e.views.scoresetter.ScoreSetterView
Caused by: android.view.InflateException: Binary XML file line #19 in com.callisto.kd205e:layout/scores_fragment: Error inflating class com.callisto.kd205e.views.scoresetter.ScoreSetterView
Caused by: java.lang.reflect.InvocationTargetException
at java.lang.reflect.Constructor.newInstance0(Native Method)
at java.lang.reflect.Constructor.newInstance(Constructor.java:343)
at android.view.LayoutInflater.createView(LayoutInflater.java:854)
at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:1006)
at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:961)
at android.view.LayoutInflater.rInflate(LayoutInflater.java:1123)
at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:1084)
at android.view.LayoutInflater.inflate(LayoutInflater.java:682)
at android.view.LayoutInflater.inflate(LayoutInflater.java:534)
at androidx.databinding.DataBindingUtil.inflate(DataBindingUtil.java:126)
at androidx.databinding.DataBindingUtil.inflate(DataBindingUtil.java:95)
at com.callisto.kd205e.fragments.scores.ScoresFragment.onCreateView(ScoresFragment.kt:44)
at androidx.fragment.app.Fragment.performCreateView(Fragment.java:2698)
at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:320)
at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1187)
at androidx.fragment.app.FragmentManager.addAddedFragments(FragmentManager.java:2224)
at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1997)
at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1953)
at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1849)
at androidx.fragment.app.FragmentManager$4.run(FragmentManager.java:413)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7356)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
Caused by: kotlin.KotlinNullPointerException
at com.callisto.kd205e.views.scoresetter.ScoreSetterView.initialize(ScoreSetterView.kt:53)
at com.callisto.kd205e.views.scoresetter.ScoreSetterView.<init>(ScoreSetterView.kt:26)
...
Я попытался создать свое собственное представление, следуя руководству Маттиаса Зигмунда . Возможно, я где-то допустил ошибку, пытаясь реализовать его предложения, но я этого не вижу.
Теперь, для всех вы, отважные души, которые осмеливаются бороться с созданным мною ужасом, вот код:
ScoresFragment.kt:
private const val ARG_PARAM1 = "characterId"
class ScoresFragment : BaseFragment()
{
private var characterId: Long? = null
private lateinit var binding: ScoresFragmentBinding
private lateinit var viewModel: ScoresViewModel
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
arguments?.let {
characterId = it.getLong(ARG_PARAM1)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View?
{
binding = DataBindingUtil.inflate(
inflater,
R.layout.scores_fragment,
container,
false
)
val application = requireNotNull(this.activity).application
val database = Kd205eDatabase.getInstance(application).dao
val viewModelFactory =
ScoresViewModelFactory(
database,
application
)
viewModel = ViewModelProvider(this, viewModelFactory)
.get(ScoresViewModel::class.java)
binding.scoresViewModel = viewModel
binding.lifecycleOwner = viewLifecycleOwner
viewModel.track(characterId!!)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?)
{
super.onViewCreated(view, savedInstanceState)
}
companion object
{
@JvmStatic
fun newInstance(characterId: Long) =
ScoresFragment().apply {
arguments = Bundle().apply {
putLong(ARG_PARAM1, characterId)
}
}
}
}
scores_fragment. xml (в значительной степени просто тестовый макет):
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:custom="http://schemas.android.com/apk/com.callisto.kd205e"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="scoresViewModel"
type="com.callisto.kd205e.fragments.scores.ScoresViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragments.species.ScoresFragment">
<com.callisto.kd205e.views.scoresetter.ScoreSetterView
android:id="@+id/scoreSetterStr"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
custom:attribute_name="Strength"
custom:attribute_base_score="10"
custom:attribute_bonus="0"
custom:attribute_icon_id="@drawable/icons8_strength"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
ScoresViewModel.kt:
class ScoresViewModel
(
val database: Kd205eDao,
application: Application
) : BaseViewModel(application)
{
private var character = MutableLiveData<Character>()
fun track(characterId: Long)
{
uiScope.launch {
character.value = setUpCharacterFromDB(characterId)
}
}
private suspend fun setUpCharacterFromDB(characterId: Long): Character?
{
return withContext(Dispatchers.IO)
{
val dbCharacter = database.getSingleCharacter(characterId)!!
val dbRace = database.getSingleRace(dbCharacter.raceId)
val attributes = database.checkAllAttributes()
for (attribute in attributes)
{
database.insertCharacterScore(
DBAbilityScore(characterId, attribute.attributeId, roll4d6MinusLowest())
)
}
val scores = database.getScoresForCharacter(characterId)
val result = Character(dbCharacter, dbRace, scores)
result
}
}
private fun roll4d6MinusLowest(): Int
{
val rolls = arrayOf((1..6).random(), (1..6).random(), (1..6).random(), (1..6).random())
return rolls.sum() - rolls.min()!!
}
}
ScoreSetterView.kt:
class ScoreSetterView(context: Context, attrs: AttributeSet) :
CustomConstraintLayout<ScoreSetterState, ScoreSetterModel>(context, attrs)
{
private var iconId: Int = 0
private var bonus: Int = 0
private var baseScore: Int = 0
private lateinit var attributeName: String
override lateinit var viewModel: ScoreSetterModel
init
{
initialize(attrs, context)
}
private fun initialize(attrs: AttributeSet, context: Context)
{
val view = View.inflate(context, R.layout.view_score_setter, this)
val typedArray = context.theme.obtainStyledAttributes(
attrs,
R.styleable.ScoreSetterView,
0, 0
)
try
{
// I get the crash right here
attributeName = typedArray.getString(R.styleable.ScoreSetterView_attribute_name)!!
baseScore = typedArray.getInteger(R.styleable.ScoreSetterView_attribute_base_score, 0)
bonus = typedArray.getInteger(R.styleable.ScoreSetterView_attribute_bonus, 0)
iconId = typedArray.getInteger(R.styleable.ScoreSetterView_attribute_icon_id, 0)
txtBaseScore.text = baseScore.toString()
txtBonus.text = bonus.toString()
txtFinalScore.text = (baseScore + bonus).toString()
imgAttributeIcon.setImageDrawable(resources.getDrawable(iconId, null))
viewModel = ScoreSetterModel()
viewModel.attributeModifier.value = bonus
viewModel.attributeScore.value = baseScore
viewModel.attributeName.value = attributeName
}
finally
{
typedArray.recycle()
}
}
override fun onLifecycleOwnerAttached(lifecycleOwner: LifecycleOwner)
{
observeLiveData(lifecycleOwner)
}
private fun observeLiveData(lifecycleOwner: LifecycleOwner)
{
viewModel.getLabel().observe(lifecycleOwner, Observer {
})
viewModel.getScore().observe(lifecycleOwner, Observer {
txtFinalScore.text = viewModel.getFinalScore().toString()
})
viewModel.getModifier().observe(lifecycleOwner, Observer {
txtFinalScore.text = viewModel.getFinalScore().toString()
})
}
}
ScoreSetterModel.kt:
class ScoreSetterModel: CustomViewModel<ScoreSetterState>
{
var attributeName = MutableLiveData<String?>()
var attributeScore = MutableLiveData<Int?>()
var attributeModifier = MutableLiveData<Int?>()
var attributeIcon = MutableLiveData<String?>()
override var state: ScoreSetterState? = null
get() = ScoreSetterState(
attributeName.value,
attributeScore.value,
attributeModifier.value,
attributeIcon.value
)
set(value) {
field = value
restore(value)
}
fun getLabel(): LiveData<String?> = attributeName
fun getScore(): LiveData<Int?> = attributeScore
fun getModifier(): LiveData<Int?> = attributeModifier
fun getIcon(): LiveData<String?> = attributeIcon
fun getFinalScore(): Int {
return getScore().value!! + getModifier().value!!
}
private fun restore(state: ScoreSetterState?)
{
attributeName.value = state?.attributeName
attributeScore.value = state?.attributeValue
attributeModifier.value = state?.attributeModifier
attributeIcon.value = state?.attributeIcon
}
}
ScoreSetterState.kt:
@Parcelize
data class ScoreSetterState(
val attributeName: String?,
val attributeValue: Int?,
val attributeModifier: Int?,
val attributeIcon: String?
): CustomViewState
CustomConstraintLayout.kt:
abstract class CustomConstraintLayout<V: CustomViewState, T: CustomViewModel<V>>
: ConstraintLayout, CustomView<V, T>
{
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)
constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr)
override fun onAttachedToWindow()
{
super.onAttachedToWindow()
val lifeCycleOwner = context as? LifecycleOwner ?: throw LifecycleOwnerNotFoundException()
onLifecycleOwnerAttached(lifeCycleOwner)
}
override fun onSaveInstanceState() = CustomViewStateWrapper(super.onSaveInstanceState(), viewModel.state)
@Suppress("UNCHECKED_CAST")
override fun onRestoreInstanceState(state: Parcelable?)
{
if (state is CustomViewStateWrapper)
{
viewModel.state = state.state as V
super.onRestoreInstanceState(state.superState)
}
}
}
CustomView.kt:
interface CustomView<V: CustomViewState, T: CustomViewModel<V>>
{
val viewModel: T
fun onLifecycleOwnerAttached(lifecycleOwner: LifecycleOwner)
}
CustomViewState.kt:
interface CustomViewState: Parcelable
CustomViewModel.kt:
interface CustomViewModel<T: CustomViewState>
{
var state: T?
}