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

Fixes #166: Story activity UI structure #195

Merged
merged 51 commits into from
Nov 7, 2019
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
0e9eaea
Introduce a generic data-binding-enabled RecyclerView adapter. No tests
BenHenning Sep 20, 2019
0ae1439
Initial structure commit
hareshkh Sep 29, 2019
14c8633
Minor changes
hareshkh Oct 2, 2019
2df7459
Merges develop
hareshkh Oct 8, 2019
446df70
Adds story fragment layout
hareshkh Oct 10, 2019
fa10f3b
Passes storyId to fragment presenter
hareshkh Oct 10, 2019
8bb838a
Passes storyId to ViewModel
hareshkh Oct 11, 2019
770fdb0
Adds code to retrieve story live data
hareshkh Oct 11, 2019
39fca77
Merges branch 'develop'
hareshkh Oct 12, 2019
01b629d
Adds button in home-activity and back button in action bar
hareshkh Oct 12, 2019
75307df
Sets story title in action bar
hareshkh Oct 12, 2019
8d6c3a7
Adds chapter summary
Oct 15, 2019
9d70736
Adds chapter list to viewModel
hareshkh Oct 15, 2019
62afbf4
Adds chapter list item
hareshkh Oct 15, 2019
3f5e390
Adds recyclerview
hareshkh Oct 17, 2019
cbfadb1
Adds placeholder thumbnail view
hareshkh Oct 18, 2019
32f468c
Merges branch develop
hareshkh Oct 18, 2019
62362f5
Adds chapter progress
hareshkh Oct 18, 2019
3ecbafb
Converts string concat
hareshkh Oct 21, 2019
34c300b
Shows story thumbnail
hareshkh Oct 21, 2019
8f82ff6
Merge branch develop
hareshkh Oct 22, 2019
3a56b6e
Restores .idea
hareshkh Oct 23, 2019
9fb9f33
Merges develop
hareshkh Oct 23, 2019
503817c
Cosmetic changes
hareshkh Oct 23, 2019
77d7ca8
Cosmetic changes to StoryViewModel
hareshkh Oct 23, 2019
0c99161
Newline at end of each file
hareshkh Oct 23, 2019
3bae028
Adds fragment only when it is not there
hareshkh Oct 30, 2019
5504af6
Removes NPE
hareshkh Oct 30, 2019
dbefd4f
Moves chapter count logic to viewModel and string to plurals
hareshkh Oct 30, 2019
fb4b815
Merges branch develop
hareshkh Oct 30, 2019
20bfdb9
Adds StoryItemViewModel and changes to recyclerViewAdapter
hareshkh Oct 31, 2019
43d84a8
Adds two viewTypes to recycler adapter
hareshkh Oct 31, 2019
207f28d
Newlines at end of files
hareshkh Oct 31, 2019
aefd855
Merge branch 'develop'
hareshkh Oct 31, 2019
d23bf1d
Minor changes to BindableAdapterTest
hareshkh Oct 31, 2019
988a37d
Adds test
hareshkh Nov 1, 2019
7b6704e
Adds routing to exploration activity
hareshkh Nov 3, 2019
70d7a16
Adds exploration loading test
hareshkh Nov 3, 2019
c3d075d
Some review comment changes
hareshkh Nov 3, 2019
1d3fa6c
Minor cosmetic changes
hareshkh Nov 3, 2019
f9ae447
Listeners to invoke ExplorationActivity
hareshkh Nov 5, 2019
c51b394
StoryId can be null
hareshkh Nov 5, 2019
d94c543
Merge branch 'develop'
hareshkh Nov 6, 2019
b05e786
Remove unrequired changes
hareshkh Nov 6, 2019
863728d
Merge branch 'develop' into story-activity-ui-structure
BenHenning Nov 6, 2019
da81a13
Merge branch 'story-activity-ui-structure' of github.com:oppia/oppia-…
BenHenning Nov 6, 2019
12886ec
Undo small accidental edits to home fragment/presenter post merge.
BenHenning Nov 6, 2019
8e1253a
Address reviewer comments (including introducing a test activity for
BenHenning Nov 6, 2019
4a3c07b
Add checkNotNull check in test activity to help quick-fail tests if they
BenHenning Nov 6, 2019
8219df2
Removes unrequired gradle change
hareshkh Nov 6, 2019
d296bd4
Add intent extras check in test
hareshkh Nov 6, 2019
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
2 changes: 2 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ dependencies {
'androidx.test:core:1.2.0',
'androidx.test.espresso:espresso-core:3.2.0',
'androidx.test.espresso:espresso-intents:3.1.0',
'com.android.support.test.espresso:espresso-contrib:3.1.0',
hareshkh marked this conversation as resolved.
Show resolved Hide resolved
'androidx.test.ext:junit:1.1.1',
'com.google.truth:truth:0.43',
'org.robolectric:robolectric:4.3',
Expand All @@ -84,6 +85,7 @@ dependencies {
'androidx.test:core:1.2.0',
'androidx.test.espresso:espresso-core:3.2.0',
'androidx.test.espresso:espresso-intents:3.1.0',
'com.android.support.test.espresso:espresso-contrib:3.1.0',
hareshkh marked this conversation as resolved.
Show resolved Hide resolved
'androidx.test.ext:junit:1.1.1',
'androidx.test:runner:1.2.0',
'com.google.truth:truth:0.43',
Expand Down
16 changes: 8 additions & 8 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,19 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/OppiaTheme">
<activity android:name=".topic.conceptcard.testing.ConceptCardFragmentTestActivity" />
<activity android:name=".player.state.testing.StateFragmentTestActivity" />
<activity android:name="org.oppia.app.topic.conceptcard.testing.ConceptCardFragmentTestActivity" />
hareshkh marked this conversation as resolved.
Show resolved Hide resolved
<activity android:name="org.oppia.app.player.state.testing.StateFragmentTestActivity" />
<activity android:name=".player.exploration.ExplorationActivity" />
<activity android:name=".topic.questionplayer.QuestionPlayerActivity" />
<activity android:name=".topic.TopicActivity" />
<activity android:name=".player.audio.testing.AudioFragmentTestActivity" />
<activity android:name=".testing.InputInteractionViewTestActivity" />
<activity android:name="org.oppia.app.home.HomeActivity" />
<activity android:name=".profile.ProfileActivity" />
<activity android:name=".story.StoryActivity" />
<activity android:name=".home.HomeActivity">
<activity android:name=".testing.InputInteractionViewTestActivity" />
<activity android:name=".story.StoryActivity"/>
<activity
android:name=".splash.SplashActivity"
hareshkh marked this conversation as resolved.
Show resolved Hide resolved
android:theme="@style/SplashScreenTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
Expand All @@ -30,9 +33,6 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name=".splash.SplashActivity"
android:theme="@style/SplashScreenTheme" />
<activity android:name=".testing.HtmlParserTestActivity" />
<activity android:name=".testing.BindableAdapterTestActivity" />
</application>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import org.oppia.app.fragment.FragmentComponent
import org.oppia.app.home.HomeActivity
import org.oppia.app.player.audio.testing.AudioFragmentTestActivity
import org.oppia.app.player.exploration.ExplorationActivity
import org.oppia.app.topic.conceptcard.testing.ConceptCardFragmentTestActivity
import org.oppia.app.player.state.testing.StateFragmentTestActivity
import org.oppia.app.profile.ProfileActivity
import org.oppia.app.story.StoryActivity
import org.oppia.app.testing.BindableAdapterTestActivity
import org.oppia.app.testing.HtmlParserTestActivity
import org.oppia.app.topic.TopicActivity
import org.oppia.app.topic.conceptcard.testing.ConceptCardFragmentTestActivity
import org.oppia.app.topic.questionplayer.QuestionPlayerActivity
import javax.inject.Provider

Expand All @@ -23,7 +23,9 @@ import javax.inject.Provider
interface ActivityComponent {
@Subcomponent.Builder
interface Builder {
@BindsInstance fun setActivity(appCompatActivity: AppCompatActivity): Builder
@BindsInstance
fun setActivity(appCompatActivity: AppCompatActivity): Builder

fun build(): ActivityComponent
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.oppia.app.databinding

import android.graphics.drawable.GradientDrawable
import android.view.View
import androidx.annotation.ColorInt
import androidx.databinding.BindingAdapter
import org.oppia.app.R

/** Used to set a rounded-rect background drawable with a data-bound color. */
@BindingAdapter("app:roundedRectDrawableWithColor")
fun setBackgroundDrawable(view: View, @ColorInt colorRgb: Int) {
view.setBackgroundResource(R.drawable.rounded_rect_background)
// The input color needs to have alpha channel prepended to it.
(view.background as GradientDrawable).setColor((0xff000000 or colorRgb.toLong()).toInt())
}
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import androidx.fragment.app.Fragment
import dagger.BindsInstance
import dagger.Subcomponent
import org.oppia.app.home.HomeFragment
import org.oppia.app.player.audio.AudioFragment
import org.oppia.app.player.exploration.ExplorationFragment
import org.oppia.app.player.state.StateFragment
import org.oppia.app.player.audio.AudioFragment
import org.oppia.app.profile.AddProfileFragment
import org.oppia.app.profile.AdminAuthFragment
import org.oppia.app.profile.ProfileChooserFragment
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/java/org/oppia/app/home/HomeFragmentPresenter.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
package org.oppia.app.home

import android.content.Context
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat.startActivity
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import org.oppia.app.databinding.HomeFragmentBinding
import org.oppia.app.fragment.FragmentScope
import org.oppia.app.story.StoryActivity
import org.oppia.app.viewmodel.ViewModelProvider
import org.oppia.domain.UserAppHistoryController
import org.oppia.domain.exploration.ExplorationDataController
import org.oppia.domain.exploration.TEST_EXPLORATION_ID_5
import org.oppia.domain.topic.TEST_STORY_ID_1
import org.oppia.util.data.AsyncResult
import org.oppia.util.logging.Logger
import javax.inject.Inject
Expand Down Expand Up @@ -64,4 +69,10 @@ class HomeFragmentPresenter @Inject constructor(
}
})
}

// TODO(#134): Remove this method once it is possible to navigate to story activity in normal flow
BenHenning marked this conversation as resolved.
Show resolved Hide resolved
fun openStory(v: View) {
hareshkh marked this conversation as resolved.
Show resolved Hide resolved
val intent = StoryActivity.createStoryActivityIntent(fragment.activity as Context, TEST_STORY_ID_1)
fragment.activity?.startActivity(intent)
}
}
25 changes: 20 additions & 5 deletions app/src/main/java/org/oppia/app/recyclerview/BindableAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class BindableAdapter<T : Any> internal constructor(
// when T1 is not assignable to T2). This likely won't have bad side effects since any time a
// non-empty list is attempted to be bound, this crash will be correctly triggered.
newDataList.firstOrNull()?.let {
check(it.javaClass.isAssignableFrom(dataClassType.java)) {
check(dataClassType.java.isAssignableFrom(it.javaClass)) {
"Trying to bind incompatible data to adapter. Data class type: ${it.javaClass}, " +
"expected adapter class type: $dataClassType."
}
Expand Down Expand Up @@ -148,6 +148,18 @@ class BindableAdapter<T : Any> internal constructor(
return this
}

/** See [registerViewDataBinder]. */
fun <DB : ViewDataBinding> registerViewDataBinderWithSameModelType(
viewType: Int = DEFAULT_VIEW_TYPE,
inflateDataBinding: (LayoutInflater, ViewGroup, Boolean) -> DB,
setViewModel: (DB, T) -> Unit
): Builder<T> {
return registerViewDataBinder(
viewType = viewType, inflateDataBinding = inflateDataBinding, setViewModel = setViewModel,
transformViewModel = { it }
)
}

/**
* Behaves in the same way as [registerViewBinder] except the inflate and bind methods correspond to a [View]
* data-binding typed [DB].
Expand All @@ -158,12 +170,15 @@ class BindableAdapter<T : Any> internal constructor(
* @param setViewModel a function that initializes the view model in the data-bound view (e.g.
* MyDataBinding::setSpecialViewModel). This may also be a function that initializes the view model & other
* view-accessible properties as necessary.
* @param transformViewModel a function that converts the input model to a model of another type (such as for cases
* when subclassing is used to represent more complex lists of data).
* @return this
*/
fun <DB : ViewDataBinding> registerViewDataBinder(
fun <DB : ViewDataBinding, T2 : T> registerViewDataBinder(
viewType: Int = DEFAULT_VIEW_TYPE,
inflateDataBinding: (LayoutInflater, ViewGroup, Boolean) -> DB,
setViewModel: (DB, T) -> Unit
setViewModel: (DB, T2) -> Unit,
transformViewModel: (T) -> T2
): Builder<T> {
checkViewTypeIsUnique(viewType)
val viewHolderFactory: ViewHolderFactory<T> = { viewGroup ->
Expand All @@ -176,7 +191,7 @@ class BindableAdapter<T : Any> internal constructor(
)
object : BindableViewHolder<T>(binding.root) {
override fun bind(data: T) {
setViewModel(binding, data)
setViewModel(binding, transformViewModel(data))
}
}
}
Expand Down Expand Up @@ -205,4 +220,4 @@ class BindableAdapter<T : Any> internal constructor(
}
}
}
}
}
hareshkh marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.oppia.app.recyclerview

import androidx.databinding.BindingAdapter
import androidx.databinding.ObservableList
import androidx.lifecycle.LiveData
import androidx.recyclerview.widget.RecyclerView

Expand All @@ -10,11 +11,27 @@ import androidx.recyclerview.widget.RecyclerView
* https://android.jlelse.eu/1bd08b4796b4.
*/
@BindingAdapter("data")
fun <T : Any> bindToRecyclerViewAdapter(recyclerView: RecyclerView, liveData: LiveData<List<T>>) {
fun <T : Any> bindToRecyclerViewAdapterWithLiveData(
recyclerView: RecyclerView,
liveData: LiveData<List<T>>
) {
liveData.value?.let { data ->
val adapter = recyclerView.adapter
checkNotNull(adapter) { "Cannot bind data to a RecyclerView missing its adapter." }
check(adapter is BindableAdapter<*>) { "Can only bind data to a BindableAdapter." }
adapter.setDataUnchecked(data)
bindToRecyclerViewAdapter(recyclerView, data)
}
}

/** A variant of [bindToRecyclerViewAdapterWithLiveData] that instead uses an observable list. */
@BindingAdapter("data")
fun <T : Any> bindToRecyclerViewAdapterWithObservableList(
hareshkh marked this conversation as resolved.
Show resolved Hide resolved
recyclerView: RecyclerView,
dataList: ObservableList<T>
) {
bindToRecyclerViewAdapter(recyclerView, dataList)
}

private fun <T : Any> bindToRecyclerViewAdapter(recyclerView: RecyclerView, dataList: List<T>) {
BenHenning marked this conversation as resolved.
Show resolved Hide resolved
val adapter = recyclerView.adapter
checkNotNull(adapter) { "Cannot bind data to a RecyclerView missing its adapter." }
check(adapter is BindableAdapter<*>) { "Can only bind data to a BindableAdapter." }
adapter.setDataUnchecked(dataList)
}
17 changes: 16 additions & 1 deletion app/src/main/java/org/oppia/app/story/StoryActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,41 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import org.oppia.app.activity.InjectableAppCompatActivity
import org.oppia.app.player.exploration.ExplorationActivity
import javax.inject.Inject

/** Activity for stories. */
class StoryActivity : InjectableAppCompatActivity() {
@Inject lateinit var storyActivityPresenter: StoryActivityPresenter

init {
instance = this
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activityComponent.inject(this)
storyActivityPresenter.handleOnCreate()
val storyId = intent.getStringExtra(STORY_ACTIVITY_STORY_ID_ARGUMENT_KEY)
storyActivityPresenter.handleOnCreate(storyId)
}

override fun onSupportNavigateUp(): Boolean {
return storyActivityPresenter.handleOnSupportNavigationUp()
}

companion object {
const val STORY_ACTIVITY_STORY_ID_ARGUMENT_KEY = "StoryActivity.story_id"
private var instance: StoryActivity? = null
hareshkh marked this conversation as resolved.
Show resolved Hide resolved

/** Returns a new [Intent] to route to [StoryActivity] for a specified story ID. */
fun createStoryActivityIntent(context: Context, storyId: String): Intent {
val intent = Intent(context, StoryActivity::class.java)
intent.putExtra(STORY_ACTIVITY_STORY_ID_ARGUMENT_KEY, storyId)
return intent
}

fun routeToExploration(explorationId: String) {
hareshkh marked this conversation as resolved.
Show resolved Hide resolved
instance?.startActivity(ExplorationActivity.createExplorationActivityIntent(instance!!, explorationId))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,22 @@ import javax.inject.Inject
/** The presenter for [StoryActivity]. */
@ActivityScope
class StoryActivityPresenter @Inject constructor(private val activity: AppCompatActivity) {
fun handleOnCreate() {
fun handleOnCreate(storyId: String) {
BenHenning marked this conversation as resolved.
Show resolved Hide resolved
activity.setContentView(R.layout.story_activity)
activity.supportActionBar?.setDisplayHomeAsUpEnabled(true)
if (getStoryFragment() == null) {
activity.supportFragmentManager.beginTransaction().add(
R.id.story_fragment_placeholder,
StoryFragment()
StoryFragment.newInstace(storyId)
).commitNow()
}
}

fun handleOnSupportNavigationUp(): Boolean {
activity.finish()
return true
}

private fun getStoryFragment(): StoryFragment? {
return activity.supportFragmentManager.findFragmentById(R.id.story_fragment_placeholder) as StoryFragment?
}
Expand Down
27 changes: 23 additions & 4 deletions app/src/main/java/org/oppia/app/story/StoryFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,36 @@ import android.view.ViewGroup
import org.oppia.app.fragment.InjectableFragment
import javax.inject.Inject

/** Fragment that displays story with chapter list. */
private const val KEY_STORY_ID = "STORY_ID"
BenHenning marked this conversation as resolved.
Show resolved Hide resolved

/** Fragment for displaying a story. */
class StoryFragment : InjectableFragment() {
@Inject
lateinit var storyFragmentPresenter: StoryFragmentPresenter

companion object {
/**
* Creates a new instance of story fragment.
hareshkh marked this conversation as resolved.
Show resolved Hide resolved
* @param storyId Used in TopicController to get correct story information.
* @return [StoryFragment]
*/
fun newInstace(storyId: String): StoryFragment {
val storyFragment = StoryFragment()
val args = Bundle()
args.putString(KEY_STORY_ID, storyId)
storyFragment.arguments = args
return storyFragment
}
}

@Inject lateinit var storyFragmentPresenter: StoryFragmentPresenter
hareshkh marked this conversation as resolved.
Show resolved Hide resolved

override fun onAttach(context: Context?) {
super.onAttach(context)
fragmentComponent.inject(this)
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return storyFragmentPresenter.handleCreateView(inflater, container)
val args = checkNotNull(arguments) { "Expected arguments to be passed to StoryFragment" }
val storyId = checkNotNull(args.getString(KEY_STORY_ID)) { "Expected storyId to be passed to StoryFragment" }
return storyFragmentPresenter.handleCreateView(inflater, container, storyId)
}
}
Loading