Skip to content

Commit

Permalink
[#18] [#19] Implement skeleton loading animation on Home screen
Browse files Browse the repository at this point in the history
  • Loading branch information
chornerman committed Dec 6, 2022
1 parent 7e8bc87 commit 2b53e22
Show file tree
Hide file tree
Showing 11 changed files with 115 additions and 21 deletions.
1 change: 0 additions & 1 deletion assets/color/colors.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="chinese_black">#15151A</color>
<color name="white_alpha_70">#B3FFFFFF</color>
</resources>
11 changes: 9 additions & 2 deletions lib/page/home/home_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:survey/model/survey_model.dart';
import 'package:survey/page/home/home_state.dart';
import 'package:survey/page/home/home_view_model.dart';
import 'package:survey/page/home/widget/home_header_widget.dart';
import 'package:survey/page/home/widget/home_skeleton_loading_widget.dart';
import 'package:survey/page/home/widget/home_surveys_indicators_widget.dart';
import 'package:survey/page/home/widget/home_surveys_page_view_widget.dart';
import 'package:survey/usecase/get_cached_surveys_use_case.dart';
Expand Down Expand Up @@ -35,15 +36,16 @@ class _HomePageState extends ConsumerState<HomePage> {
@override
void initState() {
super.initState();
ref.read(homeViewModelProvider.notifier).loadSurveysFromCache();
ref.read(homeViewModelProvider.notifier).loadSurveysFromApi();
}

@override
Widget build(BuildContext context) {
final surveys = ref.watch(_surveysStreamProvider).value ?? [];
return ref.watch(homeViewModelProvider).when(
init: () => _buildHomePage(surveys),
loading: () => _buildHomePage(surveys),
init: () => HomeSkeletonLoadingWidget(),
loading: () => _buildHomePage(surveys, shouldShowLoading: true),
cacheLoadingSuccess: () => _buildHomePage(surveys),
apiLoadingSuccess: () =>
_buildHomePage(surveys, shouldEnablePagination: true),
Expand All @@ -58,6 +60,7 @@ class _HomePageState extends ConsumerState<HomePage> {

Widget _buildHomePage(
List<SurveyModel> surveys, {
bool shouldShowLoading = false,
bool shouldEnablePagination = false,
}) {
return Scaffold(
Expand All @@ -83,6 +86,10 @@ class _HomePageState extends ConsumerState<HomePage> {
currentDate:
ref.read(homeViewModelProvider.notifier).getCurrentDate()),
),
if (shouldShowLoading)
const Center(
child: CircularProgressIndicator(color: Colors.white),
)
],
),
);
Expand Down
8 changes: 4 additions & 4 deletions lib/page/home/home_view_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ class HomeViewModel extends StateNotifier<HomeState> {
HomeViewModel(
this._getSurveysUseCase,
this._getCachedSurveysUseCase,
) : super(const HomeState.init()) {
loadSurveysFromCache();
}
) : super(const HomeState.init());

int _surveysPageNumber = 1;

Expand All @@ -30,13 +28,15 @@ class HomeViewModel extends StateNotifier<HomeState> {

void loadSurveysFromCache() async {
final surveys = _getCachedSurveysUseCase.call();
if (surveys.isNotEmpty) {
if (surveys.isNotEmpty && state != HomeState.apiLoadingSuccess()) {
_surveys.add(surveys);
state = const HomeState.cacheLoadingSuccess();
}
}

void loadSurveysFromApi() async {
if (_surveysPageNumber > 1) state = const HomeState.loading();

final result = await _getSurveysUseCase.call(GetSurveysInput(
pageNumber: _surveysPageNumber,
pageSize: _surveysPageSize,
Expand Down
75 changes: 75 additions & 0 deletions lib/page/home/widget/home_skeleton_loading_widget.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
import 'package:survey/resource/dimens.dart';

class HomeSkeletonLoadingWidget extends StatelessWidget {
const HomeSkeletonLoadingWidget({super.key});

@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final dividedScreenWidth = screenWidth / 10;

return Scaffold(
resizeToAvoidBottomInset: false,
body: Padding(
padding: const EdgeInsets.all(Dimens.space20),
child: Shimmer.fromColors(
baseColor: Colors.white12,
highlightColor: Colors.white30,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SafeArea(
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSkeleton(dividedScreenWidth * 3),
const SizedBox(height: Dimens.space14),
_buildSkeleton(dividedScreenWidth * 2.5),
],
),
const Expanded(child: const SizedBox.shrink()),
_buildSkeleton(
Dimens.homeUserAvatarSize,
height: Dimens.homeUserAvatarSize,
borderRadius: Dimens.homeUserAvatarSize / 2,
)
],
),
),
const Expanded(child: const SizedBox.shrink()),
_buildSkeleton(dividedScreenWidth),
const SizedBox(height: Dimens.space16),
_buildSkeleton(dividedScreenWidth * 7),
const SizedBox(height: Dimens.space8),
_buildSkeleton(dividedScreenWidth * 3),
const SizedBox(height: Dimens.space16),
_buildSkeleton(dividedScreenWidth * 8.5),
const SizedBox(height: Dimens.space8),
_buildSkeleton(dividedScreenWidth * 6),
],
),
),
),
);
}

Widget _buildSkeleton(
double width, {
double height = Dimens.homeSkeletonLoadingTextHeight,
double borderRadius = Dimens.homeSkeletonLoadingTextBorderRadius,
}) {
return Container(
width: width,
height: height,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(borderRadius),
color: Colors.white,
),
);
}
}
3 changes: 1 addition & 2 deletions lib/page/home/widget/home_surveys_item_widget.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:survey/gen/assets.gen.dart';
import 'package:survey/gen/colors.gen.dart';
import 'package:survey/model/survey_model.dart';
import 'package:survey/resource/dimens.dart';

