Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #37: Data Binding on Characters Activity #50

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion data/src/main/java/com/architect/coders/mu8/data/DataApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ open class DataApp : Application() {
this,
MU8Database::class.java,
"MU8-db"
).build()
)
.fallbackToDestructiveMigration()
.build()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import com.architect.coders.mu8.data.database.converter.ListStringTypeConverters
import com.architect.coders.mu8.data.database.converter.ThumbnailTypeConverters
import com.architect.coders.mu8.data.database.converter.UrlsTypeConverters

private const val DATABASE_VERSION = 1
private const val DATABASE_VERSION = 2
recaldev marked this conversation as resolved.
Show resolved Hide resolved

@Database(entities = [CharactersEntity::class], version = DATABASE_VERSION)
@TypeConverters(UrlsTypeConverters::class, ThumbnailTypeConverters::class, ListStringTypeConverters::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,4 @@ interface MarvelServiceAPI {
@Query(OFFSET) offset: Int,
@Query(LIMIT) limit: Int
): Response<BaseResponse<EventsResponse>>
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ data class Character(
val thumbnailUrl: String,
val urls: List<Urls> = emptyList(),
val comicIds: List<String> = emptyList()
)
)
4 changes: 4 additions & 0 deletions presentation/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -23,6 +24,9 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
dataBinding{
enabled true
}
}

dependencies {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,63 +1,59 @@
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.CharactersRepositoryImpl
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<Toolbar>(R.id.toolbar) }
private val progress: ProgressBar by lazy { findViewById<ProgressBar>(R.id.progress) }
private val recycler: RecyclerView by lazy { findViewById<RecyclerView>(R.id.recycler) }

private lateinit var viewModel: CharactersViewModel
private lateinit var adapter: CharactersAdapter
private lateinit var binding: ActivityCharactersBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_characters)

setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)

val toolbarTitle = findViewById<TextView>(R.id.toolbar_title)
toolbarTitle.text = getString(R.string.characters_name)
binding = DataBindingUtil.setContentView(this, R.layout.activity_characters)

viewModel = getViewModel { CharactersViewModel(CharactersUseCaseImpl(CharactersRepositoryImpl(application as DataApp))) }
initViewModel()
initToolbar()
initRecycler()

adapter = CharactersAdapter(viewModel::onCharacterClicked)
recycler.layoutManager = LinearLayoutManager(this)
recycler.adapter = adapter
viewModel.navigateToCharacter.observe(this, EventObserver { startActivity<CharactersDetailActivity> {} })
}

viewModel.model.observe(this, Observer(::updateUi))
private fun initViewModel() {
viewModel = getViewModel { CharactersViewModel(CharactersUseCaseImpl(CharactersRepositoryImpl(application as DataApp))) }
binding.viewmodel = viewModel
binding.lifecycleOwner = this
}

private fun updateUi(model: CharactersUiModel) {
recaldev marked this conversation as resolved.
Show resolved Hide resolved
progress.visibility = if (model == Loading) VISIBLE else GONE
private fun initToolbar() {
viewModel.setTitle(getString(R.string.characters_name))
}

when (model) {
is Content -> {
adapter.characters = model.characters
progress.visibility = GONE
}
is Navigation -> startActivity<CharactersDetailActivity> {}
private fun initRecycler() {
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
}
})
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,45 +1,37 @@
package com.architect.coders.mu8.characters

import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.architect.coders.mu8.R
import com.architect.coders.mu8.utils.inflate
import com.architect.coders.mu8.databinding.ViewCharacterBinding
import com.architect.coders.mu8.utils.bindingInflate
import com.architect.coders.mu8.utils.loadUrl
import com.architect.codes.mu8.characters.Character
import kotlin.properties.Delegates

class CharactersAdapter(
private val listener: (Character) -> Unit
) : RecyclerView.Adapter<CharactersAdapter.ViewHolder>() {
class CharactersAdapter(private val listener: (Character) -> Unit) : RecyclerView.Adapter<CharactersAdapter.ViewHolder>() {

var characters: List<Character> by Delegates.observable(
emptyList(),
{ _, _, _ -> notifyDataSetChanged() }
)

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(parent.inflate(R.layout.view_character, false))
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
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)
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,43 @@ package com.architect.coders.mu8.characters

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
import kotlinx.coroutines.launch

class CharactersViewModel(private val charactersUseCase: CharactersUseCase) : ScopedViewModel() {

private val _model = MutableLiveData<CharactersUiModel>()
val model: LiveData<CharactersUiModel>
get() {
if (_model.value == null) getCharacters()
return _model
}
private val _navigateToCharacter = MutableLiveData<Event<Long>>()
val navigateToCharacter: LiveData<Event<Long>> get() = _navigateToCharacter

private val _loading = MutableLiveData<Boolean>()
val loading: LiveData<Boolean> get() = _loading

private val _characters = MutableLiveData<List<Character>>()
val characters: LiveData<List<Character>> get() = _characters

private val _title = MutableLiveData<String>()
val title: LiveData<String> get() = _title

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)
}

fun setTitle(toolbarTitle: String) {
_title.value = toolbarTitle
}
}
Original file line number Diff line number Diff line change
@@ -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?) {
recaldev marked this conversation as resolved.
Show resolved Hide resolved
visibility = visible?.let {
if (visible) View.VISIBLE else View.GONE
} ?: View.GONE
}
44 changes: 44 additions & 0 deletions presentation/src/main/java/com/architect/coders/mu8/utils/Event.kt
Original file line number Diff line number Diff line change
@@ -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<out T>(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<T>(private val onEventUnhandledContent: (T) -> Unit) : Observer<Event<T>> {
override fun onChanged(event: Event<T>?) {
event?.getContentIfNotHandled()?.let { value ->
onEventUnhandledContent(value)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,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
Expand Down Expand Up @@ -57,3 +59,6 @@ inline fun <reified T : ViewModel> FragmentActivity.getViewModel(crossinline fac
}
return ViewModelProviders.of(this, vmFactory)[T::class.java]
}

fun <T : ViewDataBinding> ViewGroup.bindingInflate(@LayoutRes layoutRes: Int, attachToRoot: Boolean = true): T =
DataBindingUtil.inflate(LayoutInflater.from(context), layoutRes, this, attachToRoot)
Loading