Android Jetpack Compose - Dynamic layouts - android

Background
I'm currently investigating options for creating layouts, during the development of the project I'm looking to possibly migrate the UI to Jetpack Compose or post release, depending on the stability/flexibility of the library.
Part of the project will be using a server driven UI. However the twist being the UI is not known ahead of time and will be dynamic (server and data driven).
I have no issues with handling the business logic and presentation layers however when it comes to the UI I will need a requirement to dynamically build the UI based on the presentation data and view models.
TL;DR
With this in mind is it possible to create dynamic layouts (not to be confused with dynamic layout data) using Jetpack Compose?
As a minimal example, with traditional View and ViewGroup this can easily be achieved :
class DynamicViewActivity : AppCompatActivity() {
private lateinit var root : LinearLayout
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
// setup view group container
root = LinearLayout(this)
root.orientation = LinearLayout.VERTICAL
root.layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT)
setContentView(root, LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT))
// some lookup to create a dynamic layout
val children : List<Pair<View, LinearLayout.LayoutParams>> = getChildren(someArgs)
// add child views
children.forEach { (view, params) -> root.addView(view, params) }
}
fun <T : View> addViewToRoot(view: T, params: LinearLayout.LayoutParams) {
root.addView(view, params)
}
fun removeFromRoot(viewTag : String) {
root.findViewWithTag<View>(viewTag)?.let(root::removeView)
}
}
How do you do the same with Jetpack Compose?
Update
Following the answer from #CommonsWare I implemented the UI in Compose. As my actual code has a very thin UI layer, with all listeners and events using one and two-way data binding, and "unknowns" in the answer already addressed in my project it was incredibly easy to just swap the UI over.
Having said that I soon realised that simple things like ScrollView and View::tooltipText do not yet exist in Compose. Also there is no easy way to have layouts based on runtime configuration (screen orientation / screen bucket size etc) in comparison to xml layouts/resources. This means for me, using data binding with all the rich View framework and libraries is still the better solution.
Looking forward to Compose library updates and maybe look at some point in the future.

