From 54b316ec4be93ff4dfde93d89adc897c8cf5355d Mon Sep 17 00:00:00 2001 From: javier Date: Tue, 7 Jan 2020 20:37:15 -0500 Subject: [PATCH] Data Binding on Characters Activity #37 delete charactersUiModel because implemented databinding. Create diferent bindingAdapter for use in xml. Create Event for use navigation. javier.mcardona@gmail.com --- presentation/build.gradle | 4 ++ .../mu8/characters/CharactersActivity.kt | 58 ++++++++---------- .../mu8/characters/CharactersAdapter.kt | 13 ++-- .../characters/CharactersBindingAdapters.kt | 12 ++++ .../mu8/characters/CharactersUiModel.kt | 9 --- .../mu8/characters/CharactersViewModel.kt | 31 +++++++--- .../coders/mu8/utils/BindingAdapters.kt | 11 ++++ .../com/architect/coders/mu8/utils/Event.kt | 44 +++++++++++++ .../mu8/utils/PresentationExtensions.kt | 7 +++ .../main/res/layout/activity_characters.xml | 60 ++++++++++-------- presentation/src/main/res/layout/toolbar.xml | 30 +++++---- .../src/main/res/layout/view_character.xml | 61 +++++++++++-------- 12 files changed, 218 insertions(+), 122 deletions(-) create mode 100644 presentation/src/main/java/com/architect/coders/mu8/characters/CharactersBindingAdapters.kt delete mode 100644 presentation/src/main/java/com/architect/coders/mu8/characters/CharactersUiModel.kt create mode 100644 presentation/src/main/java/com/architect/coders/mu8/utils/BindingAdapters.kt create mode 100644 presentation/src/main/java/com/architect/coders/mu8/utils/Event.kt diff --git a/presentation/build.gradle b/presentation/build.gradle index 6320397..39cf6a6 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -3,6 +3,7 @@ apply plugin: 'com.google.gms.google-services' apply plugin: 'io.fabric' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' apply from: "$rootDir/detekt.gradle" android { @@ -23,6 +24,9 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + dataBinding{ + enabled true + } } dependencies { diff --git a/presentation/src/main/java/com/architect/coders/mu8/characters/CharactersActivity.kt b/presentation/src/main/java/com/architect/coders/mu8/characters/CharactersActivity.kt index f8704e5..1d0cc2d 100644 --- a/presentation/src/main/java/com/architect/coders/mu8/characters/CharactersActivity.kt +++ b/presentation/src/main/java/com/architect/coders/mu8/characters/CharactersActivity.kt @@ -1,46 +1,32 @@ package com.architect.coders.mu8.characters import android.os.Bundle -import android.view.View.GONE -import android.view.View.VISIBLE -import android.widget.ProgressBar -import android.widget.TextView import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.widget.Toolbar +import androidx.databinding.DataBindingUtil import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView import com.architect.coders.mu8.R -import com.architect.coders.mu8.characters.CharactersUiModel.Content -import com.architect.coders.mu8.characters.CharactersUiModel.Loading -import com.architect.coders.mu8.characters.CharactersUiModel.Navigation import com.architect.coders.mu8.characters.detail.CharactersDetailActivity import com.architect.coders.mu8.data.DataApp import com.architect.coders.mu8.data.characters.CharactersMapper import com.architect.coders.mu8.data.characters.CharactersRepositoryImpl import com.architect.coders.mu8.data.mapper.common.UrlsMapper +import com.architect.coders.mu8.databinding.ActivityCharactersBinding +import com.architect.coders.mu8.utils.EventObserver import com.architect.coders.mu8.utils.getViewModel import com.architect.coders.mu8.utils.startActivity import com.architect.codes.mu8.characters.CharactersUseCaseImpl class CharactersActivity : AppCompatActivity() { - private val toolbar: Toolbar by lazy { findViewById(R.id.toolbar) } - private val progress: ProgressBar by lazy { findViewById(R.id.progress) } - private val recycler: RecyclerView by lazy { findViewById(R.id.recycler) } - private lateinit var viewModel: CharactersViewModel private lateinit var adapter: CharactersAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_characters) - - setSupportActionBar(toolbar) - supportActionBar?.setDisplayShowTitleEnabled(false) - val toolbarTitle = findViewById(R.id.toolbar_title) - toolbarTitle.text = getString(R.string.characters_name) + val binding: ActivityCharactersBinding = + DataBindingUtil.setContentView(this, R.layout.activity_characters) viewModel = getViewModel { CharactersViewModel( @@ -50,22 +36,28 @@ class CharactersActivity : AppCompatActivity() { ) } - adapter = CharactersAdapter(viewModel::onCharacterClicked) - recycler.layoutManager = LinearLayoutManager(this) - recycler.adapter = adapter + binding.viewmodel = viewModel + binding.lifecycleOwner = this - viewModel.model.observe(this, Observer(::updateUi)) - } - - private fun updateUi(model: CharactersUiModel) { - progress.visibility = if (model == Loading) VISIBLE else GONE + setSupportActionBar(binding.toolbar.toolbarWidget) + supportActionBar?.setDisplayShowTitleEnabled(false) + binding.toolbar.toolbarTitle.text = getString(R.string.characters_name) - when (model) { - is Content -> { - adapter.characters = model.characters - progress.visibility = GONE - } - is Navigation -> startActivity {} + adapter = CharactersAdapter(viewModel::onCharacterClicked) + binding.recycler.layoutManager = LinearLayoutManager(this) + binding.recycler.adapter = adapter + + with(viewModel.characters){ + observe(this@CharactersActivity, Observer { + value?.let { + adapter.characters = it + } + }) } + + viewModel.navigateToCharacter.observe( + this, + EventObserver { startActivity {} } + ) } } diff --git a/presentation/src/main/java/com/architect/coders/mu8/characters/CharactersAdapter.kt b/presentation/src/main/java/com/architect/coders/mu8/characters/CharactersAdapter.kt index 08fa4c8..87d7999 100644 --- a/presentation/src/main/java/com/architect/coders/mu8/characters/CharactersAdapter.kt +++ b/presentation/src/main/java/com/architect/coders/mu8/characters/CharactersAdapter.kt @@ -6,6 +6,8 @@ import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.architect.coders.mu8.R +import com.architect.coders.mu8.databinding.ViewCharacterBinding +import com.architect.coders.mu8.utils.bindingInflate import com.architect.coders.mu8.utils.inflate import com.architect.coders.mu8.utils.loadUrl import com.architect.codes.mu8.characters.Character @@ -21,25 +23,22 @@ class CharactersAdapter( ) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - return ViewHolder(parent.inflate(R.layout.view_character, false)) + return ViewHolder(parent.bindingInflate(R.layout.view_character, false)) } override fun getItemCount(): Int = characters.size override fun onBindViewHolder(holder: ViewHolder, position: Int) { val character = characters[position] + holder.dataBinding.character = character holder.bind(character) holder.itemView.setOnClickListener { listener(character) } } - class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { - - private var characterPicture: ImageView = itemView.findViewById(R.id.image_character_picture) - private var characterName: TextView = itemView.findViewById(R.id.text_character_name) + class ViewHolder(val dataBinding: ViewCharacterBinding) : RecyclerView.ViewHolder(dataBinding.root) { fun bind(character: Character) { - characterPicture.loadUrl(character.thumbnailUrl) - characterName.text = character.name + dataBinding.imageCharacterPicture.loadUrl(character.thumbnailUrl) } } } diff --git a/presentation/src/main/java/com/architect/coders/mu8/characters/CharactersBindingAdapters.kt b/presentation/src/main/java/com/architect/coders/mu8/characters/CharactersBindingAdapters.kt new file mode 100644 index 0000000..3d9589b --- /dev/null +++ b/presentation/src/main/java/com/architect/coders/mu8/characters/CharactersBindingAdapters.kt @@ -0,0 +1,12 @@ +package com.architect.coders.mu8.characters + +import androidx.databinding.BindingAdapter +import androidx.recyclerview.widget.RecyclerView +import com.architect.codes.mu8.characters.Character + +@BindingAdapter("items") +fun RecyclerView.setItems(characters: List?) { + (adapter as? CharactersAdapter)?.let { + it.characters = characters ?: emptyList() + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/architect/coders/mu8/characters/CharactersUiModel.kt b/presentation/src/main/java/com/architect/coders/mu8/characters/CharactersUiModel.kt deleted file mode 100644 index 7e1358d..0000000 --- a/presentation/src/main/java/com/architect/coders/mu8/characters/CharactersUiModel.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.architect.coders.mu8.characters - -import com.architect.codes.mu8.characters.Character - -sealed class CharactersUiModel { - object Loading : CharactersUiModel() - class Content(val characters: List) : CharactersUiModel() - class Navigation(val character: Character) : CharactersUiModel() -} diff --git a/presentation/src/main/java/com/architect/coders/mu8/characters/CharactersViewModel.kt b/presentation/src/main/java/com/architect/coders/mu8/characters/CharactersViewModel.kt index 82adfde..235e658 100644 --- a/presentation/src/main/java/com/architect/coders/mu8/characters/CharactersViewModel.kt +++ b/presentation/src/main/java/com/architect/coders/mu8/characters/CharactersViewModel.kt @@ -1,7 +1,11 @@ package com.architect.coders.mu8.characters +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import com.architect.coders.mu8.utils.Event import com.architect.coders.mu8.utils.ScopedViewModel import com.architect.codes.mu8.characters.Character import com.architect.codes.mu8.characters.CharactersUseCase @@ -9,19 +13,28 @@ import kotlinx.coroutines.launch class CharactersViewModel(private val charactersUseCase: CharactersUseCase) : ScopedViewModel() { - private val _model = MutableLiveData() - val model: LiveData - get() { - if (_model.value == null) getCharacters() - return _model - } + private val _navigateToCharacter = MutableLiveData>() + val navigateToCharacter: LiveData> get() = _navigateToCharacter + + private val _loading = MutableLiveData() + val loading: LiveData get() = _loading + + private val _characters = MutableLiveData>() + val characters: LiveData> get() = _characters + + init { + getCharacters() + } private fun getCharacters() { launch { - _model.value = CharactersUiModel.Loading - _model.value = CharactersUiModel.Content(charactersUseCase()) + _loading.value = true + _characters.value = charactersUseCase() + _loading.value = false } } - fun onCharacterClicked(character: Character) = Unit + fun onCharacterClicked(character: Character) { + _navigateToCharacter.value = Event(character.id) + } } diff --git a/presentation/src/main/java/com/architect/coders/mu8/utils/BindingAdapters.kt b/presentation/src/main/java/com/architect/coders/mu8/utils/BindingAdapters.kt new file mode 100644 index 0000000..761b091 --- /dev/null +++ b/presentation/src/main/java/com/architect/coders/mu8/utils/BindingAdapters.kt @@ -0,0 +1,11 @@ +package com.architect.coders.mu8.utils + +import android.view.View +import androidx.databinding.BindingAdapter + +@BindingAdapter("visible") +fun View.setVisible(visible: Boolean?) { + visibility = visible?.let { + if (visible) View.VISIBLE else View.GONE + } ?: View.GONE +} \ No newline at end of file diff --git a/presentation/src/main/java/com/architect/coders/mu8/utils/Event.kt b/presentation/src/main/java/com/architect/coders/mu8/utils/Event.kt new file mode 100644 index 0000000..2456e2b --- /dev/null +++ b/presentation/src/main/java/com/architect/coders/mu8/utils/Event.kt @@ -0,0 +1,44 @@ +package com.architect.coders.mu8.utils + +import androidx.lifecycle.Observer + +/** + * Used as a wrapper for data that is exposed via a LiveData that represents an event. + * Taken from: https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150 + */ +open class Event(private val content: T) { + + var hasBeenHandled = false + private set // Allow external read but not write + + /** + * Returns the content and prevents its use again. + */ + fun getContentIfNotHandled(): T? { + return if (hasBeenHandled) { + null + } else { + hasBeenHandled = true + content + } + } + + /** + * Returns the content, even if it's already been handled. + */ + fun peekContent(): T = content +} + +/** + * An [Observer] for [Event]s, simplifying the pattern of checking if the [Event]'s content has + * already been handled. + * + * [onEventUnhandledContent] is *only* called if the [Event]'s contents has not been handled. + */ +class EventObserver(private val onEventUnhandledContent: (T) -> Unit) : Observer> { + override fun onChanged(event: Event?) { + event?.getContentIfNotHandled()?.let { value -> + onEventUnhandledContent(value) + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/architect/coders/mu8/utils/PresentationExtensions.kt b/presentation/src/main/java/com/architect/coders/mu8/utils/PresentationExtensions.kt index 5f039e3..5efbd31 100644 --- a/presentation/src/main/java/com/architect/coders/mu8/utils/PresentationExtensions.kt +++ b/presentation/src/main/java/com/architect/coders/mu8/utils/PresentationExtensions.kt @@ -10,6 +10,8 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import androidx.annotation.LayoutRes +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding import androidx.fragment.app.FragmentActivity import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -64,3 +66,8 @@ inline fun FragmentActivity.getViewModel(crossinline fac } return ViewModelProviders.of(this, vmFactory)[T::class.java] } + +fun ViewGroup.bindingInflate( + @LayoutRes layoutRes: Int, + attachToRoot: Boolean = true +): T = DataBindingUtil.inflate(LayoutInflater.from(context), layoutRes, this, attachToRoot) diff --git a/presentation/src/main/res/layout/activity_characters.xml b/presentation/src/main/res/layout/activity_characters.xml index 640c9a6..7b5bcd1 100644 --- a/presentation/src/main/res/layout/activity_characters.xml +++ b/presentation/src/main/res/layout/activity_characters.xml @@ -1,30 +1,40 @@ - + xmlns:tools="http://schemas.android.com/tools"> - + + + - + - - \ No newline at end of file + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/toolbar.xml b/presentation/src/main/res/layout/toolbar.xml index ee12264..dcad945 100644 --- a/presentation/src/main/res/layout/toolbar.xml +++ b/presentation/src/main/res/layout/toolbar.xml @@ -1,16 +1,20 @@ - + - - \ No newline at end of file + android:background="?attr/colorPrimary" + android:minHeight="?attr/actionBarSize" + android:theme="?attr/actionBarTheme"> + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/view_character.xml b/presentation/src/main/res/layout/view_character.xml index faf5657..8227d30 100644 --- a/presentation/src/main/res/layout/view_character.xml +++ b/presentation/src/main/res/layout/view_character.xml @@ -1,31 +1,40 @@ - + xmlns:tools="http://schemas.android.com/tools"> - + + + + + android:layout_height="wrap_content"> + + - - \ No newline at end of file + + + \ No newline at end of file