Expand Down Expand Up @@ -55,7 +54,7 @@ class HomeSurveysItemWidget extends StatelessWidget {
style: Theme.of(context)
.textTheme
.bodyText1
?.copyWith(color: ColorName.whiteAlpha70),
?.copyWith(color: Colors.white70),
),
),
Padding(
Expand Down
5 changes: 3 additions & 2 deletions lib/page/home/widget/home_surveys_page_view_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ class HomeSurveysPageViewWidget extends StatelessWidget {
itemCount: surveys.length,
controller: _pageController,
itemBuilder: (BuildContext context, int index) {
if (index + 1 == surveys.length) loadMoreSurveys.call();

WidgetsBinding.instance.addPostFrameCallback((_) {
if (index + 1 == surveys.length) loadMoreSurveys.call();
});
return HomeSurveysItemWidget(
survey: surveys[index],
onNextButtonPressed: () => {
Expand Down
2 changes: 1 addition & 1 deletion lib/resource/app_theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class AppTheme {
AppTheme._();

static ThemeData get defaultTheme => ThemeData(
scaffoldBackgroundColor: Colors.black,
scaffoldBackgroundColor: ColorName.chineseBlack,
fontFamily: FontFamily.neuzeit,
textTheme: const TextTheme(
bodyText1: TextStyle(
Expand Down
6 changes: 6 additions & 0 deletions lib/resource/dimens.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ class Dimens {

static const double space4 = 4;
static const double space5 = 5;
static const double space8 = 8;
static const double space12 = 12;
static const double space14 = 14;
static const double space15 = 15;
static const double space16 = 16;
static const double space20 = 20;
static const double space24 = 24;
static const double space110 = 110;
Expand All @@ -19,4 +22,7 @@ class Dimens {

static const double homeSurveysIndicatorsSize = 8;
static const double homeSurveysNextButtonSize = 56;
static const double homeUserAvatarSize = 36;
static const double homeSkeletonLoadingTextHeight = 18;
static const double homeSkeletonLoadingTextBorderRadius = 14;
}
7 changes: 7 additions & 0 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
shimmer:
dependency: "direct main"
description:
name: shimmer
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
sky_engine:
dependency: transitive
description: flutter
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ dependencies:
retrofit: ^3.0.1+1
rxdart: ^0.27.7
shared_preferences: ^2.0.15
shimmer: ^2.0.0

dev_dependencies:
build_runner: ^2.2.1
Expand Down
17 changes: 8 additions & 9 deletions test/page/home/home_view_model_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,34 +53,33 @@ void main() {
});

test(
'When initializing, it fetches cached surveys correctly and returns CacheLoadingSuccess state',
'When loading surveys from cache, it emits cached SurveyModel list and returns CacheLoadingSuccess state',
() {
final surveysStream = homeViewModel.surveys;
final stateStream = homeViewModel.stream;

expect(surveysStream, emitsInOrder([cachedSurveys]));
expect(
providerContainer.read(homeViewModelProvider),
const HomeState.cacheLoadingSuccess(),
);
verify(homeViewModel.loadSurveysFromCache()).called(1);
expect(stateStream, emitsInOrder([HomeState.cacheLoadingSuccess()]));

homeViewModel.loadSurveysFromCache();
});

test(
'When loads surveys from api with Success result, it emits SurveyModel list and returns ApiLoadingSuccess state',
'When loading surveys from api with Success result, it emits SurveyModel list and returns ApiLoadingSuccess state',
() async {
when(mockGetSurveysUseCase.call(any))
.thenAnswer((_) async => Success(surveys));
final surveysStream = homeViewModel.surveys;
final stateStream = homeViewModel.stream;

expect(surveysStream, emitsInOrder([cachedSurveys, surveys]));
expect(surveysStream, emitsInOrder([surveys]));
expect(stateStream, emitsInOrder([HomeState.apiLoadingSuccess()]));

homeViewModel.loadSurveysFromApi();
});

test(
'When loads surveys from api with Failed result, it returns Error state',
'When loading surveys from api with Failed result, it returns Error state',
() {
final exception = UseCaseException(Exception());
when(mockGetSurveysUseCase.call(any))
Expand Down

0 comments on commit 2b53e22

Please sign in to comment.