With this in mind is it possible to create dynamic layouts (not to be confused with dynamic layout data) using Jetpack Compose?
Sure. Compose is all functions. You can parse data and call functions based on that data, whether that data is "fill in this pre-defined UI structure" or that data is "define the UI structure".
For example, suppose your server has an endpoint that returns the following JSON:
[
{
"element": "label",
"attributes": {
// values omitted for brevity
}
},
{
"element": "field",
"attributes": {
// values omitted for brevity
}
},
// additional elements omitted for brevity
]
Your job is to assemble a UI based on that JSON. A label element should be fixed text, a field element should be a text entry field, and so on for various types. The attributes object contains details that vary by element.
So, you parse that. Suppose that you wind up with a List<UiElement> as a result, where UiElement is an interface or abstract class or something, with sub-types based upon the supported elements (e.g., LabelElement, FieldElement). Now your job is to construct a UI based on that List<UiElement>.
In View-space, you could have a function that creates a View based on a supplied UiElement:
fun buildView(element: UiElement) = when (element) {
is LabelElement -> buildTextView(element)
is FieldElement -> buildEditText(element)
else -> TODO("add other element cases here")
}
buildTextView() would assemble a TextView, whether inflating from a layout or calling a constructor. buildEditText() would assemble an EditText, whether inflating from a layout or calling a constructor. And so on. Each of those functions would be responsible for grabbing values out of the attributes and doing something useful with them, such as setting the text in a TextView or the hint in an EditText.
In the code snippet in your question, rather than your getChildren()-and-loop approach, you would iterate over the List<UiElement> and call buildView() for each UiElement in the list, and adding the result to your LinearLayout.
The Compose equivalent would be something like this:
#Composable
fun buildNode(element: UiElement) {
when (element) {
is LabelElement -> buildTextNode(element)
is FieldElement -> buildTextFieldNode(element)
else -> TODO("add other element cases here")
}
}
IOW, it would be nearly identical. The key differences are:
the #Composable annotation (also required on buildTextNode() and buildTextFieldNode())
no need to return anything, as composables are added automatically to the parent
the details of what goes in buildTextNode() and buildTextFieldNode() would be reminiscent of buildTextView() and buildEditText(), but based on composables
Your activity would have something like this:
Column {
uiElements.forEach { buildNode(it) }
}
...as a replacement for your LinearLayout.
(in reality, both examples would need a scrolling container, but we'll ignore that here as well)
All of the complexity of server-defined UI lies outside of the scope of your code sample:
How do I parse the server response?
How do I map that server response into an object model representing the desired UI?
How do I get my per-element UI bits to work?
How do I handle event listeners?
More generally, what are we doing in response to user input on this UI?
How will we re-generate this UI as needed (for views, based on configuration changes; for composables, based on recomposition)?
And so on
Some of that will be identical between a View-based UI and a Compose-based UI — JSON parsing, for example. Some of that will be substantially different, such as handling user input.
But the general approach of "parse the server response and create UI elements based on that response", views and composables are equally up to the challenge. In particular, at the level of the code sample in your question, views and composables can both handle your high-level scenario. The devil is in the details.

Related

Other state management options that I may use with Jetpack Compose, beyond State hoisting?

I am learning Jetpack compose, and I have been seen so far that lifting the state up to a composable's caller to make a composable stateless is the way to go. I`ve been using this pattern in my Compose apps.
For an app state that I need to modify from many different places of the tree, I will have to pass around a lot of callbacks, This can become difficult to manage.
I have some previous experience with Flutter. The way Flutter deals with providing a state to its descendants in the tree to overcome the above, is to use other mechanisms to manage state, namely Provider + ChangeNotifier.
Basically, with Provider, a Provider Widget is placed in the tree and all the children of the provider will have access to the values exposed by it.
Are there any other mechanisms to manage state in Jetpack Compose apps, besides State hoisting? And, what would you recommend?
If you need to share some data between views you can use view models.
#Composable
fun TestScreen() {
val viewModel = viewModel<SomeViewModel>()
Column {
Text("TestScreen text: ${viewModel.state}")
OtherView()
}
}
#Composable
fun OtherView() {
val viewModel = viewModel<SomeViewModel>()
Text("OtherScreen text: ${viewModel.state}")
}
class SomeViewModel: ViewModel() {
var state by mutableStateOf(UUID.randomUUID().toString())
}
The hierarchy topmost viewModel call creates a view model - in my case inside TestScreen. All children that call viewModel of the same class will get the same object. The exception to this is different destinations of Compose Navigation, see how to handle this case in this answer.
You can update a mutable state property of view model, and it will be reflected on all views using that model. Check out more about state in Compose.
The view model lifecycle is bound to the compose navigation route (if there is one) or to Activity / Fragment otherwise, depending on where setContent was called from.

Best way to enhance PagedList with UI-specific data and not break MVVM pattern

I'm new to MVVM pattern, but I really liked how PagedList simplifies working with paged data in repository.
But now I have the following situation:
I have repository with method like this:
fun getLibraryItems(): LiveData<PagedList<ItemInfo>>
Where ItemInfo is Repository-specific and doesn't 'know' about UI.
Next I want to 'enhance' this data object with UI specific data (map to another ui data object), which I can get only from context-aware components, f.e. it's Drawable resource.
But I cannot do 'map' straight forward from PagedList<X> to another PagedList<Y>, if I want it - I need to update my data source to accept 'mapper' function, like this:
fun <T> getLibraryItems(mapper: (ItemInfo) -> T): LiveData<PagedList<T>> {
return dataSource
.getLibraryItems()
.map(mapper)
.toLiveData()
}
This way I can 'map' ItemInfo to UI-specific type T, but I cannot do it in ViewModel, because loading Drawable resources in ViewModel is (as I understand) anti-pattern.
I don't understand how and where should I call this 'mapper', should it be Activity/Fragment or am I missing smth and over-complicating things.
I think it's possible that you map the data inside the ViewModel as you've suggested, but instead of loading Drawable resource right there you pass its identifier only. And then in your UI layer (RecyclerView Adapter,Activity or whatever) load the resource by its identifier. This way you won't have to deal with context-specific methods inside ViewModel. Also if you don't wan't to deal with android resource id's (e.g. R.drawable.my_image) you'll have to keep your own ID system and then map it to android resource id inside UI layer, though I personally consider this redundant complication.
Edit: After re-reading your question I see that my post didn't fully answer it, so here's a bit more on architecture:
Since you want to map a domain object to UI-specific object, you're right that doing so inside a ViewModel isn't the best practice and not a clean architecture way. So the right way would be to do such mappings in your UI layer just before you display the object. Since you work with PagedList I can assume your RecyclerView adapter extends PagedListAdapter. Then you can create an abstraction for supporting mappings inside an adapter.
abstract class MyPagedAdapter<T, R, VH: RecyclerView.ViewHolder>(
val dataMapper: (T)->R,
diffCallback: DiffUtil.ItemCallback<T>
): PagedListAdapter<T, VH>(diffCallback) {
fun getMappedItem(position: Int) = dataMapper.invoke(getItem(position))
}
class LibraryItemAdapter(
dataMapper: (LibraryItem)->UILibraryItem,
): MyPagedAdapter<LibraryItem, UILibraryItem, ViewHolderType>(dataMapper, /* provide diff callback here */) {
/* implement onCreateViewHolder and getItemViewType here */
override fun onBindViewHolder(holder: ViewHolderType, position: Int) {
val item = getMappedItem(position)
// configure view holder to display selected item
}
}
// Then inside your fragment/activity:
val mapper: (LibraryItem) -> UILibraryItem = { /* use your context-aware components to create an object for UI */ }
recyclerView.adapter = LibraryItemAdapter(mapper)
P.S: I'd also like to highlight that mapping the whole list inside your ViewModel/Repository/elsewhere to another class which holds a Drawable, Bitmap or any other large structure is a bad idea, you could end up flooding memory with Drawables which may be not even displayed at the moment. Get your drawables only when you're going to use them and don't store them in a list. My initial answer and later edit both keep you from doing so
P.P.S.: Don't put too much work into your mapper as it may load the UI thread a lot when user scrolls a large list quickly. Do all your business logic in domain/repository layer.

Comparison between Layout Binding and Manual Binding(with observe) in MVVM architecture of android

Using Android Jetpack components and MVVM architecture, we can get live data updates in a View from a View Model in 2 ways, one is to bind the layout with the live data variable, other way is to observe the variable in code.
To illustrate my question I have taken an example. Suppose there is a view model interface getTimeString() which returns the current time.
a) Layout Data Binding
The view in the layout looks something like this
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
...
app:binder_data_date="#{sampleVM.timeString}"/>
The binding adapter looks something like this
#BindingAdapter("binder_data_date")
public static void binder_data_date(TextView text, String data) {
text.setText(data);
}
b) Manual Data binding (just to give it a name):
In Manual data binding, there is nothing in the layout view with respect to the binder, and I observe the live data using the observe() and update the textview.
FragmentSplashScreenManualBindingBinding fragmentSplashScreenBinding;
SampleViewModel sampleViewModel1 = ViewModelProviders.of(this).get(SampleViewModel.class);
public void onSomeRandomFunc(...) {
....
sampleViewModel1.getTimeString().observe(getViewLifecycleOwner(), data -> {
fragmentSplashScreenBinding.sampleText.setText(data);
});
}
I know the first method is much easier to use and both works.
But is using the second method and the way to access the variable (fragmentSplashScreenBinding.sampleText.setText()) in fragment to update the View correct?
Will the performance get impacted if I use the second method?
Your manual Data binding is not incorrect and doesn't have a significant impact on the performance but you will lose two benefits:
Null pointer exception handling: Layout Data Binding handles null data and you don't need to check null objects to prevent app crash when you want to extract data objects and pass them to views.
Code Reusability: If you want to use your layout in different Activities, with
Layout Data Binding you just need to pass the data variable to the layout. But for Manual Data binding you should copy the same code for each java class to assign variable to views which will make a lot of boilerplate code in complex views.
Moreover, If you are using data binding to replace findViewById() as your second method there is a better way called View Binding which you can read more about it here.
Instead of answering your 2 points in post directly - let me mention few key features of both data binding and Live data - which may eventually help you choose 1 over the other.
Live data class supports Transformations - this useful class provide a way to apply any changes to be done to the live data object before dispatching it to the observers, or you may need to return a different LiveData instance based on the value of another one. Below is a sample example of applying the Transformation on LiveData from Official android docs,
class ScheduleViewModel : ViewModel() {
val userName: LiveData
init {
val result = Repository.userName
userName = Transformations.map(result) { result -> result.value }
} }
As you can see from above example - in the "init" the LiveData Object is "transformed" using Transformations.map before dispatching its content to "observers"
Data binding is mostly works with set of Observables and cannot "transform" the data under observation before dispatching like in above example.
Another useful feature of with LiveData is a class called MediatorLiveData - this subclass which may observe other LiveData objects and react based on changes to it - With data binding AFAIK its very much restricted to a specific Observable Fields.

