π Kotlin Workshop:
Github repository |
---|
https://github.com/SwinAkathon/kotlin-workshop |
![]() |
β²οΈ Duration : 4 hours ( 2 hours / module)
π Level: Beginner to Intermediate
The purpose of this workshop is to teach how to develop Android apps in Kotlin that uses the modern features of the language.
The original target audience of the workshop are students participating in Akathon4AIoT at Swinburne Vietnam. More generally, however, the workshop would be suitable for anyone who:
- has a basic knowledge in Kotlin and
- is familiar with how to use Android Studio to develop a Kotlin app.
The workshop includes two modules:
Module | Title | Features | Outcomes |
---|---|---|---|
01 | Frontend Development with Jetpack Compose | Jetpack Compose essentials | Full navigation app |
02 | Processing Large Datasets | Kotlin Flow, LiveData, Paging | Data Processing app |
All materials are stored in this github repository, which consists of several branches. Each branch is a version-level of some basic apps that can be developed using the workshop. A subset of the branches are used in the workshop. Others are available for extra references.
This workshop was prepared using various Kotlin and Jetpack-Compose materials from the following sources:
- Google code labs: https://developer.android.com/codelabs/
- Jetpack Compose developer site: https://developer.android.com/jetpack/compose/.
You can find referenced links to the above sites attached to the key Kotlin components that are used in the workshop.
We use the standard Android studio (AS)
IDE to develop mobile apps in Kotlin.
1.1.1
Read the overview to understand Android Studio
1.1.2
Follow these instructions to install Android Studio on your machine
1.1.3
Understand Android Studio UI
1.1.4
Understand the essential development tasks
The AS version that this workshop uses is: Hedgehog 2023.1.1
1.2.1
In AS, create an Empty Activity
project by following the menu: File/New/New project
and then choose the Empty Activity
type.
Name the project helloworld
and keep the default settings as shown in the image below.
Click the green Run
toolbar button to run the app on the emulator. This deploys the app on the emulator, runs it to show the Hello Android
message as displayed on the screen below:
Android panel
shows the project source code and build structure- The
source code
includes one Kotline file namedMainActivity.kt
Build structure
includes Gradle-specific configuration files.
- The
Editor panel
shows the content ofMainActivity.kt
- class
MainActivity
subclasses a built-in class namedComponentActivity
- overrides
onCreate
to invokesetContent()
with aComposable
of typedHelloworldTheme
- overrides
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
HelloworldTheme {
// A surface container using the 'background' color from the theme
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
Greeting("Android")
}
}
}
}
HelloworldTheme
was automatically generated by the IDE project and is defined in thecom.example.helloworld.ui.theme.Theme.kt
. It sets up the theme settings for the project (which are based on a built-in material design theme) and the root composable as the content.Root composable
is a top-level component that is instantiated from the built-inSurface
.Surface
: a material design surface on which to layout UI components (other composables). It is used to set common settings (e.g. colors) for the contained components.Greeting
is the app-level composable that displays app-specific content. It is defined as a function:
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
Composable Preview
: another composable, namedGreetingPreview
, which is defined inMainActivity.kt
file. It has the additional@Preview
annotation attached to it and the content is set up to call theGreeting
composable. TheRun...
button on the side bar (next to the function header) allows developer to run the composable directly on the emulator. This is useful to enable quick UI preview of any composable of an app.- Note: create one Composable Preview and reuse it to preview different composables in your app. Just list them in the content and comment out the one that you want to preview.
π» Follow these instructions to complete the task.
π» Follow these instructions to complete the task.
π» Follow these instructions to complete the task.
π» Follow these instructions to complete the task.
π» Follow these instructions to complete the task.
π» Follow these instructions to complete the task.
π» Follow these instructions to complete the task.
π» Follow these instructions to complete the task.
Use expandable icon to replace the button.
π» Follow these instructions to complete the task.
- Jetpack Compose basics (with step-by-step guide and sample app)
Jetpack Compose is a modern toolkit designed to simplify UI development. It combines a
reactive programming model
with the conciseness and ease of use of the Kotlin programming language. It isfully declarative
, meaning you describe your UI by calling a series of functions that transform data into aUI hierarchy
. When the underlying data changes, the frameworkautomatically re-executes
these functions, updating the UI hierarchy for you.A Compose app is made up of
composable functions
- just regular functions marked with@Composable
, which can call other composable functions. A function is all you need to create a new UI component. The annotation tellsCompose
to add special support to the function for updating and maintaining your UI over time.Compose
lets you structure your code into small chunks. Composable functions are often referred to as "composables
" for short.By making
small reusable composables
, it's easy to build up a library of UI elements used in your app. Each one is responsible for one part of the screen and can be edited independently.
The app that we build in this lab is called ecoms
as it has the structure of an ecommerce app.
The aim is to design an app that has the essential navigation components, which include top, bottom and drawer menu navigation. The content panel displays different a screen composable based on the user's selection on a navigation item or the user's interaction on a current screen.
Ecoms app structure (view on Google drive): https://drive.google.com/file/d/1LEv3gfwQcp_lJKYjuUDClwKyIYHj9bGR
App
composable represents the root application UI component. It provides a scaffold that holds together the navigation components and the application component screens.
The top-level composables that are referenced by App
include ModalNavigationDrawer
, TopNav
, BottomNav
and Navigation
. All four top-level components are defined in the Navigations.kt
.
ModalNavigationDrawer
'wraps' a DrawerMenu
component over the other three components.
In particular, Navigation
defines a navigation graph, every node of which is an application component screen (composable).
App.kt
navController
: a sharedNavigationController
object that controls the navigation among composablesdrawState
: captures the drawer menu state, which includes access toopen
andclose
functions (invoked when user clicks on the menu) and other propertiesModalNavigationDrawer
: modal-typed drawer menu, which appears on top of other components. It takes a DrawerMenu composable, which creates aModalDrawerSheet
containing the menu items, and aScaffold
that nicely brings together the other three other top-level composables:TopNav
,Navigation
(content screens) andBottomNav
.
package com.example.ecoms.ui
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.Scaffold
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.navigation.compose.rememberNavController
@Composable
fun App() {
// controls the navigation among composables
val navController = rememberNavController()
// drawer menu state (initialised to 'Closed') and is to be updated between Closed and Opened when user clicks on it
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
// a modal-typed drawer menu, which appears on top of other components
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = { DrawerMenu(navController, drawerState) },
) {
Scaffold(
topBar = { TopNav(navController, drawerState) },
content = { Navigation(navController, it) },
bottomBar = { BottomNav(navController) }
)
}
}
AppConfig.kt
package com.example.ecoms
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
class AppConfig {
companion object {
const val appName : String = "Ecoms"
val styleHeading = TextStyle(fontSize = 35.sp)
val styleNormal = TextStyle(fontSize = 14.sp)
val styleTitle = TextStyle(fontSize = 35.sp)
val styleDrawerMenuTitle = TextStyle(fontSize = 25.sp)
val styleBtnText = TextStyle(fontSize = 25.sp)
// colors used by Theme.kt
// COMPOSABLES MUST NOT DIRECTLY USE COLORS HERE!
val Navy = Color(0xFF073042)
val Blue = Color(0xFF4285F4)
val LightBlue = Color(0xFFD7EF12)
val Chartreuse = Color(0xFFEFF7CF)
val paddingContent = 16.dp
}
}
MainActivity.kt
package com.example.ecoms
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import com.example.ecoms.ui.App
import com.example.ecoms.ui.theme.EcomsTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
EcomsTheme {
// A surface container using the 'background' color from the theme
Surface(modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background) {
App()
}
}
}
}
}
Navigations.kt
contains definitions of the four top-level composables referenced by the App
composable.
Navigation
defines a NavHost
, which is a navigation graph consisting in a set of target composables that can be navigated to. Each navigation is performed through a route. The current code defines 5 screen composables: Home
, Products
, Search
, Favourites
, Profile
.
package com.example.ecoms.ui
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.BottomNavigation
import androidx.compose.material.BottomNavigationItem
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Divider
import androidx.compose.material3.DrawerState
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.NavigationDrawerItemDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import com.example.ecoms.AppConfig
import com.example.ecoms.modules.dashboard.view.DashBoard
import com.example.ecoms.modules.favourites.view.FavouritesScreen
import com.example.ecoms.modules.product.view.ProductScreen
import com.example.ecoms.modules.profile.view.ProfileScreen
import com.example.ecoms.modules.search.view.SearchScreen
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@Composable
fun Navigation(navController: NavHostController, paddingValues: PaddingValues) {
NavHost(
navController = navController,
startDestination = "Home",
modifier = Modifier.padding(paddingValues)
) {
composable("Home") {
DashBoard(navController)
}
composable("Products") {
ProductScreen(navController)
}
composable("Search") {
SearchScreen(navController)
}
composable("Favourites") {
FavouritesScreen(navController)
}
composable("Profile") {
ProfileScreen(navController)
}
}
}
/**
* @effects: toggle the drawer menu whose state drawState between Opened and Closed
*/
fun toggleDrawerMenu(drawerState: DrawerState, coroutineScope: CoroutineScope) {
coroutineScope.launch {
drawerState.apply {
if (isClosed) open() else close()
}
}
}
A top navigation bar contains an application name label and an IconButton
that provides access to the collapsable drawer menu.
@Composable
fun TopNav(navController: NavHostController, drawerState: DrawerState) {
val coroutineScope = rememberCoroutineScope()
TopAppBar(
title = {
Text(text = AppConfig.appName,
style = AppConfig.styleTitle
)
},
backgroundColor = MaterialTheme.colorScheme.primaryContainer,
navigationIcon = {
IconButton(onClick = { toggleDrawerMenu(drawerState, coroutineScope) }) {
Icon(
imageVector = Icons.Filled.Menu,
contentDescription = "Menu",
modifier = Modifier.size(100.dp)
)
}
}
)
}
Drawer menu
provides a slide-in top-level menu that provides access to key components of an app.
In this example, we set up the drawer menu with items representing the top-level screens (set up in the navigation graph) and using the navigation controller to navigate to them upon user selection.
Of course, we do not forget, upon navigation, to use function toggleDrawerMenu()
to close the menu.
@Composable
fun DrawerMenu(navController: NavController, drawerState: DrawerState) {
val coroutineScope = rememberCoroutineScope()
val itemColors = NavigationDrawerItemDefaults.colors(
unselectedContainerColor = MaterialTheme.colorScheme.primaryContainer, // Background color when not selected
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer, // Background color when selected
)
val items = arrayOf("Products", "Search", "Favourites", "Profile")
ModalDrawerSheet(drawerContainerColor = MaterialTheme.colorScheme.primaryContainer) {
Text(AppConfig.appName, modifier = Modifier.padding(16.dp),
style = AppConfig.styleDrawerMenuTitle)
Divider()
items.forEach { item ->
NavigationDrawerItem(
label = { Text(text = item) },
selected = false,
colors = itemColors,
onClick = {
navController.navigate(item)
toggleDrawerMenu(drawerState, coroutineScope)
}
)
}
}
}
The bottom navigation provides quick access to between 3 to 5 top-level components of the app.
@Composable
fun BottomNav(navController: NavController) {
val items = mapOf<String, ImageVector>(
"Home" to Icons.Default.Home,
"Search" to Icons.Default.Search,
"Favourites" to Icons.Default.Favorite,
"Profile" to Icons.Default.Person
)
BottomNavigation(backgroundColor = MaterialTheme.colorScheme.primaryContainer) {
items.forEach { (item, image) ->
BottomNavigationItem(
selected = navController.currentDestination?.route == item,
onClick = { navController.navigate(item) },
icon = { Icon(image, contentDescription = item,
modifier = Modifier.size(40.dp)) },
)
}
}
}
Typically, a module consists of three basic subpackages:
model
: the domain classes of the module (e.g.Product
)view
: consists of one or more composables that represent the different view screens through which the user is able to interact with and update themodel
statedao
: consists of data-access objects, responsible for loading and storing domain objects to the underlying data source.
Types | Components | Description |
---|---|---|
model | Product |
a data class describing products in terms of id , name |
view | Composables: ProductScreen , ProductItemsScreen , ProductItem |
hierarchy of composables that define the view components for the products |
dao | ProductSource |
represents a data source that stores data about the product objects |
Product
package com.example.ecoms.modules.product.model
data class Product(val id: Int, val name: String)
ProductScreen
package com.example.ecoms.modules.product.view
import android.content.Context
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.example.ecoms.ui.theme.EcomsTheme
private val LTAG = "ProductScreen"
@Composable
fun ProductScreen(navController: NavController) {
val surfaceHeight = 1f
val context: Context = navController.context
EcomsTheme {
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.border(width = 1.dp, color = MaterialTheme.colorScheme.onPrimary)
.padding(10.dp), contentAlignment = Alignment.TopCenter
) {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier
.fillMaxWidth(surfaceHeight)
.wrapContentHeight()
// ,color = MaterialTheme.colorScheme.primary
) {
Column {
// title
Text("Products",
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
fontSize = TextUnit(32f, TextUnitType.Sp))
// content
ProductItemsScreen(context)
}
}
}
}
}
ProductItemsScreen
package com.example.ecoms.modules.product.view
import android.content.Context
import android.widget.Toast
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.example.ecoms.modules.product.dao.ProductSource
@Composable
fun ProductItemsScreen(context: Context,
maxWidth: Float = 1f,
maxHeight: Float = 1f
) {
val myItems = ProductSource.products
Row(modifier = Modifier.padding(bottom = 5.dp)) {
// use row to add padding to the next item on the same surface
LazyColumn(
modifier = Modifier
.fillMaxWidth(maxWidth)
.fillMaxHeight(maxHeight)
// .border(width=1.dp,color= Color.Red)
,
horizontalAlignment = Alignment.CenterHorizontally
) {
items(myItems.size) { index ->
val product = myItems[index]
ProductItem(product = product, onCheckedChange = { isChecked ->
// todo: Handle product selection
notify(context, product.toString())
})
}
}
}
}
fun notify(context: Context, msg: String) {
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
}
ProductItem
package com.example.ecoms.modules.product.view
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.example.ecoms.modules.product.model.Product
@Composable
fun ProductItem(product: Product, onCheckedChange: (Boolean) -> Unit) {
var isChecked by remember { mutableStateOf(false) }
Row(
modifier = Modifier.fillMaxWidth().padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier.weight(1f).padding(start = 8.dp)
) {
Text(text = "ID: ${product.id}", fontWeight = FontWeight.Bold)
Text(text = "Name: ${product.name}")
}
Checkbox(
checked = isChecked,
onCheckedChange = {
isChecked = it
onCheckedChange(it)
}
)
}
}
ProductSource
package com.example.ecoms.modules.product.dao
import com.example.ecoms.modules.product.model.Product
class ProductSource () {
// customise this for each domain class type
companion object {
val products: List<Product> = readProducts()
private fun readProducts(): List<Product> {
return List(100) {
val id = it+1
Product(id, "Product ${id}")
}
}
}
}
Apply your understanding of the Product module to complete the code of the following modules:
Customer
Order
Dashboard
Favourites
Search
Profile
Extends ecoms
to include a calculator
module. The screen design is as shown in the image below.
Calculator
screen:
The aim is to incrementally read data from a (potentially large) data source, one page at a time.
The design pattern relies on 3 built-in classes:
ViewModel
: to maintain shared data between screens that is safe between configuration changesPagingSource
: to act as the data source adapter that supports data paging. We need to specialisePagingSource
for each type of data source (including structured file, relational database, JSON, XML, etc.)Flow
: to stream the data fromPagingSource
, one page at a time and in an asynchronous manner. Asynchrony is supported by coroutine.
Together, these components enable Screen
s to read and display data incrementally, giving a smooth scrolling action of the data. The LHS of the class diagram depicts a typical screen structure of an app that uses the design pattern. In this, the data items are presented on an ItemsView
composable (e.g. a list view, represented by LazyColumn
). ItemsView
is in turn composed of the ItemView
composable, which is responsible for presenting the view of each individual data item.
The above UML communication diagram depicts the logic flow of the design pattern. It shows a typical run-time execution scenario, in which objects of the classes collaborate with each other (through method invocations, a.k.a exchanging messages) in an orderly fashion. The numbers attached to each message arrow show the sequence of messages.
We briefly explain below the design and code of the module Product
of the ecoms
app, that implements the paging-aware design pattern. The full source code of this module and the app is provided in the branch advanced
of this repository.
Generic classes (reusable for different modules):
common.vmodel.PagingViewModel
: implements thePagingViewModel
in the design patterncommon.dao.PagingFileSource
: implements thePagingSource
for CSV data sourcecommon.dao.ObjectFileProc
: implements a generic string-to-object conversion function, which is used byPagingFileSource
to create objects from each line read from the CSV file.
The ProductScreen
module of the ecoms
app consists of the following components:
view.ProductScreen
composable: implements theScreen
for products.view.ProductItemsScreen
composable: implements theItemsView
for products.view.ProductItem
: implements theItemView
for products.view.ProductViewModel
: derived from the genericPagingViewModel
class forProduct
.model.Product
: implements the data class for products.dao.ProductPagingSource
: derived from a genericPagingFileSource
class forProduct
.
Product
screen:
A brief walk-through of the key code segments, which include the followings:
ProductScreen
: myViewModel initialisation withProductPagingSource
(a subtype ofPagingFileSource
)ProductItemsScreen
:pagingItems
(collected throughviewModel.getFlow().collectAsLazyPagingItems()
)- page-based items view
- visual loading progress indicator
ProductViewModel
(a subtype ofPagingViewModel
): sets up the flow with the paging data source.PagingFileSource
: a subtype ofDataSource
(which in turn is a subtype ofPagingSource
)- function
load()
: page-based loading of items from file - uses an input lambda (
objectFileProc
) to generically convert each line into an object
- function
Both real-time and static data sets are treated as data streams which need to be processed by an app. The main feature of real-time or dynamic data is that the data are streamed into the app while it is being run. This is sometimes referred to as push-based data model, where the data sources are in control of providing (or pushing) the data. This is different from the conventional pull-based data model found in typical business applications, where the application controls what and when the data are pulled in for processing.
Push-based data processing model is common among IoT applications, where the IoT nodes (including sensors and devices) become the data sources, pushing the data in real-time to the apps that are interested in processing them. The data are multicasted to the apps through channels, called topics. The apps subscribe to the topics of interest and receive the data when they become available.
In this lab, we will learn to develop a real-time data application, following the push-based model, that processes data that are received through a popular IoT-based messaging protocol, named MQTT (Message Queuing Telemetry Transport).
The following diagram extends and specialises the design pattern model presented earlier in LAB3 to construct a design model for a push-based real-time app. The net result is a push- and page-based design model that supports loading real-time data one page at a time.
The following screenshots demonstrate the humidity module of the ecoms
, which receives the real-time humidity data from the MQTT service and incrementally displays them on a LazyColumn
.
Initial push- and page-based loading screen with
LazyColumn
Subsequent page-based loading screen
In the lab, the MQTT service and a reference IoT node are already set up for you so you only need to connect the app to the service to receive data.
If you want to set up an MQTT service for your own application then follow the instructions given on this Github repository to:
- install an MQTT service on a docker container
- runs a Python-based IoT node that periodically pushes temperature, humidity, and light data to subcribers (clients) on the MQTT network (via the MQTT service)
Thanks Dr. Dai Van Pham for the Github repository!
Once the MQTT service has been set up, it and the IoT node can be run as follows:
# docker start mqtt5broker
MQTT_TEST# python IoTNode.py
Refer to the advanced
branch of this repository for the source code of ecoms
app that implement the real-time app design model with the Humidity
module. This module processes data received from the MQTT service through the channel named ihome/feeds/humidity
.
In fact, the MQTT service pushes data to three channels, one for each type of sensor reading:
- humidity:
ihome/feeds/humidity
- temperature:
ihome/feeds/temperature
- light:
ihome/feeds/light
The implementation is modular so that you can adapt it for your own module.
A brief walk-through of the key code segments, which include the followings:
HumidityScreen
: similar toProductScreen
but usesHumidityPagingSource
instead ofPagingFileSource
HumidityPagingSource
: a subtype ofPagingMqttSource
to read MQTT data from the humidity channelsPagingMqttSource
: a generic base class for reading Mqtt data from a set of input feeds (channels).- uses
KMqttClient
to connect to MQTT service and save data of each channel to a local CSV file (named after the channel) - uses
PagingDynamicFileSource
to page-based read data from the CSV file
- uses
PagingDynamicFileSource
: a sub-type ofPagingFileSource
that continuously read data from a file one page at a time. It does not stop and continuously wait for new data in the file.KMqttClient
: a wrapper class formMqttClient
provided by the Paho library.
- Adapt the
Humidity
module to implement two modules for presenting the data received from the temperature and light channels. - Apply the master-detail design pattern to create a detail view screen (composable) for each module. When the user clicks on an item, shows the detailed screen listing the item. Training resources:
Adapt the PagingMqttSource
class to implement a PagingMqttRelationalSource
that also reads streaming data from the MQTT service but uses a relational database to store the received data (instead of using a CSV file):
- Create
PagingMqttRelationalSource
in the...common.dao
package - Create a class named
PagingRelationalDataSource
, that is used by functionPagingMqttRelationalSource.saveData()
to store the received data of each channel to a database table (e.g. humidity data is stored in thehumidity
table). Create the table if it does not exist:-
uses the Jetpack's Room API to store the local data into SQLlite datatabase. Training materials:
-
a better alternative is to use an external database (e.g. MySQL) to store the data. For testing, you can run this database on the same host machine running the app.
-