DataBinding ViewModel vs RxJava 2

So basically up until now I have been using rxjava2 extensively in the applications, but decided to check out data binding, view models and live data. And Im not sure I've got all of this right, because apart from saving state during rotation of device I do not see any other clear benefits of switching, I could even say that I see downsides of introducing data binding with view model between view and rx java powered requests.
Lets see example of some registration form. It would contain:
2 inputs - name and surname
Field with 3 choices
Button with progress
In the reactive world I would have two observables with name and surname, one observable that would merge 3 choices clicks and map them to the right enum, then I could combine all the data together, communicate directly with my single responsible for sending the data in between I would have state with progress or error and tada Im done.
And here is the thing that I came up with using data binding and view models:
class LiveDataViewModel : ViewModel() {
enum class Choice {
NONE, FIRST, SECOND, THIRD
}
private val _progressVisibilityLiveData = MutableLiveData<Boolean>()
private val _errorLiveData = MutableLiveData<GlobalError>()
val progressVisibilityLiveData: LiveData<Boolean> = _progressVisibilityLiveData.apply { value = false }
val errorLiveData: LiveData<GlobalError> = _errorLiveData
val data = LiveDataData()
val observableData = ObservableField(LiveDataData())
fun actionContinue() {
_progressVisibilityLiveData.postValue(true)
if (observableData.get()?.isValid() == false) _errorLiveData.postValue(GlobalError.AllFieldsRequired)
else sendToApi()
}
private fun sendToApi() {
// TODO there would be still an rx java call to single, when we would handle error in the same way we are doing
// it in actionContinue
}
data class LiveDataData(val firstName: ObservableField<String> = ObservableField(""),
val secondName: ObservableField<String> = ObservableField(""),
val choice: ObservableField<Choice> = ObservableField(Choice.NONE)) {
fun changeChoice(newChoice: Choice) {
choice.set(newChoice)
}
fun isValid(): Boolean = !firstName.get().isNullOrEmpty() && !secondName.get().isNullOrEmpty() && choice.get() != Choice.NONE
fun toRequest(): Request = Request(firstName.get()!!, secondName.get()!!, choice.get()!!)
}
}
So I would change fields of my LiveDataData directly from xml using bindData, also I would change state of my selection box depending on this binding too, progress would have to be done manually and then it would trigger the visibility using data binding. But is it really a good way of handling such cases?
The disadvantages I see are that the whole logic in actionContinue would be manually changing values, the values from ObservableProperties could be null, so we either have to handle nullable values everywhere of we have to use !! and to be honest Im not feeling that this is the right direction.
Maybe any of you guys have thought about something similar and could eventually point me if I made some wrong assumptions or if I shouldn't use for example ObservableProperty at all. Obviously there are tons of articles about data binding and live data etc, but I haven't found any that would satisfy my curiosity. Oh and create MutableLiveData for each property from form is not an option.
RxJava is a completely different concept than DataBinding. It's more of a way of handling concurrency than it is about binding data. I 100% think it's worth learning. The Android community has embraced it with open arms.
Shameless plug: I compiled a list of RxJava resources awhile back - http://gregloesch.com/dev/2014/10/20/resources-for-learning-rxjava-android.html

LibGDX architecture design guidelines

I deal with loads of architecture design patterns and guidelines to choose from and to follow as an android client-server app developer.
The most popular ones are:
MVP or MVVM
Clean architecture
Repository pattern
Dependency injection technique
and so on...
Due to the strict rules of patterns, a developer has to admit the fact that patterns are just recommendations themselves, and are not required by Android SDK at all.
Same is true for LibGDX. There are no strict rules or requirements provided by LibGDX library, so the developer is free to decide how to write the game.
So the question is:
Are there some recommendations, design guidelines or even standards for LibGDX game developers to follow? How should I write the code (with usage of LibGDX) in a way that other developer can easily understand?
From my experiences, there is no standard everyone is following. libGDX developers come from different backgrounds. Some are backend developers in their day life, some are just hobbyist devs and learn their first development skills.
I see a lot of libGDX open-sourced projects with typical static SomeManager.getInstance() calls, while I prefer to pass-through references (as a backend developer, you will know about the advantages - testability and so on).
Even the libGDX backend itself does not follow one single approach. There are some parts getting references to managers by reflection (which is not good, because you must exclude such classes from obfusciation) and some using static getInstances().
If you also HTML5, you also must respect some GWT-based restrictions, so you are sometimes forced to go a way you would never do when developing Spring Boot applications.
Now that 3 years passed I can answer this question myself.
I found that using ECS (Entity-Component-System) is the best approach for creating games.
With this approach you'll have 3 different purpose objects. As name suggests they are:
Entity - is just a general purpose object which only contains set of component objects, and usually an ID.
Component - is a bag of data. No logic just plain POJO. The data it contains will define the behaviour of Entity which contains the component.
System - is where you put your logic. Every system should process an entity if and only if it contains a very specific set of components.
Engine - is a representation of a scene where all entities live, and are processed by systems.
The easies way of implementing such approach is by using Ashley lib. It's a lightweight implementation of ECS. Although as my experience has shown this works best in a case of real gameplay. If you want to have very specific animations (even of game objects) then use scene2d.
Scene2d represents a more familiar (to vast majority of developers) approach. It's a graph of UI elements which are placed in a hierarchy. There are very useful Actions which can be used to implement your animations. It also works best for any UI that you want to display in your game.
So to sum it all up I have worked out that you should have a couple of classes per screen to have a distinct division of labour.
Screen - I see it as a branch of running code. It contains an Engine (if we need an ECS) and a Stage (if we need an UI). Handles all game events, user input and routing.
Engine - ecs container which knows how to manage its systems, entities and components.
Stage - UI container which knows what UI elements should be present, how they should look and behave.
Simple example in kotlin (NOTE in my real project I use koin DI to bind all together):
class MyGameScreen : Screen, MyGameEngine.Callback, MyGameStage.Callback {
val engine = MyGameEngine(callback = this)
val stage = MyGameStage(callback = this)
override fun create() {
engine.create()
stage.create()
}
override fun render(delta: Float) {
enigne.update(delta)
stage.act(delta)
stage.draw()
}
override fun dispose() {
enigne.dispose()
stage.dispose()
}
override fun buttonPress() {
//handle button press
}
override fun onGameEvent(event: MyGameEngine.Event) {
//handle some game event
}
}
class MyGameEngine(
private val callback: Callback
) : com.badlogic.ashley.core.PooledEngine() {
fun create() {
// create and add all your systems and entities
}
sealed class Event {
// ...
}
interface Callback {
fun onGameEvent(event: Event)
}
}
class MyGameStage(
private val callback: Callback
) : com.badlogic.gdx.scenes.scene2d.Stage() {
val button1 = TextButton(...).apply {
addClickListener { callback.buttonPress() }
}
val label1 = Label(...)
// and so on
fun create() {
val root = Table().apply {
// add all your actors to the root table
}
val rootContainer = Container(root).apply {
setFillParent(true)
fill()
top()
}
addActor(rootContainer)
}
fun startSomeAnimation() {
val action = Actions.sequence(
// your animation
)
addAction(action)
}
interface Callback {
fun buttonPress()
}
}

Categories

Resources